@servicetitan/dte-unlayer 0.141.0 → 0.143.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.
@@ -1,43 +1,61 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { generateId } from './constants';
3
3
  import {
4
4
  emitLookupOptionsRequest,
5
- isSearchableLookupSource,
6
5
  LookupOption,
7
6
  onLookupOptionsResponse,
8
7
  } from './lookupOptionsController';
9
8
 
10
9
  /**
11
- * - `idle` -> no lookup source (or searchable source): use free-text input.
12
- * - `loading` -> request in flight: show a disabled/loading value control.
13
- * - `ready` -> host returned options: render the dropdown.
14
- * - `fallback` -> empty/error/timeout: fall back to free-text so the user is never blocked.
10
+ * - `idle` -> no lookup source: use free-text input.
11
+ * - `loading` -> initial request in flight: show a disabled/loading value control.
12
+ * - `static` -> host returned a full list: render a locally-filtered dropdown.
13
+ * - `searchable` -> host wants per-query search: render an async dropdown driven by `loadOptions`.
14
+ * - `fallback` -> empty/error/timeout: fall back to free-text so the user is never blocked.
15
15
  */
16
- export type LookupOptionsStatus = 'idle' | 'loading' | 'ready' | 'fallback';
16
+ export type LookupOptionsStatus = 'idle' | 'loading' | 'static' | 'searchable' | 'fallback';
17
17
 
18
18
  /** Time to wait for the host before falling back to free-text input. */
19
19
  const LOOKUP_OPTIONS_TIMEOUT_MS = 10_000;
20
20
 
21
21
  interface UseLookupOptionsResult {
22
22
  status: LookupOptionsStatus;
23
+ /** Full option list for `static` sources; empty for `searchable`/`idle`/`fallback`. */
23
24
  options: LookupOption[];
25
+ /**
26
+ * Per-query resolver for `searchable` sources. Emits a request with the typed
27
+ * `query` and resolves with the host's fresh list (or `[]` on timeout). Wire
28
+ * this into an async dropdown (e.g. Anvil2 `SelectField`'s `loadOptions`).
29
+ */
30
+ loadOptions: (query: string) => Promise<LookupOption[]>;
31
+ /**
32
+ * Resolve the option(s) for already-stored keys (exact match, not a text
33
+ * search). Used on open to turn a saved searchable value back into its
34
+ * human-readable label. Resolves with `[]` on timeout.
35
+ */
36
+ resolveOptions: (keys: string[]) => Promise<LookupOption[]>;
24
37
  }
25
38
 
26
39
  /**
27
40
  * Requests host-resolved dropdown options for a field's `fieldLookupSource`.
28
41
  *
29
- * Re-runs whenever `sourceKey` changes, ignores stale responses whose requestId
30
- * does not match the latest request, and cleans up its listener/timeout on
31
- * unmount or source change.
42
+ * Fires a single query-less request whenever `sourceKey` changes. The host's
43
+ * response decides the mode: a full list (`static`, filtered locally) or a
44
+ * searchable source (`searchable`, driven by `loadOptions` per keystroke).
45
+ * Stale responses (mismatched requestId) are ignored and the listener/timeout
46
+ * are cleaned up on unmount or source change.
32
47
  */
33
48
  export const useLookupOptions = (sourceKey: string | undefined): UseLookupOptionsResult => {
34
49
  const [status, setStatus] = useState<LookupOptionsStatus>('idle');
35
50
  const [options, setOptions] = useState<LookupOption[]>([]);
36
51
  const latestRequestIdRef = useRef<string | null>(null);
52
+ const sourceKeyRef = useRef<string | undefined>(sourceKey);
37
53
 
38
54
  useEffect(() => {
39
- // No source, or a searchable source we intentionally skip for now.
40
- if (!sourceKey || isSearchableLookupSource(sourceKey)) {
55
+ sourceKeyRef.current = sourceKey;
56
+ const normalizedSourceKey = sourceKey?.trim();
57
+
58
+ if (!normalizedSourceKey) {
41
59
  latestRequestIdRef.current = null;
42
60
  setStatus('idle');
43
61
  setOptions([]);
@@ -63,16 +81,21 @@ export const useLookupOptions = (sourceKey: string | undefined): UseLookupOption
63
81
  return;
64
82
  }
65
83
  clearTimeout(timeoutId);
84
+ if (response.searchable) {
85
+ setOptions([]);
86
+ setStatus('searchable');
87
+ return;
88
+ }
66
89
  if (response.options.length > 0) {
67
90
  setOptions(response.options);
68
- setStatus('ready');
91
+ setStatus('static');
69
92
  } else {
70
93
  setOptions([]);
71
94
  setStatus('fallback');
72
95
  }
73
96
  });
74
97
 
75
- emitLookupOptionsRequest({ requestId, sourceKey });
98
+ emitLookupOptionsRequest({ requestId, sourceKey: normalizedSourceKey });
76
99
 
77
100
  return () => {
78
101
  clearTimeout(timeoutId);
@@ -80,5 +103,45 @@ export const useLookupOptions = (sourceKey: string | undefined): UseLookupOption
80
103
  };
81
104
  }, [sourceKey]);
82
105
 
83
- return { status, options };
106
+ const requestOptions = useCallback((payload: { query?: string; keys?: string[] }) => {
107
+ const normalizedSourceKey = sourceKeyRef.current?.trim();
108
+ if (!normalizedSourceKey) {
109
+ return Promise.resolve<LookupOption[]>([]);
110
+ }
111
+
112
+ return new Promise<LookupOption[]>(resolve => {
113
+ const requestId = generateId();
114
+ let settled = false;
115
+
116
+ const finish = (result: LookupOption[]) => {
117
+ if (settled) {
118
+ return;
119
+ }
120
+ settled = true;
121
+ clearTimeout(timeoutId);
122
+ unsubscribe();
123
+ resolve(result);
124
+ };
125
+
126
+ const timeoutId = setTimeout(() => finish([]), LOOKUP_OPTIONS_TIMEOUT_MS);
127
+
128
+ const unsubscribe = onLookupOptionsResponse(response => {
129
+ if (response.requestId !== requestId) {
130
+ return;
131
+ }
132
+ finish(response.options);
133
+ });
134
+
135
+ emitLookupOptionsRequest({ requestId, sourceKey: normalizedSourceKey, ...payload });
136
+ });
137
+ }, []);
138
+
139
+ const loadOptions = useCallback((query: string) => requestOptions({ query }), [requestOptions]);
140
+
141
+ const resolveOptions = useCallback(
142
+ (keys: string[]) => requestOptions({ keys }),
143
+ [requestOptions],
144
+ );
145
+
146
+ return { status, options, loadOptions, resolveOptions };
84
147
  };