@warkypublic/svelix 0.1.36 → 0.1.37
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/dist/components/Boxer/Boxer.svelte +192 -45
- package/dist/components/Boxer/Boxer.svelte.d.ts +1 -0
- package/dist/components/Boxer/BoxerBaseAdapter.d.ts +31 -0
- package/dist/components/Boxer/BoxerBaseAdapter.js +83 -0
- package/dist/components/Boxer/BoxerResolveSpecAdapter.d.ts +5 -14
- package/dist/components/Boxer/BoxerResolveSpecAdapter.js +6 -49
- package/dist/components/Boxer/BoxerRestHeaderSpecAdapter.d.ts +5 -14
- package/dist/components/Boxer/BoxerRestHeaderSpecAdapter.js +6 -49
- package/dist/components/Boxer/index.d.ts +1 -0
- package/dist/components/Boxer/index.js +1 -0
- package/dist/components/Boxer/store.d.ts +10 -1
- package/dist/components/Boxer/store.js +135 -43
- package/dist/components/Boxer/types.d.ts +17 -10
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +10 -7
- package/dist/components/GlobalStateStore/GlobalStateStore.js +21 -6
- package/dist/components/GlobalStateStore/GlobalStateStore.utils.d.ts +1 -1
- package/dist/components/GlobalStateStore/GlobalStateStore.utils.js +3 -1
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +25 -9
- package/dist/components/Gridler/components/Gridler.svelte +1 -2
- package/dist/components/Screenshot/Screenshot.util.js +5 -4
- package/package.json +30 -30
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
clearable = true,
|
|
19
19
|
data = [],
|
|
20
20
|
dataSource: dataSourceProp = undefined,
|
|
21
|
+
debounceMs = 300,
|
|
21
22
|
disablePortal = false,
|
|
22
23
|
disabled,
|
|
23
24
|
error,
|
|
@@ -28,10 +29,12 @@
|
|
|
28
29
|
onAPICall: onAPICallProp = undefined,
|
|
29
30
|
onBufferChange,
|
|
30
31
|
onChange,
|
|
32
|
+
onResolveValues: onResolveValuesProp = undefined,
|
|
31
33
|
openOnClear,
|
|
32
34
|
pageSize = 50,
|
|
33
35
|
placeholder,
|
|
34
36
|
rightSection,
|
|
37
|
+
searchColumns,
|
|
35
38
|
selectFirst,
|
|
36
39
|
showAll,
|
|
37
40
|
value = $bindable<any>(undefined),
|
|
@@ -41,13 +44,23 @@
|
|
|
41
44
|
const inputId = `${instanceId}-input`;
|
|
42
45
|
const listboxId = `${instanceId}-listbox`;
|
|
43
46
|
|
|
44
|
-
// Derive effective dataSource
|
|
47
|
+
// Derive effective dataSource, onAPICall, resolver from adapter when not explicitly provided.
|
|
45
48
|
const dataSource = dataSourceProp ?? (adapter ? "server" : "local");
|
|
46
49
|
const onAPICall =
|
|
47
50
|
onAPICallProp ??
|
|
48
51
|
(adapter
|
|
49
|
-
? (params: {
|
|
50
|
-
|
|
52
|
+
? (params: {
|
|
53
|
+
page: number;
|
|
54
|
+
pageSize: number;
|
|
55
|
+
search?: string;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}) => adapter!.fetch(params)
|
|
58
|
+
: undefined);
|
|
59
|
+
const onResolveValues =
|
|
60
|
+
onResolveValuesProp ??
|
|
61
|
+
(adapter?.resolveByValue
|
|
62
|
+
? (values: any[], signal?: AbortSignal) =>
|
|
63
|
+
adapter!.resolveByValue!(values, signal)
|
|
51
64
|
: undefined);
|
|
52
65
|
|
|
53
66
|
// Create store once with initial props
|
|
@@ -63,9 +76,11 @@
|
|
|
63
76
|
onAPICall,
|
|
64
77
|
onBufferChange,
|
|
65
78
|
onChange,
|
|
79
|
+
onResolveValues,
|
|
66
80
|
openOnClear,
|
|
67
81
|
pageSize,
|
|
68
82
|
placeholder,
|
|
83
|
+
searchColumns,
|
|
69
84
|
selectFirst,
|
|
70
85
|
showAll,
|
|
71
86
|
value,
|
|
@@ -76,6 +91,7 @@
|
|
|
76
91
|
const storeSearch = $derived($store.search);
|
|
77
92
|
const storeOpened = $derived($store.opened);
|
|
78
93
|
const storeBoxerData = $derived($store.boxerData);
|
|
94
|
+
const storeSelectedItems = $derived($store.selectedItems);
|
|
79
95
|
const storeInput = $derived($store.input);
|
|
80
96
|
const boxerDataLength = $derived($store.boxerData.length);
|
|
81
97
|
|
|
@@ -145,62 +161,141 @@
|
|
|
145
161
|
}
|
|
146
162
|
});
|
|
147
163
|
|
|
148
|
-
// Debounced search — only reacts to actual search text changes
|
|
164
|
+
// Debounced search — only reacts to actual search text changes.
|
|
149
165
|
let searchTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
166
|
+
let didInitialFetch = false;
|
|
150
167
|
|
|
151
168
|
$effect(() => {
|
|
152
169
|
const search = storeSearch;
|
|
170
|
+
// Skip the initial tick: the initial-fetch effect below handles first load.
|
|
171
|
+
if (!didInitialFetch) return;
|
|
153
172
|
clearTimeout(searchTimeout);
|
|
154
173
|
searchTimeout = setTimeout(() => {
|
|
155
174
|
if (search !== undefined && storeOpened) {
|
|
156
175
|
store.fetchData(search, true);
|
|
176
|
+
parentEl?.scrollTo?.({ top: 0 });
|
|
157
177
|
}
|
|
158
|
-
},
|
|
178
|
+
}, debounceMs);
|
|
159
179
|
return () => clearTimeout(searchTimeout);
|
|
160
180
|
});
|
|
161
181
|
|
|
162
|
-
// Initial data fetch
|
|
182
|
+
// Initial data fetch + resolve labels for preloaded value(s).
|
|
163
183
|
$effect(() => {
|
|
184
|
+
didInitialFetch = true;
|
|
164
185
|
store.fetchData("", true);
|
|
186
|
+
resolvePreloadedValue(value);
|
|
187
|
+
return () => {
|
|
188
|
+
clearTimeout(searchTimeout);
|
|
189
|
+
store.cancel();
|
|
190
|
+
};
|
|
165
191
|
});
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
let lastResolvedKey: string | null = null;
|
|
194
|
+
async function resolvePreloadedValue(val: unknown) {
|
|
195
|
+
if (val == null || val === "" || (Array.isArray(val) && val.length === 0)) return;
|
|
196
|
+
if (!onResolveValues) return;
|
|
197
|
+
|
|
198
|
+
const values = Array.isArray(val) ? val : [val];
|
|
199
|
+
const key = JSON.stringify(values);
|
|
200
|
+
if (key === lastResolvedKey) return;
|
|
201
|
+
lastResolvedKey = key;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const items = await onResolveValues(values);
|
|
205
|
+
if (!items || items.length === 0) return;
|
|
206
|
+
if (multiSelect) {
|
|
207
|
+
store.setSelectedItems(items);
|
|
208
|
+
} else {
|
|
209
|
+
store.setSelectedItems(items.slice(0, 1));
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("Boxer resolveByValue error:", err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// When value prop changes from outside, resolve any new labels we don't yet have.
|
|
168
217
|
$effect(() => {
|
|
169
|
-
|
|
218
|
+
void value;
|
|
219
|
+
untrack(() => {
|
|
220
|
+
if (value == null) {
|
|
221
|
+
store.clearSelectedItems();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const knownKeys = new Set(
|
|
225
|
+
$store.selectedItems.map((i) => JSON.stringify(i.value)),
|
|
226
|
+
);
|
|
227
|
+
const needed = (Array.isArray(value) ? value : [value]).filter(
|
|
228
|
+
(v) => !knownKeys.has(JSON.stringify(v)),
|
|
229
|
+
);
|
|
230
|
+
if (needed.length === 0) {
|
|
231
|
+
if (!multiSelect) {
|
|
232
|
+
// Keep the single-item list in sync with the chosen value.
|
|
233
|
+
const match = $store.selectedItems.find(
|
|
234
|
+
(i) => JSON.stringify(i.value) === JSON.stringify(value),
|
|
235
|
+
);
|
|
236
|
+
if (match) store.setSelectedItems([match]);
|
|
237
|
+
} else {
|
|
238
|
+
// Drop any selected items whose value is no longer in `value`.
|
|
239
|
+
const keep = new Set((value as any[]).map((v) => JSON.stringify(v)));
|
|
240
|
+
const filtered = $store.selectedItems.filter((i) =>
|
|
241
|
+
keep.has(JSON.stringify(i.value)),
|
|
242
|
+
);
|
|
243
|
+
if (filtered.length !== $store.selectedItems.length) {
|
|
244
|
+
store.setSelectedItems(filtered);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Fall back to boxerData, then to the resolver.
|
|
250
|
+
const fromBoxer = $store.boxerData.filter((i) =>
|
|
251
|
+
needed.some((v) => JSON.stringify(v) === JSON.stringify(i.value)),
|
|
252
|
+
);
|
|
253
|
+
fromBoxer.forEach((i) => store.addSelectedItem(i));
|
|
254
|
+
const stillMissing = needed.filter(
|
|
255
|
+
(v) => !fromBoxer.some((i) => JSON.stringify(i.value) === JSON.stringify(v)),
|
|
256
|
+
);
|
|
257
|
+
if (stillMissing.length > 0) resolvePreloadedValue(stillMissing);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Sync displayed input + emit onBufferChange from selectedItems (single source of truth).
|
|
262
|
+
$effect(() => {
|
|
263
|
+
const selected = storeSelectedItems;
|
|
170
264
|
const opened = storeOpened;
|
|
171
265
|
const currentInput = storeInput;
|
|
172
266
|
|
|
173
267
|
if (multiSelect) {
|
|
174
|
-
const labels =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Array.isArray(value) && value.includes(item.value),
|
|
178
|
-
)
|
|
179
|
-
.map((item: BoxerItem) => item.label)
|
|
180
|
-
.join(", ");
|
|
181
|
-
if (!opened && currentInput !== labels) {
|
|
182
|
-
store.setInput(labels);
|
|
183
|
-
}
|
|
268
|
+
const labels = selected.map((i) => i.label).join(", ");
|
|
269
|
+
if (!opened && currentInput !== labels) store.setInput(labels);
|
|
270
|
+
onBufferChange?.(selected);
|
|
184
271
|
} else {
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
store.setInput(
|
|
188
|
-
} else if (!
|
|
272
|
+
const item = selected[0] ?? null;
|
|
273
|
+
if (item && currentInput !== item.label && !opened) {
|
|
274
|
+
store.setInput(item.label);
|
|
275
|
+
} else if (!item && !opened && currentInput !== "") {
|
|
189
276
|
store.setInput("");
|
|
190
277
|
}
|
|
278
|
+
onBufferChange?.(item);
|
|
191
279
|
}
|
|
280
|
+
});
|
|
192
281
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
282
|
+
// When data arrives, opportunistically enrich selectedItems for multiselect
|
|
283
|
+
// (adds labels for items that just loaded).
|
|
284
|
+
$effect(() => {
|
|
285
|
+
const boxerData = storeBoxerData;
|
|
286
|
+
if (!multiSelect || !Array.isArray(value) || value.length === 0) return;
|
|
287
|
+
untrack(() => {
|
|
288
|
+
const known = new Set(
|
|
289
|
+
$store.selectedItems.map((i) => JSON.stringify(i.value)),
|
|
197
290
|
);
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
for (const v of value as any[]) {
|
|
292
|
+
if (known.has(JSON.stringify(v))) continue;
|
|
293
|
+
const match = boxerData.find(
|
|
294
|
+
(i) => JSON.stringify(i.value) === JSON.stringify(v),
|
|
295
|
+
);
|
|
296
|
+
if (match) store.addSelectedItem(match);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
204
299
|
});
|
|
205
300
|
|
|
206
301
|
// Select first
|
|
@@ -220,17 +315,17 @@
|
|
|
220
315
|
if (multiSelect) {
|
|
221
316
|
const currentValues = Array.isArray(value) ? value : [];
|
|
222
317
|
const isSelected = currentValues.includes(option.value);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
onBufferChange?.(newBuffer);
|
|
318
|
+
if (isSelected) {
|
|
319
|
+
value = currentValues.filter((v: unknown) => v !== option.value);
|
|
320
|
+
store.removeSelectedItem(option.value);
|
|
321
|
+
} else {
|
|
322
|
+
value = [...currentValues, option.value];
|
|
323
|
+
store.addSelectedItem(option);
|
|
324
|
+
}
|
|
325
|
+
onChange?.(value);
|
|
232
326
|
} else {
|
|
233
327
|
value = option.value;
|
|
328
|
+
store.setSelectedItems([option]);
|
|
234
329
|
onChange?.(option.value);
|
|
235
330
|
store.setSearch("");
|
|
236
331
|
store.setInput(option.label);
|
|
@@ -244,6 +339,7 @@
|
|
|
244
339
|
onOptionSubmit(0);
|
|
245
340
|
} else {
|
|
246
341
|
value = multiSelect ? [] : null;
|
|
342
|
+
store.clearSelectedItems();
|
|
247
343
|
onChange?.(value);
|
|
248
344
|
store.setSearch("");
|
|
249
345
|
store.setInput("");
|
|
@@ -256,8 +352,11 @@
|
|
|
256
352
|
}
|
|
257
353
|
|
|
258
354
|
function focusDropdownItem(index: number) {
|
|
259
|
-
const
|
|
355
|
+
const length = $store.boxerData.length;
|
|
356
|
+
if (length === 0) return;
|
|
357
|
+
const clamped = Math.max(0, Math.min(index, length - 1));
|
|
260
358
|
activeOptionIndex = clamped;
|
|
359
|
+
rawVirtualizer?.scrollToIndex(clamped, { align: "auto" });
|
|
261
360
|
requestAnimationFrame(() => {
|
|
262
361
|
const el = parentEl?.querySelector<HTMLDivElement>(
|
|
263
362
|
`[data-index="${clamped}"]`,
|
|
@@ -314,10 +413,13 @@
|
|
|
314
413
|
}
|
|
315
414
|
|
|
316
415
|
function onDropdownItemKeydown(e: KeyboardEvent, index: number) {
|
|
416
|
+
const length = $store.boxerData.length;
|
|
417
|
+
const pageSizeNav = Math.max(1, Math.floor(mah / 36));
|
|
418
|
+
|
|
317
419
|
switch (e.key) {
|
|
318
420
|
case "ArrowDown":
|
|
319
421
|
e.preventDefault();
|
|
320
|
-
if (index <
|
|
422
|
+
if (index < length - 1) focusDropdownItem(index + 1);
|
|
321
423
|
break;
|
|
322
424
|
case "ArrowUp":
|
|
323
425
|
e.preventDefault();
|
|
@@ -328,6 +430,22 @@
|
|
|
328
430
|
focusDropdownItem(index - 1);
|
|
329
431
|
}
|
|
330
432
|
break;
|
|
433
|
+
case "Home":
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
focusDropdownItem(0);
|
|
436
|
+
break;
|
|
437
|
+
case "End":
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
focusDropdownItem(length - 1);
|
|
440
|
+
break;
|
|
441
|
+
case "PageDown":
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
focusDropdownItem(Math.min(length - 1, index + pageSizeNav));
|
|
444
|
+
break;
|
|
445
|
+
case "PageUp":
|
|
446
|
+
e.preventDefault();
|
|
447
|
+
focusDropdownItem(Math.max(0, index - pageSizeNav));
|
|
448
|
+
break;
|
|
331
449
|
case "Enter":
|
|
332
450
|
case " ":
|
|
333
451
|
e.preventDefault();
|
|
@@ -402,9 +520,13 @@
|
|
|
402
520
|
return value;
|
|
403
521
|
}
|
|
404
522
|
export function open() {
|
|
523
|
+
if (disabled) return;
|
|
405
524
|
store.setOpened(true);
|
|
406
525
|
activeOptionIndex = null;
|
|
407
526
|
}
|
|
527
|
+
export function refetch() {
|
|
528
|
+
return store.refetch();
|
|
529
|
+
}
|
|
408
530
|
export function setValue(val: unknown) {
|
|
409
531
|
value = val;
|
|
410
532
|
onChange?.(val);
|
|
@@ -504,7 +626,7 @@
|
|
|
504
626
|
{clearable}
|
|
505
627
|
controlsId={listboxId}
|
|
506
628
|
{disabled}
|
|
507
|
-
{error}
|
|
629
|
+
error={error ?? $store.error ?? undefined}
|
|
508
630
|
expanded={$store.opened}
|
|
509
631
|
{inputId}
|
|
510
632
|
{label}
|
|
@@ -555,7 +677,23 @@
|
|
|
555
677
|
style:left={!effectiveDisablePortal ? `${popupLeft}px` : undefined}
|
|
556
678
|
style:width={!effectiveDisablePortal ? `${popupWidth}px` : undefined}
|
|
557
679
|
>
|
|
558
|
-
{#if $store.
|
|
680
|
+
{#if $store.error}
|
|
681
|
+
<div
|
|
682
|
+
aria-live="polite"
|
|
683
|
+
class="px-3 py-2 text-sm text-error-500 flex items-center justify-between gap-2"
|
|
684
|
+
data-testid="boxer-error"
|
|
685
|
+
role="alert"
|
|
686
|
+
>
|
|
687
|
+
<span>{$store.error}</span>
|
|
688
|
+
<button
|
|
689
|
+
class="btn btn-sm preset-tonal"
|
|
690
|
+
type="button"
|
|
691
|
+
onclick={() => store.refetch()}
|
|
692
|
+
>
|
|
693
|
+
retry
|
|
694
|
+
</button>
|
|
695
|
+
</div>
|
|
696
|
+
{:else if $store.boxerData.length > 0}
|
|
559
697
|
<div
|
|
560
698
|
bind:this={parentEl}
|
|
561
699
|
class="overflow-auto"
|
|
@@ -603,6 +741,15 @@
|
|
|
603
741
|
</div>
|
|
604
742
|
</div>
|
|
605
743
|
</div>
|
|
744
|
+
{:else if $store.isFetching}
|
|
745
|
+
<div
|
|
746
|
+
aria-live="polite"
|
|
747
|
+
class="px-3 py-2 text-sm text-surface-500"
|
|
748
|
+
data-testid="boxer-loading"
|
|
749
|
+
role="status"
|
|
750
|
+
>
|
|
751
|
+
loading...
|
|
752
|
+
</div>
|
|
606
753
|
{:else}
|
|
607
754
|
<div
|
|
608
755
|
aria-live="polite"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Options } from '@warkypublic/resolvespec-js';
|
|
2
|
+
import type { BoxerAdapterConfig, BoxerFetchParams, BoxerItem, BoxerServerAdapter } from './types.js';
|
|
3
|
+
type ReadFn = (schema: string, entity: string, id: undefined, options: Options) => Promise<any>;
|
|
4
|
+
interface ResolvedAdapterConfig extends BoxerAdapterConfig {
|
|
5
|
+
labelField: string;
|
|
6
|
+
valueField: string;
|
|
7
|
+
searchOperator: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Shared implementation for server-backed adapters. Subclasses plug in a
|
|
11
|
+
* concrete resolvespec client by providing a `read` function.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class BoxerBaseAdapter implements BoxerServerAdapter {
|
|
14
|
+
protected readonly config: ResolvedAdapterConfig;
|
|
15
|
+
constructor(config: BoxerAdapterConfig);
|
|
16
|
+
protected abstract read: ReadFn;
|
|
17
|
+
fetch({ page, pageSize, search, signal }: BoxerFetchParams): Promise<{
|
|
18
|
+
data: BoxerItem[];
|
|
19
|
+
total: number;
|
|
20
|
+
}>;
|
|
21
|
+
resolveByValue(values: any[], signal?: AbortSignal): Promise<BoxerItem[]>;
|
|
22
|
+
private resolveColumns;
|
|
23
|
+
/**
|
|
24
|
+
* Each search column is emitted with `logic_operator: 'OR'` so the backend
|
|
25
|
+
* treats them as a single OR group, AND-combined with any base filters.
|
|
26
|
+
* This matches the convention used elsewhere in the project (see SvarkGrid).
|
|
27
|
+
*/
|
|
28
|
+
private buildSearchFilters;
|
|
29
|
+
private mapRow;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared implementation for server-backed adapters. Subclasses plug in a
|
|
3
|
+
* concrete resolvespec client by providing a `read` function.
|
|
4
|
+
*/
|
|
5
|
+
export class BoxerBaseAdapter {
|
|
6
|
+
config;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = {
|
|
9
|
+
labelField: 'label',
|
|
10
|
+
valueField: 'id',
|
|
11
|
+
searchOperator: 'ilike',
|
|
12
|
+
...config,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async fetch({ page, pageSize, search, signal }) {
|
|
16
|
+
signal?.throwIfAborted?.();
|
|
17
|
+
const { schema, entity, labelField, valueField, columns, sort, filters, searchColumns, searchOperator, mapItem } = this.config;
|
|
18
|
+
const cols = this.resolveColumns(columns, labelField, valueField);
|
|
19
|
+
const searchFilters = this.buildSearchFilters(search, labelField, searchColumns, searchOperator);
|
|
20
|
+
const options = {
|
|
21
|
+
columns: cols,
|
|
22
|
+
sort: sort ?? [],
|
|
23
|
+
filters: [...(filters ?? []), ...searchFilters],
|
|
24
|
+
limit: pageSize,
|
|
25
|
+
offset: page * pageSize,
|
|
26
|
+
};
|
|
27
|
+
const response = await this.read(schema, entity, undefined, options);
|
|
28
|
+
signal?.throwIfAborted?.();
|
|
29
|
+
const rows = Array.isArray(response?.data) ? response.data : (Array.isArray(response) ? response : []);
|
|
30
|
+
const total = response?.metadata?.total ?? rows.length;
|
|
31
|
+
return {
|
|
32
|
+
data: rows.map((row) => this.mapRow(row, labelField, valueField, mapItem)),
|
|
33
|
+
total,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async resolveByValue(values, signal) {
|
|
37
|
+
if (!values || values.length === 0)
|
|
38
|
+
return [];
|
|
39
|
+
signal?.throwIfAborted?.();
|
|
40
|
+
const { schema, entity, labelField, valueField, columns, filters, mapItem } = this.config;
|
|
41
|
+
const cols = this.resolveColumns(columns, labelField, valueField);
|
|
42
|
+
const response = await this.read(schema, entity, undefined, {
|
|
43
|
+
columns: cols,
|
|
44
|
+
filters: [
|
|
45
|
+
...(filters ?? []),
|
|
46
|
+
{ column: valueField, operator: 'in', value: values },
|
|
47
|
+
],
|
|
48
|
+
limit: values.length,
|
|
49
|
+
offset: 0,
|
|
50
|
+
});
|
|
51
|
+
signal?.throwIfAborted?.();
|
|
52
|
+
const rows = Array.isArray(response?.data) ? response.data : (Array.isArray(response) ? response : []);
|
|
53
|
+
return rows.map((row) => this.mapRow(row, labelField, valueField, mapItem));
|
|
54
|
+
}
|
|
55
|
+
resolveColumns(columns, labelField, valueField) {
|
|
56
|
+
if (!columns)
|
|
57
|
+
return undefined;
|
|
58
|
+
return [...new Set([...columns, labelField, valueField])];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Each search column is emitted with `logic_operator: 'OR'` so the backend
|
|
62
|
+
* treats them as a single OR group, AND-combined with any base filters.
|
|
63
|
+
* This matches the convention used elsewhere in the project (see SvarkGrid).
|
|
64
|
+
*/
|
|
65
|
+
buildSearchFilters(search, labelField, searchColumns, searchOperator) {
|
|
66
|
+
const trimmed = search?.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return [];
|
|
69
|
+
const cols = searchColumns?.length ? searchColumns : [labelField];
|
|
70
|
+
const value = searchOperator === 'ilike' || searchOperator === 'like' ? `%${trimmed}%` : trimmed;
|
|
71
|
+
return cols.map((column) => ({
|
|
72
|
+
column,
|
|
73
|
+
operator: searchOperator,
|
|
74
|
+
value,
|
|
75
|
+
logic_operator: 'OR',
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
mapRow(row, labelField, valueField, mapItem) {
|
|
79
|
+
if (mapItem)
|
|
80
|
+
return mapItem(row);
|
|
81
|
+
return { ...row, label: String(row[labelField] ?? ''), value: row[valueField] };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { Options } from '@warkypublic/resolvespec-js';
|
|
2
|
+
import { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
3
|
+
import type { BoxerAdapterConfig } from './types.js';
|
|
4
|
+
export declare class BoxerResolveSpecAdapter extends BoxerBaseAdapter {
|
|
3
5
|
private readonly client;
|
|
4
|
-
private readonly config;
|
|
5
6
|
constructor(config: BoxerAdapterConfig);
|
|
6
|
-
|
|
7
|
-
page: number;
|
|
8
|
-
pageSize: number;
|
|
9
|
-
search?: string;
|
|
10
|
-
}): Promise<{
|
|
11
|
-
data: BoxerItem[];
|
|
12
|
-
total: number;
|
|
13
|
-
}>;
|
|
14
|
-
private resolveColumns;
|
|
15
|
-
private buildSearchFilters;
|
|
16
|
-
private mapRow;
|
|
7
|
+
protected read: (schema: string, entity: string, id: undefined, options: Options) => Promise<any>;
|
|
17
8
|
}
|
|
@@ -1,56 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { ResolveSpecClient } from '@warkypublic/resolvespec-js';
|
|
3
|
-
|
|
3
|
+
import { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
4
|
+
export class BoxerResolveSpecAdapter extends BoxerBaseAdapter {
|
|
4
5
|
client;
|
|
5
|
-
config;
|
|
6
6
|
constructor(config) {
|
|
7
|
+
super(config);
|
|
7
8
|
this.client = new ResolveSpecClient({ baseUrl: config.baseUrl, token: config.token });
|
|
8
|
-
this.config = {
|
|
9
|
-
labelField: 'label',
|
|
10
|
-
valueField: 'id',
|
|
11
|
-
searchOperator: 'ilike',
|
|
12
|
-
...config,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
async fetch({ page, pageSize, search }) {
|
|
16
|
-
const { schema, entity, labelField, valueField, columns, sort, filters, searchColumns, searchOperator, mapItem } = this.config;
|
|
17
|
-
const cols = this.resolveColumns(columns, labelField, valueField);
|
|
18
|
-
const searchFilters = this.buildSearchFilters(search, labelField, searchColumns, searchOperator);
|
|
19
|
-
const options = {
|
|
20
|
-
columns: cols,
|
|
21
|
-
sort: sort ?? [],
|
|
22
|
-
filters: [...(filters ?? []), ...searchFilters],
|
|
23
|
-
limit: pageSize,
|
|
24
|
-
offset: page * pageSize,
|
|
25
|
-
};
|
|
26
|
-
const response = await this.client.read(schema, entity, undefined, options);
|
|
27
|
-
const rows = Array.isArray(response?.data) ? response.data : (Array.isArray(response) ? response : []);
|
|
28
|
-
const total = response?.metadata?.total ?? rows.length;
|
|
29
|
-
return {
|
|
30
|
-
data: rows.map((row) => this.mapRow(row, labelField, valueField, mapItem)),
|
|
31
|
-
total,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
resolveColumns(columns, labelField, valueField) {
|
|
35
|
-
if (!columns)
|
|
36
|
-
return undefined;
|
|
37
|
-
return [...new Set([...columns, labelField, valueField])];
|
|
38
|
-
}
|
|
39
|
-
buildSearchFilters(search, labelField, searchColumns, searchOperator) {
|
|
40
|
-
const trimmed = search?.trim();
|
|
41
|
-
if (!trimmed)
|
|
42
|
-
return [];
|
|
43
|
-
const cols = searchColumns?.length ? searchColumns : [labelField];
|
|
44
|
-
return cols.map((column, index) => ({
|
|
45
|
-
column,
|
|
46
|
-
operator: searchOperator,
|
|
47
|
-
value: `%${trimmed}%`,
|
|
48
|
-
logic_operator: index === 0 ? 'AND' : 'OR',
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
mapRow(row, labelField, valueField, mapItem) {
|
|
52
|
-
if (mapItem)
|
|
53
|
-
return mapItem(row);
|
|
54
|
-
return { ...row, label: String(row[labelField] ?? ''), value: row[valueField] };
|
|
55
9
|
}
|
|
10
|
+
read = (schema, entity, id, options) => {
|
|
11
|
+
return this.client.read(schema, entity, id, options);
|
|
12
|
+
};
|
|
56
13
|
}
|
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { Options } from '@warkypublic/resolvespec-js';
|
|
2
|
+
import { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
3
|
+
import type { BoxerAdapterConfig } from './types.js';
|
|
4
|
+
export declare class BoxerRestHeaderSpecAdapter extends BoxerBaseAdapter {
|
|
3
5
|
private readonly client;
|
|
4
|
-
private readonly config;
|
|
5
6
|
constructor(config: BoxerAdapterConfig);
|
|
6
|
-
|
|
7
|
-
page: number;
|
|
8
|
-
pageSize: number;
|
|
9
|
-
search?: string;
|
|
10
|
-
}): Promise<{
|
|
11
|
-
data: BoxerItem[];
|
|
12
|
-
total: number;
|
|
13
|
-
}>;
|
|
14
|
-
private resolveColumns;
|
|
15
|
-
private buildSearchFilters;
|
|
16
|
-
private mapRow;
|
|
7
|
+
protected read: (schema: string, entity: string, id: undefined, options: Options) => Promise<any>;
|
|
17
8
|
}
|
|
@@ -1,56 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { HeaderSpecClient } from '@warkypublic/resolvespec-js';
|
|
3
|
-
|
|
3
|
+
import { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
4
|
+
export class BoxerRestHeaderSpecAdapter extends BoxerBaseAdapter {
|
|
4
5
|
client;
|
|
5
|
-
config;
|
|
6
6
|
constructor(config) {
|
|
7
|
+
super(config);
|
|
7
8
|
this.client = new HeaderSpecClient({ baseUrl: config.baseUrl, token: config.token });
|
|
8
|
-
this.config = {
|
|
9
|
-
labelField: 'label',
|
|
10
|
-
valueField: 'id',
|
|
11
|
-
searchOperator: 'ilike',
|
|
12
|
-
...config,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
async fetch({ page, pageSize, search }) {
|
|
16
|
-
const { schema, entity, labelField, valueField, columns, sort, filters, searchColumns, searchOperator, mapItem } = this.config;
|
|
17
|
-
const cols = this.resolveColumns(columns, labelField, valueField);
|
|
18
|
-
const searchFilters = this.buildSearchFilters(search, labelField, searchColumns, searchOperator);
|
|
19
|
-
const options = {
|
|
20
|
-
columns: cols,
|
|
21
|
-
sort: sort ?? [],
|
|
22
|
-
filters: [...(filters ?? []), ...searchFilters],
|
|
23
|
-
limit: pageSize,
|
|
24
|
-
offset: page * pageSize,
|
|
25
|
-
};
|
|
26
|
-
const response = await this.client.read(schema, entity, undefined, options);
|
|
27
|
-
const rows = Array.isArray(response?.data) ? response.data : (Array.isArray(response) ? response : []);
|
|
28
|
-
const total = response?.metadata?.total ?? rows.length;
|
|
29
|
-
return {
|
|
30
|
-
data: rows.map((row) => this.mapRow(row, labelField, valueField, mapItem)),
|
|
31
|
-
total,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
resolveColumns(columns, labelField, valueField) {
|
|
35
|
-
if (!columns)
|
|
36
|
-
return undefined;
|
|
37
|
-
return [...new Set([...columns, labelField, valueField])];
|
|
38
|
-
}
|
|
39
|
-
buildSearchFilters(search, labelField, searchColumns, searchOperator) {
|
|
40
|
-
const trimmed = search?.trim();
|
|
41
|
-
if (!trimmed)
|
|
42
|
-
return [];
|
|
43
|
-
const cols = searchColumns?.length ? searchColumns : [labelField];
|
|
44
|
-
return cols.map((column, index) => ({
|
|
45
|
-
column,
|
|
46
|
-
operator: searchOperator,
|
|
47
|
-
value: `%${trimmed}%`,
|
|
48
|
-
logic_operator: index === 0 ? 'AND' : 'OR',
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
mapRow(row, labelField, valueField, mapItem) {
|
|
52
|
-
if (mapItem)
|
|
53
|
-
return mapItem(row);
|
|
54
|
-
return { ...row, label: String(row[labelField] ?? ''), value: row[valueField] };
|
|
55
9
|
}
|
|
10
|
+
read = (schema, entity, id, options) => {
|
|
11
|
+
return this.client.read(schema, entity, id, options);
|
|
12
|
+
};
|
|
56
13
|
}
|