9 Commits
v0.1.8 ... main

9 changed files with 120 additions and 140 deletions

View File

@@ -19,7 +19,7 @@
"LICENSE" "LICENSE"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.8", "@govoplan/core-webui": "^0.1.9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -1,3 +1,3 @@
"""GovOPlaN organizations module.""" """GovOPlaN organizations module."""
__version__ = "0.1.6" __version__ = "0.1.8"

View File

@@ -16,6 +16,7 @@ from govoplan_core.core.modules import (
RoleTemplate, RoleTemplate,
) )
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY
from govoplan_core.core.views import ViewSurface
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
from govoplan_organizations.backend.db import models as organization_models # noqa: F401 - populate metadata from govoplan_organizations.backend.db import models as organization_models # noqa: F401 - populate metadata
@@ -98,6 +99,15 @@ manifest = ModuleManifest(
package_name="@govoplan/organizations-webui", package_name="@govoplan/organizations-webui",
routes=(FrontendRoute(path="/organizations", component="OrganizationsPage", required_any=ORGANIZATIONS_READ_SCOPES, order=70),), routes=(FrontendRoute(path="/organizations", component="OrganizationsPage", required_any=ORGANIZATIONS_READ_SCOPES, order=70),),
nav_items=(NavItem(path="/organizations", label="Organizations", icon="users", required_any=ORGANIZATIONS_READ_SCOPES, order=70),), nav_items=(NavItem(path="/organizations", label="Organizations", icon="users", required_any=ORGANIZATIONS_READ_SCOPES, order=70),),
view_surfaces=(
ViewSurface(
id="organizations.admin.tenant",
module_id="organizations",
kind="section",
label="Organizations administration",
order=85,
),
),
), ),
migration_spec=MigrationSpec( migration_spec=MigrationSpec(
module_id="organizations", module_id="organizations",

View File

@@ -6,6 +6,9 @@
"main": "src/index.ts", "main": "src/index.ts",
"module": "src/index.ts", "module": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"scripts": {
"test:organizations-tree": "node scripts/test-organizations-tree-structure.mjs"
},
"exports": { "exports": {
".": { ".": {
"types": "./src/index.ts", "types": "./src/index.ts",
@@ -14,7 +17,7 @@
"./styles/organizations.css": "./src/styles/organizations.css" "./styles/organizations.css": "./src/styles/organizations.css"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.8", "@govoplan/core-webui": "^0.1.9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.0.0", "react": "^19.0.0",

View File

@@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const source = readFileSync(
new URL("../src/features/organizations/OrganizationsPage.tsx", import.meta.url),
"utf8"
);
const styles = readFileSync(
new URL("../src/styles/organizations.css", import.meta.url),
"utf8"
);
assert(source.includes("ExplorerTree,"), "Organizations imports the central ExplorerTree");
assert(source.includes("<ExplorerTree"), "the organization hierarchy uses ExplorerTree");
assert(source.includes("collapsible={false}"), "the always-expanded hierarchy uses the non-collapsible contract");
assert(source.includes("renderNodeActions="), "per-unit controls use the sibling node-action slot");
assert(source.includes("<IconButton"), "per-unit controls use the central icon-only action primitive");
assert(!source.includes("renderUnitTreeNodes"), "the custom recursive renderer was removed");
assert(!source.includes("organizations-tree-row"), "the custom tree row was removed");
assert(!source.includes("organizations-tree-node"), "the custom tree node was removed");
assert(!styles.includes("organizations-tree-"), "Organizations no longer owns shared tree styling");

View File

@@ -121,7 +121,7 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
</Card> </Card>
<Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc"> <Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc">
<div className="organizations-form-grid"> <div className="admin-form-grid two-columns">
<FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d"> <FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d">
<select value={draft.audit_detail_level} disabled={!canWrite || busy} onChange={(event) => setDraft({ ...draft, audit_detail_level: event.target.value as OrganizationAuditDetailLevel })}> <select value={draft.audit_detail_level} disabled={!canWrite || busy} onChange={(event) => setDraft({ ...draft, audit_detail_level: event.target.value as OrganizationAuditDetailLevel })}>
{AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)} {AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}

View File

@@ -8,20 +8,25 @@ import {
DataGrid, DataGrid,
Dialog, Dialog,
DismissibleAlert, DismissibleAlert,
ExplorerTree,
FormField, FormField,
IconButton,
LoadingFrame, LoadingFrame,
ModuleSubnav, ModuleSubnav,
PageTitle, PageTitle,
StatusBadge, StatusBadge,
TableActionGroup,
ToggleSwitch, ToggleSwitch,
hasScope, hasScope,
isViewSurfaceVisible,
useEffectiveView,
useUnsavedDraftGuard, useUnsavedDraftGuard,
usePlatformUiCapabilities, usePlatformUiCapabilities,
useViewSurfaces,
type ApiSettings, type ApiSettings,
type AuthInfo, type AuthInfo,
type DataGridColumn, type DataGridColumn,
type ModuleSubnavGroup, type ModuleSubnavGroup,
type OrganizationFunctionActionContribution,
type OrganizationFunctionActionsUiCapability type OrganizationFunctionActionsUiCapability
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
import { import {
@@ -294,15 +299,6 @@ function activeStatus(active: boolean): JSX.Element {
return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-organizations.active.a733b809" : "i18n:govoplan-organizations.inactive.09af574c"} />; return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-organizations.active.a733b809" : "i18n:govoplan-organizations.inactive.09af574c"} />;
} }
function RowActions({ disabled, onEdit, extra }: { disabled: boolean; onEdit: () => void; extra?: JSX.Element[] }) {
return (
<div className="organizations-row-actions">
<AdminIconButton label="i18n:govoplan-organizations.edit.7dce1220" icon={<Edit3 size={16} aria-hidden="true" />} disabled={disabled} onClick={onEdit} />
{extra}
</div>
);
}
function SluggedFields({ function SluggedFields({
draft, draft,
onChange, onChange,
@@ -385,6 +381,8 @@ export default function OrganizationsPage({
const [selectedUnitId, setSelectedUnitId] = useState(""); const [selectedUnitId, setSelectedUnitId] = useState("");
const [changeRequestId, setChangeRequestId] = useState(""); const [changeRequestId, setChangeRequestId] = useState("");
const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions"); const functionActionCapabilities = usePlatformUiCapabilities<OrganizationFunctionActionsUiCapability>("organizations.functionActions");
const effectiveView = useEffectiveView();
const viewSurfaces = useViewSurfaces();
const canWriteModel = hasScope(auth, "organizations:model:write"); const canWriteModel = hasScope(auth, "organizations:model:write");
const canWriteUnits = hasScope(auth, "organizations:unit:write"); const canWriteUnits = hasScope(auth, "organizations:unit:write");
@@ -400,8 +398,11 @@ export default function OrganizationsPage({
() => functionActionCapabilities () => functionActionCapabilities
.flatMap((capability) => capability.actions) .flatMap((capability) => capability.actions)
.filter((contribution) => contributionVisible(auth, contribution)) .filter((contribution) => contributionVisible(auth, contribution))
.filter((contribution) =>
isViewSurfaceVisible(effectiveView, contribution.surfaceId, viewSurfaces)
)
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100)), .sort((left, right) => (left.order ?? 100) - (right.order ?? 100)),
[auth, functionActionCapabilities] [auth, effectiveView, functionActionCapabilities, viewSurfaces]
); );
const unitsByParentId = useMemo(() => { const unitsByParentId = useMemo(() => {
const mapped = new Map<string, OrganizationUnitItem[]>(); const mapped = new Map<string, OrganizationUnitItem[]>();
@@ -870,7 +871,7 @@ export default function OrganizationsPage({
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug }, { id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" }, { id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editUnitType(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editUnitType(row) }]} /> }
]; ];
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [ const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
@@ -878,7 +879,7 @@ export default function OrganizationsPage({
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind }, { id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug }, { id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editStructure(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editStructure(row) }]} /> }
]; ];
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [ const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
@@ -888,7 +889,7 @@ export default function OrganizationsPage({
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) }, { id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) }, { id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editRelationType(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editRelationType(row) }]} /> }
]; ];
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [ const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
@@ -897,7 +898,7 @@ export default function OrganizationsPage({
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) }, { id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug }, { id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteUnits || busy} onEdit={() => editUnit(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editUnit(row) }]} /> }
]; ];
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [ const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
@@ -906,7 +907,7 @@ export default function OrganizationsPage({
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) }, { id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) }, { id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteUnits || busy} onEdit={() => editRelation(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editRelation(row) }]} /> }
]; ];
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [ const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
@@ -915,7 +916,7 @@ export default function OrganizationsPage({
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) }, { id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) }, { id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "", width: 88, sticky: "end", render: (row) => <RowActions disabled={!canWriteModel || busy} onEdit={() => editFunctionType(row)} /> } { id: "actions", header: "i18n:govoplan-organizations.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editFunctionType(row) }]} /> }
]; ];
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [ const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
@@ -926,20 +927,26 @@ export default function OrganizationsPage({
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) }, { id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ {
id: "actions", id: "actions",
header: "", header: "i18n:govoplan-organizations.actions.c3cd636a",
width: functionActionContributions.length ? 220 : 88, width: 88 + (functionActionContributions.length * 40),
sticky: "end", sticky: "end",
render: (row) => ( resizable: false,
<RowActions align: "right",
disabled={!canWriteFunctions || busy} render: (row) => <TableActionGroup actions={[
onEdit={() => editFunction(row)} {
extra={functionActionContributions.map((contribution) => ( id: "edit",
<span className="organizations-contributed-action" key={contribution.id}> label: "i18n:govoplan-organizations.edit.7dce1220",
{contribution.render({ settings, auth, function: row })} icon: <Edit3 size={16} aria-hidden="true" />,
</span> disabled: !canWriteFunctions || busy,
))} onClick: () => editFunction(row)
/> },
) ...functionActionContributions.map((contribution) => ({
id: contribution.id,
label: contribution.label,
icon: contribution.icon,
onClick: () => contribution.onClick({ settings, auth, function: row })
}))
]} />
} }
]; ];
@@ -1003,41 +1010,42 @@ export default function OrganizationsPage({
); );
} }
function renderUnitTreeNodes(parentId = "", depth = 0, seen: ReadonlySet<string> = new Set()): JSX.Element[] {
return (unitsByParentId.get(parentId) ?? []).flatMap((unit) => {
if (seen.has(unit.id)) return [];
const nextSeen = new Set(seen);
nextSeen.add(unit.id);
return [
<div className="organizations-tree-row" key={unit.id}>
<button
type="button"
className={`organizations-tree-node ${selectedUnitId === unit.id ? "active" : ""}`.trim()}
style={{ paddingLeft: `${10 + depth * 18}px` }}
onClick={() => setSelectedUnitId(unit.id)}
>
<strong>{unit.name}</strong>
<span>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</span>
</button>
<Button type="button" variant="ghost" disabled={!canWriteUnits || busy} onClick={() => addSubunit(unit)} title="i18n:govoplan-organizations.add_subunit.8256a8f7">
<Plus size={16} aria-hidden="true" /> i18n:govoplan-organizations.add_subunit.8256a8f7
</Button>
</div>,
...renderUnitTreeNodes(unit.id, depth + 1, nextSeen)
];
});
}
function renderUnitsSection() { function renderUnitsSection() {
return ( return (
<div className="organizations-table-stack"> <div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}> <Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<div className="organizations-tree-toolbar"> <div className="explorer-tree-toolbar">
{selectedUnitId && <Button type="button" variant="ghost" disabled={busy} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>} {selectedUnitId && <Button type="button" variant="ghost" disabled={busy} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>}
</div> </div>
<div className="organizations-tree-list"> {model.units.length ? (
{model.units.length ? renderUnitTreeNodes() : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>} <ExplorerTree
</div> nodes={unitsByParentId.get("") ?? []}
getNodeId={(unit) => unit.id}
getNodeLabel={(unit) => unit.name}
getNodeChildren={(unit) => unitsByParentId.get(unit.id) ?? []}
activeId={selectedUnitId}
collapsible={false}
depth={0}
className="explorer-tree-scroll-region"
getNodeWrapStyle={(_unit, context) => ({ paddingLeft: `${Math.min(context.depth * 18, 72)}px` })}
onOpen={(unit) => setSelectedUnitId(unit.id)}
renderNodeContent={(unit) => (
<span className="explorer-tree-node-content">
<strong>{unit.name}</strong>
<small>{unitTypeById.get(unit.unit_type_id || "")?.name ?? "i18n:govoplan-organizations.no_unit_type.73e799f5"}</small>
</span>
)}
renderNodeActions={(unit) => (
<IconButton
label="i18n:govoplan-organizations.add_subunit.8256a8f7"
icon={<Plus size={16} aria-hidden="true" />}
variant="ghost"
disabled={!canWriteUnits || busy}
onClick={() => addSubunit(unit)}
/>
)}
/>
) : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
</Card> </Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}> <Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" /> <DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
@@ -1129,7 +1137,7 @@ export default function OrganizationsPage({
</> </>
)} )}
> >
<form id={formId} className="organizations-form-grid" onSubmit={(event) => void submit(event)}> <form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
{renderEditorFields()} {renderEditorFields()}
</form> </form>
</Dialog> </Dialog>

View File

@@ -23,6 +23,7 @@ const organizationAdminSections: AdminSectionsUiCapability = {
sections: [ sections: [
{ {
id: "tenant-organization-settings", id: "tenant-organization-settings",
surfaceId: "organizations.admin.tenant",
label: "i18n:govoplan-organizations.organizations.220edf64", label: "i18n:govoplan-organizations.organizations.220edf64",
group: "TENANT", group: "TENANT",
order: 85, order: 85,
@@ -45,6 +46,15 @@ export const organizationsModule: PlatformWebModule = {
dependencies: ["access"], dependencies: ["access"],
optionalDependencies: ["admin"], optionalDependencies: ["admin"],
translations, translations,
viewSurfaces: [
{
id: "organizations.admin.tenant",
moduleId: "organizations",
kind: "section",
label: "Organizations administration",
order: 85
}
],
navItems: [ navItems: [
{ {
to: "/organizations", to: "/organizations",

View File

@@ -1,6 +1,6 @@
.organizations-workspace { .organizations-workspace {
grid-template-columns: 230px minmax(0, 1fr); grid-template-columns: 230px minmax(0, 1fr);
background: var(--bg, #f8f7f4); background: var(--bg);
} }
.organizations-page { .organizations-page {
@@ -30,32 +30,6 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.organizations-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.organizations-form-grid .wide {
grid-column: 1 / -1;
}
.organizations-form-actions,
.organizations-row-actions {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: nowrap;
}
.organizations-row-actions {
justify-content: flex-end;
}
.organizations-contributed-action {
display: inline-flex;
}
.organizations-check-list { .organizations-check-list {
display: grid; display: grid;
gap: 10px; gap: 10px;
@@ -76,51 +50,6 @@
padding-top: 2px; padding-top: 2px;
} }
.organizations-tree-toolbar {
display: flex;
justify-content: flex-start;
margin-bottom: 12px;
}
.organizations-tree-list {
display: grid;
gap: 4px;
max-height: 640px;
overflow: auto;
}
.organizations-tree-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 6px;
}
.organizations-tree-node {
display: grid;
gap: 2px;
width: 100%;
min-width: 0;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 7px;
background: transparent;
color: var(--text);
text-align: left;
cursor: pointer;
}
.organizations-tree-node:hover,
.organizations-tree-node.active {
border-color: var(--line);
background: var(--surface-muted, #f6f7f9);
}
.organizations-tree-node span {
color: var(--muted);
font-size: 12px;
}
.organizations-field-note { .organizations-field-note {
margin: 8px 0 0; margin: 8px 0 0;
color: var(--muted); color: var(--muted);
@@ -147,8 +76,4 @@
.organizations-workspace { .organizations-workspace {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.organizations-form-grid {
grid-template-columns: 1fr;
}
} }