@gooddata/sdk-ui-ext 11.44.0-alpha.0 → 11.44.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,11 @@ export function isMeasureValueFilter(filter) {
23
23
  if (filter === undefined || filter === null) {
24
24
  return false;
25
25
  }
26
+ // A ranking filter also references a measure (by local id or by ref), so guard against it explicitly.
27
+ // This keeps isMeasureValueFilter and isRankingFilter mutually exclusive regardless of call order.
28
+ if (isRankingFilter(filter)) {
29
+ return false;
30
+ }
26
31
  const measureValueFilter = filter;
27
32
  return !!measureValueFilter.measureLocalIdentifier || !!measureValueFilter.measureRef;
28
33
  }
@@ -34,8 +39,10 @@ export function isActiveMeasureValueFilter(filter) {
34
39
  }
35
40
  export function isRankingFilter(filter) {
36
41
  const filterAsRankingFilter = filter;
42
+ // A ranking filter is uniquely identified by a top-level operator + value (which measure value
43
+ // filters never have). The ranked measure may be referenced by local id (string) or by ObjRef
44
+ // (catalog metric), so the measure shape must not be part of the check.
37
45
  return (!!filter &&
38
- typeof filterAsRankingFilter.measure === "string" &&
39
46
  typeof filterAsRankingFilter.operator === "string" &&
40
47
  typeof filterAsRankingFilter.value === "number");
41
48
  }
@@ -141,6 +148,13 @@ function sanitizeMeasureValueFilter(filter, attributeBucketItems, measureBucketI
141
148
  }
142
149
  return measureBucketItems.some((measureBucketItem) => measureBucketItem.localIdentifier === filter.measureLocalIdentifier);
143
150
  }
151
+ /**
152
+ * A ranking filter references a catalog measure (not in the buckets) via measureRef. Such filters are
153
+ * kept as-is, mirroring measure value filters.
154
+ */
155
+ function isCatalogRankingFilter(filter) {
156
+ return filter.measureRef !== undefined;
157
+ }
144
158
  export function sanitizeFilters(newReferencePoint, oldReferencePoint) {
145
159
  const attributeBucketItems = getAllAttributeItems(newReferencePoint.buckets);
146
160
  const oldAttributeBucketItems = getAllAttributeItems(oldReferencePoint.buckets);
@@ -180,10 +194,15 @@ export function sanitizeFilters(newReferencePoint, oldReferencePoint) {
180
194
  }
181
195
  return attributeBucketItems.some((attributeBucketItem) => attributeBucketItem.attribute === filter.attribute);
182
196
  }
183
- else if (isMeasureValueFilter(filter)) {
184
- return sanitizeMeasureValueFilter(filter, attributeBucketItems, measureBucketItems);
185
- }
186
197
  else if (isRankingFilter(filter)) {
198
+ // A ranking filter referencing a catalog measure by ObjRef need not have that measure in the
199
+ // buckets, and may have no attributes (granularity is added later) - mirrors measure value
200
+ // filters. Keep it as-is.
201
+ if (isCatalogRankingFilter(filter)) {
202
+ return true;
203
+ }
204
+ // Legacy ranking filter referencing a bucket measure by local identifier - it requires a
205
+ // slicing attribute and the ranked measure to be present in the buckets.
187
206
  if (attributeBucketItems.length === 0) {
188
207
  return false;
189
208
  }
@@ -192,6 +211,9 @@ export function sanitizeFilters(newReferencePoint, oldReferencePoint) {
192
211
  filter.attributes.every((localIdentifier) => attributeBucketItems.some((attributeBucketItem) => attributeBucketItem.localIdentifier === localIdentifier));
193
212
  return hasValidMeasure && hasValidAttributes;
194
213
  }
214
+ else if (isMeasureValueFilter(filter)) {
215
+ return sanitizeMeasureValueFilter(filter, attributeBucketItems, measureBucketItems);
216
+ }
195
217
  return false;
196
218
  });
197
219
  return {
@@ -1491,6 +1491,14 @@ export declare interface IObjectShareControllerState {
1491
1491
  subview: "main" | "addGrantee";
1492
1492
  status: "idle" | "loading" | "success" | "error" | "saving";
1493
1493
  error?: Error;
1494
+ /**
1495
+ * Whether object-level permissions are unavailable to the current user — the
1496
+ * manage-gated access-list endpoint returned 404, the backend's signal that
1497
+ * the caller cannot manage this object's sharing. Distinct from a transient
1498
+ * load error (5xx / network), which may still resolve: consumers use this to
1499
+ * hide the share UI entirely rather than to retry.
1500
+ */
1501
+ accessUnavailable: boolean;
1494
1502
  summary: IObjectAccessSummary | undefined;
1495
1503
  grantees: IObjectShareGrantee[];
1496
1504
  generalAccess: GeneralAccessValue;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Whether an access-list error means object-level permissions aren't available to
3
+ * the caller: the manage-gated permissions endpoint returns **404** for an object
4
+ * (or label) the caller can't manage. We match that exact status and nothing else
5
+ * — a transient failure (5xx / 403 / network) may still resolve, so it must not be
6
+ * read as a permanent "no". If the backend ever signals this with a different
7
+ * status, widen here deliberately, in coordination with the backend.
8
+ *
9
+ * @internal
10
+ */
11
+ export declare function isPermissionsNotAvailable(error: unknown): boolean;
12
+ //# sourceMappingURL=accessErrors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessErrors.d.ts","sourceRoot":"","sources":["../../src/share/accessErrors.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEjE"}
@@ -0,0 +1,15 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isUnexpectedResponseError } from "@gooddata/sdk-backend-spi";
3
+ /**
4
+ * Whether an access-list error means object-level permissions aren't available to
5
+ * the caller: the manage-gated permissions endpoint returns **404** for an object
6
+ * (or label) the caller can't manage. We match that exact status and nothing else
7
+ * — a transient failure (5xx / 403 / network) may still resolve, so it must not be
8
+ * read as a permanent "no". If the backend ever signals this with a different
9
+ * status, widen here deliberately, in coordination with the backend.
10
+ *
11
+ * @internal
12
+ */
13
+ export function isPermissionsNotAvailable(error) {
14
+ return isUnexpectedResponseError(error) && error.httpStatus === 404;
15
+ }
@@ -3,6 +3,13 @@ import type { IObjectShareGrantee, ObjectSharePermissionLevel } from "./objectSh
3
3
  import type { IObjectShareLabel } from "./types.js";
4
4
  /** Stable row id shared by grantee rows and picker options: `user:<ref>` / `group:<ref>`. */
5
5
  export declare function granteeId(kind: "user" | "group", ref: ObjRef): string;
6
+ /**
7
+ * Whether a grantee row carries no human name — its name is just the serialized
8
+ * ref the backend grant fell back to when the permissions endpoint returned no
9
+ * name. The signal both for backfilling a row from the picker/assignee cache and
10
+ * for deciding whether to resolve names eagerly at all.
11
+ */
12
+ export declare function granteeNameUnresolved(grantee: IObjectShareGrantee): boolean;
6
13
  /** Case-insensitive match of an assignee against the picker query (name, or email for users). */
7
14
  export declare function assigneeMatchesQuery(assignee: IAvailableAccessGrantee, query: string): boolean;
8
15
  /** The row's directly-granted level — the strongest permission present, defaulting to VIEW. */
@@ -39,6 +46,17 @@ export declare function buildLabelMutations(principal: LabelScopePrincipal, desi
39
46
  ref: ObjRef;
40
47
  grantee: IGranularAccessGrantee;
41
48
  }>;
49
+ /**
50
+ * Multi-principal variant of {@link buildLabelMutations}: groups the per-label
51
+ * writes so each label is one write carrying every principal that changes on it.
52
+ * Keys on `label.id`, not the raw `ObjRef` — a Map keyed on ObjRef would key on
53
+ * object identity and fail to merge equal-but-distinct refs.
54
+ */
55
+ export declare function buildLabelMutationsForPrincipals(principals: LabelScopePrincipal[], desiredLabelIds: ReadonlySet<string>, currentLabelIds: ReadonlySet<string>, labels: IObjectShareLabel[]): Array<{
56
+ id: string;
57
+ ref: ObjRef;
58
+ grantees: IGranularAccessGrantee[];
59
+ }>;
42
60
  /** Stable empty-labels default so the hook's default arg doesn't churn identities. */
43
61
  export declare const NO_LABELS: IObjectShareLabel[];
44
62
  /** Shared empty id-set for "no labels in scope" diffs. */
@@ -1 +1 @@
1
- {"version":3,"file":"objectShareController.helpers.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,MAAM,EAId,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM9F;AAKD,iGAA+F;AAC/F,wBAAgB,WAAW,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,0BAA0B,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,0BAA0B,EAClC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACxC,0BAA0B,GAAG,SAAS,CAGxC;AAYD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,EAAE,CA0BjG;AAUD,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,0BAA0B,GAAG,MAAM,GAC3C,sBAAsB,CAKxB;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GACzB;IAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,CAAC;AAElC,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,0BAA0B,GAAG,MAAM,0BAO7C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,MAAM,EAAE,iBAAiB,EAAE,GAC5B,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAWzD;AAED,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,iBAAiB,EAAO,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAU/E"}
1
+ {"version":3,"file":"objectShareController.helpers.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.helpers.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,MAAM,EAId,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACxG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAE3E;AAED,iGAAiG;AACjG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,uBAAuB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM9F;AAKD,iGAA+F;AAC/F,wBAAgB,WAAW,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,0BAA0B,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,0BAA0B,EAClC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACxC,0BAA0B,GAAG,SAAS,CAGxC;AAYD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,GAAG,SAAS,GAAG,mBAAmB,EAAE,CA0BjG;AAUD,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,0BAA0B,GAAG,MAAM,GAC3C,sBAAsB,CAKxB;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GACzB;IAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,iBAAiB,EAAE,IAAI,CAAA;CAAE,CAAC;AAElC,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,0BAA0B,GAAG,MAAM,0BAO7C;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,MAAM,EAAE,iBAAiB,EAAE,GAC5B,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAWzD;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC5C,UAAU,EAAE,mBAAmB,EAAE,EACjC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,MAAM,EAAE,iBAAiB,EAAE,GAC5B,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,sBAAsB,EAAE,CAAA;CAAE,CAAC,CAcxE;AAED,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,iBAAiB,EAAO,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAU/E"}
@@ -4,6 +4,15 @@ import { isGranularUserAccess, isGranularUserGroupAccess, objRefToString, } from
4
4
  export function granteeId(kind, ref) {
5
5
  return `${kind}:${objRefToString(ref)}`;
6
6
  }
7
+ /**
8
+ * Whether a grantee row carries no human name — its name is just the serialized
9
+ * ref the backend grant fell back to when the permissions endpoint returned no
10
+ * name. The signal both for backfilling a row from the picker/assignee cache and
11
+ * for deciding whether to resolve names eagerly at all.
12
+ */
13
+ export function granteeNameUnresolved(grantee) {
14
+ return grantee.name === objRefToString(grantee.granteeRef);
15
+ }
7
16
  /** Case-insensitive match of an assignee against the picker query (name, or email for users). */
8
17
  export function assigneeMatchesQuery(assignee, query) {
9
18
  if (!query) {
@@ -105,6 +114,27 @@ export function buildLabelMutations(principal, desiredLabelIds, currentLabelIds,
105
114
  }
106
115
  return writes;
107
116
  }
117
+ /**
118
+ * Multi-principal variant of {@link buildLabelMutations}: groups the per-label
119
+ * writes so each label is one write carrying every principal that changes on it.
120
+ * Keys on `label.id`, not the raw `ObjRef` — a Map keyed on ObjRef would key on
121
+ * object identity and fail to merge equal-but-distinct refs.
122
+ */
123
+ export function buildLabelMutationsForPrincipals(principals, desiredLabelIds, currentLabelIds, labels) {
124
+ const byLabel = new Map();
125
+ for (const label of labels) {
126
+ for (const principal of principals) {
127
+ const writes = buildLabelMutations(principal, desiredLabelIds, currentLabelIds, [label]);
128
+ if (writes.length === 0) {
129
+ continue;
130
+ }
131
+ const entry = byLabel.get(label.id) ?? { id: label.id, ref: label.ref, grantees: [] };
132
+ entry.grantees.push(writes[0].grantee);
133
+ byLabel.set(label.id, entry);
134
+ }
135
+ }
136
+ return Array.from(byLabel.values());
137
+ }
108
138
  /** Stable empty-labels default so the hook's default arg doesn't churn identities. */
109
139
  export const NO_LABELS = [];
110
140
  /** Shared empty id-set for "no labels in scope" diffs. */
@@ -51,6 +51,14 @@ export interface IObjectShareControllerState {
51
51
  subview: "main" | "addGrantee";
52
52
  status: "idle" | "loading" | "success" | "error" | "saving";
53
53
  error?: Error;
54
+ /**
55
+ * Whether object-level permissions are unavailable to the current user — the
56
+ * manage-gated access-list endpoint returned 404, the backend's signal that
57
+ * the caller cannot manage this object's sharing. Distinct from a transient
58
+ * load error (5xx / network), which may still resolve: consumers use this to
59
+ * hide the share UI entirely rather than to retry.
60
+ */
61
+ accessUnavailable: boolean;
54
62
  summary: IObjectAccessSummary | undefined;
55
63
  grantees: IObjectShareGrantee[];
56
64
  generalAccess: GeneralAccessValue;
@@ -1 +1 @@
1
- {"version":3,"file":"objectShareController.types.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"objectShareController.types.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.types.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,13 @@ export interface IAccessList {
25
25
  status: IObjectShareControllerState["status"];
26
26
  /** Error from the initial/target-change load. */
27
27
  loadError: Error | undefined;
28
+ /**
29
+ * Whether the current target's load was denied because the caller can't manage
30
+ * its permissions (manage-gated endpoint returns 404). Derived from the live
31
+ * fetch, not the persisted `loadError`, so it can't lag a target switch and
32
+ * flag a new target with the previous one's 404.
33
+ */
34
+ accessUnavailable: boolean;
28
35
  /** Write a grant change to the backend and toast. False on failure; no refetch. */
29
36
  commit: (mutate: IGranularAccessGrantee[], successMessage: {
30
37
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"useAccessList.d.ts","sourceRoot":"","sources":["../../src/share/useAccessList.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAAE,KAAK,sBAAsB,EAAE,KAAK,MAAM,EAAkB,MAAM,qBAAqB,CAAC;AAE/F,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAmB,MAAM,sBAAsB,CAAC;AAK7G,OAAO,KAAK,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,iFAAiF;IACjF,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,wFAAwF;IACxF,OAAO,EAAE,OAAO,CAAC;IACjB,iGAA+F;IAC/F,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,uFAAqF;IACrF,aAAa,EAAE,kBAAkB,CAAC;IAClC,6FAA2F;IAC3F,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,iEAAiE;IACjE,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,MAAM,EAAE,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IAC9C,iDAAiD;IACjD,SAAS,EAAE,KAAK,GAAG,SAAS,CAAC;IAE7B,mFAAmF;IACnF,MAAM,EAAE,CAAC,MAAM,EAAE,sBAAsB,EAAE,EAAE,cAAc,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/F,sFAAoF;IACpF,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjE,2FAA2F;IAC3F,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjC,qGAAqG;IACrG,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC;IACzE,8EAA8E;IAC9E,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC3E;;;;OAIG;IACH,iBAAiB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAC1E,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAC/E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,GAClC,WAAW,CAiOb"}
1
+ {"version":3,"file":"useAccessList.d.ts","sourceRoot":"","sources":["../../src/share/useAccessList.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,EAEH,KAAK,sBAAsB,EAC3B,KAAK,MAAM,EAEd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAmB,MAAM,sBAAsB,CAAC;AAW7G,OAAO,KAAK,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,iFAAiF;IACjF,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,wFAAwF;IACxF,OAAO,EAAE,OAAO,CAAC;IACjB,iGAA+F;IAC/F,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,uFAAqF;IACrF,aAAa,EAAE,kBAAkB,CAAC;IAClC,6FAA2F;IAC3F,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,iEAAiE;IACjE,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC1C,+DAA+D;IAC/D,MAAM,EAAE,2BAA2B,CAAC,QAAQ,CAAC,CAAC;IAC9C,iDAAiD;IACjD,SAAS,EAAE,KAAK,GAAG,SAAS,CAAC;IAC7B;;;;;OAKG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAE3B,mFAAmF;IACnF,MAAM,EAAE,CAAC,MAAM,EAAE,sBAAsB,EAAE,EAAE,cAAc,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/F,sFAAoF;IACpF,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjE,2FAA2F;IAC3F,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjC,qGAAqG;IACrG,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC;IACzE,8EAA8E;IAC9E,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAC3E;;;;OAIG;IACH,iBAAiB,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAC1E,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAC/E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,GAClC,WAAW,CA4Qb"}
@@ -1,11 +1,12 @@
1
1
  // (C) 2026 GoodData Corporation
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { objRefToString } from "@gooddata/sdk-model";
3
+ import { objRefToString, } from "@gooddata/sdk-model";
4
4
  import { useBackendStrict, useCancelablePromise, useWorkspaceStrict } from "@gooddata/sdk-ui";
5
5
  import { useToastMessage } from "@gooddata/sdk-ui-kit";
6
+ import { isPermissionsNotAvailable } from "./accessErrors.js";
6
7
  import { deriveGeneralAccess, deriveWorkspacePermissionLevel } from "./accessSummary.js";
7
8
  import { objectShareMessages } from "./messages.js";
8
- import { assigneeMatchesQuery, granteeId, granteesFromAccessList } from "./objectShareController.helpers.js";
9
+ import { assigneeMatchesQuery, granteeId, granteeNameUnresolved, granteesFromAccessList, } from "./objectShareController.helpers.js";
9
10
  /**
10
11
  * Owns the backend access list. It is fetched once (per target) then seeded into
11
12
  * local state, which is authoritative while the dialog/summary is mounted:
@@ -41,6 +42,31 @@ export function useAccessList(target, onSaved) {
41
42
  // is a serialized `kind:identifier`, which loses Uri-vs-Id). Reused for writes.
42
43
  const [knownRefs, setKnownRefs] = useState({});
43
44
  const targetKey = target ? objRefToString(target.ref) : undefined;
45
+ // Learn each assignee's real name + ref into the caches, keyed by the same
46
+ // grantee id the rows use. Written from both the picker (loadOptions) and the
47
+ // eager resolve below, so a granted row can show a human name even when its
48
+ // access-list grant carried only a raw id. Must not depend on the caches it
49
+ // writes, or it would re-trigger loadOptions' fetch.
50
+ const cacheAssignees = useCallback((assignees) => {
51
+ const withIds = assignees.map((a) => ({
52
+ assignee: a,
53
+ id: granteeId(a.type === "user" ? "user" : "group", a.ref),
54
+ }));
55
+ setKnownNames((prev) => {
56
+ const next = { ...prev };
57
+ for (const { assignee, id } of withIds) {
58
+ next[id] = assignee.name;
59
+ }
60
+ return next;
61
+ });
62
+ setKnownRefs((prev) => {
63
+ const next = { ...prev };
64
+ for (const { assignee, id } of withIds) {
65
+ next[id] = assignee.ref;
66
+ }
67
+ return next;
68
+ });
69
+ }, []);
44
70
  // Initial (and target-change) load. Seeds local state once per target; thereafter
45
71
  // local state is authoritative and mutations write through it.
46
72
  const { result: fetchedList, status: fetchStatus, error: fetchError, } = useCancelablePromise({
@@ -85,9 +111,28 @@ export function useAccessList(target, onSaved) {
85
111
  const namedGrantees = useMemo(() => hasList
86
112
  ? grantees.map((g) => {
87
113
  const known = knownNames[g.id];
88
- return known && g.name === objRefToString(g.granteeRef) ? { ...g, name: known } : g;
114
+ return known && granteeNameUnresolved(g) ? { ...g, name: known } : g;
89
115
  })
90
116
  : [], [hasList, grantees, knownNames]);
117
+ // Eagerly resolve display names when a fetched grant carried only a raw id.
118
+ // The permissions endpoint returns grants without user/group names, so after a
119
+ // page reload a row would show the raw id until the picker is opened. Fetch the
120
+ // available assignees (the same source the picker uses) to fill the name cache,
121
+ // but only when something is actually unresolved — a backend that does return
122
+ // names skips the extra request entirely. Keyed on the serialized targetKey, not
123
+ // the target object, so an inline-ref consumer re-rendering mid-fetch doesn't
124
+ // cancel it. Once the cache lands, `needsNameResolve` flips false and the promise
125
+ // is withdrawn, so it never refetches — an id the listing can't name just stays.
126
+ const needsNameResolve = hasList && grantees.some((g) => granteeNameUnresolved(g) && !knownNames[g.id]);
127
+ useCancelablePromise({
128
+ promise: target && needsNameResolve
129
+ ? () => backend.workspace(workspace).objectPermissions().getAvailableAssignees(target)
130
+ : undefined,
131
+ onSuccess: cacheAssignees,
132
+ // Best-effort backfill: on error the raw id stays (pre-fix behavior, no
133
+ // regression) and the picker can still resolve it on demand. No toast.
134
+ onError: () => { },
135
+ }, [backend, workspace, targetKey, needsNameResolve]);
91
136
  // Stable sorted key of the currently-granted ids — drives the picker's
92
137
  // "exclude already-granted" filter. Keyed on ids only (not names), so the
93
138
  // picker's own name-cache writes can't change loadOptions' identity and
@@ -119,6 +164,10 @@ export function useAccessList(target, onSaved) {
119
164
  ? "success"
120
165
  : "loading"
121
166
  : "idle";
167
+ // Derived from the live fetch (status + error travel together), not the
168
+ // persisted loadError — so a target switch can't briefly flag the new target
169
+ // with the previous target's 404 before the load effect updates loadError.
170
+ const accessUnavailable = fetchStatus === "error" && isPermissionsNotAvailable(fetchError);
122
171
  // Write a single grant change to the backend, then toast. The caller applies
123
172
  // the optimistic local write-through and rolls it back on failure; there is no
124
173
  // refetch — local state stays authoritative.
@@ -159,21 +208,8 @@ export function useAccessList(target, onSaved) {
159
208
  }));
160
209
  // Remember every assignee's real name + ref so granted rows can show them
161
210
  // even when the access-list grant later returns only a raw id. Safe here:
162
- // neither is a dependency of loadOptions, so this won't re-trigger it.
163
- setKnownNames((prev) => {
164
- const next = { ...prev };
165
- for (const { assignee, id } of withIds) {
166
- next[id] = assignee.name;
167
- }
168
- return next;
169
- });
170
- setKnownRefs((prev) => {
171
- const next = { ...prev };
172
- for (const { assignee, id } of withIds) {
173
- next[id] = assignee.ref;
174
- }
175
- return next;
176
- });
211
+ // the caches aren't dependencies of loadOptions, so this won't re-trigger it.
212
+ cacheAssignees(assignees);
177
213
  const excluded = new Set(excludedIdsKey ? excludedIdsKey.split(",") : []);
178
214
  const selectable = withIds
179
215
  .filter(({ id }) => !excluded.has(id)) // hide anyone already granted
@@ -191,7 +227,7 @@ export function useAccessList(target, onSaved) {
191
227
  .filter(({ assignee }) => assignee.type !== "user")
192
228
  .map(({ assignee, id }) => ({ id, kind: "group", name: assignee.name })),
193
229
  };
194
- }, [backend, workspace, target, excludedIdsKey]);
230
+ }, [backend, workspace, target, excludedIdsKey, cacheAssignees]);
195
231
  // Reuse the picker's original ref (preserves UriRef vs IdentifierRef);
196
232
  // fall back to the serialized id only if it wasn't cached.
197
233
  const refForId = useCallback((id) => knownRefs[id] ?? { identifier: id.split(":", 2)[1] }, [knownRefs]);
@@ -204,6 +240,7 @@ export function useAccessList(target, onSaved) {
204
240
  summary,
205
241
  status,
206
242
  loadError,
243
+ accessUnavailable,
207
244
  commit,
208
245
  loadOptions,
209
246
  refForId,
@@ -1,4 +1,4 @@
1
- import { type IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
1
+ import type { IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
2
2
  import { type LabelScopePrincipal } from "./objectShareController.helpers.js";
3
3
  import type { IObjectShareLabel } from "./types.js";
4
4
  /**
@@ -34,6 +34,16 @@ export interface ILabelScope {
34
34
  * if any write fails (callers surface the error and roll back).
35
35
  */
36
36
  reconcileLabelScope: (principal: LabelScopePrincipal, desiredLabelIds: ReadonlySet<string>, currentLabelIds: ReadonlySet<string>) => Promise<boolean>;
37
+ /**
38
+ * Multi-principal {@link reconcileLabelScope}: one `manageObjectPermissions`
39
+ * per label carrying all principals that change on it (used by Add). Reports
40
+ * `failedLabelIds` so the caller can keep the labels that landed rather than
41
+ * discard the whole scope on a partial failure.
42
+ */
43
+ reconcileLabelScopeMany: (principals: LabelScopePrincipal[], desiredLabelIds: ReadonlySet<string>, currentLabelIds: ReadonlySet<string>) => Promise<{
44
+ ok: boolean;
45
+ failedLabelIds: string[];
46
+ }>;
37
47
  }
38
48
  /**
39
49
  * 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,EAAE,KAAK,wBAAwB,EAA6B,MAAM,2BAA2B,CAAC;AAGrG,OAAO,EACH,KAAK,mBAAmB,EAG3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAWpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;OAIG;IACH,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC;;;;;;;;OAQG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB,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;;;;OAIG;IACH,mBAAmB,EAAE,CACjB,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,KACnC,OAAO,CAAC,OAAO,CAAC,CAAC;CACzB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,OAAO,EAAE,OAAO,EAChB,mBAAmB,EAAE,MAAM,EAAE,EAC7B,WAAW,EAAE,OAAO,EACpB,aAAa,EAAE,OAAO,GACvB,WAAW,CAsKb"}
1
+ {"version":3,"file":"useLabelScope.d.ts","sourceRoot":"","sources":["../../src/share/useLabelScope.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAI1E,OAAO,EACH,KAAK,mBAAmB,EAI3B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IACxB;;;;OAIG;IACH,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC;;;;;;;;OAQG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB,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;;;;OAIG;IACH,mBAAmB,EAAE,CACjB,SAAS,EAAE,mBAAmB,EAC9B,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,KACnC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtB;;;;;OAKG;IACH,uBAAuB,EAAE,CACrB,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,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,OAAO,EAAE,OAAO,EAChB,mBAAmB,EAAE,MAAM,EAAE,EAC7B,WAAW,EAAE,OAAO,EACpB,aAAa,EAAE,OAAO,GACvB,WAAW,CAqMb"}
@@ -1,16 +1,8 @@
1
1
  // (C) 2026 GoodData Corporation
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
- import { isUnexpectedResponseError } from "@gooddata/sdk-backend-spi";
4
3
  import { useBackendStrict, useWorkspaceStrict } from "@gooddata/sdk-ui";
5
- import { buildLabelMutations, isGranteeGrantedIn, } from "./objectShareController.helpers.js";
6
- /**
7
- * Whether a label's access-list error means the label is genuinely not
8
- * independently permissionable (a definitive 404 / 501), as opposed to a
9
- * transient failure (5xx / 403 / network) that should NOT drop a real label.
10
- */
11
- function isNotPermissionable(error) {
12
- return isUnexpectedResponseError(error) && (error.httpStatus === 404 || error.httpStatus === 501);
13
- }
4
+ import { isPermissionsNotAvailable } from "./accessErrors.js";
5
+ import { buildLabelMutations, buildLabelMutationsForPrincipals, isGranteeGrantedIn, } from "./objectShareController.helpers.js";
14
6
  /**
15
7
  * Owns label-scope resolution + writes for {@link useObjectShareController}.
16
8
  * Resolves each grantee's scope by fetching every label's access list, tracks
@@ -72,10 +64,10 @@ export function useLabelScope(target, targetKey, labels, hasList, committedGrant
72
64
  .objectPermissions()
73
65
  .getAccessList({ kind: "label", ref: label.ref })
74
66
  .then((list) => ({ label, list }))
75
- // Only a definitive 404/501 means the label can't take a per-label
67
+ // Only a definitive 404 means the label can't take a per-label
76
68
  // grant. A transient failure (5xx / 403 / network) must NOT drop a
77
69
  // real label — return it without grant info so it stays grantable.
78
- .catch((error) => ({ label, transient: !isNotPermissionable(error) })))).then((results) => {
70
+ .catch((error) => ({ label, transient: !isPermissionsNotAvailable(error) })))).then((results) => {
79
71
  if (cancelled) {
80
72
  return;
81
73
  }
@@ -87,7 +79,7 @@ export function useLabelScope(target, targetKey, labels, hasList, committedGrant
87
79
  for (const result of results) {
88
80
  if ("transient" in result) {
89
81
  // Keep transiently-failed labels permissionable (don't hide a real
90
- // label); skip definitively-not-permissionable ones (404/501).
82
+ // label); skip definitively-not-permissionable ones (404).
91
83
  if (result.transient) {
92
84
  permissionable.add(result.label.id);
93
85
  }
@@ -156,11 +148,24 @@ export function useLabelScope(target, targetKey, labels, hasList, committedGrant
156
148
  const results = await Promise.allSettled(writes.map((w) => svc.manageObjectPermissions({ kind: "label", ref: w.ref }, [w.grantee])));
157
149
  return results.every((r) => r.status === "fulfilled");
158
150
  }, [effectiveLabels, backend, workspace]);
151
+ const reconcileLabelScopeMany = useCallback(async (principals, desiredLabelIds, currentLabelIds) => {
152
+ const writes = buildLabelMutationsForPrincipals(principals, desiredLabelIds, currentLabelIds, effectiveLabels);
153
+ if (writes.length === 0) {
154
+ return { ok: true, failedLabelIds: [] };
155
+ }
156
+ const svc = backend.workspace(workspace).objectPermissions();
157
+ const results = await Promise.allSettled(writes.map((w) => svc.manageObjectPermissions({ kind: "label", ref: w.ref }, w.grantees)));
158
+ const failedLabelIds = writes
159
+ .filter((_, i) => results[i].status === "rejected")
160
+ .map((w) => w.id);
161
+ return { ok: failedLabelIds.length === 0, failedLabelIds };
162
+ }, [effectiveLabels, backend, workspace]);
159
163
  return {
160
164
  effectiveLabels,
161
165
  labelsResolved,
162
166
  selectedLabelIdsByGrantee,
163
167
  setSelectedLabelIdsByGrantee,
164
168
  reconcileLabelScope,
169
+ reconcileLabelScopeMany,
165
170
  };
166
171
  }
@@ -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;AAa1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CAyjBxB"}
1
+ {"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAa1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CA8jBxB"}
@@ -42,7 +42,7 @@ export function useObjectShareController(target, options) {
42
42
  const [workspaceLevelSaving, setWorkspaceLevelSaving] = useState(false);
43
43
  // The backend access list, seeded into local state (rows, general access,
44
44
  // summary, the commit/picker primitives) lives in its own hook.
45
- const { targetKey, hasList, grantees, generalAccess, workspaceLevel, summary, status, loadError, commit, loadOptions, refForId, setGrantees, setGeneralAccess, setWorkspaceLevel, setKnownNames, } = useAccessList(target, onSaved);
45
+ const { targetKey, hasList, grantees, generalAccess, workspaceLevel, summary, status, loadError, accessUnavailable, commit, loadOptions, refForId, setGrantees, setGeneralAccess, setWorkspaceLevel, setKnownNames, } = useAccessList(target, onSaved);
46
46
  // Always-current target key, read inside async mutation finalizers that captured
47
47
  // an older one. A write started for object A resolves after the user navigated to
48
48
  // object B; because local state is authoritative (no refetch), applying A's
@@ -55,7 +55,7 @@ export function useObjectShareController(target, options) {
55
55
  // row is excluded so its scope is dropped, not re-seeded).
56
56
  const committedGranteeIds = useMemo(() => grantees.filter((g) => g.pending !== "removing").map((g) => g.id), [grantees]);
57
57
  // Per-label scope resolution + the single label-write path live in their own hook.
58
- const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, reconcileLabelScope, } = useLabelScope(target, targetKey, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
58
+ const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, reconcileLabelScope, reconcileLabelScopeMany, } = useLabelScope(target, targetKey, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
59
59
  // Drop the transient UI buffers when the permission target changes. The detail
60
60
  // view is reused across objects and closes the dialog by toggling `isOpen`
61
61
  // alone, so without this a staged add-grantee subview or general-access confirm
@@ -170,23 +170,22 @@ export function useObjectShareController(target, options) {
170
170
  }
171
171
  // Object grant landed — clear the saving marker on the new rows.
172
172
  setGrantees((prev) => prev.map((g) => (addedIds.includes(g.id) ? { ...g, pending: undefined } : g)));
173
- // Mirror each new grantee's full label scope; pin failures to primary-only.
174
- const scoped = await Promise.all(pendingGrantees.map((g) => reconcileLabelScope({ kind: g.kind, granteeRef: refForId(g.id) }, allLabelIdSet, EMPTY_IDS)));
173
+ // One label write per label carrying all added grantees, not one per grantee.
174
+ const principals = pendingGrantees.map((g) => ({ kind: g.kind, granteeRef: refForId(g.id) }));
175
+ const { ok: labelsOk, failedLabelIds } = await reconcileLabelScopeMany(principals, allLabelIdSet, EMPTY_IDS);
175
176
  if (targetKeyRef.current !== startedFor) {
176
177
  return;
177
178
  }
178
- const failed = pendingGrantees.filter((_, i) => !scoped[i]);
179
- if (failed.length > 0) {
179
+ if (!labelsOk) {
180
180
  toast.addWarning(objectShareMessages.toastLabelScopePartial);
181
- // Some non-primary label writes didn't persist. Don't drop the entry
182
- // a missing entry means "all selected", which would falsely show full
183
- // access. Pin the failed grantees to the primary label only (the one
184
- // that's always granted with the object), reflecting what actually stuck.
185
- const primaryIds = effectiveLabels.filter((l) => l.isPrimary).map((l) => l.id);
181
+ // Drop only the failed labels, keeping those that landed otherwise local
182
+ // scope under-reports and changeGranteeLabels later skips their revokes.
183
+ const failed = new Set(failedLabelIds);
184
+ const survived = allLabelIds.filter((id) => !failed.has(id));
186
185
  setSelectedLabelIdsByGrantee((prev) => {
187
186
  const next = { ...prev };
188
- for (const g of failed) {
189
- next[g.id] = primaryIds;
187
+ for (const id of addedIds) {
188
+ next[id] = survived;
190
189
  }
191
190
  return next;
192
191
  });
@@ -197,7 +196,7 @@ export function useObjectShareController(target, options) {
197
196
  commit,
198
197
  closeAddGrantee,
199
198
  effectiveLabels,
200
- reconcileLabelScope,
199
+ reconcileLabelScopeMany,
201
200
  refForId,
202
201
  toast,
203
202
  setGrantees,
@@ -458,6 +457,7 @@ export function useObjectShareController(target, options) {
458
457
  subview,
459
458
  status,
460
459
  error: loadError,
460
+ accessUnavailable,
461
461
  summary,
462
462
  grantees,
463
463
  generalAccess,
@@ -475,6 +475,7 @@ export function useObjectShareController(target, options) {
475
475
  subview,
476
476
  status,
477
477
  loadError,
478
+ accessUnavailable,
478
479
  summary,
479
480
  grantees,
480
481
  generalAccess,