runeforge 0.0.21 → 0.0.23
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/crud/Field.svelte +1 -0
- package/dist/components/crud/GenericCRUD.svelte +10 -3
- package/dist/components/crud/utils/resolution.d.ts +1 -0
- package/dist/components/crud/utils/resolution.js +5 -1
- package/dist/components/form/Select.svelte +63 -14
- package/dist/components/form/Select.svelte.d.ts +3 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/es.js +1 -0
- package/dist/i18n/types.d.ts +1 -0
- package/dist/types/attribute.d.ts +8 -0
- package/dist/types/crud.d.ts +8 -2
- package/package.json +1 -1
|
@@ -121,6 +121,15 @@
|
|
|
121
121
|
|
|
122
122
|
let activeAction = $state<{ action: CustomAction<T>; item: T } | null>(null);
|
|
123
123
|
|
|
124
|
+
async function runAction(action: CustomAction<T>, item: T) {
|
|
125
|
+
if (!(action.condition?.(item) ?? true)) return;
|
|
126
|
+
if (action.href) {
|
|
127
|
+
await goto(action.href(item));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
activeAction = { action, item };
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
async function navList() {
|
|
125
134
|
await goto('?');
|
|
126
135
|
}
|
|
@@ -322,9 +331,7 @@
|
|
|
322
331
|
onCreate={navCreate}
|
|
323
332
|
onEdit={navEdit}
|
|
324
333
|
onView={navRead}
|
|
325
|
-
onAction={
|
|
326
|
-
if (action.condition?.(item) ?? true) activeAction = { action, item };
|
|
327
|
-
}}
|
|
334
|
+
onAction={runAction}
|
|
328
335
|
/>
|
|
329
336
|
{/if}
|
|
330
337
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AttributeMetadata, AttributeType, SelectOption } from '../../../types/attribute.js';
|
|
2
2
|
import type { FieldDefinition } from '../../../types/crud.js';
|
|
3
3
|
export declare function resolveOptions(m: AttributeMetadata, d: unknown): SelectOption[] | undefined;
|
|
4
|
+
export declare function resolveDefault(m: AttributeMetadata, d: unknown): unknown;
|
|
4
5
|
export declare function resolveFormatter(m: AttributeMetadata, d: unknown): import("../../../index.ts").CellFormatter<any, any> | undefined;
|
|
5
6
|
export declare function inferType(key: string, value: unknown): AttributeType;
|
|
6
7
|
export declare function buildFieldDefinitions<T extends object = Record<string, unknown>>(meta: Partial<Record<string, AttributeMetadata>>, data: unknown, excludedFlag: 'excludedFromCreate' | 'excludedFromRead' | 'excludedFromUpdate', excluded: Set<string>): FieldDefinition<T>[];
|
|
@@ -3,6 +3,9 @@ export function resolveOptions(m, d) {
|
|
|
3
3
|
return undefined;
|
|
4
4
|
return typeof m.options === 'function' ? m.options(d) : m.options;
|
|
5
5
|
}
|
|
6
|
+
export function resolveDefault(m, d) {
|
|
7
|
+
return typeof m.default === 'function' ? m.default(d) : m.default;
|
|
8
|
+
}
|
|
6
9
|
export function resolveFormatter(m, d) {
|
|
7
10
|
return m.formatter?.(d);
|
|
8
11
|
}
|
|
@@ -34,11 +37,12 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
34
37
|
required: m.required,
|
|
35
38
|
autocomplete: m.autocomplete,
|
|
36
39
|
placeholder: m.placeholder,
|
|
37
|
-
default: m
|
|
40
|
+
default: resolveDefault(m, data),
|
|
38
41
|
options: resolveOptions(m, data),
|
|
39
42
|
dependentOptions: m.dependentOptions
|
|
40
43
|
? (record) => m.dependentOptions(data, record)
|
|
41
44
|
: undefined,
|
|
45
|
+
search: m.search,
|
|
42
46
|
disabled: m.disabled,
|
|
43
47
|
seed: m.seed,
|
|
44
48
|
groupedAs: m.groupedAs,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getStrings } from '../../i18n/context.js';
|
|
3
|
+
import type { SearchResolver } from '../../types/attribute.js';
|
|
3
4
|
|
|
4
5
|
const strings = getStrings();
|
|
5
6
|
|
|
@@ -7,6 +8,8 @@
|
|
|
7
8
|
name,
|
|
8
9
|
value = $bindable(''),
|
|
9
10
|
options = [],
|
|
11
|
+
search: searchFn,
|
|
12
|
+
searchDebounceMs = 300,
|
|
10
13
|
placeholder = strings.selectPlaceholder,
|
|
11
14
|
error = '',
|
|
12
15
|
disabled = false,
|
|
@@ -14,41 +17,84 @@
|
|
|
14
17
|
name?: string;
|
|
15
18
|
value?: string;
|
|
16
19
|
options?: { value: string; label: string }[];
|
|
20
|
+
search?: SearchResolver;
|
|
21
|
+
searchDebounceMs?: number;
|
|
17
22
|
placeholder?: string;
|
|
18
23
|
error?: string;
|
|
19
24
|
disabled?: boolean;
|
|
20
25
|
} = $props();
|
|
21
26
|
|
|
22
27
|
let open = $state(false);
|
|
23
|
-
let
|
|
28
|
+
let query = $state('');
|
|
24
29
|
let container: HTMLDivElement;
|
|
25
30
|
|
|
31
|
+
// Options resolved by `searchFn` for the current query; null while no
|
|
32
|
+
// server search has run yet (e.g. box just opened, query still empty).
|
|
33
|
+
let remoteResults = $state<{ value: string; label: string }[] | null>(null);
|
|
34
|
+
let searching = $state(false);
|
|
35
|
+
// Label of whatever was last picked from `remoteResults`, kept around so the
|
|
36
|
+
// closed-state button can still show it even though it isn't in `options`.
|
|
37
|
+
let pickedLabel = $state<string | null>(null);
|
|
38
|
+
|
|
39
|
+
let searchToken = 0;
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (!searchFn) return;
|
|
42
|
+
const q = query.trim();
|
|
43
|
+
if (!q) {
|
|
44
|
+
remoteResults = null;
|
|
45
|
+
searching = false;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const token = ++searchToken;
|
|
49
|
+
searching = true;
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
searchFn(q).then((results) => {
|
|
52
|
+
if (token !== searchToken) return; // stale response, a newer query took over
|
|
53
|
+
remoteResults = results;
|
|
54
|
+
searching = false;
|
|
55
|
+
});
|
|
56
|
+
}, searchDebounceMs);
|
|
57
|
+
return () => clearTimeout(timer);
|
|
58
|
+
});
|
|
59
|
+
|
|
26
60
|
const filtered = $derived(
|
|
27
|
-
|
|
28
|
-
?
|
|
29
|
-
:
|
|
61
|
+
searchFn
|
|
62
|
+
? (remoteResults ?? options)
|
|
63
|
+
: query.trim()
|
|
64
|
+
? options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
|
|
65
|
+
: options
|
|
30
66
|
);
|
|
31
67
|
|
|
32
68
|
const selectedLabel = $derived(
|
|
33
|
-
|
|
69
|
+
value === ''
|
|
70
|
+
? placeholder
|
|
71
|
+
: (options.find((o) => o.value === value)?.label ?? pickedLabel ?? placeholder)
|
|
34
72
|
);
|
|
35
73
|
|
|
36
74
|
function toggle() {
|
|
37
75
|
if (disabled) return;
|
|
38
76
|
open = !open;
|
|
39
|
-
if (!open)
|
|
77
|
+
if (!open) query = '';
|
|
40
78
|
}
|
|
41
79
|
|
|
42
|
-
function pick(
|
|
43
|
-
value =
|
|
80
|
+
function pick(option: { value: string; label: string }) {
|
|
81
|
+
value = option.value;
|
|
82
|
+
pickedLabel = option.label;
|
|
44
83
|
open = false;
|
|
45
|
-
|
|
84
|
+
query = '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clear() {
|
|
88
|
+
value = '';
|
|
89
|
+
pickedLabel = null;
|
|
90
|
+
open = false;
|
|
91
|
+
query = '';
|
|
46
92
|
}
|
|
47
93
|
|
|
48
94
|
function onWindowClick(e: MouseEvent) {
|
|
49
95
|
if (open && !container.contains(e.target as Node)) {
|
|
50
96
|
open = false;
|
|
51
|
-
|
|
97
|
+
query = '';
|
|
52
98
|
}
|
|
53
99
|
}
|
|
54
100
|
</script>
|
|
@@ -78,7 +124,7 @@
|
|
|
78
124
|
type="text"
|
|
79
125
|
class="input input-bordered input-sm w-full"
|
|
80
126
|
placeholder={strings.selectSearch}
|
|
81
|
-
bind:value={
|
|
127
|
+
bind:value={query}
|
|
82
128
|
autocomplete="off"
|
|
83
129
|
/>
|
|
84
130
|
</div>
|
|
@@ -87,11 +133,14 @@
|
|
|
87
133
|
<button
|
|
88
134
|
type="button"
|
|
89
135
|
class="w-full rounded-btn px-3 py-2 text-left text-sm text-base-content/40 hover:bg-base-200"
|
|
90
|
-
onclick={
|
|
136
|
+
onclick={clear}
|
|
91
137
|
>
|
|
92
138
|
{placeholder}
|
|
93
139
|
</button>
|
|
94
140
|
</li>
|
|
141
|
+
{#if searching}
|
|
142
|
+
<li class="px-3 py-2 text-sm text-base-content/40">{strings.selectSearching}</li>
|
|
143
|
+
{/if}
|
|
95
144
|
{#each filtered as option (option.value)}
|
|
96
145
|
<li>
|
|
97
146
|
<button
|
|
@@ -99,13 +148,13 @@
|
|
|
99
148
|
class="w-full rounded-btn px-3 py-2 text-left text-sm hover:bg-base-200"
|
|
100
149
|
class:bg-primary={value === option.value}
|
|
101
150
|
class:text-primary-content={value === option.value}
|
|
102
|
-
onclick={() => pick(option
|
|
151
|
+
onclick={() => pick(option)}
|
|
103
152
|
>
|
|
104
153
|
{option.label}
|
|
105
154
|
</button>
|
|
106
155
|
</li>
|
|
107
156
|
{/each}
|
|
108
|
-
{#if filtered.length === 0}
|
|
157
|
+
{#if !searching && filtered.length === 0}
|
|
109
158
|
<li class="px-3 py-2 text-sm text-base-content/40">{strings.selectNoResults}</li>
|
|
110
159
|
{/if}
|
|
111
160
|
</ul>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SearchResolver } from '../../types/attribute.js';
|
|
1
2
|
type $$ComponentProps = {
|
|
2
3
|
name?: string;
|
|
3
4
|
value?: string;
|
|
@@ -5,6 +6,8 @@ type $$ComponentProps = {
|
|
|
5
6
|
value: string;
|
|
6
7
|
label: string;
|
|
7
8
|
}[];
|
|
9
|
+
search?: SearchResolver;
|
|
10
|
+
searchDebounceMs?: number;
|
|
8
11
|
placeholder?: string;
|
|
9
12
|
error?: string;
|
|
10
13
|
disabled?: boolean;
|
package/dist/i18n/en.js
CHANGED
package/dist/i18n/es.js
CHANGED
package/dist/i18n/types.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type SelectOption = {
|
|
|
20
20
|
export type OptionsResolver = SelectOption[] | ((data: any) => SelectOption[]);
|
|
21
21
|
export type FormatterResolver = (data?: any) => CellFormatter<any, any>;
|
|
22
22
|
export type DependentOptionsResolver = (data: any, record: Record<string, unknown>) => SelectOption[];
|
|
23
|
+
export type SearchResolver = (query: string) => Promise<SelectOption[]>;
|
|
23
24
|
export type DisabledResolver = (record: Record<string, unknown>) => boolean;
|
|
24
25
|
export type SeedResolver = (instance: any) => unknown;
|
|
25
26
|
export type AttributeMetadata = {
|
|
@@ -27,6 +28,10 @@ export type AttributeMetadata = {
|
|
|
27
28
|
type?: AttributeType;
|
|
28
29
|
options?: OptionsResolver;
|
|
29
30
|
dependentOptions?: DependentOptionsResolver;
|
|
31
|
+
/** Select fields only: fetch options matching what the user typed (e.g. a
|
|
32
|
+
* server-side search) instead of filtering the (possibly partial) `options`
|
|
33
|
+
* list in memory. Leave unset to keep the default in-memory filtering. */
|
|
34
|
+
search?: SearchResolver;
|
|
30
35
|
disabled?: DisabledResolver;
|
|
31
36
|
seed?: SeedResolver;
|
|
32
37
|
component?: CellComponent<any, any>;
|
|
@@ -34,6 +39,9 @@ export type AttributeMetadata = {
|
|
|
34
39
|
required?: boolean;
|
|
35
40
|
autocomplete?: FullAutoFill;
|
|
36
41
|
placeholder?: string;
|
|
42
|
+
/** Initial value on the create form. Pass a plain value, or a function of
|
|
43
|
+
* `data` (e.g. to default a select to an option derived from prefetched
|
|
44
|
+
* data) evaluated once when the create fields are resolved. */
|
|
37
45
|
default?: any;
|
|
38
46
|
excludedFromList?: boolean;
|
|
39
47
|
excludedFromCreate?: boolean;
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Component } from 'svelte';
|
|
2
2
|
import type { FullAutoFill } from 'svelte/elements';
|
|
3
|
-
import type { AttributeType } from './attribute.js';
|
|
3
|
+
import type { AttributeType, SearchResolver } from './attribute.js';
|
|
4
4
|
import type { CellComponent, CellFormatter } from './table.js';
|
|
5
5
|
export type ColumnDefinition<T extends object = Record<string, unknown>> = {
|
|
6
6
|
[K in keyof T & string]: {
|
|
@@ -29,6 +29,7 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
29
29
|
value: string;
|
|
30
30
|
label: string;
|
|
31
31
|
}[];
|
|
32
|
+
search?: SearchResolver;
|
|
32
33
|
disabled?: (record: Record<string, unknown>) => boolean;
|
|
33
34
|
seed?: (instance: any) => unknown;
|
|
34
35
|
groupedAs?: string;
|
|
@@ -51,7 +52,12 @@ export interface CustomAction<T extends object = Record<string, unknown>> {
|
|
|
51
52
|
icon: any;
|
|
52
53
|
endpoint?: string;
|
|
53
54
|
condition?: (item: T) => boolean;
|
|
54
|
-
|
|
55
|
+
/** Renders as a modal-like panel when the action runs. Mutually exclusive
|
|
56
|
+
* with `href` — provide exactly one of the two. */
|
|
57
|
+
view?: Component<any>;
|
|
58
|
+
/** Navigates to the given URL instead of opening `view`. Takes priority
|
|
59
|
+
* over `view` if both are somehow set. */
|
|
60
|
+
href?: (item: T) => string;
|
|
55
61
|
}
|
|
56
62
|
export interface RowAction<T extends object = Record<string, unknown>> {
|
|
57
63
|
label: string;
|