@gooddata/sdk-ui-ext 11.52.0-alpha.6 → 11.52.0-alpha.7
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/esm/dataLoaders/InsightDataLoader.d.ts +6 -0
- package/esm/dataLoaders/InsightDataLoader.d.ts.map +1 -1
- package/esm/dataLoaders/InsightDataLoader.js +4 -1
- package/esm/dataLoaders/index.d.ts +13 -0
- package/esm/dataLoaders/index.d.ts.map +1 -1
- package/esm/dataLoaders/index.js +14 -0
- package/esm/index.d.ts +1 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -1
- package/esm/insightView/InsightRenderer.d.ts +2 -5
- package/esm/insightView/InsightRenderer.d.ts.map +1 -1
- package/esm/insightView/InsightRenderer.js +1 -2
- package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.d.ts +6 -1
- package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.js +25 -4
- package/esm/internal/components/pluggableVisualizations/pivotTable/PluggablePivotTable.d.ts.map +1 -1
- package/esm/internal/components/pluggableVisualizations/pivotTable/PluggablePivotTable.js +0 -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/notificationsPanel/Notification/Notification.d.ts.map +1 -1
- package/esm/notificationsPanel/Notification/Notification.js +6 -3
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.d.ts +3 -3
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.d.ts.map +1 -1
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.js +4 -4
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.d.ts +9 -3
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.d.ts.map +1 -1
- package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.js +4 -4
- package/esm/sdk-ui-ext.d.ts +13 -0
- package/esm/share/ObjectShareDialog.js +22 -6
- 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 +117 -17
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +181 -44
- package/esm/share/objectShareController.types.d.ts +34 -10
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +6 -0
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +26 -6
- package/esm/share/useLabelScope.d.ts +46 -1
- package/esm/share/useLabelScope.d.ts.map +1 -1
- package/esm/share/useLabelScope.js +98 -6
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +213 -45
- package/esm/share/useObjectShareDialog.d.ts +7 -0
- package/esm/share/useObjectShareDialog.d.ts.map +1 -1
- package/esm/share/useObjectShareDialog.js +22 -2
- package/package.json +21 -21
|
@@ -3,7 +3,7 @@ import { useCallback, useMemo, useState } from "react";
|
|
|
3
3
|
import { useToastMessage } from "@gooddata/sdk-ui-kit";
|
|
4
4
|
import { composeEffectiveWorkspaceAccess } from "./accessSummary.js";
|
|
5
5
|
import { objectShareMessages } from "./messages.js";
|
|
6
|
-
import { EMPTY_IDS, NO_LABELS, granularGranteeFor, levelsAbove, levelsBelow, sortGrantees, toGranularGrantee, userDisplayPair, } from "./objectShareController.helpers.js";
|
|
6
|
+
import { EMPTY_IDS, NO_LABELS, changesEffectiveLevel, granularGranteeFor, levelsAbove, levelsBelow, sortGrantees, toGranularGrantee, userDisplayPair, } from "./objectShareController.helpers.js";
|
|
7
7
|
import { useAccessList } from "./useAccessList.js";
|
|
8
8
|
import { useLabelScope } from "./useLabelScope.js";
|
|
9
9
|
/**
|
|
@@ -35,7 +35,7 @@ export function useObjectShareController(target, options) {
|
|
|
35
35
|
const [subview, setSubview] = useState("main");
|
|
36
36
|
const [pendingGrantees, setPendingGrantees] = useState([]);
|
|
37
37
|
const [pendingGeneralAccess, setPendingGeneralAccess] = useState(undefined);
|
|
38
|
-
const { hasList, grantees, seededWithoutSelfGrant, selfIdentity, selfIdentityResolved, generalAccess, workspaceLevel, workspaceInheritedLevel, workspaceLevelSaving, summary, status, loadError, commit, loadOptions, applyGranteeAdd, applyGranteeLevel, applyGranteeRemove, settleGranteeEdit, failGranteeEdit, applyRuleEdit, settleRuleEdit, failRuleEdit, } = useAccessList(target);
|
|
38
|
+
const { hasList, grantees, seededWithoutSelfGrant, selfIdentity, selfIdentityResolved, generalAccess, workspaceLevel, workspaceInheritedLevel, workspaceLevelSaving, summary, status, loadError, commit, loadOptions, applyGranteeAdd, applyGranteeLevel, applyGranteeLock, applyGranteeRemove, settleGranteeEdit, failGranteeEdit, applyRuleEdit, settleRuleEdit, failRuleEdit, } = useAccessList(target);
|
|
39
39
|
// Grantee ids the label-scope probe resolves against — a removing row is excluded
|
|
40
40
|
// so its scope is dropped, not re-seeded.
|
|
41
41
|
const committedGranteeIds = useMemo(() => {
|
|
@@ -48,7 +48,16 @@ export function useObjectShareController(target, options) {
|
|
|
48
48
|
return ids;
|
|
49
49
|
}, [grantees]);
|
|
50
50
|
// Per-label scope resolution + the single label-write path live in their own hook.
|
|
51
|
-
const { effectiveLabels, labelsResolved, labelsInitializing, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, reconcileLabelScope, } = useLabelScope(target, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
|
|
51
|
+
const { effectiveLabels, labelsResolved, labelsInitializing, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, inheritedLabelIdsByGrantee, directLabelIdsByGrantee, recordDirectLabelWrites, forgetGranteeLabels, reconcileLabelScope, reconcileLabelScopes, regradeLabelScope, } = useLabelScope(target, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
|
|
52
|
+
// The labels THIS workspace granted — the only ones a write may touch. Read straight
|
|
53
|
+
// from the tracked direct set rather than subtracting "inherited" from the scope: the
|
|
54
|
+
// subtraction relied on the probe still describing our own grants, and after a revoke it
|
|
55
|
+
// did not, so a later re-grade re-created access that had just been removed. A label
|
|
56
|
+
// held by inheritance ALONE is absent here, so no write can reach it: revoking one is a
|
|
57
|
+
// no-op whose compensation could mint a real grant, and re-grading one would invent one.
|
|
58
|
+
// A dual-granted label IS here — its local grant is ours to revoke and re-grade, or it
|
|
59
|
+
// would outlive the inherited grant hiding it.
|
|
60
|
+
const directLabelScopeOf = useCallback((granteeId) => new Set(directLabelIdsByGrantee[granteeId] ?? effectiveLabels.map((l) => l.id)), [directLabelIdsByGrantee, effectiveLabels]);
|
|
52
61
|
// Effective access: the direct state composed with inherited rule access.
|
|
53
62
|
// Also pins the level to VIEW while restricted, so the (hidden) dropdown
|
|
54
63
|
// never shows a stale SHARE left over from a prior grant.
|
|
@@ -74,12 +83,12 @@ export function useObjectShareController(target, options) {
|
|
|
74
83
|
// overlay, the revert and any toast beyond `commit`'s own.
|
|
75
84
|
const applyAccessChange = useCallback(async (params) => {
|
|
76
85
|
const { principal, objectMutation, successMessage, labelDesired, labelCurrent } = params;
|
|
77
|
-
const { ok: labelsOk } = await reconcileLabelScope([principal], labelDesired, labelCurrent);
|
|
86
|
+
const { ok: labelsOk, failedLabelIds } = await reconcileLabelScope([principal], labelDesired, labelCurrent);
|
|
78
87
|
if (!labelsOk && params.abortIfLabelsFail) {
|
|
79
88
|
// Don't write the object on a half-applied label scope: undo whatever
|
|
80
89
|
// mirrored and surface the failure to the caller.
|
|
81
90
|
await reconcileLabelScope([principal], labelCurrent, labelDesired);
|
|
82
|
-
return { ok: false, labelsOk: false };
|
|
91
|
+
return { ok: false, labelsOk: false, failedLabelIds };
|
|
83
92
|
}
|
|
84
93
|
const ok = await commit([objectMutation], successMessage);
|
|
85
94
|
if (!ok) {
|
|
@@ -87,7 +96,10 @@ export function useObjectShareController(target, options) {
|
|
|
87
96
|
// don't drift.
|
|
88
97
|
await reconcileLabelScope([principal], labelCurrent, labelDesired);
|
|
89
98
|
}
|
|
90
|
-
|
|
99
|
+
// The failed ids are reported, not just counted: a caller that keeps the row
|
|
100
|
+
// must keep those labels in its scope, or the local state would claim access
|
|
101
|
+
// the backend still grants and a later edit would diff against a lie.
|
|
102
|
+
return { ok, labelsOk, failedLabelIds };
|
|
91
103
|
}, [commit, reconcileLabelScope]);
|
|
92
104
|
// ── Grantee mutations (add / level / remove / labels) ────────────────────────
|
|
93
105
|
const confirmAddGrantees = useCallback(async () => {
|
|
@@ -105,18 +117,30 @@ export function useObjectShareController(target, options) {
|
|
|
105
117
|
name: g.name,
|
|
106
118
|
email: g.email,
|
|
107
119
|
level: g.permissionLevel,
|
|
120
|
+
// The add writes a grant in THIS workspace, so the new row holds a
|
|
121
|
+
// direct grant at the picked level — without it the row would read as
|
|
122
|
+
// inherited-only and its Remove would be (correctly) refused.
|
|
123
|
+
directLevel: g.permissionLevel,
|
|
108
124
|
}));
|
|
109
125
|
const mutations = pendingGrantees.map((g) => toGranularGrantee(g.kind, g.ref, g.permissionLevel));
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
126
|
+
// Each grantee is granted the label scope picked in the add dialog; an
|
|
127
|
+
// untouched picker leaves it undefined, which means ALL labels (the picker's
|
|
128
|
+
// Add is gated until labels have loaded, so the set is known here). The
|
|
129
|
+
// primary label is always in scope, exactly as `changeGranteeLabels` keeps it.
|
|
130
|
+
// Reflect each scope before the writes so the rows show them immediately.
|
|
113
131
|
const allLabelIds = effectiveLabels.map((l) => l.id);
|
|
114
|
-
const
|
|
132
|
+
const primaryLabelIds = effectiveLabels.filter((l) => l.isPrimary).map((l) => l.id);
|
|
133
|
+
const scopeById = new Map(pendingGrantees.map((g) => [
|
|
134
|
+
g.id,
|
|
135
|
+
g.labelIds === undefined
|
|
136
|
+
? allLabelIds
|
|
137
|
+
: Array.from(new Set([...g.labelIds, ...primaryLabelIds])),
|
|
138
|
+
]));
|
|
115
139
|
addedRows.forEach(applyGranteeAdd);
|
|
116
140
|
setSelectedLabelIdsByGrantee((prev) => {
|
|
117
141
|
const next = { ...prev };
|
|
118
142
|
for (const id of addedIds) {
|
|
119
|
-
next[id] =
|
|
143
|
+
next[id] = scopeById.get(id);
|
|
120
144
|
}
|
|
121
145
|
return next;
|
|
122
146
|
});
|
|
@@ -134,19 +158,30 @@ export function useObjectShareController(target, options) {
|
|
|
134
158
|
});
|
|
135
159
|
return;
|
|
136
160
|
}
|
|
137
|
-
// One label write per label carrying all added grantees, not one per grantee
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
// One label write per label carrying all added grantees, not one per grantee —
|
|
162
|
+
// even when their scopes differ, so a shared label stays a single call. Each
|
|
163
|
+
// label grant carries that grantee's picked level, so the label mirrors the
|
|
164
|
+
// object grant instead of landing on VIEW.
|
|
165
|
+
const { ok: labelsOk, failedLabelIds } = await reconcileLabelScopes(pendingGrantees.map((g) => ({
|
|
166
|
+
principal: { kind: g.kind, granteeRef: g.ref, level: g.permissionLevel },
|
|
167
|
+
desiredLabelIds: new Set(scopeById.get(g.id)),
|
|
168
|
+
currentLabelIds: EMPTY_IDS,
|
|
169
|
+
})));
|
|
170
|
+
const failed = new Set(failedLabelIds);
|
|
171
|
+
// Every label grant that landed is now a grant of OURS — record it, or a later
|
|
172
|
+
// removal would not know to revoke it.
|
|
173
|
+
for (const id of addedIds) {
|
|
174
|
+
recordDirectLabelWrites(id, scopeById.get(id).filter((labelId) => !failed.has(labelId)), []);
|
|
175
|
+
}
|
|
140
176
|
if (!labelsOk) {
|
|
141
177
|
toast.addWarning(objectShareMessages.toastLabelScopePartial);
|
|
142
|
-
// Drop only the failed labels
|
|
143
|
-
// scope under-reports and changeGranteeLabels
|
|
144
|
-
|
|
145
|
-
const survived = allLabelIds.filter((id) => !failed.has(id));
|
|
178
|
+
// Drop only the failed labels from each grantee's own scope, keeping those
|
|
179
|
+
// that landed — otherwise local scope under-reports and changeGranteeLabels
|
|
180
|
+
// later skips their revokes.
|
|
146
181
|
setSelectedLabelIdsByGrantee((prev) => {
|
|
147
182
|
const next = { ...prev };
|
|
148
183
|
for (const id of addedIds) {
|
|
149
|
-
next[id] =
|
|
184
|
+
next[id] = scopeById.get(id).filter((labelId) => !failed.has(labelId));
|
|
150
185
|
}
|
|
151
186
|
return next;
|
|
152
187
|
});
|
|
@@ -161,7 +196,8 @@ export function useObjectShareController(target, options) {
|
|
|
161
196
|
commit,
|
|
162
197
|
closeAddGrantee,
|
|
163
198
|
effectiveLabels,
|
|
164
|
-
|
|
199
|
+
reconcileLabelScopes,
|
|
200
|
+
recordDirectLabelWrites,
|
|
165
201
|
toast,
|
|
166
202
|
applyGranteeAdd,
|
|
167
203
|
settleGranteeEdit,
|
|
@@ -170,23 +206,83 @@ export function useObjectShareController(target, options) {
|
|
|
170
206
|
]);
|
|
171
207
|
const changePermissionLevel = useCallback(async (granteeId, level) => {
|
|
172
208
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
173
|
-
|
|
209
|
+
// A pick that cannot move the effective level is refused rather than quietly
|
|
210
|
+
// strengthening the grant made here — the rule lives in `changesEffectiveLevel`
|
|
211
|
+
// so every reaction to a pick agrees, and no confirm is staged for a no-op.
|
|
212
|
+
// Also refused before the label scope resolves: the re-grade below would run
|
|
213
|
+
// over the assumed-all fallback and mint grants the grantee never held. The
|
|
214
|
+
// UI gates on the same fact (isMutable); the controller stays authoritative.
|
|
215
|
+
if (!grantee || grantee.pending || !labelsResolved || !changesEffectiveLevel(grantee, level)) {
|
|
174
216
|
return;
|
|
175
217
|
}
|
|
176
218
|
applyGranteeLevel(granteeId, level);
|
|
219
|
+
// A label grant is an independent access-list entry carrying its own
|
|
220
|
+
// level, so re-grading the object alone would leave the grantee's labels
|
|
221
|
+
// at the level they were granted at.
|
|
222
|
+
//
|
|
223
|
+
// Order so a label grant is never left above the object grant: lower the
|
|
224
|
+
// labels BEFORE lowering the object, and raise the object BEFORE raising
|
|
225
|
+
// the labels. Either way a failure leaves labels no broader than the
|
|
226
|
+
// object, and a failed object write puts the labels back.
|
|
227
|
+
const previousLevel = grantee.directLevel ?? grantee.level;
|
|
228
|
+
const lowering = levelsBelow(previousLevel).includes(level);
|
|
229
|
+
const scope = directLabelScopeOf(granteeId);
|
|
230
|
+
const regradeLabels = (to) => regradeLabelScope([{ kind: grantee.kind, granteeRef: grantee.granteeRef, level: to }], scope);
|
|
231
|
+
let labelsOk = true;
|
|
232
|
+
if (lowering) {
|
|
233
|
+
labelsOk = (await regradeLabels(level)).ok;
|
|
234
|
+
if (!labelsOk) {
|
|
235
|
+
// A label that kept the old, HIGHER level while the object dropped
|
|
236
|
+
// would leave label access above object access — the one state this
|
|
237
|
+
// ordering exists to prevent. So abort: put every in-scope label back
|
|
238
|
+
// and leave the object alone. Same all-or-nothing policy the
|
|
239
|
+
// general-access change applies to its label mirror.
|
|
240
|
+
await regradeLabels(previousLevel);
|
|
241
|
+
failGranteeEdit(granteeId);
|
|
242
|
+
toast.addError(objectShareMessages.toastError);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
177
246
|
const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, level)], objectShareMessages.toastAccessUpdated);
|
|
178
247
|
// The settled entry persists on success (base still holds the old level);
|
|
179
248
|
// failure restores the last committed entry, or the fetched row.
|
|
180
|
-
if (ok) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
249
|
+
if (!ok) {
|
|
250
|
+
if (lowering) {
|
|
251
|
+
await regradeLabels(previousLevel);
|
|
252
|
+
}
|
|
184
253
|
failGranteeEdit(granteeId);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!lowering) {
|
|
257
|
+
labelsOk = (await regradeLabels(level)).ok;
|
|
258
|
+
}
|
|
259
|
+
settleGranteeEdit(granteeId);
|
|
260
|
+
if (!labelsOk) {
|
|
261
|
+
// The object level changed but some labels kept the old one — warn so
|
|
262
|
+
// the partial state isn't mistaken for success.
|
|
263
|
+
toast.addWarning(objectShareMessages.toastLabelScopePartial);
|
|
185
264
|
}
|
|
186
|
-
}, [
|
|
265
|
+
}, [
|
|
266
|
+
grantees,
|
|
267
|
+
labelsResolved,
|
|
268
|
+
commit,
|
|
269
|
+
directLabelScopeOf,
|
|
270
|
+
regradeLabelScope,
|
|
271
|
+
toast,
|
|
272
|
+
applyGranteeLevel,
|
|
273
|
+
settleGranteeEdit,
|
|
274
|
+
failGranteeEdit,
|
|
275
|
+
]);
|
|
187
276
|
const removeGrantee = useCallback(async (granteeId) => {
|
|
188
277
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
189
|
-
|
|
278
|
+
// Only a grant made in THIS workspace can be revoked here. With access
|
|
279
|
+
// inherited-only (a group, or a parent workspace) there is nothing local
|
|
280
|
+
// to revoke: the write would be an empty-permissions no-op that the row
|
|
281
|
+
// then reported as removed. The menu also disables Remove in that case;
|
|
282
|
+
// this keeps the controller — not the UI — authoritative. Same for an
|
|
283
|
+
// unresolved label scope: the revoke below would run over the assumed-all
|
|
284
|
+
// fallback and revoke (then compensate with) grants that never existed.
|
|
285
|
+
if (!grantee || grantee.pending || !labelsResolved || grantee.directLevel === undefined) {
|
|
190
286
|
return;
|
|
191
287
|
}
|
|
192
288
|
// Mark the row removed but keep it visible (muted) until the write lands.
|
|
@@ -198,12 +294,20 @@ export function useObjectShareController(target, options) {
|
|
|
198
294
|
// the grantee didn't hold. The object revoke is what matters; a label
|
|
199
295
|
// revoke that fails is non-fatal (surfaced as a warning), so don't abort
|
|
200
296
|
// on it.
|
|
201
|
-
const { ok, labelsOk } = await applyAccessChange({
|
|
202
|
-
|
|
297
|
+
const { ok, labelsOk, failedLabelIds } = await applyAccessChange({
|
|
298
|
+
// The level matters only for the compensating re-grant if the object
|
|
299
|
+
// revoke fails: without it the labels would come back at the default
|
|
300
|
+
// VIEW, quietly downgrading an EDIT/SHARE grantee we just failed to
|
|
301
|
+
// remove.
|
|
302
|
+
principal: {
|
|
303
|
+
kind: grantee.kind,
|
|
304
|
+
granteeRef: grantee.granteeRef,
|
|
305
|
+
level: grantee.directLevel,
|
|
306
|
+
},
|
|
203
307
|
objectMutation: toGranularGrantee(grantee.kind, grantee.granteeRef, "none"),
|
|
204
308
|
successMessage: objectShareMessages.toastAccessUpdated,
|
|
205
309
|
labelDesired: EMPTY_IDS,
|
|
206
|
-
labelCurrent:
|
|
310
|
+
labelCurrent: directLabelScopeOf(granteeId),
|
|
207
311
|
abortIfLabelsFail: false,
|
|
208
312
|
});
|
|
209
313
|
if (!ok) {
|
|
@@ -215,10 +319,37 @@ export function useObjectShareController(target, options) {
|
|
|
215
319
|
// Object revoke landed. For a fetched row the settled `removed` entry
|
|
216
320
|
// persists to keep it hidden; a revoked overlay-born row drops entirely.
|
|
217
321
|
settleGranteeEdit(granteeId);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
322
|
+
// Every local label grant is gone except the ones whose revoke failed — record
|
|
323
|
+
// that, so a later write on a surviving row cannot act on grants we removed.
|
|
324
|
+
const revokedIds = [...directLabelScopeOf(granteeId)].filter((id) => !failedLabelIds.includes(id));
|
|
325
|
+
recordDirectLabelWrites(granteeId, [], revokedIds);
|
|
326
|
+
if (grantee.inheritedLevel === undefined) {
|
|
327
|
+
// Row is gone for good; drop its bookkeeping — unless a label revoke
|
|
328
|
+
// failed. The direct set is then the ONLY record that the label is still
|
|
329
|
+
// granted (the probe never re-runs), and forgetting it would let the
|
|
330
|
+
// grant outlive a same-session re-add: reseeded from the new writes
|
|
331
|
+
// alone, no later diff would ever revoke it.
|
|
332
|
+
if (failedLabelIds.length === 0) {
|
|
333
|
+
forgetGranteeLabels(granteeId);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// A grantee who also inherits survives as an inherited-only row, so its
|
|
338
|
+
// scope must survive too — narrowed to what the removal could not take away.
|
|
339
|
+
// Dropping the entry would fall back to "all labels" (the probe does not
|
|
340
|
+
// re-run for grantee changes) and overstate what they can reach. Any label
|
|
341
|
+
// whose revoke FAILED stays in: the backend still grants it.
|
|
342
|
+
setSelectedLabelIdsByGrantee((prev) => {
|
|
343
|
+
const keep = new Set([
|
|
344
|
+
...(inheritedLabelIdsByGrantee[granteeId] ?? []),
|
|
345
|
+
...failedLabelIds,
|
|
346
|
+
]);
|
|
347
|
+
const survivingIds = effectiveLabels
|
|
348
|
+
.filter((l) => l.isPrimary || keep.has(l.id))
|
|
349
|
+
.map((l) => l.id);
|
|
350
|
+
return { ...prev, [granteeId]: survivingIds };
|
|
351
|
+
});
|
|
352
|
+
}
|
|
222
353
|
if (!labelsOk) {
|
|
223
354
|
// Object access is gone but some per-label grants couldn't be
|
|
224
355
|
// revoked — warn so the leftover scope isn't mistaken for success.
|
|
@@ -226,9 +357,13 @@ export function useObjectShareController(target, options) {
|
|
|
226
357
|
}
|
|
227
358
|
}, [
|
|
228
359
|
grantees,
|
|
360
|
+
labelsResolved,
|
|
229
361
|
applyAccessChange,
|
|
362
|
+
directLabelScopeOf,
|
|
363
|
+
recordDirectLabelWrites,
|
|
364
|
+
forgetGranteeLabels,
|
|
230
365
|
effectiveLabels,
|
|
231
|
-
|
|
366
|
+
inheritedLabelIdsByGrantee,
|
|
232
367
|
toast,
|
|
233
368
|
applyGranteeRemove,
|
|
234
369
|
settleGranteeEdit,
|
|
@@ -237,27 +372,54 @@ export function useObjectShareController(target, options) {
|
|
|
237
372
|
]);
|
|
238
373
|
const changeGranteeLabels = useCallback(async (granteeId, requested) => {
|
|
239
374
|
const grantee = grantees.find((g) => g.id === granteeId);
|
|
240
|
-
|
|
375
|
+
// Refused before the label scope resolves — the diff below would otherwise
|
|
376
|
+
// run against the assumed-all fallback (the UI gates on the same fact via
|
|
377
|
+
// `isMutable`; the controller stays authoritative).
|
|
378
|
+
if (!grantee || grantee.pending || !labelsResolved || !target) {
|
|
241
379
|
return;
|
|
242
380
|
}
|
|
243
|
-
//
|
|
381
|
+
// The primary label is always in scope, and so is a label the grantee only
|
|
382
|
+
// inherits: there is no local grant to revoke, so dropping it would report
|
|
383
|
+
// access removed that this workspace cannot reach. The checklist locks both;
|
|
384
|
+
// this keeps the controller — not the UI — authoritative.
|
|
244
385
|
const primaryIds = effectiveLabels.filter((l) => l.isPrimary).map((l) => l.id);
|
|
245
|
-
const
|
|
386
|
+
const inheritedIds = inheritedLabelIdsByGrantee[granteeId] ?? [];
|
|
387
|
+
const nextScope = Array.from(new Set([...requested, ...primaryIds, ...inheritedIds]));
|
|
246
388
|
const currentScope = selectedLabelIdsByGrantee[granteeId] ?? effectiveLabels.map((l) => l.id);
|
|
247
389
|
const desired = new Set(nextScope);
|
|
248
|
-
|
|
390
|
+
// Inherited labels join the CURRENT side too: an add-path scope is seeded
|
|
391
|
+
// before inheritance is known for the new row, so without this an untouched
|
|
392
|
+
// Apply would diff an inherited label as newly checked and grant it locally.
|
|
393
|
+
const current = new Set([...currentScope, ...inheritedIds]);
|
|
249
394
|
// Applying the checklist unchanged is a no-op — don't lock the row or
|
|
250
395
|
// toast success over zero writes.
|
|
251
396
|
if (desired.size === current.size && nextScope.every((id) => current.has(id))) {
|
|
252
397
|
return;
|
|
253
398
|
}
|
|
254
399
|
// Reflect the new scope immediately and lock the row until the writes
|
|
255
|
-
// settle — an overlapping label edit or removal would race these writes
|
|
256
|
-
//
|
|
257
|
-
// level
|
|
258
|
-
|
|
400
|
+
// settle — an overlapping label edit or removal would race these writes on
|
|
401
|
+
// the same labels. A lock-only overlay, because this changes no object
|
|
402
|
+
// access: re-applying the displayed level would persist the EFFECTIVE level
|
|
403
|
+
// as this row's direct grant, so an inherited EDIT over a direct VIEW would
|
|
404
|
+
// become a local EDIT after any label edit.
|
|
405
|
+
applyGranteeLock(granteeId);
|
|
259
406
|
setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [granteeId]: nextScope }));
|
|
260
|
-
|
|
407
|
+
// Labels newly brought into scope are granted at the level the grantee
|
|
408
|
+
// holds on the object — with no grant of their own here (inherited-only
|
|
409
|
+
// access) there is no level to mirror, so VIEW stands.
|
|
410
|
+
const { ok, failedLabelIds } = await reconcileLabelScope([
|
|
411
|
+
{
|
|
412
|
+
kind: grantee.kind,
|
|
413
|
+
granteeRef: grantee.granteeRef,
|
|
414
|
+
level: grantee.directLevel ?? "VIEW",
|
|
415
|
+
},
|
|
416
|
+
], desired, current);
|
|
417
|
+
// Record what the writes actually did to OUR grants: a label brought into scope
|
|
418
|
+
// gains one, a label dropped loses one. Only over the labels this workspace could
|
|
419
|
+
// write — an inherited-only label is absent from the diff either way.
|
|
420
|
+
const failedIds = new Set(failedLabelIds);
|
|
421
|
+
const writable = directLabelScopeOf(granteeId);
|
|
422
|
+
recordDirectLabelWrites(granteeId, nextScope.filter((id) => !current.has(id) && !failedIds.has(id)), currentScope.filter((id) => writable.has(id) && !desired.has(id) && !failedIds.has(id)));
|
|
261
423
|
if (ok) {
|
|
262
424
|
settleGranteeEdit(granteeId);
|
|
263
425
|
toast.addSuccess(objectShareMessages.toastAccessUpdated);
|
|
@@ -281,11 +443,15 @@ export function useObjectShareController(target, options) {
|
|
|
281
443
|
}, [
|
|
282
444
|
grantees,
|
|
283
445
|
target,
|
|
446
|
+
labelsResolved,
|
|
284
447
|
effectiveLabels,
|
|
285
448
|
selectedLabelIdsByGrantee,
|
|
449
|
+
inheritedLabelIdsByGrantee,
|
|
450
|
+
directLabelScopeOf,
|
|
451
|
+
recordDirectLabelWrites,
|
|
286
452
|
reconcileLabelScope,
|
|
287
453
|
toast,
|
|
288
|
-
|
|
454
|
+
applyGranteeLock,
|
|
289
455
|
settleGranteeEdit,
|
|
290
456
|
failGranteeEdit,
|
|
291
457
|
setSelectedLabelIdsByGrantee,
|
|
@@ -479,6 +645,7 @@ export function useObjectShareController(target, options) {
|
|
|
479
645
|
labelsResolved,
|
|
480
646
|
labelsInitializing,
|
|
481
647
|
selectedLabelIdsByGrantee,
|
|
648
|
+
inheritedLabelIdsByGrantee,
|
|
482
649
|
pendingGeneralAccess,
|
|
483
650
|
pendingGrantees,
|
|
484
651
|
};
|
|
@@ -500,6 +667,7 @@ export function useObjectShareController(target, options) {
|
|
|
500
667
|
labelsResolved,
|
|
501
668
|
labelsInitializing,
|
|
502
669
|
selectedLabelIdsByGrantee,
|
|
670
|
+
inheritedLabelIdsByGrantee,
|
|
503
671
|
pendingGeneralAccess,
|
|
504
672
|
pendingGrantees,
|
|
505
673
|
]);
|
|
@@ -44,6 +44,13 @@ export interface IObjectShareDialogViewModel {
|
|
|
44
44
|
* for the signed-in user's own sole grant, undefined for every other row.
|
|
45
45
|
*/
|
|
46
46
|
rowDisabledLevels: (grantee: IObjectShareGrantee) => ObjectSharePermissionLevel[] | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Whether Remove access must render disabled: the grantee holds no grant in this
|
|
49
|
+
* workspace, so there is nothing here to revoke (their access is inherited).
|
|
50
|
+
*/
|
|
51
|
+
isRowRemoveDisabled: (grantee: IObjectShareGrantee) => boolean;
|
|
52
|
+
/** Tooltip explaining why Remove access is disabled on this row. */
|
|
53
|
+
rowRemoveDisabledTooltip: string;
|
|
47
54
|
/** Close the whole dialog, discarding any staged self-restriction. */
|
|
48
55
|
onClose: () => void;
|
|
49
56
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectShareDialog.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareDialog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useObjectShareDialog.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareDialog.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAI1E,OAAO,EACH,KAAK,6BAA6B,EAClC,KAAK,2BAA2B,EAChC,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAClC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAY1E;;;;;;;GAOG;AACH,MAAM,WAAW,2BAA2B;IACxC,yFAAyF;IACzF,KAAK,EAAE,2BAA2B,CAAC;IACnC,yEAAyE;IACzE,OAAO,EAAE,6BAA6B,CAAC;IAEvC,8EAA8E;IAC9E,eAAe,EAAE,OAAO,CAAC;IACzB,qDAAqD;IACrD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,4EAA0E;IAC1E,0BAA0B,EAAE,OAAO,CAAC;IACpC,+EAA+E;IAC/E,yBAAyB,EAAE,OAAO,CAAC;IAEnC;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,SAAS,EAAE,OAAO,CAAC;IAEnB,kGAAkG;IAClG,uBAAuB,EAAE,0BAA0B,EAAE,GAAG,SAAS,CAAC;IAClE;;;OAGG;IACH,iBAAiB,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,0BAA0B,EAAE,GAAG,SAAS,CAAC;IAC9F;;;OAGG;IACH,mBAAmB,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC/D,oEAAoE;IACpE,wBAAwB,EAAE,MAAM,CAAC;IAEjC,sEAAsE;IACtE,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;OAGG;IACH,qBAAqB,EAAE,CAAC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACjG,qGAAqG;IACrG,WAAW,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACpD,qFAAqF;IACrF,oBAAoB,EAAE,MAAM,IAAI,CAAC;IACjC,+FAA6F;IAC7F,qBAAqB,EAAE,MAAM,IAAI,CAAC;CACrC;AAED;;;;GAIG;AACH,UAAU,2BAA2B;IACjC,wFAAsF;IACtF,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC;IAC7C,oGAAoG;IACpG,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,mFAAmF;IACnF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC1D,2FAA2F;IAC3F,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC7B,oGAAoG;IACpG,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iGAAiG;IACjG,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,MAAM,EACN,OAAO,EACP,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACd,EAAE,2BAA2B,GAAG,2BAA2B,CA6G3D"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { useIntl } from "react-intl";
|
|
4
|
+
import { objectShareMessages } from "./messages.js";
|
|
5
|
+
import { changesEffectiveLevel, removalChangesEffectiveLevel } from "./objectShareController.helpers.js";
|
|
3
6
|
import { useObjectShareController } from "./useObjectShareController.js";
|
|
4
7
|
/**
|
|
5
8
|
* Owns everything {@link ObjectShareDialog} needs to render for one dialog session:
|
|
@@ -10,6 +13,7 @@ import { useObjectShareController } from "./useObjectShareController.js";
|
|
|
10
13
|
* @internal
|
|
11
14
|
*/
|
|
12
15
|
export function useObjectShareDialog({ target, onClose, onSummaryChange, labels, labelsLoading, labelsError, }) {
|
|
16
|
+
const intl = useIntl();
|
|
13
17
|
const { state, actions } = useObjectShareController(target, { labels, labelsError, labelsLoading });
|
|
14
18
|
const [pendingSelfChange, setPendingSelfChange] = useState(undefined);
|
|
15
19
|
// Synchronize the displayed access summary out to the consumer — the legitimate
|
|
@@ -31,7 +35,12 @@ export function useObjectShareDialog({ target, onClose, onSummaryChange, labels,
|
|
|
31
35
|
// classified it as self-managed — those changes route through the confirm.
|
|
32
36
|
const onRowPermissionChange = useCallback((grantee, level) => {
|
|
33
37
|
if (state.selfManagedGranteeId === grantee.id) {
|
|
34
|
-
|
|
38
|
+
// Stage the confirm only for a pick that would really move the level the
|
|
39
|
+
// user effectively holds. Comparing against the displayed level instead
|
|
40
|
+
// opened "Restrict your access?" for a pick under an inherited floor, which
|
|
41
|
+
// the controller then refuses — the user would confirm a restriction that
|
|
42
|
+
// never happens. Same predicate the controller applies.
|
|
43
|
+
if (changesEffectiveLevel(grantee, level)) {
|
|
35
44
|
setPendingSelfChange({ granteeId: grantee.id, level });
|
|
36
45
|
}
|
|
37
46
|
return;
|
|
@@ -39,7 +48,11 @@ export function useObjectShareDialog({ target, onClose, onSummaryChange, labels,
|
|
|
39
48
|
void actions.changePermissionLevel(grantee.id, level);
|
|
40
49
|
}, [state.selfManagedGranteeId, actions]);
|
|
41
50
|
const onRowRemove = useCallback((grantee) => {
|
|
42
|
-
|
|
51
|
+
// Stage the confirm only when the removal can lower the level the user
|
|
52
|
+
// effectively holds — under an inherited floor the revoke removes the local
|
|
53
|
+
// grant but restricts nothing, so the "Restrict your access?" warning would
|
|
54
|
+
// promise a restriction that never happens. Same policy as the picks above.
|
|
55
|
+
if (state.selfManagedGranteeId === grantee.id && removalChangesEffectiveLevel(grantee)) {
|
|
43
56
|
setPendingSelfChange({ granteeId: grantee.id, level: "none" });
|
|
44
57
|
return;
|
|
45
58
|
}
|
|
@@ -60,6 +73,11 @@ export function useObjectShareDialog({ target, onClose, onSummaryChange, labels,
|
|
|
60
73
|
}, [pendingSelfChange, actions]);
|
|
61
74
|
const onSelfRestrictCancel = useCallback(() => setPendingSelfChange(undefined), []);
|
|
62
75
|
const rowDisabledLevels = useCallback((grantee) => state.selfManagedGranteeId === grantee.id ? state.selfManagedDisabledLevels : undefined, [state.selfManagedGranteeId, state.selfManagedDisabledLevels]);
|
|
76
|
+
// Levels below an inherited one are deliberately NOT disabled: lowering the
|
|
77
|
+
// direct grant still reduces the effective level whenever it sits above what is
|
|
78
|
+
// inherited (direct EDIT under an inherited SHARE → picking VIEW yields SHARE).
|
|
79
|
+
// The row's warning badge is what explains the inherited floor.
|
|
80
|
+
const isRowRemoveDisabled = useCallback((grantee) => grantee.directLevel === undefined, []);
|
|
63
81
|
const isMutable = state.status === "success" && state.labelsResolved;
|
|
64
82
|
const isLoading = state.status === "loading" || state.labelsInitializing;
|
|
65
83
|
return {
|
|
@@ -73,6 +91,8 @@ export function useObjectShareDialog({ target, onClose, onSummaryChange, labels,
|
|
|
73
91
|
isLoading,
|
|
74
92
|
workspaceDisabledLevels: state.workspaceDisabledLevels,
|
|
75
93
|
rowDisabledLevels,
|
|
94
|
+
isRowRemoveDisabled,
|
|
95
|
+
rowRemoveDisabledTooltip: intl.formatMessage(objectShareMessages.granteeRemoveInherited),
|
|
76
96
|
onClose,
|
|
77
97
|
onRowPermissionChange,
|
|
78
98
|
onRowRemove,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-ui-ext",
|
|
3
|
-
"version": "11.52.0-alpha.
|
|
3
|
+
"version": "11.52.0-alpha.7",
|
|
4
4
|
"description": "GoodData.UI SDK - Extensions",
|
|
5
5
|
"license": "LicenseRef-LICENSE",
|
|
6
6
|
"author": "GoodData Corporation",
|
|
@@ -63,20 +63,20 @@
|
|
|
63
63
|
"ts-invariant": "0.10.3",
|
|
64
64
|
"tslib": "2.8.1",
|
|
65
65
|
"uuid": "11.1.1",
|
|
66
|
-
"@gooddata/sdk-backend-base": "11.52.0-alpha.
|
|
67
|
-
"@gooddata/sdk-backend-spi": "11.52.0-alpha.
|
|
68
|
-
"@gooddata/sdk-embedding": "11.52.0-alpha.
|
|
69
|
-
"@gooddata/sdk-model": "11.52.0-alpha.
|
|
70
|
-
"@gooddata/sdk-ui": "11.52.0-alpha.
|
|
71
|
-
"@gooddata/sdk-ui-charts": "11.52.0-alpha.
|
|
72
|
-
"@gooddata/sdk-ui-geo": "11.52.0-alpha.
|
|
73
|
-
"@gooddata/sdk-ui-filters": "11.52.0-alpha.
|
|
74
|
-
"@gooddata/sdk-ui-
|
|
75
|
-
"@gooddata/sdk-ui-
|
|
76
|
-
"@gooddata/sdk-ui-
|
|
77
|
-
"@gooddata/sdk-ui-vis-commons": "11.52.0-alpha.
|
|
78
|
-
"@gooddata/
|
|
79
|
-
"@gooddata/
|
|
66
|
+
"@gooddata/sdk-backend-base": "11.52.0-alpha.7",
|
|
67
|
+
"@gooddata/sdk-backend-spi": "11.52.0-alpha.7",
|
|
68
|
+
"@gooddata/sdk-embedding": "11.52.0-alpha.7",
|
|
69
|
+
"@gooddata/sdk-model": "11.52.0-alpha.7",
|
|
70
|
+
"@gooddata/sdk-ui": "11.52.0-alpha.7",
|
|
71
|
+
"@gooddata/sdk-ui-charts": "11.52.0-alpha.7",
|
|
72
|
+
"@gooddata/sdk-ui-geo": "11.52.0-alpha.7",
|
|
73
|
+
"@gooddata/sdk-ui-filters": "11.52.0-alpha.7",
|
|
74
|
+
"@gooddata/sdk-ui-kit": "11.52.0-alpha.7",
|
|
75
|
+
"@gooddata/sdk-ui-semantic-search": "11.52.0-alpha.7",
|
|
76
|
+
"@gooddata/sdk-ui-pivot": "11.52.0-alpha.7",
|
|
77
|
+
"@gooddata/sdk-ui-vis-commons": "11.52.0-alpha.7",
|
|
78
|
+
"@gooddata/util": "11.52.0-alpha.7",
|
|
79
|
+
"@gooddata/sdk-ui-theme-provider": "11.52.0-alpha.7"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@microsoft/api-documenter": "^7.17.0",
|
|
@@ -124,12 +124,12 @@
|
|
|
124
124
|
"typescript": "5.9.3",
|
|
125
125
|
"vitest": "4.1.8",
|
|
126
126
|
"vitest-dom": "0.1.1",
|
|
127
|
-
"@gooddata/eslint-config": "11.52.0-alpha.
|
|
128
|
-
"@gooddata/
|
|
129
|
-
"@gooddata/
|
|
130
|
-
"@gooddata/reference-workspace": "11.52.0-alpha.
|
|
131
|
-
"@gooddata/sdk-backend-mockingbird": "11.52.0-alpha.
|
|
132
|
-
"@gooddata/stylelint-config": "11.52.0-alpha.
|
|
127
|
+
"@gooddata/eslint-config": "11.52.0-alpha.7",
|
|
128
|
+
"@gooddata/oxlint-config": "11.52.0-alpha.7",
|
|
129
|
+
"@gooddata/i18n-toolkit": "11.52.0-alpha.7",
|
|
130
|
+
"@gooddata/reference-workspace": "11.52.0-alpha.7",
|
|
131
|
+
"@gooddata/sdk-backend-mockingbird": "11.52.0-alpha.7",
|
|
132
|
+
"@gooddata/stylelint-config": "11.52.0-alpha.7"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
135
|
"react": "^18.0.0 || ^19.0.0",
|