Some checks failed
CI/CD / test-frontend (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / detect-changes (pull_request) Has been cancelled
Fix broken folder creation (sw.prompt), team member add (envelope unwrap), dropdown overflow (min-width:max-content), welcome auto-disable when surfaces installed, zero default bundled packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
8.0 KiB
JavaScript
179 lines
8.0 KiB
JavaScript
/**
|
|
* Admin > People > Teams
|
|
*/
|
|
const { html } = window;
|
|
const { useState, useEffect, useCallback } = hooks;
|
|
|
|
export default function TeamsSection() {
|
|
const [teams, setTeams] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [detail, setDetail] = useState(null); // team object or null
|
|
const [members, setMembers] = useState([]);
|
|
const [allUsers, setAllUsers] = useState([]);
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [showAddMember, setShowAddMember] = useState(false);
|
|
|
|
const loadTeams = useCallback(async () => {
|
|
try {
|
|
const resp = await sw.api.admin.teams.list();
|
|
// Paginated response: { data: [...], total, page }
|
|
const list = Array.isArray(resp) ? resp : (resp && resp.data ? resp.data : []);
|
|
setTeams(list);
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
finally { setLoading(false); }
|
|
}, []);
|
|
|
|
useEffect(() => { loadTeams(); }, []);
|
|
|
|
async function openDetail(team) {
|
|
setDetail(team);
|
|
try {
|
|
const [m, u] = await Promise.all([
|
|
sw.api.admin.teams.members(team.id),
|
|
sw.api.admin.users.list(),
|
|
]);
|
|
setMembers(m || []);
|
|
const uList = Array.isArray(u) ? u : (u && u.data) || [];
|
|
setAllUsers(uList);
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function createTeam(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
try {
|
|
await sw.api.admin.teams.create({ name: form.name.value.trim(), description: form.desc.value.trim() });
|
|
sw.toast('Team created', 'success');
|
|
setShowCreate(false);
|
|
loadTeams();
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function deleteTeam(id) {
|
|
const ok = await sw.confirm('Delete this team? All members will be removed.', true);
|
|
if (!ok) return;
|
|
try {
|
|
await sw.api.admin.teams.del(id);
|
|
sw.toast('Team deleted', 'success');
|
|
setDetail(null);
|
|
loadTeams();
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function addMember(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
try {
|
|
await sw.api.admin.teams.addMember(detail.id, form.userId.value, form.role.value);
|
|
sw.toast('Member added', 'success');
|
|
setShowAddMember(false);
|
|
const m = await sw.api.admin.teams.members(detail.id);
|
|
setMembers(m || []);
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function removeMember(mid) {
|
|
try {
|
|
await sw.api.admin.teams.removeMember(detail.id, mid);
|
|
sw.toast('Member removed', 'success');
|
|
setMembers(prev => prev.filter(m => m.id !== mid));
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
async function updateMemberRole(mid, role) {
|
|
try {
|
|
await sw.api.admin.teams.updateMember(detail.id, mid, role);
|
|
sw.toast('Role updated', 'success');
|
|
setMembers(prev => prev.map(m => m.id === mid ? { ...m, role } : m));
|
|
} catch (e) { sw.toast(e.message, 'error'); }
|
|
}
|
|
|
|
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
|
|
|
|
// Detail view
|
|
if (detail) return html`
|
|
<div>
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
<button class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${() => setDetail(null)}>\u2190 Back</button>
|
|
<h4 style="margin:0;">${detail.name}</h4>
|
|
<div style="flex:1"></div>
|
|
<button class="sw-btn sw-btn--danger sw-btn--sm" onClick=${() => deleteTeam(detail.id)}>Delete Team</button>
|
|
</div>
|
|
|
|
${detail.description && html`<p class="text-muted" style="font-size:12px;margin-bottom:12px;">${detail.description}</p>`}
|
|
|
|
<h5 style="margin:16px 0 8px;">Members (${members.length})</h5>
|
|
<div class="admin-list">
|
|
${members.map(m => html`
|
|
<div class="admin-surface-row" key=${m.id}>
|
|
<span style="flex:1;">${m.display_name || m.username || 'Unknown'}</span>
|
|
<select value=${m.role} onChange=${e => updateMemberRole(m.id, e.target.value)}
|
|
style="font-size:11px;padding:2px 4px;">
|
|
<option value="member">member</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
<button class="sw-btn sw-btn--danger sw-btn--sm" onClick=${() => removeMember(m.id)}>Remove</button>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
|
|
${showAddMember
|
|
? html`
|
|
<form class="admin-inline-form" style="margin-top:8px;" onSubmit=${addMember}>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>User</label>
|
|
<select name="userId">
|
|
<option value="">Select user\u2026</option>
|
|
${allUsers.map(u => html`<option key=${u.id} value=${u.id}>${u.display_name || u.username}</option>`)}
|
|
</select>
|
|
</div>
|
|
<div class="form-group"><label>Role</label>
|
|
<select name="role"><option value="member">Member</option><option value="admin">Admin</option></select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row" style="margin-top:8px;gap:8px;">
|
|
<button type="submit" class="sw-btn sw-btn--primary sw-btn--sm">Add</button>
|
|
<button type="button" class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${() => setShowAddMember(false)}>Cancel</button>
|
|
</div>
|
|
</form>
|
|
`
|
|
: html`<button class="sw-btn sw-btn--secondary sw-btn--sm" style="margin-top:8px;" onClick=${() => setShowAddMember(true)}>+ Add Member</button>`
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
// List view
|
|
return html`
|
|
<div>
|
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
|
|
<div style="flex:1"></div>
|
|
<button class="sw-btn sw-btn--primary sw-btn--md" onClick=${() => setShowCreate(true)}>+ Add</button>
|
|
</div>
|
|
|
|
${showCreate && html`
|
|
<form class="admin-inline-form" style="margin-bottom:16px;" onSubmit=${createTeam}>
|
|
<div class="form-row">
|
|
<div class="form-group"><label>Name</label><input name="name" placeholder="Team name" /></div>
|
|
<div class="form-group"><label>Description</label><input name="desc" placeholder="Optional" /></div>
|
|
</div>
|
|
<div class="form-row" style="margin-top:8px;gap:8px;">
|
|
<button type="submit" class="sw-btn sw-btn--primary sw-btn--sm">Create</button>
|
|
<button type="button" class="sw-btn sw-btn--secondary sw-btn--sm" onClick=${() => setShowCreate(false)}>Cancel</button>
|
|
</div>
|
|
</form>
|
|
`}
|
|
|
|
<div class="admin-list">
|
|
${teams.length === 0 && html`<div class="empty-hint">No teams</div>`}
|
|
${teams.map(t => html`
|
|
<div class="admin-surface-row" key=${t.id} style="cursor:pointer;" onClick=${() => openDetail(t)}>
|
|
<strong style="flex:1;">${t.name}</strong>
|
|
<span class="text-muted" style="font-size:12px;">${t.member_count ?? '?'} members</span>
|
|
<span class="badge ${t.is_active ? 'badge-active' : 'badge-inactive'}">${t.is_active ? 'active' : 'inactive'}</span>
|
|
</div>
|
|
`)}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|