Ablage je Einheit, Stufe 2: Verwaltung und Organisationsseite (#684)
- OrgAdministration: eine neue Einheit bekommt ihren Bereich (Mitglieder: die Einheit samt Untereinheiten, schreiben); reservierte Namen und belegte Bereichs-Ids werden als Einheiten-Id abgelehnt. Uebergreifende Bereiche anlegen (mit Eigentuemer), Name/Zweck/geschuetzte Ordner aendern, Mitgliederliste ersetzen, nur leere Bereiche loeschen, nie den eigenen einer Einheit. Eine geloeschte Einheit verlaesst alle Mitgliederlisten; ihr Bereich geht nur, wenn er leer ist, sonst bleibt er ohne Eigentuemer. Nachruesten des eigenen Bereichs fuer Einheiten von vor #684. Alles im AuditLog. - API unter /api/organisation/workspaces; die Uebersicht liefert je Bereich Mitglieder, Dateizahl und - von der Instanz berechnet - wer hineinkommt. - Organisationsseite: Bereiche unter der Struktur, Bearbeitung mit Mitglieder-Editor, "Wer hineinkommt", Bereichskarte an der Einheit, Bereiche am Agenten. Sprachschluessel en/de. - Die Nachschau nach einem Absturz bekommt die Organisation mit, sonst fände sie einen Aufruf in einem benannten Bereich nicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ff20ce2563
commit
609298dd7a
@@ -2298,6 +2298,25 @@ button.org-einheit .org-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* #684: workspaces below the structure */
|
||||
.org-bereiche {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--divider);
|
||||
}
|
||||
|
||||
.org-bereiche > p {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.org-mitglieder select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.org-mitglieder td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.org-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -127,6 +127,43 @@
|
||||
"org.toolsLostIntro": "Der neue Platz hat eine Rechte-Obergrenze. Diese Agenten verlören Werkzeuge:",
|
||||
"org.toolsLostConfirm": "Werkzeuge entfernen und übernehmen",
|
||||
"org.runningRefusal": "Nicht, solange {agents} läuft — ein Agent darf nicht mitten im Lauf andere Rechte bekommen.",
|
||||
"org.workspaces": "Arbeitsbereiche",
|
||||
"org.workspacesHint": "Gemeinsame Ordner mit Mitgliederliste. Wer nicht auf der Liste steht, sieht den Bereich nicht.",
|
||||
"org.noWorkspaces": "Noch keine Arbeitsbereiche — eine Einheit bekommt ihren beim Anlegen.",
|
||||
"org.newWorkspace": "Neuer Arbeitsbereich",
|
||||
"org.newWorkspaceHint": "Für etwas, das mehrere Einheiten bewusst teilen, etwa einen Artikelstamm. Die Eigentümer-Einheit verwaltet ihn; Leser kommen danach dazu.",
|
||||
"org.workspaceId": "Id (Ordnername)",
|
||||
"org.workspaceName": "Name",
|
||||
"org.purpose": "Zweck",
|
||||
"org.owner": "Eigentümer",
|
||||
"org.noOwner": "— Instanz (Mensch) —",
|
||||
"org.fileCount": "{n} Dateien",
|
||||
"org.unitOwnWorkspace": "eigener Bereich der Einheit",
|
||||
"org.unitWorkspace": "Arbeitsbereich",
|
||||
"org.unitWorkspaceMissing": "Diese Einheit hat noch keinen eigenen Arbeitsbereich.",
|
||||
"org.createUnitWorkspace": "Arbeitsbereich anlegen",
|
||||
"org.editWorkspace": "Arbeitsbereich bearbeiten",
|
||||
"org.protectedPaths": "Geschützte Ordner",
|
||||
"org.protectedHint": "Durch Komma getrennt. Dort können Dateien hinzukommen, ändern oder löschen dürfen sie nur Verwalter des Bereichs.",
|
||||
"org.memberEntries": "Mitgliederliste",
|
||||
"org.membersHint": "Das Recht steht am Bereich. Ein Einheitseintrag gilt auch für die Einheiten darunter, wenn nicht abgeschaltet. Löschen braucht Verwalten.",
|
||||
"org.noWorkspaceMembers": "niemand",
|
||||
"org.memberKind": "Art",
|
||||
"org.memberWho": "Wer",
|
||||
"org.memberUnit": "Einheit",
|
||||
"org.memberAgent": "Agent",
|
||||
"org.accessLevel": "Stufe",
|
||||
"org.access.Read": "lesen",
|
||||
"org.access.Write": "schreiben",
|
||||
"org.access.Manage": "verwalten",
|
||||
"org.includeSubUnits": "inkl. Untereinheiten",
|
||||
"org.addMember": "Mitglied hinzufügen",
|
||||
"org.removeMember": "Mitglied entfernen",
|
||||
"org.whoGetsIn": "Wer hineinkommt",
|
||||
"org.nobodyHasAccess": "Niemand — nur ein Mensch kommt heran.",
|
||||
"org.deleteWorkspace": "Arbeitsbereich löschen",
|
||||
"org.deleteWorkspaceHint": "Nur ein leerer Bereich lässt sich löschen. Abgelegtes wird von hier aus nie gelöscht.",
|
||||
"org.unitWorkspaceStays": "Der eigene Bereich einer Einheit geht mit der Einheit. Hält er dann noch Dateien, bleibt er ohne Eigentümer.",
|
||||
"team.title": "Teamübersicht",
|
||||
"team.subtitle": "Jede Einheit ist eine Karte, verbunden wie die Organisation. Eine Linie trägt die Farbe des dringlichsten Zustands darunter, eine rote Linie führt also zum Problem. Klick auf Karte oder Agent zeigt Einzelheiten.",
|
||||
"team.noInstance": "Keine Instanz ausgewählt. Oben eine aktive Instanz wählen.",
|
||||
|
||||
@@ -127,6 +127,43 @@
|
||||
"org.toolsLostIntro": "The new position has a tool ceiling. These agents would lose tools:",
|
||||
"org.toolsLostConfirm": "Remove the tools and apply",
|
||||
"org.runningRefusal": "Not while {agents} is running — an agent must not get different rights in the middle of a run.",
|
||||
"org.workspaces": "Workspaces",
|
||||
"org.workspacesHint": "Shared folders with a member list. Whoever is not on the list does not see the workspace.",
|
||||
"org.noWorkspaces": "No workspaces yet — a unit gets one when it is created.",
|
||||
"org.newWorkspace": "New workspace",
|
||||
"org.newWorkspaceHint": "For something several units share on purpose, like an article master. The owning unit manages it; add readers afterwards.",
|
||||
"org.workspaceId": "Id (folder name)",
|
||||
"org.workspaceName": "Name",
|
||||
"org.purpose": "Purpose",
|
||||
"org.owner": "Owner",
|
||||
"org.noOwner": "— instance (human) —",
|
||||
"org.fileCount": "{n} files",
|
||||
"org.unitOwnWorkspace": "the unit's own workspace",
|
||||
"org.unitWorkspace": "Workspace",
|
||||
"org.unitWorkspaceMissing": "This unit has no workspace of its own yet.",
|
||||
"org.createUnitWorkspace": "Create its workspace",
|
||||
"org.editWorkspace": "Edit workspace",
|
||||
"org.protectedPaths": "Protected folders",
|
||||
"org.protectedHint": "Comma separated. Files there can be added, but only members who manage the workspace change or delete them.",
|
||||
"org.memberEntries": "Member list",
|
||||
"org.membersHint": "The right stands at the workspace. A unit entry includes the units below it unless switched off. Deleting files needs Manage.",
|
||||
"org.noWorkspaceMembers": "nobody",
|
||||
"org.memberKind": "Kind",
|
||||
"org.memberWho": "Who",
|
||||
"org.memberUnit": "Unit",
|
||||
"org.memberAgent": "Agent",
|
||||
"org.accessLevel": "Level",
|
||||
"org.access.Read": "read",
|
||||
"org.access.Write": "write",
|
||||
"org.access.Manage": "manage",
|
||||
"org.includeSubUnits": "incl. sub-units",
|
||||
"org.addMember": "Add member",
|
||||
"org.removeMember": "Remove member",
|
||||
"org.whoGetsIn": "Who gets in",
|
||||
"org.nobodyHasAccess": "Nobody — only a human reaches it.",
|
||||
"org.deleteWorkspace": "Delete workspace",
|
||||
"org.deleteWorkspaceHint": "Only an empty workspace can be deleted. Nothing filed is deleted from here.",
|
||||
"org.unitWorkspaceStays": "A unit's own workspace goes with its unit. If it still holds files then, it stays without owner.",
|
||||
"team.title": "Team overview",
|
||||
"team.subtitle": "Every unit is a card, connected as the organisation. A line takes the colour of the most urgent state below it, so a red line leads to the problem. Click a card or an agent for details.",
|
||||
"team.noInstance": "No instance selected. Choose an active instance in the header.",
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
//
|
||||
// A list, not a graph: this is where the structure is edited. Watching it work is the team
|
||||
// overview (#711).
|
||||
//
|
||||
// #684: Workspaces sit beside the units. A unit gets its own; a workspace across units is
|
||||
// made on purpose, with an owner. The right stands at the workspace — its member list — and
|
||||
// the page shows, computed by the instance, who that list actually lets in.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { erstelleElement, erstelleKarte, erstellePlakette, erstelleModal, zeigeMeldung, formatiereDatumZeit } from "../modules/dom.js";
|
||||
@@ -13,9 +17,10 @@ import { t } from "../modules/i18n.js";
|
||||
|
||||
const SETTINGS = ["DailyCostUsd", "DailyTokens", "Model", "WhenExhausted"];
|
||||
const NUMERIC = new Set(["DailyCostUsd", "DailyTokens"]);
|
||||
const ACCESS = ["Read", "Write", "Manage"];
|
||||
|
||||
let data = null;
|
||||
let selection = null; // { kind: "unit" | "agent", id }
|
||||
let selection = null; // { kind: "unit" | "agent" | "workspace", id }
|
||||
let treeBox = null;
|
||||
let detailBox = null;
|
||||
let reasonInput = null;
|
||||
@@ -57,11 +62,35 @@ async function load() {
|
||||
|
||||
function find(sel) {
|
||||
if (!sel || !data) return null;
|
||||
if (sel.kind === "workspace") return data.workspaces.find((w) => w.id === sel.id);
|
||||
return sel.kind === "unit"
|
||||
? data.units.find((u) => u.id === sel.id)
|
||||
: data.agents.find((a) => a.agentId === sel.id);
|
||||
}
|
||||
|
||||
/** A unit's own workspace, if it has one. */
|
||||
function ownWorkspace(unitId) {
|
||||
return data.workspaces.find((w) => w.isUnitOwn && w.ownerUnitId === unitId);
|
||||
}
|
||||
|
||||
function unitPath(unitId) {
|
||||
return data.units.find((u) => u.id === unitId)?.path ?? unitId;
|
||||
}
|
||||
|
||||
function accessLabel(access) {
|
||||
return t(`org.access.${access}`);
|
||||
}
|
||||
|
||||
/** "Purchasing (write, incl. sub-units); ads (read)". */
|
||||
function membersSummary(workspace) {
|
||||
if (!workspace.members.length) return t("org.noWorkspaceMembers");
|
||||
return workspace.members.map((m) => {
|
||||
const who = m.unitId ? unitPath(m.unitId) : m.agentId;
|
||||
const sub = m.unitId && m.includeSubUnits ? `, ${t("org.includeSubUnits")}` : "";
|
||||
return `${who} (${accessLabel(m.access)}${sub})`;
|
||||
}).join("; ");
|
||||
}
|
||||
|
||||
function childrenOf(unitId) {
|
||||
return data.units.filter((u) => (u.parentId ?? null) === unitId);
|
||||
}
|
||||
@@ -180,7 +209,28 @@ function renderTree() {
|
||||
treeBox.replaceChildren(
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal org-baum-kopf" },
|
||||
erstelleElement("h2", { text: t("org.structure") }), newUnit),
|
||||
list);
|
||||
list,
|
||||
workspaceList());
|
||||
}
|
||||
|
||||
function workspaceList() {
|
||||
const rows = data.workspaces.map((w) => erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: `org-knoten org-bereich ${selection?.kind === "workspace" && selection.id === w.id ? "aktiv" : ""}`,
|
||||
onclick: () => select({ kind: "workspace", id: w.id })
|
||||
},
|
||||
erstelleElement("span", { klasse: "org-name", text: `▤ ${w.name}` }),
|
||||
erstelleElement("span", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `${w.ownerUnitId ? unitPath(w.ownerUnitId) : t("org.noOwner")} · ${t("org.fileCount", { n: w.fileCount })}`
|
||||
})));
|
||||
|
||||
return erstelleElement("div", { klasse: "org-bereiche" },
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal org-baum-kopf" },
|
||||
erstelleElement("h2", { text: t("org.workspaces") }),
|
||||
erstelleElement("button", { type: "button", klasse: "klein", text: t("org.newWorkspace"), onclick: () => openCreateWorkspace() })),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.workspacesHint") }),
|
||||
...(rows.length ? rows : [erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.noWorkspaces") })]));
|
||||
}
|
||||
|
||||
function unitBlock(unit, depth) {
|
||||
@@ -261,7 +311,10 @@ function renderDetail() {
|
||||
return;
|
||||
}
|
||||
|
||||
detailBox.replaceChildren(reasonCard, ...(selection.kind === "unit" ? unitPanel(item) : agentPanel(item)));
|
||||
const panels = selection.kind === "unit" ? unitPanel(item)
|
||||
: selection.kind === "workspace" ? workspacePanel(item)
|
||||
: agentPanel(item);
|
||||
detailBox.replaceChildren(reasonCard, ...panels);
|
||||
}
|
||||
|
||||
function unitPanel(unit) {
|
||||
@@ -311,6 +364,7 @@ function unitPanel(unit) {
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.moveUnitHint") })
|
||||
]));
|
||||
|
||||
cards.push(unitWorkspaceCard(unit));
|
||||
cards.push(ceilingCard(unit));
|
||||
cards.push(settingsCard(unit));
|
||||
|
||||
@@ -456,7 +510,8 @@ function agentPanel(agent) {
|
||||
erstelleElement("p", { text: `${t("org.unit")}: ${unit ? unit.path : t("org.topLevel")}${agent.isLead ? ` (★ ${t("org.lead")})` : ""}` }),
|
||||
erstelleElement("p", { text: `${t("org.answersTo")}: ${agent.effectiveLeadId ?? t("org.human")}` }),
|
||||
agent.unitSinceUtc ? erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.since", { time: formatiereDatumZeit(agent.unitSinceUtc) }) }) : null,
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("org.tools")}: ${agent.tools.join(", ") || "—"}` })
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("org.tools")}: ${agent.tools.join(", ") || "—"}` }),
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("org.workspaces")}: ${agentWorkspaces(agent) || "—"}` })
|
||||
]),
|
||||
erstelleKarte(t("org.moveAgent"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.moveAgentHint") }),
|
||||
@@ -469,3 +524,207 @@ function agentPanel(agent) {
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
// ─── workspaces (#684) ───
|
||||
|
||||
function agentWorkspaces(agent) {
|
||||
return data.workspaces
|
||||
.map((w) => ({ w, grant: w.access.find((a) => a.agentId === agent.agentId) }))
|
||||
.filter((x) => x.grant)
|
||||
.map((x) => `${x.w.id} (${accessLabel(x.grant.access)})`)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function unitWorkspaceCard(unit) {
|
||||
const own = ownWorkspace(unit.id);
|
||||
if (!own) {
|
||||
return erstelleKarte(t("org.unitWorkspace"), [
|
||||
erstelleElement("p", { klasse: "text-klein", text: t("org.unitWorkspaceMissing") }),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "klein", text: t("org.createUnitWorkspace"),
|
||||
onclick: () => send(`/organisation/units/${encodeURIComponent(unit.id)}/workspace`, "POST", { reason: reason() })
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
return erstelleKarte(t("org.unitWorkspace"), [
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${own.id} · ${t("org.fileCount", { n: own.fileCount })}` }),
|
||||
erstelleElement("p", { klasse: "text-klein", text: `${t("org.memberEntries")}: ${membersSummary(own)}` }),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: t("org.editWorkspace"),
|
||||
onclick: () => select({ kind: "workspace", id: own.id })
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
function openCreateWorkspace() {
|
||||
const id = erstelleElement("input", { type: "text", placeholder: "articles", required: true });
|
||||
const name = erstelleElement("input", { type: "text", placeholder: "Article master", required: true });
|
||||
const purpose = erstelleElement("textarea", { rows: 2 });
|
||||
const owner = unitOptions(erstelleElement("select"), null, new Set(), t("org.noOwner"));
|
||||
|
||||
const form = erstelleElement("form", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.workspaceId") }), id,
|
||||
erstelleElement("label", { text: t("org.workspaceName") }), name,
|
||||
erstelleElement("label", { text: t("org.purpose") }), purpose,
|
||||
erstelleElement("label", { text: t("org.owner") }), owner,
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.newWorkspaceHint") }));
|
||||
|
||||
const cancel = erstelleElement("button", { type: "button", klasse: "flach", text: t("common.cancel") });
|
||||
const create = erstelleElement("button", { type: "submit", klasse: "primaer", text: t("common.create") });
|
||||
const modal = erstelleModal(t("org.newWorkspace"), form, [cancel, create]);
|
||||
cancel.addEventListener("click", () => modal.schliessen());
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
// The owning unit manages what it owns; everyone else is added on purpose afterwards.
|
||||
const members = owner.value ? [{ unitId: owner.value, agentId: null, access: "Manage", includeSubUnits: true }] : [];
|
||||
const ok = await send("/organisation/workspaces", "POST", {
|
||||
id: id.value.trim(), name: name.value.trim(), purpose: purpose.value, ownerUnitId: owner.value || null, members, reason: reason()
|
||||
});
|
||||
if (ok) {
|
||||
modal.schliessen();
|
||||
select({ kind: "workspace", id: id.value.trim() });
|
||||
}
|
||||
});
|
||||
modal.oeffnen();
|
||||
}
|
||||
|
||||
function workspacePanel(workspace) {
|
||||
const base = `/organisation/workspaces/${encodeURIComponent(workspace.id)}`;
|
||||
const owner = workspace.ownerUnitId ? unitPath(workspace.ownerUnitId) : t("org.noOwner");
|
||||
|
||||
// Name, purpose, protected folders.
|
||||
const name = erstelleElement("input", { type: "text", value: workspace.name });
|
||||
const purpose = erstelleElement("textarea", { rows: 2 });
|
||||
purpose.value = workspace.purpose ?? "";
|
||||
const protectedPaths = erstelleElement("input", { type: "text", value: workspace.protectedPaths.join(", "), placeholder: "prices, archive" });
|
||||
|
||||
const general = erstelleKarte(workspace.name, [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein",
|
||||
text: `${workspace.id} · ${t("org.owner")}: ${owner}${workspace.isUnitOwn ? ` (${t("org.unitOwnWorkspace")})` : ""}` }),
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein",
|
||||
text: `${t("org.fileCount", { n: workspace.fileCount })} · SharedWorkspaces/${workspace.id}/` }),
|
||||
erstelleElement("div", { klasse: "formular-raster" },
|
||||
erstelleElement("label", { text: t("org.workspaceName") }), name,
|
||||
erstelleElement("label", { text: t("org.purpose") }), purpose,
|
||||
erstelleElement("label", { text: t("org.protectedPaths") }), protectedPaths,
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.protectedHint") }),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("common.save"),
|
||||
onclick: () => send(base, "PATCH", {
|
||||
name: name.value,
|
||||
purpose: purpose.value,
|
||||
protectedPaths: protectedPaths.value.split(",").map((x) => x.trim()).filter(Boolean),
|
||||
reason: reason()
|
||||
})
|
||||
}))
|
||||
]);
|
||||
|
||||
return [general, membersCard(workspace, base), accessCard(workspace), deleteWorkspaceCard(workspace, base)];
|
||||
}
|
||||
|
||||
/** The member list, edited as a whole and saved in one go. */
|
||||
function membersCard(workspace, base) {
|
||||
const rows = erstelleElement("tbody");
|
||||
|
||||
const addRow = (member) => {
|
||||
const kind = erstelleElement("select", {},
|
||||
erstelleElement("option", { value: "unit", text: t("org.memberUnit") }),
|
||||
erstelleElement("option", { value: "agent", text: t("org.memberAgent") }));
|
||||
kind.value = member.agentId ? "agent" : "unit";
|
||||
|
||||
const target = erstelleElement("select");
|
||||
const fillTarget = () => {
|
||||
target.replaceChildren();
|
||||
const options = kind.value === "unit"
|
||||
? data.units.map((u) => [u.id, u.path])
|
||||
: data.agents.map((a) => [a.agentId, a.displayName && a.displayName !== a.agentId ? `${a.displayName} (${a.agentId})` : a.agentId]);
|
||||
for (const [value, text] of options) target.append(erstelleElement("option", { value, text }));
|
||||
const current = kind.value === "unit" ? member.unitId : member.agentId;
|
||||
if (current) target.value = current;
|
||||
};
|
||||
|
||||
const access = erstelleElement("select", {},
|
||||
...ACCESS.map((a) => erstelleElement("option", { value: a, text: accessLabel(a) })));
|
||||
access.value = member.access ?? "Read";
|
||||
|
||||
const sub = erstelleElement("input", { type: "checkbox" });
|
||||
sub.checked = member.includeSubUnits !== false;
|
||||
const syncSub = () => { sub.disabled = kind.value !== "unit"; };
|
||||
|
||||
kind.addEventListener("change", () => { fillTarget(); syncSub(); });
|
||||
fillTarget();
|
||||
syncSub();
|
||||
|
||||
const row = erstelleElement("tr", {},
|
||||
erstelleElement("td", {}, kind),
|
||||
erstelleElement("td", {}, target),
|
||||
erstelleElement("td", {}, access),
|
||||
erstelleElement("td", {}, erstelleElement("label", { klasse: "gruppe-horizontal text-klein" }, sub, t("org.includeSubUnits"))),
|
||||
erstelleElement("td", {}, erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: "×", title: t("org.removeMember"), "aria-label": t("org.removeMember"),
|
||||
onclick: () => row.remove()
|
||||
})));
|
||||
row.lesen = () => ({
|
||||
unitId: kind.value === "unit" ? target.value : null,
|
||||
agentId: kind.value === "agent" ? target.value : null,
|
||||
access: access.value,
|
||||
includeSubUnits: kind.value === "unit" ? sub.checked : true
|
||||
});
|
||||
rows.append(row);
|
||||
};
|
||||
|
||||
for (const member of workspace.members) addRow(member);
|
||||
|
||||
return erstelleKarte(t("org.memberEntries"), [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.membersHint") }),
|
||||
erstelleElement("table", { klasse: "daten-tabelle org-mitglieder" },
|
||||
erstelleElement("thead", {}, erstelleElement("tr", {},
|
||||
...[t("org.memberKind"), t("org.memberWho"), t("org.accessLevel"), "", ""].map((h) => erstelleElement("th", { text: h })))),
|
||||
rows),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" },
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: t("org.addMember"),
|
||||
onclick: () => addRow({ unitId: data.units[0]?.id ?? null, agentId: data.units.length ? null : data.agents[0]?.agentId, access: "Read" })
|
||||
}),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "primaer klein", text: t("common.save"),
|
||||
onclick: () => send(`${base}/members`, "PUT", {
|
||||
members: [...rows.children].map((r) => r.lesen()),
|
||||
reason: reason()
|
||||
})
|
||||
}))
|
||||
]);
|
||||
}
|
||||
|
||||
/** Who the list lets in — computed by the instance from the tree, not guessed here. */
|
||||
function accessCard(workspace) {
|
||||
return erstelleKarte(t("org.whoGetsIn"), [
|
||||
workspace.access.length
|
||||
? erstelleElement("ul", { klasse: "org-liste" }, ...workspace.access.map((a) => erstelleElement("li", {},
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "flach klein", text: `${a.agentId} — ${accessLabel(a.access)}`,
|
||||
onclick: () => select({ kind: "agent", id: a.agentId })
|
||||
}))))
|
||||
: erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.nobodyHasAccess") })
|
||||
]);
|
||||
}
|
||||
|
||||
function deleteWorkspaceCard(workspace, base) {
|
||||
if (workspace.isUnitOwn) {
|
||||
return erstelleKarte(null, [erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.unitWorkspaceStays") })]);
|
||||
}
|
||||
return erstelleKarte(null, [
|
||||
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.deleteWorkspaceHint") }),
|
||||
erstelleElement("button", {
|
||||
type: "button", klasse: "gefahr klein", text: t("org.deleteWorkspace"),
|
||||
disabled: workspace.fileCount > 0,
|
||||
onclick: async () => {
|
||||
if (await send(`${base}?reason=${encodeURIComponent(reason())}`, "DELETE")) {
|
||||
selection = null;
|
||||
renderDetail();
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user