firstly 0.6.2 → 0.7.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.
- package/CHANGELOG.md +20 -0
- package/esm/changeLog/index.d.ts +1 -0
- package/esm/svelte/FF_Config.svelte +2 -2
- package/esm/svelte/FF_Config.svelte.js +3 -0
- package/esm/svelte/grid/DefaultInput.svelte +42 -0
- package/esm/svelte/grid/DefaultInput.svelte.d.ts +9 -0
- package/esm/svelte/grid/FF_Cell.svelte +98 -0
- package/esm/svelte/grid/FF_Cell.svelte.d.ts +10 -0
- package/esm/svelte/grid/FF_CellValue.svelte +24 -0
- package/esm/svelte/grid/FF_CellValue.svelte.d.ts +28 -0
- package/esm/svelte/grid/FF_Cell_Content.svelte +37 -0
- package/esm/svelte/grid/FF_Cell_Content.svelte.d.ts +10 -0
- package/esm/svelte/grid/FF_Cell_Error.svelte +26 -0
- package/esm/svelte/grid/FF_Cell_Error.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Cell_Hint.svelte +26 -0
- package/esm/svelte/grid/FF_Cell_Hint.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Cell_Label.svelte +34 -0
- package/esm/svelte/grid/FF_Cell_Label.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Grid.svelte +385 -0
- package/esm/svelte/grid/FF_Grid.svelte.d.ts +47 -0
- package/esm/svelte/grid/GroupFields.svelte +141 -0
- package/esm/svelte/grid/GroupFields.svelte.d.ts +45 -0
- package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte +9 -0
- package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte.d.ts +18 -0
- package/esm/svelte/grid/buildCells.d.ts +16 -0
- package/esm/svelte/grid/buildCells.js +79 -0
- package/esm/svelte/grid/cellComponent.d.ts +9 -0
- package/esm/svelte/grid/cellComponent.js +31 -0
- package/esm/svelte/grid/cellConfig.d.ts +13 -0
- package/esm/svelte/grid/cellConfig.js +48 -0
- package/esm/svelte/grid/cellTypes.d.ts +168 -0
- package/esm/svelte/grid/cellTypes.js +1 -0
- package/esm/svelte/grid/index.d.ts +10 -0
- package/esm/svelte/grid/index.js +12 -0
- package/esm/svelte/grid/metaKind.d.ts +27 -0
- package/esm/svelte/grid/metaKind.js +34 -0
- package/esm/svelte/index.d.ts +3 -2
- package/esm/svelte/index.js +3 -2
- package/package.json +4 -3
- package/esm/svelte/DemoForm.svelte +0 -121
- package/esm/svelte/DemoForm.svelte.d.ts +0 -42
- package/esm/svelte/DemoGrid.svelte +0 -258
- package/esm/svelte/DemoGrid.svelte.d.ts +0 -49
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object">
|
|
2
|
+
// PUBLISHED batteries-included grid — `import { FF_Grid } from '..'` and go, zero setup:
|
|
3
|
+
// it bundles a default input + a neutral default skin. Reads the entity `hub` (every prop overrides).
|
|
4
|
+
// For full control over markup + look, copy the boutique <App_Grid> instead (degit src/boutique/grid).
|
|
5
|
+
import { untrack } from 'svelte'
|
|
6
|
+
|
|
7
|
+
import { repo } from 'remult'
|
|
8
|
+
import type { ClassType, EntityFilter, EntityOrderBy } from 'remult'
|
|
9
|
+
|
|
10
|
+
import { errorMessage } from '../../core/helper.js'
|
|
11
|
+
import type { DialogClose } from '../dialog.svelte.js'
|
|
12
|
+
import FF_Config from '../FF_Config.svelte'
|
|
13
|
+
import { ffConfig } from '../FF_Config.svelte.js'
|
|
14
|
+
import { ff } from '../ff.svelte.js'
|
|
15
|
+
import type { FF_Many, ManyStrategy } from '../ff.svelte.js'
|
|
16
|
+
import Icon from '../ui/Icon.svelte'
|
|
17
|
+
import { LibIcon_Add, LibIcon_Edit } from '../ui/LibIcon.js'
|
|
18
|
+
import { buildCells } from './buildCells.js'
|
|
19
|
+
import type { ActionConfig, CellInput, CellMode, HubConfig } from './cellTypes.js'
|
|
20
|
+
import DefaultInput from './DefaultInput.svelte'
|
|
21
|
+
import FF_CellValue from './FF_CellValue.svelte'
|
|
22
|
+
import GroupFields from './GroupFields.svelte'
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
entity: ClassType<T>
|
|
26
|
+
/** Grid columns + the default fields for the create/edit forms. Defaults to the entity hub. */
|
|
27
|
+
cells?: CellInput<T>[]
|
|
28
|
+
where?: EntityFilter<T>
|
|
29
|
+
orderBy?: EntityOrderBy<T>
|
|
30
|
+
strategy?: ManyStrategy
|
|
31
|
+
pageSize?: number
|
|
32
|
+
enabled?: boolean
|
|
33
|
+
/** Display mode. `'readonly'` disables create/edit/delete; `'edit'` (default) enables them.
|
|
34
|
+
* Extensible via `CellMode` (future: `'filter'`). */
|
|
35
|
+
mode?: CellMode
|
|
36
|
+
/** Create action ({} on, false off). Defaults to the hub, then on. */
|
|
37
|
+
insert?: ActionConfig<T> | false
|
|
38
|
+
/** Edit action. */
|
|
39
|
+
update?: ActionConfig<T> | false
|
|
40
|
+
/** Delete action. */
|
|
41
|
+
delete?: ActionConfig<T> | false
|
|
42
|
+
/** Placeholder rows shown during the first load (kept the same height to avoid a shift). */
|
|
43
|
+
skeletonRows?: number
|
|
44
|
+
}
|
|
45
|
+
let {
|
|
46
|
+
entity,
|
|
47
|
+
cells,
|
|
48
|
+
where,
|
|
49
|
+
orderBy,
|
|
50
|
+
strategy: strategyProp,
|
|
51
|
+
pageSize: pageSizeProp,
|
|
52
|
+
enabled = true,
|
|
53
|
+
mode = 'edit',
|
|
54
|
+
insert,
|
|
55
|
+
update,
|
|
56
|
+
delete: deleteProp,
|
|
57
|
+
skeletonRows = 2,
|
|
58
|
+
}: Props = $props()
|
|
59
|
+
|
|
60
|
+
const hub = untrack(() => (repo(entity).metadata.options.hub ?? {}) as HubConfig<T>)
|
|
61
|
+
const strategy = untrack(() => strategyProp ?? hub.strategy ?? 'paginate')
|
|
62
|
+
const pageSize = untrack(() => pageSizeProp ?? hub.pageSize ?? 25)
|
|
63
|
+
|
|
64
|
+
let sort = $state<EntityOrderBy<T> | undefined>(untrack(() => orderBy ?? hub.orderBy))
|
|
65
|
+
|
|
66
|
+
const m = untrack(() =>
|
|
67
|
+
ff(entity).many(
|
|
68
|
+
() => ({ where: where ?? hub.where, orderBy: sort, pageSize, enabled }),
|
|
69
|
+
strategy,
|
|
70
|
+
),
|
|
71
|
+
) as unknown as FF_Many<T, 'paginate'>
|
|
72
|
+
|
|
73
|
+
const cfg = ffConfig()
|
|
74
|
+
const defaultSortable = $derived(hub.defaultSortable ?? cfg.cell?.defaultSortable)
|
|
75
|
+
|
|
76
|
+
// the dialog is portaled outside <FF_Config>; re-provide the captured config, but BUNDLE default
|
|
77
|
+
// inputs so a zero-setup app still gets working form fields (its own inputs override).
|
|
78
|
+
const dialogCellConfig = $derived({
|
|
79
|
+
...cfg.cell,
|
|
80
|
+
inputs: {
|
|
81
|
+
text: DefaultInput,
|
|
82
|
+
number: DefaultInput,
|
|
83
|
+
checkbox: DefaultInput,
|
|
84
|
+
...cfg.cell?.inputs,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const listCells = $derived(cells ?? hub.cells)
|
|
89
|
+
const cols = $derived(buildCells(m.meta, listCells, { defaultSortable }))
|
|
90
|
+
const count = $derived(m.aggregates?.$count ?? m.items.length)
|
|
91
|
+
|
|
92
|
+
const isReadonly = $derived(mode === 'readonly')
|
|
93
|
+
const insertCfg = $derived(isReadonly ? false : (insert ?? hub.insert ?? {}))
|
|
94
|
+
const updateCfg = $derived(isReadonly ? false : (update ?? hub.update ?? {}))
|
|
95
|
+
const deleteCfg = $derived(isReadonly ? false : (deleteProp ?? hub.delete ?? {}))
|
|
96
|
+
const canCreate = $derived(insertCfg !== false)
|
|
97
|
+
const canEdit = $derived(updateCfg !== false)
|
|
98
|
+
const canDelete = $derived(deleteCfg !== false)
|
|
99
|
+
const showRowActions = $derived(canEdit || canDelete)
|
|
100
|
+
|
|
101
|
+
const newIcon = $derived(insertCfg !== false ? (insertCfg.icon ?? LibIcon_Add) : LibIcon_Add)
|
|
102
|
+
const editIcon = $derived(updateCfg !== false ? (updateCfg.icon ?? LibIcon_Edit) : LibIcon_Edit)
|
|
103
|
+
|
|
104
|
+
function toggleSort(key: string) {
|
|
105
|
+
const cur = (sort as Record<string, 'asc' | 'desc'> | undefined)?.[key]
|
|
106
|
+
sort = { [key]: cur === 'asc' ? 'desc' : 'asc' } as EntityOrderBy<T>
|
|
107
|
+
}
|
|
108
|
+
const sortDir = (key: string) => (sort as Record<string, string> | undefined)?.[key]
|
|
109
|
+
|
|
110
|
+
const idOf = (row: T) => m.meta.idMetadata.getId(row)
|
|
111
|
+
|
|
112
|
+
let errors = $state<Record<string, string | undefined>>({})
|
|
113
|
+
let saveError = $state('')
|
|
114
|
+
let creating = $state(false)
|
|
115
|
+
|
|
116
|
+
const openEdit = (row: T) => {
|
|
117
|
+
creating = false
|
|
118
|
+
errors = {}
|
|
119
|
+
saveError = ''
|
|
120
|
+
m.editInDialog(row, dialogBody)
|
|
121
|
+
}
|
|
122
|
+
const openCreate = () => {
|
|
123
|
+
creating = true
|
|
124
|
+
errors = {}
|
|
125
|
+
saveError = ''
|
|
126
|
+
m.createInDialog(dialogBody)
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
{#snippet dialogBody(close: DialogClose)}
|
|
131
|
+
{#if m.draft}
|
|
132
|
+
{@const draft = m.draft}
|
|
133
|
+
{@const action = creating ? insertCfg : updateCfg}
|
|
134
|
+
{@const formCells = (action !== false && action.cells) || listCells}
|
|
135
|
+
<FF_Config cell={dialogCellConfig}>
|
|
136
|
+
<GroupFields
|
|
137
|
+
meta={m.meta}
|
|
138
|
+
{draft}
|
|
139
|
+
cells={formCells}
|
|
140
|
+
mode="edit"
|
|
141
|
+
{errors}
|
|
142
|
+
error={saveError}
|
|
143
|
+
busy={m.isWriting}
|
|
144
|
+
saveLabel={creating ? 'Create' : 'Save'}
|
|
145
|
+
canSave={creating ? m.meta.apiInsertAllowed() : m.meta.apiUpdateAllowed(draft)}
|
|
146
|
+
onsave={async () => {
|
|
147
|
+
try {
|
|
148
|
+
await m.save()
|
|
149
|
+
errors = {}
|
|
150
|
+
saveError = ''
|
|
151
|
+
close({ ok: true })
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const ms = (err as { modelState?: Record<string, string> })?.modelState
|
|
154
|
+
errors = ms ?? {}
|
|
155
|
+
saveError = ms ? '' : errorMessage(err)
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
ondelete={!creating && canDelete
|
|
159
|
+
? async () => {
|
|
160
|
+
const res = await m.confirmRemove(draft)
|
|
161
|
+
if (res.ok) close({ ok: true })
|
|
162
|
+
}
|
|
163
|
+
: undefined}
|
|
164
|
+
/>
|
|
165
|
+
</FF_Config>
|
|
166
|
+
{/if}
|
|
167
|
+
{/snippet}
|
|
168
|
+
|
|
169
|
+
<div data-ff-grid>
|
|
170
|
+
<div data-ff-grid-toolbar>
|
|
171
|
+
{#if canCreate}
|
|
172
|
+
<button data-ff-grid-new title="New" disabled={!m.meta.apiInsertAllowed()} onclick={openCreate}>
|
|
173
|
+
<Icon size="1.05rem" data={newIcon} />
|
|
174
|
+
</button>
|
|
175
|
+
{/if}
|
|
176
|
+
<span data-ff-grid-count>{count}</span>
|
|
177
|
+
</div>
|
|
178
|
+
{#if m.error && !m.draft}<p data-ff-grid-error>{m.error}</p>{/if}
|
|
179
|
+
<table>
|
|
180
|
+
<thead>
|
|
181
|
+
<tr>
|
|
182
|
+
{#each cols as cell, i (cell.col ?? `${cell.kind}-${i}`)}
|
|
183
|
+
<th
|
|
184
|
+
data-col={cell.col}
|
|
185
|
+
data-sortable={cell.sortable || undefined}
|
|
186
|
+
style:text-align={cell.align}
|
|
187
|
+
class={cell.class}
|
|
188
|
+
onclick={() => cell.sortable && cell.col && toggleSort(cell.col)}
|
|
189
|
+
>
|
|
190
|
+
{cell.caption}{#if cell.col && sortDir(cell.col)}<span data-sort
|
|
191
|
+
>{sortDir(cell.col) === 'asc' ? ' ▲' : ' ▼'}</span
|
|
192
|
+
>{/if}
|
|
193
|
+
</th>
|
|
194
|
+
{/each}
|
|
195
|
+
{#if showRowActions}<th></th>{/if}
|
|
196
|
+
</tr>
|
|
197
|
+
</thead>
|
|
198
|
+
<tbody>
|
|
199
|
+
{#if m.loading.init}
|
|
200
|
+
{#each Array(skeletonRows) as _, i (i)}
|
|
201
|
+
<tr
|
|
202
|
+
>{#each cols as cell, i (cell.col ?? `${cell.kind}-${i}`)}<td><span data-sk></span></td
|
|
203
|
+
>{/each}{#if showRowActions}<td data-ff-grid-actions><span data-sk data-sk-btn></span></td
|
|
204
|
+
>{/if}</tr
|
|
205
|
+
>
|
|
206
|
+
{/each}
|
|
207
|
+
{:else}
|
|
208
|
+
{#each m.items as row (idOf(row))}
|
|
209
|
+
<tr>
|
|
210
|
+
{#each cols as cell, i (cell.col ?? `${cell.kind}-${i}`)}
|
|
211
|
+
<td data-col={cell.col} style:text-align={cell.align} class={cell.class}>
|
|
212
|
+
<FF_CellValue {cell} {row} />
|
|
213
|
+
</td>
|
|
214
|
+
{/each}
|
|
215
|
+
{#if showRowActions}
|
|
216
|
+
<td data-ff-grid-actions>
|
|
217
|
+
<button
|
|
218
|
+
data-ff-grid-edit
|
|
219
|
+
title="Edit"
|
|
220
|
+
disabled={!m.meta.apiUpdateAllowed(row)}
|
|
221
|
+
onclick={() => openEdit(row)}
|
|
222
|
+
>
|
|
223
|
+
<Icon size="1.05rem" data={editIcon} />
|
|
224
|
+
</button>
|
|
225
|
+
</td>
|
|
226
|
+
{/if}
|
|
227
|
+
</tr>
|
|
228
|
+
{/each}
|
|
229
|
+
{/if}
|
|
230
|
+
</tbody>
|
|
231
|
+
</table>
|
|
232
|
+
|
|
233
|
+
{#if strategy === 'paginate' && m.hasNextPage}
|
|
234
|
+
<button data-ff-grid-more disabled={m.loading.more} onclick={() => m.more()}>More</button>
|
|
235
|
+
{/if}
|
|
236
|
+
{#if !m.loading.init && m.items.length === 0}<p data-ff-grid-empty>Nothing yet.</p>{/if}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<style>
|
|
240
|
+
/* default skin bundled with the published FF_Grid. :global reaches the portaled dialog. */
|
|
241
|
+
:global([data-ff-grid] table) {
|
|
242
|
+
width: 100%;
|
|
243
|
+
border-collapse: collapse;
|
|
244
|
+
font-size: 14px;
|
|
245
|
+
}
|
|
246
|
+
:global([data-ff-grid] :is(th, td)) {
|
|
247
|
+
padding: 6px 9px;
|
|
248
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 13%, transparent);
|
|
249
|
+
text-align: left;
|
|
250
|
+
white-space: nowrap;
|
|
251
|
+
}
|
|
252
|
+
:global([data-ff-grid] th:first-child),
|
|
253
|
+
:global([data-ff-grid] td:first-child) {
|
|
254
|
+
width: 100%;
|
|
255
|
+
}
|
|
256
|
+
:global([data-ff-grid] th) {
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
user-select: none;
|
|
259
|
+
}
|
|
260
|
+
:global([data-ff-grid] th[data-sortable]) {
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
:global([data-ff-grid] tbody tr:hover) {
|
|
264
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
265
|
+
}
|
|
266
|
+
:global([data-ff-grid-toolbar]) {
|
|
267
|
+
display: flex;
|
|
268
|
+
align-items: center;
|
|
269
|
+
gap: 10px;
|
|
270
|
+
margin-bottom: 8px;
|
|
271
|
+
}
|
|
272
|
+
:global([data-ff-grid-count]) {
|
|
273
|
+
margin-left: auto;
|
|
274
|
+
font-size: 15px;
|
|
275
|
+
opacity: 0.75;
|
|
276
|
+
font-variant-numeric: tabular-nums;
|
|
277
|
+
}
|
|
278
|
+
:global([data-ff-grid-empty]) {
|
|
279
|
+
opacity: 0.6;
|
|
280
|
+
font-size: 14px;
|
|
281
|
+
padding: 8px 0;
|
|
282
|
+
}
|
|
283
|
+
:global([data-ff-grid-actions]) {
|
|
284
|
+
text-align: right;
|
|
285
|
+
}
|
|
286
|
+
:global([data-ff-grid] button) {
|
|
287
|
+
display: inline-flex;
|
|
288
|
+
align-items: center;
|
|
289
|
+
gap: 5px;
|
|
290
|
+
font: inherit;
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
padding: 5px 9px;
|
|
293
|
+
color: inherit;
|
|
294
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
295
|
+
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
296
|
+
border-radius: 7px;
|
|
297
|
+
}
|
|
298
|
+
:global([data-ff-grid] button:disabled) {
|
|
299
|
+
opacity: 0.45;
|
|
300
|
+
cursor: not-allowed;
|
|
301
|
+
}
|
|
302
|
+
:global([data-ff-grid] [data-ff-grid-edit]),
|
|
303
|
+
:global([data-ff-grid] [data-ff-grid-new]) {
|
|
304
|
+
background: transparent;
|
|
305
|
+
border-color: transparent;
|
|
306
|
+
padding: 3px 5px;
|
|
307
|
+
opacity: 0.6;
|
|
308
|
+
}
|
|
309
|
+
:global([data-ff-grid] [data-ff-grid-edit]:hover),
|
|
310
|
+
:global([data-ff-grid] [data-ff-grid-new]:hover) {
|
|
311
|
+
background: color-mix(in srgb, currentColor 12%, transparent);
|
|
312
|
+
opacity: 1;
|
|
313
|
+
}
|
|
314
|
+
:global([data-ff-grid] [data-ff-grid-more]) {
|
|
315
|
+
display: flex;
|
|
316
|
+
margin: 12px auto 0;
|
|
317
|
+
}
|
|
318
|
+
:global([data-ff-grid] [data-sk]) {
|
|
319
|
+
display: inline-block;
|
|
320
|
+
width: 70%;
|
|
321
|
+
height: 1.05em;
|
|
322
|
+
border-radius: 4px;
|
|
323
|
+
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
324
|
+
animation: -global-ff-sk 1.2s ease-in-out infinite;
|
|
325
|
+
}
|
|
326
|
+
:global([data-ff-grid] [data-sk-btn]) {
|
|
327
|
+
width: 1.4em;
|
|
328
|
+
}
|
|
329
|
+
@keyframes -global-ff-sk {
|
|
330
|
+
0%,
|
|
331
|
+
100% {
|
|
332
|
+
opacity: 0.4;
|
|
333
|
+
}
|
|
334
|
+
50% {
|
|
335
|
+
opacity: 0.85;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/* dialog form (portaled) */
|
|
339
|
+
:global([data-ff-form]),
|
|
340
|
+
:global([data-ff-group]) {
|
|
341
|
+
display: block;
|
|
342
|
+
min-width: 280px;
|
|
343
|
+
}
|
|
344
|
+
:global([data-ff-form] button) {
|
|
345
|
+
display: inline-flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 5px;
|
|
348
|
+
font: inherit;
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
padding: 5px 11px;
|
|
351
|
+
color: inherit;
|
|
352
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
353
|
+
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
354
|
+
border-radius: 7px;
|
|
355
|
+
}
|
|
356
|
+
:global([data-ff-form-actions]) {
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 8px;
|
|
360
|
+
width: 100%;
|
|
361
|
+
margin-top: 8px;
|
|
362
|
+
}
|
|
363
|
+
:global([data-ff-form-actions] [data-primary]) {
|
|
364
|
+
margin-left: auto;
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
}
|
|
367
|
+
:global([data-ff-form-actions] [data-danger]) {
|
|
368
|
+
color: var(--color-error, #dc2626);
|
|
369
|
+
border-color: color-mix(in srgb, var(--color-error, #dc2626) 45%, transparent);
|
|
370
|
+
}
|
|
371
|
+
:global([data-ff-readonly]) {
|
|
372
|
+
display: block;
|
|
373
|
+
box-sizing: border-box;
|
|
374
|
+
padding: 5px 8px;
|
|
375
|
+
}
|
|
376
|
+
:global([data-ff-form-error]) {
|
|
377
|
+
color: var(--color-error, #dc2626);
|
|
378
|
+
font-size: 13px;
|
|
379
|
+
margin: 4px 0 0;
|
|
380
|
+
}
|
|
381
|
+
:global([data-ff-cell-error]) {
|
|
382
|
+
color: var(--color-error, #dc2626);
|
|
383
|
+
font-size: 12px;
|
|
384
|
+
}
|
|
385
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ClassType, EntityFilter, EntityOrderBy } from 'remult';
|
|
2
|
+
import type { ManyStrategy } from '../ff.svelte.js';
|
|
3
|
+
import type { ActionConfig, CellInput, CellMode } from './cellTypes.js';
|
|
4
|
+
declare function $$render<T extends object>(): {
|
|
5
|
+
props: {
|
|
6
|
+
entity: ClassType<T>;
|
|
7
|
+
/** Grid columns + the default fields for the create/edit forms. Defaults to the entity hub. */
|
|
8
|
+
cells?: CellInput<T>[];
|
|
9
|
+
where?: EntityFilter<T>;
|
|
10
|
+
orderBy?: EntityOrderBy<T>;
|
|
11
|
+
strategy?: ManyStrategy;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/** Display mode. `'readonly'` disables create/edit/delete; `'edit'` (default) enables them.
|
|
15
|
+
* Extensible via `CellMode` (future: `'filter'`). */
|
|
16
|
+
mode?: CellMode;
|
|
17
|
+
/** Create action ({} on, false off). Defaults to the hub, then on. */
|
|
18
|
+
insert?: ActionConfig<T> | false;
|
|
19
|
+
/** Edit action. */
|
|
20
|
+
update?: ActionConfig<T> | false;
|
|
21
|
+
/** Delete action. */
|
|
22
|
+
delete?: ActionConfig<T> | false;
|
|
23
|
+
/** Placeholder rows shown during the first load (kept the same height to avoid a shift). */
|
|
24
|
+
skeletonRows?: number;
|
|
25
|
+
};
|
|
26
|
+
exports: {};
|
|
27
|
+
bindings: "";
|
|
28
|
+
slots: {};
|
|
29
|
+
events: {};
|
|
30
|
+
};
|
|
31
|
+
declare class __sveltets_Render<T extends object> {
|
|
32
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
33
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
34
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
35
|
+
bindings(): "";
|
|
36
|
+
exports(): {};
|
|
37
|
+
}
|
|
38
|
+
interface $$IsomorphicComponent {
|
|
39
|
+
new <T extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
40
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
41
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
42
|
+
<T extends object>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
43
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
44
|
+
}
|
|
45
|
+
declare const FFGrid: $$IsomorphicComponent;
|
|
46
|
+
type FFGrid<T extends object> = InstanceType<typeof FFGrid<T>>;
|
|
47
|
+
export default FFGrid;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object">
|
|
2
|
+
// Published "group of fields" — a metadata-driven group of cells bound to a draft.
|
|
3
|
+
// • mode 'edit' → cells are inputs (registered via FF_Config.cell.inputs)
|
|
4
|
+
// • mode 'readonly' → cells show their values
|
|
5
|
+
// Give it `onsave` (in edit mode) and the group BECOMES a form, with a Save + optional
|
|
6
|
+
// Delete action row. Reused by the published FF_Grid + the boutique App_Grid / App_Group.
|
|
7
|
+
import type { EntityMetadata } from 'remult'
|
|
8
|
+
|
|
9
|
+
import { ffConfig } from '../FF_Config.svelte.js'
|
|
10
|
+
import Icon from '../ui/Icon.svelte'
|
|
11
|
+
import { LibIcon_Delete, LibIcon_Save } from '../ui/LibIcon.js'
|
|
12
|
+
import { buildCells } from './buildCells.js'
|
|
13
|
+
import type { CellInput } from './cellTypes.js'
|
|
14
|
+
import FF_Cell from './FF_Cell.svelte'
|
|
15
|
+
import FF_CellValue from './FF_CellValue.svelte'
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
/** Entity metadata (from the ff handle's `.meta`). */
|
|
19
|
+
meta: EntityMetadata<T>
|
|
20
|
+
/** The record being shown/edited — bound directly (mutated in place in edit mode). */
|
|
21
|
+
draft: T
|
|
22
|
+
cells?: CellInput<T>[]
|
|
23
|
+
/** 'edit' (inputs) or 'readonly' (values). */
|
|
24
|
+
mode?: 'edit' | 'readonly'
|
|
25
|
+
errors?: Record<string, string | undefined>
|
|
26
|
+
error?: string
|
|
27
|
+
busy?: boolean
|
|
28
|
+
saveLabel?: string
|
|
29
|
+
canSave?: boolean
|
|
30
|
+
/** When provided (in edit mode) the group becomes a form with a Save action. */
|
|
31
|
+
onsave?: () => void | Promise<void>
|
|
32
|
+
/** When provided, a Delete action is shown. Omit to hide it. */
|
|
33
|
+
ondelete?: () => void | Promise<void>
|
|
34
|
+
/** Show Delete but disabled (pure UI). */
|
|
35
|
+
disableDelete?: boolean
|
|
36
|
+
}
|
|
37
|
+
let {
|
|
38
|
+
meta,
|
|
39
|
+
draft,
|
|
40
|
+
cells,
|
|
41
|
+
mode = 'edit',
|
|
42
|
+
errors = {},
|
|
43
|
+
error = '',
|
|
44
|
+
busy = false,
|
|
45
|
+
saveLabel = 'Save',
|
|
46
|
+
canSave = true,
|
|
47
|
+
onsave,
|
|
48
|
+
ondelete,
|
|
49
|
+
disableDelete = false,
|
|
50
|
+
}: Props = $props()
|
|
51
|
+
|
|
52
|
+
const cols = $derived(buildCells(meta, cells))
|
|
53
|
+
const isForm = $derived(mode === 'edit' && !!onsave)
|
|
54
|
+
|
|
55
|
+
// Read app-level config ONCE at init (getContext must run during init).
|
|
56
|
+
const cfg = ffConfig()
|
|
57
|
+
const inputFor = (inputType: string) => cfg.cell.inputs?.[inputType]
|
|
58
|
+
|
|
59
|
+
// keep `draft as Record` casts in <script> so prettier can't strip generics in markup
|
|
60
|
+
const get = (key: string): unknown => (draft as Record<string, unknown>)[key]
|
|
61
|
+
const set = (key: string, v: unknown) => {
|
|
62
|
+
;(draft as Record<string, unknown>)[key] = v
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function submit(e: SubmitEvent) {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
await onsave?.()
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
{#snippet body()}
|
|
72
|
+
<FF_Cell>
|
|
73
|
+
{#each cols as cell, i (cell.col ?? `${cell.kind}-${i}`)}
|
|
74
|
+
{#if cell.col}
|
|
75
|
+
{@const col = cell.col}
|
|
76
|
+
{#if mode === 'readonly'}
|
|
77
|
+
<FF_Cell key={col} ui={cell.ui} label={{ html: cell.caption }}>
|
|
78
|
+
<span data-ff-readonly data-input-type={cell.inputType}
|
|
79
|
+
><FF_CellValue {cell} row={draft} /></span
|
|
80
|
+
>
|
|
81
|
+
</FF_Cell>
|
|
82
|
+
{:else}
|
|
83
|
+
<FF_Cell
|
|
84
|
+
key={col}
|
|
85
|
+
ui={cell.ui}
|
|
86
|
+
label={{ html: cell.caption }}
|
|
87
|
+
error={{ html: errors[col] }}
|
|
88
|
+
content={{
|
|
89
|
+
component: inputFor(cell.inputType),
|
|
90
|
+
props: {
|
|
91
|
+
type: cell.inputType,
|
|
92
|
+
placeholder: cell.field?.options.placeholder,
|
|
93
|
+
valueConverter: cell.field?.valueConverter,
|
|
94
|
+
},
|
|
95
|
+
}}
|
|
96
|
+
bind:value={() => get(col), (v) => set(col, v)}
|
|
97
|
+
></FF_Cell>
|
|
98
|
+
{/if}
|
|
99
|
+
{:else}
|
|
100
|
+
<FF_Cell ui={cell.ui}></FF_Cell>
|
|
101
|
+
{/if}
|
|
102
|
+
{/each}
|
|
103
|
+
|
|
104
|
+
{#if onsave}
|
|
105
|
+
<!-- Action row is rendered in both modes (kept empty in readonly) so switching mode
|
|
106
|
+
doesn't shift the layout. The buttons only show in edit mode. -->
|
|
107
|
+
<FF_Cell content={{ config: { align: 'MiddleRight' } }}>
|
|
108
|
+
<div data-ff-form-actions>
|
|
109
|
+
{#if mode === 'edit'}
|
|
110
|
+
{#if ondelete}
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
data-danger
|
|
114
|
+
title="Delete"
|
|
115
|
+
disabled={busy || disableDelete}
|
|
116
|
+
onclick={ondelete}
|
|
117
|
+
>
|
|
118
|
+
<Icon size="1.05rem" data={LibIcon_Delete} />
|
|
119
|
+
</button>
|
|
120
|
+
{/if}
|
|
121
|
+
<button type="submit" data-primary disabled={busy || !canSave}>
|
|
122
|
+
<Icon size="1.05rem" data={LibIcon_Save} />
|
|
123
|
+
{saveLabel}
|
|
124
|
+
</button>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
</FF_Cell>
|
|
128
|
+
{/if}
|
|
129
|
+
</FF_Cell>
|
|
130
|
+
{/snippet}
|
|
131
|
+
|
|
132
|
+
{#if isForm}
|
|
133
|
+
<form data-ff-form onsubmit={submit}>
|
|
134
|
+
{@render body()}
|
|
135
|
+
{#if error}<p data-ff-form-error>{error}</p>{/if}
|
|
136
|
+
</form>
|
|
137
|
+
{:else}
|
|
138
|
+
<div data-ff-group>
|
|
139
|
+
{@render body()}
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EntityMetadata } from 'remult';
|
|
2
|
+
import type { CellInput } from './cellTypes.js';
|
|
3
|
+
declare function $$render<T extends object>(): {
|
|
4
|
+
props: {
|
|
5
|
+
/** Entity metadata (from the ff handle's `.meta`). */
|
|
6
|
+
meta: EntityMetadata<T>;
|
|
7
|
+
/** The record being shown/edited — bound directly (mutated in place in edit mode). */
|
|
8
|
+
draft: T;
|
|
9
|
+
cells?: CellInput<T>[];
|
|
10
|
+
/** 'edit' (inputs) or 'readonly' (values). */
|
|
11
|
+
mode?: "edit" | "readonly";
|
|
12
|
+
errors?: Record<string, string | undefined>;
|
|
13
|
+
error?: string;
|
|
14
|
+
busy?: boolean;
|
|
15
|
+
saveLabel?: string;
|
|
16
|
+
canSave?: boolean;
|
|
17
|
+
/** When provided (in edit mode) the group becomes a form with a Save action. */
|
|
18
|
+
onsave?: () => void | Promise<void>;
|
|
19
|
+
/** When provided, a Delete action is shown. Omit to hide it. */
|
|
20
|
+
ondelete?: () => void | Promise<void>;
|
|
21
|
+
/** Show Delete but disabled (pure UI). */
|
|
22
|
+
disableDelete?: boolean;
|
|
23
|
+
};
|
|
24
|
+
exports: {};
|
|
25
|
+
bindings: "";
|
|
26
|
+
slots: {};
|
|
27
|
+
events: {};
|
|
28
|
+
};
|
|
29
|
+
declare class __sveltets_Render<T extends object> {
|
|
30
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
31
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
32
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
33
|
+
bindings(): "";
|
|
34
|
+
exports(): {};
|
|
35
|
+
}
|
|
36
|
+
interface $$IsomorphicComponent {
|
|
37
|
+
new <T extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
38
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
39
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
40
|
+
<T extends object>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
41
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
42
|
+
}
|
|
43
|
+
declare const GroupFields: $$IsomorphicComponent;
|
|
44
|
+
type GroupFields<T extends object> = InstanceType<typeof GroupFields<T>>;
|
|
45
|
+
export default GroupFields;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import FF_Config from '../../FF_Config.svelte'
|
|
3
|
+
import FF_Cell from '../FF_Cell.svelte'
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<FF_Config cell={{ config: { label: { width: 33, order: 1, align: 'MiddleLeft' } } }}>
|
|
7
|
+
<!-- Pass an error so hasError=true and label width is NOT forced to 100 -->
|
|
8
|
+
<FF_Cell label={{ html: 'X' }} error={{ html: 'err' }} />
|
|
9
|
+
</FF_Config>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const FFCellContextWrapper: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type FFCellContextWrapper = InstanceType<typeof FFCellContextWrapper>;
|
|
18
|
+
export default FFCellContextWrapper;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EntityMetadata } from 'remult';
|
|
2
|
+
import type { Cell, CellInput } from './cellTypes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build headless cell descriptors from entity metadata.
|
|
5
|
+
* `cells` is a terse list of field keys and/or config objects; omit it to auto-build
|
|
6
|
+
* from visible fields. Per-cell config overrides the field's `ui` option (escape the SSoT).
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildCells<E>(meta: EntityMetadata<E>, cells?: CellInput<E>[], opts?: {
|
|
9
|
+
defaultSortable?: boolean;
|
|
10
|
+
}): Cell<E>[];
|
|
11
|
+
/**
|
|
12
|
+
* Formatted display string for a cell + row. Uses remult `field.displayValue` (which already
|
|
13
|
+
* formats enums/dates/value-lists). Relations/multi-enum that need richer rendering should use
|
|
14
|
+
* a `cellSnippet` escape hatch (Stage 0 does not auto-resolve the related row's caption).
|
|
15
|
+
*/
|
|
16
|
+
export declare function displayCell<E>(cell: Cell<E>, row: E): string;
|