@gooddata/sdk-ui-ext 11.43.0-alpha.0 → 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 (45) hide show
  1. package/NOTICE +130 -130
  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/assets/radar/bucket-title-measures.svg +1 -1
  6. package/esm/internal/assets/radar/bucket-title-segment.svg +1 -1
  7. package/esm/internal/assets/radar/bucket-title-trend.svg +1 -1
  8. package/esm/internal/components/configurationControls/customTooltip/CustomTooltipSection.d.ts.map +1 -1
  9. package/esm/internal/components/configurationControls/customTooltip/CustomTooltipSection.js +3 -2
  10. package/esm/internal/translations/en-US.localization-bundle.d.ts +44 -0
  11. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  12. package/esm/internal/translations/en-US.localization-bundle.js +44 -0
  13. package/esm/sdk-ui-ext.d.ts +274 -0
  14. package/esm/share/ObjectShareDialog.d.ts +67 -0
  15. package/esm/share/ObjectShareDialog.d.ts.map +1 -0
  16. package/esm/share/ObjectShareDialog.js +102 -0
  17. package/esm/share/accessSummary.d.ts +20 -0
  18. package/esm/share/accessSummary.d.ts.map +1 -0
  19. package/esm/share/accessSummary.js +26 -0
  20. package/esm/share/messages.d.ts +44 -0
  21. package/esm/share/messages.d.ts.map +1 -0
  22. package/esm/share/messages.js +23 -0
  23. package/esm/share/objectShareController.helpers.d.ts +80 -0
  24. package/esm/share/objectShareController.helpers.d.ts.map +1 -0
  25. package/esm/share/objectShareController.helpers.js +152 -0
  26. package/esm/share/objectShareController.types.d.ts +110 -0
  27. package/esm/share/objectShareController.types.d.ts.map +1 -0
  28. package/esm/share/objectShareController.types.js +2 -0
  29. package/esm/share/types.d.ts +47 -0
  30. package/esm/share/types.d.ts.map +1 -0
  31. package/esm/share/types.js +2 -0
  32. package/esm/share/useAccessList.d.ts +51 -0
  33. package/esm/share/useAccessList.d.ts.map +1 -0
  34. package/esm/share/useAccessList.js +263 -0
  35. package/esm/share/useLabelScope.d.ts +54 -0
  36. package/esm/share/useLabelScope.d.ts.map +1 -0
  37. package/esm/share/useLabelScope.js +160 -0
  38. package/esm/share/useObjectShare.d.ts +50 -0
  39. package/esm/share/useObjectShare.d.ts.map +1 -0
  40. package/esm/share/useObjectShare.js +17 -0
  41. package/esm/share/useObjectShareController.d.ts +24 -0
  42. package/esm/share/useObjectShareController.d.ts.map +1 -0
  43. package/esm/share/useObjectShareController.js +347 -0
  44. package/esm/tsdoc-metadata.json +1 -1
  45. package/package.json +21 -21
@@ -0,0 +1,347 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { useCallback, useMemo, useState } from "react";
3
+ import { useToastMessage } from "@gooddata/sdk-ui-kit";
4
+ import { objectShareMessages } from "./messages.js";
5
+ import { EMPTY_IDS, NO_LABELS, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
6
+ import { useAccessList } from "./useAccessList.js";
7
+ import { useLabelScope } from "./useLabelScope.js";
8
+ /**
9
+ * Manages the share dialog state and backend I/O for a single shareable
10
+ * object.
11
+ *
12
+ * The access list is fetched (and re-fetched after every save) via
13
+ * `useCancelablePromise`; everything that reflects the backend — the grantee
14
+ * rows, general-access value, summary — is derived from it on render, never
15
+ * copied into local state. Local state is only the dialog's own transient UI:
16
+ * the active subview, the add-grantee buffer, the pending general-access
17
+ * confirm, and an in-flight save flag. Top-level open/close is the consumer's
18
+ * concern (see {@link ObjectShareDialog}).
19
+ *
20
+ * Mutations follow a **commit-on-interaction** model: each access change is
21
+ * sent immediately; the general-access toggle goes through a confirm step
22
+ * because it is high-impact. There is no batched Save. The list is fetched
23
+ * eagerly so `state.summary` also drives an inline access row while closed.
24
+ *
25
+ * @internal
26
+ */
27
+ export function useObjectShareController(target, onSaved, labels = NO_LABELS, labelsError = false, labelsLoading = false) {
28
+ const toast = useToastMessage();
29
+ // UI-local buffers — never backend data.
30
+ const [subview, setSubview] = useState("main");
31
+ const [pendingGrantees, setPendingGrantees] = useState([]);
32
+ const [pendingGeneralAccess, setPendingGeneralAccess] = useState(undefined);
33
+ // The backend access list + optimistic overlay (rows, general access, summary,
34
+ // the commit/picker primitives) live in their own hook.
35
+ const { targetKey, currentAccessList, grantees, granteeIdsKey, generalAccess, summary, status, loadError, commit, loadOptions, refForId, setOverlay, setKnownNames, setOptimisticGeneralAccess, } = useAccessList(target, onSaved);
36
+ // Per-label scope resolution + the single label-write path live in their own hook.
37
+ const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, optimisticScopeRef, reconcileLabelScope, } = useLabelScope(target, targetKey, labels, currentAccessList, granteeIdsKey, labelsError, labelsLoading);
38
+ // Drop the transient UI buffers when the permission target changes. The detail
39
+ // view is reused across objects and closes the dialog by toggling `isOpen`
40
+ // alone, so without this a staged add-grantee subview or general-access confirm
41
+ // would survive navigation and reappear on (and apply to) the next object.
42
+ // Reset during render — React's "adjust state on prop change" idiom — rather
43
+ // than in an effect, so the stale buffers never reach a commit.
44
+ const [lastTargetKey, setLastTargetKey] = useState(targetKey);
45
+ if (lastTargetKey !== targetKey) {
46
+ setLastTargetKey(targetKey);
47
+ setSubview("main");
48
+ setPendingGrantees([]);
49
+ setPendingGeneralAccess(undefined);
50
+ }
51
+ const reset = useCallback(() => {
52
+ setSubview("main");
53
+ setPendingGrantees([]);
54
+ setPendingGeneralAccess(undefined);
55
+ }, []);
56
+ const openAddGrantee = useCallback(() => {
57
+ setSubview("addGrantee");
58
+ setPendingGrantees([]);
59
+ }, []);
60
+ const closeAddGrantee = useCallback(() => {
61
+ setSubview("main");
62
+ setPendingGrantees([]);
63
+ }, []);
64
+ const confirmAddGrantees = useCallback(async () => {
65
+ if (pendingGrantees.length === 0) {
66
+ return;
67
+ }
68
+ // Optimistically insert the picked grantees as pending rows, carrying the
69
+ // picker's display name so the new row never renders a raw id.
70
+ const added = pendingGrantees.map((g) => ({
71
+ id: g.id,
72
+ grantee: {
73
+ id: g.id,
74
+ kind: g.kind,
75
+ granteeRef: refForId(g.id),
76
+ name: g.name,
77
+ level: g.permissionLevel,
78
+ },
79
+ }));
80
+ const mutations = pendingGrantees.map((g) => toGranularGrantee(g.kind, refForId(g.id), g.permissionLevel));
81
+ setOverlay((prev) => {
82
+ const next = { ...prev };
83
+ for (const { id, grantee } of added) {
84
+ next[id] = { op: "set", grantee, pending: true };
85
+ }
86
+ return next;
87
+ });
88
+ // Cache the picked names so the rows keep them after the overlay reconciles.
89
+ setKnownNames((prev) => {
90
+ const next = { ...prev };
91
+ for (const { id, grantee } of added) {
92
+ next[id] = grantee.name;
93
+ }
94
+ return next;
95
+ });
96
+ // Leave the add subview at once so the new rows show in the main list.
97
+ closeAddGrantee();
98
+ const ok = await commit(mutations, objectShareMessages.toastGranteeAdded);
99
+ if (!ok) {
100
+ // Roll back the failed adds; refresh() already reconciled on success.
101
+ setOverlay((prev) => {
102
+ const next = { ...prev };
103
+ for (const { id } of added) {
104
+ delete next[id];
105
+ }
106
+ return next;
107
+ });
108
+ return;
109
+ }
110
+ // Default each new grantee to ALL labels (the picker's Add is gated until
111
+ // labels have loaded, so the set is known here). Reflect the full scope
112
+ // optimistically and hold it through read-after-write lag; if any label
113
+ // write fails, surface it and drop that grantee's optimistic scope rather
114
+ // than claim access that didn't persist.
115
+ const allLabelIds = effectiveLabels.map((l) => l.id);
116
+ const allLabelIdSet = new Set(allLabelIds);
117
+ for (const g of pendingGrantees) {
118
+ optimisticScopeRef.current.add(g.id);
119
+ }
120
+ setSelectedLabelIdsByGrantee((prev) => {
121
+ const next = { ...prev };
122
+ for (const g of pendingGrantees) {
123
+ next[g.id] = allLabelIds;
124
+ }
125
+ return next;
126
+ });
127
+ const scoped = await Promise.all(pendingGrantees.map((g) => reconcileLabelScope({ kind: g.kind, granteeRef: refForId(g.id) }, allLabelIdSet, EMPTY_IDS)));
128
+ const failed = pendingGrantees.filter((_, i) => !scoped[i]);
129
+ if (failed.length > 0) {
130
+ toast.addWarning(objectShareMessages.toastLabelScopePartial);
131
+ // Some non-primary label writes didn't persist. Don't drop the entry —
132
+ // a missing entry means "all selected", which would falsely show full
133
+ // access. Pin the failed grantees to the primary label only (the one
134
+ // that's always granted with the object), reflecting what actually stuck.
135
+ const primaryIds = effectiveLabels.filter((l) => l.isPrimary).map((l) => l.id);
136
+ setSelectedLabelIdsByGrantee((prev) => {
137
+ const next = { ...prev };
138
+ for (const g of failed) {
139
+ next[g.id] = primaryIds;
140
+ }
141
+ return next;
142
+ });
143
+ }
144
+ }, [
145
+ pendingGrantees,
146
+ commit,
147
+ closeAddGrantee,
148
+ effectiveLabels,
149
+ reconcileLabelScope,
150
+ refForId,
151
+ toast,
152
+ setOverlay,
153
+ setKnownNames,
154
+ setSelectedLabelIdsByGrantee,
155
+ optimisticScopeRef,
156
+ ]);
157
+ const changePermissionLevel = useCallback(async (granteeId, level) => {
158
+ const grantee = grantees.find((g) => g.id === granteeId);
159
+ if (!grantee || grantee.level === level || grantee.pending) {
160
+ return;
161
+ }
162
+ const previousLevel = grantee.level;
163
+ // Show the new level immediately; keep it through the backend's
164
+ // read-after-write lag (reconcileOverlay clears it once confirmed).
165
+ setOverlay((prev) => ({
166
+ ...prev,
167
+ [granteeId]: { op: "set", grantee: { ...grantee, level }, pending: true },
168
+ }));
169
+ const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, level)], objectShareMessages.toastAccessUpdated);
170
+ if (!ok) {
171
+ // Revert to the prior level on failure.
172
+ setOverlay((prev) => ({
173
+ ...prev,
174
+ [granteeId]: { op: "set", grantee: { ...grantee, level: previousLevel }, pending: false },
175
+ }));
176
+ }
177
+ }, [grantees, commit, setOverlay]);
178
+ const removeGrantee = useCallback(async (granteeId) => {
179
+ const grantee = grantees.find((g) => g.id === granteeId);
180
+ if (!grantee || grantee.pending) {
181
+ return;
182
+ }
183
+ // Mark the row removed but keep it visible (muted) until the write lands.
184
+ setOverlay((prev) => ({ ...prev, [granteeId]: { op: "remove", pending: true } }));
185
+ // Revoke the grantee's per-label grants too — they are independent
186
+ // access-list entries, so the object revoke alone would leave them
187
+ // behind. Reconcile to an empty scope (current = all permissionable).
188
+ const allCurrent = new Set(effectiveLabels.map((l) => l.id));
189
+ const principal = { kind: grantee.kind, granteeRef: grantee.granteeRef };
190
+ const labelsRevoked = await reconcileLabelScope(principal, EMPTY_IDS, allCurrent);
191
+ const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, "none")], objectShareMessages.toastAccessUpdated);
192
+ if (!ok) {
193
+ // Restore the row on failure — and re-grant the labels we just
194
+ // revoked, so a failed object revoke doesn't strip label access
195
+ // while the grantee row stays.
196
+ if (labelsRevoked) {
197
+ await reconcileLabelScope(principal, allCurrent, EMPTY_IDS);
198
+ }
199
+ setOverlay((prev) => {
200
+ const { [granteeId]: _omit, ...rest } = prev;
201
+ return rest;
202
+ });
203
+ }
204
+ else if (!labelsRevoked) {
205
+ // Object access is gone but some per-label grants couldn't be
206
+ // revoked — warn so the leftover scope isn't mistaken for success.
207
+ toast.addWarning(objectShareMessages.toastLabelScopePartial);
208
+ }
209
+ }, [grantees, commit, effectiveLabels, reconcileLabelScope, toast, setOverlay]);
210
+ const changeGranteeLabels = useCallback(async (granteeId, requested) => {
211
+ const grantee = grantees.find((g) => g.id === granteeId);
212
+ if (!grantee || !target) {
213
+ return;
214
+ }
215
+ // Primary label is always in scope; never let it be dropped.
216
+ const primaryIds = effectiveLabels.filter((l) => l.isPrimary).map((l) => l.id);
217
+ const nextScope = Array.from(new Set([...requested, ...primaryIds]));
218
+ const currentScope = selectedLabelIdsByGrantee[granteeId] ?? effectiveLabels.map((l) => l.id);
219
+ const desired = new Set(nextScope);
220
+ const current = new Set(currentScope);
221
+ // Optimistic: reflect the new scope immediately and hold it through lag.
222
+ const previousScope = currentScope;
223
+ optimisticScopeRef.current.add(granteeId);
224
+ setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [granteeId]: nextScope }));
225
+ const ok = await reconcileLabelScope({ kind: grantee.kind, granteeRef: grantee.granteeRef }, desired, current);
226
+ if (ok) {
227
+ toast.addSuccess(objectShareMessages.toastAccessUpdated);
228
+ onSaved?.();
229
+ // Keep the optimistic scope as the source of truth — re-resolving now
230
+ // would clobber it with the backend's read-after-write-stale value
231
+ // (same lag the grantee overlay handles).
232
+ }
233
+ else {
234
+ toast.addError(objectShareMessages.toastError);
235
+ setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [granteeId]: previousScope }));
236
+ }
237
+ }, [
238
+ grantees,
239
+ target,
240
+ effectiveLabels,
241
+ selectedLabelIdsByGrantee,
242
+ reconcileLabelScope,
243
+ toast,
244
+ onSaved,
245
+ setSelectedLabelIdsByGrantee,
246
+ optimisticScopeRef,
247
+ ]);
248
+ const requestGeneralAccessChange = useCallback((next) => {
249
+ if (next !== generalAccess) {
250
+ setPendingGeneralAccess(next);
251
+ }
252
+ }, [generalAccess]);
253
+ const confirmGeneralAccessChange = useCallback(async () => {
254
+ const next = pendingGeneralAccess;
255
+ if (!next || next === generalAccess) {
256
+ setPendingGeneralAccess(undefined);
257
+ return;
258
+ }
259
+ const previous = generalAccess;
260
+ // Apply optimistically and close the confirm at once — the radio + summary
261
+ // reflect `next` immediately; the write commits in the background.
262
+ setOptimisticGeneralAccess(next);
263
+ setPendingGeneralAccess(undefined);
264
+ const principal = { allWorkspaceUsers: true };
265
+ const allIds = new Set(effectiveLabels.map((l) => l.id));
266
+ // The workspace rule must cover every label too, or the workspace would
267
+ // hold object access while non-primary labels stay ungranted. Mirror it
268
+ // first (WORKSPACE → all labels, RESTRICTED → none), then write the object.
269
+ const [desired, current] = next === "WORKSPACE" ? [allIds, EMPTY_IDS] : [EMPTY_IDS, allIds];
270
+ const mirrored = await reconcileLabelScope(principal, desired, current);
271
+ if (!mirrored) {
272
+ // The label mirror failed: don't write the object grant on top of a
273
+ // half-applied label scope. Undo whatever mirrored, revert the
274
+ // optimistic override and surface the error.
275
+ await reconcileLabelScope(principal, current, desired);
276
+ setOptimisticGeneralAccess((value) => (value === next ? previous : value));
277
+ toast.addError(objectShareMessages.toastError);
278
+ return;
279
+ }
280
+ const ok = await commit([granularGranteeFor(principal, next === "WORKSPACE" ? "VIEW" : "none")], objectShareMessages.toastGeneralAccessUpdated);
281
+ if (!ok) {
282
+ // Object write failed: undo the label mirror too, so labels and object
283
+ // don't drift, and revert the optimistic override.
284
+ await reconcileLabelScope(principal, current, desired);
285
+ setOptimisticGeneralAccess((value) => (value === next ? previous : value));
286
+ }
287
+ }, [
288
+ pendingGeneralAccess,
289
+ generalAccess,
290
+ commit,
291
+ effectiveLabels,
292
+ reconcileLabelScope,
293
+ toast,
294
+ setOptimisticGeneralAccess,
295
+ ]);
296
+ const actions = useMemo(() => ({
297
+ reset,
298
+ openAddGrantee,
299
+ closeAddGrantee,
300
+ setPendingGrantees,
301
+ loadOptions,
302
+ confirmAddGrantees,
303
+ changePermissionLevel,
304
+ removeGrantee,
305
+ changeGranteeLabels,
306
+ requestGeneralAccessChange,
307
+ cancelGeneralAccessChange: () => setPendingGeneralAccess(undefined),
308
+ confirmGeneralAccessChange,
309
+ }), [
310
+ reset,
311
+ openAddGrantee,
312
+ closeAddGrantee,
313
+ loadOptions,
314
+ confirmAddGrantees,
315
+ changePermissionLevel,
316
+ removeGrantee,
317
+ changeGranteeLabels,
318
+ requestGeneralAccessChange,
319
+ confirmGeneralAccessChange,
320
+ ]);
321
+ const state = useMemo(() => ({
322
+ subview,
323
+ status,
324
+ error: loadError,
325
+ summary,
326
+ grantees,
327
+ generalAccess,
328
+ labels: effectiveLabels,
329
+ labelsResolved,
330
+ selectedLabelIdsByGrantee,
331
+ pendingGeneralAccess,
332
+ pendingGrantees,
333
+ }), [
334
+ subview,
335
+ status,
336
+ loadError,
337
+ summary,
338
+ grantees,
339
+ generalAccess,
340
+ effectiveLabels,
341
+ labelsResolved,
342
+ selectedLabelIdsByGrantee,
343
+ pendingGeneralAccess,
344
+ pendingGrantees,
345
+ ]);
346
+ return useMemo(() => ({ state, actions }), [state, actions]);
347
+ }
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.7"
8
+ "packageVersion": "7.58.9"
9
9
  }
10
10
  ]
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-ext",
3
- "version": "11.43.0-alpha.0",
3
+ "version": "11.43.0-alpha.2",
4
4
  "description": "GoodData.UI SDK - Extensions",
5
5
  "license": "LicenseRef-LICENSE",
6
6
  "author": "GoodData Corporation",
@@ -64,20 +64,20 @@
64
64
  "ts-invariant": "0.10.3",
65
65
  "tslib": "2.8.1",
66
66
  "uuid": "11.1.1",
67
- "@gooddata/sdk-backend-base": "11.43.0-alpha.0",
68
- "@gooddata/sdk-embedding": "11.43.0-alpha.0",
69
- "@gooddata/sdk-backend-spi": "11.43.0-alpha.0",
70
- "@gooddata/sdk-model": "11.43.0-alpha.0",
71
- "@gooddata/sdk-ui": "11.43.0-alpha.0",
72
- "@gooddata/sdk-ui-charts": "11.43.0-alpha.0",
73
- "@gooddata/sdk-ui-filters": "11.43.0-alpha.0",
74
- "@gooddata/sdk-ui-geo": "11.43.0-alpha.0",
75
- "@gooddata/sdk-ui-pivot": "11.43.0-alpha.0",
76
- "@gooddata/sdk-ui-kit": "11.43.0-alpha.0",
77
- "@gooddata/sdk-ui-semantic-search": "11.43.0-alpha.0",
78
- "@gooddata/sdk-ui-vis-commons": "11.43.0-alpha.0",
79
- "@gooddata/sdk-ui-theme-provider": "11.43.0-alpha.0",
80
- "@gooddata/util": "11.43.0-alpha.0"
67
+ "@gooddata/sdk-backend-base": "11.43.0-alpha.2",
68
+ "@gooddata/sdk-backend-spi": "11.43.0-alpha.2",
69
+ "@gooddata/sdk-embedding": "11.43.0-alpha.2",
70
+ "@gooddata/sdk-model": "11.43.0-alpha.2",
71
+ "@gooddata/sdk-ui": "11.43.0-alpha.2",
72
+ "@gooddata/sdk-ui-charts": "11.43.0-alpha.2",
73
+ "@gooddata/sdk-ui-filters": "11.43.0-alpha.2",
74
+ "@gooddata/sdk-ui-geo": "11.43.0-alpha.2",
75
+ "@gooddata/sdk-ui-kit": "11.43.0-alpha.2",
76
+ "@gooddata/sdk-ui-semantic-search": "11.43.0-alpha.2",
77
+ "@gooddata/sdk-ui-theme-provider": "11.43.0-alpha.2",
78
+ "@gooddata/sdk-ui-pivot": "11.43.0-alpha.2",
79
+ "@gooddata/sdk-ui-vis-commons": "11.43.0-alpha.2",
80
+ "@gooddata/util": "11.43.0-alpha.2"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@microsoft/api-documenter": "^7.17.0",
@@ -125,12 +125,12 @@
125
125
  "typescript": "5.9.3",
126
126
  "vitest": "4.1.8",
127
127
  "vitest-dom": "0.1.1",
128
- "@gooddata/eslint-config": "11.43.0-alpha.0",
129
- "@gooddata/sdk-backend-mockingbird": "11.43.0-alpha.0",
130
- "@gooddata/i18n-toolkit": "11.43.0-alpha.0",
131
- "@gooddata/reference-workspace": "11.43.0-alpha.0",
132
- "@gooddata/stylelint-config": "11.43.0-alpha.0",
133
- "@gooddata/oxlint-config": "11.43.0-alpha.0"
128
+ "@gooddata/eslint-config": "11.43.0-alpha.2",
129
+ "@gooddata/i18n-toolkit": "11.43.0-alpha.2",
130
+ "@gooddata/reference-workspace": "11.43.0-alpha.2",
131
+ "@gooddata/oxlint-config": "11.43.0-alpha.2",
132
+ "@gooddata/sdk-backend-mockingbird": "11.43.0-alpha.2",
133
+ "@gooddata/stylelint-config": "11.43.0-alpha.2"
134
134
  },
135
135
  "peerDependencies": {
136
136
  "react": "^18.0.0 || ^19.0.0",