@gooddata/sdk-ui-ext 11.52.0-alpha.6 → 11.52.0
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/de-DE.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.js +2 -0
- package/esm/internal/translations/en-AU.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-AU.localization-bundle.js +2 -0
- package/esm/internal/translations/en-GB.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-GB.localization-bundle.js +2 -0
- 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/internal/translations/es-419.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-419.localization-bundle.js +2 -0
- package/esm/internal/translations/es-ES.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-ES.localization-bundle.js +2 -0
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fi-FI.localization-bundle.js +2 -0
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-CA.localization-bundle.js +2 -0
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-FR.localization-bundle.js +2 -0
- package/esm/internal/translations/id-ID.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/id-ID.localization-bundle.js +2 -0
- package/esm/internal/translations/it-IT.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/it-IT.localization-bundle.js +2 -0
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ja-JP.localization-bundle.js +2 -0
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ko-KR.localization-bundle.js +2 -0
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/nl-NL.localization-bundle.js +2 -0
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pl-PL.localization-bundle.js +2 -0
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-BR.localization-bundle.js +2 -0
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-PT.localization-bundle.js +2 -0
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ru-RU.localization-bundle.js +2 -0
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/sl-SI.localization-bundle.js +2 -0
- package/esm/internal/translations/th-TH.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/th-TH.localization-bundle.js +2 -0
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/tr-TR.localization-bundle.js +2 -0
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/uk-UA.localization-bundle.js +2 -0
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/vi-VN.localization-bundle.js +2 -0
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-HK.localization-bundle.js +2 -0
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hans.localization-bundle.js +2 -0
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +2 -0
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.js +2 -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
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
3
|
import { useBackendStrict, useCancelablePromise, useWorkspaceStrict } from "@gooddata/sdk-ui";
|
|
4
4
|
import { isPermissionsNotAvailable } from "./accessErrors.js";
|
|
5
|
-
import {
|
|
6
|
-
/**
|
|
5
|
+
import { buildLabelMutationsForScopes, buildLabelRegrades, granteeGrantIn, } from "./objectShareController.helpers.js";
|
|
6
|
+
/**
|
|
7
|
+
* Pure: each grantee's in-scope label ids — primary always counts, others wherever
|
|
8
|
+
* the grantee holds the label, whether granted here or inherited. An inherited label
|
|
9
|
+
* is genuinely in scope: the grantee can read it, so the checklist must show it
|
|
10
|
+
* checked, exactly as the parent workspace shows it.
|
|
11
|
+
*/
|
|
7
12
|
function resolveScopes(results, committedGranteeIds) {
|
|
8
13
|
const resolved = {};
|
|
9
14
|
for (const id of committedGranteeIds) {
|
|
@@ -14,13 +19,52 @@ function resolveScopes(results, committedGranteeIds) {
|
|
|
14
19
|
continue;
|
|
15
20
|
}
|
|
16
21
|
for (const id of committedGranteeIds) {
|
|
17
|
-
if (result.label.isPrimary ||
|
|
22
|
+
if (result.label.isPrimary || granteeGrantIn(result.list, id)) {
|
|
18
23
|
resolved[id].push(result.label.id);
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
return resolved;
|
|
23
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Pure: per-grantee label provenance, split by which question it answers. A label that is
|
|
31
|
+
* both granted here and inherited appears in BOTH maps, and that is the point — the two
|
|
32
|
+
* facts drive different decisions.
|
|
33
|
+
*
|
|
34
|
+
* - `inherited` — access survives revoking whatever this workspace granted, so the
|
|
35
|
+
* checkbox stays checked and locked. Immutable for the session: no write from this
|
|
36
|
+
* workspace can change a group's or a parent's grant, so deriving it from the probe
|
|
37
|
+
* cannot go stale.
|
|
38
|
+
* - `direct` — granted HERE, so it is the only thing a write may target. This one DOES
|
|
39
|
+
* change under our own writes, so the hook keeps it as an optimistic overlay seeded from
|
|
40
|
+
* here rather than re-deriving it from a probe that runs once per session.
|
|
41
|
+
*
|
|
42
|
+
* The primary label is excluded from both: it is locked for every grantee regardless, and
|
|
43
|
+
* never written.
|
|
44
|
+
*/
|
|
45
|
+
function resolveLabelProvenance(results, committedGranteeIds) {
|
|
46
|
+
const inherited = {};
|
|
47
|
+
const direct = {};
|
|
48
|
+
for (const id of committedGranteeIds) {
|
|
49
|
+
inherited[id] = [];
|
|
50
|
+
direct[id] = [];
|
|
51
|
+
}
|
|
52
|
+
for (const result of results) {
|
|
53
|
+
if ("transient" in result || result.label.isPrimary) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
for (const id of committedGranteeIds) {
|
|
57
|
+
const grant = granteeGrantIn(result.list, id);
|
|
58
|
+
if (grant?.inherited) {
|
|
59
|
+
inherited[id].push(result.label.id);
|
|
60
|
+
}
|
|
61
|
+
if (grant?.direct) {
|
|
62
|
+
direct[id].push(result.label.id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { inherited, direct };
|
|
67
|
+
}
|
|
24
68
|
/**
|
|
25
69
|
* Owns label-scope resolution + writes for {@link useObjectShareController}.
|
|
26
70
|
* Resolves each grantee's scope by fetching every label's access list, tracks
|
|
@@ -41,6 +85,10 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
41
85
|
// a fresh add / labels edit must survive the backend's lagging re-read, so the
|
|
42
86
|
// probe below only seeds grantees it doesn't already know.
|
|
43
87
|
const [selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee] = useState({});
|
|
88
|
+
// Which labels THIS workspace grants each grantee. Optimistic for the same reason the
|
|
89
|
+
// scope is, and for a sharper one: our own revokes and grants change it, and the probe
|
|
90
|
+
// will not run again to notice.
|
|
91
|
+
const [directLabelIdsByGrantee, setDirectLabelIdsByGrantee] = useState({});
|
|
44
92
|
// Whether the session's first probe has settled (see `labelsInitializing`) —
|
|
45
93
|
// a fact about session history, so it is state, flipped once in onSuccess.
|
|
46
94
|
const [everResolved, setEverResolved] = useState(false);
|
|
@@ -52,6 +100,7 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
52
100
|
if (seenLabelsKey !== labelsKey) {
|
|
53
101
|
setSeenLabelsKey(labelsKey);
|
|
54
102
|
setSelectedLabelIdsByGrantee({});
|
|
103
|
+
setDirectLabelIdsByGrantee({});
|
|
55
104
|
}
|
|
56
105
|
// Probe each label's access list to learn which are permissionable (some 404) and
|
|
57
106
|
// which each grantee holds. `hasList` gates it: a list with no named grantees keeps
|
|
@@ -73,6 +122,7 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
73
122
|
onSuccess: (results) => {
|
|
74
123
|
setEverResolved(true);
|
|
75
124
|
const resolved = resolveScopes(results, committedGranteeIds);
|
|
125
|
+
const { direct } = resolveLabelProvenance(results, committedGranteeIds);
|
|
76
126
|
// Seed only grantees we don't already have a scope for — an optimistic
|
|
77
127
|
// scope is authoritative and must survive this re-resolution. Fired once
|
|
78
128
|
// per resolution (onSuccess), so reading `prev` is safe.
|
|
@@ -83,6 +133,14 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
83
133
|
}
|
|
84
134
|
return next;
|
|
85
135
|
});
|
|
136
|
+
// Same rule for the direct set: what we wrote outranks what the probe read.
|
|
137
|
+
setDirectLabelIdsByGrantee((prev) => {
|
|
138
|
+
const next = {};
|
|
139
|
+
for (const id of committedGranteeIds) {
|
|
140
|
+
next[id] = prev[id] ?? direct[id];
|
|
141
|
+
}
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
86
144
|
},
|
|
87
145
|
},
|
|
88
146
|
// Keyed on the label set's stable string hash, not the array (rebuilt every
|
|
@@ -112,6 +170,30 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
112
170
|
}
|
|
113
171
|
return permissionable;
|
|
114
172
|
}, [labelLists]);
|
|
173
|
+
// Inheritance is the one fact no write from here can change, so it stays a pure
|
|
174
|
+
// derivation of the probe — there is no optimistic version of "someone else granted
|
|
175
|
+
// this", and nothing we do can make it stale.
|
|
176
|
+
const inheritedLabelIdsByGrantee = useMemo(() => (labelLists ? resolveLabelProvenance(labelLists, committedGranteeIds).inherited : {}), [labelLists, committedGranteeIds]);
|
|
177
|
+
const recordDirectLabelWrites = useCallback((granteeId, granted, revoked) => {
|
|
178
|
+
if (granted.length === 0 && revoked.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setDirectLabelIdsByGrantee((prev) => {
|
|
182
|
+
const dropped = new Set(revoked);
|
|
183
|
+
const kept = (prev[granteeId] ?? []).filter((id) => !dropped.has(id));
|
|
184
|
+
return { ...prev, [granteeId]: Array.from(new Set([...kept, ...granted])) };
|
|
185
|
+
});
|
|
186
|
+
}, []);
|
|
187
|
+
const forgetGranteeLabels = useCallback((granteeId) => {
|
|
188
|
+
setSelectedLabelIdsByGrantee((prev) => {
|
|
189
|
+
const { [granteeId]: _scope, ...rest } = prev;
|
|
190
|
+
return rest;
|
|
191
|
+
});
|
|
192
|
+
setDirectLabelIdsByGrantee((prev) => {
|
|
193
|
+
const { [granteeId]: _direct, ...rest } = prev;
|
|
194
|
+
return rest;
|
|
195
|
+
});
|
|
196
|
+
}, []);
|
|
115
197
|
// Only permissionable labels are scope-controllable; assume all until resolved.
|
|
116
198
|
const effectiveLabels = useMemo(() => hasList && permissionableLabelIds
|
|
117
199
|
? labels.filter((l) => permissionableLabelIds.has(l.id))
|
|
@@ -137,8 +219,9 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
137
219
|
// drift. Writes settle independently (allSettled), and the failed labels are
|
|
138
220
|
// reported by id — never collapsed into one boolean, or a partial failure would
|
|
139
221
|
// roll callers back past writes that actually landed.
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
// Issues one manageObjectPermissions per label and reports the failures by id.
|
|
223
|
+
// Shared by the scope diff and the level re-grade so both settle identically.
|
|
224
|
+
const writeLabels = useCallback(async (writes) => {
|
|
142
225
|
if (writes.length === 0) {
|
|
143
226
|
return { ok: true, failedLabelIds: [] };
|
|
144
227
|
}
|
|
@@ -148,13 +231,22 @@ export function useLabelScope(target, labels, hasList, committedGranteeIds, labe
|
|
|
148
231
|
.filter((_, i) => results[i].status === "rejected")
|
|
149
232
|
.map((w) => w.id);
|
|
150
233
|
return { ok: failedLabelIds.length === 0, failedLabelIds };
|
|
151
|
-
}, [
|
|
234
|
+
}, [backend, workspace]);
|
|
235
|
+
const reconcileLabelScopes = useCallback((changes) => writeLabels(buildLabelMutationsForScopes(changes, effectiveLabels)), [effectiveLabels, writeLabels]);
|
|
236
|
+
const reconcileLabelScope = useCallback((principals, desiredLabelIds, currentLabelIds) => reconcileLabelScopes(principals.map((principal) => ({ principal, desiredLabelIds, currentLabelIds }))), [reconcileLabelScopes]);
|
|
237
|
+
const regradeLabelScope = useCallback((principals, scopeLabelIds) => writeLabels(buildLabelRegrades(principals, scopeLabelIds, effectiveLabels)), [effectiveLabels, writeLabels]);
|
|
152
238
|
return {
|
|
153
239
|
effectiveLabels,
|
|
154
240
|
labelsResolved,
|
|
155
241
|
labelsInitializing,
|
|
156
242
|
selectedLabelIdsByGrantee,
|
|
157
243
|
setSelectedLabelIdsByGrantee,
|
|
244
|
+
inheritedLabelIdsByGrantee,
|
|
245
|
+
directLabelIdsByGrantee,
|
|
246
|
+
recordDirectLabelWrites,
|
|
247
|
+
forgetGranteeLabels,
|
|
158
248
|
reconcileLabelScope,
|
|
249
|
+
reconcileLabelScopes,
|
|
250
|
+
regradeLabelScope,
|
|
159
251
|
};
|
|
160
252
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAkB1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CAkxBxB"}
|
|
@@ -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"}
|