runeforge 0.0.29 → 0.0.30
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,263 +1,311 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import Avatar from '../Avatar.svelte';
|
|
4
|
+
import Label from '../form/Label.svelte';
|
|
5
|
+
import Select from '../form/Select.svelte';
|
|
6
|
+
import MultiSelect from '../form/MultiSelect.svelte';
|
|
7
|
+
import Tree from '../form/Tree.svelte';
|
|
8
|
+
import EmbeddedField from './EmbeddedField.svelte';
|
|
9
|
+
import { fieldLabel, initials } from './utils/misc.js';
|
|
10
|
+
import type { FieldDefinition } from '../../types/crud.js';
|
|
11
|
+
import { getStrings } from '../../i18n/context.js';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const strings = getStrings();
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
onMount(() => {
|
|
16
|
+
import('cally');
|
|
17
|
+
});
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
let {
|
|
20
|
+
field,
|
|
21
|
+
record = $bindable({} as Record<string, unknown>),
|
|
22
|
+
error = '',
|
|
23
|
+
readonly = false,
|
|
24
|
+
class: className = ''
|
|
25
|
+
}: {
|
|
26
|
+
field: FieldDefinition<T>;
|
|
27
|
+
record?: Record<string, unknown>;
|
|
28
|
+
error?: string;
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
class?: string;
|
|
31
|
+
} = $props();
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const datePopId = $props.id();
|
|
34
|
+
const dateAnchorName = `--date-anchor-${datePopId}`;
|
|
35
|
+
let datePopoverEl: HTMLElement | undefined = $state();
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const name = $derived(readonly ? undefined : field.attribute);
|
|
38
|
+
const labelText = $derived(fieldLabel(field));
|
|
39
|
+
let filePreview = $state<string | null>(null);
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
function onFileChange(e: Event & { currentTarget: HTMLInputElement }) {
|
|
42
|
+
const file = e.currentTarget.files?.[0];
|
|
43
|
+
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
44
|
+
filePreview = file && file.type.startsWith('image/') ? URL.createObjectURL(file) : null;
|
|
45
|
+
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
$effect(() => () => {
|
|
48
|
+
if (filePreview) URL.revokeObjectURL(filePreview);
|
|
49
|
+
});
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const saved = $derived(record[field.attribute]);
|
|
52
|
+
const preview = $derived(filePreview ?? (typeof saved === 'string' && saved ? saved : null));
|
|
53
|
+
const avatarInitials = $derived(initials(record.firstName as string, record.lastName as string));
|
|
54
|
+
const displayValue = $derived(saved == null ? '' : String(saved));
|
|
55
|
+
// Only for readonly branches with no other resolution mechanism (select/
|
|
56
|
+
// multiselect/tree already resolve their own label from `options`, and
|
|
57
|
+
// some of their formatters return HTML meant for a table cell, not plain
|
|
58
|
+
// input text). Falls back to `displayValue` so a value with no formatter
|
|
59
|
+
// still renders instead of going blank.
|
|
60
|
+
const formattedValue = $derived(
|
|
61
|
+
readonly && field.formatter ? field.formatter(saved, record) : displayValue
|
|
62
|
+
);
|
|
63
|
+
const selectOptions = $derived(
|
|
64
|
+
field.dependentOptions ? field.dependentOptions(record) : (field.options ?? [])
|
|
65
|
+
);
|
|
66
|
+
const fieldDisabled = $derived(field.disabled ? field.disabled(record) : false);
|
|
67
|
+
const fieldRequired = $derived(
|
|
68
|
+
typeof field.required === 'function' ? field.required(record) : !!field.required
|
|
69
|
+
);
|
|
70
|
+
const fieldHidden = $derived(
|
|
71
|
+
typeof field.hidden === 'function' ? field.hidden(record) : !!field.hidden
|
|
72
|
+
);
|
|
73
|
+
const isMultiValued = $derived(field.type === 'multiselect' || field.type === 'tree');
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
$effect(() => {
|
|
76
|
+
if (!field.dependentOptions || isMultiValued) return;
|
|
77
|
+
const current = record[field.attribute];
|
|
78
|
+
if (current && !selectOptions.some((o) => o.value === String(current))) {
|
|
79
|
+
record[field.attribute] = '';
|
|
80
|
+
}
|
|
81
|
+
});
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
$effect(() => {
|
|
84
|
+
if (!field.dependentOptions || !isMultiValued) return;
|
|
85
|
+
const current = record[field.attribute];
|
|
86
|
+
if (!Array.isArray(current)) return;
|
|
87
|
+
const validValues = new Set(selectOptions.map((o) => o.value));
|
|
88
|
+
const pruned = current.filter((v) => validValues.has(String(v)));
|
|
89
|
+
if (pruned.length !== current.length) {
|
|
90
|
+
record[field.attribute] = pruned;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
79
93
|
</script>
|
|
80
94
|
|
|
81
95
|
{#if !fieldHidden}
|
|
82
|
-
<div class="flex flex-col gap-1 {className}">
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
<div class="flex flex-col gap-1 {className}">
|
|
97
|
+
{#if field.type === 'file'}
|
|
98
|
+
<div class="flex justify-center">
|
|
99
|
+
<Avatar
|
|
100
|
+
src={preview}
|
|
101
|
+
text={avatarInitials}
|
|
102
|
+
alt={labelText}
|
|
103
|
+
class="w-20 rounded-full"
|
|
104
|
+
textClass="text-xl"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
<Label
|
|
110
|
+
text={labelText}
|
|
111
|
+
for={field.attribute}
|
|
112
|
+
capitalize={true}
|
|
113
|
+
required={fieldRequired && !readonly}
|
|
114
|
+
/>
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
116
|
+
{#if field.type === 'boolean'}
|
|
117
|
+
<input
|
|
118
|
+
type="checkbox"
|
|
119
|
+
id={field.attribute}
|
|
120
|
+
{name}
|
|
121
|
+
class="toggle toggle-primary"
|
|
122
|
+
checked={!!saved}
|
|
123
|
+
disabled={readonly || fieldDisabled}
|
|
124
|
+
onchange={(e) => {
|
|
125
|
+
record[field.attribute] = (e.currentTarget as HTMLInputElement).checked;
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
{:else if field.type === 'file'}
|
|
129
|
+
{#if !readonly}
|
|
130
|
+
<input
|
|
131
|
+
type="file"
|
|
132
|
+
id={field.attribute}
|
|
133
|
+
{name}
|
|
134
|
+
class="file-input file-input-bordered w-full"
|
|
135
|
+
class:file-input-error={!!error}
|
|
136
|
+
disabled={fieldDisabled}
|
|
137
|
+
onchange={onFileChange}
|
|
138
|
+
/>
|
|
139
|
+
{/if}
|
|
140
|
+
<!-- read-only file value is shown as the avatar above -->
|
|
141
|
+
{:else if field.type === 'select'}
|
|
142
|
+
{#if readonly}
|
|
143
|
+
<input
|
|
144
|
+
type="text"
|
|
145
|
+
id={field.attribute}
|
|
146
|
+
class="input input-bordered w-full"
|
|
147
|
+
value={selectOptions.find((o) => o.value === String(saved))?.label ?? displayValue}
|
|
148
|
+
disabled
|
|
149
|
+
/>
|
|
150
|
+
{:else}
|
|
151
|
+
<Select
|
|
152
|
+
name={field.attribute}
|
|
153
|
+
bind:value={record[field.attribute] as string}
|
|
154
|
+
options={selectOptions}
|
|
155
|
+
search={field.search}
|
|
156
|
+
placeholder={field.placeholder}
|
|
157
|
+
disabled={fieldDisabled}
|
|
158
|
+
{error}
|
|
159
|
+
/>
|
|
160
|
+
{/if}
|
|
161
|
+
{:else if field.type === 'datetime'}
|
|
162
|
+
{#if readonly}
|
|
163
|
+
<input
|
|
164
|
+
type="text"
|
|
165
|
+
id={field.attribute}
|
|
166
|
+
class="input input-bordered w-full"
|
|
167
|
+
value={formattedValue}
|
|
168
|
+
disabled
|
|
169
|
+
/>
|
|
170
|
+
{:else}
|
|
171
|
+
<input
|
|
172
|
+
type="hidden"
|
|
173
|
+
{name}
|
|
174
|
+
value={String(record[field.attribute] ?? '')}
|
|
175
|
+
disabled={fieldDisabled}
|
|
176
|
+
/>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
class="input input-bordered w-full text-left font-normal"
|
|
180
|
+
class:opacity-40={!record[field.attribute]}
|
|
181
|
+
disabled={fieldDisabled}
|
|
182
|
+
popovertarget={datePopId}
|
|
183
|
+
style="anchor-name:{dateAnchorName}"
|
|
184
|
+
>
|
|
185
|
+
{record[field.attribute] ? displayValue : (field.placeholder ?? '')}
|
|
186
|
+
</button>
|
|
187
|
+
<div
|
|
188
|
+
popover="auto"
|
|
189
|
+
id={datePopId}
|
|
190
|
+
bind:this={datePopoverEl}
|
|
191
|
+
style="position-anchor:{dateAnchorName}; position-try-fallbacks:flip-block;"
|
|
192
|
+
class="dropdown mt-1 rounded-box border border-base-content/10 bg-base-100 p-2 shadow-lg"
|
|
193
|
+
>
|
|
194
|
+
<calendar-date
|
|
195
|
+
class="cally"
|
|
196
|
+
value={String(record[field.attribute] ?? '')}
|
|
197
|
+
onchange={(e: Event) => {
|
|
198
|
+
if (fieldDisabled) return;
|
|
199
|
+
record[field.attribute] = (e.currentTarget as HTMLElement & { value: string }).value;
|
|
200
|
+
datePopoverEl?.hidePopover();
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<svg
|
|
204
|
+
aria-label={strings.previous}
|
|
205
|
+
class="fill-current size-4"
|
|
206
|
+
{...{ slot: 'previous' }}
|
|
207
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
208
|
+
viewBox="0 0 24 24"><path fill="currentColor" d="M15.75 19.5 8.25 12l7.5-7.5" /></svg
|
|
209
|
+
>
|
|
210
|
+
<svg
|
|
211
|
+
aria-label={strings.next}
|
|
212
|
+
class="fill-current size-4"
|
|
213
|
+
{...{ slot: 'next' }}
|
|
214
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
215
|
+
viewBox="0 0 24 24"><path fill="currentColor" d="m8.25 4.5 7.5 7.5-7.5 7.5" /></svg
|
|
216
|
+
>
|
|
217
|
+
<calendar-month></calendar-month>
|
|
218
|
+
</calendar-date>
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
{:else if field.type === 'textarea'}
|
|
222
|
+
{#if readonly}
|
|
223
|
+
<textarea
|
|
224
|
+
id={field.attribute}
|
|
225
|
+
class="textarea textarea-bordered bg-base-100 w-full"
|
|
226
|
+
value={formattedValue}
|
|
227
|
+
disabled
|
|
228
|
+
></textarea>
|
|
229
|
+
{:else}
|
|
230
|
+
<textarea
|
|
231
|
+
id={field.attribute}
|
|
232
|
+
{name}
|
|
233
|
+
placeholder={field.placeholder ?? ''}
|
|
234
|
+
bind:value={record[field.attribute]}
|
|
235
|
+
class="textarea textarea-bordered bg-base-100 w-full"
|
|
236
|
+
class:textarea-error={!!error}
|
|
237
|
+
disabled={fieldDisabled}
|
|
238
|
+
></textarea>
|
|
239
|
+
{/if}
|
|
240
|
+
{:else if field.type === 'embedded'}
|
|
241
|
+
<EmbeddedField {field} bind:record {readonly} />
|
|
242
|
+
{:else if field.type === 'multiselect'}
|
|
243
|
+
{#if readonly}
|
|
244
|
+
<input
|
|
245
|
+
type="text"
|
|
246
|
+
id={field.attribute}
|
|
247
|
+
class="input input-bordered w-full"
|
|
248
|
+
value={(Array.isArray(saved) ? saved : [])
|
|
249
|
+
.map((v) => selectOptions.find((o) => o.value === String(v))?.label ?? String(v))
|
|
250
|
+
.join(', ')}
|
|
251
|
+
disabled
|
|
252
|
+
/>
|
|
253
|
+
{:else}
|
|
254
|
+
<MultiSelect
|
|
255
|
+
name={field.attribute}
|
|
256
|
+
bind:value={record[field.attribute] as string[]}
|
|
257
|
+
options={selectOptions}
|
|
258
|
+
search={field.search}
|
|
259
|
+
placeholder={field.placeholder}
|
|
260
|
+
disabled={fieldDisabled}
|
|
261
|
+
{error}
|
|
262
|
+
/>
|
|
263
|
+
{/if}
|
|
264
|
+
{:else if field.type === 'tree'}
|
|
265
|
+
{#if readonly}
|
|
266
|
+
<input
|
|
267
|
+
type="text"
|
|
268
|
+
id={field.attribute}
|
|
269
|
+
class="input input-bordered w-full"
|
|
270
|
+
value={(Array.isArray(saved) ? saved : [])
|
|
271
|
+
.map((v) => selectOptions.find((o) => o.value === String(v))?.label ?? String(v))
|
|
272
|
+
.join(', ')}
|
|
273
|
+
disabled
|
|
274
|
+
/>
|
|
275
|
+
{:else}
|
|
276
|
+
<Tree
|
|
277
|
+
name={field.attribute}
|
|
278
|
+
bind:value={record[field.attribute] as string[]}
|
|
279
|
+
options={selectOptions}
|
|
280
|
+
disabled={fieldDisabled}
|
|
281
|
+
defaultExpanded={field.defaultExpanded ?? true}
|
|
282
|
+
/>
|
|
283
|
+
{/if}
|
|
284
|
+
{:else if readonly}
|
|
285
|
+
<input
|
|
286
|
+
type={field.type ?? 'text'}
|
|
287
|
+
id={field.attribute}
|
|
288
|
+
class="input input-bordered w-full"
|
|
289
|
+
value={formattedValue}
|
|
290
|
+
disabled
|
|
291
|
+
/>
|
|
292
|
+
{:else}
|
|
293
|
+
<input
|
|
294
|
+
type={field.type ?? 'text'}
|
|
295
|
+
id={field.attribute}
|
|
296
|
+
{name}
|
|
297
|
+
placeholder={field.placeholder ?? ''}
|
|
298
|
+
bind:value={record[field.attribute]}
|
|
299
|
+
autocomplete={field.autocomplete}
|
|
300
|
+
step={field.type === 'number' ? 'any' : undefined}
|
|
301
|
+
class="input input-bordered w-full"
|
|
302
|
+
class:input-error={!!error}
|
|
303
|
+
disabled={fieldDisabled}
|
|
304
|
+
/>
|
|
305
|
+
{/if}
|
|
258
306
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
</div>
|
|
307
|
+
{#if error}
|
|
308
|
+
<span class="text-error text-xs">{error}</span>
|
|
309
|
+
{/if}
|
|
310
|
+
</div>
|
|
263
311
|
{/if}
|
|
@@ -19,7 +19,10 @@ export function inferType(key, value) {
|
|
|
19
19
|
return 'email';
|
|
20
20
|
if (k.includes('password') || k.includes('hash'))
|
|
21
21
|
return 'password';
|
|
22
|
-
if (k.includes('description') ||
|
|
22
|
+
if (k.includes('description') ||
|
|
23
|
+
k.includes('bio') ||
|
|
24
|
+
k.includes('notes') ||
|
|
25
|
+
k.includes('content'))
|
|
23
26
|
return 'textarea';
|
|
24
27
|
return 'text';
|
|
25
28
|
}
|
|
@@ -46,6 +49,7 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
46
49
|
disabled: m.disabled,
|
|
47
50
|
hidden: m.hidden,
|
|
48
51
|
seed: m.seed,
|
|
52
|
+
formatter: resolveFormatter(m, data),
|
|
49
53
|
groupedAs: m.groupedAs,
|
|
50
54
|
min: m.min,
|
|
51
55
|
max: m.max,
|
|
@@ -53,9 +57,7 @@ export function buildFieldDefinitions(meta, data, excludedFlag, excluded) {
|
|
|
53
57
|
minLength: m.minLength,
|
|
54
58
|
maxLength: m.maxLength,
|
|
55
59
|
pattern: m.pattern,
|
|
56
|
-
fields: m.fields
|
|
57
|
-
? buildFieldDefinitions(m.fields, data, excludedFlag, new Set())
|
|
58
|
-
: undefined,
|
|
60
|
+
fields: m.fields ? buildFieldDefinitions(m.fields, data, excludedFlag, new Set()) : undefined,
|
|
59
61
|
itemLabel: m.itemLabel,
|
|
60
62
|
defaultExpanded: m.defaultExpanded,
|
|
61
63
|
row: m.row
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface FieldDefinition<T extends object = Record<string, unknown>> {
|
|
|
34
34
|
disabled?: (record: Record<string, unknown>) => boolean;
|
|
35
35
|
hidden?: boolean | ((record: Record<string, unknown>) => boolean);
|
|
36
36
|
seed?: (instance: any) => unknown;
|
|
37
|
+
/** Read-only rendering only (the Read view, and any other readonly Field) */
|
|
38
|
+
formatter?: (value: unknown, record: Record<string, unknown>) => string;
|
|
37
39
|
groupedAs?: string;
|
|
38
40
|
min?: number;
|
|
39
41
|
max?: number;
|