@gooddata/sdk-ui-ext 11.43.0-alpha.3 → 11.43.0-alpha.5
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 +641 -841
- package/esm/index.d.ts +2 -2
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/de-DE.localization-bundle.js +43 -1
- package/esm/internal/translations/en-AU.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-AU.localization-bundle.js +44 -2
- package/esm/internal/translations/en-GB.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/en-GB.localization-bundle.js +44 -2
- package/esm/internal/translations/es-419.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-419.localization-bundle.js +44 -2
- package/esm/internal/translations/es-ES.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/es-ES.localization-bundle.js +44 -2
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fi-FI.localization-bundle.js +43 -1
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-CA.localization-bundle.js +43 -1
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/fr-FR.localization-bundle.js +43 -1
- package/esm/internal/translations/id-ID.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/id-ID.localization-bundle.js +43 -1
- package/esm/internal/translations/it-IT.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/it-IT.localization-bundle.js +43 -1
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ja-JP.localization-bundle.js +43 -1
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ko-KR.localization-bundle.js +43 -1
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/nl-NL.localization-bundle.js +43 -1
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pl-PL.localization-bundle.js +43 -1
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-BR.localization-bundle.js +43 -1
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/pt-PT.localization-bundle.js +43 -1
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/ru-RU.localization-bundle.js +43 -1
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/sl-SI.localization-bundle.js +43 -1
- package/esm/internal/translations/th-TH.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/th-TH.localization-bundle.js +43 -1
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/tr-TR.localization-bundle.js +43 -1
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/uk-UA.localization-bundle.js +43 -1
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/vi-VN.localization-bundle.js +43 -1
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-HK.localization-bundle.js +44 -2
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hans.localization-bundle.js +43 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +42 -0
- package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
- package/esm/internal/translations/zh-Hant.localization-bundle.js +45 -3
- package/esm/sdk-ui-ext.d.ts +31 -10
- package/esm/share/ObjectShareDialog.js +2 -2
- package/esm/share/objectShareController.helpers.d.ts +1 -27
- package/esm/share/objectShareController.helpers.d.ts.map +1 -1
- package/esm/share/objectShareController.helpers.js +25 -48
- package/esm/share/objectShareController.types.d.ts +46 -4
- package/esm/share/objectShareController.types.d.ts.map +1 -1
- package/esm/share/useAccessList.d.ts +31 -18
- package/esm/share/useAccessList.d.ts.map +1 -1
- package/esm/share/useAccessList.js +85 -133
- package/esm/share/useLabelScope.d.ts +6 -8
- package/esm/share/useLabelScope.d.ts.map +1 -1
- package/esm/share/useLabelScope.js +34 -28
- package/esm/share/useObjectShareController.d.ts +19 -16
- package/esm/share/useObjectShareController.d.ts.map +1 -1
- package/esm/share/useObjectShareController.js +212 -119
- package/package.json +21 -22
- package/esm/share/useObjectShare.d.ts +0 -50
- package/esm/share/useObjectShare.d.ts.map +0 -1
- package/esm/share/useObjectShare.js +0 -17
|
@@ -12,9 +12,11 @@ export function assigneeMatchesQuery(assignee, query) {
|
|
|
12
12
|
const email = assignee.type === "user" ? (assignee.email ?? "") : "";
|
|
13
13
|
return `${assignee.name} ${email}`.toLowerCase().includes(query);
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
15
|
+
/** Permission levels from strongest to weakest; the row shows the strongest it holds. */
|
|
16
|
+
const LEVELS_STRONGEST_FIRST = ["EDIT", "SHARE", "VIEW"];
|
|
17
|
+
/** The row's directly-granted level — the strongest permission present, defaulting to VIEW. */
|
|
16
18
|
export function directLevel(permissions) {
|
|
17
|
-
return permissions.includes(
|
|
19
|
+
return LEVELS_STRONGEST_FIRST.find((level) => permissions.includes(level)) ?? "VIEW";
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* The effective permission to surface as a warning, or undefined when the direct
|
|
@@ -23,7 +25,17 @@ export function directLevel(permissions) {
|
|
|
23
25
|
* than what the row's permission control shows.
|
|
24
26
|
*/
|
|
25
27
|
export function effectivePermissionAbove(direct, inheritedPermissions) {
|
|
26
|
-
|
|
28
|
+
// A direct EDIT already outranks any inherited SHARE — never warn in that case.
|
|
29
|
+
return direct === "VIEW" && inheritedPermissions.includes("SHARE") ? "SHARE" : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** The permission-derived fields shared by every grantee row, regardless of kind. */
|
|
32
|
+
function granteeAccess(permissions, inheritedPermissions) {
|
|
33
|
+
const level = directLevel(permissions);
|
|
34
|
+
return {
|
|
35
|
+
level,
|
|
36
|
+
effectivePermission: effectivePermissionAbove(level, inheritedPermissions),
|
|
37
|
+
inheritsShare: inheritedPermissions.includes("SHARE"),
|
|
38
|
+
};
|
|
27
39
|
}
|
|
28
40
|
export function granteesFromAccessList(list) {
|
|
29
41
|
if (!list) {
|
|
@@ -32,71 +44,36 @@ export function granteesFromAccessList(list) {
|
|
|
32
44
|
const out = [];
|
|
33
45
|
for (const g of list.grants) {
|
|
34
46
|
if (isGranularUserAccess(g)) {
|
|
35
|
-
const level = directLevel(g.permissions);
|
|
36
47
|
out.push({
|
|
37
48
|
id: granteeId("user", g.user.ref),
|
|
38
49
|
kind: "user",
|
|
39
50
|
granteeRef: g.user.ref,
|
|
40
51
|
name: g.user.fullName ?? g.user.email ?? g.user.login,
|
|
41
52
|
email: g.user.email,
|
|
42
|
-
|
|
43
|
-
effectivePermission: effectivePermissionAbove(level, g.inheritedPermissions),
|
|
53
|
+
...granteeAccess(g.permissions, g.inheritedPermissions),
|
|
44
54
|
});
|
|
45
55
|
}
|
|
46
56
|
else if (isGranularUserGroupAccess(g)) {
|
|
47
|
-
const level = directLevel(g.permissions);
|
|
48
57
|
out.push({
|
|
49
58
|
id: granteeId("group", g.userGroup.ref),
|
|
50
59
|
kind: "group",
|
|
51
60
|
granteeRef: g.userGroup.ref,
|
|
52
61
|
name: g.userGroup.name ?? objRefToString(g.userGroup.ref),
|
|
53
|
-
|
|
54
|
-
effectivePermission: effectivePermissionAbove(level, g.inheritedPermissions),
|
|
62
|
+
...granteeAccess(g.permissions, g.inheritedPermissions),
|
|
55
63
|
});
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
return out;
|
|
59
67
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const [id, entry] of Object.entries(overlay)) {
|
|
68
|
-
if (entry.op === "remove") {
|
|
69
|
-
byId.delete(id);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
byId.set(id, { ...entry.grantee, pending: entry.pending ? "saving" : undefined });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [...byId.values()];
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Drop overlay entries the freshly-fetched list now confirms: a `set` whose
|
|
79
|
-
* committed level matches the intended level, or a `remove` whose grantee is
|
|
80
|
-
* gone. Unconfirmed entries are kept (server still lagging) but lose `pending`
|
|
81
|
-
* so the row shows the intended value without a spinner.
|
|
82
|
-
*/
|
|
83
|
-
export function reconcileOverlay(committed, overlay) {
|
|
84
|
-
const byId = new Map(committed.map((g) => [g.id, g]));
|
|
85
|
-
const next = {};
|
|
86
|
-
for (const [id, entry] of Object.entries(overlay)) {
|
|
87
|
-
if (entry.op === "remove") {
|
|
88
|
-
if (byId.has(id)) {
|
|
89
|
-
next[id] = { op: "remove", pending: false };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
else if (byId.get(id)?.level !== entry.grantee.level) {
|
|
93
|
-
next[id] = { op: "set", grantee: entry.grantee, pending: false };
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return next;
|
|
97
|
-
}
|
|
68
|
+
/** The permission set a grant carries at each level; a higher level always implies VIEW. */
|
|
69
|
+
const PERMISSIONS_BY_LEVEL = {
|
|
70
|
+
none: [],
|
|
71
|
+
VIEW: ["VIEW"],
|
|
72
|
+
SHARE: ["SHARE", "VIEW"],
|
|
73
|
+
EDIT: ["EDIT", "VIEW"],
|
|
74
|
+
};
|
|
98
75
|
export function toGranularGrantee(kind, granteeRef, level) {
|
|
99
|
-
const permissions =
|
|
76
|
+
const permissions = PERMISSIONS_BY_LEVEL[level];
|
|
100
77
|
return kind === "user"
|
|
101
78
|
? { type: "granularUser", granteeRef, permissions, inheritedPermissions: [] }
|
|
102
79
|
: { type: "granularGroup", granteeRef, permissions, inheritedPermissions: [] };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import type { ObjRef } from "@gooddata/sdk-model";
|
|
1
|
+
import type { AccessGranularPermission, ObjRef } from "@gooddata/sdk-model";
|
|
2
2
|
import type { GeneralAccessValue, IUiGranteeAsyncOptions, IUiPickedGrantee } from "@gooddata/sdk-ui-kit";
|
|
3
3
|
import type { IObjectAccessSummary, IObjectShareLabel } from "./types.js";
|
|
4
4
|
/**
|
|
5
|
-
* Permission level surfaced in the share dialog
|
|
6
|
-
*
|
|
5
|
+
* Permission level surfaced in the share dialog — the model's granular access
|
|
6
|
+
* permission (VIEW / EDIT / SHARE). `EDIT` is display-only: the dialog reflects
|
|
7
|
+
* an EDIT grant as a read-only "Can edit" row but cannot assign or change it
|
|
8
|
+
* (granting EDIT is not part of the share UI), so only VIEW and SHARE are
|
|
9
|
+
* selectable in the permission menu.
|
|
7
10
|
*
|
|
8
11
|
* @internal
|
|
9
12
|
*/
|
|
10
|
-
export type ObjectSharePermissionLevel =
|
|
13
|
+
export type ObjectSharePermissionLevel = AccessGranularPermission;
|
|
11
14
|
/**
|
|
12
15
|
* Dialog row derived from a backend grant.
|
|
13
16
|
*
|
|
@@ -28,6 +31,12 @@ export interface IObjectShareGrantee {
|
|
|
28
31
|
* the row's "effective permission" warning badge.
|
|
29
32
|
*/
|
|
30
33
|
effectivePermission?: ObjectSharePermissionLevel;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the grantee inherits SHARE (e.g. via a group), regardless of the
|
|
36
|
+
* current direct `level`. Retained from the fetch so `effectivePermission` can
|
|
37
|
+
* be recomputed locally when the direct level changes (no refetch).
|
|
38
|
+
*/
|
|
39
|
+
inheritsShare?: boolean;
|
|
31
40
|
/**
|
|
32
41
|
* Row-level in-flight state for optimistic updates: `"saving"` while a level
|
|
33
42
|
* change or freshly-added grant is being committed, `"removing"` while a
|
|
@@ -107,4 +116,37 @@ export interface IObjectShareController {
|
|
|
107
116
|
state: IObjectShareControllerState;
|
|
108
117
|
actions: IObjectShareControllerActions;
|
|
109
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Options for {@link useObjectShare}.
|
|
121
|
+
*
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export interface IUseObjectShareOptions {
|
|
125
|
+
/**
|
|
126
|
+
* Fires after each successful access mutation (add grantee, change level,
|
|
127
|
+
* remove, general access toggle). Use it to keep UI outside the dialog in
|
|
128
|
+
* sync with edits made inside it (e.g. refresh an inline access row).
|
|
129
|
+
*/
|
|
130
|
+
onSaved?: () => void;
|
|
131
|
+
/**
|
|
132
|
+
* Labels (display forms) of the shared attribute, enabling the per-grantee
|
|
133
|
+
* label-scope picker. Omit for objects without labels (e.g. facts).
|
|
134
|
+
*/
|
|
135
|
+
labels?: IObjectShareLabel[];
|
|
136
|
+
/**
|
|
137
|
+
* Whether loading the object's labels failed. While true the controller stays
|
|
138
|
+
* label-unresolved so every access-changing control is disabled: with the
|
|
139
|
+
* label set unknown, reconciling access would diff against an empty set and
|
|
140
|
+
* silently orphan any real per-label grants. Distinct from an object that
|
|
141
|
+
* genuinely has no labels (omit `labels`), where editing is safe.
|
|
142
|
+
*/
|
|
143
|
+
labelsError?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Whether the object's labels are still loading. While true the controller stays
|
|
146
|
+
* label-unresolved (same gating as `labelsError`): the labels aren't passed yet,
|
|
147
|
+
* so an empty list must not be mistaken for a label-free object — otherwise row
|
|
148
|
+
* controls would reconcile against an empty set and orphan real per-label grants.
|
|
149
|
+
*/
|
|
150
|
+
labelsLoading?: boolean;
|
|
151
|
+
}
|
|
110
152
|
//# sourceMappingURL=objectShareController.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"objectShareController.types.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"objectShareController.types.d.ts","sourceRoot":"","sources":["../../src/share/objectShareController.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import type { IObjectPermissionsObject } from "@gooddata/sdk-backend-spi";
|
|
2
|
-
import { type IGranularAccessGrantee, type
|
|
2
|
+
import { type IGranularAccessGrantee, type ObjRef } from "@gooddata/sdk-model";
|
|
3
3
|
import { type GeneralAccessValue, type IUiGranteeAsyncOptions } from "@gooddata/sdk-ui-kit";
|
|
4
|
-
import { type IGranteeOverlayEntry } from "./objectShareController.helpers.js";
|
|
5
4
|
import type { IObjectShareControllerState, IObjectShareGrantee } from "./objectShareController.types.js";
|
|
6
5
|
import type { IObjectAccessSummary } from "./types.js";
|
|
7
6
|
/**
|
|
8
|
-
* The owned access list
|
|
7
|
+
* The owned access list for {@link useObjectShareController}.
|
|
9
8
|
*
|
|
10
9
|
* @internal
|
|
11
10
|
*/
|
|
12
11
|
export interface IAccessList {
|
|
13
12
|
/** Stable serialized key of the current target's ref, or undefined when none. */
|
|
14
13
|
targetKey: string | undefined;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
/**
|
|
14
|
+
/** True once the current target's list has been fetched and seeded into local state. */
|
|
15
|
+
hasList: boolean;
|
|
16
|
+
/** Local grantee rows — seeded from the fetch, then authoritative; mutations write through. */
|
|
18
17
|
grantees: IObjectShareGrantee[];
|
|
19
|
-
/**
|
|
20
|
-
granteeIdsKey: string;
|
|
21
|
-
/** Optimistic-aware general access (workspace vs restricted). */
|
|
18
|
+
/** Workspace vs restricted general access — local state; mutations write through. */
|
|
22
19
|
generalAccess: GeneralAccessValue;
|
|
23
|
-
/**
|
|
20
|
+
/** Workspace-rule permission level (VIEW/SHARE) — local state; mutations write through. */
|
|
21
|
+
workspaceLevel: "VIEW" | "SHARE";
|
|
22
|
+
/** Inline access summary, or undefined before the first load. */
|
|
24
23
|
summary: IObjectAccessSummary | undefined;
|
|
25
24
|
/** Top-level load status surfaced as the controller status. */
|
|
26
25
|
status: IObjectShareControllerState["status"];
|
|
27
26
|
/** Error from the initial/target-change load. */
|
|
28
27
|
loadError: Error | undefined;
|
|
29
|
-
/**
|
|
28
|
+
/** Write a grant change to the backend and toast. False on failure; no refetch. */
|
|
30
29
|
commit: (mutate: IGranularAccessGrantee[], successMessage: {
|
|
31
30
|
id: string;
|
|
32
31
|
}) => Promise<boolean>;
|
|
@@ -34,16 +33,30 @@ export interface IAccessList {
|
|
|
34
33
|
loadOptions: (search: string) => Promise<IUiGranteeAsyncOptions>;
|
|
35
34
|
/** Resolve a grantee id back to the picker's original ObjRef (preserves Uri vs Id ref). */
|
|
36
35
|
refForId: (id: string) => ObjRef;
|
|
37
|
-
|
|
36
|
+
/** Write through a local grantee-row change (insert / level / remove); rolled back by the caller. */
|
|
37
|
+
setGrantees: React.Dispatch<React.SetStateAction<IObjectShareGrantee[]>>;
|
|
38
|
+
/** Write through a local general-access change; rolled back by the caller. */
|
|
39
|
+
setGeneralAccess: React.Dispatch<React.SetStateAction<GeneralAccessValue>>;
|
|
40
|
+
/**
|
|
41
|
+
* Write through the local workspace-rule permission level. A general-access write
|
|
42
|
+
* always grants workspace VIEW, so the caller sets this to keep the summary from
|
|
43
|
+
* showing a stale SHARE inherited from the initial fetch.
|
|
44
|
+
*/
|
|
45
|
+
setWorkspaceLevel: React.Dispatch<React.SetStateAction<"VIEW" | "SHARE">>;
|
|
38
46
|
setKnownNames: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
|
39
|
-
setOptimisticGeneralAccess: React.Dispatch<React.SetStateAction<GeneralAccessValue | undefined>>;
|
|
40
47
|
}
|
|
41
48
|
/**
|
|
42
|
-
* Owns the backend access list
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
49
|
+
* Owns the backend access list. It is fetched once (per target) then seeded into
|
|
50
|
+
* local state, which is authoritative while the dialog/summary is mounted:
|
|
51
|
+
* mutations write through to it directly and roll back the one changed entry on
|
|
52
|
+
* failure. There is no post-write refetch, so the grantee list never blanks and
|
|
53
|
+
* never fights the backend's read-after-write lag. A target switch re-seeds from
|
|
54
|
+
* the new fetch; the seed is gated on a per-target stamp so a late fetch for a
|
|
55
|
+
* previous target can't clobber the current one.
|
|
56
|
+
*
|
|
57
|
+
* Effective (inherited) permissions and display names come from that single
|
|
58
|
+
* fetch: inherited access doesn't change as a result of editing a direct grant,
|
|
59
|
+
* so the badge stays correct for the session without re-reading.
|
|
47
60
|
*
|
|
48
61
|
* @internal
|
|
49
62
|
*/
|
|
@@ -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,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,17 +1,23 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
|
-
import { useCallback, useEffect, useMemo,
|
|
3
|
-
import { objRefToString
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
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
6
|
import { deriveGeneralAccess, deriveWorkspacePermissionLevel } from "./accessSummary.js";
|
|
7
7
|
import { objectShareMessages } from "./messages.js";
|
|
8
|
-
import { assigneeMatchesQuery, granteeId, granteesFromAccessList
|
|
8
|
+
import { assigneeMatchesQuery, granteeId, granteesFromAccessList } from "./objectShareController.helpers.js";
|
|
9
9
|
/**
|
|
10
|
-
* Owns the backend access list
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Owns the backend access list. It is fetched once (per target) then seeded into
|
|
11
|
+
* local state, which is authoritative while the dialog/summary is mounted:
|
|
12
|
+
* mutations write through to it directly and roll back the one changed entry on
|
|
13
|
+
* failure. There is no post-write refetch, so the grantee list never blanks and
|
|
14
|
+
* never fights the backend's read-after-write lag. A target switch re-seeds from
|
|
15
|
+
* the new fetch; the seed is gated on a per-target stamp so a late fetch for a
|
|
16
|
+
* previous target can't clobber the current one.
|
|
17
|
+
*
|
|
18
|
+
* Effective (inherited) permissions and display names come from that single
|
|
19
|
+
* fetch: inherited access doesn't change as a result of editing a direct grant,
|
|
20
|
+
* so the badge stays correct for the session without re-reading.
|
|
15
21
|
*
|
|
16
22
|
* @internal
|
|
17
23
|
*/
|
|
@@ -19,37 +25,24 @@ export function useAccessList(target, onSaved) {
|
|
|
19
25
|
const backend = useBackendStrict();
|
|
20
26
|
const workspace = useWorkspaceStrict();
|
|
21
27
|
const toast = useToastMessage();
|
|
22
|
-
|
|
23
|
-
const [
|
|
24
|
-
const [
|
|
28
|
+
// Authoritative local state, seeded from the fetch below.
|
|
29
|
+
const [grantees, setGrantees] = useState([]);
|
|
30
|
+
const [generalAccess, setGeneralAccess] = useState("RESTRICTED");
|
|
31
|
+
const [workspaceLevel, setWorkspaceLevel] = useState("VIEW");
|
|
32
|
+
// The target the local state was seeded for; undefined until the first seed.
|
|
33
|
+
// Reading state as belonging to the current target hinges on this matching
|
|
34
|
+
// `targetKey` — a target switch makes the old state stale until the re-seed.
|
|
35
|
+
const [seededTarget, setSeededTarget] = useState(undefined);
|
|
25
36
|
const [loadError, setLoadError] = useState(undefined);
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// flight. The backend has read-after-write lag, so we show the intended state
|
|
29
|
-
// and clear an entry only once a refresh confirms it (or the write fails).
|
|
30
|
-
const [overlay, setOverlay] = useState({});
|
|
31
|
-
// Display names learned from the picker; keep a human name on a row after the
|
|
32
|
-
// optimistic overlay reconciles away (the grant often returns only a raw id).
|
|
37
|
+
// Display names learned from the picker; used to give a fetched row a human
|
|
38
|
+
// name when its grant carried only a raw id.
|
|
33
39
|
const [knownNames, setKnownNames] = useState({});
|
|
34
40
|
// Original ObjRef per grantee id, learned from the picker (the access-list id
|
|
35
41
|
// is a serialized `kind:identifier`, which loses Uri-vs-Id). Reused for writes.
|
|
36
42
|
const [knownRefs, setKnownRefs] = useState({});
|
|
37
|
-
// Optimistic general-access override — held from confirm until the fetched list
|
|
38
|
-
// reflects it, so the radio + summary update instantly.
|
|
39
|
-
const [optimisticGeneralAccess, setOptimisticGeneralAccess] = useState(undefined);
|
|
40
43
|
const targetKey = target ? objRefToString(target.ref) : undefined;
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// means everything derived ignores it immediately — no reset effect.
|
|
44
|
-
const currentAccessList = accessListTarget === targetKey ? accessList : undefined;
|
|
45
|
-
// Always-current target key, read inside async callbacks that captured an older
|
|
46
|
-
// one. A background refresh started for the previous target can resolve after a
|
|
47
|
-
// switch; without this it would stamp the list back to the stale target, leaving
|
|
48
|
-
// the new target's `currentAccessList` undefined (and, with the load-gated
|
|
49
|
-
// status, stuck "loading" since its own fetch already settled).
|
|
50
|
-
const targetKeyRef = useRef(targetKey);
|
|
51
|
-
targetKeyRef.current = targetKey;
|
|
52
|
-
// Initial (and target-change) load. Feeds setAccessList once; thereafter we own it.
|
|
44
|
+
// Initial (and target-change) load. Seeds local state once per target; thereafter
|
|
45
|
+
// local state is authoritative and mutations write through it.
|
|
53
46
|
const { result: fetchedList, status: fetchStatus, error: fetchError, } = useCancelablePromise({
|
|
54
47
|
promise: target
|
|
55
48
|
? () => backend.workspace(workspace).objectPermissions().getAccessList(target)
|
|
@@ -58,118 +51,77 @@ export function useAccessList(target, onSaved) {
|
|
|
58
51
|
// Key on the serialized ref, not the ObjRef object — an inline idRef(...) is a
|
|
59
52
|
// new instance each render and would otherwise refetch forever.
|
|
60
53
|
[backend, workspace, target?.kind, targetKey]);
|
|
54
|
+
// Local state belongs to the current target only once its list has been seeded
|
|
55
|
+
// AND no fetch is in flight for it. Requiring `fetchStatus === "success"` (not
|
|
56
|
+
// just a matching `seededTarget`) matters when the same object is reopened after
|
|
57
|
+
// navigating away: the deps change re-runs `useCancelablePromise`, which resets to
|
|
58
|
+
// `loading` while it refetches. Without this, the still-matching previous
|
|
59
|
+
// `seededTarget` would keep `hasList` true and surface the prior session's
|
|
60
|
+
// grantees as `success` (mutations enabled) until the new list lands.
|
|
61
|
+
const hasList = seededTarget === targetKey && targetKey !== undefined && fetchStatus === "success";
|
|
61
62
|
useEffect(() => {
|
|
62
63
|
if (fetchStatus === "success" && fetchedList) {
|
|
63
64
|
// The cancelable promise is keyed on targetKey, so this list is for it.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
setGrantees(granteesFromAccessList(fetchedList));
|
|
66
|
+
setGeneralAccess(deriveGeneralAccess(fetchedList.grants));
|
|
67
|
+
setWorkspaceLevel(deriveWorkspacePermissionLevel(fetchedList.grants));
|
|
68
|
+
setSeededTarget(targetKey);
|
|
67
69
|
setLoadError(undefined);
|
|
68
|
-
setOverlay({}); // a target-change load is authoritative; drop stale overlay
|
|
69
|
-
setOptimisticGeneralAccess(undefined);
|
|
70
70
|
}
|
|
71
71
|
else if (fetchStatus === "error") {
|
|
72
|
-
setLoadStatus("error");
|
|
73
72
|
setLoadError(fetchError instanceof Error ? fetchError : new Error(String(fetchError)));
|
|
74
73
|
}
|
|
75
74
|
else if (fetchStatus === "loading") {
|
|
76
|
-
|
|
75
|
+
// A (re)fetch is in flight — including when the same object is reopened
|
|
76
|
+
// after navigating away. Drop the seed stamp so the previous session's
|
|
77
|
+
// list isn't surfaced as the current one until the fresh fetch is seeded.
|
|
78
|
+
setSeededTarget(undefined);
|
|
77
79
|
}
|
|
78
80
|
}, [fetchStatus, fetchedList, fetchError, targetKey]);
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const fresh = await backend.workspace(workspace).objectPermissions().getAccessList(target);
|
|
87
|
-
// The target may have changed while this refresh was in flight. Stamping
|
|
88
|
-
// a stale target's list now would clobber the new target's stamp and
|
|
89
|
-
// strand it loading, so drop the result if we've since switched away.
|
|
90
|
-
if (targetKeyRef.current !== targetKey) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
setAccessList(fresh);
|
|
94
|
-
setAccessListTarget(targetKey);
|
|
95
|
-
setOverlay((prev) => reconcileOverlay(granteesFromAccessList(fresh), prev));
|
|
96
|
-
// Drop the optimistic general-access override once the server confirms it.
|
|
97
|
-
const freshGeneralAccess = deriveGeneralAccess(fresh.grants);
|
|
98
|
-
setOptimisticGeneralAccess((prev) => (prev === freshGeneralAccess ? undefined : prev));
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// The refresh failed, but the mutation that triggered it already
|
|
102
|
-
// succeeded. The overlay can't be reconciled against fresh data now, so
|
|
103
|
-
// settle it optimistically: keep each entry's intended state (a `set`
|
|
104
|
-
// value stays, a `remove` keeps hiding the row) but clear its `pending`
|
|
105
|
-
// flag. Otherwise rows would spin on "saving"/"removing" forever even
|
|
106
|
-
// though the change persisted and a success toast was shown.
|
|
107
|
-
setOverlay((prev) => {
|
|
108
|
-
let changed = false;
|
|
109
|
-
const next = {};
|
|
110
|
-
for (const [id, entry] of Object.entries(prev)) {
|
|
111
|
-
if (entry.pending) {
|
|
112
|
-
changed = true;
|
|
113
|
-
next[id] = { ...entry, pending: false };
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
next[id] = entry;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return changed ? next : prev;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}, [backend, workspace, target, targetKey]);
|
|
123
|
-
// Committed rows overlaid with optimistic intent; backfill a human name from
|
|
124
|
-
// the picker cache where the grant only carried a raw id.
|
|
125
|
-
const grantees = useMemo(() => {
|
|
126
|
-
if (!currentAccessList) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
const committed = granteesFromAccessList(currentAccessList).map((g) => {
|
|
81
|
+
// The current target's rows, with a human name backfilled from the picker
|
|
82
|
+
// cache where the grant returned only a raw id. Empty until the current
|
|
83
|
+
// target's list is seeded, so a stale previous target's rows never show
|
|
84
|
+
// through the switch window. Derivation, not state — re-applies as the cache grows.
|
|
85
|
+
const namedGrantees = useMemo(() => hasList
|
|
86
|
+
? grantees.map((g) => {
|
|
130
87
|
const known = knownNames[g.id];
|
|
131
88
|
return known && g.name === objRefToString(g.granteeRef) ? { ...g, name: known } : g;
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
89
|
+
})
|
|
90
|
+
: [], [hasList, grantees, knownNames]);
|
|
91
|
+
// Stable sorted key of the currently-granted ids — drives the picker's
|
|
92
|
+
// "exclude already-granted" filter. Keyed on ids only (not names), so the
|
|
93
|
+
// picker's own name-cache writes can't change loadOptions' identity and
|
|
94
|
+
// re-trigger its fetch.
|
|
95
|
+
const excludedIdsKey = useMemo(() => grantees
|
|
96
|
+
.filter((g) => g.pending !== "removing")
|
|
136
97
|
.map((g) => g.id)
|
|
137
98
|
.sort()
|
|
138
|
-
.join(",");
|
|
139
|
-
const committedGeneralAccess = useMemo(() => (currentAccessList ? deriveGeneralAccess(currentAccessList.grants) : "RESTRICTED"), [currentAccessList]);
|
|
140
|
-
const generalAccess = optimisticGeneralAccess ?? committedGeneralAccess;
|
|
99
|
+
.join(","), [grantees]);
|
|
141
100
|
const summary = useMemo(() => {
|
|
142
|
-
if (!
|
|
101
|
+
if (!hasList) {
|
|
143
102
|
return undefined;
|
|
144
103
|
}
|
|
145
104
|
return {
|
|
146
105
|
generalAccess,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// (which can still read SHARE through read-after-write lag).
|
|
150
|
-
workspaceLevel: optimisticGeneralAccess
|
|
151
|
-
? "VIEW"
|
|
152
|
-
: deriveWorkspacePermissionLevel(currentAccessList.grants),
|
|
153
|
-
granteeCount: grantees.length,
|
|
106
|
+
workspaceLevel: generalAccess === "WORKSPACE" ? workspaceLevel : "VIEW",
|
|
107
|
+
granteeCount: namedGrantees.filter((g) => g.pending !== "removing").length,
|
|
154
108
|
};
|
|
155
|
-
}, [
|
|
156
|
-
// "success" only once the *current* target's list has
|
|
157
|
-
// target switch the
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// row hide both summary and skeleton. Treat "no list for this target yet" as
|
|
162
|
-
// loading.
|
|
109
|
+
}, [hasList, generalAccess, workspaceLevel, namedGrantees]);
|
|
110
|
+
// "success" only once the *current* target's list has been seeded. After a
|
|
111
|
+
// target switch the previous seed is stale (seededTarget !== targetKey), so
|
|
112
|
+
// gating on hasList keeps the status at "loading" until the new list lands —
|
|
113
|
+
// never "success" with no list, which would let the dialog enable mutations
|
|
114
|
+
// and the catalog row hide both summary and skeleton.
|
|
163
115
|
const status = target
|
|
164
|
-
?
|
|
116
|
+
? fetchStatus === "error"
|
|
165
117
|
? "error"
|
|
166
|
-
:
|
|
167
|
-
? "
|
|
168
|
-
: "
|
|
118
|
+
: hasList
|
|
119
|
+
? "success"
|
|
120
|
+
: "loading"
|
|
169
121
|
: "idle";
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
122
|
+
// Write a single grant change to the backend, then toast. The caller applies
|
|
123
|
+
// the optimistic local write-through and rolls it back on failure; there is no
|
|
124
|
+
// refetch — local state stays authoritative.
|
|
173
125
|
const commit = useCallback(async (mutate, successMessage) => {
|
|
174
126
|
if (!target) {
|
|
175
127
|
return false;
|
|
@@ -181,18 +133,17 @@ export function useAccessList(target, onSaved) {
|
|
|
181
133
|
.manageObjectPermissions(target, mutate);
|
|
182
134
|
toast.addSuccess(successMessage);
|
|
183
135
|
onSaved?.();
|
|
184
|
-
await refresh();
|
|
185
136
|
return true;
|
|
186
137
|
}
|
|
187
138
|
catch {
|
|
188
139
|
toast.addError(objectShareMessages.toastError);
|
|
189
140
|
return false;
|
|
190
141
|
}
|
|
191
|
-
}, [backend, workspace, target, toast, onSaved
|
|
142
|
+
}, [backend, workspace, target, toast, onSaved]);
|
|
192
143
|
// Picker loader — fetches available assignees on demand, filters by the typed
|
|
193
|
-
// query, excludes anything already granted. Depends only on the
|
|
194
|
-
// (
|
|
195
|
-
//
|
|
144
|
+
// query, excludes anything already granted. Depends only on the granted set
|
|
145
|
+
// (grantees), so its identity changes only when that set actually changes. It
|
|
146
|
+
// must NOT write state that feeds its own deps.
|
|
196
147
|
const loadOptions = useCallback(async (search) => {
|
|
197
148
|
if (!target) {
|
|
198
149
|
return { groups: [], users: [] };
|
|
@@ -223,7 +174,7 @@ export function useAccessList(target, onSaved) {
|
|
|
223
174
|
}
|
|
224
175
|
return next;
|
|
225
176
|
});
|
|
226
|
-
const excluded = new Set(
|
|
177
|
+
const excluded = new Set(excludedIdsKey ? excludedIdsKey.split(",") : []);
|
|
227
178
|
const selectable = withIds
|
|
228
179
|
.filter(({ id }) => !excluded.has(id)) // hide anyone already granted
|
|
229
180
|
.filter(({ assignee }) => assigneeMatchesQuery(assignee, query));
|
|
@@ -240,24 +191,25 @@ export function useAccessList(target, onSaved) {
|
|
|
240
191
|
.filter(({ assignee }) => assignee.type !== "user")
|
|
241
192
|
.map(({ assignee, id }) => ({ id, kind: "group", name: assignee.name })),
|
|
242
193
|
};
|
|
243
|
-
}, [backend, workspace, target,
|
|
194
|
+
}, [backend, workspace, target, excludedIdsKey]);
|
|
244
195
|
// Reuse the picker's original ref (preserves UriRef vs IdentifierRef);
|
|
245
196
|
// fall back to the serialized id only if it wasn't cached.
|
|
246
197
|
const refForId = useCallback((id) => knownRefs[id] ?? { identifier: id.split(":", 2)[1] }, [knownRefs]);
|
|
247
198
|
return {
|
|
248
199
|
targetKey,
|
|
249
|
-
|
|
250
|
-
grantees,
|
|
251
|
-
granteeIdsKey,
|
|
200
|
+
hasList,
|
|
201
|
+
grantees: namedGrantees,
|
|
252
202
|
generalAccess,
|
|
203
|
+
workspaceLevel,
|
|
253
204
|
summary,
|
|
254
205
|
status,
|
|
255
206
|
loadError,
|
|
256
207
|
commit,
|
|
257
208
|
loadOptions,
|
|
258
209
|
refForId,
|
|
259
|
-
|
|
210
|
+
setGrantees,
|
|
211
|
+
setGeneralAccess,
|
|
212
|
+
setWorkspaceLevel,
|
|
260
213
|
setKnownNames,
|
|
261
|
-
setOptimisticGeneralAccess,
|
|
262
214
|
};
|
|
263
215
|
}
|