feat(web): pop-out modal forms for setup/subscriptions/roles
Add a reusable ui/Modal (Radix Dialog + terminal chrome) and move the add/edit forms in the Devices setup, Subscriptions and Roles screens into it, leaving each list in the page behind the modal. The Devices wizard's per- category device form is also fully translated (setup.* i18n keys). Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
type Permission,
|
||||
type SessionUser,
|
||||
} from "./api.js";
|
||||
import { Modal } from "./ui/Modal.js";
|
||||
|
||||
// Role management (admin). Compose a role from the permission grid (a checkbox
|
||||
// matrix of resource × action) and name it; users are then assigned a role. The
|
||||
@@ -56,8 +57,7 @@ export function RolesManager({ user }: { user: SessionUser | null }) {
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h1 className="text-sm font-bold uppercase tracking-widest text-term-amber">{t("roles.title")}</h1>
|
||||
{canCreate && (
|
||||
<button type="button" onClick={() => { setEditing("new"); setError(null); }}
|
||||
className="rounded-term border border-term-green bg-term-green/10 px-3 py-1 text-[12px] uppercase tracking-wider text-term-green">
|
||||
<button type="button" className="btn btn-go btn-sm" onClick={() => { setEditing("new"); setError(null); }}>
|
||||
{t("roles.add")}
|
||||
</button>
|
||||
)}
|
||||
@@ -65,21 +65,28 @@ export function RolesManager({ user }: { user: SessionUser | null }) {
|
||||
|
||||
{error && <div className="mb-2 rounded-term border border-term-red px-3 py-2 text-[12px] text-term-red">{error}</div>}
|
||||
|
||||
{editing && (
|
||||
<RoleEditor
|
||||
role={editing === "new" ? null : editing}
|
||||
grouped={grouped}
|
||||
onCancel={() => setEditing(null)}
|
||||
onSubmit={async (v) => {
|
||||
try {
|
||||
if (editing === "new") await createRole(v);
|
||||
else await updateRole(editing.id, v);
|
||||
setEditing(null);
|
||||
invalidate();
|
||||
} catch (e) { onError(e); }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Modal
|
||||
open={editing != null}
|
||||
onClose={() => setEditing(null)}
|
||||
title={editing && editing !== "new" ? t("roles.editTitle") : t("roles.new")}
|
||||
width="max-w-2xl"
|
||||
>
|
||||
{editing && (
|
||||
<RoleEditor
|
||||
role={editing === "new" ? null : editing}
|
||||
grouped={grouped}
|
||||
onCancel={() => setEditing(null)}
|
||||
onSubmit={async (v) => {
|
||||
try {
|
||||
if (editing === "new") await createRole(v);
|
||||
else await updateRole(editing.id, v);
|
||||
setEditing(null);
|
||||
invalidate();
|
||||
} catch (e) { onError(e); }
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{roles.map((r) => (
|
||||
@@ -98,13 +105,11 @@ export function RolesManager({ user }: { user: SessionUser | null }) {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{canUpdate && !r.builtin && (
|
||||
<button type="button" onClick={() => { setEditing(r); setError(null); }}
|
||||
className="text-[11px] uppercase tracking-wider text-term-muted hover:text-term-text">{t("roles.edit")}</button>
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={() => { setEditing(r); setError(null); }}>{t("roles.edit")}</button>
|
||||
)}
|
||||
{canDelete && !r.builtin && (
|
||||
<button type="button"
|
||||
onClick={() => { if (confirm(t("roles.confirmDelete", { name: r.name }))) deleteRoleSafe(r.id, invalidate, onError); }}
|
||||
className="text-[11px] uppercase tracking-wider text-term-red hover:text-term-text">{t("roles.delete")}</button>
|
||||
<button type="button" className="btn btn-danger btn-sm"
|
||||
onClick={() => { if (confirm(t("roles.confirmDelete", { name: r.name }))) deleteRoleSafe(r.id, invalidate, onError); }}>{t("roles.delete")}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,17 +145,13 @@ function RoleEditor({
|
||||
const valid = name.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="mb-3 rounded-term border border-term-border bg-term-panel p-3">
|
||||
<div className="mb-2 text-[12px] font-semibold uppercase tracking-wider text-term-amber">
|
||||
{role ? t("roles.editTitle") : t("roles.new")}
|
||||
<div>
|
||||
<div className="field mb-3 w-64">
|
||||
<span className="label">{t("roles.name")}</span>
|
||||
<input className="input" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<label className="mb-3 block text-[11px] text-term-muted">
|
||||
{t("roles.name")}
|
||||
<input value={name} onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1 w-64 rounded-term border border-term-border bg-term-panel-2 px-2 py-1 text-[12px] text-term-text" />
|
||||
</label>
|
||||
|
||||
<div className="text-[11px] uppercase tracking-wider text-term-muted">{t("roles.permissions")}</div>
|
||||
<div className="label">{t("roles.permissions")}</div>
|
||||
<div className="mt-1 grid grid-cols-1 gap-1">
|
||||
{Object.entries(grouped).map(([resource, list]) => (
|
||||
<div key={resource} className="flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-term-border py-1.5">
|
||||
@@ -159,7 +160,7 @@ function RoleEditor({
|
||||
const action = p.split(":")[1]!;
|
||||
return (
|
||||
<label key={p} className="flex items-center gap-1 text-[12px] text-term-text">
|
||||
<input type="checkbox" checked={perms.has(p)} onChange={() => toggle(p)} />
|
||||
<input type="checkbox" className="accent-term-amber" checked={perms.has(p)} onChange={() => toggle(p)} />
|
||||
{action}
|
||||
</label>
|
||||
);
|
||||
@@ -169,10 +170,8 @@ function RoleEditor({
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<button type="button" onClick={onCancel}
|
||||
className="rounded-term border border-term-border px-3 py-1 text-[12px] uppercase tracking-wider text-term-muted">{t("common.cancel")}</button>
|
||||
<button type="button" disabled={!valid} onClick={() => onSubmit({ name: name.trim(), permissions: [...perms] })}
|
||||
className="rounded-term border border-term-green bg-term-green/10 px-3 py-1 text-[12px] uppercase tracking-wider text-term-green disabled:opacity-40">{t("common.save")}</button>
|
||||
<button type="button" className="btn btn-sm" onClick={onCancel}>{t("common.cancel")}</button>
|
||||
<button type="button" className="btn btn-primary btn-sm" disabled={!valid} onClick={() => onSubmit({ name: name.trim(), permissions: [...perms] })}>{t("common.save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user