@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.
- package/NOTICE +75 -75
- package/esm/internal/components/configurationControls/DropdownControl.d.ts.map +1 -1
- package/esm/internal/components/configurationControls/DropdownControl.js +13 -8
- package/esm/internal/interfaces/Visualization.d.ts +10 -1
- package/esm/internal/interfaces/Visualization.d.ts.map +1 -1
- package/esm/internal/utils/bucketHelper.d.ts.map +1 -1
- package/esm/internal/utils/bucketHelper.js +26 -4
- package/esm/sdk-ui-ext.d.ts +8 -0
- package/esm/share/accessErrors.d.ts +12 -0
- package/esm/share/accessErrors.d.ts.map +1 -0
- package/esm/share/accessErrors.js +15 -0
- package/esm/share/objectShareController.helpers.d.ts +18 -0
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +30 -0
- package/esm/share/objectShareController.types.d.ts +8 -0
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +7 -0
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +56 -19
- package/esm/share/useLabelScope.d.ts +11 -1
- package/esm/share/useLabelScope.d.ts.map +1 -1
- package/esm/share/useLabelScope.js +18 -13
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +15 -14
- package/package.json +21 -21
|
@@ -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 {
|
package/esm/sdk-ui-ext.d.ts
CHANGED
|
@@ -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":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AACH,MAAM,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAElE;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAChC,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,0BAA0B,CAAC;IAClC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,MAAM,GAAG,YAAY,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5D,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAE1C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,aAAa,EAAE,kBAAkB,CAAC;IAClC;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC;;;;OAIG;IACH,oBAAoB,EAAE,OAAO,CAAC;IAE9B;;;OAGG;IACH,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEpD,mEAAiE;IACjE,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C,qEAAqE;IACrE,eAAe,EAAE,gBAAgB,EAAE,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC1C,sFAAsF;IACtF,KAAK,EAAE,MAAM,IAAI,CAAC;IAElB,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,kBAAkB,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;IACvD;;;;OAIG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjE,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,oEAAoE;IACpE,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,0BAA0B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/F,oCAAoC;IACpC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD;;;OAGG;IACH,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtF,+DAA+D;IAC/D,0BAA0B,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/D,yBAAyB,EAAE,MAAM,IAAI,CAAC;IACtC,4DAA4D;IAC5D,0BAA0B,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD;;;;;OAKG;IACH,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,2BAA2B,CAAC;IACnC,OAAO,EAAE,6BAA6B,CAAC;CAC1C;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CAC3B"}
|
|
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,
|
|
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 &&
|
|
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
|
-
//
|
|
163
|
-
|
|
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 {
|
|
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,
|
|
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 {
|
|
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
|
|
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: !
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
174
|
-
const
|
|
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
|
-
|
|
179
|
-
if (failed.length > 0) {
|
|
179
|
+
if (!labelsOk) {
|
|
180
180
|
toast.addWarning(objectShareMessages.toastLabelScopePartial);
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
189
|
-
next[
|
|
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
|
-
|
|
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,
|