@gooddata/sdk-ui-ext 11.43.0-alpha.2 → 11.43.0-alpha.4

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 (171) hide show
  1. package/NOTICE +4826 -13873
  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/components/configurationControls/ConfigSection.d.ts +2 -1
  6. package/esm/internal/components/configurationControls/ConfigSection.d.ts.map +1 -1
  7. package/esm/internal/components/configurationControls/ConfigSection.js +7 -3
  8. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorDropdown.d.ts +4 -0
  9. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorDropdown.d.ts.map +1 -1
  10. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorDropdown.js +13 -2
  11. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorPalette.d.ts +5 -0
  12. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorPalette.d.ts.map +1 -1
  13. package/esm/internal/components/configurationControls/colors/colorDropdown/ColorPalette.js +5 -3
  14. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts +13 -0
  15. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.d.ts.map +1 -0
  16. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingDialog.js +158 -0
  17. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts +12 -0
  18. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.d.ts.map +1 -0
  19. package/esm/internal/components/configurationControls/conditionalFormatting/ConditionalFormattingSection.js +54 -0
  20. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingColors.d.ts +14 -0
  21. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingColors.d.ts.map +1 -0
  22. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingColors.js +32 -0
  23. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts +28 -0
  24. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.d.ts.map +1 -0
  25. package/esm/internal/components/configurationControls/conditionalFormatting/conditionalFormattingModel.js +149 -0
  26. package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts +1 -0
  27. package/esm/internal/components/configurationPanels/ConfigurationPanelContent.d.ts.map +1 -1
  28. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts +1 -0
  29. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.d.ts.map +1 -1
  30. package/esm/internal/components/configurationPanels/PivotTableConfigurationPanel.js +9 -1
  31. package/esm/internal/components/dialogs/userManagementDialogs/AddWorkspaceToSubjects.d.ts +0 -1
  32. package/esm/internal/components/dialogs/userManagementDialogs/AddWorkspaceToSubjects.d.ts.map +1 -1
  33. package/esm/internal/components/dialogs/userManagementDialogs/AddWorkspaceToSubjects.js +3 -3
  34. package/esm/internal/components/dialogs/userManagementDialogs/UserEditDialog.d.ts +0 -1
  35. package/esm/internal/components/dialogs/userManagementDialogs/UserEditDialog.d.ts.map +1 -1
  36. package/esm/internal/components/dialogs/userManagementDialogs/UserEditDialog.js +2 -2
  37. package/esm/internal/components/dialogs/userManagementDialogs/UserGroupEditDialog.d.ts +0 -1
  38. package/esm/internal/components/dialogs/userManagementDialogs/UserGroupEditDialog.d.ts.map +1 -1
  39. package/esm/internal/components/dialogs/userManagementDialogs/UserGroupEditDialog.js +3 -3
  40. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/AddWorkspace.d.ts +1 -2
  41. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/AddWorkspace.d.ts.map +1 -1
  42. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/AddWorkspace.js +2 -2
  43. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/GranularPermissions.d.ts +1 -2
  44. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/GranularPermissions.d.ts.map +1 -1
  45. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/GranularPermissions.js +3 -3
  46. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/PermissionsDropdown.d.ts +0 -1
  47. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/PermissionsDropdown.d.ts.map +1 -1
  48. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/PermissionsDropdown.js +3 -5
  49. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/WorkspaceItem.d.ts +1 -2
  50. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/WorkspaceItem.d.ts.map +1 -1
  51. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceItem/WorkspaceItem.js +2 -2
  52. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceList.d.ts +0 -1
  53. package/esm/internal/components/dialogs/userManagementDialogs/Workspace/WorkspaceList.d.ts.map +1 -1
  54. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts +3 -0
  55. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.d.ts.map +1 -1
  56. package/esm/internal/components/pluggableVisualizations/pivotTableNext/PluggablePivotTableNext.js +33 -5
  57. package/esm/internal/constants/supportedProperties.d.ts.map +1 -1
  58. package/esm/internal/constants/supportedProperties.js +1 -0
  59. package/esm/internal/translations/de-DE.localization-bundle.d.ts +11 -0
  60. package/esm/internal/translations/de-DE.localization-bundle.d.ts.map +1 -1
  61. package/esm/internal/translations/de-DE.localization-bundle.js +12 -1
  62. package/esm/internal/translations/en-AU.localization-bundle.d.ts +11 -0
  63. package/esm/internal/translations/en-AU.localization-bundle.d.ts.map +1 -1
  64. package/esm/internal/translations/en-AU.localization-bundle.js +12 -1
  65. package/esm/internal/translations/en-GB.localization-bundle.d.ts +11 -0
  66. package/esm/internal/translations/en-GB.localization-bundle.d.ts.map +1 -1
  67. package/esm/internal/translations/en-GB.localization-bundle.js +12 -1
  68. package/esm/internal/translations/en-US.localization-bundle.d.ts +168 -0
  69. package/esm/internal/translations/en-US.localization-bundle.d.ts.map +1 -1
  70. package/esm/internal/translations/en-US.localization-bundle.js +168 -0
  71. package/esm/internal/translations/es-419.localization-bundle.d.ts +11 -0
  72. package/esm/internal/translations/es-419.localization-bundle.d.ts.map +1 -1
  73. package/esm/internal/translations/es-419.localization-bundle.js +12 -1
  74. package/esm/internal/translations/es-ES.localization-bundle.d.ts +11 -0
  75. package/esm/internal/translations/es-ES.localization-bundle.d.ts.map +1 -1
  76. package/esm/internal/translations/es-ES.localization-bundle.js +12 -1
  77. package/esm/internal/translations/fi-FI.localization-bundle.d.ts +11 -0
  78. package/esm/internal/translations/fi-FI.localization-bundle.d.ts.map +1 -1
  79. package/esm/internal/translations/fi-FI.localization-bundle.js +12 -1
  80. package/esm/internal/translations/fr-CA.localization-bundle.d.ts +11 -0
  81. package/esm/internal/translations/fr-CA.localization-bundle.d.ts.map +1 -1
  82. package/esm/internal/translations/fr-CA.localization-bundle.js +12 -1
  83. package/esm/internal/translations/fr-FR.localization-bundle.d.ts +11 -0
  84. package/esm/internal/translations/fr-FR.localization-bundle.d.ts.map +1 -1
  85. package/esm/internal/translations/fr-FR.localization-bundle.js +12 -1
  86. package/esm/internal/translations/id-ID.localization-bundle.d.ts +11 -0
  87. package/esm/internal/translations/id-ID.localization-bundle.d.ts.map +1 -1
  88. package/esm/internal/translations/id-ID.localization-bundle.js +12 -1
  89. package/esm/internal/translations/it-IT.localization-bundle.d.ts +11 -0
  90. package/esm/internal/translations/it-IT.localization-bundle.d.ts.map +1 -1
  91. package/esm/internal/translations/it-IT.localization-bundle.js +12 -1
  92. package/esm/internal/translations/ja-JP.localization-bundle.d.ts +11 -0
  93. package/esm/internal/translations/ja-JP.localization-bundle.d.ts.map +1 -1
  94. package/esm/internal/translations/ja-JP.localization-bundle.js +12 -1
  95. package/esm/internal/translations/ko-KR.localization-bundle.d.ts +11 -0
  96. package/esm/internal/translations/ko-KR.localization-bundle.d.ts.map +1 -1
  97. package/esm/internal/translations/ko-KR.localization-bundle.js +12 -1
  98. package/esm/internal/translations/nl-NL.localization-bundle.d.ts +11 -0
  99. package/esm/internal/translations/nl-NL.localization-bundle.d.ts.map +1 -1
  100. package/esm/internal/translations/nl-NL.localization-bundle.js +12 -1
  101. package/esm/internal/translations/pl-PL.localization-bundle.d.ts +11 -0
  102. package/esm/internal/translations/pl-PL.localization-bundle.d.ts.map +1 -1
  103. package/esm/internal/translations/pl-PL.localization-bundle.js +12 -1
  104. package/esm/internal/translations/pt-BR.localization-bundle.d.ts +11 -0
  105. package/esm/internal/translations/pt-BR.localization-bundle.d.ts.map +1 -1
  106. package/esm/internal/translations/pt-BR.localization-bundle.js +12 -1
  107. package/esm/internal/translations/pt-PT.localization-bundle.d.ts +11 -0
  108. package/esm/internal/translations/pt-PT.localization-bundle.d.ts.map +1 -1
  109. package/esm/internal/translations/pt-PT.localization-bundle.js +12 -1
  110. package/esm/internal/translations/ru-RU.localization-bundle.d.ts +11 -0
  111. package/esm/internal/translations/ru-RU.localization-bundle.d.ts.map +1 -1
  112. package/esm/internal/translations/ru-RU.localization-bundle.js +12 -1
  113. package/esm/internal/translations/sl-SI.localization-bundle.d.ts +11 -0
  114. package/esm/internal/translations/sl-SI.localization-bundle.d.ts.map +1 -1
  115. package/esm/internal/translations/sl-SI.localization-bundle.js +12 -1
  116. package/esm/internal/translations/th-TH.localization-bundle.d.ts +11 -0
  117. package/esm/internal/translations/th-TH.localization-bundle.d.ts.map +1 -1
  118. package/esm/internal/translations/th-TH.localization-bundle.js +12 -1
  119. package/esm/internal/translations/tr-TR.localization-bundle.d.ts +11 -0
  120. package/esm/internal/translations/tr-TR.localization-bundle.d.ts.map +1 -1
  121. package/esm/internal/translations/tr-TR.localization-bundle.js +12 -1
  122. package/esm/internal/translations/uk-UA.localization-bundle.d.ts +11 -0
  123. package/esm/internal/translations/uk-UA.localization-bundle.d.ts.map +1 -1
  124. package/esm/internal/translations/uk-UA.localization-bundle.js +12 -1
  125. package/esm/internal/translations/vi-VN.localization-bundle.d.ts +11 -0
  126. package/esm/internal/translations/vi-VN.localization-bundle.d.ts.map +1 -1
  127. package/esm/internal/translations/vi-VN.localization-bundle.js +12 -1
  128. package/esm/internal/translations/zh-HK.localization-bundle.d.ts +11 -0
  129. package/esm/internal/translations/zh-HK.localization-bundle.d.ts.map +1 -1
  130. package/esm/internal/translations/zh-HK.localization-bundle.js +12 -1
  131. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts +11 -0
  132. package/esm/internal/translations/zh-Hans.localization-bundle.d.ts.map +1 -1
  133. package/esm/internal/translations/zh-Hans.localization-bundle.js +12 -1
  134. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts +11 -0
  135. package/esm/internal/translations/zh-Hant.localization-bundle.d.ts.map +1 -1
  136. package/esm/internal/translations/zh-Hant.localization-bundle.js +12 -1
  137. package/esm/internal/utils/propertiesHelper.d.ts +2 -1
  138. package/esm/internal/utils/propertiesHelper.d.ts.map +1 -1
  139. package/esm/internal/utils/propertiesHelper.js +3 -0
  140. package/esm/locales.d.ts +79 -0
  141. package/esm/locales.d.ts.map +1 -1
  142. package/esm/locales.js +46 -0
  143. package/esm/sdk-ui-ext.d.ts +24 -10
  144. package/esm/share/ObjectShareDialog.js +2 -2
  145. package/esm/share/objectShareController.helpers.d.ts +0 -26
  146. package/esm/share/objectShareController.helpers.d.ts.map +1 -1
  147. package/esm/share/objectShareController.helpers.js +2 -38
  148. package/esm/share/objectShareController.types.d.ts +39 -0
  149. package/esm/share/objectShareController.types.d.ts.map +1 -1
  150. package/esm/share/useAccessList.d.ts +31 -18
  151. package/esm/share/useAccessList.d.ts.map +1 -1
  152. package/esm/share/useAccessList.js +85 -133
  153. package/esm/share/useLabelScope.d.ts +6 -8
  154. package/esm/share/useLabelScope.d.ts.map +1 -1
  155. package/esm/share/useLabelScope.js +34 -28
  156. package/esm/share/useObjectShareController.d.ts +19 -16
  157. package/esm/share/useObjectShareController.d.ts.map +1 -1
  158. package/esm/share/useObjectShareController.js +212 -119
  159. package/package.json +21 -21
  160. package/styles/css/main.css +238 -0
  161. package/styles/css/main.css.map +1 -1
  162. package/styles/internal/css/conditional_formatting.css +239 -0
  163. package/styles/internal/css/conditional_formatting.css.map +1 -0
  164. package/styles/internal/css/config_panel.css +5 -0
  165. package/styles/internal/css/config_panel.css.map +1 -1
  166. package/styles/internal/scss/conditional_formatting.scss +269 -0
  167. package/styles/internal/scss/config_panel.scss +11 -0
  168. package/styles/scss/main.scss +1 -0
  169. package/esm/share/useObjectShare.d.ts +0 -50
  170. package/esm/share/useObjectShare.d.ts.map +0 -1
  171. package/esm/share/useObjectShare.js +0 -17
@@ -1,40 +1,56 @@
1
1
  // (C) 2026 GoodData Corporation
2
- import { useCallback, useMemo, useState } from "react";
2
+ import { useCallback, useMemo, useRef, useState } from "react";
3
3
  import { useToastMessage } from "@gooddata/sdk-ui-kit";
4
4
  import { objectShareMessages } from "./messages.js";
5
- import { EMPTY_IDS, NO_LABELS, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
5
+ import { EMPTY_IDS, NO_LABELS, effectivePermissionAbove, granularGranteeFor, toGranularGrantee, } from "./objectShareController.helpers.js";
6
6
  import { useAccessList } from "./useAccessList.js";
7
7
  import { useLabelScope } from "./useLabelScope.js";
8
8
  /**
9
- * Manages the share dialog state and backend I/O for a single shareable
10
- * object.
9
+ * Manages the share dialog state and backend I/O for a single shareable object.
11
10
  *
12
- * The access list is fetched (and re-fetched after every save) via
13
- * `useCancelablePromise`; everything that reflects the backend the grantee
14
- * rows, general-access value, summary is derived from it on render, never
15
- * copied into local state. Local state is only the dialog's own transient UI:
16
- * the active subview, the add-grantee buffer, the pending general-access
17
- * confirm, and an in-flight save flag. Top-level open/close is the consumer's
18
- * concern (see {@link ObjectShareDialog}).
11
+ * The access list is fetched once and seeded into local state, which is the
12
+ * source of truth while the dialog/summary is mounted. Each access change is
13
+ * written through to local state immediately and sent to the backend; on failure
14
+ * the one changed entry is rolled back. There is no post-write refetch, so the
15
+ * grantee list never blanks and never fights the backend's read-after-write lag.
16
+ * Top-level open/close is the consumer's concern (see {@link ObjectShareDialog}).
19
17
  *
20
- * Mutations follow a **commit-on-interaction** model: each access change is
21
- * sent immediately; the general-access toggle goes through a confirm step
22
- * because it is high-impact. There is no batched Save. The list is fetched
23
- * eagerly so `state.summary` also drives an inline access row while closed.
18
+ * Mutations follow a **commit-on-interaction** model: each access change is sent
19
+ * immediately; the general-access toggle goes through a confirm step because it
20
+ * is high-impact. There is no batched Save. The list is fetched eagerly so
21
+ * `state.summary` also drives an inline access row while closed.
22
+ *
23
+ * Most consumers do not call this directly — render {@link ObjectShareDialog} with
24
+ * plain props and it owns its controller. Call this (exported as `useObjectShare`)
25
+ * only to share a single access-list fetch between the dialog and an inline summary
26
+ * row: call it once, read `state.summary` for the row, and pass the controller into
27
+ * the dialog.
24
28
  *
25
29
  * @internal
26
30
  */
27
- export function useObjectShareController(target, onSaved, labels = NO_LABELS, labelsError = false, labelsLoading = false) {
31
+ export function useObjectShareController(target, options) {
32
+ const { onSaved, labels = NO_LABELS, labelsError = false, labelsLoading = false } = options ?? {};
28
33
  const toast = useToastMessage();
29
34
  // UI-local buffers — never backend data.
30
35
  const [subview, setSubview] = useState("main");
31
36
  const [pendingGrantees, setPendingGrantees] = useState([]);
32
37
  const [pendingGeneralAccess, setPendingGeneralAccess] = useState(undefined);
33
- // The backend access list + optimistic overlay (rows, general access, summary,
34
- // the commit/picker primitives) live in their own hook.
35
- const { targetKey, currentAccessList, grantees, granteeIdsKey, generalAccess, summary, status, loadError, commit, loadOptions, refForId, setOverlay, setKnownNames, setOptimisticGeneralAccess, } = useAccessList(target, onSaved);
38
+ // The backend access list, seeded into local state (rows, general access,
39
+ // summary, the commit/picker primitives) lives in its own hook.
40
+ const { targetKey, hasList, grantees, generalAccess, workspaceLevel, summary, status, loadError, commit, loadOptions, refForId, setGrantees, setGeneralAccess, setWorkspaceLevel, setKnownNames, } = useAccessList(target, onSaved);
41
+ // Always-current target key, read inside async mutation finalizers that captured
42
+ // an older one. A write started for object A resolves after the user navigated to
43
+ // object B; because local state is authoritative (no refetch), applying A's
44
+ // finalizer to B's seeded list would corrupt B (e.g. clobber a same-id row, or
45
+ // revert B's general access). Each finalizer captures the target it started for
46
+ // and bails if it no longer matches.
47
+ const targetKeyRef = useRef(targetKey);
48
+ targetKeyRef.current = targetKey;
49
+ // The committed grantee ids the label-scope probe resolves against (a removing
50
+ // row is excluded so its scope is dropped, not re-seeded).
51
+ const committedGranteeIds = useMemo(() => grantees.filter((g) => g.pending !== "removing").map((g) => g.id), [grantees]);
36
52
  // Per-label scope resolution + the single label-write path live in their own hook.
37
- const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, optimisticScopeRef, reconcileLabelScope, } = useLabelScope(target, targetKey, labels, currentAccessList, granteeIdsKey, labelsError, labelsLoading);
53
+ const { effectiveLabels, labelsResolved, selectedLabelIdsByGrantee, setSelectedLabelIdsByGrantee, reconcileLabelScope, } = useLabelScope(target, targetKey, labels, hasList, committedGranteeIds, labelsError, labelsLoading);
38
54
  // Drop the transient UI buffers when the permission target changes. The detail
39
55
  // view is reused across objects and closes the dialog by toggling `isOpen`
40
56
  // alone, so without this a staged add-grantee subview or general-access confirm
@@ -61,70 +77,97 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
61
77
  setSubview("main");
62
78
  setPendingGrantees([]);
63
79
  }, []);
80
+ // The single object+labels write path shared by remove and general access (the
81
+ // two changes that must keep the object grant and its label mirror consistent).
82
+ // Mirrors the labels first, then writes the object; if the object write fails it
83
+ // undoes the label mirror so the two never drift. `abortIfLabelsFail` distinguishes
84
+ // the two callers' label-failure policy: general access aborts (labels and object
85
+ // are one logical change), remove proceeds (the object revoke is what matters; a
86
+ // leftover label grant is only surfaced as a warning). Returns the object-write
87
+ // result and whether the label mirror fully applied; the caller owns the local
88
+ // write-through, the revert and any toast beyond `commit`'s own.
89
+ const applyAccessChange = useCallback(async (params) => {
90
+ const { principal, objectMutation, successMessage, labelDesired, labelCurrent } = params;
91
+ const labelsOk = await reconcileLabelScope(principal, labelDesired, labelCurrent);
92
+ if (!labelsOk && params.abortIfLabelsFail) {
93
+ // Don't write the object on a half-applied label scope: undo whatever
94
+ // mirrored and surface the failure to the caller.
95
+ await reconcileLabelScope(principal, labelCurrent, labelDesired);
96
+ return { ok: false, labelsOk: false };
97
+ }
98
+ const ok = await commit([objectMutation], successMessage);
99
+ if (!ok) {
100
+ // Object write failed — undo the label mirror so labels and object
101
+ // don't drift.
102
+ await reconcileLabelScope(principal, labelCurrent, labelDesired);
103
+ }
104
+ return { ok, labelsOk };
105
+ }, [commit, reconcileLabelScope]);
64
106
  const confirmAddGrantees = useCallback(async () => {
65
107
  if (pendingGrantees.length === 0) {
66
108
  return;
67
109
  }
68
- // Optimistically insert the picked grantees as pending rows, carrying the
69
- // picker's display name so the new row never renders a raw id.
70
- const added = pendingGrantees.map((g) => ({
110
+ const startedFor = targetKey;
111
+ // Insert the picked grantees as pending rows, carrying the picker's display
112
+ // name so the new row never renders a raw id.
113
+ const addedIds = pendingGrantees.map((g) => g.id);
114
+ const addedRows = pendingGrantees.map((g) => ({
71
115
  id: g.id,
72
- grantee: {
73
- id: g.id,
74
- kind: g.kind,
75
- granteeRef: refForId(g.id),
76
- name: g.name,
77
- level: g.permissionLevel,
78
- },
116
+ kind: g.kind,
117
+ granteeRef: refForId(g.id),
118
+ name: g.name,
119
+ level: g.permissionLevel,
120
+ pending: "saving",
79
121
  }));
80
122
  const mutations = pendingGrantees.map((g) => toGranularGrantee(g.kind, refForId(g.id), g.permissionLevel));
81
- setOverlay((prev) => {
123
+ // Default each new grantee to ALL labels (the picker's Add is gated until
124
+ // labels have loaded, so the set is known here). Reflect the full scope
125
+ // before the writes so the row shows it immediately.
126
+ const allLabelIds = effectiveLabels.map((l) => l.id);
127
+ const allLabelIdSet = new Set(allLabelIds);
128
+ setGrantees((prev) => [...prev.filter((g) => !addedIds.includes(g.id)), ...addedRows]);
129
+ setKnownNames((prev) => {
82
130
  const next = { ...prev };
83
- for (const { id, grantee } of added) {
84
- next[id] = { op: "set", grantee, pending: true };
131
+ for (const g of pendingGrantees) {
132
+ next[g.id] = g.name;
85
133
  }
86
134
  return next;
87
135
  });
88
- // Cache the picked names so the rows keep them after the overlay reconciles.
89
- setKnownNames((prev) => {
136
+ setSelectedLabelIdsByGrantee((prev) => {
90
137
  const next = { ...prev };
91
- for (const { id, grantee } of added) {
92
- next[id] = grantee.name;
138
+ for (const id of addedIds) {
139
+ next[id] = allLabelIds;
93
140
  }
94
141
  return next;
95
142
  });
96
143
  // Leave the add subview at once so the new rows show in the main list.
97
144
  closeAddGrantee();
98
145
  const ok = await commit(mutations, objectShareMessages.toastGranteeAdded);
146
+ // Bail if the target switched mid-add: the new object's list was already
147
+ // seeded, so rolling back / clearing markers / mirroring labels by these ids
148
+ // would hit the wrong object.
149
+ if (targetKeyRef.current !== startedFor) {
150
+ return;
151
+ }
99
152
  if (!ok) {
100
- // Roll back the failed adds; refresh() already reconciled on success.
101
- setOverlay((prev) => {
153
+ // Roll back the failed adds.
154
+ setGrantees((prev) => prev.filter((g) => !addedIds.includes(g.id)));
155
+ setSelectedLabelIdsByGrantee((prev) => {
102
156
  const next = { ...prev };
103
- for (const { id } of added) {
157
+ for (const id of addedIds) {
104
158
  delete next[id];
105
159
  }
106
160
  return next;
107
161
  });
108
162
  return;
109
163
  }
110
- // Default each new grantee to ALL labels (the picker's Add is gated until
111
- // labels have loaded, so the set is known here). Reflect the full scope
112
- // optimistically and hold it through read-after-write lag; if any label
113
- // write fails, surface it and drop that grantee's optimistic scope rather
114
- // than claim access that didn't persist.
115
- const allLabelIds = effectiveLabels.map((l) => l.id);
116
- const allLabelIdSet = new Set(allLabelIds);
117
- for (const g of pendingGrantees) {
118
- optimisticScopeRef.current.add(g.id);
119
- }
120
- setSelectedLabelIdsByGrantee((prev) => {
121
- const next = { ...prev };
122
- for (const g of pendingGrantees) {
123
- next[g.id] = allLabelIds;
124
- }
125
- return next;
126
- });
164
+ // Object grant landed clear the saving marker on the new rows.
165
+ setGrantees((prev) => prev.map((g) => (addedIds.includes(g.id) ? { ...g, pending: undefined } : g)));
166
+ // Mirror each new grantee's full label scope; pin failures to primary-only.
127
167
  const scoped = await Promise.all(pendingGrantees.map((g) => reconcileLabelScope({ kind: g.kind, granteeRef: refForId(g.id) }, allLabelIdSet, EMPTY_IDS)));
168
+ if (targetKeyRef.current !== startedFor) {
169
+ return;
170
+ }
128
171
  const failed = pendingGrantees.filter((_, i) => !scoped[i]);
129
172
  if (failed.length > 0) {
130
173
  toast.addWarning(objectShareMessages.toastLabelScopePartial);
@@ -143,70 +186,101 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
143
186
  }
144
187
  }, [
145
188
  pendingGrantees,
189
+ targetKey,
146
190
  commit,
147
191
  closeAddGrantee,
148
192
  effectiveLabels,
149
193
  reconcileLabelScope,
150
194
  refForId,
151
195
  toast,
152
- setOverlay,
196
+ setGrantees,
153
197
  setKnownNames,
154
198
  setSelectedLabelIdsByGrantee,
155
- optimisticScopeRef,
156
199
  ]);
157
200
  const changePermissionLevel = useCallback(async (granteeId, level) => {
158
201
  const grantee = grantees.find((g) => g.id === granteeId);
159
202
  if (!grantee || grantee.level === level || grantee.pending) {
160
203
  return;
161
204
  }
205
+ const startedFor = targetKey;
162
206
  const previousLevel = grantee.level;
163
- // Show the new level immediately; keep it through the backend's
164
- // read-after-write lag (reconcileOverlay clears it once confirmed).
165
- setOverlay((prev) => ({
166
- ...prev,
167
- [granteeId]: { op: "set", grantee: { ...grantee, level }, pending: true },
168
- }));
207
+ // Recompute the inherited-SHARE badge for the new direct level: raising a
208
+ // VIEW grant to SHARE makes the inherited SHARE no longer "above" the
209
+ // direct grant (badge hidden); lowering it back surfaces the warning again.
210
+ const nextEffective = effectivePermissionAbove(level, grantee.inheritsShare ? ["SHARE"] : []);
211
+ // Show the new level immediately (marked saving), then write it through.
212
+ setGrantees((prev) => prev.map((g) => g.id === granteeId
213
+ ? { ...g, level, effectivePermission: nextEffective, pending: "saving" }
214
+ : g));
169
215
  const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, level)], objectShareMessages.toastAccessUpdated);
170
- if (!ok) {
171
- // Revert to the prior level on failure.
172
- setOverlay((prev) => ({
173
- ...prev,
174
- [granteeId]: { op: "set", grantee: { ...grantee, level: previousLevel }, pending: false },
175
- }));
216
+ // The target may have changed while the write was in flight; applying this
217
+ // finalizer to the newly seeded list would corrupt the other object's row.
218
+ if (targetKeyRef.current !== startedFor) {
219
+ return;
176
220
  }
177
- }, [grantees, commit, setOverlay]);
221
+ setGrantees((prev) => prev.map((g) => g.id === granteeId
222
+ ? {
223
+ ...g,
224
+ level: ok ? level : previousLevel,
225
+ effectivePermission: ok
226
+ ? nextEffective
227
+ : effectivePermissionAbove(previousLevel, g.inheritsShare ? ["SHARE"] : []),
228
+ pending: undefined,
229
+ }
230
+ : g));
231
+ }, [grantees, targetKey, commit, setGrantees]);
178
232
  const removeGrantee = useCallback(async (granteeId) => {
179
233
  const grantee = grantees.find((g) => g.id === granteeId);
180
234
  if (!grantee || grantee.pending) {
181
235
  return;
182
236
  }
237
+ const startedFor = targetKey;
183
238
  // Mark the row removed but keep it visible (muted) until the write lands.
184
- setOverlay((prev) => ({ ...prev, [granteeId]: { op: "remove", pending: true } }));
239
+ setGrantees((prev) => prev.map((g) => (g.id === granteeId ? { ...g, pending: "removing" } : g)));
185
240
  // Revoke the grantee's per-label grants too — they are independent
186
- // access-list entries, so the object revoke alone would leave them
187
- // behind. Reconcile to an empty scope (current = all permissionable).
188
- const allCurrent = new Set(effectiveLabels.map((l) => l.id));
189
- const principal = { kind: grantee.kind, granteeRef: grantee.granteeRef };
190
- const labelsRevoked = await reconcileLabelScope(principal, EMPTY_IDS, allCurrent);
191
- const ok = await commit([toGranularGrantee(grantee.kind, grantee.granteeRef, "none")], objectShareMessages.toastAccessUpdated);
241
+ // access-list entries, so the object revoke alone would leave them behind
242
+ // (current = all permissionable). The object revoke is what matters; a
243
+ // label revoke that fails is non-fatal (surfaced as a warning), so don't
244
+ // abort on it.
245
+ const { ok, labelsOk } = await applyAccessChange({
246
+ principal: { kind: grantee.kind, granteeRef: grantee.granteeRef },
247
+ objectMutation: toGranularGrantee(grantee.kind, grantee.granteeRef, "none"),
248
+ successMessage: objectShareMessages.toastAccessUpdated,
249
+ labelDesired: EMPTY_IDS,
250
+ labelCurrent: new Set(effectiveLabels.map((l) => l.id)),
251
+ abortIfLabelsFail: false,
252
+ });
253
+ // Bail if the target switched mid-write: the row no longer belongs to the
254
+ // seeded list, and dropping/restoring "it" would hit the other object.
255
+ if (targetKeyRef.current !== startedFor) {
256
+ return;
257
+ }
192
258
  if (!ok) {
193
- // Restore the row on failure — and re-grant the labels we just
194
- // revoked, so a failed object revoke doesn't strip label access
195
- // while the grantee row stays.
196
- if (labelsRevoked) {
197
- await reconcileLabelScope(principal, allCurrent, EMPTY_IDS);
198
- }
199
- setOverlay((prev) => {
200
- const { [granteeId]: _omit, ...rest } = prev;
201
- return rest;
202
- });
259
+ // Object revoke failed (applyAccessChange already re-granted the
260
+ // labels) restore the row.
261
+ setGrantees((prev) => prev.map((g) => (g.id === granteeId ? { ...g, pending: undefined } : g)));
262
+ return;
203
263
  }
204
- else if (!labelsRevoked) {
264
+ // Object revoke landed — drop the row from local state.
265
+ setGrantees((prev) => prev.filter((g) => g.id !== granteeId));
266
+ setSelectedLabelIdsByGrantee((prev) => {
267
+ const { [granteeId]: _omit, ...rest } = prev;
268
+ return rest;
269
+ });
270
+ if (!labelsOk) {
205
271
  // Object access is gone but some per-label grants couldn't be
206
272
  // revoked — warn so the leftover scope isn't mistaken for success.
207
273
  toast.addWarning(objectShareMessages.toastLabelScopePartial);
208
274
  }
209
- }, [grantees, commit, effectiveLabels, reconcileLabelScope, toast, setOverlay]);
275
+ }, [
276
+ grantees,
277
+ targetKey,
278
+ applyAccessChange,
279
+ effectiveLabels,
280
+ toast,
281
+ setGrantees,
282
+ setSelectedLabelIdsByGrantee,
283
+ ]);
210
284
  const changeGranteeLabels = useCallback(async (granteeId, requested) => {
211
285
  const grantee = grantees.find((g) => g.id === granteeId);
212
286
  if (!grantee || !target) {
@@ -218,17 +292,19 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
218
292
  const currentScope = selectedLabelIdsByGrantee[granteeId] ?? effectiveLabels.map((l) => l.id);
219
293
  const desired = new Set(nextScope);
220
294
  const current = new Set(currentScope);
221
- // Optimistic: reflect the new scope immediately and hold it through lag.
295
+ const startedFor = targetKey;
296
+ // Reflect the new scope immediately; restore it on failure.
222
297
  const previousScope = currentScope;
223
- optimisticScopeRef.current.add(granteeId);
224
298
  setSelectedLabelIdsByGrantee((prev) => ({ ...prev, [granteeId]: nextScope }));
225
299
  const ok = await reconcileLabelScope({ kind: grantee.kind, granteeRef: grantee.granteeRef }, desired, current);
300
+ // Bail if the target switched mid-write — the scope map now belongs to the
301
+ // other object, so neither the success no-op nor the rollback applies here.
302
+ if (targetKeyRef.current !== startedFor) {
303
+ return;
304
+ }
226
305
  if (ok) {
227
306
  toast.addSuccess(objectShareMessages.toastAccessUpdated);
228
307
  onSaved?.();
229
- // Keep the optimistic scope as the source of truth — re-resolving now
230
- // would clobber it with the backend's read-after-write-stale value
231
- // (same lag the grantee overlay handles).
232
308
  }
233
309
  else {
234
310
  toast.addError(objectShareMessages.toastError);
@@ -237,13 +313,13 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
237
313
  }, [
238
314
  grantees,
239
315
  target,
316
+ targetKey,
240
317
  effectiveLabels,
241
318
  selectedLabelIdsByGrantee,
242
319
  reconcileLabelScope,
243
320
  toast,
244
321
  onSaved,
245
322
  setSelectedLabelIdsByGrantee,
246
- optimisticScopeRef,
247
323
  ]);
248
324
  const requestGeneralAccessChange = useCallback((next) => {
249
325
  if (next !== generalAccess) {
@@ -256,42 +332,59 @@ export function useObjectShareController(target, onSaved, labels = NO_LABELS, la
256
332
  setPendingGeneralAccess(undefined);
257
333
  return;
258
334
  }
335
+ const startedFor = targetKey;
259
336
  const previous = generalAccess;
260
- // Apply optimistically and close the confirm at once — the radio + summary
261
- // reflect `next` immediately; the write commits in the background.
262
- setOptimisticGeneralAccess(next);
337
+ const previousWorkspaceLevel = workspaceLevel;
338
+ // Apply the new value and close the confirm at once the radio + summary
339
+ // reflect `next` immediately; the write commits in the background. The
340
+ // workspace rule is always written as VIEW, so reflect VIEW now too —
341
+ // otherwise the summary would keep a SHARE level left over from the fetch.
342
+ setGeneralAccess(next);
343
+ if (next === "WORKSPACE") {
344
+ setWorkspaceLevel("VIEW");
345
+ }
263
346
  setPendingGeneralAccess(undefined);
264
347
  const principal = { allWorkspaceUsers: true };
265
348
  const allIds = new Set(effectiveLabels.map((l) => l.id));
266
- // The workspace rule must cover every label too, or the workspace would
267
- // hold object access while non-primary labels stay ungranted. Mirror it
268
- // first (WORKSPACE → all labels, RESTRICTED → none), then write the object.
349
+ // The workspace rule must cover every label too, or the workspace would hold
350
+ // object access while non-primary labels stay ungranted. Mirror the labels
351
+ // (WORKSPACE → all, RESTRICTED → none) then write the object; the two are one
352
+ // logical change, so abort if the labels fail.
269
353
  const [desired, current] = next === "WORKSPACE" ? [allIds, EMPTY_IDS] : [EMPTY_IDS, allIds];
270
- const mirrored = await reconcileLabelScope(principal, desired, current);
271
- if (!mirrored) {
272
- // The label mirror failed: don't write the object grant on top of a
273
- // half-applied label scope. Undo whatever mirrored, revert the
274
- // optimistic override and surface the error.
275
- await reconcileLabelScope(principal, current, desired);
276
- setOptimisticGeneralAccess((value) => (value === next ? previous : value));
277
- toast.addError(objectShareMessages.toastError);
354
+ const { ok, labelsOk } = await applyAccessChange({
355
+ principal,
356
+ objectMutation: granularGranteeFor(principal, next === "WORKSPACE" ? "VIEW" : "none"),
357
+ successMessage: objectShareMessages.toastGeneralAccessUpdated,
358
+ labelDesired: desired,
359
+ labelCurrent: current,
360
+ abortIfLabelsFail: true,
361
+ });
362
+ // Bail if the target switched mid-write — reverting now would change the
363
+ // other object's general access.
364
+ if (targetKeyRef.current !== startedFor) {
278
365
  return;
279
366
  }
280
- const ok = await commit([granularGranteeFor(principal, next === "WORKSPACE" ? "VIEW" : "none")], objectShareMessages.toastGeneralAccessUpdated);
281
367
  if (!ok) {
282
- // Object write failed: undo the label mirror too, so labels and object
283
- // don't drift, and revert the optimistic override.
284
- await reconcileLabelScope(principal, current, desired);
285
- setOptimisticGeneralAccess((value) => (value === next ? previous : value));
368
+ // Labels or object write failed (applyAccessChange already undid the
369
+ // label mirror) revert the value (and the workspace level). The
370
+ // label-abort path produced no toast, so surface the error here; a
371
+ // failed object write already toasted via commit, so don't double up.
372
+ setGeneralAccess(previous);
373
+ setWorkspaceLevel(previousWorkspaceLevel);
374
+ if (!labelsOk) {
375
+ toast.addError(objectShareMessages.toastError);
376
+ }
286
377
  }
287
378
  }, [
288
379
  pendingGeneralAccess,
289
380
  generalAccess,
290
- commit,
381
+ workspaceLevel,
382
+ targetKey,
383
+ applyAccessChange,
291
384
  effectiveLabels,
292
- reconcileLabelScope,
293
385
  toast,
294
- setOptimisticGeneralAccess,
386
+ setGeneralAccess,
387
+ setWorkspaceLevel,
295
388
  ]);
296
389
  const actions = useMemo(() => ({
297
390
  reset,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gooddata/sdk-ui-ext",
3
- "version": "11.43.0-alpha.2",
3
+ "version": "11.43.0-alpha.4",
4
4
  "description": "GoodData.UI SDK - Extensions",
5
5
  "license": "LicenseRef-LICENSE",
6
6
  "author": "GoodData Corporation",
@@ -64,20 +64,20 @@
64
64
  "ts-invariant": "0.10.3",
65
65
  "tslib": "2.8.1",
66
66
  "uuid": "11.1.1",
67
- "@gooddata/sdk-backend-base": "11.43.0-alpha.2",
68
- "@gooddata/sdk-backend-spi": "11.43.0-alpha.2",
69
- "@gooddata/sdk-embedding": "11.43.0-alpha.2",
70
- "@gooddata/sdk-model": "11.43.0-alpha.2",
71
- "@gooddata/sdk-ui": "11.43.0-alpha.2",
72
- "@gooddata/sdk-ui-charts": "11.43.0-alpha.2",
73
- "@gooddata/sdk-ui-filters": "11.43.0-alpha.2",
74
- "@gooddata/sdk-ui-geo": "11.43.0-alpha.2",
75
- "@gooddata/sdk-ui-kit": "11.43.0-alpha.2",
76
- "@gooddata/sdk-ui-semantic-search": "11.43.0-alpha.2",
77
- "@gooddata/sdk-ui-theme-provider": "11.43.0-alpha.2",
78
- "@gooddata/sdk-ui-pivot": "11.43.0-alpha.2",
79
- "@gooddata/sdk-ui-vis-commons": "11.43.0-alpha.2",
80
- "@gooddata/util": "11.43.0-alpha.2"
67
+ "@gooddata/sdk-backend-base": "11.43.0-alpha.4",
68
+ "@gooddata/sdk-embedding": "11.43.0-alpha.4",
69
+ "@gooddata/sdk-model": "11.43.0-alpha.4",
70
+ "@gooddata/sdk-backend-spi": "11.43.0-alpha.4",
71
+ "@gooddata/sdk-ui": "11.43.0-alpha.4",
72
+ "@gooddata/sdk-ui-charts": "11.43.0-alpha.4",
73
+ "@gooddata/sdk-ui-filters": "11.43.0-alpha.4",
74
+ "@gooddata/sdk-ui-geo": "11.43.0-alpha.4",
75
+ "@gooddata/sdk-ui-kit": "11.43.0-alpha.4",
76
+ "@gooddata/sdk-ui-semantic-search": "11.43.0-alpha.4",
77
+ "@gooddata/sdk-ui-pivot": "11.43.0-alpha.4",
78
+ "@gooddata/sdk-ui-theme-provider": "11.43.0-alpha.4",
79
+ "@gooddata/sdk-ui-vis-commons": "11.43.0-alpha.4",
80
+ "@gooddata/util": "11.43.0-alpha.4"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@microsoft/api-documenter": "^7.17.0",
@@ -125,12 +125,12 @@
125
125
  "typescript": "5.9.3",
126
126
  "vitest": "4.1.8",
127
127
  "vitest-dom": "0.1.1",
128
- "@gooddata/eslint-config": "11.43.0-alpha.2",
129
- "@gooddata/i18n-toolkit": "11.43.0-alpha.2",
130
- "@gooddata/reference-workspace": "11.43.0-alpha.2",
131
- "@gooddata/oxlint-config": "11.43.0-alpha.2",
132
- "@gooddata/sdk-backend-mockingbird": "11.43.0-alpha.2",
133
- "@gooddata/stylelint-config": "11.43.0-alpha.2"
128
+ "@gooddata/eslint-config": "11.43.0-alpha.4",
129
+ "@gooddata/i18n-toolkit": "11.43.0-alpha.4",
130
+ "@gooddata/oxlint-config": "11.43.0-alpha.4",
131
+ "@gooddata/sdk-backend-mockingbird": "11.43.0-alpha.4",
132
+ "@gooddata/reference-workspace": "11.43.0-alpha.4",
133
+ "@gooddata/stylelint-config": "11.43.0-alpha.4"
134
134
  },
135
135
  "peerDependencies": {
136
136
  "react": "^18.0.0 || ^19.0.0",