@gooddata/sdk-ui-ext 11.43.0-alpha.3 → 11.43.0-alpha.5
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 +641 -841
- package/esm/index.d.ts +2 -2
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.js +43 -1
- package/esm/internal/translations/en-AU.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-AU.localization-bundle.js +44 -2
- package/esm/internal/translations/en-GB.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-GB.localization-bundle.js +44 -2
- package/esm/internal/translations/es-419.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-419.localization-bundle.js +44 -2
- package/esm/internal/translations/es-ES.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-ES.localization-bundle.js +44 -2
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fi-FI.localization-bundle.js +43 -1
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-CA.localization-bundle.js +43 -1
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-FR.localization-bundle.js +43 -1
- package/esm/internal/translations/id-ID.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/id-ID.localization-bundle.js +43 -1
- package/esm/internal/translations/it-IT.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/it-IT.localization-bundle.js +43 -1
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ja-JP.localization-bundle.js +43 -1
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ko-KR.localization-bundle.js +43 -1
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/nl-NL.localization-bundle.js +43 -1
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pl-PL.localization-bundle.js +43 -1
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-BR.localization-bundle.js +43 -1
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-PT.localization-bundle.js +43 -1
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ru-RU.localization-bundle.js +43 -1
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/sl-SI.localization-bundle.js +43 -1
- package/esm/internal/translations/th-TH.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/th-TH.localization-bundle.js +43 -1
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/tr-TR.localization-bundle.js +43 -1
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/uk-UA.localization-bundle.js +43 -1
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/vi-VN.localization-bundle.js +43 -1
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-HK.localization-bundle.js +44 -2
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hans.localization-bundle.js +43 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.js +45 -3
- package/esm/sdk-ui-ext.d.ts +31 -10
- package/esm/share/ObjectShareDialog.js +2 -2
- package/esm/share/objectShareController.helpers.d.ts +1 -27
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +25 -48
- package/esm/share/objectShareController.types.d.ts +46 -4
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +31 -18
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +85 -133
- package/esm/share/useLabelScope.d.ts +6 -8
- package/esm/share/useLabelScope.d.ts.map +1 -1
- package/esm/share/useLabelScope.js +34 -28
- package/esm/share/useObjectShareController.d.ts +19 -16
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +212 -119
- package/package.json +21 -22
- package/esm/share/useObjectShare.d.ts +0 -50
- package/esm/share/useObjectShare.d.ts.map +0 -1
- package/esm/share/useObjectShare.js +0 -17
|
@@ -1,40 +1,56 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useToastMessage } from "@gooddata/sdk-ui-kit";
|
|
4
4
|
import { objectShareMessages } from "./messages.js";
|
|
5
|
-
import { EMPTY_IDS, NO_LABELS, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
|
|
5
|
+
import { EMPTY_IDS, NO_LABELS, effectivePermissionAbove, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
|
|
6
6
|
import { useAccessList } from "./useAccessList.js";
|
|
7
7
|
import { useLabelScope } from "./useLabelScope.js";
|
|
8
8
|
/**
|
|
9
|
-
* Manages the share dialog state and backend I/O for a single shareable
|
|
10
|
-
* object.
|
|
9
|
+
* Manages the share dialog state and backend I/O for a single shareable object.
|
|
11
10
|
*
|
|
12
|
-
* The access list is fetched
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* concern (see {@link ObjectShareDialog}).
|
|
11
|
+
* The access list is fetched once and seeded into local state, which is the
|
|
12
|
+
* source of truth while the dialog/summary is mounted. Each access change is
|
|
13
|
+
* written through to local state immediately and sent to the backend; on failure
|
|
14
|
+
* the one changed entry is rolled back. There is no post-write refetch, so the
|
|
15
|
+
* grantee list never blanks and never fights the backend's read-after-write lag.
|
|
16
|
+
* Top-level open/close is the consumer's concern (see {@link ObjectShareDialog}).
|
|
19
17
|
*
|
|
20
|
-
* Mutations follow a **commit-on-interaction** model: each access change is
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* Mutations follow a **commit-on-interaction** model: each access change is sent
|
|
19
|
+
* immediately; the general-access toggle goes through a confirm step because it
|
|
20
|
+
* is high-impact. There is no batched Save. The list is fetched eagerly so
|
|
21
|
+
* `state.summary` also drives an inline access row while closed.
|
|
22
|
+
*
|
|
23
|
+
* Most consumers do not call this directly — render {@link ObjectShareDialog} with
|
|
24
|
+
* plain props and it owns its controller. Call this (exported as `useObjectShare`)
|
|
25
|
+
* only to share a single access-list fetch between the dialog and an inline summary
|
|
26
|
+
* row: call it once, read `state.summary` for the row, and pass the controller into
|
|
27
|
+
* the dialog.
|
|
24
28
|
*
|
|
25
29
|
* @internal
|
|
26
30
|
*/
|
|
27
|
-
export function useObjectShareController(target,
|
|
31
|
+
export function useObjectShareController(target, options) {
|
|
32
|
+
const { onSaved, labels = NO_LABELS, labelsError = false, labelsLoading = false } = options ?? {};
|
|
28
33
|
const toast = useToastMessage();
|
|
29
34
|
// UI-local buffers — never backend data.
|
|
30
35
|
const [subview, setSubview] = useState("main");
|
|
31
36
|
const [pendingGrantees, setPendingGrantees] = useState([]);
|
|
32
37
|
const [pendingGeneralAccess, setPendingGeneralAccess] = useState(undefined);
|
|
33
|
-
// The backend access list
|
|
34
|
-
// the commit/picker primitives)
|
|
35
|
-
const { targetKey,
|
|
38
|
+
// The backend access list, seeded into local state (rows, general access,
|
|
39
|
+
// summary, the commit/picker primitives) lives in its own hook.
|
|
40
|
+
const { targetKey, hasList, grantees, generalAccess, workspaceLevel, summary, status, loadError, commit, loadOptions, refForId, setGrantees, setGeneralAccess, setWorkspaceLevel, setKnownNames, } = useAccessList(target, onSaved);
|
|
41
|
+
// Always-current target key, read inside async mutation finalizers that captured
|
|
42
|
+
// an older one. A write started for object A resolves after the user navigated to
|
|
43
|
+
// object B; because local state is authoritative (no refetch), applying A's
|
|
44
|
+
// finalizer to B's seeded list would corrupt B (e.g. clobber a same-id row, or
|
|
45
|
+
// revert B's general access). Each finalizer captures the target it started for
|
|
46
|
+
// and bails if it no longer matches.
|
|
47
|
+
const targetKeyRef = useRef(targetKey);
|
|
48
|
+
targetKeyRef.current = targetKey;
|
|
49
|
+
// The committed grantee ids the label-scope probe resolves against (a removing
|
|
50
|
+
// row is excluded so its scope is dropped, not re-seeded).
|
|
51
|
+
const committedGranteeIds = useMemo(() => grantees.filter((g) => g.pending !== "removing").map((g) => g.id), [grantees]);
|
|
36
52
|
// Per-label scope resolution + the single label-write path live in their own hook.
|
|
37
|
-
const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee,
|
|
53
|
+
const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, reconcileLabelScope, } = useLabelScope(target, targetKey, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
|
|
38
54
|
// Drop the transient UI buffers when the permission target changes. The detail
|
|
39
55
|
// view is reused across objects and closes the dialog by toggling `isOpen`
|
|
40
56
|
// alone, so without this a staged add-grantee subview or general-access confirm
|
|
@@ -61,70 +77,97 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
|
|
|
61
77
|
setSubview("main");
|
|
62
78
|
setPendingGrantees([]);
|
|
63
79
|
}, []);
|
|
80
|
+
// The single object+labels write path shared by remove and general access (the
|
|
81
|
+
// two changes that must keep the object grant and its label mirror consistent).
|
|
82
|
+
// Mirrors the labels first, then writes the object; if the object write fails it
|
|
83
|
+
// undoes the label mirror so the two never drift. `abortIfLabelsFail` distinguishes
|
|
84
|
+
// the two callers' label-failure policy: general access aborts (labels and object
|
|
85
|
+
// are one logical change), remove proceeds (the object revoke is what matters; a
|
|
86
|
+
// leftover label grant is only surfaced as a warning). Returns the object-write
|
|
87
|
+
// result and whether the label mirror fully applied; the caller owns the local
|
|
88
|
+
// write-through, the revert and any toast beyond `commit`'s own.
|
|
89
|
+
const applyAccessChange = useCallback(async (params) => {
|
|
90
|
+
const { principal, objectMutation, successMessage, labelDesired, labelCurrent } = params;
|
|
91
|
+
const labelsOk = await reconcileLabelScope(principal, labelDesired, labelCurrent);
|
|
92
|
+
if (!labelsOk && params.abortIfLabelsFail) {
|
|
93
|
+
// Don't write the object on a half-applied label scope: undo whatever
|
|
94
|
+
// mirrored and surface the failure to the caller.
|
|
95
|
+
await reconcileLabelScope(principal, labelCurrent, labelDesired);
|
|
96
|
+
return { ok: false, labelsOk: false };
|
|
97
|
+
}
|
|
98
|
+
const ok = await commit([objectMutation], successMessage);
|
|
99
|
+
if (!ok) {
|
|
100
|
+
// Object write failed — undo the label mirror so labels and object
|
|
101
|
+
// don't drift.
|
|
102
|
+
await reconcileLabelScope(principal, labelCurrent, labelDesired);
|
|
103
|
+
}
|
|
104
|
+
return { ok, labelsOk };
|
|
105
|
+
}, [commit, reconcileLabelScope]);
|
|
64
106
|
const confirmAddGrantees = useCallback(async () => {
|
|
65
107
|
if (pendingGrantees.length === 0) {
|
|
66
108
|
return;
|
|
67
109
|
}
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
110
|
+
const startedFor = targetKey;
|
|
111
|
+
// Insert the picked grantees as pending rows, carrying the picker's display
|
|
112
|
+
// name so the new row never renders a raw id.
|
|
113
|
+
const addedIds = pendingGrantees.map((g) => g.id);
|
|
114
|
+
const addedRows = pendingGrantees.map((g) => ({
|
|
71
115
|
id: g.id,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
level: g.permissionLevel,
|
|
78
|
-
},
|
|
116
|
+
kind: g.kind,
|
|
117
|
+
granteeRef: refForId(g.id),
|
|
118
|
+
name: g.name,
|
|
119
|
+
level: g.permissionLevel,
|
|
120
|
+
pending: "saving",
|
|
79
121
|
}));
|
|
80
122
|
const mutations = pendingGrantees.map((g) => toGranularGrantee(g.kind, refForId(g.id), g.permissionLevel));
|
|
81
|
-
|
|
123
|
+
// Default each new grantee to ALL labels (the picker's Add is gated until
|
|
124
|
+
// labels have loaded, so the set is known here). Reflect the full scope
|
|
125
|
+
// before the writes so the row shows it immediately.
|
|
126
|
+
const allLabelIds = effectiveLabels.map((l) => l.id);
|
|
127
|
+
const allLabelIdSet = new Set(allLabelIds);
|
|
128
|
+
setGrantees((prev) => [...prev.filter((g) => !addedIds.includes(g.id)), ...addedRows]);
|
|
129
|
+
setKnownNames((prev) => {
|
|
82
130
|
const next = { ...prev };
|
|
83
|
-
for (const
|
|
84
|
-
next[id] =
|
|
131
|
+
for (const g of pendingGrantees) {
|
|
132
|
+
next[g.id] = g.name;
|
|
85
133
|
}
|
|
86
134
|
return next;
|
|
87
135
|
});
|
|
88
|
-
|
|
89
|
-
setKnownNames((prev) => {
|
|
136
|
+
setSelectedLabelIdsByGrantee((prev) => {
|
|
90
137
|
const next = { ...prev };
|
|
91
|
-
for (const
|
|
92
|
-
next[id] =
|
|
138
|
+
for (const id of addedIds) {
|
|
139
|
+
next[id] = allLabelIds;
|
|
93
140
|
}
|
|
94
141
|
return next;
|
|
95
142
|
});
|
|
96
143
|
// Leave the add subview at once so the new rows show in the main list.
|
|
97
144
|
closeAddGrantee();
|
|
98
145
|
const ok = await commit(mutations, objectShareMessages.toastGranteeAdded);
|
|
146
|
+
// Bail if the target switched mid-add: the new object's list was already
|
|
147
|
+
// seeded, so rolling back / clearing markers / mirroring labels by these ids
|
|
148
|
+
// would hit the wrong object.
|
|
149
|
+
if (targetKeyRef.current !== startedFor) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
99
152
|
if (!ok) {
|
|
100
|
-
// Roll back the failed adds
|
|
101
|
-
|
|
153
|
+
// Roll back the failed adds.
|
|
154
|
+
setGrantees((prev) => prev.filter((g) => !addedIds.includes(g.id)));
|
|
155
|
+
setSelectedLabelIdsByGrantee((prev) => {
|
|
102
156
|
const next = { ...prev };
|
|
103
|
-
for (const
|
|
157
|
+
for (const id of addedIds) {
|
|
104
158
|
delete next[id];
|
|
105
159
|
}
|
|
106
160
|
return next;
|
|
107
161
|
});
|
|
108
162
|
return;
|
|
109
163
|
}
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
//
|
|
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
|
-
});
|
|
164
|
+
// Object grant landed — clear the saving marker on the new rows.
|
|
165
|
+
setGrantees((prev) => prev.map((g) => (addedIds.includes(g.id) ? { ...g, pending: undefined } : g)));
|
|
166
|
+
// Mirror each new grantee's full label scope; pin failures to primary-only.
|
|
127
167
|
const scoped = await Promise.all(pendingGrantees.map((g) => reconcileLabelScope({ kind: g.kind, granteeRef: refForId(g.id) }, allLabelIdSet, EMPTY_IDS)));
|
|
168
|
+
if (targetKeyRef.current !== startedFor) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
128
171
|
const failed = pendingGrantees.filter((_, i) => !scoped[i]);
|
|
129
172
|
if (failed.length > 0) {
|
|
130
173
|
toast.addWarning(objectShareMessages.toastLabelScopePartial);
|
|
@@ -143,70 +186,101 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
|
|
|
143
186
|
}
|
|
144
187
|
}, [
|
|
145
188
|
pendingGrantees,
|
|
189
|
+
targetKey,
|
|
146
190
|
commit,
|
|
147
191
|
closeAddGrantee,
|
|
148
192
|
effectiveLabels,
|
|
149
193
|
reconcileLabelScope,
|
|
150
194
|
refForId,
|
|
151
195
|
toast,
|
|
152
|
-
|
|
196
|
+
setGrantees,
|
|
153
197
|
setKnownNames,
|
|
154
198
|
setSelectedLabelIdsByGrantee,
|
|
155
|
-
optimisticScopeRef,
|
|
156
199
|
]);
|
|
157
200
|
const changePermissionLevel = useCallback(async (granteeId, level) => {
|
|
158
201
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
159
202
|
if (!grantee || grantee.level === level || grantee.pending) {
|
|
160
203
|
return;
|
|
161
204
|
}
|
|
205
|
+
const startedFor = targetKey;
|
|
162
206
|
const previousLevel = grantee.level;
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
207
|
+
// Recompute the inherited-SHARE badge for the new direct level: raising a
|
|
208
|
+
// VIEW grant to SHARE makes the inherited SHARE no longer "above" the
|
|
209
|
+
// direct grant (badge hidden); lowering it back surfaces the warning again.
|
|
210
|
+
const nextEffective = effectivePermissionAbove(level, grantee.inheritsShare ? ["SHARE"] : []);
|
|
211
|
+
// Show the new level immediately (marked saving), then write it through.
|
|
212
|
+
setGrantees((prev) => prev.map((g) => g.id === granteeId
|
|
213
|
+
? { ...g, level, effectivePermission: nextEffective, pending: "saving" }
|
|
214
|
+
: g));
|
|
169
215
|
const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, level)], objectShareMessages.toastAccessUpdated);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
[granteeId]: { op: "set", grantee: { ...grantee, level: previousLevel }, pending: false },
|
|
175
|
-
}));
|
|
216
|
+
// The target may have changed while the write was in flight; applying this
|
|
217
|
+
// finalizer to the newly seeded list would corrupt the other object's row.
|
|
218
|
+
if (targetKeyRef.current !== startedFor) {
|
|
219
|
+
return;
|
|
176
220
|
}
|
|
177
|
-
|
|
221
|
+
setGrantees((prev) => prev.map((g) => g.id === granteeId
|
|
222
|
+
? {
|
|
223
|
+
...g,
|
|
224
|
+
level: ok ? level : previousLevel,
|
|
225
|
+
effectivePermission: ok
|
|
226
|
+
? nextEffective
|
|
227
|
+
: effectivePermissionAbove(previousLevel, g.inheritsShare ? ["SHARE"] : []),
|
|
228
|
+
pending: undefined,
|
|
229
|
+
}
|
|
230
|
+
: g));
|
|
231
|
+
}, [grantees, targetKey, commit, setGrantees]);
|
|
178
232
|
const removeGrantee = useCallback(async (granteeId) => {
|
|
179
233
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
180
234
|
if (!grantee || grantee.pending) {
|
|
181
235
|
return;
|
|
182
236
|
}
|
|
237
|
+
const startedFor = targetKey;
|
|
183
238
|
// Mark the row removed but keep it visible (muted) until the write lands.
|
|
184
|
-
|
|
239
|
+
setGrantees((prev) => prev.map((g) => (g.id === granteeId ? { ...g, pending: "removing" } : g)));
|
|
185
240
|
// Revoke the grantee's per-label grants too — they are independent
|
|
186
|
-
// access-list entries, so the object revoke alone would leave them
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
241
|
+
// access-list entries, so the object revoke alone would leave them behind
|
|
242
|
+
// (current = all permissionable). The object revoke is what matters; a
|
|
243
|
+
// label revoke that fails is non-fatal (surfaced as a warning), so don't
|
|
244
|
+
// abort on it.
|
|
245
|
+
const { ok, labelsOk } = await applyAccessChange({
|
|
246
|
+
principal: { kind: grantee.kind, granteeRef: grantee.granteeRef },
|
|
247
|
+
objectMutation: toGranularGrantee(grantee.kind, grantee.granteeRef, "none"),
|
|
248
|
+
successMessage: objectShareMessages.toastAccessUpdated,
|
|
249
|
+
labelDesired: EMPTY_IDS,
|
|
250
|
+
labelCurrent: new Set(effectiveLabels.map((l) => l.id)),
|
|
251
|
+
abortIfLabelsFail: false,
|
|
252
|
+
});
|
|
253
|
+
// Bail if the target switched mid-write: the row no longer belongs to the
|
|
254
|
+
// seeded list, and dropping/restoring "it" would hit the other object.
|
|
255
|
+
if (targetKeyRef.current !== startedFor) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
192
258
|
if (!ok) {
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await reconcileLabelScope(principal, allCurrent, EMPTY_IDS);
|
|
198
|
-
}
|
|
199
|
-
setOverlay((prev) => {
|
|
200
|
-
const { [granteeId]: _omit, ...rest } = prev;
|
|
201
|
-
return rest;
|
|
202
|
-
});
|
|
259
|
+
// Object revoke failed (applyAccessChange already re-granted the
|
|
260
|
+
// labels) — restore the row.
|
|
261
|
+
setGrantees((prev) => prev.map((g) => (g.id === granteeId ? { ...g, pending: undefined } : g)));
|
|
262
|
+
return;
|
|
203
263
|
}
|
|
204
|
-
|
|
264
|
+
// Object revoke landed — drop the row from local state.
|
|
265
|
+
setGrantees((prev) => prev.filter((g) => g.id !== granteeId));
|
|
266
|
+
setSelectedLabelIdsByGrantee((prev) => {
|
|
267
|
+
const { [granteeId]: _omit, ...rest } = prev;
|
|
268
|
+
return rest;
|
|
269
|
+
});
|
|
270
|
+
if (!labelsOk) {
|
|
205
271
|
// Object access is gone but some per-label grants couldn't be
|
|
206
272
|
// revoked — warn so the leftover scope isn't mistaken for success.
|
|
207
273
|
toast.addWarning(objectShareMessages.toastLabelScopePartial);
|
|
208
274
|
}
|
|
209
|
-
}, [
|
|
275
|
+
}, [
|
|
276
|
+
grantees,
|
|
277
|
+
targetKey,
|
|
278
|
+
applyAccessChange,
|
|
279
|
+
effectiveLabels,
|
|
280
|
+
toast,
|
|
281
|
+
setGrantees,
|
|
282
|
+
setSelectedLabelIdsByGrantee,
|
|
283
|
+
]);
|
|
210
284
|
const changeGranteeLabels = useCallback(async (granteeId, requested) => {
|
|
211
285
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
212
286
|
if (!grantee || !target) {
|
|
@@ -218,17 +292,19 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
|
|
|
218
292
|
const currentScope = selectedLabelIdsByGrantee[granteeId] ?? effectiveLabels.map((l) => l.id);
|
|
219
293
|
const desired = new Set(nextScope);
|
|
220
294
|
const current = new Set(currentScope);
|
|
221
|
-
|
|
295
|
+
const startedFor = targetKey;
|
|
296
|
+
// Reflect the new scope immediately; restore it on failure.
|
|
222
297
|
const previousScope = currentScope;
|
|
223
|
-
optimisticScopeRef.current.add(granteeId);
|
|
224
298
|
setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [granteeId]: nextScope }));
|
|
225
299
|
const ok = await reconcileLabelScope({ kind: grantee.kind, granteeRef: grantee.granteeRef }, desired, current);
|
|
300
|
+
// Bail if the target switched mid-write — the scope map now belongs to the
|
|
301
|
+
// other object, so neither the success no-op nor the rollback applies here.
|
|
302
|
+
if (targetKeyRef.current !== startedFor) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
226
305
|
if (ok) {
|
|
227
306
|
toast.addSuccess(objectShareMessages.toastAccessUpdated);
|
|
228
307
|
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
308
|
}
|
|
233
309
|
else {
|
|
234
310
|
toast.addError(objectShareMessages.toastError);
|
|
@@ -237,13 +313,13 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
|
|
|
237
313
|
}, [
|
|
238
314
|
grantees,
|
|
239
315
|
target,
|
|
316
|
+
targetKey,
|
|
240
317
|
effectiveLabels,
|
|
241
318
|
selectedLabelIdsByGrantee,
|
|
242
319
|
reconcileLabelScope,
|
|
243
320
|
toast,
|
|
244
321
|
onSaved,
|
|
245
322
|
setSelectedLabelIdsByGrantee,
|
|
246
|
-
optimisticScopeRef,
|
|
247
323
|
]);
|
|
248
324
|
const requestGeneralAccessChange = useCallback((next) => {
|
|
249
325
|
if (next !== generalAccess) {
|
|
@@ -256,42 +332,59 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
|
|
|
256
332
|
setPendingGeneralAccess(undefined);
|
|
257
333
|
return;
|
|
258
334
|
}
|
|
335
|
+
const startedFor = targetKey;
|
|
259
336
|
const previous = generalAccess;
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
337
|
+
const previousWorkspaceLevel = workspaceLevel;
|
|
338
|
+
// Apply the new value and close the confirm at once — the radio + summary
|
|
339
|
+
// reflect `next` immediately; the write commits in the background. The
|
|
340
|
+
// workspace rule is always written as VIEW, so reflect VIEW now too —
|
|
341
|
+
// otherwise the summary would keep a SHARE level left over from the fetch.
|
|
342
|
+
setGeneralAccess(next);
|
|
343
|
+
if (next === "WORKSPACE") {
|
|
344
|
+
setWorkspaceLevel("VIEW");
|
|
345
|
+
}
|
|
263
346
|
setPendingGeneralAccess(undefined);
|
|
264
347
|
const principal = { allWorkspaceUsers: true };
|
|
265
348
|
const allIds = new Set(effectiveLabels.map((l) => l.id));
|
|
266
|
-
// The workspace rule must cover every label too, or the workspace would
|
|
267
|
-
//
|
|
268
|
-
//
|
|
349
|
+
// The workspace rule must cover every label too, or the workspace would hold
|
|
350
|
+
// object access while non-primary labels stay ungranted. Mirror the labels
|
|
351
|
+
// (WORKSPACE → all, RESTRICTED → none) then write the object; the two are one
|
|
352
|
+
// logical change, so abort if the labels fail.
|
|
269
353
|
const [desired, current] = next === "WORKSPACE" ? [allIds, EMPTY_IDS] : [EMPTY_IDS, allIds];
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
354
|
+
const { ok, labelsOk } = await applyAccessChange({
|
|
355
|
+
principal,
|
|
356
|
+
objectMutation: granularGranteeFor(principal, next === "WORKSPACE" ? "VIEW" : "none"),
|
|
357
|
+
successMessage: objectShareMessages.toastGeneralAccessUpdated,
|
|
358
|
+
labelDesired: desired,
|
|
359
|
+
labelCurrent: current,
|
|
360
|
+
abortIfLabelsFail: true,
|
|
361
|
+
});
|
|
362
|
+
// Bail if the target switched mid-write — reverting now would change the
|
|
363
|
+
// other object's general access.
|
|
364
|
+
if (targetKeyRef.current !== startedFor) {
|
|
278
365
|
return;
|
|
279
366
|
}
|
|
280
|
-
const ok = await commit([granularGranteeFor(principal, next === "WORKSPACE" ? "VIEW" : "none")], objectShareMessages.toastGeneralAccessUpdated);
|
|
281
367
|
if (!ok) {
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
368
|
+
// Labels or object write failed (applyAccessChange already undid the
|
|
369
|
+
// label mirror) — revert the value (and the workspace level). The
|
|
370
|
+
// label-abort path produced no toast, so surface the error here; a
|
|
371
|
+
// failed object write already toasted via commit, so don't double up.
|
|
372
|
+
setGeneralAccess(previous);
|
|
373
|
+
setWorkspaceLevel(previousWorkspaceLevel);
|
|
374
|
+
if (!labelsOk) {
|
|
375
|
+
toast.addError(objectShareMessages.toastError);
|
|
376
|
+
}
|
|
286
377
|
}
|
|
287
378
|
}, [
|
|
288
379
|
pendingGeneralAccess,
|
|
289
380
|
generalAccess,
|
|
290
|
-
|
|
381
|
+
workspaceLevel,
|
|
382
|
+
targetKey,
|
|
383
|
+
applyAccessChange,
|
|
291
384
|
effectiveLabels,
|
|
292
|
-
reconcileLabelScope,
|
|
293
385
|
toast,
|
|
294
|
-
|
|
386
|
+
setGeneralAccess,
|
|
387
|
+
setWorkspaceLevel,
|
|
295
388
|
]);
|
|
296
389
|
const actions = useMemo(() => ({
|
|
297
390
|
reset,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-ui-ext",
|
|
3
|
-
"version": "11.43.0-alpha.
|
|
3
|
+
"version": "11.43.0-alpha.5",
|
|
4
4
|
"description": "GoodData.UI SDK - Extensions",
|
|
5
5
|
"license": "LicenseRef-LICENSE",
|
|
6
6
|
"author": "GoodData Corporation",
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"date-fns": "4.1.0",
|
|
49
49
|
"debounce-promise": "^3.1.2",
|
|
50
50
|
"default-import": "1.1.5",
|
|
51
|
-
"fixed-data-table-2": "2.0.22",
|
|
52
51
|
"js-yaml": "^4.1.1",
|
|
53
52
|
"json-stable-stringify": "1.3.0",
|
|
54
53
|
"lodash-es": "^4.17.23",
|
|
@@ -64,20 +63,20 @@
|
|
|
64
63
|
"ts-invariant": "0.10.3",
|
|
65
64
|
"tslib": "2.8.1",
|
|
66
65
|
"uuid": "11.1.1",
|
|
67
|
-
"@gooddata/sdk-backend-base": "11.43.0-alpha.
|
|
68
|
-
"@gooddata/sdk-
|
|
69
|
-
"@gooddata/sdk-backend-spi": "11.43.0-alpha.
|
|
70
|
-
"@gooddata/sdk-
|
|
71
|
-
"@gooddata/sdk-ui": "11.43.0-alpha.
|
|
72
|
-
"@gooddata/sdk-ui-
|
|
73
|
-
"@gooddata/sdk-ui-filters": "11.43.0-alpha.
|
|
74
|
-
"@gooddata/sdk-ui-
|
|
75
|
-
"@gooddata/sdk-ui-kit": "11.43.0-alpha.
|
|
76
|
-
"@gooddata/sdk-ui-pivot": "11.43.0-alpha.
|
|
77
|
-
"@gooddata/sdk-ui-
|
|
78
|
-
"@gooddata/sdk-ui-
|
|
79
|
-
"@gooddata/sdk-ui-
|
|
80
|
-
"@gooddata/util": "11.43.0-alpha.
|
|
66
|
+
"@gooddata/sdk-backend-base": "11.43.0-alpha.5",
|
|
67
|
+
"@gooddata/sdk-embedding": "11.43.0-alpha.5",
|
|
68
|
+
"@gooddata/sdk-backend-spi": "11.43.0-alpha.5",
|
|
69
|
+
"@gooddata/sdk-model": "11.43.0-alpha.5",
|
|
70
|
+
"@gooddata/sdk-ui": "11.43.0-alpha.5",
|
|
71
|
+
"@gooddata/sdk-ui-charts": "11.43.0-alpha.5",
|
|
72
|
+
"@gooddata/sdk-ui-filters": "11.43.0-alpha.5",
|
|
73
|
+
"@gooddata/sdk-ui-geo": "11.43.0-alpha.5",
|
|
74
|
+
"@gooddata/sdk-ui-kit": "11.43.0-alpha.5",
|
|
75
|
+
"@gooddata/sdk-ui-pivot": "11.43.0-alpha.5",
|
|
76
|
+
"@gooddata/sdk-ui-semantic-search": "11.43.0-alpha.5",
|
|
77
|
+
"@gooddata/sdk-ui-vis-commons": "11.43.0-alpha.5",
|
|
78
|
+
"@gooddata/sdk-ui-theme-provider": "11.43.0-alpha.5",
|
|
79
|
+
"@gooddata/util": "11.43.0-alpha.5"
|
|
81
80
|
},
|
|
82
81
|
"devDependencies": {
|
|
83
82
|
"@microsoft/api-documenter": "^7.17.0",
|
|
@@ -125,12 +124,12 @@
|
|
|
125
124
|
"typescript": "5.9.3",
|
|
126
125
|
"vitest": "4.1.8",
|
|
127
126
|
"vitest-dom": "0.1.1",
|
|
128
|
-
"@gooddata/eslint-config": "11.43.0-alpha.
|
|
129
|
-
"@gooddata/i18n-toolkit": "11.43.0-alpha.
|
|
130
|
-
"@gooddata/oxlint-config": "11.43.0-alpha.
|
|
131
|
-
"@gooddata/
|
|
132
|
-
"@gooddata/
|
|
133
|
-
"@gooddata/stylelint-config": "11.43.0-alpha.
|
|
127
|
+
"@gooddata/eslint-config": "11.43.0-alpha.5",
|
|
128
|
+
"@gooddata/i18n-toolkit": "11.43.0-alpha.5",
|
|
129
|
+
"@gooddata/oxlint-config": "11.43.0-alpha.5",
|
|
130
|
+
"@gooddata/reference-workspace": "11.43.0-alpha.5",
|
|
131
|
+
"@gooddata/sdk-backend-mockingbird": "11.43.0-alpha.5",
|
|
132
|
+
"@gooddata/stylelint-config": "11.43.0-alpha.5"
|
|
134
133
|
},
|
|
135
134
|
"peerDependencies": {
|
|
136
135
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
|
|
2
|
-
import type { IObjectShareController } from "./objectShareController.types.js";
|
|
3
|
-
import type { IObjectShareLabel } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
* Options for {@link useObjectShare}.
|
|
6
|
-
*
|
|
7
|
-
* @internal
|
|
8
|
-
*/
|
|
9
|
-
export interface IUseObjectShareOptions {
|
|
10
|
-
/**
|
|
11
|
-
* Fires after each successful access mutation (add grantee, change level,
|
|
12
|
-
* remove, general access toggle). Use it to keep UI outside the dialog in
|
|
13
|
-
* sync with edits made inside it (e.g. refresh an inline access row).
|
|
14
|
-
*/
|
|
15
|
-
onSaved?: () => void;
|
|
16
|
-
/**
|
|
17
|
-
* Labels (display forms) of the shared attribute, enabling the per-grantee
|
|
18
|
-
* label-scope picker. Omit for objects without labels (e.g. facts).
|
|
19
|
-
*/
|
|
20
|
-
labels?: IObjectShareLabel[];
|
|
21
|
-
/**
|
|
22
|
-
* Whether loading the object's labels failed. While true the controller stays
|
|
23
|
-
* label-unresolved so every access-changing control is disabled: with the
|
|
24
|
-
* label set unknown, reconciling access would diff against an empty set and
|
|
25
|
-
* silently orphan any real per-label grants. Distinct from an object that
|
|
26
|
-
* genuinely has no labels (omit `labels`), where editing is safe.
|
|
27
|
-
*/
|
|
28
|
-
labelsError?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Whether the object's labels are still loading. While true the controller stays
|
|
31
|
-
* label-unresolved (same gating as `labelsError`): the labels aren't passed yet,
|
|
32
|
-
* so an empty list must not be mistaken for a label-free object — otherwise row
|
|
33
|
-
* controls would reconcile against an empty set and orphan real per-label grants.
|
|
34
|
-
*/
|
|
35
|
-
labelsLoading?: boolean;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Connected controller hook backing {@link ObjectShareDialog}. Most consumers
|
|
39
|
-
* do not need this — render `ObjectShareDialog` with plain props and it owns
|
|
40
|
-
* its controller. Use this hook directly only to share a single access-list
|
|
41
|
-
* fetch between the dialog and an inline summary row: call it once, read
|
|
42
|
-
* `state.summary` for the row, and pass the controller into the dialog.
|
|
43
|
-
*
|
|
44
|
-
* The hook eagerly fetches the access list on mount so `state.summary` is
|
|
45
|
-
* usable while the dialog is closed.
|
|
46
|
-
*
|
|
47
|
-
* @internal
|
|
48
|
-
*/
|
|
49
|
-
export declare function useObjectShare(target: IObjectPermissionsObject | undefined, options?: IUseObjectShareOptions): IObjectShareController;
|
|
50
|
-
//# sourceMappingURL=useObjectShare.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectShare.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShare.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAE1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC1B,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CAQxB"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// (C) 2026 GoodData Corporation
|
|
2
|
-
import { useObjectShareController } from "./useObjectShareController.js";
|
|
3
|
-
/**
|
|
4
|
-
* Connected controller hook backing {@link ObjectShareDialog}. Most consumers
|
|
5
|
-
* do not need this — render `ObjectShareDialog` with plain props and it owns
|
|
6
|
-
* its controller. Use this hook directly only to share a single access-list
|
|
7
|
-
* fetch between the dialog and an inline summary row: call it once, read
|
|
8
|
-
* `state.summary` for the row, and pass the controller into the dialog.
|
|
9
|
-
*
|
|
10
|
-
* The hook eagerly fetches the access list on mount so `state.summary` is
|
|
11
|
-
* usable while the dialog is closed.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
export function useObjectShare(target, options) {
|
|
16
|
-
return useObjectShareController(target, options?.onSaved, options?.labels, options?.labelsError, options?.labelsLoading);
|
|
17
|
-
}
|