web: bulk-select accounts in the admin table and link the studio from the top bar
The users table grows a checkbox column with a select-all box in its head, and three controls above it: SELECT ALL, SELECT NONE and DELETE SELECTED naming the count. An admin account is the API's own privilege source, so it gets no box and select-all skips it; the picks clear once the deletes land. The landing top bar gains a RecipesCam web studio button between the account slot and Download RecipesCam. It hides with the rest of the wide row under 1160px, where the burger sheet already offers the studio.
This commit is contained in:
@@ -42,6 +42,7 @@ export function Admin() {
|
||||
const [owner, setOwner] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [note, setNote] = useState<string | null>(null);
|
||||
const [picked, setPicked] = useState<number[]>([]);
|
||||
const filePick = useRef<HTMLInputElement>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -132,6 +133,24 @@ export function Admin() {
|
||||
});
|
||||
};
|
||||
|
||||
// The users table's bulk actions. An admin account is the API's own privilege
|
||||
// source, so it is never a moderation target: select-all skips it and a stray
|
||||
// id is filtered out before the deletes run.
|
||||
const selectable = users.filter((u) => !u.admin).map((u) => u.id);
|
||||
const allPicked = selectable.length > 0 && selectable.every((id) => picked.includes(id));
|
||||
const togglePicked = (id: number) =>
|
||||
setPicked((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
||||
const deleteSelected = () => {
|
||||
const ids = picked.filter((id) => selectable.includes(id));
|
||||
if (ids.length === 0) return;
|
||||
if (!window.confirm(t('adm.userDeleteSelectedConfirm', { n: ids.length }))) return;
|
||||
return run(async () => {
|
||||
for (const id of ids) await api.adminDeleteUser(id);
|
||||
setPicked([]);
|
||||
await refreshPhotos();
|
||||
});
|
||||
};
|
||||
|
||||
const shown = owner ? photos.filter((p) => p.email === owner) : photos;
|
||||
|
||||
return (
|
||||
@@ -266,10 +285,50 @@ export function Admin() {
|
||||
|
||||
{tab === 'users' ? (
|
||||
users.length === 0 ? <p className="hint adm-note">{t('adm.noUsers')}</p> : (
|
||||
<div className="adm-table-wrap">
|
||||
<>
|
||||
<div className="adm-bulk">
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
data-key="adm-users-all"
|
||||
disabled={busy || selectable.length === 0}
|
||||
onClick={() => setPicked(selectable)}
|
||||
>
|
||||
{t('adm.selectAll')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
data-key="adm-users-none"
|
||||
disabled={busy || picked.length === 0}
|
||||
onClick={() => setPicked([])}
|
||||
>
|
||||
{t('adm.selectNone')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn adm-danger"
|
||||
data-key="adm-users-delete"
|
||||
disabled={busy || picked.length === 0}
|
||||
onClick={() => void deleteSelected()}
|
||||
>
|
||||
{t('adm.deleteSelected', { n: picked.length })}
|
||||
</button>
|
||||
</div>
|
||||
<div className="adm-table-wrap">
|
||||
<table className="adm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="adm-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
data-key="adm-user-select-all"
|
||||
aria-label={t('adm.selectAll')}
|
||||
checked={allPicked}
|
||||
disabled={busy || selectable.length === 0}
|
||||
onChange={() => setPicked(allPicked ? [] : selectable)}
|
||||
/>
|
||||
</th>
|
||||
<th>{t('adm.userPicture')}</th>
|
||||
<th>{t('adm.userName')}</th>
|
||||
<th>{t('adm.userEmail')}</th>
|
||||
@@ -282,6 +341,18 @@ export function Admin() {
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id} data-key={`adm-user-${u.id}`}>
|
||||
<td className="adm-check">
|
||||
{u.admin ? null : (
|
||||
<input
|
||||
type="checkbox"
|
||||
data-key={`adm-user-check-${u.id}`}
|
||||
aria-label={u.email}
|
||||
checked={picked.includes(u.id)}
|
||||
disabled={busy}
|
||||
onChange={() => togglePicked(u.id)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{/* The account's own picture, straight from the
|
||||
public avatar route — nothing to fetch. */}
|
||||
@@ -352,7 +423,8 @@ export function Admin() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user