@parallelworks/ui 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -8
- package/dist/access/AccessDrawer.d.ts +55 -0
- package/dist/access/AccessDrawer.js +445 -0
- package/dist/access/AccessList.d.ts +58 -0
- package/dist/access/AccessList.js +382 -0
- package/dist/access/GrantView.d.ts +35 -0
- package/dist/access/GrantView.js +188 -0
- package/dist/access/GroupPicker.d.ts +35 -0
- package/dist/access/GroupPicker.js +336 -0
- package/dist/access/focus.d.ts +10 -0
- package/dist/access/focus.js +27 -0
- package/dist/access/index.d.ts +3 -0
- package/dist/access/index.js +3 -0
- package/dist/access/model.d.ts +49 -0
- package/dist/access/model.js +129 -0
- package/dist/access/strings.d.ts +122 -0
- package/dist/access/strings.js +151 -0
- package/dist/access/types.d.ts +56 -0
- package/dist/ai/ai.css +5 -3
- package/dist/ai/components/SlashMenu.js +1 -1
- package/dist/ai/components/agent/AgentMessageParts.js +1 -1
- package/dist/ai/strings.js +0 -1
- package/dist/ai/ui/sidebar.js +1 -1
- package/dist/components/Combobox.d.ts +19 -0
- package/dist/components/Combobox.js +101 -50
- package/dist/components/Drawer.d.ts +6 -1
- package/dist/components/Drawer.js +25 -16
- package/dist/components/Field.d.ts +5 -4
- package/dist/components/Field.js +27 -4
- package/dist/components/InfoTip.d.ts +12 -4
- package/dist/components/InfoTip.js +13 -3
- package/dist/components/MarkdownImpl.d.ts +1 -0
- package/dist/components/MarkdownImpl.js +1 -0
- package/dist/components/Menu.js +4 -4
- package/dist/components/MenuEntries.js +1 -1
- package/dist/components/OptionDescription.d.ts +7 -0
- package/dist/components/OptionDescription.js +19 -0
- package/dist/components/Popover.d.ts +4 -1
- package/dist/components/Popover.js +2 -1
- package/dist/components/Provider.d.ts +9 -0
- package/dist/components/Provider.js +4 -0
- package/dist/components/Select.js +13 -11
- package/dist/components/comboboxItems.d.ts +10 -0
- package/dist/components/comboboxItems.js +8 -1
- package/dist/components/floating.d.ts +5 -2
- package/dist/components/floating.js +2 -2
- package/dist/components/focus.d.ts +5 -0
- package/dist/components/focus.js +21 -1
- package/dist/editor/Editor.js +13 -3
- package/dist/editor/Monaco.d.ts +8 -1
- package/dist/editor/Monaco.js +16 -3
- package/dist/editor/editorModel.d.ts +1 -1
- package/dist/editor/editorModel.js +1 -1
- package/dist/editor/index.d.ts +1 -0
- package/dist/editor/index.js +2 -1
- package/dist/editor/languages.d.ts +24 -0
- package/dist/editor/languages.js +31 -0
- package/dist/editor/monacoFeatures.d.ts +7 -0
- package/dist/editor/monacoFeatures.js +19 -0
- package/dist/editor/themes.d.ts +5 -0
- package/dist/editor/themes.js +24 -4
- package/dist/editor/workers.d.ts +3 -3
- package/dist/editor/workers.js +8 -4
- package/dist/editor/yamlLanguage.js +55 -38
- package/dist/file-explorer/FileExplorer.d.ts +18 -4
- package/dist/file-explorer/FileExplorer.js +64 -20
- package/dist/file-explorer/FilePreview/Viewer.js +12 -8
- package/dist/file-explorer/FileRow.d.ts +6 -2
- package/dist/file-explorer/FileRow.js +35 -17
- package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
- package/dist/file-explorer/FileRowsWindow.js +2 -1
- package/dist/file-explorer/FileTable.d.ts +3 -1
- package/dist/file-explorer/FileTable.js +3 -2
- package/dist/file-explorer/TreeView.d.ts +5 -1
- package/dist/file-explorer/TreeView.js +49 -25
- package/dist/file-explorer/helpers.js +3 -3
- package/dist/file-explorer/lib/paging.d.ts +1 -0
- package/dist/file-explorer/lib/paging.js +5 -1
- package/dist/file-explorer/lib/utils.d.ts +3 -0
- package/dist/file-explorer/lib/utils.js +6 -1
- package/dist/form/Wizard/utils.js +2 -1
- package/dist/list/ActionToolbar.js +3 -2
- package/dist/list/ListPage.js +1 -1
- package/dist/list/ListTable.d.ts +5 -0
- package/dist/list/ListTable.js +6 -2
- package/dist/styles/theme.css +82 -62
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +5 -3
- package/dist/theme/derive.js +61 -3
- package/package.json +9 -2
- package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
- package/dist/file-explorer/FilePreview/CodePreviewEditor.js +0 -64
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AccessModel } from './model.js';
|
|
3
|
+
import type { AccessStrings } from './strings.js';
|
|
4
|
+
/** A removal waiting on the reader: one role, or every role the group holds. */
|
|
5
|
+
export interface PendingRemoval {
|
|
6
|
+
groupId: string;
|
|
7
|
+
roleIds: string[];
|
|
8
|
+
/** Every role the group holds, from its "Remove all access". */
|
|
9
|
+
all: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ListActions {
|
|
12
|
+
readOnly: boolean;
|
|
13
|
+
/** The grants being saved, by `key()`. */
|
|
14
|
+
saving: ReadonlySet<string>;
|
|
15
|
+
removal: PendingRemoval | undefined;
|
|
16
|
+
onAskRemove: (removal: PendingRemoval) => void;
|
|
17
|
+
onCancelRemove: () => void;
|
|
18
|
+
onRemove: (removal: PendingRemoval) => void;
|
|
19
|
+
onGrantMore: (groupId: string) => void;
|
|
20
|
+
/** The failed save's callout, and where it belongs: under the role or group it was about. */
|
|
21
|
+
failure: {
|
|
22
|
+
node: ReactNode;
|
|
23
|
+
roleId?: string | undefined;
|
|
24
|
+
groupId: string;
|
|
25
|
+
} | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare function HighImpact({ t }: {
|
|
28
|
+
t: AccessStrings;
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
/** Asks before a group loses roles, saying exactly what it loses and what it keeps. */
|
|
31
|
+
export declare function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t, }: {
|
|
32
|
+
model: AccessModel;
|
|
33
|
+
removal: PendingRemoval;
|
|
34
|
+
saving: boolean;
|
|
35
|
+
onCancel: () => void;
|
|
36
|
+
onConfirm: () => void;
|
|
37
|
+
t: AccessStrings;
|
|
38
|
+
}): import("react").JSX.Element | null;
|
|
39
|
+
/** Marks the button that asked for a removal, so focus can go back to it. */
|
|
40
|
+
export declare const removeKey: (removal: PendingRemoval) => string;
|
|
41
|
+
interface ViewProps {
|
|
42
|
+
model: AccessModel;
|
|
43
|
+
/** What the filter left: each role's groups, or each group's roles. */
|
|
44
|
+
shown: ReadonlyMap<string, readonly string[]>;
|
|
45
|
+
/** The rows in view, in order. */
|
|
46
|
+
order: readonly string[];
|
|
47
|
+
expanded: ReadonlySet<string>;
|
|
48
|
+
onExpandedChange: (id: string, open: boolean) => void;
|
|
49
|
+
filtering: boolean;
|
|
50
|
+
actions: ListActions;
|
|
51
|
+
t: AccessStrings;
|
|
52
|
+
sectionId: (id: string) => string;
|
|
53
|
+
}
|
|
54
|
+
/** Who has each role, role by role. */
|
|
55
|
+
export declare function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null)[];
|
|
56
|
+
/** Which roles each group has, group by group. */
|
|
57
|
+
export declare function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId, }: ViewProps): (import("react").JSX.Element | null)[];
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { AddIcon, ChevronRightIcon, OrganizationIcon, TrashIcon } from "../icons.js";
|
|
3
|
+
import { Loader } from "../components/Loader.js";
|
|
4
|
+
import { Button } from "../components/Button.js";
|
|
5
|
+
import { IconButton } from "../components/IconButton.js";
|
|
6
|
+
import { Callout } from "../components/Callout.js";
|
|
7
|
+
import { collapsePanelClassName } from "../components/Disclosure.js";
|
|
8
|
+
import { StatusBadge } from "../components/StatusBadge.js";
|
|
9
|
+
import { inheritedRoles, key, overlapOf, removalOf } from "./model.js";
|
|
10
|
+
import { GroupTile } from "./GroupPicker.js";
|
|
11
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { useEffect, useId, useRef } from "react";
|
|
13
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
14
|
+
//#region src/access/AccessList.tsx
|
|
15
|
+
var groupLabel = (group, t) => group.kind === "everyone" ? t.everyoneInOrganization : group.name;
|
|
16
|
+
var sizeOf = (group, t) => group.memberCount === void 0 ? void 0 : group.kind === "everyone" ? t.people(group.memberCount) : t.members(group.memberCount);
|
|
17
|
+
function HighImpact({ t }) {
|
|
18
|
+
return /* @__PURE__ */ jsx(StatusBadge, {
|
|
19
|
+
variant: "warning",
|
|
20
|
+
children: t.highImpact
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** A role or group whose rows fold under it. */
|
|
24
|
+
function Section({ id, open, onOpenChange, disabled, lead, title, description, aside, narrowAside, notice, children }) {
|
|
25
|
+
return /* @__PURE__ */ jsxs(Collapsible.Root, {
|
|
26
|
+
open: open && !disabled,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
disabled,
|
|
29
|
+
className: "border-b border-(--theme-border)",
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsxs(Collapsible.Trigger, {
|
|
32
|
+
id,
|
|
33
|
+
className: cx("group/section flex w-full items-center gap-3 px-4 py-3 text-left outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--theme-focus)", disabled ? "cursor-default" : "cursor-pointer hover:bg-(--theme-hover)"),
|
|
34
|
+
children: [
|
|
35
|
+
/* @__PURE__ */ jsx(ChevronRightIcon, {
|
|
36
|
+
"aria-hidden": "true",
|
|
37
|
+
className: cx("size-4 shrink-0 text-(--theme-muted-text-color) transition-transform group-data-panel-open/section:rotate-90 motion-reduce:transition-none", disabled && "invisible")
|
|
38
|
+
}),
|
|
39
|
+
lead,
|
|
40
|
+
/* @__PURE__ */ jsxs("span", {
|
|
41
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx("span", {
|
|
44
|
+
className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5",
|
|
45
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
46
|
+
className: "font-medium wrap-break-word",
|
|
47
|
+
children: title
|
|
48
|
+
})
|
|
49
|
+
}),
|
|
50
|
+
description && /* @__PURE__ */ jsx("span", {
|
|
51
|
+
className: "hidden text-sm text-(--theme-muted-text-color) @min-[30rem]/access:block",
|
|
52
|
+
children: description
|
|
53
|
+
}),
|
|
54
|
+
narrowAside && /* @__PURE__ */ jsx("span", {
|
|
55
|
+
className: "text-sm text-(--theme-muted-text-color) @min-[30rem]/access:hidden",
|
|
56
|
+
children: narrowAside
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
}),
|
|
60
|
+
aside && /* @__PURE__ */ jsx("span", {
|
|
61
|
+
className: "hidden shrink-0 items-center gap-1 text-sm text-(--theme-muted-text-color) @min-[30rem]/access:flex",
|
|
62
|
+
children: aside
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ jsx(Collapsible.Panel, {
|
|
67
|
+
className: collapsePanelClassName,
|
|
68
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
69
|
+
className: "pb-3",
|
|
70
|
+
children
|
|
71
|
+
})
|
|
72
|
+
}),
|
|
73
|
+
notice && /* @__PURE__ */ jsx("div", {
|
|
74
|
+
className: "px-4 pb-3",
|
|
75
|
+
children: notice
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** A line under a section: a group under a role, or a role under a group. */
|
|
81
|
+
function Row({ lead, title, meta, action, muted }) {
|
|
82
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
83
|
+
className: "flex items-center gap-3 py-1.5 ps-11 pe-4 @min-[30rem]/access:ps-11",
|
|
84
|
+
children: [
|
|
85
|
+
lead,
|
|
86
|
+
/* @__PURE__ */ jsxs("div", {
|
|
87
|
+
className: cx("min-w-0 flex-1", muted && "text-(--theme-muted-text-color)"),
|
|
88
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
89
|
+
className: "flex min-w-0 flex-wrap items-center gap-x-2",
|
|
90
|
+
children: title
|
|
91
|
+
}), meta && /* @__PURE__ */ jsx("div", {
|
|
92
|
+
className: "text-sm text-(--theme-muted-text-color)",
|
|
93
|
+
children: meta
|
|
94
|
+
})]
|
|
95
|
+
}),
|
|
96
|
+
action
|
|
97
|
+
]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function RemoveButton({ label, saving, removal, onAsk }) {
|
|
101
|
+
return saving ? /* @__PURE__ */ jsx("span", {
|
|
102
|
+
className: "flex size-7 items-center justify-center",
|
|
103
|
+
children: /* @__PURE__ */ jsx(Loader, {
|
|
104
|
+
inline: true,
|
|
105
|
+
size: "1em",
|
|
106
|
+
label
|
|
107
|
+
})
|
|
108
|
+
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
109
|
+
icon: /* @__PURE__ */ jsx(TrashIcon, {}),
|
|
110
|
+
label,
|
|
111
|
+
"data-remove": removeKey(removal),
|
|
112
|
+
onClick: () => onAsk(removal)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Asks before a group loses roles, saying exactly what it loses and what it keeps. */
|
|
116
|
+
function RemoveConfirm({ model, removal, saving, onCancel, onConfirm, t }) {
|
|
117
|
+
const titleId = useId();
|
|
118
|
+
const cancel = useRef(null);
|
|
119
|
+
useEffect(() => cancel.current?.focus(), []);
|
|
120
|
+
const group = model.groupById.get(removal.groupId);
|
|
121
|
+
if (!group) return null;
|
|
122
|
+
const everyone = group.kind === "everyone";
|
|
123
|
+
const roleNames = removal.roleIds.map((id) => model.roleById.get(id)?.name ?? id);
|
|
124
|
+
const impact = removalOf(model, removal.groupId, removal.roleIds);
|
|
125
|
+
const own = impact.keep.map((role) => role.name);
|
|
126
|
+
const throughEveryone = impact.keepThroughEveryone.map((role) => role.name);
|
|
127
|
+
return /* @__PURE__ */ jsx("fieldset", {
|
|
128
|
+
"aria-labelledby": titleId,
|
|
129
|
+
className: "m-0 min-w-0 border-0 px-4 py-2",
|
|
130
|
+
onKeyDown: (e) => {
|
|
131
|
+
if (e.key === "Escape" && !e.defaultPrevented) {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
onCancel();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
children: /* @__PURE__ */ jsxs(Callout, {
|
|
137
|
+
tone: "error",
|
|
138
|
+
icon: false,
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx("p", {
|
|
141
|
+
id: titleId,
|
|
142
|
+
className: "m-0 font-semibold text-(--theme-app)",
|
|
143
|
+
children: removal.all ? t.removeAllTitle(groupLabel(group, t)) : t.removeTitle(groupLabel(group, t), roleNames)
|
|
144
|
+
}),
|
|
145
|
+
/* @__PURE__ */ jsx("div", {
|
|
146
|
+
className: "mt-1 text-(--theme-app)",
|
|
147
|
+
children: impact.lost.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
148
|
+
/* @__PURE__ */ jsx("p", {
|
|
149
|
+
className: "m-0 text-(--theme-muted-text-color)",
|
|
150
|
+
children: t.losesIntro(group.memberCount, everyone)
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ jsx("ul", {
|
|
153
|
+
className: "my-2 grid list-disc gap-x-6 gap-y-1 ps-5 @min-[30rem]/access:grid-cols-2",
|
|
154
|
+
children: impact.lost.map((permission) => /* @__PURE__ */ jsx("li", { children: model.permissionLabel(permission) }, permission))
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ jsx("p", {
|
|
157
|
+
className: "m-0 text-(--theme-muted-text-color)",
|
|
158
|
+
children: t.keeps(own, throughEveryone)
|
|
159
|
+
})
|
|
160
|
+
] }) : /* @__PURE__ */ jsx("p", {
|
|
161
|
+
className: "m-0 text-(--theme-muted-text-color)",
|
|
162
|
+
children: t.losesNothing(own, throughEveryone)
|
|
163
|
+
})
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ jsxs("div", {
|
|
166
|
+
className: "mt-3 flex justify-end gap-2",
|
|
167
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
168
|
+
ref: cancel,
|
|
169
|
+
variant: "ghost",
|
|
170
|
+
onClick: onCancel,
|
|
171
|
+
disabled: saving,
|
|
172
|
+
children: t.common.cancel
|
|
173
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
174
|
+
variant: "danger",
|
|
175
|
+
loading: saving,
|
|
176
|
+
onClick: onConfirm,
|
|
177
|
+
children: removal.all ? t.removeAllAccess : t.confirmRemove(roleNames)
|
|
178
|
+
})]
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/** Marks the button that asked for a removal, so focus can go back to it. */
|
|
185
|
+
var removeKey = (removal) => `${removal.groupId}\u0000${removal.all ? "*" : removal.roleIds.join(",")}`;
|
|
186
|
+
var sameRemoval = (a, groupId, roleId) => a !== void 0 && a.groupId === groupId && (roleId === void 0 ? a.all : !a.all && a.roleIds.includes(roleId));
|
|
187
|
+
/** Who has each role, role by role. */
|
|
188
|
+
function ByRole({ model, shown, order, expanded, onExpandedChange, filtering, actions, t, sectionId }) {
|
|
189
|
+
return order.map((roleId) => {
|
|
190
|
+
const role = model.roleById.get(roleId);
|
|
191
|
+
if (!role) return null;
|
|
192
|
+
const holders = model.holders.get(roleId) ?? [];
|
|
193
|
+
const groupIds = shown.get(roleId) ?? [];
|
|
194
|
+
const everyone = holders.some((group) => group.kind === "everyone");
|
|
195
|
+
const others = holders.filter((group) => group.kind !== "everyone").length;
|
|
196
|
+
const count = filtering && groupIds.length !== holders.length ? t.groupsShown(groupIds.length, holders.length) : others > 0 || !everyone ? t.groups(everyone ? others : holders.length) : void 0;
|
|
197
|
+
const everyoneTag = everyone && /* @__PURE__ */ jsxs(StatusBadge, {
|
|
198
|
+
variant: "info",
|
|
199
|
+
size: "md",
|
|
200
|
+
children: [/* @__PURE__ */ jsx(OrganizationIcon, {
|
|
201
|
+
"aria-hidden": "true",
|
|
202
|
+
className: "size-3"
|
|
203
|
+
}), t.everyone]
|
|
204
|
+
});
|
|
205
|
+
const failure = actions.failure?.roleId === roleId ? actions.failure.node : void 0;
|
|
206
|
+
return /* @__PURE__ */ jsx(Section, {
|
|
207
|
+
id: sectionId(roleId),
|
|
208
|
+
open: expanded.has(roleId),
|
|
209
|
+
onOpenChange: (open) => onExpandedChange(roleId, open),
|
|
210
|
+
disabled: holders.length === 0,
|
|
211
|
+
notice: failure,
|
|
212
|
+
title: /* @__PURE__ */ jsxs(Fragment, { children: [role.name, role.highImpact && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(HighImpact, { t })] })] }),
|
|
213
|
+
description: role.description,
|
|
214
|
+
aside: /* @__PURE__ */ jsxs(Fragment, { children: [everyoneTag, count] }),
|
|
215
|
+
narrowAside: everyoneTag && !count ? /* @__PURE__ */ jsx("span", {
|
|
216
|
+
className: "mt-1 inline-flex",
|
|
217
|
+
children: everyoneTag
|
|
218
|
+
}) : count,
|
|
219
|
+
children: groupIds.map((groupId) => {
|
|
220
|
+
const group = model.groupById.get(groupId);
|
|
221
|
+
if (!group) return null;
|
|
222
|
+
if (sameRemoval(actions.removal, groupId, roleId) && actions.removal) return /* @__PURE__ */ jsx(RemoveConfirm, {
|
|
223
|
+
model,
|
|
224
|
+
removal: actions.removal,
|
|
225
|
+
saving: actions.saving.size > 0,
|
|
226
|
+
onCancel: actions.onCancelRemove,
|
|
227
|
+
onConfirm: () => actions.removal && actions.onRemove(actions.removal),
|
|
228
|
+
t
|
|
229
|
+
}, groupId);
|
|
230
|
+
const overlap = overlapOf(model, groupId, role);
|
|
231
|
+
const hint = overlap.coveredBy.length > 0 ? t.coveredBy(overlap.coveredBy.map((other) => other.name)) : overlap.everyone ? t.everyoneHasThisToo : void 0;
|
|
232
|
+
const label = t.removeRole(groupLabel(group, t), role.name);
|
|
233
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
234
|
+
lead: /* @__PURE__ */ jsx(GroupTile, {
|
|
235
|
+
everyone: group.kind === "everyone",
|
|
236
|
+
className: "hidden @min-[30rem]/access:flex"
|
|
237
|
+
}),
|
|
238
|
+
title: groupLabel(group, t),
|
|
239
|
+
meta: [sizeOf(group, t), hint].filter(Boolean).join(" · "),
|
|
240
|
+
action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
|
|
241
|
+
label,
|
|
242
|
+
saving: actions.saving.has(key({
|
|
243
|
+
groupId,
|
|
244
|
+
roleId
|
|
245
|
+
})),
|
|
246
|
+
removal: {
|
|
247
|
+
groupId,
|
|
248
|
+
roleIds: [roleId],
|
|
249
|
+
all: false
|
|
250
|
+
},
|
|
251
|
+
onAsk: actions.onAskRemove
|
|
252
|
+
})
|
|
253
|
+
}, groupId);
|
|
254
|
+
})
|
|
255
|
+
}, roleId);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/** Which roles each group has, group by group. */
|
|
259
|
+
function ByGroup({ model, shown, order, expanded, onExpandedChange, actions, t, sectionId }) {
|
|
260
|
+
return order.map((groupId) => {
|
|
261
|
+
const group = model.groupById.get(groupId);
|
|
262
|
+
if (!group) return null;
|
|
263
|
+
const held = model.held.get(groupId) ?? [];
|
|
264
|
+
const roleIds = new Set(shown.get(groupId) ?? held.map((role) => role.id));
|
|
265
|
+
const roles = held.filter((role) => roleIds.has(role.id));
|
|
266
|
+
const inherited = inheritedRoles(model, groupId).filter((role) => !held.some((own) => own.id === role.id));
|
|
267
|
+
const everyone = group.kind === "everyone";
|
|
268
|
+
const failure = actions.failure?.groupId === groupId ? actions.failure.node : void 0;
|
|
269
|
+
const badges = held.length <= 2 ? held : held.slice(0, 1);
|
|
270
|
+
const hidden = held.slice(badges.length);
|
|
271
|
+
const roleBadges = /* @__PURE__ */ jsxs(Fragment, { children: [badges.map((role) => /* @__PURE__ */ jsx(StatusBadge, {
|
|
272
|
+
size: "md",
|
|
273
|
+
variant: role.highImpact ? "warning" : "default",
|
|
274
|
+
children: role.name
|
|
275
|
+
}, role.id)), hidden.length > 0 && /* @__PURE__ */ jsxs(StatusBadge, {
|
|
276
|
+
size: "md",
|
|
277
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
278
|
+
"aria-hidden": "true",
|
|
279
|
+
children: t.moreRoles(hidden.length)
|
|
280
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
281
|
+
className: "sr-only",
|
|
282
|
+
children: t.moreRolesLabel(hidden.map((role) => role.name))
|
|
283
|
+
})]
|
|
284
|
+
})] });
|
|
285
|
+
return /* @__PURE__ */ jsx(Section, {
|
|
286
|
+
id: sectionId(groupId),
|
|
287
|
+
open: expanded.has(groupId),
|
|
288
|
+
onOpenChange: (open) => onExpandedChange(groupId, open),
|
|
289
|
+
lead: /* @__PURE__ */ jsx(GroupTile, {
|
|
290
|
+
everyone,
|
|
291
|
+
className: "hidden @min-[30rem]/access:flex"
|
|
292
|
+
}),
|
|
293
|
+
notice: failure,
|
|
294
|
+
title: groupLabel(group, t),
|
|
295
|
+
description: everyone ? t.everyoneJoinLater : sizeOf(group, t),
|
|
296
|
+
aside: roleBadges,
|
|
297
|
+
narrowAside: /* @__PURE__ */ jsx("span", {
|
|
298
|
+
className: "mt-1 flex flex-wrap gap-1",
|
|
299
|
+
children: roleBadges
|
|
300
|
+
}),
|
|
301
|
+
children: sameRemoval(actions.removal, groupId) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
|
|
302
|
+
model,
|
|
303
|
+
removal: actions.removal,
|
|
304
|
+
saving: actions.saving.size > 0,
|
|
305
|
+
onCancel: actions.onCancelRemove,
|
|
306
|
+
onConfirm: () => actions.removal && actions.onRemove(actions.removal),
|
|
307
|
+
t
|
|
308
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
309
|
+
roles.map((role) => sameRemoval(actions.removal, groupId, role.id) && actions.removal ? /* @__PURE__ */ jsx(RemoveConfirm, {
|
|
310
|
+
model,
|
|
311
|
+
removal: actions.removal,
|
|
312
|
+
saving: actions.saving.size > 0,
|
|
313
|
+
onCancel: actions.onCancelRemove,
|
|
314
|
+
onConfirm: () => actions.removal && actions.onRemove(actions.removal),
|
|
315
|
+
t
|
|
316
|
+
}, role.id) : /* @__PURE__ */ jsx(RoleRow, {
|
|
317
|
+
model,
|
|
318
|
+
group,
|
|
319
|
+
role,
|
|
320
|
+
actions,
|
|
321
|
+
t
|
|
322
|
+
}, role.id)),
|
|
323
|
+
inherited.map((role) => /* @__PURE__ */ jsx(Row, {
|
|
324
|
+
muted: true,
|
|
325
|
+
title: role.name,
|
|
326
|
+
meta: t.throughEveryone
|
|
327
|
+
}, role.id)),
|
|
328
|
+
!actions.readOnly && /* @__PURE__ */ jsxs("div", {
|
|
329
|
+
className: "flex flex-wrap items-center gap-2 ps-11 pe-4 pt-2",
|
|
330
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
331
|
+
variant: "secondary",
|
|
332
|
+
size: "sm",
|
|
333
|
+
icon: /* @__PURE__ */ jsx(AddIcon, {}),
|
|
334
|
+
onClick: () => actions.onGrantMore(groupId),
|
|
335
|
+
children: t.grantMoreRoles
|
|
336
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
337
|
+
variant: "ghost",
|
|
338
|
+
size: "sm",
|
|
339
|
+
"data-remove": removeKey({
|
|
340
|
+
groupId,
|
|
341
|
+
roleIds: [],
|
|
342
|
+
all: true
|
|
343
|
+
}),
|
|
344
|
+
className: "text-(--theme-error-text)",
|
|
345
|
+
onClick: () => actions.onAskRemove({
|
|
346
|
+
groupId,
|
|
347
|
+
roleIds: held.map((role) => role.id),
|
|
348
|
+
all: true
|
|
349
|
+
}),
|
|
350
|
+
children: t.removeAllAccess
|
|
351
|
+
})]
|
|
352
|
+
})
|
|
353
|
+
] })
|
|
354
|
+
}, groupId);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
function RoleRow({ model, group, role, actions, t }) {
|
|
358
|
+
const overlap = overlapOf(model, group.id, role);
|
|
359
|
+
const meta = overlap.coveredBy.length > 0 ? t.redundant(overlap.coveredBy.map((other) => other.name)) : role.description;
|
|
360
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
361
|
+
title: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
362
|
+
className: "font-medium",
|
|
363
|
+
children: role.name
|
|
364
|
+
}), role.highImpact && /* @__PURE__ */ jsx(HighImpact, { t })] }),
|
|
365
|
+
meta,
|
|
366
|
+
action: !actions.readOnly && /* @__PURE__ */ jsx(RemoveButton, {
|
|
367
|
+
label: t.removeRole(groupLabel(group, t), role.name),
|
|
368
|
+
saving: actions.saving.has(key({
|
|
369
|
+
groupId: group.id,
|
|
370
|
+
roleId: role.id
|
|
371
|
+
})),
|
|
372
|
+
removal: {
|
|
373
|
+
groupId: group.id,
|
|
374
|
+
roleIds: [role.id],
|
|
375
|
+
all: false
|
|
376
|
+
},
|
|
377
|
+
onAsk: actions.onAskRemove
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
export { ByGroup, ByRole, HighImpact, RemoveConfirm, removeKey };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type DescribeGroup, type DetailsCache, type SearchGroups } from './GroupPicker.js';
|
|
3
|
+
import { type AccessModel } from './model.js';
|
|
4
|
+
import type { AccessStrings } from './strings.js';
|
|
5
|
+
import type { AccessChange, AccessGroup } from './types.js';
|
|
6
|
+
export interface GrantSelection {
|
|
7
|
+
groupIds: string[];
|
|
8
|
+
roleIds: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The grant form's body and footer, which the drawer lays out in its own body and footer. */
|
|
11
|
+
export declare function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t, }: {
|
|
12
|
+
model: AccessModel;
|
|
13
|
+
selection: GrantSelection;
|
|
14
|
+
onSelectionChange: (next: GrantSelection) => void;
|
|
15
|
+
resourceName: string;
|
|
16
|
+
describeGroup?: DescribeGroup | undefined;
|
|
17
|
+
searchGroups?: SearchGroups | undefined;
|
|
18
|
+
groupCount?: number | undefined;
|
|
19
|
+
recentGroupIds?: readonly string[] | undefined;
|
|
20
|
+
cache: DetailsCache;
|
|
21
|
+
seen: Map<string, AccessGroup>;
|
|
22
|
+
saving: boolean;
|
|
23
|
+
/** The failed grant's callout, which stays in the form so the choices can be retried. */
|
|
24
|
+
failure: ReactNode;
|
|
25
|
+
onGrant: (change: AccessChange) => void;
|
|
26
|
+
onCancel: () => void;
|
|
27
|
+
/** Sends focus somewhere once the change it goes with has rendered. */
|
|
28
|
+
focusLater: (find: () => HTMLElement | null | undefined) => void;
|
|
29
|
+
/** The drawer's body, above its footer, which the open group list fills. */
|
|
30
|
+
boundary: Element | null;
|
|
31
|
+
t: AccessStrings;
|
|
32
|
+
}): {
|
|
33
|
+
body: ReactNode;
|
|
34
|
+
footer: ReactNode;
|
|
35
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Button } from "../components/Button.js";
|
|
2
|
+
import { Callout } from "../components/Callout.js";
|
|
3
|
+
import { Combobox } from "../components/Combobox.js";
|
|
4
|
+
import { StatusBadge } from "../components/StatusBadge.js";
|
|
5
|
+
import { covers, grantPlan, overlapOf } from "./model.js";
|
|
6
|
+
import { GroupPicker } from "./GroupPicker.js";
|
|
7
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useId, useRef, useState } from "react";
|
|
9
|
+
//#region src/access/GrantView.tsx
|
|
10
|
+
/** The grant form's body and footer, which the drawer lays out in its own body and footer. */
|
|
11
|
+
function useGrantView({ model, selection, onSelectionChange, resourceName, describeGroup, searchGroups, groupCount, recentGroupIds, cache, seen, saving, failure, onGrant, onCancel, focusLater, boundary, t }) {
|
|
12
|
+
const uid = useId();
|
|
13
|
+
const [confirming, setConfirming] = useState(false);
|
|
14
|
+
const keepButton = useRef(null);
|
|
15
|
+
const grantId = `${uid}grant`;
|
|
16
|
+
const { groupIds, roleIds } = selection;
|
|
17
|
+
const roles = roleIds.map((id) => model.roleById.get(id)).filter((role) => role !== void 0);
|
|
18
|
+
const roleNames = roles.map((role) => role.name);
|
|
19
|
+
const groupName = (id) => {
|
|
20
|
+
const group = seen.get(id);
|
|
21
|
+
return group?.kind === "everyone" ? t.everyone : group?.name ?? id;
|
|
22
|
+
};
|
|
23
|
+
const plan = grantPlan(model, groupIds, roleIds);
|
|
24
|
+
const everyone = model.everyone;
|
|
25
|
+
const toEveryone = everyone !== void 0 && groupIds.includes(everyone.id);
|
|
26
|
+
const people = everyone?.memberCount;
|
|
27
|
+
const roleItems = model.roles.map((role) => {
|
|
28
|
+
const chosen = roleIds.filter((id) => id !== role.id).map((id) => model.roleById.get(id)).filter((other) => other !== void 0);
|
|
29
|
+
const holders = groupIds.filter((id) => (model.held.get(id) ?? []).some((held) => held.id === role.id));
|
|
30
|
+
const coveredBy = chosen.filter((other) => covers(other, role)).map((r) => r.name);
|
|
31
|
+
const everyoneHas = everyone !== void 0 && !groupIds.includes(everyone.id) && (overlapOf(model, everyone.id, role).coveredBy.length > 0 || (model.held.get(everyone.id) ?? []).some((held) => held.id === role.id));
|
|
32
|
+
const hints = [
|
|
33
|
+
holders.length > 0 && t.alreadyHave(holders.map(groupName)),
|
|
34
|
+
coveredBy.length > 0 && t.alreadyCovered(coveredBy),
|
|
35
|
+
everyoneHas && t.everyoneHasIt
|
|
36
|
+
].filter((hint) => typeof hint === "string");
|
|
37
|
+
return {
|
|
38
|
+
value: role.id,
|
|
39
|
+
label: role.name,
|
|
40
|
+
description: [role.description, ...hints].filter(Boolean).join(" "),
|
|
41
|
+
badge: role.highImpact ? /* @__PURE__ */ jsx(StatusBadge, {
|
|
42
|
+
variant: "warning",
|
|
43
|
+
children: t.highImpact
|
|
44
|
+
}) : null
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const getting = [...new Set(plan.grant.map((grant) => grant.groupId))].map(groupName);
|
|
48
|
+
const status = groupIds.length === 0 && roleIds.length === 0 ? {
|
|
49
|
+
text: t.pickGroupsAndRoles,
|
|
50
|
+
ready: false
|
|
51
|
+
} : groupIds.length === 0 ? {
|
|
52
|
+
text: t.pickGroups,
|
|
53
|
+
ready: false
|
|
54
|
+
} : roleIds.length === 0 ? {
|
|
55
|
+
text: t.pickRoles,
|
|
56
|
+
ready: false
|
|
57
|
+
} : plan.grant.length === 0 ? {
|
|
58
|
+
text: t.nothingToGrant(groupIds.map(groupName), roleNames),
|
|
59
|
+
ready: false
|
|
60
|
+
} : {
|
|
61
|
+
text: t.grantSummary(getting, [...new Set(plan.grant.map((grant) => grant.roleId))].map((id) => model.roleById.get(id)?.name ?? id)),
|
|
62
|
+
ready: true
|
|
63
|
+
};
|
|
64
|
+
const grant = () => {
|
|
65
|
+
setConfirming(false);
|
|
66
|
+
onGrant({
|
|
67
|
+
grant: plan.grant,
|
|
68
|
+
revoke: []
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const statusId = `${uid}status`;
|
|
72
|
+
const body = /* @__PURE__ */ jsxs("div", {
|
|
73
|
+
className: "flex flex-col gap-5 p-4",
|
|
74
|
+
children: [
|
|
75
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
76
|
+
htmlFor: `${uid}groups`,
|
|
77
|
+
className: "mb-1.5 block text-sm font-medium",
|
|
78
|
+
children: t.groupsLabel
|
|
79
|
+
}), /* @__PURE__ */ jsx(GroupPicker, {
|
|
80
|
+
id: `${uid}groups`,
|
|
81
|
+
describedBy: `${uid}groupsHint`,
|
|
82
|
+
model,
|
|
83
|
+
value: groupIds,
|
|
84
|
+
onValueChange: (next) => onSelectionChange({
|
|
85
|
+
groupIds: next,
|
|
86
|
+
roleIds
|
|
87
|
+
}),
|
|
88
|
+
granting: roles,
|
|
89
|
+
resourceName,
|
|
90
|
+
describeGroup,
|
|
91
|
+
searchGroups,
|
|
92
|
+
groupCount,
|
|
93
|
+
recentGroupIds,
|
|
94
|
+
cache,
|
|
95
|
+
seen,
|
|
96
|
+
boundary,
|
|
97
|
+
t
|
|
98
|
+
})] }),
|
|
99
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
100
|
+
htmlFor: `${uid}roles`,
|
|
101
|
+
className: "mb-1.5 block text-sm font-medium",
|
|
102
|
+
children: t.rolesLabel
|
|
103
|
+
}), /* @__PURE__ */ jsx(Combobox, {
|
|
104
|
+
multiple: true,
|
|
105
|
+
id: `${uid}roles`,
|
|
106
|
+
items: roleItems,
|
|
107
|
+
value: roleIds,
|
|
108
|
+
onValueChange: (next) => onSelectionChange({
|
|
109
|
+
groupIds,
|
|
110
|
+
roleIds: next
|
|
111
|
+
}),
|
|
112
|
+
placeholder: t.searchRoles
|
|
113
|
+
})] }),
|
|
114
|
+
toEveryone && roles.length > 0 && /* @__PURE__ */ jsx(Callout, {
|
|
115
|
+
tone: "warning",
|
|
116
|
+
title: t.everyoneWarningTitle(roleNames, people),
|
|
117
|
+
children: t.everyoneWarning(resourceName)
|
|
118
|
+
}),
|
|
119
|
+
failure
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
const stopConfirming = () => {
|
|
123
|
+
setConfirming(false);
|
|
124
|
+
focusLater(() => document.getElementById(grantId));
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
body,
|
|
128
|
+
footer: confirming ? /* @__PURE__ */ jsxs("fieldset", {
|
|
129
|
+
"aria-labelledby": statusId,
|
|
130
|
+
className: "m-0 flex w-full min-w-0 flex-wrap items-center gap-x-4 gap-y-2 border-0 p-0",
|
|
131
|
+
onKeyDown: (e) => {
|
|
132
|
+
if (e.key === "Escape") {
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
stopConfirming();
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
138
|
+
id: statusId,
|
|
139
|
+
className: "m-0 min-w-0 flex-1 basis-48 text-sm font-medium",
|
|
140
|
+
children: t.confirmEveryoneTitle(roleNames, people)
|
|
141
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
142
|
+
className: "ms-auto flex shrink-0 gap-2",
|
|
143
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
144
|
+
ref: keepButton,
|
|
145
|
+
variant: "secondary",
|
|
146
|
+
onClick: stopConfirming,
|
|
147
|
+
disabled: saving,
|
|
148
|
+
children: t.common.cancel
|
|
149
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
150
|
+
variant: roles.some((role) => role.highImpact) ? "danger" : "primary",
|
|
151
|
+
loading: saving,
|
|
152
|
+
onClick: grant,
|
|
153
|
+
children: t.grantToEveryone
|
|
154
|
+
})]
|
|
155
|
+
})]
|
|
156
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
157
|
+
className: "flex w-full flex-wrap items-center gap-x-4 gap-y-2",
|
|
158
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
159
|
+
id: statusId,
|
|
160
|
+
className: "m-0 min-w-0 flex-1 basis-48 text-sm text-(--theme-muted-text-color)",
|
|
161
|
+
children: status.text
|
|
162
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
163
|
+
className: "ms-auto flex shrink-0 gap-2",
|
|
164
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
165
|
+
variant: "secondary",
|
|
166
|
+
onClick: onCancel,
|
|
167
|
+
children: t.common.cancel
|
|
168
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
169
|
+
id: grantId,
|
|
170
|
+
variant: "primary",
|
|
171
|
+
focusableWhenDisabled: true,
|
|
172
|
+
disabled: !status.ready,
|
|
173
|
+
"aria-describedby": statusId,
|
|
174
|
+
loading: saving,
|
|
175
|
+
onClick: () => {
|
|
176
|
+
if (toEveryone) {
|
|
177
|
+
setConfirming(true);
|
|
178
|
+
focusLater(() => keepButton.current);
|
|
179
|
+
} else grant();
|
|
180
|
+
},
|
|
181
|
+
children: toEveryone ? t.grantToEveryone : t.grant(roleNames)
|
|
182
|
+
})]
|
|
183
|
+
})]
|
|
184
|
+
})
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
export { useGrantView };
|