feat: add personal and group view ownership
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
type EffectiveViewProjection
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type ViewScopeType = "system" | "tenant";
|
||||
export type ViewScopeType = "system" | "tenant" | "group" | "user";
|
||||
export type ViewAssignmentScopeType = "system" | "tenant" | "group" | "user";
|
||||
export type ViewAssignmentMode = "available" | "default" | "required";
|
||||
|
||||
@@ -148,12 +148,14 @@ export async function selectEffectiveView(
|
||||
|
||||
export async function fetchViewDefinitions(
|
||||
settings: ApiSettings,
|
||||
scopeType: ViewScopeType
|
||||
scopeType: ViewScopeType,
|
||||
scopeId?: string | null
|
||||
): Promise<ViewDefinition[]> {
|
||||
const response = await apiFetch<{ definitions: ViewDefinition[] }>(
|
||||
settings,
|
||||
apiPath("/api/v1/views/definitions", {
|
||||
scope_type: scopeType,
|
||||
scope_id: scopeId || undefined,
|
||||
include_inherited: scopeType === "tenant"
|
||||
})
|
||||
);
|
||||
@@ -164,6 +166,7 @@ export function createViewDefinition(
|
||||
settings: ApiSettings,
|
||||
payload: {
|
||||
scope_type: ViewScopeType;
|
||||
scope_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
visible_surface_ids: string[];
|
||||
|
||||
122
webui/src/features/views/PersonalViewsPanel.tsx
Normal file
122
webui/src/features/views/PersonalViewsPanel.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
FormField,
|
||||
hasScope,
|
||||
type ApiSettings,
|
||||
type AuthInfo
|
||||
} from "@govoplan/core-webui";
|
||||
import ViewsAdminPanel from "./ViewsAdminPanel";
|
||||
import type { ViewScopeType } from "../../api/views";
|
||||
|
||||
|
||||
type OwnerOption = {
|
||||
key: string;
|
||||
scopeType: Extract<ViewScopeType, "group" | "user">;
|
||||
scopeId: string;
|
||||
label: string;
|
||||
description: string;
|
||||
canWrite: boolean;
|
||||
};
|
||||
|
||||
|
||||
export default function PersonalViewsPanel({
|
||||
settings,
|
||||
auth
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
}) {
|
||||
const options = useMemo(() => ownerOptions(auth), [auth]);
|
||||
const [ownerKey, setOwnerKey] = useState(options[0]?.key ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (!options.some((option) => option.key === ownerKey)) {
|
||||
setOwnerKey(options[0]?.key ?? "");
|
||||
}
|
||||
}, [options, ownerKey]);
|
||||
|
||||
const owner = options.find((option) => option.key === ownerKey) ?? options[0];
|
||||
if (!owner) {
|
||||
return (
|
||||
<p className="muted">
|
||||
You do not have access to personal or group View definitions.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="views-personal-panel">
|
||||
{options.length > 1 && (
|
||||
<div className="views-owner-toolbar">
|
||||
<FormField label="View owner">
|
||||
<select
|
||||
value={owner.key}
|
||||
onChange={(event) => setOwnerKey(event.target.value)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.key} value={option.key}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
<ViewsAdminPanel
|
||||
key={owner.key}
|
||||
settings={settings}
|
||||
scopeType={owner.scopeType}
|
||||
scopeId={owner.scopeId}
|
||||
canWriteDefinitions={owner.canWrite}
|
||||
canWriteAssignments={false}
|
||||
showAssignments={false}
|
||||
embedded
|
||||
title={owner.label}
|
||||
description={owner.description}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ownerOptions(auth: AuthInfo): OwnerOption[] {
|
||||
const tenantManager = hasScope(auth, "views:definition:write");
|
||||
const options: OwnerOption[] = [];
|
||||
if (
|
||||
tenantManager ||
|
||||
hasScope(auth, "views:personal_definition:read") ||
|
||||
hasScope(auth, "views:personal_definition:write")
|
||||
) {
|
||||
options.push({
|
||||
key: `user:${auth.user.account_id}`,
|
||||
scopeType: "user",
|
||||
scopeId: auth.user.account_id,
|
||||
label: "My Views",
|
||||
description:
|
||||
"Design personal interface projections. Publishing a View makes it available to this account.",
|
||||
canWrite:
|
||||
tenantManager ||
|
||||
hasScope(auth, "views:personal_definition:write")
|
||||
});
|
||||
}
|
||||
const canReadGroups =
|
||||
tenantManager ||
|
||||
hasScope(auth, "views:group_definition:read") ||
|
||||
hasScope(auth, "views:group_definition:write");
|
||||
if (canReadGroups) {
|
||||
for (const group of auth.groups) {
|
||||
options.push({
|
||||
key: `group:${group.id}`,
|
||||
scopeType: "group",
|
||||
scopeId: group.id,
|
||||
label: `Group: ${group.name}`,
|
||||
description:
|
||||
"Design reusable interface projections for this group. Publishing a View makes it available to all current group members.",
|
||||
canWrite:
|
||||
tenantManager ||
|
||||
hasScope(auth, "views:group_definition:write")
|
||||
});
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Archive,
|
||||
Check,
|
||||
CheckSquare2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
MinusSquare,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Send,
|
||||
Square,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -16,8 +19,11 @@ import {
|
||||
ConfirmDialog,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
ExplorerTree,
|
||||
FormField,
|
||||
IconButton,
|
||||
SelectionList,
|
||||
SelectionListItem,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
adminErrorMessage,
|
||||
@@ -77,13 +83,23 @@ const LOCKOUT_SURFACES = new Set([
|
||||
export default function ViewsAdminPanel({
|
||||
settings,
|
||||
scopeType,
|
||||
scopeId,
|
||||
canWriteDefinitions,
|
||||
canWriteAssignments
|
||||
canWriteAssignments,
|
||||
showAssignments = scopeType === "system" || scopeType === "tenant",
|
||||
embedded = false,
|
||||
title: titleOverride,
|
||||
description: descriptionOverride
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
scopeType: ViewScopeType;
|
||||
scopeId?: string | null;
|
||||
canWriteDefinitions: boolean;
|
||||
canWriteAssignments: boolean;
|
||||
showAssignments?: boolean;
|
||||
embedded?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const surfaces = useViewSurfaces();
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
@@ -133,8 +149,10 @@ export default function ViewsAdminPanel({
|
||||
setError("");
|
||||
try {
|
||||
const [nextDefinitions, nextAssignments] = await Promise.all([
|
||||
fetchViewDefinitions(settings, scopeType),
|
||||
fetchViewAssignments(settings, scopeType)
|
||||
fetchViewDefinitions(settings, scopeType, scopeId),
|
||||
showAssignments && (scopeType === "system" || scopeType === "tenant")
|
||||
? fetchViewAssignments(settings, scopeType)
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
setDefinitions(nextDefinitions);
|
||||
setAssignments(nextAssignments);
|
||||
@@ -161,6 +179,8 @@ export default function ViewsAdminPanel({
|
||||
void load();
|
||||
}, [
|
||||
scopeType,
|
||||
scopeId,
|
||||
showAssignments,
|
||||
settings.accessToken,
|
||||
settings.apiBaseUrl,
|
||||
settings.apiKey
|
||||
@@ -268,6 +288,7 @@ export default function ViewsAdminPanel({
|
||||
.map((surface) => surface.id);
|
||||
const created = await createViewDefinition(settings, {
|
||||
scope_type: scopeType,
|
||||
scope_id: scopeId || null,
|
||||
name: createDraft.name.trim(),
|
||||
description: createDraft.description.trim() || null,
|
||||
visible_surface_ids: visibleSurfaceIds
|
||||
@@ -411,11 +432,26 @@ export default function ViewsAdminPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const title = scopeType === "system" ? "System Views" : "Tenant Views";
|
||||
const title =
|
||||
titleOverride ??
|
||||
({
|
||||
system: "System Views",
|
||||
tenant: "Tenant Views",
|
||||
group: "Group Views",
|
||||
user: "My Views"
|
||||
} satisfies Record<ViewScopeType, string>)[scopeType];
|
||||
const description =
|
||||
scopeType === "system"
|
||||
? "Publish reusable interface projections and assign instance-wide defaults or requirements."
|
||||
: "Tailor the visible interface for this tenant, its groups, and individual users.";
|
||||
descriptionOverride ??
|
||||
({
|
||||
system:
|
||||
"Publish reusable interface projections and assign instance-wide defaults or requirements.",
|
||||
tenant:
|
||||
"Tailor the visible interface for this tenant, its groups, and individual users.",
|
||||
group:
|
||||
"Design reusable interface projections for members of this group.",
|
||||
user:
|
||||
"Design personal interface projections that remain available to this account."
|
||||
} satisfies Record<ViewScopeType, string>)[scopeType];
|
||||
const canCreate =
|
||||
canWriteDefinitions &&
|
||||
surfaces.some((surface) => surface.kind === "navigation") &&
|
||||
@@ -426,6 +462,7 @@ export default function ViewsAdminPanel({
|
||||
<AdminPageLayout
|
||||
title={title}
|
||||
description={description}
|
||||
className={embedded ? "views-admin-page embedded" : "views-admin-page"}
|
||||
loading={loading}
|
||||
error={error}
|
||||
success={success}
|
||||
@@ -453,42 +490,42 @@ export default function ViewsAdminPanel({
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="views-admin-shell">
|
||||
<div className="views-management-layout">
|
||||
<div className="views-admin-shell">
|
||||
<aside className="views-definition-pane" aria-label="View definitions">
|
||||
<div className="views-pane-heading">
|
||||
<strong>Definitions</strong>
|
||||
<span>{definitions.length}</span>
|
||||
</div>
|
||||
<div className="views-definition-list">
|
||||
<SelectionList
|
||||
className="views-definition-list"
|
||||
label="View definitions"
|
||||
>
|
||||
{definitions.map((definition) => (
|
||||
<button
|
||||
<SelectionListItem
|
||||
key={definition.id}
|
||||
type="button"
|
||||
className={
|
||||
definition.id === selectedId
|
||||
? "views-definition-item active"
|
||||
: "views-definition-item"
|
||||
}
|
||||
className="views-definition-item"
|
||||
selected={definition.id === selectedId}
|
||||
onClick={() => selectDefinition(definition.id)}
|
||||
>
|
||||
<span className="views-definition-item-main">
|
||||
<strong>{definition.name}</strong>
|
||||
<small>
|
||||
{definition.scope_type === "system" ? "System" : "Tenant"}
|
||||
{scopeLabel(definition)}
|
||||
{" · "}
|
||||
revision {definition.current_revision}
|
||||
</small>
|
||||
</span>
|
||||
<StatusBadge status={definition.status} />
|
||||
<ChevronRight size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</SelectionListItem>
|
||||
))}
|
||||
{!definitions.length && (
|
||||
<div className="views-empty-list">
|
||||
No Views have been defined in this scope.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SelectionList>
|
||||
</aside>
|
||||
|
||||
<main className="views-editor-pane">
|
||||
@@ -610,17 +647,6 @@ export default function ViewsAdminPanel({
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AssignmentsSection
|
||||
assignments={assignments}
|
||||
definitions={definitions}
|
||||
scopeType={scopeType}
|
||||
busy={busy}
|
||||
canWrite={canWriteAssignments}
|
||||
onCreate={openCreateAssignment}
|
||||
onEdit={openEditAssignment}
|
||||
onToggle={toggleAssignment}
|
||||
onDelete={setDeleteAssignmentTarget}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="views-empty-editor">
|
||||
@@ -629,6 +655,21 @@ export default function ViewsAdminPanel({
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
{showAssignments &&
|
||||
(scopeType === "system" || scopeType === "tenant") && (
|
||||
<AssignmentsSection
|
||||
assignments={assignments}
|
||||
definitions={definitions}
|
||||
scopeType={scopeType}
|
||||
busy={busy}
|
||||
canWrite={canWriteAssignments}
|
||||
onCreate={openCreateAssignment}
|
||||
onEdit={openEditAssignment}
|
||||
onToggle={toggleAssignment}
|
||||
onDelete={setDeleteAssignmentTarget}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AdminPageLayout>
|
||||
|
||||
@@ -1026,6 +1067,7 @@ function SurfaceSelector({
|
||||
onChange: (surfaceIds: string[]) => void;
|
||||
}) {
|
||||
const [filter, setFilter] = useState("");
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
||||
const byId = useMemo(
|
||||
() => new Map(surfaces.map((surface) => [surface.id, surface])),
|
||||
[surfaces]
|
||||
@@ -1098,6 +1140,26 @@ function SurfaceSelector({
|
||||
}
|
||||
|
||||
const normalizedFilter = filter.trim().toLowerCase();
|
||||
const visibleModules = modules.filter((moduleSurface) => {
|
||||
if (!normalizedFilter) return true;
|
||||
return [moduleSurface, ...descendants(moduleSurface.id)]
|
||||
.map((surface) =>
|
||||
typeof surface === "string" ? byId.get(surface) : surface
|
||||
)
|
||||
.filter((surface): surface is PlatformViewSurface => Boolean(surface))
|
||||
.some((surface) =>
|
||||
`${surface.label} ${surface.id} ${surface.description ?? ""}`
|
||||
.toLowerCase()
|
||||
.includes(normalizedFilter)
|
||||
);
|
||||
});
|
||||
const effectiveExpandedIds = normalizedFilter
|
||||
? new Set(
|
||||
surfaces
|
||||
.filter((surface) => (childrenByParent.get(surface.id) ?? []).length > 0)
|
||||
.map((surface) => surface.id)
|
||||
)
|
||||
: expandedIds;
|
||||
|
||||
return (
|
||||
<div className="views-surface-selector">
|
||||
@@ -1110,94 +1172,80 @@ function SurfaceSelector({
|
||||
/>
|
||||
</div>
|
||||
<div className="views-surface-modules">
|
||||
{modules.map((moduleSurface) => {
|
||||
const moduleSurfaces = [
|
||||
moduleSurface,
|
||||
...descendants(moduleSurface.id)
|
||||
.map((id) => byId.get(id))
|
||||
.filter((item): item is PlatformViewSurface => Boolean(item))
|
||||
];
|
||||
const matches = !normalizedFilter || moduleSurfaces.some((surface) =>
|
||||
`${surface.label} ${surface.id} ${surface.description ?? ""}`
|
||||
.toLowerCase()
|
||||
.includes(normalizedFilter)
|
||||
);
|
||||
if (!matches) return null;
|
||||
const childIds = moduleSurfaces.slice(1).map((surface) => surface.id);
|
||||
const selectedChildren = childIds.filter((id) => selectedSet.has(id)).length;
|
||||
return (
|
||||
<details key={moduleSurface.id} open={Boolean(normalizedFilter)}>
|
||||
<summary>
|
||||
<SurfaceCheckbox
|
||||
checked={selectedSet.has(moduleSurface.id)}
|
||||
indeterminate={
|
||||
selectedChildren > 0 && selectedChildren < childIds.length
|
||||
}
|
||||
disabled={disabled || requiredIds.has(moduleSurface.id)}
|
||||
label={moduleSurface.label}
|
||||
secondary={`${selectedChildren}/${childIds.length}`}
|
||||
onChange={(checked) => toggle(moduleSurface.id, checked)}
|
||||
/>
|
||||
</summary>
|
||||
<div className="views-surface-children">
|
||||
{moduleSurfaces.slice(1).map((surface) => (
|
||||
<SurfaceCheckbox
|
||||
key={surface.id}
|
||||
checked={selectedSet.has(surface.id)}
|
||||
disabled={disabled || requiredIds.has(surface.id)}
|
||||
label={surface.label}
|
||||
secondary={`${surface.kind} · ${surface.description ?? surface.id}`}
|
||||
onChange={(checked) => toggle(surface.id, checked)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
<ExplorerTree
|
||||
nodes={visibleModules}
|
||||
getNodeId={(surface) => surface.id}
|
||||
getNodeLabel={(surface) => surface.label}
|
||||
getNodeChildren={(surface) => childrenByParent.get(surface.id) ?? []}
|
||||
expandedIds={effectiveExpandedIds}
|
||||
depth={0}
|
||||
className="views-surface-tree"
|
||||
onToggle={(surface) => {
|
||||
setExpandedIds((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(surface.id)) next.delete(surface.id);
|
||||
else next.add(surface.id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onOpen={(surface) => {
|
||||
if (disabled || requiredIds.has(surface.id)) return;
|
||||
toggle(surface.id, !selectedSet.has(surface.id));
|
||||
}}
|
||||
renderToggleIcon={(_surface, context) =>
|
||||
context.hasChildren ? (
|
||||
context.expanded ? (
|
||||
<ChevronDown size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronRight size={16} aria-hidden="true" />
|
||||
)
|
||||
) : null
|
||||
}
|
||||
getNodeButtonClassName={(surface) =>
|
||||
disabled || requiredIds.has(surface.id)
|
||||
? "views-surface-node disabled"
|
||||
: "views-surface-node"
|
||||
}
|
||||
renderNodeContent={(surface) => {
|
||||
const childIds = descendants(surface.id);
|
||||
const selectedChildren = childIds.filter((id) =>
|
||||
selectedSet.has(id)
|
||||
).length;
|
||||
const checked = selectedSet.has(surface.id);
|
||||
const indeterminate =
|
||||
selectedChildren > 0 && selectedChildren < childIds.length;
|
||||
const secondary =
|
||||
surface.kind === "module"
|
||||
? `${selectedChildren}/${childIds.length} surfaces`
|
||||
: `${surface.kind} · ${surface.description ?? surface.id}`;
|
||||
return (
|
||||
<>
|
||||
<span className="views-surface-check" aria-hidden="true">
|
||||
{indeterminate ? (
|
||||
<MinusSquare size={17} />
|
||||
) : checked ? (
|
||||
<CheckSquare2 size={17} />
|
||||
) : (
|
||||
<Square size={17} />
|
||||
)}
|
||||
</span>
|
||||
<span className="explorer-tree-node-content">
|
||||
<strong>{surface.label}</strong>
|
||||
<small>{secondary}</small>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{!visibleModules.length && (
|
||||
<div className="views-empty-list">No matching surfaces.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function SurfaceCheckbox({
|
||||
checked,
|
||||
indeterminate = false,
|
||||
disabled,
|
||||
label,
|
||||
secondary,
|
||||
onChange
|
||||
}: {
|
||||
checked: boolean;
|
||||
indeterminate?: boolean;
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
secondary: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.indeterminate = indeterminate;
|
||||
}, [indeterminate]);
|
||||
return (
|
||||
<label className={disabled ? "views-surface-row disabled" : "views-surface-row"}>
|
||||
<input
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
/>
|
||||
<span>
|
||||
<strong>{label}</strong>
|
||||
<small>{secondary}</small>
|
||||
</span>
|
||||
{checked && <Check size={15} aria-hidden="true" />}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function emptyAssignmentDraft(scopeType: ViewScopeType): AssignmentDraft {
|
||||
return {
|
||||
scopeType: scopeType === "system" ? "system" : "tenant",
|
||||
@@ -1211,6 +1259,19 @@ function emptyAssignmentDraft(scopeType: ViewScopeType): AssignmentDraft {
|
||||
}
|
||||
|
||||
|
||||
function scopeLabel(definition: ViewDefinition): string {
|
||||
const label = {
|
||||
system: "System",
|
||||
tenant: "Tenant",
|
||||
group: "Group",
|
||||
user: "User"
|
||||
} satisfies Record<ViewScopeType, string>;
|
||||
return definition.scope_id && definition.scope_type !== "tenant"
|
||||
? `${label[definition.scope_type]} · ${definition.scope_id}`
|
||||
: label[definition.scope_type];
|
||||
}
|
||||
|
||||
|
||||
function definitionDraftKey(draft: DefinitionDraft): string {
|
||||
return JSON.stringify({
|
||||
name: draft.name.trim(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
hasScope,
|
||||
type AdminSectionsUiCapability,
|
||||
type PlatformWebModule,
|
||||
type SettingsSectionsUiCapability,
|
||||
type ViewsRuntimeUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import ViewSelector from "./components/ViewSelector";
|
||||
@@ -13,6 +14,9 @@ import "./styles/views.css";
|
||||
const ViewsAdminPanel = lazy(
|
||||
() => import("./features/views/ViewsAdminPanel")
|
||||
);
|
||||
const PersonalViewsPanel = lazy(
|
||||
() => import("./features/views/PersonalViewsPanel")
|
||||
);
|
||||
|
||||
const viewsAdminSections: AdminSectionsUiCapability = {
|
||||
sections: [
|
||||
@@ -63,6 +67,28 @@ const viewsRuntime: ViewsRuntimeUiCapability = {
|
||||
Selector: ViewSelector
|
||||
};
|
||||
|
||||
const viewsSettingsSections: SettingsSectionsUiCapability = {
|
||||
sections: [
|
||||
{
|
||||
id: "views",
|
||||
label: "Views",
|
||||
group: "ui",
|
||||
order: 35,
|
||||
surfaceId: "views.settings.personal",
|
||||
anyOf: [
|
||||
"views:definition:read",
|
||||
"views:definition:write",
|
||||
"views:group_definition:read",
|
||||
"views:group_definition:write",
|
||||
"views:personal_definition:read",
|
||||
"views:personal_definition:write"
|
||||
],
|
||||
render: ({ settings, auth }) =>
|
||||
createElement(PersonalViewsPanel, { settings, auth })
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const viewsModule: PlatformWebModule = {
|
||||
id: "views",
|
||||
label: "Views",
|
||||
@@ -90,13 +116,20 @@ export const viewsModule: PlatformWebModule = {
|
||||
kind: "section",
|
||||
label: "Tenant Views administration",
|
||||
order: 30
|
||||
},
|
||||
{
|
||||
id: "views.settings.personal",
|
||||
moduleId: "views",
|
||||
kind: "section",
|
||||
label: "Personal and group Views",
|
||||
order: 40
|
||||
}
|
||||
],
|
||||
uiCapabilities: {
|
||||
"admin.sections": viewsAdminSections,
|
||||
"settings.sections": viewsSettingsSections,
|
||||
"views.runtime": viewsRuntime
|
||||
}
|
||||
};
|
||||
|
||||
export default viewsModule;
|
||||
|
||||
|
||||
@@ -29,6 +29,32 @@
|
||||
opacity: .78;
|
||||
}
|
||||
|
||||
.views-management-layout {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.views-admin-page.embedded > .admin-page-heading {
|
||||
position: static;
|
||||
margin: 0 0 18px;
|
||||
padding: 0 0 14px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.views-personal-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.views-owner-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.views-owner-toolbar .form-field {
|
||||
width: min(360px, 100%);
|
||||
}
|
||||
|
||||
.views-admin-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
@@ -81,24 +107,7 @@
|
||||
grid-template-columns: minmax(0, 1fr) auto 18px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
padding: 9px 8px 9px 10px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.views-definition-item:hover {
|
||||
background: var(--hover-tint-soft);
|
||||
}
|
||||
|
||||
.views-definition-item.active {
|
||||
background: var(--accent-hover-bg);
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.views-definition-item-main {
|
||||
@@ -159,13 +168,18 @@
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.views-surface-section,
|
||||
.views-assignments-section {
|
||||
.views-surface-section {
|
||||
margin-top: 22px;
|
||||
padding-top: 18px;
|
||||
border-top: var(--border-line);
|
||||
}
|
||||
|
||||
.views-assignments-section {
|
||||
min-width: 0;
|
||||
padding-top: 18px;
|
||||
border-top: var(--border-line);
|
||||
}
|
||||
|
||||
.views-section-heading {
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
@@ -200,58 +214,25 @@
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.views-surface-modules details {
|
||||
border-bottom: var(--border-line);
|
||||
}
|
||||
|
||||
.views-surface-modules details:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.views-surface-modules summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.views-surface-modules summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.views-surface-children {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 0 0 6px 24px;
|
||||
}
|
||||
|
||||
.views-surface-row {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr) 18px;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-height: 42px;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.views-surface-row:hover {
|
||||
background: var(--hover-tint-soft);
|
||||
}
|
||||
|
||||
.views-surface-row.disabled {
|
||||
.views-surface-node.disabled {
|
||||
cursor: default;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.views-surface-row strong,
|
||||
.views-surface-row small {
|
||||
display: block;
|
||||
letter-spacing: 0;
|
||||
.views-surface-tree .explorer-tree-node-wrap {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.views-surface-row small {
|
||||
margin-top: 2px;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--muted);
|
||||
.views-surface-tree .explorer-tree-node {
|
||||
min-height: 42px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.views-surface-check {
|
||||
display: inline-grid;
|
||||
flex: 0 0 18px;
|
||||
place-items: center;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.views-assignment-list {
|
||||
|
||||
Reference in New Issue
Block a user