Line data Source code
1 : import * as React from 'react';
2 :
3 : import type { ConnectorType } from '../../connections/connector_info.js';
4 : import { SnippetBrowser } from './snippets_overlay.js';
5 : import type { SnippetRegistry } from './snippet_registry.js';
6 : import { useFocusTrap } from '../../ui/foundations/focus.js';
7 : import { Overlay, OverlaySize } from '../../ui/foundations/overlay.js';
8 :
9 1 : const IGNORE_OUTSIDE_CLICK = () => {};
10 :
11 : interface PendingRequest {
12 : resolve: (text: string | null) => void;
13 : signal?: AbortSignal;
14 : onAbort?: () => void;
15 : }
16 :
17 : export interface SnippetsDialogController {
18 : /// Browse the snippets, resolves with the text of the used snippet or null if the dialog was closed
19 : request(signal?: AbortSignal): Promise<string | null>;
20 : }
21 :
22 : export interface SnippetsDialogHookResult {
23 : controller: SnippetsDialogController;
24 : dialog: React.ReactElement | null;
25 : }
26 :
27 : /// The snippet browser of the script cards as a dialog of the shell, listing the snippets of the connector
28 1 : export function useSnippetsDialog(connectorType: ConnectorType, registry?: SnippetRegistry): SnippetsDialogHookResult {
29 1 : const [open, setOpen] = React.useState(false);
30 1 : const pendingRequestRef = React.useRef<PendingRequest | null>(null);
31 :
32 1 : const finish = React.useCallback((text: string | null) => {
33 1 : const pending = pendingRequestRef.current;
34 1 : pendingRequestRef.current = null;
35 1 : if (pending?.signal != null && pending.onAbort != null) {
36 1 : pending.signal.removeEventListener('abort', pending.onAbort);
37 : }
38 1 : setOpen(false);
39 1 : pending?.resolve(text);
40 1 : }, []);
41 1 : const dismiss = React.useCallback(() => finish(null), [finish]);
42 :
43 1 : const request = React.useCallback((signal?: AbortSignal) => {
44 1 : if (pendingRequestRef.current != null) {
45 1 : return Promise.reject(new Error('A snippets request is already pending.'));
46 : }
47 1 : if (signal?.aborted) return Promise.resolve(null);
48 :
49 1 : setOpen(true);
50 1 : return new Promise<string | null>(resolve => {
51 1 : const pending: PendingRequest = { resolve, signal };
52 1 : pending.onAbort = dismiss;
53 1 : pendingRequestRef.current = pending;
54 1 : signal?.addEventListener('abort', pending.onAbort, { once: true });
55 1 : });
56 1 : }, [dismiss]);
57 :
58 1 : React.useEffect(() => () => {
59 1 : const pending = pendingRequestRef.current;
60 1 : pendingRequestRef.current = null;
61 1 : if (pending?.signal != null && pending.onAbort != null) {
62 0 : pending.signal.removeEventListener('abort', pending.onAbort);
63 : }
64 1 : pending?.resolve(null);
65 1 : }, []);
66 :
67 1 : const controller = React.useMemo<SnippetsDialogController>(() => ({ request }), [request]);
68 1 : return {
69 1 : controller,
70 1 : dialog: open ? <SnippetsDialog connectorType={connectorType} registry={registry} onApply={finish} onClose={dismiss} /> : null,
71 1 : };
72 1 : }
73 :
74 : interface SnippetsDialogProps {
75 : connectorType: ConnectorType;
76 : registry?: SnippetRegistry;
77 : onApply: (text: string) => void;
78 : onClose: () => void;
79 : }
80 :
81 1 : function SnippetsDialog(props: SnippetsDialogProps): React.ReactElement {
82 1 : const overlayRef = React.useRef<HTMLDivElement>(null);
83 1 : const listRef = React.useRef<HTMLDivElement>(null);
84 :
85 1 : useFocusTrap({
86 1 : containerRef: overlayRef as React.RefObject<HTMLElement>,
87 1 : initialFocusRef: listRef as React.RefObject<HTMLElement>,
88 1 : restoreFocusOnCleanUp: true,
89 1 : });
90 :
91 1 : return (
92 1 : <Overlay
93 1 : ref={overlayRef}
94 1 : centered
95 1 : width={OverlaySize.XXL}
96 1 : height={OverlaySize.L}
97 1 : preventFocusOnOpen
98 1 : onEscape={props.onClose}
99 1 : onClickOutside={IGNORE_OUTSIDE_CLICK}
100 : >
101 1 : <SnippetBrowser
102 1 : connectorType={props.connectorType}
103 1 : registry={props.registry}
104 1 : listRef={listRef}
105 1 : onApply={props.onApply}
106 1 : onClose={props.onClose}
107 1 : />
108 1 : </Overlay>
109 : );
110 1 : }
|