@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.
Files changed (50) hide show
  1. package/esm/dataLoaders/InsightDataLoader.d.ts +6 -0
  2. package/esm/dataLoaders/InsightDataLoader.d.ts.map +1 -1
  3. package/esm/dataLoaders/InsightDataLoader.js +4 -1
  4. package/esm/dataLoaders/index.d.ts +13 -0
  5. package/esm/dataLoaders/index.d.ts.map +1 -1
  6. package/esm/dataLoaders/index.js +14 -0
  7. package/esm/index.d.ts +1 -1
  8. package/esm/index.d.ts.map +1 -1
  9. package/esm/index.js +1 -1
  10. package/esm/insightView/InsightRenderer.d.ts +2 -5
  11. package/esm/insightView/InsightRenderer.d.ts.map +1 -1
  12. package/esm/insightView/InsightRenderer.js +1 -2
  13. package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.d.ts +6 -1
  14. package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.d.ts.map +1 -1
  15. package/esm/internal/components/pluggableVisualizations/mekko/PluggableMekko.js +25 -4
  16. package/esm/internal/components/pluggableVisualizations/pivotTable/PluggablePivotTable.d.ts.map +1 -1
  17. package/esm/internal/components/pluggableVisualizations/pivotTable/PluggablePivotTable.js +0 -1
  18. package/esm/internal/translations/en-US.localization-bundle.d.ts +4 -0
  19. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  20. package/esm/internal/translations/en-US.localization-bundle.js +4 -0
  21. package/esm/notificationsPanel/Notification/Notification.d.ts.map +1 -1
  22. package/esm/notificationsPanel/Notification/Notification.js +6 -3
  23. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.d.ts +3 -3
  24. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.d.ts.map +1 -1
  25. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetail.js +4 -4
  26. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.d.ts +9 -3
  27. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.d.ts.map +1 -1
  28. package/esm/notificationsPanel/NotificationFiltersDetail/NotificationFiltersDetailDialog.js +4 -4
  29. package/esm/sdk-ui-ext.d.ts +13 -0
  30. package/esm/share/ObjectShareDialog.js +22 -6
  31. package/esm/share/messages.d.ts +7 -0
  32. package/esm/share/messages.d.ts.map +1 -1
  33. package/esm/share/messages.js +5 -0
  34. package/esm/share/objectShareController.helpers.d.ts +117 -17
  35. package/esm/share/objectShareController.helpers.d.ts.map +1 -1
  36. package/esm/share/objectShareController.helpers.js +181 -44
  37. package/esm/share/objectShareController.types.d.ts +34 -10
  38. package/esm/share/objectShareController.types.d.ts.map +1 -1
  39. package/esm/share/useAccessList.d.ts +6 -0
  40. package/esm/share/useAccessList.d.ts.map +1 -1
  41. package/esm/share/useAccessList.js +26 -6
  42. package/esm/share/useLabelScope.d.ts +46 -1
  43. package/esm/share/useLabelScope.d.ts.map +1 -1
  44. package/esm/share/useLabelScope.js +98 -6
  45. package/esm/share/useObjectShareController.d.ts.map +1 -1
  46. package/esm/share/useObjectShareController.js +213 -45
  47. package/esm/share/useObjectShareDialog.d.ts +7 -0
  48. package/esm/share/useObjectShareDialog.d.ts.map +1 -1
  49. package/esm/share/useObjectShareDialog.js +22 -2
  50. package/package.json +21 -21
@@ -5,7 +5,7 @@ import { convertError, useBackendStrict, useCancelablePromise, useWorkspaceStric
5
5
  import { useToastMessage } from "@gooddata/sdk-ui-kit";
6
6
  import { composeEffectiveWorkspaceAccess, deriveGeneralAccess, deriveInheritedWorkspaceLevel, deriveWorkspacePermissionLevel, } from "./accessSummary.js";
7
7
  import { objectShareMessages } from "./messages.js";
8
- import { assigneeIdentityFacts, assigneeMatchesQuery, effectivePermissionAbove, granteeId, granteesFromAccessList, mergeGrantees, userDisplayPair, userIdentityFacts, } from "./objectShareController.helpers.js";
8
+ import { assigneeIdentityFacts, assigneeMatchesQuery, granteeId, granteesFromAccessList, mergeGrantees, userDisplayPair, userIdentityFacts, withDirectLevel, } from "./objectShareController.helpers.js";
9
9
  /**
10
10
  * Owns the backend access list for ONE dialog session: the hook is mounted while
11
11
  * the dialog is open, for a single target that must not change while mounted
@@ -167,16 +167,25 @@ export function useAccessList(target) {
167
167
  const settled = current ? { ...current, settled: undefined } : undefined;
168
168
  if (current?.kind === "added") {
169
169
  // Overlay-born row: stay an `added` entry, or the merge would drop it.
170
- const grantee = {
171
- ...current.grantee,
172
- level,
173
- effectivePermission: effectivePermissionAbove(level, current.grantee.inheritedLevel),
174
- };
170
+ const grantee = withDirectLevel(current.grantee, level);
175
171
  return { ...prev, [id]: { kind: "added", grantee, pending: true, settled } };
176
172
  }
177
173
  return { ...prev, [id]: { kind: "level", level, pending: true, settled } };
178
174
  });
179
175
  }, []);
176
+ // Lock a row for a write that does NOT change its access (a label-scope edit). The
177
+ // lock WRAPS whatever the row committed rather than modifying it: reusing the
178
+ // existing entry and flipping `pending` would re-arm it, and `pending` means
179
+ // something different per kind — a settled removal would read as a revoke in flight
180
+ // and render the row as removing at its pre-removal level. Both settling and failing
181
+ // unwrap back to the entry underneath, because a label write changes neither.
182
+ const applyGranteeLock = useCallback((id) => {
183
+ setGranteeEdits((prev) => {
184
+ const current = prev[id];
185
+ const settled = current ? { ...current, settled: undefined } : undefined;
186
+ return { ...prev, [id]: { kind: "locked", pending: true, settled } };
187
+ });
188
+ }, []);
180
189
  const applyGranteeRemove = useCallback((id) => {
181
190
  setGranteeEdits((prev) => {
182
191
  const current = prev[id];
@@ -190,6 +199,16 @@ export function useAccessList(target) {
190
199
  if (!current) {
191
200
  return prev;
192
201
  }
202
+ // A lock is a wrapper over the row's committed state, so settling it unwraps —
203
+ // same as failing. Keeping a settled `locked` entry would discard the entry it
204
+ // wrapped and drop the row back to its fetched state.
205
+ if (current.kind === "locked") {
206
+ if (!current.settled) {
207
+ const { [id]: _omit, ...rest } = prev;
208
+ return rest;
209
+ }
210
+ return { ...prev, [id]: current.settled };
211
+ }
193
212
  // A settled removal always keeps its entry: for a base row (including a
194
213
  // removed re-added one) it is what keeps the row hidden; for an
195
214
  // overlay-born row it renders nothing and is harmless.
@@ -238,6 +257,7 @@ export function useAccessList(target) {
238
257
  loadOptions,
239
258
  applyGranteeAdd,
240
259
  applyGranteeLevel,
260
+ applyGranteeLock,
241
261
  applyGranteeRemove,
242
262
  settleGranteeEdit,
243
263
  failGranteeEdit,
@@ -1,5 +1,5 @@
1
1
  import type { IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
2
- import { type LabelScopePrincipal } from "./objectShareController.helpers.js";
2
+ import { type ILabelScopeChange, type LabelScopePrincipal } from "./objectShareController.helpers.js";
3
3
  import type { IObjectShareLabel } from "./types.js";
4
4
  /**
5
5
  * Per-label access scope for the share dialog: which labels each grantee can
@@ -38,6 +38,30 @@ export interface ILabelScope {
38
38
  /** Per-grantee label scope: grantee id → label ids in scope (primary always in). */
39
39
  selectedLabelIdsByGrantee: Record<string, string[]>;
40
40
  setSelectedLabelIdsByGrantee: React.Dispatch<React.SetStateAction<Record<string, string[]>>>;
41
+ /**
42
+ * In-scope labels whose access the grantee INHERITS, per grantee id — including ones
43
+ * they also hold a local grant on. Callers render these locked: unchecking cannot take
44
+ * the access away. Derived straight from the probe, never optimistic, because only the
45
+ * backend can say where a grant lives.
46
+ */
47
+ inheritedLabelIdsByGrantee: Record<string, string[]>;
48
+ /**
49
+ * Labels THIS workspace grants the grantee, per grantee id — the only ones a write may
50
+ * target, and the complement of "inherited only". An optimistic overlay, not a
51
+ * derivation: the per-label probe runs once per session, so after the dialog revokes or
52
+ * grants a label the probe's answer is stale and acting on it re-creates access that was
53
+ * just removed. Every label write reports its outcome through
54
+ * {@link recordDirectLabelWrites}.
55
+ */
56
+ directLabelIdsByGrantee: Record<string, string[]>;
57
+ /**
58
+ * Record the outcome of the dialog's OWN label writes so the direct set stays true:
59
+ * `granted` ids gain a local grant, `revoked` ids lose theirs. Pass only the writes
60
+ * that actually landed.
61
+ */
62
+ recordDirectLabelWrites: (granteeId: string, granted: readonly string[], revoked: readonly string[]) => void;
63
+ /** Forget a grantee's label bookkeeping once their row is gone for good. */
64
+ forgetGranteeLabels: (granteeId: string) => void;
41
65
  /**
42
66
  * The single per-label write path. Diffs `desired` vs `current` over the
43
67
  * permissionable labels and issues one `manageObjectPermissions` per changed
@@ -49,6 +73,27 @@ export interface ILabelScope {
49
73
  ok: boolean;
50
74
  failedLabelIds: string[];
51
75
  }>;
76
+ /**
77
+ * Per-principal variant of {@link reconcileLabelScope}: each principal moves its
78
+ * OWN scope, for grantees added in one step that each picked a different label
79
+ * scope. Still one write per label carrying every principal that changes on it,
80
+ * so a shared label costs one call regardless of how the scopes differ.
81
+ */
82
+ reconcileLabelScopes: (changes: ILabelScopeChange[]) => Promise<{
83
+ ok: boolean;
84
+ failedLabelIds: string[];
85
+ }>;
86
+ /**
87
+ * Re-grades an UNCHANGED label scope to each principal's `level` — what a
88
+ * permission-level change on the object needs so its labels don't keep the level
89
+ * they were granted at. Not a diff: every in-scope label is rewritten (a diff
90
+ * over an unchanged set produces no writes). Same independent settling and
91
+ * `failedLabelIds` reporting as {@link reconcileLabelScope}.
92
+ */
93
+ regradeLabelScope: (principals: LabelScopePrincipal[], scopeLabelIds: ReadonlySet<string>) => Promise<{
94
+ ok: boolean;
95
+ failedLabelIds: string[];
96
+ }>;
52
97
  }
53
98
  /**
54
99
  * Owns label-scope resolution + writes for {@link useObjectShareController}.
@@ -1 +1 @@
1
- {"version":3,"file":"useLabelScope.d.ts","sourceRoot":"","sources":["../../src/share/useLabelScope.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAK1E,OAAO,EACH,KAAK,mBAAmB,EAG3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6BpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;OAIG;IACH,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC;;;;;;;;OAQG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sFAAoF;IACpF,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpD,4BAA4B,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7F;;;;;;OAMG;IACH,mBAAmB,EAAE,CACjB,UAAU,EAAE,mBAAmB,EAAE,EACjC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,KACnC,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,MAAM,EAAE,iBAAiB,EAAE,EAC3B,OAAO,EAAE,OAAO,EAChB,mBAAmB,EAAE,MAAM,EAAE,EAC7B,WAAW,EAAE,OAAO,EACpB,aAAa,EAAE,OAAO,GACvB,WAAW,CAmKb"}
1
+ {"version":3,"file":"useLabelScope.d.ts","sourceRoot":"","sources":["../../src/share/useLabelScope.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAK1E,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAI3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6EpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;OAIG;IACH,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC;;;;;;;;OAQG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sFAAoF;IACpF,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACpD,4BAA4B,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7F;;;;;OAKG;IACH,0BAA0B,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrD;;;;;;;OAOG;IACH,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAClD;;;;OAIG;IACH,uBAAuB,EAAE,CACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,MAAM,EAAE,KACzB,IAAI,CAAC;IACV,4EAA4E;IAC5E,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD;;;;;;OAMG;IACH,mBAAmB,EAAE,CACjB,UAAU,EAAE,mBAAmB,EAAE,EACjC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,KACnC,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACxD;;;;;OAKG;IACH,oBAAoB,EAAE,CAClB,OAAO,EAAE,iBAAiB,EAAE,KAC3B,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACxD;;;;;;OAMG;IACH,iBAAiB,EAAE,CACf,UAAU,EAAE,mBAAmB,EAAE,EACjC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,KACjC,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,MAAM,EAAE,iBAAiB,EAAE,EAC3B,OAAO,EAAE,OAAO,EAChB,mBAAmB,EAAE,MAAM,EAAE,EAC7B,WAAW,EAAE,OAAO,EACpB,aAAa,EAAE,OAAO,GACvB,WAAW,CA6Ob"}
@@ -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 { buildLabelMutationsForPrincipals, isGranteeGrantedIn, } from "./objectShareController.helpers.js";
6
- /** Pure: each grantee's in-scope label ids — primary always counts, others only where the grantee is granted. */
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 || isGranteeGrantedIn(result.list, id)) {
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
- const reconcileLabelScope = useCallback(async (principals, desiredLabelIds, currentLabelIds) => {
141
- const writes = buildLabelMutationsForPrincipals(principals, desiredLabelIds, currentLabelIds, effectiveLabels);
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
- }, [effectiveLabels, backend, workspace]);
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;AAiB1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CA+kBxB"}
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"}