@gooddata/sdk-ui-ext 11.43.0-alpha.1 → 11.43.0-alpha.2

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.
Files changed (41) hide show
  1. package/NOTICE +3 -3
  2. package/esm/index.d.ts +4 -0
  3. package/esm/index.d.ts.map +1 -1
  4. package/esm/index.js +2 -0
  5. package/esm/internal/components/configurationControls/customTooltip/CustomTooltipSection.d.ts.map +1 -1
  6. package/esm/internal/components/configurationControls/customTooltip/CustomTooltipSection.js +3 -2
  7. package/esm/internal/translations/en-US.localization-bundle.d.ts +44 -0
  8. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  9. package/esm/internal/translations/en-US.localization-bundle.js +44 -0
  10. package/esm/sdk-ui-ext.d.ts +274 -0
  11. package/esm/share/ObjectShareDialog.d.ts +67 -0
  12. package/esm/share/ObjectShareDialog.d.ts.map +1 -0
  13. package/esm/share/ObjectShareDialog.js +102 -0
  14. package/esm/share/accessSummary.d.ts +20 -0
  15. package/esm/share/accessSummary.d.ts.map +1 -0
  16. package/esm/share/accessSummary.js +26 -0
  17. package/esm/share/messages.d.ts +44 -0
  18. package/esm/share/messages.d.ts.map +1 -0
  19. package/esm/share/messages.js +23 -0
  20. package/esm/share/objectShareController.helpers.d.ts +80 -0
  21. package/esm/share/objectShareController.helpers.d.ts.map +1 -0
  22. package/esm/share/objectShareController.helpers.js +152 -0
  23. package/esm/share/objectShareController.types.d.ts +110 -0
  24. package/esm/share/objectShareController.types.d.ts.map +1 -0
  25. package/esm/share/objectShareController.types.js +2 -0
  26. package/esm/share/types.d.ts +47 -0
  27. package/esm/share/types.d.ts.map +1 -0
  28. package/esm/share/types.js +2 -0
  29. package/esm/share/useAccessList.d.ts +51 -0
  30. package/esm/share/useAccessList.d.ts.map +1 -0
  31. package/esm/share/useAccessList.js +263 -0
  32. package/esm/share/useLabelScope.d.ts +54 -0
  33. package/esm/share/useLabelScope.d.ts.map +1 -0
  34. package/esm/share/useLabelScope.js +160 -0
  35. package/esm/share/useObjectShare.d.ts +50 -0
  36. package/esm/share/useObjectShare.d.ts.map +1 -0
  37. package/esm/share/useObjectShare.js +17 -0
  38. package/esm/share/useObjectShareController.d.ts +24 -0
  39. package/esm/share/useObjectShareController.d.ts.map +1 -0
  40. package/esm/share/useObjectShareController.js +347 -0
  41. package/package.json +21 -21
@@ -0,0 +1,102 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { UiAddGranteeDialog, UiConfirmDialog, UiGranteeRowControls, UiObjectShareDialog, } from "@gooddata/sdk-ui-kit";
6
+ import { objectShareMessages } from "./messages.js";
7
+ import { useObjectShare } from "./useObjectShare.js";
8
+ /**
9
+ * Connected share dialog. Renders the main share modal, the add-grantee
10
+ * sub-dialog and the general-access confirm dialog, all driven by one
11
+ * controller. Each underlying dialog manages its own open state, so they can
12
+ * be safely co-mounted; focus, backdrop and portal lifecycles are owned by
13
+ * `UiModalDialog` inside the kit.
14
+ *
15
+ * Mount unconditionally and toggle via `isOpen`; the contents stay invisible
16
+ * while closed. For the common case pass plain props and let the dialog own its
17
+ * controller:
18
+ *
19
+ * ```tsx
20
+ * <ObjectShareDialog target={ref} objectTitle={title} isOpen={open} onClose={close} />
21
+ * ```
22
+ *
23
+ * @internal
24
+ */
25
+ export function ObjectShareDialog({ target, objectTitle, isOpen, onClose, onSaved, labels, labelsLoading, labelsError, controller, }) {
26
+ const intl = useIntl();
27
+ // Own a controller unless the consumer injects one. The hook must run
28
+ // unconditionally (rules of hooks), so when a controller is injected we pass
29
+ // it an undefined target — it then no-ops and never duplicates the fetch the
30
+ // injected controller already owns.
31
+ const ownController = useObjectShare(controller ? undefined : target, {
32
+ onSaved,
33
+ labels,
34
+ labelsError,
35
+ labelsLoading,
36
+ });
37
+ const { state, actions } = controller ?? ownController;
38
+ const handleClose = useCallback(() => {
39
+ actions.reset();
40
+ onClose();
41
+ }, [actions, onClose]);
42
+ const isAddGranteeOpen = isOpen && state.subview === "addGrantee";
43
+ const isShareOpen = isOpen && state.subview !== "addGrantee";
44
+ const isConfirmOpen = isOpen && !!state.pendingGeneralAccess;
45
+ // Don't allow mutations until the access list has loaded: before then the
46
+ // controller reports empty grantees + RESTRICTED, which is a placeholder, not
47
+ // the real state. Acting on it would write from a false assumption. Mutations
48
+ // are also gated until per-label scope resolves (`labelsResolved`): a new
49
+ // grantee defaults to all labels, and any access change before resolution
50
+ // would diff against an assumed-full scope (skipping grants / revoking the
51
+ // wrong labels). `labelsResolved` is false while labels are still loading and
52
+ // when their fetch failed, so a pending/failed label set blocks every
53
+ // access-changing control here — Add, row controls and general access alike.
54
+ const isLoaded = state.status === "success";
55
+ const isMutable = isLoaded && state.labelsResolved;
56
+ const isAddDisabled = !isMutable;
57
+ // Map the controller's labels to the picker's item shape; the primary label
58
+ // is rendered locked (always selected, can't be unchecked).
59
+ const labelItems = state.labels.map((l) => ({
60
+ id: l.id,
61
+ label: l.title,
62
+ kind: l.isPrimary ? "primary" : undefined,
63
+ locked: l.isPrimary,
64
+ }));
65
+ return (_jsxs(_Fragment, { children: [_jsx(UiObjectShareDialog, { isOpen: isShareOpen, objectTitle: objectTitle, onClose: handleClose, grantees: state.grantees.map((g) => ({
66
+ id: g.id,
67
+ kind: g.kind,
68
+ name: g.name,
69
+ email: g.email,
70
+ isPending: g.pending !== undefined,
71
+ controls: (_jsx(UiGranteeRowControls, { labels: labelItems, selectedLabelIds: state.selectedLabelIdsByGrantee[g.id] ?? state.labels.map((l) => l.id), permissionLevel: g.level, effectivePermission: g.effectivePermission,
72
+ // Disable row controls while saving, and until per-label
73
+ // scope resolves (labels still loading, failed, or the
74
+ // probe in flight) — removing or re-scoping before then
75
+ // would diff against the "assume all"/empty placeholder
76
+ // and silently orphan real per-label grants.
77
+ isDisabled: g.pending !== undefined || !state.labelsResolved, onLabelsChange: (selectedIds) => {
78
+ void actions.changeGranteeLabels(g.id, selectedIds);
79
+ }, onPermissionChange: (level) => {
80
+ void actions.changePermissionLevel(g.id, level);
81
+ }, onRemoveAccess: () => {
82
+ void actions.removeGrantee(g.id);
83
+ } })),
84
+ })), onAddClick: actions.openAddGrantee, isAddDisabled: isAddDisabled, generalAccess: state.generalAccess, onGeneralAccessChange: actions.requestGeneralAccessChange,
85
+ // Gated on the same condition as Add: changing general access also
86
+ // mirrors the label scope, so it must wait for resolution and stay
87
+ // disabled when label metadata failed to load.
88
+ isGeneralAccessDisabled: !isMutable,
89
+ // On a failed load the empty grantee list + RESTRICTED radio are a
90
+ // placeholder, not the real policy — show why instead of letting it
91
+ // read as "no one has access".
92
+ error: state.status === "error" ? intl.formatMessage(objectShareMessages.loadError) : undefined }), _jsx(UiAddGranteeDialog, { isOpen: isAddGranteeOpen, objectTitle: objectTitle, loadOptions: actions.loadOptions, selectedGrantees: state.pendingGrantees, onSelectedGranteesChange: actions.setPendingGrantees, onBack: actions.closeAddGrantee, onClose: handleClose, onCancel: actions.closeAddGrantee, onShare: () => {
93
+ void actions.confirmAddGrantees();
94
+ } }), _jsx(UiConfirmDialog, { isOpen: isConfirmOpen, title: intl.formatMessage(state.pendingGeneralAccess === "RESTRICTED"
95
+ ? objectShareMessages.confirmRestrictTitle
96
+ : objectShareMessages.confirmGrantWorkspaceTitle), description: intl.formatMessage(state.pendingGeneralAccess === "RESTRICTED"
97
+ ? objectShareMessages.confirmRestrictDescription
98
+ : objectShareMessages.confirmGrantWorkspaceDescription, { title: objectTitle }), confirmLabel: intl.formatMessage(objectShareMessages.confirmButton), confirmVariant: "primary", onCancel: actions.cancelGeneralAccessChange, onClose: actions.cancelGeneralAccessChange, onConfirm: () => {
99
+ void actions.confirmGeneralAccessChange();
100
+ } })
101
+ ] }));
102
+ }
@@ -0,0 +1,20 @@
1
+ import type { AccessGranteeDetail, IGranularRulesAccess } from "@gooddata/sdk-model";
2
+ import type { GeneralAccessValue } from "@gooddata/sdk-ui-kit";
3
+ /**
4
+ * Find the `allWorkspaceUsers` rule grant if one exists. Its presence with
5
+ * non-empty `permissions` is the single signal that distinguishes WORKSPACE
6
+ * from RESTRICTED general access.
7
+ */
8
+ export declare function findAllWorkspaceUsersGrant(grants: AccessGranteeDetail[]): IGranularRulesAccess | undefined;
9
+ export declare function deriveGeneralAccess(grants: AccessGranteeDetail[]): GeneralAccessValue;
10
+ /**
11
+ * Workspace-wide permission level when the rule grant exists. Defaults to
12
+ * VIEW; promoted to SHARE only when the rule explicitly permits SHARE. EDIT
13
+ * is intentionally not surfaced — the UI caps at VIEW/SHARE.
14
+ */
15
+ export declare function deriveWorkspacePermissionLevel(grants: AccessGranteeDetail[]): "VIEW" | "SHARE";
16
+ /**
17
+ * Count of named grantees (users + user groups). The `allWorkspaceUsers` rule
18
+ * grant is excluded.
19
+ */
20
+ //# sourceMappingURL=accessSummary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessSummary.d.ts","sourceRoot":"","sources":["../../src/share/accessSummary.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,oBAAoB,GAAG,SAAS,CAE1G;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,kBAAkB,CAGrF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,MAAM,GAAG,OAAO,CAG9F;AAED;;;GAGG"}
@@ -0,0 +1,26 @@
1
+ // (C) 2026 GoodData Corporation
2
+ /**
3
+ * Find the `allWorkspaceUsers` rule grant if one exists. Its presence with
4
+ * non-empty `permissions` is the single signal that distinguishes WORKSPACE
5
+ * from RESTRICTED general access.
6
+ */
7
+ export function findAllWorkspaceUsersGrant(grants) {
8
+ return grants.find((g) => g.type === "allWorkspaceUsers");
9
+ }
10
+ export function deriveGeneralAccess(grants) {
11
+ const rule = findAllWorkspaceUsersGrant(grants);
12
+ return rule && rule.permissions.length > 0 ? "WORKSPACE" : "RESTRICTED";
13
+ }
14
+ /**
15
+ * Workspace-wide permission level when the rule grant exists. Defaults to
16
+ * VIEW; promoted to SHARE only when the rule explicitly permits SHARE. EDIT
17
+ * is intentionally not surfaced — the UI caps at VIEW/SHARE.
18
+ */
19
+ export function deriveWorkspacePermissionLevel(grants) {
20
+ const rule = findAllWorkspaceUsersGrant(grants);
21
+ return rule?.permissions.includes("SHARE") ? "SHARE" : "VIEW";
22
+ }
23
+ /**
24
+ * Count of named grantees (users + user groups). The `allWorkspaceUsers` rule
25
+ * grant is excluded.
26
+ */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Messages owned by the connected `ObjectShareDialog` and its hooks. Strings
3
+ * for the consumer's launcher button or inline access row live in the
4
+ * consumer's own bundle (see e.g. `analyticsCatalog.share.*` in
5
+ * `@gooddata/sdk-ui-catalog`).
6
+ *
7
+ * @internal
8
+ */
9
+ export declare const objectShareMessages: {
10
+ confirmRestrictTitle: {
11
+ id: string;
12
+ };
13
+ confirmRestrictDescription: {
14
+ id: string;
15
+ };
16
+ confirmGrantWorkspaceTitle: {
17
+ id: string;
18
+ };
19
+ confirmGrantWorkspaceDescription: {
20
+ id: string;
21
+ };
22
+ confirmButton: {
23
+ id: string;
24
+ };
25
+ toastGranteeAdded: {
26
+ id: string;
27
+ };
28
+ toastAccessUpdated: {
29
+ id: string;
30
+ };
31
+ toastGeneralAccessUpdated: {
32
+ id: string;
33
+ };
34
+ toastError: {
35
+ id: string;
36
+ };
37
+ toastLabelScopePartial: {
38
+ id: string;
39
+ };
40
+ loadError: {
41
+ id: string;
42
+ };
43
+ };
44
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/share/messages.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAY9B,CAAC"}
@@ -0,0 +1,23 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { defineMessages } from "react-intl";
3
+ /**
4
+ * Messages owned by the connected `ObjectShareDialog` and its hooks. Strings
5
+ * for the consumer's launcher button or inline access row live in the
6
+ * consumer's own bundle (see e.g. `analyticsCatalog.share.*` in
7
+ * `@gooddata/sdk-ui-catalog`).
8
+ *
9
+ * @internal
10
+ */
11
+ export const objectShareMessages = defineMessages({
12
+ confirmRestrictTitle: { id: "objectShare.confirm.restrict.title" },
13
+ confirmRestrictDescription: { id: "objectShare.confirm.restrict.description" },
14
+ confirmGrantWorkspaceTitle: { id: "objectShare.confirm.grantWorkspace.title" },
15
+ confirmGrantWorkspaceDescription: { id: "objectShare.confirm.grantWorkspace.description" },
16
+ confirmButton: { id: "objectShare.confirm.button" },
17
+ toastGranteeAdded: { id: "objectShare.toast.granteeAdded" },
18
+ toastAccessUpdated: { id: "objectShare.toast.accessUpdated" },
19
+ toastGeneralAccessUpdated: { id: "objectShare.toast.generalAccessUpdated" },
20
+ toastError: { id: "objectShare.toast.error" },
21
+ toastLabelScopePartial: { id: "objectShare.toast.labelScopePartial" },
22
+ loadError: { id: "objectShare.loadError" },
23
+ });
@@ -0,0 +1,80 @@
1
+ import { type IAvailableAccessGrantee, type IGranularAccessGrantee, type IObjectAccessList, type ObjRef } from "@gooddata/sdk-model";
2
+ import type { IObjectShareGrantee, ObjectSharePermissionLevel } from "./objectShareController.types.js";
3
+ import type { IObjectShareLabel } from "./types.js";
4
+ /** Stable row id shared by grantee rows and picker options: `user:<ref>` / `group:<ref>`. */
5
+ export declare function granteeId(kind: "user" | "group", ref: ObjRef): string;
6
+ /** Case-insensitive match of an assignee against the picker query (name, or email for users). */
7
+ export declare function assigneeMatchesQuery(assignee: IAvailableAccessGrantee, query: string): boolean;
8
+ /** The row's directly-granted level. SHARE outranks VIEW. */
9
+ export declare function directLevel(permissions: readonly string[]): ObjectSharePermissionLevel;
10
+ /**
11
+ * The effective permission to surface as a warning, or undefined when the direct
12
+ * grant already covers it. Set only when the grantee *inherits* SHARE (e.g. via a
13
+ * group) but is directly granted just VIEW — i.e. the effective access is higher
14
+ * than what the row's permission control shows.
15
+ */
16
+ export declare function effectivePermissionAbove(direct: ObjectSharePermissionLevel, inheritedPermissions: readonly string[]): ObjectSharePermissionLevel | undefined;
17
+ export declare function granteesFromAccessList(list: IObjectAccessList | undefined): IObjectShareGrantee[];
18
+ /**
19
+ * Optimistic overlay entry for a single grantee id. `"set"` carries the intended
20
+ * row (a fresh add or a level change); `"remove"` marks a removal. `pending` is
21
+ * true while the backend write is in flight.
22
+ */
23
+ export type IGranteeOverlayEntry = {
24
+ op: "set";
25
+ grantee: IObjectShareGrantee;
26
+ pending: boolean;
27
+ } | {
28
+ op: "remove";
29
+ pending: boolean;
30
+ };
31
+ /**
32
+ * Merge the committed rows with the optimistic overlay: `set` replaces/inserts a
33
+ * row (annotated pending), `remove` drops it. Entries with no committed effect
34
+ * still apply so the UI reflects the user's intent during read-after-write lag.
35
+ */
36
+ export declare function mergeOverlay(committed: IObjectShareGrantee[], overlay: Record<string, IGranteeOverlayEntry>): IObjectShareGrantee[];
37
+ /**
38
+ * Drop overlay entries the freshly-fetched list now confirms: a `set` whose
39
+ * committed level matches the intended level, or a `remove` whose grantee is
40
+ * gone. Unconfirmed entries are kept (server still lagging) but lose `pending`
41
+ * so the row shows the intended value without a spinner.
42
+ */
43
+ export declare function reconcileOverlay(committed: IObjectShareGrantee[], overlay: Record<string, IGranteeOverlayEntry>): Record<string, IGranteeOverlayEntry>;
44
+ export declare function toGranularGrantee(kind: "user" | "group", granteeRef: ObjRef, level: ObjectSharePermissionLevel | "none"): IGranularAccessGrantee;
45
+ /**
46
+ * The principal a label-scope reconcile applies to: a named user/group, or the
47
+ * implicit all-workspace-users rule (general access).
48
+ */
49
+ export type LabelScopePrincipal = {
50
+ kind: "user" | "group";
51
+ granteeRef: ObjRef;
52
+ } | {
53
+ allWorkspaceUsers: true;
54
+ };
55
+ export declare function granularGranteeFor(principal: LabelScopePrincipal, level: ObjectSharePermissionLevel | "none"): IGranularAccessGrantee;
56
+ /**
57
+ * Pure diff: the per-label writes needed to move `principal`'s label scope from
58
+ * `current` to `desired`. The primary label is always kept in scope (never
59
+ * revoked). `labels` is the permissionable set (404 forms already excluded), so
60
+ * every returned write targets a label that can actually take a grant. No I/O —
61
+ * the single source of truth for what add / remove / general-access / the labels
62
+ * picker each write, so they can't drift apart.
63
+ */
64
+ export declare function buildLabelMutations(principal: LabelScopePrincipal, desiredLabelIds: ReadonlySet<string>, currentLabelIds: ReadonlySet<string>, labels: IObjectShareLabel[]): Array<{
65
+ ref: ObjRef;
66
+ grantee: IGranularAccessGrantee;
67
+ }>;
68
+ /** Stable empty-labels default so the hook's default arg doesn't churn identities. */
69
+ export declare const NO_LABELS: IObjectShareLabel[];
70
+ /** Shared empty id-set for "no labels in scope" diffs. */
71
+ export declare const EMPTY_IDS: ReadonlySet<string>;
72
+ /**
73
+ * Whether a fetched grant for `granteeId` exists with a non-empty permission set.
74
+ * Inspects the raw grants rather than `granteesFromAccessList`, which normalizes
75
+ * every listed user/group to a VIEW level even when its `permissions` are empty —
76
+ * so a revoked-but-still-listed entry would otherwise read as granted, mis-scoping
77
+ * a per-label checkbox to a label the grantee can't actually access.
78
+ */
79
+ export declare function isGranteeGrantedIn(list: IObjectAccessList, id: string): boolean;
80
+ //# sourceMappingURL=objectShareController.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"objectShareController.helpers.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,MAAM,EAId,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM9F;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,0BAA0B,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,0BAA0B,EAClC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACxC,0BAA0B,GAAG,SAAS,CAExC;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,EAAE,CA8BjG;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,mBAAmB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC7D;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,YAAY,CACxB,SAAS,EAAE,mBAAmB,EAAE,EAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAC9C,mBAAmB,EAAE,CAUvB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC5B,SAAS,EAAE,mBAAmB,EAAE,EAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,GAC9C,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAatC;AAED,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,0BAA0B,GAAG,MAAM,GAC3C,sBAAsB,CAMxB;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GACzB;IAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,CAAC;AAElC,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,0BAA0B,GAAG,MAAM,0BAO7C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,MAAM,EAAE,iBAAiB,EAAE,GAC5B,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAWzD;AAED,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,iBAAiB,EAAO,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAU/E"}
@@ -0,0 +1,152 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isGranularUserAccess, isGranularUserGroupAccess, objRefToString, } from "@gooddata/sdk-model";
3
+ /** Stable row id shared by grantee rows and picker options: `user:<ref>` / `group:<ref>`. */
4
+ export function granteeId(kind, ref) {
5
+ return `${kind}:${objRefToString(ref)}`;
6
+ }
7
+ /** Case-insensitive match of an assignee against the picker query (name, or email for users). */
8
+ export function assigneeMatchesQuery(assignee, query) {
9
+ if (!query) {
10
+ return true;
11
+ }
12
+ const email = assignee.type === "user" ? (assignee.email ?? "") : "";
13
+ return `${assignee.name} ${email}`.toLowerCase().includes(query);
14
+ }
15
+ /** The row's directly-granted level. SHARE outranks VIEW. */
16
+ export function directLevel(permissions) {
17
+ return permissions.includes("SHARE") ? "SHARE" : "VIEW";
18
+ }
19
+ /**
20
+ * The effective permission to surface as a warning, or undefined when the direct
21
+ * grant already covers it. Set only when the grantee *inherits* SHARE (e.g. via a
22
+ * group) but is directly granted just VIEW — i.e. the effective access is higher
23
+ * than what the row's permission control shows.
24
+ */
25
+ export function effectivePermissionAbove(direct, inheritedPermissions) {
26
+ return direct !== "SHARE" && inheritedPermissions.includes("SHARE") ? "SHARE" : undefined;
27
+ }
28
+ export function granteesFromAccessList(list) {
29
+ if (!list) {
30
+ return [];
31
+ }
32
+ const out = [];
33
+ for (const g of list.grants) {
34
+ if (isGranularUserAccess(g)) {
35
+ const level = directLevel(g.permissions);
36
+ out.push({
37
+ id: granteeId("user", g.user.ref),
38
+ kind: "user",
39
+ granteeRef: g.user.ref,
40
+ name: g.user.fullName ?? g.user.email ?? g.user.login,
41
+ email: g.user.email,
42
+ level,
43
+ effectivePermission: effectivePermissionAbove(level, g.inheritedPermissions),
44
+ });
45
+ }
46
+ else if (isGranularUserGroupAccess(g)) {
47
+ const level = directLevel(g.permissions);
48
+ out.push({
49
+ id: granteeId("group", g.userGroup.ref),
50
+ kind: "group",
51
+ granteeRef: g.userGroup.ref,
52
+ name: g.userGroup.name ?? objRefToString(g.userGroup.ref),
53
+ level,
54
+ effectivePermission: effectivePermissionAbove(level, g.inheritedPermissions),
55
+ });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+ /**
61
+ * Merge the committed rows with the optimistic overlay: `set` replaces/inserts a
62
+ * row (annotated pending), `remove` drops it. Entries with no committed effect
63
+ * still apply so the UI reflects the user's intent during read-after-write lag.
64
+ */
65
+ export function mergeOverlay(committed, overlay) {
66
+ const byId = new Map(committed.map((g) => [g.id, g]));
67
+ for (const [id, entry] of Object.entries(overlay)) {
68
+ if (entry.op === "remove") {
69
+ byId.delete(id);
70
+ }
71
+ else {
72
+ byId.set(id, { ...entry.grantee, pending: entry.pending ? "saving" : undefined });
73
+ }
74
+ }
75
+ return [...byId.values()];
76
+ }
77
+ /**
78
+ * Drop overlay entries the freshly-fetched list now confirms: a `set` whose
79
+ * committed level matches the intended level, or a `remove` whose grantee is
80
+ * gone. Unconfirmed entries are kept (server still lagging) but lose `pending`
81
+ * so the row shows the intended value without a spinner.
82
+ */
83
+ export function reconcileOverlay(committed, overlay) {
84
+ const byId = new Map(committed.map((g) => [g.id, g]));
85
+ const next = {};
86
+ for (const [id, entry] of Object.entries(overlay)) {
87
+ if (entry.op === "remove") {
88
+ if (byId.has(id)) {
89
+ next[id] = { op: "remove", pending: false };
90
+ }
91
+ }
92
+ else if (byId.get(id)?.level !== entry.grantee.level) {
93
+ next[id] = { op: "set", grantee: entry.grantee, pending: false };
94
+ }
95
+ }
96
+ return next;
97
+ }
98
+ export function toGranularGrantee(kind, granteeRef, level) {
99
+ const permissions = level === "none" ? [] : level === "SHARE" ? ["SHARE", "VIEW"] : ["VIEW"];
100
+ return kind === "user"
101
+ ? { type: "granularUser", granteeRef, permissions, inheritedPermissions: [] }
102
+ : { type: "granularGroup", granteeRef, permissions, inheritedPermissions: [] };
103
+ }
104
+ export function granularGranteeFor(principal, level) {
105
+ if ("allWorkspaceUsers" in principal) {
106
+ const permissions = level === "none" ? [] : ["VIEW"];
107
+ return { type: "allWorkspaceUsers", permissions, inheritedPermissions: [] };
108
+ }
109
+ return toGranularGrantee(principal.kind, principal.granteeRef, level);
110
+ }
111
+ /**
112
+ * Pure diff: the per-label writes needed to move `principal`'s label scope from
113
+ * `current` to `desired`. The primary label is always kept in scope (never
114
+ * revoked). `labels` is the permissionable set (404 forms already excluded), so
115
+ * every returned write targets a label that can actually take a grant. No I/O —
116
+ * the single source of truth for what add / remove / general-access / the labels
117
+ * picker each write, so they can't drift apart.
118
+ */
119
+ export function buildLabelMutations(principal, desiredLabelIds, currentLabelIds, labels) {
120
+ const writes = [];
121
+ for (const label of labels) {
122
+ const wanted = label.isPrimary || desiredLabelIds.has(label.id);
123
+ const had = label.isPrimary || currentLabelIds.has(label.id);
124
+ if (wanted === had) {
125
+ continue;
126
+ }
127
+ writes.push({ ref: label.ref, grantee: granularGranteeFor(principal, wanted ? "VIEW" : "none") });
128
+ }
129
+ return writes;
130
+ }
131
+ /** Stable empty-labels default so the hook's default arg doesn't churn identities. */
132
+ export const NO_LABELS = [];
133
+ /** Shared empty id-set for "no labels in scope" diffs. */
134
+ export const EMPTY_IDS = new Set();
135
+ /**
136
+ * Whether a fetched grant for `granteeId` exists with a non-empty permission set.
137
+ * Inspects the raw grants rather than `granteesFromAccessList`, which normalizes
138
+ * every listed user/group to a VIEW level even when its `permissions` are empty —
139
+ * so a revoked-but-still-listed entry would otherwise read as granted, mis-scoping
140
+ * a per-label checkbox to a label the grantee can't actually access.
141
+ */
142
+ export function isGranteeGrantedIn(list, id) {
143
+ return list.grants.some((g) => {
144
+ if (isGranularUserAccess(g)) {
145
+ return id === granteeId("user", g.user.ref) && g.permissions.length > 0;
146
+ }
147
+ if (isGranularUserGroupAccess(g)) {
148
+ return id === granteeId("group", g.userGroup.ref) && g.permissions.length > 0;
149
+ }
150
+ return false;
151
+ });
152
+ }
@@ -0,0 +1,110 @@
1
+ import type { ObjRef } from "@gooddata/sdk-model";
2
+ import type { GeneralAccessValue, IUiGranteeAsyncOptions, IUiPickedGrantee } from "@gooddata/sdk-ui-kit";
3
+ import type { IObjectAccessSummary, IObjectShareLabel } from "./types.js";
4
+ /**
5
+ * Permission level surfaced in the share dialog. `EDIT` is intentionally
6
+ * not represented — the underlying permission menu caps at VIEW/SHARE.
7
+ *
8
+ * @internal
9
+ */
10
+ export type ObjectSharePermissionLevel = "VIEW" | "SHARE";
11
+ /**
12
+ * Dialog row derived from a backend grant.
13
+ *
14
+ * @internal
15
+ */
16
+ export interface IObjectShareGrantee {
17
+ /** Stable id used as the dialog row's React key. `user:<ref>` / `group:<ref>`. */
18
+ id: string;
19
+ kind: "user" | "group";
20
+ granteeRef: ObjRef;
21
+ name: string;
22
+ email?: string;
23
+ level: ObjectSharePermissionLevel;
24
+ /**
25
+ * Effective permission when it is *higher* than the directly-granted `level`
26
+ * because the grantee inherits a stronger permission (e.g. via a group).
27
+ * Undefined when the direct grant already is the effective permission. Drives
28
+ * the row's "effective permission" warning badge.
29
+ */
30
+ effectivePermission?: ObjectSharePermissionLevel;
31
+ /**
32
+ * Row-level in-flight state for optimistic updates: `"saving"` while a level
33
+ * change or freshly-added grant is being committed, `"removing"` while a
34
+ * removal is in flight. Undefined when the row reflects committed state.
35
+ */
36
+ pending?: "saving" | "removing";
37
+ }
38
+ /**
39
+ * @internal
40
+ */
41
+ export interface IObjectShareControllerState {
42
+ subview: "main" | "addGrantee";
43
+ status: "idle" | "loading" | "success" | "error" | "saving";
44
+ error?: Error;
45
+ summary: IObjectAccessSummary | undefined;
46
+ grantees: IObjectShareGrantee[];
47
+ generalAccess: GeneralAccessValue;
48
+ /**
49
+ * Labels (display forms) of the shared attribute, in source order. Empty for
50
+ * objects without labels (e.g. facts) — the labels picker is then hidden.
51
+ */
52
+ labels: IObjectShareLabel[];
53
+ /**
54
+ * Whether per-label scope resolution has finished. While false, a grantee's
55
+ * label scope is not yet known: consumers must not treat a missing
56
+ * `selectedLabelIdsByGrantee` entry as "all selected" for editing, and should
57
+ * keep the Add action disabled.
58
+ */
59
+ labelsResolved: boolean;
60
+ /**
61
+ * Per-grantee label scope: grantee id → the label ids that grantee can access.
62
+ * The primary label is always included. Empty entry means "all labels" has not
63
+ * yet been resolved; consumers should treat a missing entry as all-selected.
64
+ */
65
+ selectedLabelIdsByGrantee: Record<string, string[]>;
66
+ /** Pending confirm — when set, the confirm dialog is visible. */
67
+ pendingGeneralAccess?: GeneralAccessValue;
68
+ /** Grantees staged in the add-grantee dialog before confirmation. */
69
+ pendingGrantees: IUiPickedGrantee[];
70
+ }
71
+ /**
72
+ * @internal
73
+ */
74
+ export interface IObjectShareControllerActions {
75
+ /** Reset transient dialog state (subview + pending buffers). Call on dialog close. */
76
+ reset: () => void;
77
+ openAddGrantee: () => void;
78
+ closeAddGrantee: () => void;
79
+ setPendingGrantees: (next: IUiPickedGrantee[]) => void;
80
+ /**
81
+ * Loader for the add-grantee picker. Wraps `getAvailableAssignees` with
82
+ * client-side search + already-picked filtering, returning the picker's
83
+ * `{ groups, users }` shape.
84
+ */
85
+ loadOptions: (search: string) => Promise<IUiGranteeAsyncOptions>;
86
+ /** Commit all pending grantees to the backend. */
87
+ confirmAddGrantees: () => Promise<void>;
88
+ /** Change the permission level for a single grantee. Auto-saves. */
89
+ changePermissionLevel: (granteeId: string, level: ObjectSharePermissionLevel) => Promise<void>;
90
+ /** Remove a grantee. Auto-saves. */
91
+ removeGrantee: (granteeId: string) => Promise<void>;
92
+ /**
93
+ * Scope a grantee's access to the given label ids (the primary label is always
94
+ * kept). Sends a per-label VIEW/none grant for each label that changed. Auto-saves.
95
+ */
96
+ changeGranteeLabels: (granteeId: string, selectedLabelIds: string[]) => Promise<void>;
97
+ /** Stage a general access change; opens the confirm dialog. */
98
+ requestGeneralAccessChange: (next: GeneralAccessValue) => void;
99
+ cancelGeneralAccessChange: () => void;
100
+ /** Commit the pending general access change. Auto-saves. */
101
+ confirmGeneralAccessChange: () => Promise<void>;
102
+ }
103
+ /**
104
+ * @internal
105
+ */
106
+ export interface IObjectShareController {
107
+ state: IObjectShareControllerState;
108
+ actions: IObjectShareControllerActions;
109
+ }
110
+ //# sourceMappingURL=objectShareController.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"objectShareController.types.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAChC,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,0BAA0B,CAAC;IAClC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5D,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAE1C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,aAAa,EAAE,kBAAkB,CAAC;IAElC;;;OAGG;IACH,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEpD,mEAAiE;IACjE,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C,qEAAqE;IACrE,eAAe,EAAE,gBAAgB,EAAE,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC1C,sFAAsF;IACtF,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD;;;;OAIG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjE,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,oEAAoE;IACpE,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,0BAA0B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/F,oCAAoC;IACpC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD;;;OAGG;IACH,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtF,+DAA+D;IAC/D,0BAA0B,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/D,yBAAyB,EAAE,MAAM,IAAI,CAAC;IACtC,4DAA4D;IAC5D,0BAA0B,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,2BAA2B,CAAC;IACnC,OAAO,EAAE,6BAA6B,CAAC;CAC1C"}
@@ -0,0 +1,2 @@
1
+ // (C) 2026 GoodData Corporation
2
+ export {};
@@ -0,0 +1,47 @@
1
+ import type { ObjRef } from "@gooddata/sdk-model";
2
+ import type { GeneralAccessValue } from "@gooddata/sdk-ui-kit";
3
+ /**
4
+ * A label (attribute display form) of the shared attribute. The share dialog
5
+ * scopes a grantee's access to a subset of these via per-label permission grants.
6
+ *
7
+ * @internal
8
+ */
9
+ export interface IObjectShareLabel {
10
+ /** Backend ref of the label, used as the `kind: "label"` permission target. */
11
+ ref: ObjRef;
12
+ /** Stable id (serialized ref) — the picker's item id and selection key. */
13
+ id: string;
14
+ /** Display title shown in the labels picker. */
15
+ title: string;
16
+ /**
17
+ * The primary (key) label. It is always accessible and cannot be unchecked —
18
+ * rendered locked in the picker. Sharing an attribute always includes it.
19
+ */
20
+ isPrimary: boolean;
21
+ /**
22
+ * The default display label — the one shown by default in visualizations.
23
+ * At most one label is the default. Distinct from the primary (key) label.
24
+ */
25
+ isDefault: boolean;
26
+ }
27
+ /**
28
+ * Aggregated, read-only access state for an object — what the inline access row
29
+ * in a host UI needs to render the current sharing state without opening the
30
+ * full share dialog.
31
+ *
32
+ * - `generalAccess` — `RESTRICTED` if only named grantees can access the object;
33
+ * `WORKSPACE` if the workspace-wide rule grant is present with non-empty permissions.
34
+ * - `workspaceLevel` — the rule grant's permission level; `VIEW` by default,
35
+ * `SHARE` when the rule explicitly permits `SHARE`. EDIT is intentionally not
36
+ * surfaced (the UI caps at VIEW/SHARE).
37
+ * - `granteeCount` — number of named grantees (users + groups). Excludes the
38
+ * workspace-wide rule grant itself.
39
+ *
40
+ * @internal
41
+ */
42
+ export interface IObjectAccessSummary {
43
+ generalAccess: GeneralAccessValue;
44
+ workspaceLevel: "VIEW" | "SHARE";
45
+ granteeCount: number;
46
+ }
47
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/share/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAC9B,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA2E;IAC3E,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,oBAAoB;IACjC,aAAa,EAAE,kBAAkB,CAAC;IAClC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;CACxB"}
@@ -0,0 +1,2 @@
1
+ // (C) 2026 GoodData Corporation
2
+ export {};