@gooddata/sdk-ui-ext 11.51.0-alpha.0 → 11.51.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.
- package/NOTICE +4 -4
- package/esm/index.d.ts +3 -2
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -1
- package/esm/internal/translations/en-US.localization-bundle.d.ts +4 -0
- package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-US.localization-bundle.js +4 -0
- package/esm/sdk-ui-ext.d.ts +56 -305
- package/esm/share/ObjectShareDialog.d.ts +19 -23
- package/esm/share/ObjectShareDialog.d.ts.map +1 -1
- package/esm/share/ObjectShareDialog.js +85 -171
- package/esm/share/accessSummary.d.ts +18 -8
- package/esm/share/accessSummary.d.ts.map +1 -1
- package/esm/share/accessSummary.js +25 -13
- package/esm/share/messages.d.ts +7 -0
- package/esm/share/messages.d.ts.map +1 -1
- package/esm/share/messages.js +5 -0
- package/esm/share/objectShareController.helpers.d.ts +83 -5
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +144 -10
- package/esm/share/objectShareController.types.d.ts +63 -64
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/types.d.ts +4 -5
- package/esm/share/types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +64 -63
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +174 -221
- package/esm/share/useLabelScope.d.ts +18 -13
- package/esm/share/useLabelScope.d.ts.map +1 -1
- package/esm/share/useLabelScope.js +112 -123
- package/esm/share/useObjectShareController.d.ts +14 -17
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +211 -224
- package/esm/share/useObjectShareDialog.d.ts +90 -0
- package/esm/share/useObjectShareDialog.d.ts.map +1 -0
- package/esm/share/useObjectShareDialog.js +82 -0
- package/package.json +21 -21
- package/src/notificationsPanel/NotificationsPanel/DefaultNotificationsPanelButton.scss +4 -1
- package/styles/css/main.css +1 -1
- package/styles/css/main.css.map +1 -1
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
|
-
import { useCallback,
|
|
3
|
-
import { useBackendStrict, useWorkspaceStrict } from "@gooddata/sdk-ui";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useBackendStrict, useCancelablePromise, useWorkspaceStrict } from "@gooddata/sdk-ui";
|
|
4
4
|
import { isPermissionsNotAvailable } from "./accessErrors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { buildLabelMutationsForPrincipals, isGranteeGrantedIn, } from "./objectShareController.helpers.js";
|
|
6
|
+
/** Pure: each grantee's in-scope label ids — primary always counts, others only where the grantee is granted. */
|
|
7
|
+
function resolveScopes(results, committedGranteeIds) {
|
|
8
|
+
const resolved = {};
|
|
9
|
+
for (const id of committedGranteeIds) {
|
|
10
|
+
resolved[id] = [];
|
|
11
|
+
}
|
|
12
|
+
for (const result of results) {
|
|
13
|
+
if ("transient" in result) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
for (const id of committedGranteeIds) {
|
|
17
|
+
if (result.label.isPrimary || isGranteeGrantedIn(result.list, id)) {
|
|
18
|
+
resolved[id].push(result.label.id);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* Owns label-scope resolution + writes for {@link useObjectShareController}.
|
|
8
26
|
* Resolves each grantee's scope by fetching every label's access list, tracks
|
|
@@ -11,144 +29,115 @@ import { buildLabelMutations, buildLabelMutationsForPrincipals, isGranteeGranted
|
|
|
11
29
|
*
|
|
12
30
|
* The resolved scope is local-authoritative: the probe seeds a scope only for
|
|
13
31
|
* grantees it doesn't already know, so an optimistic scope written for a freshly
|
|
14
|
-
* added grantee is never overwritten by the backend's lagging read.
|
|
15
|
-
*
|
|
32
|
+
* added grantee is never overwritten by the backend's lagging read. Session-scoped
|
|
33
|
+
* like its owner — the target is fixed for the mount (see {@link ObjectShareDialog}).
|
|
16
34
|
*
|
|
17
35
|
* @internal
|
|
18
36
|
*/
|
|
19
|
-
export function useLabelScope(target,
|
|
37
|
+
export function useLabelScope(target, labels, hasList, committedGranteeIds, labelsError, labelsLoading) {
|
|
20
38
|
const backend = useBackendStrict();
|
|
21
39
|
const workspace = useWorkspaceStrict();
|
|
40
|
+
// The optimistic label-scope overlay — kept authoritative: a scope written for
|
|
41
|
+
// a fresh add / labels edit must survive the backend's lagging re-read, so the
|
|
42
|
+
// probe below only seeds grantees it doesn't already know.
|
|
22
43
|
const [selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee] = useState({});
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
const [permissionableLabelIds, setPermissionableLabelIds] = useState(undefined);
|
|
44
|
+
// Whether the session's first probe has settled (see `labelsInitializing`) —
|
|
45
|
+
// a fact about session history, so it is state, flipped once in onSuccess.
|
|
46
|
+
const [everResolved, setEverResolved] = useState(false);
|
|
27
47
|
const labelsKey = labels.map((l) => l.id).join(",");
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// labels against stale ids, so add/share could skip expected per-label grants.
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
setPermissionableLabelIds(undefined);
|
|
37
|
-
}, [targetKey, labelsKey]);
|
|
38
|
-
// A target switch — or a label-set change under the same target (labels finish
|
|
39
|
-
// loading, a label added/removed) — drops every resolved scope so the seeding
|
|
40
|
-
// effect below re-resolves from the current label set's per-label lists rather
|
|
41
|
-
// than preserving scopes computed against the old labels. Without the labelsKey
|
|
42
|
-
// reset, an existing grantee would keep a scope missing a newly-added label even
|
|
43
|
-
// when the backend grants them access to it. (An add under the SAME labels only
|
|
44
|
-
// changes granteeIdsKey, not labelsKey, so an optimistic scope still survives.)
|
|
45
|
-
useEffect(() => {
|
|
48
|
+
// Drop every resolved scope when the label SET changes (labels finish loading, a
|
|
49
|
+
// label added/removed) — the probe must re-resolve, or a grantee keeps a scope
|
|
50
|
+
// missing a newly-granted label. Render-time adjust-on-change.
|
|
51
|
+
const [seenLabelsKey, setSeenLabelsKey] = useState(labelsKey);
|
|
52
|
+
if (seenLabelsKey !== labelsKey) {
|
|
53
|
+
setSeenLabelsKey(labelsKey);
|
|
46
54
|
setSelectedLabelIdsByGrantee({});
|
|
47
|
-
}
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
if (cancelled) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const resolved = {};
|
|
75
|
-
for (const id of committedGranteeIds) {
|
|
76
|
-
resolved[id] = [];
|
|
77
|
-
}
|
|
78
|
-
const permissionable = new Set();
|
|
79
|
-
for (const result of results) {
|
|
80
|
-
if ("transient" in result) {
|
|
81
|
-
// Keep transiently-failed labels permissionable (don't hide a real
|
|
82
|
-
// label); skip definitively-not-permissionable ones (404).
|
|
83
|
-
if (result.transient) {
|
|
84
|
-
permissionable.add(result.label.id);
|
|
85
|
-
}
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const { label, list } = result;
|
|
89
|
-
permissionable.add(label.id);
|
|
90
|
-
for (const id of committedGranteeIds) {
|
|
91
|
-
// Primary label is always part of the scope; others are scoped
|
|
92
|
-
// only when the grantee actually holds a grant on that label.
|
|
93
|
-
if (label.isPrimary || isGranteeGrantedIn(list, id)) {
|
|
94
|
-
resolved[id].push(label.id);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
55
|
+
}
|
|
56
|
+
// Probe each label's access list to learn which are permissionable (some 404) and
|
|
57
|
+
// which each grantee holds. `hasList` gates it: a list with no named grantees keeps
|
|
58
|
+
// `granteeIdsKey` empty, but the permissionable set (404 filtering) must still resolve.
|
|
59
|
+
const { result: labelLists } = useCancelablePromise({
|
|
60
|
+
promise: target && labels.length > 0 && hasList
|
|
61
|
+
? () => Promise.all(labels.map((label) => backend
|
|
62
|
+
.workspace(workspace)
|
|
63
|
+
.objectPermissions()
|
|
64
|
+
.getAccessList({ kind: "label", ref: label.ref })
|
|
65
|
+
.then((list) => ({ label, list }))
|
|
66
|
+
// Only a definitive 404 means the label can't take a
|
|
67
|
+
// grant; a transient failure must NOT drop a real label.
|
|
68
|
+
.catch((error) => ({
|
|
69
|
+
label,
|
|
70
|
+
transient: !isPermissionsNotAvailable(error),
|
|
71
|
+
}))))
|
|
72
|
+
: undefined,
|
|
73
|
+
onSuccess: (results) => {
|
|
74
|
+
setEverResolved(true);
|
|
75
|
+
const resolved = resolveScopes(results, committedGranteeIds);
|
|
76
|
+
// Seed only grantees we don't already have a scope for — an optimistic
|
|
77
|
+
// scope is authoritative and must survive this re-resolution. Fired once
|
|
78
|
+
// per resolution (onSuccess), so reading `prev` is safe.
|
|
98
79
|
setSelectedLabelIdsByGrantee((prev) => {
|
|
99
|
-
// Seed a scope only for grantees we don't already have one for. A
|
|
100
|
-
// scope written optimistically (a fresh add, a labels edit) is
|
|
101
|
-
// local-authoritative and must survive the re-resolution that the
|
|
102
|
-
// grantee-set change triggers — the backend's lagging read would
|
|
103
|
-
// otherwise reset it. Grantees gone from the list are dropped.
|
|
104
80
|
const next = {};
|
|
105
81
|
for (const id of committedGranteeIds) {
|
|
106
82
|
next[id] = prev[id] ?? resolved[id];
|
|
107
83
|
}
|
|
108
84
|
return next;
|
|
109
85
|
});
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
// Keyed on the label set's stable string hash, not the array (rebuilt every
|
|
89
|
+
// render — listing it would refetch each render, an infinite loop). NOT keyed
|
|
90
|
+
// on the grantee set: adds and removes maintain their scope optimistically,
|
|
91
|
+
// so a per-grantee-change re-probe would only discard its own results while
|
|
92
|
+
// flipping labelsResolved false and disabling controls for the round trip.
|
|
93
|
+
[backend, workspace, labelsKey, hasList]);
|
|
94
|
+
// Permissionable ids derived straight from the probe result — `undefined` (assume
|
|
95
|
+
// all) until it resolves, which also invalidates automatically on a dep change
|
|
96
|
+
// (useCancelablePromise resets to loading), so no separate reset is needed.
|
|
97
|
+
const permissionableLabelIds = useMemo(() => {
|
|
98
|
+
if (!labelLists) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
const permissionable = new Set();
|
|
102
|
+
for (const result of labelLists) {
|
|
103
|
+
// Keep transiently-failed labels permissionable; skip definitive 404s.
|
|
104
|
+
if ("transient" in result) {
|
|
105
|
+
if (result.transient) {
|
|
106
|
+
permissionable.add(result.label.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
permissionable.add(result.label.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return permissionable;
|
|
114
|
+
}, [labelLists]);
|
|
115
|
+
// Only permissionable labels are scope-controllable; assume all until resolved.
|
|
121
116
|
const effectiveLabels = useMemo(() => hasList && permissionableLabelIds
|
|
122
117
|
? labels.filter((l) => permissionableLabelIds.has(l.id))
|
|
123
118
|
: labels, [labels, permissionableLabelIds, hasList]);
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
// labels are pending, regardless of the current (possibly empty) list.
|
|
119
|
+
// A transiently-failed probe means that label's per-grantee grants are UNKNOWN —
|
|
120
|
+
// the label stays visible (permissionable), but the scope must not count as
|
|
121
|
+
// resolved: edits would diff against an invented current and could orphan or
|
|
122
|
+
// skip that label's real grants. Same philosophy as the `labelsError` gate.
|
|
123
|
+
const hasTransientProbe = labelLists?.some((r) => "transient" in r && r.transient) ?? false;
|
|
124
|
+
// Resolved once the probe produced a permissionable set with no unknowns, or for
|
|
125
|
+
// a genuinely label-free object. An EMPTY `labels` list counts as resolved only
|
|
126
|
+
// when labels aren't still loading and didn't error — otherwise row controls
|
|
127
|
+
// would reconcile against an empty set and silently orphan real per-label grants.
|
|
134
128
|
const labelsPending = labelsError || labelsLoading;
|
|
135
|
-
const labelsResolved = !labelsPending &&
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const svc = backend.workspace(workspace).objectPermissions();
|
|
148
|
-
const results = await Promise.allSettled(writes.map((w) => svc.manageObjectPermissions({ kind: "label", ref: w.ref }, [w.grantee])));
|
|
149
|
-
return results.every((r) => r.status === "fulfilled");
|
|
150
|
-
}, [effectiveLabels, backend, workspace]);
|
|
151
|
-
const reconcileLabelScopeMany = useCallback(async (principals, desiredLabelIds, currentLabelIds) => {
|
|
129
|
+
const labelsResolved = !labelsPending &&
|
|
130
|
+
(labels.length === 0 || (permissionableLabelIds !== undefined && !hasTransientProbe));
|
|
131
|
+
// First-resolution only: metadata still loading, or a probe will run and hasn't
|
|
132
|
+
// settled once. A metadata ERROR is not initializing (nothing more will load —
|
|
133
|
+
// the dialog reveals with disabled controls instead of holding placeholders).
|
|
134
|
+
const labelsInitializing = labelsLoading || (labels.length > 0 && !everResolved);
|
|
135
|
+
// The single per-label write path (see the interface doc) — used by add, remove,
|
|
136
|
+
// general access and the labels picker alike, so their label behavior can't
|
|
137
|
+
// drift. Writes settle independently (allSettled), and the failed labels are
|
|
138
|
+
// reported by id — never collapsed into one boolean, or a partial failure would
|
|
139
|
+
// roll callers back past writes that actually landed.
|
|
140
|
+
const reconcileLabelScope = useCallback(async (principals, desiredLabelIds, currentLabelIds) => {
|
|
152
141
|
const writes = buildLabelMutationsForPrincipals(principals, desiredLabelIds, currentLabelIds, effectiveLabels);
|
|
153
142
|
if (writes.length === 0) {
|
|
154
143
|
return { ok: true, failedLabelIds: [] };
|
|
@@ -163,9 +152,9 @@ export function useLabelScope(target, targetKey, labels, hasList, committedGrant
|
|
|
163
152
|
return {
|
|
164
153
|
effectiveLabels,
|
|
165
154
|
labelsResolved,
|
|
155
|
+
labelsInitializing,
|
|
166
156
|
selectedLabelIdsByGrantee,
|
|
167
157
|
setSelectedLabelIdsByGrantee,
|
|
168
158
|
reconcileLabelScope,
|
|
169
|
-
reconcileLabelScopeMany,
|
|
170
159
|
};
|
|
171
160
|
}
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import type { IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
|
|
2
2
|
import { type IObjectShareController, type IUseObjectShareOptions } from "./objectShareController.types.js";
|
|
3
3
|
/**
|
|
4
|
-
* Manages the share
|
|
4
|
+
* Manages the share-dialog state and backend I/O for ONE dialog session: the hook
|
|
5
|
+
* is mounted while the dialog is open, for a single target that must not change
|
|
6
|
+
* while mounted (see {@link ObjectShareDialog}). Unmounting discards every
|
|
7
|
+
* transient — pending rows, staged confirms, optimistic overlays — so no explicit
|
|
8
|
+
* reset exists or is needed.
|
|
5
9
|
*
|
|
6
|
-
* The access list is fetched once and
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The access list is fetched once, and the displayed state is *derived* from that
|
|
11
|
+
* fetch composed with a small local edit overlay (owned by `useAccessList`). Each
|
|
12
|
+
* access change writes its overlay entry (so the row updates immediately), commits
|
|
13
|
+
* to the backend, then settles the entry on success or reverts it on failure (to
|
|
14
|
+
* the last committed entry, or the fetched value). There is no post-write refetch,
|
|
15
|
+
* so the grantee list never blanks and never fights read-after-write lag.
|
|
12
16
|
*
|
|
13
17
|
* Mutations follow a **commit-on-interaction** model: each access change is sent
|
|
14
18
|
* immediately; the general-access toggle goes through a confirm step because it
|
|
15
|
-
* is high-impact. There is no batched Save.
|
|
16
|
-
* `
|
|
17
|
-
*
|
|
18
|
-
* Most consumers do not call this directly — render {@link ObjectShareDialog} with
|
|
19
|
-
* plain props and it owns its controller. Call this (exported as `useObjectShare`)
|
|
20
|
-
* only to share a single access-list fetch between the dialog and an inline summary
|
|
21
|
-
* row: call it once, read `state.summary` for the row, and pass the controller into
|
|
22
|
-
* the dialog. Grantee rows carry identity facts only (`name`/`email` are undefined
|
|
23
|
-
* when unknown); the display fallback order is not exposed, so {@link ObjectShareDialog}
|
|
24
|
-
* is the only sanctioned row renderer.
|
|
19
|
+
* is high-impact. There is no batched Save. Grantee rows carry identity facts only
|
|
20
|
+
* (`name`/`email` are undefined when unknown); the display fallback order is not
|
|
21
|
+
* exposed, so {@link ObjectShareDialog} is the only sanctioned row renderer.
|
|
25
22
|
*
|
|
26
23
|
* @internal
|
|
27
24
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAiB1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CA+kBxB"}
|