@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.
Files changed (99) hide show
  1. package/NOTICE +641 -841
  2. package/esm/index.d.ts +2 -2
  3. package/esm/index.d.ts.map +1 -1
  4. package/esm/index.js +1 -1
  5. package/esm/internal/translations/de-DE.localization-bundle.d.ts +42 -0
  6. package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
  7. package/esm/internal/translations/de-DE.localization-bundle.js +43 -1
  8. package/esm/internal/translations/en-AU.localization-bundle.d.ts +42 -0
  9. package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
  10. package/esm/internal/translations/en-AU.localization-bundle.js +44 -2
  11. package/esm/internal/translations/en-GB.localization-bundle.d.ts +42 -0
  12. package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
  13. package/esm/internal/translations/en-GB.localization-bundle.js +44 -2
  14. package/esm/internal/translations/es-419.localization-bundle.d.ts +42 -0
  15. package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
  16. package/esm/internal/translations/es-419.localization-bundle.js +44 -2
  17. package/esm/internal/translations/es-ES.localization-bundle.d.ts +42 -0
  18. package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
  19. package/esm/internal/translations/es-ES.localization-bundle.js +44 -2
  20. package/esm/internal/translations/fi-FI.localization-bundle.d.ts +42 -0
  21. package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
  22. package/esm/internal/translations/fi-FI.localization-bundle.js +43 -1
  23. package/esm/internal/translations/fr-CA.localization-bundle.d.ts +42 -0
  24. package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
  25. package/esm/internal/translations/fr-CA.localization-bundle.js +43 -1
  26. package/esm/internal/translations/fr-FR.localization-bundle.d.ts +42 -0
  27. package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
  28. package/esm/internal/translations/fr-FR.localization-bundle.js +43 -1
  29. package/esm/internal/translations/id-ID.localization-bundle.d.ts +42 -0
  30. package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
  31. package/esm/internal/translations/id-ID.localization-bundle.js +43 -1
  32. package/esm/internal/translations/it-IT.localization-bundle.d.ts +42 -0
  33. package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
  34. package/esm/internal/translations/it-IT.localization-bundle.js +43 -1
  35. package/esm/internal/translations/ja-JP.localization-bundle.d.ts +42 -0
  36. package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
  37. package/esm/internal/translations/ja-JP.localization-bundle.js +43 -1
  38. package/esm/internal/translations/ko-KR.localization-bundle.d.ts +42 -0
  39. package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
  40. package/esm/internal/translations/ko-KR.localization-bundle.js +43 -1
  41. package/esm/internal/translations/nl-NL.localization-bundle.d.ts +42 -0
  42. package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
  43. package/esm/internal/translations/nl-NL.localization-bundle.js +43 -1
  44. package/esm/internal/translations/pl-PL.localization-bundle.d.ts +42 -0
  45. package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
  46. package/esm/internal/translations/pl-PL.localization-bundle.js +43 -1
  47. package/esm/internal/translations/pt-BR.localization-bundle.d.ts +42 -0
  48. package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
  49. package/esm/internal/translations/pt-BR.localization-bundle.js +43 -1
  50. package/esm/internal/translations/pt-PT.localization-bundle.d.ts +42 -0
  51. package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
  52. package/esm/internal/translations/pt-PT.localization-bundle.js +43 -1
  53. package/esm/internal/translations/ru-RU.localization-bundle.d.ts +42 -0
  54. package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
  55. package/esm/internal/translations/ru-RU.localization-bundle.js +43 -1
  56. package/esm/internal/translations/sl-SI.localization-bundle.d.ts +42 -0
  57. package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
  58. package/esm/internal/translations/sl-SI.localization-bundle.js +43 -1
  59. package/esm/internal/translations/th-TH.localization-bundle.d.ts +42 -0
  60. package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
  61. package/esm/internal/translations/th-TH.localization-bundle.js +43 -1
  62. package/esm/internal/translations/tr-TR.localization-bundle.d.ts +42 -0
  63. package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
  64. package/esm/internal/translations/tr-TR.localization-bundle.js +43 -1
  65. package/esm/internal/translations/uk-UA.localization-bundle.d.ts +42 -0
  66. package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
  67. package/esm/internal/translations/uk-UA.localization-bundle.js +43 -1
  68. package/esm/internal/translations/vi-VN.localization-bundle.d.ts +42 -0
  69. package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
  70. package/esm/internal/translations/vi-VN.localization-bundle.js +43 -1
  71. package/esm/internal/translations/zh-HK.localization-bundle.d.ts +42 -0
  72. package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
  73. package/esm/internal/translations/zh-HK.localization-bundle.js +44 -2
  74. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +42 -0
  75. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
  76. package/esm/internal/translations/zh-Hans.localization-bundle.js +43 -1
  77. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +42 -0
  78. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
  79. package/esm/internal/translations/zh-Hant.localization-bundle.js +45 -3
  80. package/esm/sdk-ui-ext.d.ts +31 -10
  81. package/esm/share/ObjectShareDialog.js +2 -2
  82. package/esm/share/objectShareController.helpers.d.ts +1 -27
  83. package/esm/share/objectShareController.helpers.d.ts.map +1 -1
  84. package/esm/share/objectShareController.helpers.js +25 -48
  85. package/esm/share/objectShareController.types.d.ts +46 -4
  86. package/esm/share/objectShareController.types.d.ts.map +1 -1
  87. package/esm/share/useAccessList.d.ts +31 -18
  88. package/esm/share/useAccessList.d.ts.map +1 -1
  89. package/esm/share/useAccessList.js +85 -133
  90. package/esm/share/useLabelScope.d.ts +6 -8
  91. package/esm/share/useLabelScope.d.ts.map +1 -1
  92. package/esm/share/useLabelScope.js +34 -28
  93. package/esm/share/useObjectShareController.d.ts +19 -16
  94. package/esm/share/useObjectShareController.d.ts.map +1 -1
  95. package/esm/share/useObjectShareController.js +212 -119
  96. package/package.json +21 -22
  97. package/esm/share/useObjectShare.d.ts +0 -50
  98. package/esm/share/useObjectShare.d.ts.map +0 -1
  99. 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
- /** The row's directly-granted level. SHARE outranks VIEW. */
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("SHARE") ? "SHARE" : "VIEW";
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
- return direct !== "SHARE" && inheritedPermissions.includes("SHARE") ? "SHARE" : undefined;
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
- level,
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
- level,
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
- * Merge the committed rows with the optimistic overlay: `set` replaces/inserts a
62
- * row (annotated pending), `remove` drops it. Entries with no committed effect
63
- * still apply so the UI reflects the user's intent during read-after-write lag.
64
- */
65
- export function mergeOverlay(committed, overlay) {
66
- const byId = new Map(committed.map((g) => [g.id, g]));
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 = level === "none" ? [] : level === "SHARE" ? ["SHARE", "VIEW"] : ["VIEW"];
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. `EDIT` is intentionally
6
- * not represented the underlying permission menu caps at VIEW/SHARE.
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 = "VIEW" | "SHARE";
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;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG,MAAM,GAAG,OAAO,CAAC;AAE1D;;;;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,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;IAElC;;;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;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,2BAA2B,CAAC;IACnC,OAAO,EAAE,6BAA6B,CAAC;CAC1C"}
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 IObjectAccessList, type ObjRef } from "@gooddata/sdk-model";
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 + optimistic overlay for {@link useObjectShareController}.
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
- /** The owned list as it applies to the *current* target (undefined while a switch is pending). */
16
- currentAccessList: IObjectAccessList | undefined;
17
- /** Committed grantee rows overlaid with optimistic intent (add/level/remove). */
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
- /** Sorted, comma-joined committed grantee idsa stable key for resolution effects. */
20
- granteeIdsKey: string;
21
- /** Optimistic-aware general access (workspace vs restricted). */
18
+ /** Workspace vs restricted general accesslocal state; mutations write through. */
22
19
  generalAccess: GeneralAccessValue;
23
- /** Inline access summary (optimistic-aware), or undefined before the first load. */
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
- /** Commit a grant change, toast on success, then background-refresh. False on failure. */
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
- setOverlay: React.Dispatch<React.SetStateAction<Record<string, IGranteeOverlayEntry>>>;
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 and its optimistic overlay. The list is fetched
43
- * via `useCancelablePromise` then owned locally: mutations patch the overlay and
44
- * a background refresh reconciles it, so the grantee list never blanks between a
45
- * change and the server response. A target switch is handled by stamping the
46
- * fetched list with its target and reading through `currentAccessList`.
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,EACH,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,MAAM,EAEd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAmB,MAAM,sBAAsB,CAAC;AAI7G,OAAO,EACH,KAAK,oBAAoB,EAM5B,MAAM,oCAAoC,CAAC;AAC5C,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,kGAAkG;IAClG,iBAAiB,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACjD,iFAAiF;IACjF,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,0FAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,aAAa,EAAE,kBAAkB,CAAC;IAClC,oFAAoF;IACpF,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,0FAA0F;IAC1F,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,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;IACvF,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC5E,0BAA0B,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC,CAAC;CACpG;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CACzB,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,GAClC,WAAW,CAsRb"}
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, useRef, useState } from "react";
3
- import { objRefToString, } from "@gooddata/sdk-model";
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, mergeOverlay, reconcileOverlay, } from "./objectShareController.helpers.js";
8
+ import { assigneeMatchesQuery, granteeId, granteesFromAccessList } from "./objectShareController.helpers.js";
9
9
  /**
10
- * Owns the backend access list and its optimistic overlay. The list is fetched
11
- * via `useCancelablePromise` then owned locally: mutations patch the overlay and
12
- * a background refresh reconciles it, so the grantee list never blanks between a
13
- * change and the server response. A target switch is handled by stamping the
14
- * fetched list with its target and reading through `currentAccessList`.
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
- const [accessList, setAccessList] = useState(undefined);
23
- const [accessListTarget, setAccessListTarget] = useState(undefined);
24
- const [loadStatus, setLoadStatus] = useState(target ? "loading" : "idle");
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
- // Optimistic overlay keyed by grantee id. Each entry holds the *intended* row
27
- // (add/level-change) or a removal, plus a pending flag while the write is in
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
- // The owned list as it applies to the *current* target. A target switch makes
42
- // the previous fetch's list stale until the new one lands; gating on the stamp
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
- setAccessList(fetchedList);
65
- setAccessListTarget(targetKey);
66
- setLoadStatus("idle");
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
- setLoadStatus("loading");
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
- // Background refresh pulls server truth without nulling the current list,
80
- // then reconciles the overlay (clears entries the server now confirms).
81
- const refresh = useCallback(async () => {
82
- if (!target) {
83
- return;
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
- return mergeOverlay(committed, overlay);
134
- }, [currentAccessList, overlay, knownNames]);
135
- const granteeIdsKey = granteesFromAccessList(currentAccessList)
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 (!currentAccessList) {
101
+ if (!hasList) {
143
102
  return undefined;
144
103
  }
145
104
  return {
146
105
  generalAccess,
147
- // While the optimistic override is active we just wrote the workspace
148
- // rule as VIEW, so report VIEW rather than the stale fetched grant
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
- }, [currentAccessList, generalAccess, optimisticGeneralAccess, grantees]);
156
- // "success" only once the *current* target's list has actually landed. After a
157
- // target switch the stamp clears `currentAccessList` immediately, but
158
- // `loadStatus` still reads "idle" from the previous target's completed load
159
- // until the effect re-runs so gating on loadStatus alone would briefly report
160
- // "success" with no list, letting the dialog enable mutations and the catalog
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
- ? loadStatus === "error"
116
+ ? fetchStatus === "error"
165
117
  ? "error"
166
- : loadStatus === "loading" || !currentAccessList
167
- ? "loading"
168
- : "success"
118
+ : hasList
119
+ ? "success"
120
+ : "loading"
169
121
  : "idle";
170
- // Commit a single grant change against the backend, then reconcile via refresh.
171
- // Optimistic state is applied by the caller; on failure the caller's rollback
172
- // runs and a toast is shown.
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, refresh]);
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 grant sources
194
- // (accessList, overlay), so its identity changes only when the granted set
195
- // actually changes. It must NOT write state that feeds its own deps.
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(mergeOverlay(granteesFromAccessList(currentAccessList), overlay).map((g) => g.id));
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, currentAccessList, overlay]);
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
- currentAccessList,
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
- setOverlay,
210
+ setGrantees,
211
+ setGeneralAccess,
212
+ setWorkspaceLevel,
260
213
  setKnownNames,
261
- setOptimisticGeneralAccess,
262
214
  };
263
215
  }