@gooddata/sdk-ui-kit 11.42.0-alpha.2 → 11.42.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.
- package/esm/@ui/@types/icon.d.ts +1 -1
- package/esm/@ui/@types/icon.d.ts.map +1 -1
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialog.d.ts +6 -31
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialog.d.ts.map +1 -1
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialog.js +8 -24
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialogCard.d.ts +56 -0
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialogCard.d.ts.map +1 -0
- package/esm/@ui/UiAddGranteeDialog/UiAddGranteeDialogCard.js +40 -0
- package/esm/@ui/UiAddGranteeDialog/useGranteeSelection.d.ts +35 -0
- package/esm/@ui/UiAddGranteeDialog/useGranteeSelection.d.ts.map +1 -0
- package/esm/@ui/UiAddGranteeDialog/useGranteeSelection.js +29 -0
- package/esm/@ui/UiAutocomplete/UiAutocomplete.d.ts +9 -0
- package/esm/@ui/UiAutocomplete/UiAutocomplete.d.ts.map +1 -0
- package/esm/@ui/UiAutocomplete/UiAutocomplete.js +230 -0
- package/esm/@ui/UiAutocomplete/types.d.ts +61 -0
- package/esm/@ui/UiAutocomplete/types.d.ts.map +1 -0
- package/esm/@ui/UiAutocomplete/types.js +2 -0
- package/esm/@ui/UiAutocomplete/useAsyncListSource.d.ts +42 -0
- package/esm/@ui/UiAutocomplete/useAsyncListSource.d.ts.map +1 -0
- package/esm/@ui/UiAutocomplete/useAsyncListSource.js +112 -0
- package/esm/@ui/UiCombobox/UiComboboxInput.d.ts +7 -2
- package/esm/@ui/UiCombobox/UiComboboxInput.d.ts.map +1 -1
- package/esm/@ui/UiCombobox/UiComboboxInput.js +2 -2
- package/esm/@ui/UiCombobox/useCombobox.d.ts.map +1 -1
- package/esm/@ui/UiCombobox/useCombobox.js +8 -15
- package/esm/@ui/UiCombobox/useComboboxChrome.d.ts +16 -5
- package/esm/@ui/UiCombobox/useComboboxChrome.d.ts.map +1 -1
- package/esm/@ui/UiCombobox/useComboboxChrome.js +53 -25
- package/esm/@ui/UiCombobox/useComboboxSelection.d.ts +2 -3
- package/esm/@ui/UiCombobox/useComboboxSelection.d.ts.map +1 -1
- package/esm/@ui/UiCombobox/useComboboxSelection.js +5 -9
- package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.d.ts +73 -0
- package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.d.ts.map +1 -0
- package/esm/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.js +69 -0
- package/esm/@ui/UiIcon/icons.d.ts.map +1 -1
- package/esm/@ui/UiIcon/icons.js +2 -0
- package/esm/@ui/UiTags/UiTags.js +1 -1
- package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.d.ts +8 -0
- package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.d.ts.map +1 -1
- package/esm/Dialog/StylingEditorDialog/StylingEditorDialog.js +28 -3
- package/esm/Dropdown/Dropdown.d.ts +4 -2
- package/esm/Dropdown/Dropdown.d.ts.map +1 -1
- package/esm/Dropdown/Dropdown.js +2 -2
- package/esm/WidgetNotice/WidgetNotice.d.ts.map +1 -1
- package/esm/WidgetNotice/WidgetNotice.js +1 -1
- package/esm/index.d.ts +4 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +3 -0
- package/esm/locales.d.ts +34 -0
- package/esm/locales.d.ts.map +1 -1
- package/esm/locales.js +14 -0
- package/esm/sdk-ui-kit.d.ts +217 -26
- package/package.json +11 -11
- package/src/@ui/UiAddGranteeDialog/UiAddGranteeDialog.scss +0 -14
- package/src/@ui/UiAutocomplete/UiAutocomplete.scss +53 -0
- package/src/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.scss +28 -0
- package/src/@ui/index.scss +2 -0
- package/styles/css/main.css +65 -5
- package/styles/css/main.css.map +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IUiAutocompleteOption, IUiAutocompleteSection } from "./types.js";
|
|
2
|
+
type LoadResult<T extends IUiAutocompleteOption> = {
|
|
3
|
+
sections: IUiAutocompleteSection<T>[];
|
|
4
|
+
hasNextPage?: boolean;
|
|
5
|
+
};
|
|
6
|
+
/** @internal */
|
|
7
|
+
export type AsyncListSourceLoader<T extends IUiAutocompleteOption> = (search: string, page: number) => Promise<LoadResult<T>>;
|
|
8
|
+
/** @internal */
|
|
9
|
+
export interface IUseAsyncListSourceOptions {
|
|
10
|
+
debounceMs: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `loadingMore` keeps prior pages visible while the next page is in flight;
|
|
14
|
+
* `error` blanks out the list.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export type AsyncListStatus = "loading" | "loadingMore" | "idle" | "error";
|
|
19
|
+
/** @internal */
|
|
20
|
+
export interface IAsyncListSource<T extends IUiAutocompleteOption> {
|
|
21
|
+
inputValue: string;
|
|
22
|
+
setInputValue: (next: string) => void;
|
|
23
|
+
sections: IUiAutocompleteSection<T>[];
|
|
24
|
+
status: AsyncListStatus;
|
|
25
|
+
hasNextPage: boolean;
|
|
26
|
+
loadMore: () => void;
|
|
27
|
+
reset: () => void;
|
|
28
|
+
/** Re-run the current page-0 query — used to recover from an error state. */
|
|
29
|
+
retry: () => void;
|
|
30
|
+
/** Increments on each page-0 load; lets callers tag per-query synthetic rows. */
|
|
31
|
+
generation: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Async data source for an autocomplete-shaped picker: debounce via
|
|
35
|
+
* `useDebouncedState`, fetching with stale-result protection via
|
|
36
|
+
* `useCancelablePromise`, section merge on pagination.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare function useAsyncListSource<T extends IUiAutocompleteOption>(loadOptions: AsyncListSourceLoader<T>, { debounceMs }: IUseAsyncListSourceOptions): IAsyncListSource<T>;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=useAsyncListSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAsyncListSource.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiAutocomplete/useAsyncListSource.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEhF,KAAK,UAAU,CAAC,CAAC,SAAS,qBAAqB,IAAI;IAC/C,QAAQ,EAAE,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,gBAAgB;AAChB,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,qBAAqB,IAAI,CACjE,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,KACX,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAE5B,gBAAgB;AAChB,MAAM,WAAW,0BAA0B;IACvC,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3E,gBAAgB;AAChB,MAAM,WAAW,gBAAgB,CAAC,CAAC,SAAS,qBAAqB;IAC7D,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;IACtC,MAAM,EAAE,eAAe,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,+EAA6E;IAC7E,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACtB;AAeD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,qBAAqB,EAC9D,WAAW,EAAE,qBAAqB,CAAC,CAAC,CAAC,EACrC,EAAE,UAAU,EAAE,EAAE,0BAA0B,GAC3C,gBAAgB,CAAC,CAAC,CAAC,CAqHrB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useCancelablePromise, useDebouncedState } from "@gooddata/sdk-ui";
|
|
4
|
+
/**
|
|
5
|
+
* Async data source for an autocomplete-shaped picker: debounce via
|
|
6
|
+
* `useDebouncedState`, fetching with stale-result protection via
|
|
7
|
+
* `useCancelablePromise`, section merge on pagination.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function useAsyncListSource(loadOptions, { debounceMs }) {
|
|
12
|
+
const [inputValue, setInputValue, debouncedQuery, setInputValueImmediate] = useDebouncedState("", debounceMs);
|
|
13
|
+
// Bumping `retryToken` re-runs page-0 even when the query is unchanged, so a
|
|
14
|
+
// failed load can recover (e.g. when the popup is reopened) without the user
|
|
15
|
+
// having to edit the search text.
|
|
16
|
+
const [retryToken, setRetryToken] = useState(0);
|
|
17
|
+
const retry = useCallback(() => setRetryToken((t) => t + 1), []);
|
|
18
|
+
// Each page-0 load bumps the generation; Load-more pages are tagged with the
|
|
19
|
+
// generation they belong to and ignored once it advances. Counting loads
|
|
20
|
+
// (not comparing result objects) stays correct even if a loader returns the
|
|
21
|
+
// same object for a repeated query.
|
|
22
|
+
const [generation, setGeneration] = useState(0);
|
|
23
|
+
const queryLoad = useCancelablePromise({ promise: () => loadOptions(debouncedQuery, 0), onSuccess: () => setGeneration((g) => g + 1) }, [debouncedQuery, loadOptions, retryToken]);
|
|
24
|
+
const [paginationState, setPagination] = useState(null);
|
|
25
|
+
const pagination = paginationState?.generation === generation ? paginationState : null;
|
|
26
|
+
const pendingPage = pagination?.pendingPage ?? null;
|
|
27
|
+
// While the typed value is ahead of the debounced one the fetch has not
|
|
28
|
+
// started yet, but the UI must already report loading (and below, drop the
|
|
29
|
+
// sections) so the previous query's options are not left clickable.
|
|
30
|
+
const isDebouncing = inputValue !== debouncedQuery;
|
|
31
|
+
const isQueryLoading = queryLoad.status === "pending" || queryLoad.status === "loading";
|
|
32
|
+
useCancelablePromise({
|
|
33
|
+
// Hold pagination until page-0 of the current query has settled, so a
|
|
34
|
+
// query change mid-pagination can't fetch the new query at an old page.
|
|
35
|
+
promise: pendingPage == null || isDebouncing || isQueryLoading
|
|
36
|
+
? null
|
|
37
|
+
: () => loadOptions(debouncedQuery, pendingPage),
|
|
38
|
+
onSuccess: (result) => setPagination((prev) => prev?.pendingPage == null
|
|
39
|
+
? prev
|
|
40
|
+
: {
|
|
41
|
+
generation: prev.generation,
|
|
42
|
+
sections: mergeSections(prev.sections, result.sections),
|
|
43
|
+
pagesLoaded: prev.pagesLoaded + 1,
|
|
44
|
+
hasNextPage: !!result.hasNextPage,
|
|
45
|
+
pendingPage: null,
|
|
46
|
+
}),
|
|
47
|
+
// Keep the already-loaded pages; dropping back to idle re-shows the
|
|
48
|
+
// Load-more row so the user can retry the failed page.
|
|
49
|
+
onError: () => setPagination((prev) => (prev ? { ...prev, pendingPage: null } : prev)),
|
|
50
|
+
}, [pendingPage, debouncedQuery, loadOptions, isDebouncing, isQueryLoading]);
|
|
51
|
+
const status = isDebouncing || isQueryLoading
|
|
52
|
+
? "loading"
|
|
53
|
+
: queryLoad.status === "error"
|
|
54
|
+
? "error"
|
|
55
|
+
: pendingPage == null
|
|
56
|
+
? "idle"
|
|
57
|
+
: "loadingMore";
|
|
58
|
+
const sections = useMemo(() => {
|
|
59
|
+
if (status === "loading" || status === "error" || queryLoad.result === undefined) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return pagination
|
|
63
|
+
? mergeSections(queryLoad.result.sections, pagination.sections)
|
|
64
|
+
: queryLoad.result.sections;
|
|
65
|
+
}, [status, queryLoad.result, pagination]);
|
|
66
|
+
const hasNextPage = (status === "idle" || status === "loadingMore") &&
|
|
67
|
+
(pagination ? pagination.hasNextPage : !!queryLoad.result?.hasNextPage);
|
|
68
|
+
const loadMore = useCallback(() => {
|
|
69
|
+
if (status !== "idle" || !hasNextPage) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setPagination({
|
|
73
|
+
generation,
|
|
74
|
+
sections: pagination?.sections ?? [],
|
|
75
|
+
pagesLoaded: pagination?.pagesLoaded ?? 0,
|
|
76
|
+
hasNextPage,
|
|
77
|
+
pendingPage: (pagination?.pagesLoaded ?? 0) + 1,
|
|
78
|
+
});
|
|
79
|
+
}, [status, hasNextPage, generation, pagination]);
|
|
80
|
+
// `setInputValueImmediate` from useDebouncedState isn't memoized, but it only
|
|
81
|
+
// delegates to stable `useState` setters, so omitting it keeps `reset`
|
|
82
|
+
// referentially stable without going stale.
|
|
83
|
+
const reset = useCallback(() => {
|
|
84
|
+
setInputValueImmediate("");
|
|
85
|
+
setPagination(null);
|
|
86
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
87
|
+
}, []);
|
|
88
|
+
return useMemo(() => ({
|
|
89
|
+
inputValue,
|
|
90
|
+
setInputValue,
|
|
91
|
+
sections,
|
|
92
|
+
status,
|
|
93
|
+
hasNextPage,
|
|
94
|
+
loadMore,
|
|
95
|
+
reset,
|
|
96
|
+
retry,
|
|
97
|
+
generation,
|
|
98
|
+
}), [inputValue, setInputValue, sections, status, hasNextPage, loadMore, reset, retry, generation]);
|
|
99
|
+
}
|
|
100
|
+
function mergeSections(current, incoming) {
|
|
101
|
+
const merged = current.map((s) => ({ ...s, options: [...s.options] }));
|
|
102
|
+
for (const next of incoming) {
|
|
103
|
+
const existing = merged.find((s) => s.id === next.id);
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.options.push(...next.options);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
merged.push({ ...next, options: [...next.options] });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return merged;
|
|
112
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type FocusEvent, type KeyboardEvent, type MouseEvent } from "react";
|
|
2
|
+
import { type IAccessibilityConfigBase } from "../../typings/accessibility.js";
|
|
2
3
|
/** @internal */
|
|
3
4
|
export interface IUiComboboxInputProps {
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Accessible name/description for the input (e.g. `ariaLabel`,
|
|
7
|
+
* `ariaDescribedBy`). The combobox role and listbox-wiring attributes are
|
|
8
|
+
* owned by the component and override anything passed here.
|
|
9
|
+
*/
|
|
10
|
+
accessibilityConfig?: IAccessibilityConfigBase;
|
|
6
11
|
/** Visible placeholder. */
|
|
7
12
|
placeholder?: string;
|
|
8
13
|
/** Form field name forwarded to the underlying input. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UiComboboxInput.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/UiComboboxInput.tsx"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,UAAU,EAIlB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"UiComboboxInput.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/UiComboboxInput.tsx"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,UAAU,EAIlB,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAK/E,gBAAgB;AAChB,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,wBAAwB,CAAC;IAC/C,2BAA2B;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IAC7D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACxD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACvD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,gBAAgB,CAAC,KAAK,IAAI,CAAC;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,oHAsF3B,CAAC"}
|
|
@@ -10,7 +10,7 @@ import { useComboboxState } from "./UiComboboxContext.js";
|
|
|
10
10
|
* @internal
|
|
11
11
|
*/
|
|
12
12
|
export const UiComboboxInput = forwardRef(function UiComboboxInput(props, forwardedRef) {
|
|
13
|
-
const {
|
|
13
|
+
const { accessibilityConfig, placeholder, name, autoFocus, onKeyDown: callerOnKeyDown, onFocus: callerOnFocus, onBlur: callerOnBlur, onClick: callerOnClick, dataTestId, } = props;
|
|
14
14
|
const { inputValue, onInputChange, onInputKeyDown, onInputBlur, isOpen, setIsOpen, anchorRef, activeOption, listboxId, } = useComboboxState();
|
|
15
15
|
const handleKeyDown = useCallback((event) => {
|
|
16
16
|
onInputKeyDown(event);
|
|
@@ -33,12 +33,12 @@ export const UiComboboxInput = forwardRef(function UiComboboxInput(props, forwar
|
|
|
33
33
|
// Browser autofill would overlap the listbox; the combobox
|
|
34
34
|
// owns its own typeahead so we suppress all native suggestions.
|
|
35
35
|
autoComplete: "off", autoCapitalize: "none", autoCorrect: "off", dataTestId: dataTestId, onKeyDown: handleKeyDown, onFocus: callerOnFocus, onBlur: handleBlur, onClick: handleClick, accessibilityConfig: {
|
|
36
|
+
...accessibilityConfig,
|
|
36
37
|
role: "combobox",
|
|
37
38
|
ariaAutocomplete: "list",
|
|
38
39
|
ariaExpanded: isOpen,
|
|
39
40
|
ariaActiveDescendant: activeOption?.id,
|
|
40
41
|
ariaHaspopup: "listbox",
|
|
41
42
|
ariaControls: listboxId,
|
|
42
|
-
ariaLabel,
|
|
43
43
|
} }));
|
|
44
44
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCombobox.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useCombobox.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkBtE,gBAAgB;AAChB,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"useCombobox.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useCombobox.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkBtE,gBAAgB;AAChB,wBAAgB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,gBAAgB,CAuEvE"}
|
|
@@ -15,23 +15,16 @@ const comboboxKeys = makeKeyboardNavigation({
|
|
|
15
15
|
export function useCombobox(params) {
|
|
16
16
|
const { value, defaultValue = "", onValueChange, options, creatable = false } = params;
|
|
17
17
|
const listboxId = useId();
|
|
18
|
-
const chrome = useComboboxChrome();
|
|
19
18
|
const selection = useComboboxSelection({
|
|
20
19
|
options,
|
|
21
20
|
value,
|
|
22
21
|
defaultValue,
|
|
23
22
|
onValueChange,
|
|
24
23
|
creatable,
|
|
25
|
-
setIsOpen: chrome.setIsOpen,
|
|
26
|
-
setActiveIndex: chrome.setActiveIndex,
|
|
24
|
+
setIsOpen: (open) => chrome.setIsOpen(open),
|
|
27
25
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const optionCount = selection.availableOptions.length;
|
|
31
|
-
const safeActiveIndex = chrome.activeIndex == null ? null : Math.min(chrome.activeIndex, optionCount - 1);
|
|
32
|
-
const activeOption = safeActiveIndex == null || optionCount === 0
|
|
33
|
-
? undefined
|
|
34
|
-
: selection.availableOptions[safeActiveIndex];
|
|
26
|
+
const chrome = useComboboxChrome(selection.availableOptions);
|
|
27
|
+
const activeOption = chrome.activeOption;
|
|
35
28
|
const onInputKeyDown = useMemo(() => {
|
|
36
29
|
// Leave Enter unhandled when there's no selectable target so creatable
|
|
37
30
|
// flows like UiTags (add tag on unhandled Enter) can react to it.
|
|
@@ -53,12 +46,12 @@ export function useCombobox(params) {
|
|
|
53
46
|
}
|
|
54
47
|
: undefined;
|
|
55
48
|
return comboboxKeys({
|
|
56
|
-
onArrowDown: () => chrome.focusByDelta(1
|
|
57
|
-
onArrowUp: () => chrome.focusByDelta(-1
|
|
49
|
+
onArrowDown: () => chrome.focusByDelta(1),
|
|
50
|
+
onArrowUp: () => chrome.focusByDelta(-1),
|
|
58
51
|
onEnter,
|
|
59
52
|
onEscape,
|
|
60
53
|
});
|
|
61
|
-
}, [chrome, selection, activeOption
|
|
54
|
+
}, [chrome, selection, activeOption]);
|
|
62
55
|
return useMemo(() => ({
|
|
63
56
|
availableOptions: selection.availableOptions,
|
|
64
57
|
inputValue: selection.inputValue,
|
|
@@ -74,8 +67,8 @@ export function useCombobox(params) {
|
|
|
74
67
|
selectOption: selection.selectOption,
|
|
75
68
|
anchorRef: chrome.anchorRef,
|
|
76
69
|
registerItemRef: chrome.registerItemRef,
|
|
77
|
-
shouldRenderPopup:
|
|
70
|
+
shouldRenderPopup: selection.availableOptions.length > 0,
|
|
78
71
|
creatable,
|
|
79
72
|
listboxId,
|
|
80
|
-
}), [chrome, selection, onInputKeyDown, activeOption, creatable, listboxId
|
|
73
|
+
}), [chrome, selection, onInputKeyDown, activeOption, creatable, listboxId]);
|
|
81
74
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import type { IUiComboboxState } from "./types.js";
|
|
1
|
+
import type { IUiComboboxOption, IUiComboboxState } from "./types.js";
|
|
2
2
|
type IUseComboboxChromeReturn = Pick<IUiComboboxState, "isOpen" | "setIsOpen" | "activeIndex" | "setActiveIndex" | "anchorRef" | "registerItemRef"> & {
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/** The currently highlighted option, or undefined. */
|
|
4
|
+
activeOption: IUiComboboxOption | undefined;
|
|
5
|
+
/** Open the popup if needed and move the highlight by `delta`, wrapping around. */
|
|
6
|
+
focusByDelta: (delta: 1 | -1) => void;
|
|
5
7
|
};
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Owns popup open/close, highlight, and scroll. The highlight is tracked by
|
|
10
|
+
* option **id**, not index: when the option list changes (async results arrive,
|
|
11
|
+
* a row is filtered out) a highlighted row that's gone simply resolves to no
|
|
12
|
+
* index, so the highlight can never point at a different row than the user
|
|
13
|
+
* navigated to. Callers pass the current `options` so the id ⇄ index mapping
|
|
14
|
+
* and `focusByDelta` wrapping stay in sync with what's rendered.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function useComboboxChrome(options: IUiComboboxOption[]): IUseComboboxChromeReturn;
|
|
8
19
|
export {};
|
|
9
20
|
//# sourceMappingURL=useComboboxChrome.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useComboboxChrome.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useComboboxChrome.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"useComboboxChrome.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useComboboxChrome.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE,KAAK,wBAAwB,GAAG,IAAI,CAChC,gBAAgB,EAChB,QAAQ,GAAG,WAAW,GAAG,aAAa,GAAG,gBAAgB,GAAG,WAAW,GAAG,iBAAiB,CAC9F,GAAG;IACA,sDAAsD;IACtD,YAAY,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC5C,mFAAmF;IACnF,YAAY,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,wBAAwB,CA6FxF"}
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
2
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Owns popup open/close, highlight, and scroll. The highlight is tracked by
|
|
5
|
+
* option **id**, not index: when the option list changes (async results arrive,
|
|
6
|
+
* a row is filtered out) a highlighted row that's gone simply resolves to no
|
|
7
|
+
* index, so the highlight can never point at a different row than the user
|
|
8
|
+
* navigated to. Callers pass the current `options` so the id ⇄ index mapping
|
|
9
|
+
* and `focusByDelta` wrapping stay in sync with what's rendered.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function useComboboxChrome(options) {
|
|
14
|
+
const [isOpen, setIsOpenState] = useState(false);
|
|
15
|
+
const [activeId, setActiveId] = useState(null);
|
|
16
|
+
// A highlight only has meaning while the popup is open, so closing always
|
|
17
|
+
// drops it — reopening then starts fresh. This is the single point that
|
|
18
|
+
// keeps a stale highlight from surviving Escape / blur / reset and being
|
|
19
|
+
// Enter-selected on the next open.
|
|
20
|
+
const setIsOpen = useCallback((open) => {
|
|
21
|
+
setIsOpenState(open);
|
|
22
|
+
if (!open) {
|
|
23
|
+
setActiveId(null);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
7
26
|
const anchorRef = useRef(null);
|
|
8
27
|
const itemRefs = useRef([]);
|
|
9
28
|
// Target index for when the scroll request runs before the item mounts.
|
|
10
29
|
const pendingScrollIndexRef = useRef(null);
|
|
30
|
+
const activeIndex = activeId == null ? null : options.findIndex((o) => o.id === activeId);
|
|
31
|
+
// findIndex returns -1 for a vanished row; surface that as "no highlight".
|
|
32
|
+
const safeActiveIndex = activeIndex != null && activeIndex >= 0 ? activeIndex : null;
|
|
33
|
+
const activeOption = safeActiveIndex == null ? undefined : options[safeActiveIndex];
|
|
11
34
|
const scrollIndexIntoView = useCallback((index) => {
|
|
12
|
-
if (index == null) {
|
|
13
|
-
pendingScrollIndexRef.current = null;
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
35
|
const node = itemRefs.current[index];
|
|
17
36
|
if (node) {
|
|
18
37
|
pendingScrollIndexRef.current = null;
|
|
@@ -29,33 +48,42 @@ export function useComboboxChrome() {
|
|
|
29
48
|
node.scrollIntoView({ block: "nearest" });
|
|
30
49
|
}
|
|
31
50
|
}, []);
|
|
32
|
-
const
|
|
51
|
+
const setActiveIndex = useCallback((index) => setActiveId(index == null ? null : (options[index]?.id ?? null)), [options]);
|
|
52
|
+
const focusByDelta = useCallback((delta) => {
|
|
53
|
+
const total = options.length;
|
|
33
54
|
if (total === 0) {
|
|
34
55
|
setIsOpen(true);
|
|
35
56
|
return;
|
|
36
57
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const current = activeIndex == null ? null : Math.min(activeIndex, total - 1);
|
|
48
|
-
const next = current == null ? (delta === 1 ? 0 : total - 1) : (current + delta + total) % total;
|
|
49
|
-
setActiveIndex(next);
|
|
58
|
+
// Resolve against the current list so a stale/removed highlight still
|
|
59
|
+
// produces a sensible next target.
|
|
60
|
+
const current = safeActiveIndex;
|
|
61
|
+
const next = !isOpen || current == null
|
|
62
|
+
? delta === 1
|
|
63
|
+
? 0
|
|
64
|
+
: total - 1
|
|
65
|
+
: (current + delta + total) % total;
|
|
66
|
+
setIsOpen(true);
|
|
67
|
+
setActiveId(options[next]?.id ?? null);
|
|
50
68
|
scrollIndexIntoView(next);
|
|
51
|
-
}, [isOpen,
|
|
69
|
+
}, [isOpen, options, safeActiveIndex, scrollIndexIntoView, setIsOpen]);
|
|
52
70
|
return useMemo(() => ({
|
|
53
71
|
isOpen,
|
|
54
72
|
setIsOpen,
|
|
55
|
-
activeIndex,
|
|
73
|
+
activeIndex: safeActiveIndex,
|
|
74
|
+
activeOption,
|
|
75
|
+
setActiveIndex,
|
|
76
|
+
anchorRef,
|
|
77
|
+
registerItemRef,
|
|
78
|
+
focusByDelta,
|
|
79
|
+
}), [
|
|
80
|
+
isOpen,
|
|
81
|
+
setIsOpen,
|
|
82
|
+
safeActiveIndex,
|
|
83
|
+
activeOption,
|
|
56
84
|
setActiveIndex,
|
|
57
85
|
anchorRef,
|
|
58
86
|
registerItemRef,
|
|
59
87
|
focusByDelta,
|
|
60
|
-
|
|
88
|
+
]);
|
|
61
89
|
}
|
|
@@ -7,18 +7,17 @@ export interface IUseComboboxSelectionParams {
|
|
|
7
7
|
onValueChange?: (value: string) => void;
|
|
8
8
|
creatable?: boolean;
|
|
9
9
|
setIsOpen: (open: boolean) => void;
|
|
10
|
-
setActiveIndex: (index: number | null) => void;
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
12
|
* @internal
|
|
14
13
|
*/
|
|
15
|
-
export declare function useComboboxSelection({ options, value, defaultValue, onValueChange, creatable, setIsOpen
|
|
14
|
+
export declare function useComboboxSelection({ options, value, defaultValue, onValueChange, creatable, setIsOpen }: IUseComboboxSelectionParams): {
|
|
16
15
|
availableOptions: IUiComboboxOption[];
|
|
17
16
|
inputValue: string;
|
|
18
17
|
onInputChange: (next: string) => void;
|
|
19
18
|
onInputBlur: () => void;
|
|
20
19
|
selectedOption: IUiComboboxOption | undefined;
|
|
21
|
-
selectOption: (option: IUiComboboxOption
|
|
20
|
+
selectOption: (option: IUiComboboxOption) => void;
|
|
22
21
|
resetState: () => void;
|
|
23
22
|
};
|
|
24
23
|
//# sourceMappingURL=useComboboxSelection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useComboboxSelection.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useComboboxSelection.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,gBAAgB;AAChB,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"useComboboxSelection.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiCombobox/useComboboxSelection.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,gBAAgB;AAChB,MAAM,WAAW,2BAA2B;IACxC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,OAAO,EACP,KAAK,EACL,YAAiB,EACjB,aAAa,EACb,SAAiB,EACjB,SAAS,EACZ,EAAE,2BAA2B;;;;;;;;EA4E7B"}
|
|
@@ -4,14 +4,13 @@ import { normalizeValue } from "./utils.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
-
export function useComboboxSelection({ options, value, defaultValue = "", onValueChange, creatable = false, setIsOpen,
|
|
7
|
+
export function useComboboxSelection({ options, value, defaultValue = "", onValueChange, creatable = false, setIsOpen, }) {
|
|
8
8
|
const [inputValue, setInputValue] = useControlledValue({ value, defaultValue, onValueChange });
|
|
9
9
|
const [selectedOption, setSelectedOption] = useState(undefined);
|
|
10
10
|
const resetState = useCallback(() => {
|
|
11
11
|
setInputValue(defaultValue);
|
|
12
|
-
setActiveIndex(null);
|
|
13
12
|
setSelectedOption(undefined);
|
|
14
|
-
}, [defaultValue, setInputValue
|
|
13
|
+
}, [defaultValue, setInputValue]);
|
|
15
14
|
const availableOptions = useMemo(() => {
|
|
16
15
|
const value = normalizeValue(inputValue);
|
|
17
16
|
const selectedValue = selectedOption ? normalizeValue(selectedOption.label) : undefined;
|
|
@@ -31,20 +30,17 @@ export function useComboboxSelection({ options, value, defaultValue = "", onValu
|
|
|
31
30
|
}
|
|
32
31
|
return matchedOptions;
|
|
33
32
|
}, [options, inputValue, selectedOption, creatable]);
|
|
34
|
-
const selectOption = useCallback((option
|
|
35
|
-
// Hover sets
|
|
33
|
+
const selectOption = useCallback((option) => {
|
|
34
|
+
// Hover sets the highlight without checking `disabled`, so without
|
|
36
35
|
// this guard a disabled row could be Enter-confirmed even though
|
|
37
36
|
// clicking it is blocked in the list item.
|
|
38
37
|
if (option.disabled) {
|
|
39
38
|
return;
|
|
40
39
|
}
|
|
41
|
-
if (index !== undefined) {
|
|
42
|
-
setActiveIndex(index);
|
|
43
|
-
}
|
|
44
40
|
setSelectedOption(option);
|
|
45
41
|
setIsOpen(false);
|
|
46
42
|
setInputValue(option.label);
|
|
47
|
-
}, [
|
|
43
|
+
}, [setIsOpen, setInputValue]);
|
|
48
44
|
const onInputChange = useCallback((next) => {
|
|
49
45
|
setInputValue(next);
|
|
50
46
|
setIsOpen(true);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type GranteeAvatarKind } from "../UiGranteeAvatar/UiGranteeAvatar.js";
|
|
2
|
+
import { type PermissionMenuLevel } from "../UiPermissionMenu/UiPermissionMenu.js";
|
|
3
|
+
/**
|
|
4
|
+
* One pickable user or group entry.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface IUiGranteeAsyncOption {
|
|
9
|
+
/** Stable identifier — used as the React key and as the value passed to `onSelect`. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Avatar variant. */
|
|
12
|
+
kind: GranteeAvatarKind;
|
|
13
|
+
/** Display name shown on the row. */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Optional email subline (users only). */
|
|
16
|
+
email?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A previously-picked grantee with its current permission level.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface IUiPickedGrantee extends IUiGranteeAsyncOption {
|
|
24
|
+
/** Permission level chosen for this grantee. Drives the row's permission menu trigger label. */
|
|
25
|
+
permissionLevel: PermissionMenuLevel;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Result returned by the consumer's loader: separate `groups` and `users` arrays
|
|
29
|
+
* so the dropdown can render the two sections independently. Either may be empty.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export interface IUiGranteeAsyncOptions {
|
|
34
|
+
groups: IUiGranteeAsyncOption[];
|
|
35
|
+
users: IUiGranteeAsyncOption[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export interface IUiGranteeAsyncPickerProps {
|
|
41
|
+
/**
|
|
42
|
+
* Loader called whenever the (debounced) search query changes. The empty
|
|
43
|
+
* string is passed on mount to populate the initial dropdown. Pagination
|
|
44
|
+
* is intentionally not exposed at this layer — the loader returns the
|
|
45
|
+
* full search result and the consumer caps backend results elsewhere.
|
|
46
|
+
*/
|
|
47
|
+
loadOptions: (search: string) => Promise<IUiGranteeAsyncOptions>;
|
|
48
|
+
/**
|
|
49
|
+
* Grantees already picked. Rendered below the search input as `UiGranteeRow`s
|
|
50
|
+
* with a `UiPermissionMenu` trigger and a Remove action. Also filtered out
|
|
51
|
+
* of the dropdown so the user cannot pick the same grantee twice.
|
|
52
|
+
*/
|
|
53
|
+
selectedGrantees?: ReadonlyArray<IUiPickedGrantee>;
|
|
54
|
+
/** Fires when the user picks an option from the dropdown. */
|
|
55
|
+
onSelect: (option: IUiGranteeAsyncOption) => void;
|
|
56
|
+
/** Fires when the user changes the permission level on a picked row. */
|
|
57
|
+
onPermissionChange?: (grantee: IUiPickedGrantee, next: PermissionMenuLevel) => void;
|
|
58
|
+
/** Fires when the user picks Remove access in the row's permission menu. */
|
|
59
|
+
onRemove?: (grantee: IUiPickedGrantee) => void;
|
|
60
|
+
/** Test id forwarded to the root element. */
|
|
61
|
+
dataTestId?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sectioned async grantee picker — specialization of `UiAutocomplete` for the
|
|
65
|
+
* share/access-control domain. Renders a search input with a dropdown split
|
|
66
|
+
* into `Groups` and `Users` sections, and a list of picked grantees below
|
|
67
|
+
* (each as a `UiGranteeRow` with a `UiPermissionMenu` trigger). The
|
|
68
|
+
* permission menu carries the Remove action; consumers wire it via `onRemove`.
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export declare function UiGranteeAsyncPicker({ loadOptions, selectedGrantees, onSelect, onPermissionChange, onRemove, dataTestId }: IUiGranteeAsyncPickerProps): import("react/jsx-runtime").JSX.Element;
|
|
73
|
+
//# sourceMappingURL=UiGranteeAsyncPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UiGranteeAsyncPicker.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiGranteeAsyncPicker/UiGranteeAsyncPicker.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE/E,OAAO,EAAE,KAAK,mBAAmB,EAAoB,MAAM,yCAAyC,CAAC;AAIrG;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IAClC,yFAAuF;IACvF,EAAE,EAAE,MAAM,CAAC;IACX,sBAAsB;IACtB,IAAI,EAAE,iBAAiB,CAAC;IACxB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAiB,SAAQ,qBAAqB;IAC3D,gGAAgG;IAChG,eAAe,EAAE,mBAAmB,CAAC;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACnC,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,KAAK,EAAE,qBAAqB,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACvC;;;;;OAKG;IACH,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACjE;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACnD,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACpF,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC/C,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAOD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,WAAW,EACX,gBAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACb,EAAE,0BAA0B,2CA2G5B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// (C) 2026 GoodData Corporation
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import { useIntl } from "react-intl";
|
|
5
|
+
import { olpAddGranteeDialogMessages, olpPermissionMessages, uiGranteeAsyncPickerMessages, } from "../../locales.js";
|
|
6
|
+
import { bem } from "../@utils/bem.js";
|
|
7
|
+
import { UiAutocomplete } from "../UiAutocomplete/UiAutocomplete.js";
|
|
8
|
+
import { UiButton } from "../UiButton/UiButton.js";
|
|
9
|
+
import { UiGranteeRow } from "../UiGranteeRow/UiGranteeRow.js";
|
|
10
|
+
import { UiPermissionMenu } from "../UiPermissionMenu/UiPermissionMenu.js";
|
|
11
|
+
const { b, e } = bem("gd-ui-kit-grantee-async-picker");
|
|
12
|
+
/**
|
|
13
|
+
* Sectioned async grantee picker — specialization of `UiAutocomplete` for the
|
|
14
|
+
* share/access-control domain. Renders a search input with a dropdown split
|
|
15
|
+
* into `Groups` and `Users` sections, and a list of picked grantees below
|
|
16
|
+
* (each as a `UiGranteeRow` with a `UiPermissionMenu` trigger). The
|
|
17
|
+
* permission menu carries the Remove action; consumers wire it via `onRemove`.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
export function UiGranteeAsyncPicker({ loadOptions, selectedGrantees = [], onSelect, onPermissionChange, onRemove, dataTestId, }) {
|
|
22
|
+
const intl = useIntl();
|
|
23
|
+
const groupsLabel = intl.formatMessage(uiGranteeAsyncPickerMessages.sectionGroups);
|
|
24
|
+
const usersLabel = intl.formatMessage(uiGranteeAsyncPickerMessages.sectionUsers);
|
|
25
|
+
const canViewLabel = intl.formatMessage(olpPermissionMessages.canView);
|
|
26
|
+
const canShareLabel = intl.formatMessage(olpPermissionMessages.canViewAndShare);
|
|
27
|
+
const selectedIds = useMemo(() => selectedGrantees.map((g) => g.id), [selectedGrantees]);
|
|
28
|
+
const adaptedLoadOptions = useCallback(async (search) => {
|
|
29
|
+
const { groups, users } = await loadOptions(search);
|
|
30
|
+
const sections = [];
|
|
31
|
+
if (groups.length > 0) {
|
|
32
|
+
sections.push({
|
|
33
|
+
id: "groups",
|
|
34
|
+
label: groupsLabel,
|
|
35
|
+
options: groups.map((g) => ({ id: g.id, label: g.name, grantee: g })),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (users.length > 0) {
|
|
39
|
+
sections.push({
|
|
40
|
+
id: "users",
|
|
41
|
+
label: usersLabel,
|
|
42
|
+
options: users.map((u) => ({
|
|
43
|
+
id: u.id,
|
|
44
|
+
label: u.name,
|
|
45
|
+
secondaryText: u.email,
|
|
46
|
+
grantee: u,
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// The grantee picker doesn't paginate at the kit level — backend
|
|
51
|
+
// pagination, if any, is hidden inside the consumer's loader.
|
|
52
|
+
return { sections };
|
|
53
|
+
}, [loadOptions, groupsLabel, usersLabel]);
|
|
54
|
+
const handleSelect = useCallback((option) => {
|
|
55
|
+
onSelect(option.grantee);
|
|
56
|
+
}, [onSelect]);
|
|
57
|
+
const autocompleteMessages = useMemo(() => ({
|
|
58
|
+
searchPlaceholder: intl.formatMessage(uiGranteeAsyncPickerMessages.searchPlaceholder),
|
|
59
|
+
stateError: intl.formatMessage(uiGranteeAsyncPickerMessages.stateError),
|
|
60
|
+
stateNoMatch: intl.formatMessage(uiGranteeAsyncPickerMessages.stateNoMatch),
|
|
61
|
+
}), [intl]);
|
|
62
|
+
return (_jsxs("div", { className: b(), "data-testid": dataTestId, children: [
|
|
63
|
+
_jsx(UiAutocomplete, { loadOptions: adaptedLoadOptions, selectedIds: selectedIds, onSelect: handleSelect, messages: autocompleteMessages, accessibilityConfig: {
|
|
64
|
+
ariaLabel: intl.formatMessage(olpAddGranteeDialogMessages.userOrGroup),
|
|
65
|
+
} }), selectedGrantees.length === 0 ? (_jsx("div", { className: e("empty-state"), children: intl.formatMessage(olpAddGranteeDialogMessages.emptyState) })) : null, selectedGrantees.length > 0 ? (_jsx("ul", { className: e("picked-list"), children: selectedGrantees.map((g) => {
|
|
66
|
+
const triggerLabel = g.permissionLevel === "SHARE" ? canShareLabel : canViewLabel;
|
|
67
|
+
return (_jsx("li", { className: e("picked-item"), children: _jsx(UiGranteeRow, { kind: g.kind, name: g.name, email: g.email, controls: _jsx(UiPermissionMenu, { selectedLevel: g.permissionLevel, onPermissionChange: (next) => onPermissionChange?.(g, next), onRemoveAccess: onRemove ? () => onRemove(g) : undefined, anchor: _jsx(UiButton, { label: triggerLabel, variant: "dropdownInline", size: "small", iconAfter: "navigateDown" }) }) }) }, g.id));
|
|
68
|
+
}) })) : null] }));
|
|
69
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiIcon/icons.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../../src/@ui/UiIcon/icons.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,CAk/BjD,CAAC"}
|