@gooddata/sdk-ui-ext 11.49.0-alpha.1 → 11.50.0-alpha.0

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 (94) hide show
  1. package/esm/index.d.ts +1 -1
  2. package/esm/index.d.ts.map +1 -1
  3. package/esm/internal/translations/de-DE.localization-bundle.d.ts +1 -0
  4. package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
  5. package/esm/internal/translations/de-DE.localization-bundle.js +1 -0
  6. package/esm/internal/translations/en-AU.localization-bundle.d.ts +1 -0
  7. package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
  8. package/esm/internal/translations/en-AU.localization-bundle.js +1 -0
  9. package/esm/internal/translations/en-GB.localization-bundle.d.ts +1 -0
  10. package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
  11. package/esm/internal/translations/en-GB.localization-bundle.js +1 -0
  12. package/esm/internal/translations/en-US.localization-bundle.d.ts +5 -9
  13. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  14. package/esm/internal/translations/en-US.localization-bundle.js +15 -19
  15. package/esm/internal/translations/es-419.localization-bundle.d.ts +1 -0
  16. package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
  17. package/esm/internal/translations/es-419.localization-bundle.js +1 -0
  18. package/esm/internal/translations/es-ES.localization-bundle.d.ts +1 -0
  19. package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
  20. package/esm/internal/translations/es-ES.localization-bundle.js +1 -0
  21. package/esm/internal/translations/fi-FI.localization-bundle.d.ts +1 -0
  22. package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
  23. package/esm/internal/translations/fi-FI.localization-bundle.js +1 -0
  24. package/esm/internal/translations/fr-CA.localization-bundle.d.ts +1 -0
  25. package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
  26. package/esm/internal/translations/fr-CA.localization-bundle.js +1 -0
  27. package/esm/internal/translations/fr-FR.localization-bundle.d.ts +1 -0
  28. package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
  29. package/esm/internal/translations/fr-FR.localization-bundle.js +1 -0
  30. package/esm/internal/translations/id-ID.localization-bundle.d.ts +1 -0
  31. package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
  32. package/esm/internal/translations/id-ID.localization-bundle.js +1 -0
  33. package/esm/internal/translations/it-IT.localization-bundle.d.ts +1 -0
  34. package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
  35. package/esm/internal/translations/it-IT.localization-bundle.js +1 -0
  36. package/esm/internal/translations/ja-JP.localization-bundle.d.ts +1 -0
  37. package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
  38. package/esm/internal/translations/ja-JP.localization-bundle.js +1 -0
  39. package/esm/internal/translations/ko-KR.localization-bundle.d.ts +1 -0
  40. package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
  41. package/esm/internal/translations/ko-KR.localization-bundle.js +1 -0
  42. package/esm/internal/translations/nl-NL.localization-bundle.d.ts +1 -0
  43. package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
  44. package/esm/internal/translations/nl-NL.localization-bundle.js +1 -0
  45. package/esm/internal/translations/pl-PL.localization-bundle.d.ts +1 -0
  46. package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
  47. package/esm/internal/translations/pl-PL.localization-bundle.js +1 -0
  48. package/esm/internal/translations/pt-BR.localization-bundle.d.ts +1 -0
  49. package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
  50. package/esm/internal/translations/pt-BR.localization-bundle.js +1 -0
  51. package/esm/internal/translations/pt-PT.localization-bundle.d.ts +1 -0
  52. package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
  53. package/esm/internal/translations/pt-PT.localization-bundle.js +1 -0
  54. package/esm/internal/translations/ru-RU.localization-bundle.d.ts +1 -0
  55. package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
  56. package/esm/internal/translations/ru-RU.localization-bundle.js +1 -0
  57. package/esm/internal/translations/sl-SI.localization-bundle.d.ts +1 -0
  58. package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
  59. package/esm/internal/translations/sl-SI.localization-bundle.js +1 -0
  60. package/esm/internal/translations/th-TH.localization-bundle.d.ts +1 -0
  61. package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
  62. package/esm/internal/translations/th-TH.localization-bundle.js +1 -0
  63. package/esm/internal/translations/tr-TR.localization-bundle.d.ts +1 -0
  64. package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
  65. package/esm/internal/translations/tr-TR.localization-bundle.js +2 -1
  66. package/esm/internal/translations/uk-UA.localization-bundle.d.ts +1 -0
  67. package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
  68. package/esm/internal/translations/uk-UA.localization-bundle.js +1 -0
  69. package/esm/internal/translations/vi-VN.localization-bundle.d.ts +1 -0
  70. package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
  71. package/esm/internal/translations/vi-VN.localization-bundle.js +1 -0
  72. package/esm/internal/translations/zh-HK.localization-bundle.d.ts +1 -0
  73. package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
  74. package/esm/internal/translations/zh-HK.localization-bundle.js +1 -0
  75. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +1 -0
  76. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
  77. package/esm/internal/translations/zh-Hans.localization-bundle.js +1 -0
  78. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +1 -0
  79. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
  80. package/esm/internal/translations/zh-Hant.localization-bundle.js +1 -0
  81. package/esm/sdk-ui-ext.d.ts +45 -53
  82. package/esm/share/ObjectShareDialog.d.ts.map +1 -1
  83. package/esm/share/ObjectShareDialog.js +118 -58
  84. package/esm/share/messages.d.ts +10 -8
  85. package/esm/share/messages.d.ts.map +1 -1
  86. package/esm/share/messages.js +10 -6
  87. package/esm/share/objectShareController.types.d.ts +45 -53
  88. package/esm/share/objectShareController.types.d.ts.map +1 -1
  89. package/esm/share/useAccessList.d.ts +20 -24
  90. package/esm/share/useAccessList.d.ts.map +1 -1
  91. package/esm/share/useAccessList.js +39 -30
  92. package/esm/share/useObjectShareController.d.ts.map +1 -1
  93. package/esm/share/useObjectShareController.js +10 -201
  94. package/package.json +21 -21
@@ -1,6 +1,6 @@
1
1
  // (C) 2026 GoodData Corporation
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { idRef, objRefToString, } from "@gooddata/sdk-model";
3
+ import { idRef, objRefToString, serializeObjRef, } 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 { isPermissionsNotAvailable } from "./accessErrors.js";
@@ -28,6 +28,9 @@ export function useAccessList(target, onSaved, dialogOpen) {
28
28
  const toast = useToastMessage();
29
29
  // Authoritative local state, seeded from the fetch below.
30
30
  const [grantees, setGrantees] = useState([]);
31
+ // Load-time fact, deliberately NOT derived from the live list: local mutations
32
+ // must not change it (see the IAccessList doc).
33
+ const [seededWithoutGrants, setSeededWithoutGrants] = useState(false);
31
34
  const [generalAccess, setGeneralAccess] = useState("RESTRICTED");
32
35
  const [workspaceLevel, setWorkspaceLevel] = useState("VIEW");
33
36
  const [inheritedWorkspaceLevel, setInheritedWorkspaceLevel] = useState(undefined);
@@ -44,7 +47,11 @@ export function useAccessList(target, onSaved, dialogOpen) {
44
47
  // Memoized current-user fetch — the profile doesn't change while mounted,
45
48
  // so resolve it at most once and share the promise across callers.
46
49
  const currentUserCache = useRef(undefined);
47
- const targetKey = target ? objRefToString(target.ref) : undefined;
50
+ // Kind-qualified: distinct permission targets can share an identifier (e.g. an
51
+ // attribute and a metric), and every target-scoped guard — the seed stamp, the
52
+ // async-finalizer bail-outs, the dialog's staged-confirm reset — compares this
53
+ // key. serializeObjRef keeps the ref's own `type` qualifier too.
54
+ const targetKey = target ? `${target.kind}:${serializeObjRef(target.ref)}` : undefined;
48
55
  // Written from both the picker (loadOptions) and the on-open resolve. Must not
49
56
  // depend on the cache it writes, or it would re-trigger loadOptions' fetch.
50
57
  const cacheAssignees = useCallback((assignees) => {
@@ -80,7 +87,9 @@ export function useAccessList(target, onSaved, dialogOpen) {
80
87
  useEffect(() => {
81
88
  if (fetchStatus === "success" && fetchedList) {
82
89
  // The cancelable promise is keyed on targetKey, so this list is for it.
83
- setGrantees(granteesFromAccessList(fetchedList));
90
+ const seededGrantees = granteesFromAccessList(fetchedList);
91
+ setGrantees(seededGrantees);
92
+ setSeededWithoutGrants(seededGrantees.length === 0);
84
93
  setGeneralAccess(deriveGeneralAccess(fetchedList.grants));
85
94
  setWorkspaceLevel(deriveWorkspacePermissionLevel(fetchedList.grants));
86
95
  setInheritedWorkspaceLevel(deriveInheritedWorkspaceLevel(fetchedList.grants));
@@ -104,7 +113,7 @@ export function useAccessList(target, onSaved, dialogOpen) {
104
113
  .getUser()
105
114
  .catch((error) => {
106
115
  // Don't cache a rejected promise, or a transient profile-read
107
- // failure would make every later transfer fail immediately.
116
+ // failure would stick for the rest of the session.
108
117
  currentUserCache.current = undefined;
109
118
  throw error;
110
119
  });
@@ -112,16 +121,27 @@ export function useAccessList(target, onSaved, dialogOpen) {
112
121
  return currentUserCache.current;
113
122
  }, [backend]);
114
123
  // Resolve the current user only while the dialog is open — keeps the summary-only
115
- // path free of a profile request (the transfer gate and self-row facts it feeds
116
- // aren't shown there).
117
- const { result: currentUser } = useCancelablePromise({
124
+ // path free of a profile request (the self-row facts it feeds aren't shown there).
125
+ const { result: currentUser, status: currentUserStatus } = useCancelablePromise({
118
126
  promise: dialogOpen && targetKey ? () => getCurrentUser() : undefined,
119
127
  onError: () => { },
120
128
  },
121
129
  // Key on target presence, not identity — the resolved user is
122
130
  // target-independent, so a target switch must not reset it.
123
131
  [getCurrentUser, targetKey !== undefined, dialogOpen]);
132
+ // Success only: while pending OR after a (silently swallowed) failure the
133
+ // rows' isSelf is just its unresolved default, and consumers must not treat
134
+ // an unidentified row as safe to mutate. Retried naturally on dialog reopen
135
+ // (the promise is keyed on `dialogOpen`; the failure isn't cached).
136
+ const selfIdentityResolved = currentUserStatus === "success";
124
137
  const selfId = currentUser ? granteeId("user", idRef(currentUser.login)) : undefined;
138
+ // De-collapsed like every listing fact — on tiger the user id is often the email.
139
+ const selfIdentity = useMemo(() => currentUser
140
+ ? {
141
+ ...userIdentityFacts(idRef(currentUser.login), currentUser.fullName, currentUser.email),
142
+ id: currentUser.login,
143
+ }
144
+ : undefined, [currentUser]);
125
145
  // Rows with identity facts backfilled from the assignee cache — or, for the
126
146
  // signed-in user's own row (absent from the listing by design), from the
127
147
  // profile. Empty until the current target's list is seeded; a derivation, so
@@ -130,15 +150,14 @@ export function useAccessList(target, onSaved, dialogOpen) {
130
150
  if (!hasList) {
131
151
  return [];
132
152
  }
133
- // De-collapsed like every listing fact — on tiger the user id is often the email.
134
- const selfFacts = currentUser
135
- ? userIdentityFacts(idRef(currentUser.login), currentUser.fullName, currentUser.email)
136
- : undefined;
137
153
  return grantees.map((g) => {
138
- const known = knownAssignees[g.id]?.facts ?? (g.id === selfId ? selfFacts : undefined);
139
- return known ? { ...g, name: g.name ?? known.name, email: g.email ?? known.email } : g;
154
+ const known = knownAssignees[g.id]?.facts ?? (g.id === selfId ? selfIdentity : undefined);
155
+ const isSelf = g.id === selfId;
156
+ return known
157
+ ? { ...g, isSelf, name: g.name ?? known.name, email: g.email ?? known.email }
158
+ : { ...g, isSelf };
140
159
  });
141
- }, [hasList, grantees, knownAssignees, currentUser, selfId]);
160
+ }, [hasList, grantees, knownAssignees, selfIdentity, selfId]);
142
161
  // Resolve identity facts on dialog open — grants often carry only raw ids
143
162
  // (the post-reload state). Unconditional: no missing-facts gate to keep in
144
163
  // sync with the cache, and summary-only consumers never fetch. Keyed on the
@@ -211,14 +230,10 @@ export function useAccessList(target, onSaved, dialogOpen) {
211
230
  }
212
231
  }, [backend, workspace, target, toast, onSaved]);
213
232
  // Picker loader — fetches available assignees on demand, filters by the typed
214
- // query, and by default excludes anything already granted. Depends only on the
233
+ // query, and excludes anything already granted. Depends only on the
215
234
  // granted set (grantees), so its identity changes only when that set actually
216
235
  // changes. It must NOT write state that feeds its own deps.
217
- //
218
- // `includeGranted` keeps already-granted grantees in the result. The
219
- // transfer-ownership picker needs this: promoting an existing viewer to owner
220
- // is a primary case, and the add-grantee exclusion would otherwise hide them.
221
- const loadOptions = useCallback(async (search, includeGranted = false) => {
236
+ const loadOptions = useCallback(async (search) => {
222
237
  if (!target) {
223
238
  return { groups: [], users: [] };
224
239
  }
@@ -235,7 +250,7 @@ export function useAccessList(target, onSaved, dialogOpen) {
235
250
  // even when the access-list grant later returns only a raw id. Safe here:
236
251
  // the caches aren't dependencies of loadOptions, so this won't re-trigger it.
237
252
  cacheAssignees(assignees);
238
- const excluded = new Set(includeGranted || !excludedIdsKey ? [] : excludedIdsKey.split(","));
253
+ const excluded = new Set(excludedIdsKey ? excludedIdsKey.split(",") : []);
239
254
  const selectable = withIds
240
255
  .filter(({ id }) => !excluded.has(id)) // hide anyone already granted
241
256
  .filter(({ assignee }) => assigneeMatchesQuery(assignee, query));
@@ -256,17 +271,13 @@ export function useAccessList(target, onSaved, dialogOpen) {
256
271
  // Reuse the picker's original ref (preserves UriRef vs IdentifierRef);
257
272
  // fall back to the serialized id only if it wasn't cached.
258
273
  const refForId = useCallback((id) => knownAssignees[id]?.ref ?? { identifier: id.split(":", 2)[1] }, [knownAssignees]);
259
- const getCurrentUserRef = useCallback(
260
- // Not `user.ref`: the profile resolves it as a uriRef while access-list
261
- // grantee refs are idRefs keyed by user id (= profile login), and
262
- // areObjRefsEqual never matches mixed shapes — self-row matching and
263
- // the self grant write need the permission API's id space.
264
- () => getCurrentUser().then((user) => idRef(user.login)), [getCurrentUser]);
265
- const canTransferOwnership = selfId !== undefined && namedGrantees.some((g) => g.id === selfId && g.level === "EDIT");
266
274
  return {
267
275
  targetKey,
268
276
  hasList,
269
277
  grantees: namedGrantees,
278
+ seededWithoutGrants,
279
+ selfIdentity,
280
+ selfIdentityResolved,
270
281
  generalAccess,
271
282
  workspaceLevel,
272
283
  inheritedWorkspaceLevel,
@@ -277,8 +288,6 @@ export function useAccessList(target, onSaved, dialogOpen) {
277
288
  commit,
278
289
  loadOptions,
279
290
  refForId,
280
- getCurrentUserRef,
281
- canTransferOwnership,
282
291
  setGrantees,
283
292
  setGeneralAccess,
284
293
  setWorkspaceLevel,
@@ -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;AAmB1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CAo0BxB"}
1
+ {"version":3,"file":"useObjectShareController.d.ts","sourceRoot":"","sources":["../../src/share/useObjectShareController.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAc1E,OAAO,EACH,KAAK,sBAAsB,EAI3B,KAAK,sBAAsB,EAE9B,MAAM,kCAAkC,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,wBAAwB,GAAG,SAAS,EAC5C,OAAO,CAAC,EAAE,sBAAsB,GACjC,sBAAsB,CAmmBxB"}
@@ -1,7 +1,6 @@
1
1
  // (C) 2026 GoodData Corporation
2
2
  import { useCallback, useMemo, useRef, useState } from "react";
3
- import { areObjRefsEqual } from "@gooddata/sdk-model";
4
- import { useToastMessage, } from "@gooddata/sdk-ui-kit";
3
+ import { useToastMessage } from "@gooddata/sdk-ui-kit";
5
4
  import { composeEffectiveWorkspaceAccess } from "./accessSummary.js";
6
5
  import { objectShareMessages } from "./messages.js";
7
6
  import { EMPTY_IDS, NO_LABELS, effectivePermissionAbove, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
@@ -39,10 +38,6 @@ export function useObjectShareController(target, options) {
39
38
  const [subview, setSubview] = useState("main");
40
39
  const [pendingGrantees, setPendingGrantees] = useState([]);
41
40
  const [pendingGeneralAccess, setPendingGeneralAccess] = useState(undefined);
42
- // Transfer-ownership buffers. Cleared on close and on target switch.
43
- const [transferTarget, setTransferTargetState] = useState(undefined);
44
- const [transferAlsoRemoveSelf, setTransferAlsoRemoveSelf] = useState(false);
45
- const [transferSaving, setTransferSaving] = useState(false);
46
41
  // True while a workspace-level re-grade is committing. The workspace rule has no
47
42
  // grantee row to carry a `pending` flag (unlike named grantees), so this gates
48
43
  // re-entry and disables the dropdown to keep rapid VIEW↔SHARE toggles from issuing
@@ -50,7 +45,7 @@ export function useObjectShareController(target, options) {
50
45
  const [workspaceLevelSaving, setWorkspaceLevelSaving] = useState(false);
51
46
  // The backend access list, seeded into local state (rows, general access,
52
47
  // summary, the commit/picker primitives) lives in its own hook.
53
- const { targetKey, hasList, grantees, generalAccess, workspaceLevel, inheritedWorkspaceLevel, summary, status, loadError, accessUnavailable, commit, loadOptions, refForId, getCurrentUserRef, canTransferOwnership, setGrantees, setGeneralAccess, setWorkspaceLevel, } = useAccessList(target, onSaved, isOpen);
48
+ const { targetKey, hasList, grantees, seededWithoutGrants, selfIdentity, selfIdentityResolved, generalAccess, workspaceLevel, inheritedWorkspaceLevel, summary, status, loadError, accessUnavailable, commit, loadOptions, refForId, setGrantees, setGeneralAccess, setWorkspaceLevel, } = useAccessList(target, onSaved, isOpen);
54
49
  // Always-current target key, read inside async mutation finalizers that captured
55
50
  // an older one. A write started for object A resolves after the user navigated to
56
51
  // object B; because local state is authoritative (no refetch), applying A's
@@ -77,21 +72,12 @@ export function useObjectShareController(target, options) {
77
72
  setPendingGrantees([]);
78
73
  setPendingGeneralAccess(undefined);
79
74
  setWorkspaceLevelSaving(false);
80
- setTransferTargetState(undefined);
81
- setTransferAlsoRemoveSelf(false);
82
- setTransferSaving(false);
83
75
  }
84
76
  const reset = useCallback(() => {
85
77
  setSubview("main");
86
78
  setPendingGrantees([]);
87
79
  setPendingGeneralAccess(undefined);
88
80
  setWorkspaceLevelSaving(false);
89
- setTransferTargetState(undefined);
90
- setTransferAlsoRemoveSelf(false);
91
- // Deliberately NOT clearing transferSaving here: closing the dialog must not
92
- // release the transfer lock while the write is still in flight, or a reopened
93
- // dialog could mutate concurrently with the unfinished transfer. The in-flight
94
- // confirmTransferOwnership clears it itself once the write settles.
95
81
  }, []);
96
82
  const openAddGrantee = useCallback(() => {
97
83
  setSubview("addGrantee");
@@ -457,172 +443,6 @@ export function useObjectShareController(target, options) {
457
443
  commit,
458
444
  setWorkspaceLevel,
459
445
  ]);
460
- const openTransferOwnership = useCallback(() => {
461
- setSubview("transferOwnership");
462
- setTransferTargetState(undefined);
463
- setTransferAlsoRemoveSelf(false);
464
- }, []);
465
- const closeTransferOwnership = useCallback(() => {
466
- setSubview("main");
467
- setTransferTargetState(undefined);
468
- setTransferAlsoRemoveSelf(false);
469
- }, []);
470
- const setTransferTarget = useCallback((owner) => {
471
- setTransferTargetState(owner);
472
- }, []);
473
- // The picked user already owns the object when a grantee row for them holds
474
- // EDIT — then there's nothing to transfer (the dialog offers to remove the
475
- // current user's own access instead).
476
- const transferTargetIsOwner = useMemo(() => transferTarget ? grantees.some((g) => g.id === transferTarget.id && g.level === "EDIT") : false, [transferTarget, grantees]);
477
- // Insert a brand-new grantee row with its label scope — the shared seed for a
478
- // freshly-granted principal (used when a transfer promotes a user who wasn't
479
- // already a grantee; the same row shape the add flow creates). Identity
480
- // renders from the facts cache.
481
- const seedNewGranteeRow = useCallback((row, labelIds) => {
482
- setGrantees((prev) => [...prev, row]);
483
- setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [row.id]: labelIds }));
484
- }, [setGrantees, setSelectedLabelIdsByGrantee]);
485
- const confirmTransferOwnership = useCallback(async () => {
486
- if (!transferTarget || transferSaving) {
487
- return;
488
- }
489
- const startedFor = targetKey;
490
- const ownerRef = refForId(transferTarget.id);
491
- let selfRef;
492
- try {
493
- selfRef = await getCurrentUserRef();
494
- }
495
- catch {
496
- toast.addError(objectShareMessages.toastError);
497
- return;
498
- }
499
- if (targetKeyRef.current !== startedFor) {
500
- return;
501
- }
502
- // Guard against transferring to yourself: the picker's loader already
503
- // excludes the current user, but if a backend ever returned self the batch
504
- // below would emit conflicting grants for one ref (EDIT + VIEW/none). Bail
505
- // and just close — there is nothing to transfer to oneself.
506
- if (areObjRefsEqual(ownerRef, selfRef)) {
507
- closeTransferOwnership();
508
- return;
509
- }
510
- // Build the write. A real transfer grants the picked user EDIT and changes
511
- // the current user's own grant: kept as VIEW by default, or removed when
512
- // "Also remove my access" is set. When the picked user already owns the
513
- // object there's nothing to transfer — only the self-access choice applies,
514
- // and "keep" is then a pure no-op (don't downgrade an existing owner-self to
515
- // VIEW just for confirming).
516
- const mutations = [];
517
- if (!transferTargetIsOwner) {
518
- mutations.push(toGranularGrantee("user", ownerRef, "EDIT"));
519
- mutations.push(toGranularGrantee("user", selfRef, transferAlsoRemoveSelf ? "none" : "VIEW"));
520
- }
521
- else if (transferAlsoRemoveSelf) {
522
- mutations.push(toGranularGrantee("user", selfRef, "none"));
523
- }
524
- if (mutations.length === 0) {
525
- closeTransferOwnership();
526
- return;
527
- }
528
- // Whether the new owner already has a grantee row (was granted before this
529
- // transfer). A brand-new owner needs a row seeded and their labels mirrored,
530
- // exactly like the add-grantee flow; an existing grantee already has both.
531
- const existingOwnerRow = transferTargetIsOwner
532
- ? undefined
533
- : grantees.find((g) => g.id === transferTarget.id);
534
- const ownerIsNew = !transferTargetIsOwner && !existingOwnerRow;
535
- const allLabelIds = effectiveLabels.map((l) => l.id);
536
- setTransferSaving(true);
537
- // "Ownership transferred" only when ownership actually moves; the
538
- // already-owner path just changes the current user's own access.
539
- const successMessage = transferTargetIsOwner
540
- ? objectShareMessages.toastAccessUpdated
541
- : objectShareMessages.toastOwnershipTransferred;
542
- const ok = await commit(mutations, successMessage);
543
- if (targetKeyRef.current !== startedFor) {
544
- return;
545
- }
546
- if (!ok) {
547
- setTransferSaving(false);
548
- return;
549
- }
550
- // Mirror per-label grants for the same reason add/remove do: label
551
- // permissions are independent access-list entries, so the object write
552
- // alone would leave a new owner without label access, or leave a removed
553
- // self with orphaned label grants. A failed label write is non-fatal here
554
- // (the object grant is what matters) — surfaced as a warning, like remove.
555
- //
556
- // Do every backend label write first, then apply all local state behind a
557
- // single target guard. Since no local write happens before that guard, a
558
- // target switch during a label await can't land a half-applied transfer on
559
- // the next object's seeded list — unlike the optimistic finalizers, which
560
- // write before committing and so must re-guard after each await.
561
- let labelsOk = true;
562
- if (ownerIsNew) {
563
- // Grant the brand-new owner every label — the add-grantee default scope.
564
- const granted = await reconcileLabelScope({ kind: "user", granteeRef: ownerRef }, new Set(allLabelIds), EMPTY_IDS);
565
- labelsOk = labelsOk && granted;
566
- }
567
- if (transferAlsoRemoveSelf) {
568
- // Self lost object access — revoke their per-label grants too.
569
- const revoked = await reconcileLabelScope({ kind: "user", granteeRef: selfRef }, EMPTY_IDS, new Set(allLabelIds));
570
- labelsOk = labelsOk && revoked;
571
- }
572
- if (targetKeyRef.current !== startedFor) {
573
- return;
574
- }
575
- // Reflect the object-grant changes locally (authoritative, no refetch).
576
- if (!transferTargetIsOwner) {
577
- const nextEffective = effectivePermissionAbove("EDIT", []);
578
- if (ownerIsNew) {
579
- seedNewGranteeRow({
580
- id: transferTarget.id,
581
- kind: "user",
582
- granteeRef: ownerRef,
583
- level: "EDIT",
584
- effectivePermission: nextEffective,
585
- }, allLabelIds);
586
- }
587
- else {
588
- // Existing grantee: their label scope is unchanged by an
589
- // object-level promotion, so only the row's level is raised.
590
- setGrantees((prev) => prev.map((g) => g.id === transferTarget.id
591
- ? { ...g, level: "EDIT", effectivePermission: nextEffective }
592
- : g));
593
- }
594
- }
595
- // The current user may have their own grantee row (the access list keeps
596
- // every grant, including self) — reflect their object-grant change locally
597
- // too: dropped when "remove my access", otherwise downgraded to VIEW. Match
598
- // by ref since self isn't tied to a picker-derived row id.
599
- setGrantees((prev) => transferAlsoRemoveSelf
600
- ? prev.filter((g) => !areObjRefsEqual(g.granteeRef, selfRef))
601
- : prev.map((g) => areObjRefsEqual(g.granteeRef, selfRef)
602
- ? { ...g, level: "VIEW", effectivePermission: undefined }
603
- : g));
604
- if (!labelsOk) {
605
- toast.addWarning(objectShareMessages.toastLabelScopePartial);
606
- }
607
- setTransferSaving(false);
608
- closeTransferOwnership();
609
- }, [
610
- transferTarget,
611
- transferSaving,
612
- transferTargetIsOwner,
613
- transferAlsoRemoveSelf,
614
- targetKey,
615
- grantees,
616
- effectiveLabels,
617
- refForId,
618
- getCurrentUserRef,
619
- commit,
620
- reconcileLabelScope,
621
- toast,
622
- setGrantees,
623
- seedNewGranteeRow,
624
- closeTransferOwnership,
625
- ]);
626
446
  const actions = useMemo(() => ({
627
447
  reset,
628
448
  openAddGrantee,
@@ -637,11 +457,6 @@ export function useObjectShareController(target, options) {
637
457
  cancelGeneralAccessChange: () => setPendingGeneralAccess(undefined),
638
458
  confirmGeneralAccessChange,
639
459
  changeWorkspaceLevel,
640
- openTransferOwnership,
641
- closeTransferOwnership,
642
- setTransferTarget,
643
- setTransferAlsoRemoveSelf,
644
- confirmTransferOwnership,
645
460
  }), [
646
461
  reset,
647
462
  openAddGrantee,
@@ -654,10 +469,6 @@ export function useObjectShareController(target, options) {
654
469
  requestGeneralAccessChange,
655
470
  confirmGeneralAccessChange,
656
471
  changeWorkspaceLevel,
657
- openTransferOwnership,
658
- closeTransferOwnership,
659
- setTransferTarget,
660
- confirmTransferOwnership,
661
472
  ]);
662
473
  const state = useMemo(() => ({
663
474
  subview,
@@ -665,6 +476,10 @@ export function useObjectShareController(target, options) {
665
476
  error: loadError,
666
477
  accessUnavailable,
667
478
  summary,
479
+ targetKey,
480
+ selfIdentity,
481
+ selfIdentityResolved,
482
+ seededWithoutGrants,
668
483
  grantees,
669
484
  // Effective access: the direct state composed with inherited rule
670
485
  // access. Also pins the level to VIEW while restricted, so the (hidden)
@@ -680,17 +495,16 @@ export function useObjectShareController(target, options) {
680
495
  selectedLabelIdsByGrantee,
681
496
  pendingGeneralAccess,
682
497
  pendingGrantees,
683
- transferTarget,
684
- transferAlsoRemoveSelf,
685
- transferTargetIsOwner,
686
- transferSaving,
687
- canTransferOwnership,
688
498
  }), [
689
499
  subview,
690
500
  status,
691
501
  loadError,
692
502
  accessUnavailable,
693
503
  summary,
504
+ targetKey,
505
+ selfIdentity,
506
+ selfIdentityResolved,
507
+ seededWithoutGrants,
694
508
  grantees,
695
509
  generalAccess,
696
510
  workspaceLevel,
@@ -701,11 +515,6 @@ export function useObjectShareController(target, options) {
701
515
  selectedLabelIdsByGrantee,
702
516
  pendingGeneralAccess,
703
517
  pendingGrantees,
704
- transferTarget,
705
- transferAlsoRemoveSelf,
706
- transferTargetIsOwner,
707
- transferSaving,
708
- canTransferOwnership,
709
518
  ]);
710
519
  return useMemo(() => ({ state, actions }), [state, actions]);
711
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-ext",
3
- "version": "11.49.0-alpha.1",
3
+ "version": "11.50.0-alpha.0",
4
4
  "description": "GoodData.UI SDK - Extensions",
5
5
  "license": "LicenseRef-LICENSE",
6
6
  "author": "GoodData Corporation",
@@ -63,20 +63,20 @@
63
63
  "ts-invariant": "0.10.3",
64
64
  "tslib": "2.8.1",
65
65
  "uuid": "11.1.1",
66
- "@gooddata/sdk-backend-base": "11.49.0-alpha.1",
67
- "@gooddata/sdk-backend-spi": "11.49.0-alpha.1",
68
- "@gooddata/sdk-embedding": "11.49.0-alpha.1",
69
- "@gooddata/sdk-model": "11.49.0-alpha.1",
70
- "@gooddata/sdk-ui": "11.49.0-alpha.1",
71
- "@gooddata/sdk-ui-charts": "11.49.0-alpha.1",
72
- "@gooddata/sdk-ui-geo": "11.49.0-alpha.1",
73
- "@gooddata/sdk-ui-filters": "11.49.0-alpha.1",
74
- "@gooddata/sdk-ui-pivot": "11.49.0-alpha.1",
75
- "@gooddata/sdk-ui-theme-provider": "11.49.0-alpha.1",
76
- "@gooddata/sdk-ui-vis-commons": "11.49.0-alpha.1",
77
- "@gooddata/sdk-ui-kit": "11.49.0-alpha.1",
78
- "@gooddata/sdk-ui-semantic-search": "11.49.0-alpha.1",
79
- "@gooddata/util": "11.49.0-alpha.1"
66
+ "@gooddata/sdk-backend-base": "11.50.0-alpha.0",
67
+ "@gooddata/sdk-backend-spi": "11.50.0-alpha.0",
68
+ "@gooddata/sdk-embedding": "11.50.0-alpha.0",
69
+ "@gooddata/sdk-model": "11.50.0-alpha.0",
70
+ "@gooddata/sdk-ui": "11.50.0-alpha.0",
71
+ "@gooddata/sdk-ui-charts": "11.50.0-alpha.0",
72
+ "@gooddata/sdk-ui-filters": "11.50.0-alpha.0",
73
+ "@gooddata/sdk-ui-geo": "11.50.0-alpha.0",
74
+ "@gooddata/sdk-ui-kit": "11.50.0-alpha.0",
75
+ "@gooddata/sdk-ui-pivot": "11.50.0-alpha.0",
76
+ "@gooddata/sdk-ui-semantic-search": "11.50.0-alpha.0",
77
+ "@gooddata/sdk-ui-theme-provider": "11.50.0-alpha.0",
78
+ "@gooddata/sdk-ui-vis-commons": "11.50.0-alpha.0",
79
+ "@gooddata/util": "11.50.0-alpha.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@microsoft/api-documenter": "^7.17.0",
@@ -124,12 +124,12 @@
124
124
  "typescript": "5.9.3",
125
125
  "vitest": "4.1.8",
126
126
  "vitest-dom": "0.1.1",
127
- "@gooddata/eslint-config": "11.49.0-alpha.1",
128
- "@gooddata/i18n-toolkit": "11.49.0-alpha.1",
129
- "@gooddata/oxlint-config": "11.49.0-alpha.1",
130
- "@gooddata/reference-workspace": "11.49.0-alpha.1",
131
- "@gooddata/sdk-backend-mockingbird": "11.49.0-alpha.1",
132
- "@gooddata/stylelint-config": "11.49.0-alpha.1"
127
+ "@gooddata/eslint-config": "11.50.0-alpha.0",
128
+ "@gooddata/i18n-toolkit": "11.50.0-alpha.0",
129
+ "@gooddata/oxlint-config": "11.50.0-alpha.0",
130
+ "@gooddata/reference-workspace": "11.50.0-alpha.0",
131
+ "@gooddata/stylelint-config": "11.50.0-alpha.0",
132
+ "@gooddata/sdk-backend-mockingbird": "11.50.0-alpha.0"
133
133
  },
134
134
  "peerDependencies": {
135
135
  "react": "^18.0.0 || ^19.0.0",