firstly 0.10.8 → 0.12.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 +22 -0
- package/esm/changeLog/index.d.ts +1 -0
- package/esm/changeLog/server/index.d.ts +2 -2
- package/esm/changeLog/server/index.js +2 -2
- package/esm/core/FF_Allow.d.ts +3 -3
- package/esm/core/FF_Allow.js +3 -3
- package/esm/core/FF_Filter.d.ts +3 -3
- package/esm/core/FF_Filter.js +3 -3
- package/esm/core/FF_Validators.d.ts +2 -2
- package/esm/core/FF_Validators.js +2 -2
- package/esm/core/index.d.ts +1 -0
- package/esm/core/index.js +1 -1
- package/esm/cron/server/index.d.ts +1 -1
- package/esm/cron/server/index.js +1 -1
- package/esm/mail/ui/WriteMail.svelte +1 -1
- package/esm/sqlAdmin/index.d.ts +1 -0
- package/esm/sqlAdmin/index.js +1 -0
- package/esm/sqlAdmin/presetQueries.d.ts +8 -0
- package/esm/sqlAdmin/presetQueries.js +26 -0
- package/esm/sqlAdmin/server/index.d.ts +2 -2
- package/esm/sqlAdmin/server/index.js +2 -2
- package/esm/sqlAdmin/ui/SqlAdmin.svelte +12 -41
- package/esm/sqlAdmin/ui/SqlAdmin.svelte.d.ts +5 -1
- package/esm/sqlAdmin/ui/SqlDrift.svelte +41 -41
- package/esm/svelte/FF_DialogManager.svelte +4 -4
- package/esm/svelte/FF_PromptDefault.svelte +6 -6
- package/esm/svelte/class/SP.svelte.d.ts +1 -0
- package/esm/svelte/class/SP.svelte.js +14 -15
- package/esm/svelte/dialog.svelte.d.ts +1 -1
- package/esm/svelte/grid/FF_Grid.svelte +11 -8
- package/esm/svelte/grid/buildCells.js +6 -0
- package/esm/svelte/grid/cellTypes.d.ts +4 -0
- package/esm/svelte/handleError.d.ts +8 -2
- package/esm/svelte/handleError.js +24 -2
- package/esm/svelte/index.d.ts +1 -1
- package/esm/svelte/index.js +1 -1
- package/esm/svelte/query/index.svelte.d.ts +40 -0
- package/esm/svelte/query/index.svelte.js +112 -0
- package/esm/svelte/query/read.remote.d.ts +7 -0
- package/esm/svelte/query/read.remote.js +27 -0
- package/esm/svelte/remultApiLoad.d.ts +21 -9
- package/esm/svelte/remultApiLoad.js +21 -9
- package/esm/svelte/server/handleCaching.d.ts +2 -2
- package/esm/svelte/server/handleCaching.js +1 -1
- package/esm/svelte/ui/Icon.svelte +0 -1
- package/esm/vite/index.js +11 -1
- package/package.json +8 -3
- package/bin/args.d.ts +0 -13
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
type="button"
|
|
189
189
|
{onclick}
|
|
190
190
|
disabled={loading}
|
|
191
|
-
class="bg-secondary
|
|
191
|
+
class="bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50"
|
|
192
192
|
>
|
|
193
193
|
{loading ? 'Running…' : label}
|
|
194
194
|
</button>
|
|
@@ -199,19 +199,19 @@
|
|
|
199
199
|
type="button"
|
|
200
200
|
{onclick}
|
|
201
201
|
{disabled}
|
|
202
|
-
class="bg-primary
|
|
202
|
+
class="bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
|
203
203
|
>
|
|
204
204
|
{label}
|
|
205
205
|
</button>
|
|
206
206
|
{/snippet}
|
|
207
207
|
|
|
208
208
|
{#snippet resultTable(headers: string[], children: import('svelte').Snippet)}
|
|
209
|
-
<div class="
|
|
209
|
+
<div class="overflow-auto border border-border">
|
|
210
210
|
<table class="w-full border-collapse text-sm">
|
|
211
211
|
<thead class="bg-muted">
|
|
212
212
|
<tr>
|
|
213
213
|
{#each headers as h (h)}
|
|
214
|
-
<th class="border-
|
|
214
|
+
<th class="border-b border-border px-3 py-2 text-left font-semibold">{h}</th>
|
|
215
215
|
{/each}
|
|
216
216
|
</tr>
|
|
217
217
|
</thead>
|
|
@@ -222,18 +222,18 @@
|
|
|
222
222
|
</div>
|
|
223
223
|
{/snippet}
|
|
224
224
|
|
|
225
|
-
<div class="border-border bg-card text-card-foreground
|
|
225
|
+
<div class="border border-border bg-card text-card-foreground">
|
|
226
226
|
<header class="px-5 py-4">
|
|
227
227
|
<h2 class="text-lg font-semibold">Schema drift</h2>
|
|
228
|
-
<p class="
|
|
228
|
+
<p class="mt-1 text-sm text-muted-foreground">
|
|
229
229
|
Remult creates tables and adds columns, but never revisits them. Each check below is a dry run
|
|
230
230
|
first; applying runs in one transaction.
|
|
231
231
|
</p>
|
|
232
232
|
{#if flagsError}
|
|
233
233
|
<pre
|
|
234
|
-
class="border-destructive/40 bg-destructive/10
|
|
234
|
+
class="mt-3 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{flagsError}</pre>
|
|
235
235
|
{:else if flags && !Object.values(flags).some(Boolean)}
|
|
236
|
-
<p class="
|
|
236
|
+
<p class="mt-3 text-sm text-muted-foreground">
|
|
237
237
|
No check enabled. Opt in with <code class="font-mono"
|
|
238
238
|
>sqlAdmin({ drift: { entities, relationIndexes: true, ... } })</code
|
|
239
239
|
>.
|
|
@@ -243,9 +243,9 @@
|
|
|
243
243
|
|
|
244
244
|
<!-- Flow 1: primary keys -->
|
|
245
245
|
{#if flags?.primaryKeys}
|
|
246
|
-
<section class="border-
|
|
246
|
+
<section class="border-t border-border p-5">
|
|
247
247
|
<h3 class="text-sm font-semibold">Primary key drift</h3>
|
|
248
|
-
<p class="
|
|
248
|
+
<p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
|
|
249
249
|
Postgres only sets a PK on first table creation and never ALTERs it, so a later
|
|
250
250
|
<code class="font-mono">id</code> change is silently ignored. Applying runs every migration atomically.
|
|
251
251
|
</p>
|
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
|
|
262
262
|
{#if pkError}
|
|
263
263
|
<pre
|
|
264
|
-
class="border-destructive/40 bg-destructive/10
|
|
264
|
+
class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{pkError}</pre>
|
|
265
265
|
{/if}
|
|
266
266
|
|
|
267
267
|
{#if pkResult}
|
|
268
268
|
{#if pkResult.applied}
|
|
269
|
-
<p class="border-primary/30 bg-primary/10
|
|
269
|
+
<p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
|
|
270
270
|
Applied · {pkDrift} table{pkDrift === 1 ? '' : 's'} migrated
|
|
271
271
|
</p>
|
|
272
272
|
{/if}
|
|
@@ -274,15 +274,15 @@
|
|
|
274
274
|
{#snippet pkRows()}
|
|
275
275
|
{#each pkPlans as p (p.table)}
|
|
276
276
|
<tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
|
|
277
|
-
<td class="border-
|
|
278
|
-
<td class="border-border
|
|
277
|
+
<td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
|
|
278
|
+
<td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
|
|
279
279
|
{p.before.join(', ') || '-'}
|
|
280
280
|
</td>
|
|
281
|
-
<td class="border-border
|
|
281
|
+
<td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
|
|
282
282
|
{p.after.join(', ')}
|
|
283
283
|
</td>
|
|
284
284
|
<td
|
|
285
|
-
class="border-
|
|
285
|
+
class="border-b border-border px-3 py-2 font-mono"
|
|
286
286
|
class:text-primary={p.action === 'migrate' || p.action === 'create'}
|
|
287
287
|
class:text-muted-foreground={p.action === 'missing'}
|
|
288
288
|
>
|
|
@@ -298,9 +298,9 @@
|
|
|
298
298
|
|
|
299
299
|
<!-- Flow 2: relation indexes -->
|
|
300
300
|
{#if flags?.relationIndexes}
|
|
301
|
-
<section class="border-
|
|
301
|
+
<section class="border-t border-border p-5">
|
|
302
302
|
<h3 class="text-sm font-semibold">Relation indexes</h3>
|
|
303
|
-
<p class="
|
|
303
|
+
<p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
|
|
304
304
|
Postgres doesn't auto-index foreign keys. This indexes the FK columns of
|
|
305
305
|
<code class="font-mono">toOne</code> relations - unless they're already a leftmost prefix of an
|
|
306
306
|
existing index (the PK counts), so <code class="font-mono">a</code> on a
|
|
@@ -320,12 +320,12 @@
|
|
|
320
320
|
|
|
321
321
|
{#if relError}
|
|
322
322
|
<pre
|
|
323
|
-
class="border-destructive/40 bg-destructive/10
|
|
323
|
+
class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{relError}</pre>
|
|
324
324
|
{/if}
|
|
325
325
|
|
|
326
326
|
{#if relResult}
|
|
327
327
|
{#if relResult.applied}
|
|
328
|
-
<p class="border-primary/30 bg-primary/10
|
|
328
|
+
<p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
|
|
329
329
|
Created · {relMissing} index{relMissing === 1 ? '' : 'es'}
|
|
330
330
|
</p>
|
|
331
331
|
{/if}
|
|
@@ -333,12 +333,12 @@
|
|
|
333
333
|
{#snippet relRows()}
|
|
334
334
|
{#each relPlans as p (p.name)}
|
|
335
335
|
<tr class="even:bg-muted/40" class:opacity-50={p.action === 'ok'}>
|
|
336
|
-
<td class="border-
|
|
337
|
-
<td class="border-border
|
|
336
|
+
<td class="border-b border-border px-3 py-2 font-mono">{p.name}</td>
|
|
337
|
+
<td class="border-b border-border px-3 py-2 font-mono text-muted-foreground">
|
|
338
338
|
{p.table} ({p.columns.join(', ')})
|
|
339
339
|
</td>
|
|
340
340
|
<td
|
|
341
|
-
class="border-
|
|
341
|
+
class="border-b border-border px-3 py-2 font-mono"
|
|
342
342
|
class:text-primary={p.action === 'create'}
|
|
343
343
|
>
|
|
344
344
|
{p.action === 'ok'
|
|
@@ -357,9 +357,9 @@
|
|
|
357
357
|
|
|
358
358
|
<!-- Flow 3: drop orphan columns -->
|
|
359
359
|
{#if flags?.orphanColumns}
|
|
360
|
-
<section class="border-
|
|
360
|
+
<section class="border-t border-border p-5">
|
|
361
361
|
<h3 class="text-sm font-semibold">Orphan columns</h3>
|
|
362
|
-
<p class="
|
|
362
|
+
<p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
|
|
363
363
|
Remult only ever ADDs columns, never drops them, so a field removed from an entity leaves its
|
|
364
364
|
column behind. This lists live columns no entity field declares - e.g. a
|
|
365
365
|
<code class="font-mono">createdAt</code> now covered by the changelog. Destructive, so nothing
|
|
@@ -377,23 +377,23 @@
|
|
|
377
377
|
|
|
378
378
|
{#if dropError}
|
|
379
379
|
<pre
|
|
380
|
-
class="border-destructive/40 bg-destructive/10
|
|
380
|
+
class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{dropError}</pre>
|
|
381
381
|
{/if}
|
|
382
382
|
|
|
383
383
|
{#if dropResult}
|
|
384
384
|
{#if dropResult.applied}
|
|
385
|
-
<p class="border-primary/30 bg-primary/10
|
|
385
|
+
<p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
|
|
386
386
|
Dropped · {dropResult.dropped.length} column{dropResult.dropped.length === 1 ? '' : 's'}
|
|
387
387
|
</p>
|
|
388
388
|
{/if}
|
|
389
389
|
|
|
390
390
|
{#if dropPlans.length === 0}
|
|
391
|
-
<p class="text-muted-foreground
|
|
391
|
+
<p class="text-sm text-muted-foreground">No orphan columns - every column maps to a field.</p>
|
|
392
392
|
{:else}
|
|
393
393
|
{#snippet dropRows()}
|
|
394
394
|
{#each dropPlans as p (keyOf(p))}
|
|
395
395
|
<tr class="even:bg-muted/40">
|
|
396
|
-
<td class="border-
|
|
396
|
+
<td class="border-b border-border px-3 py-2">
|
|
397
397
|
<input
|
|
398
398
|
type="checkbox"
|
|
399
399
|
checked={selected.has(keyOf(p))}
|
|
@@ -401,8 +401,8 @@
|
|
|
401
401
|
aria-label={`Drop ${keyOf(p)}`}
|
|
402
402
|
/>
|
|
403
403
|
</td>
|
|
404
|
-
<td class="border-
|
|
405
|
-
<td class="border-border
|
|
404
|
+
<td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
|
|
405
|
+
<td class="border-b border-border px-3 py-2 font-mono text-primary">{p.column}</td>
|
|
406
406
|
</tr>
|
|
407
407
|
{/each}
|
|
408
408
|
{/snippet}
|
|
@@ -414,9 +414,9 @@
|
|
|
414
414
|
|
|
415
415
|
<!-- Flow 4: nullability drift -->
|
|
416
416
|
{#if flags?.nullable}
|
|
417
|
-
<section class="border-
|
|
417
|
+
<section class="border-t border-border p-5">
|
|
418
418
|
<h3 class="text-sm font-semibold">Nullability drift</h3>
|
|
419
|
-
<p class="
|
|
419
|
+
<p class="mt-1 mb-4 max-w-[80ch] text-sm text-muted-foreground">
|
|
420
420
|
A field that is not <code class="font-mono">allowNull</code> makes Remult create the column
|
|
421
421
|
<code class="font-mono">default '' not null</code>, and it never relaxes that afterwards. So a
|
|
422
422
|
column added before its field learned <code class="font-mono">allowNull</code> still hands every
|
|
@@ -439,25 +439,25 @@
|
|
|
439
439
|
|
|
440
440
|
{#if nullError}
|
|
441
441
|
<pre
|
|
442
|
-
class="border-destructive/40 bg-destructive/10
|
|
442
|
+
class="mb-4 overflow-auto border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">{nullError}</pre>
|
|
443
443
|
{/if}
|
|
444
444
|
|
|
445
445
|
{#if nullResult}
|
|
446
446
|
{#if nullResult.applied}
|
|
447
|
-
<p class="border-primary/30 bg-primary/10
|
|
447
|
+
<p class="mb-3 border border-primary/30 bg-primary/10 p-2 text-sm text-primary">
|
|
448
448
|
Fixed · {nullResult.fixed.length} column{nullResult.fixed.length === 1 ? '' : 's'}
|
|
449
449
|
</p>
|
|
450
450
|
{/if}
|
|
451
451
|
|
|
452
452
|
{#if nullPlans.length === 0}
|
|
453
|
-
<p class="text-muted-foreground
|
|
453
|
+
<p class="text-sm text-muted-foreground">
|
|
454
454
|
No drift - every nullable field has a plain nullable column.
|
|
455
455
|
</p>
|
|
456
456
|
{:else}
|
|
457
457
|
{#snippet nullRows()}
|
|
458
458
|
{#each nullPlans as p (keyOf(p))}
|
|
459
459
|
<tr class="even:bg-muted/40">
|
|
460
|
-
<td class="border-
|
|
460
|
+
<td class="border-b border-border px-3 py-2">
|
|
461
461
|
<input
|
|
462
462
|
type="checkbox"
|
|
463
463
|
checked={nullSelected.has(keyOf(p))}
|
|
@@ -465,14 +465,14 @@
|
|
|
465
465
|
aria-label={`Fix ${keyOf(p)}`}
|
|
466
466
|
/>
|
|
467
467
|
</td>
|
|
468
|
-
<td class="border-
|
|
469
|
-
<td class="border-border
|
|
470
|
-
<td class="border-
|
|
468
|
+
<td class="border-b border-border px-3 py-2 font-mono">{p.table}</td>
|
|
469
|
+
<td class="border-b border-border px-3 py-2 font-mono text-primary">{p.column}</td>
|
|
470
|
+
<td class="border-b border-border px-3 py-2 text-sm">
|
|
471
471
|
{[p.dropDefault && 'drop default', p.dropNotNull && 'drop not null']
|
|
472
472
|
.filter(Boolean)
|
|
473
473
|
.join(' · ')}
|
|
474
474
|
</td>
|
|
475
|
-
<td class="border-
|
|
475
|
+
<td class="border-b border-border px-3 py-2 text-sm">
|
|
476
476
|
{p.blankToNull ? "'' → NULL" : 'rows kept'}
|
|
477
477
|
</td>
|
|
478
478
|
</tr>
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
role="dialog"
|
|
197
197
|
aria-modal="true"
|
|
198
198
|
tabindex="-1"
|
|
199
|
-
class="
|
|
199
|
+
class="relative z-[1] max-h-[90vh] w-full border-border bg-background text-foreground {widthClass[
|
|
200
200
|
width
|
|
201
201
|
]} overflow-auto rounded-lg border p-5 shadow-xl"
|
|
202
202
|
>
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
<button
|
|
208
208
|
type="button"
|
|
209
209
|
aria-label="Close"
|
|
210
|
-
class="
|
|
210
|
+
class="absolute top-3 right-3 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:text-foreground"
|
|
211
211
|
onclick={dismiss}
|
|
212
212
|
>
|
|
213
213
|
✕
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
aria-labelledby={title ? `ff-dlg-title-${id}` : undefined}
|
|
243
243
|
aria-label={title ? undefined : message}
|
|
244
244
|
aria-describedby="ff-dlg-desc-{id}"
|
|
245
|
-
class="
|
|
245
|
+
class="relative z-[1] w-full max-w-sm rounded-lg border border-border bg-background p-5 text-foreground shadow-xl"
|
|
246
246
|
>
|
|
247
247
|
{#if title}
|
|
248
248
|
<h2 id="ff-dlg-title-{id}" class="mb-2 text-lg font-semibold">{title}</h2>
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
<div class="mt-5 flex justify-end gap-2">
|
|
252
252
|
<button
|
|
253
253
|
type="button"
|
|
254
|
-
class="
|
|
254
|
+
class="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
|
|
255
255
|
onclick={cancel}
|
|
256
256
|
>
|
|
257
257
|
{cancelLabel}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
tabindex="-1"
|
|
39
39
|
aria-labelledby={item.title ? `ff-prompt-title-${item.id}` : undefined}
|
|
40
40
|
aria-label={item.title ? undefined : item.label ? resolveMessage(item.label) : undefined}
|
|
41
|
-
class="
|
|
41
|
+
class="relative z-[1] w-full max-w-sm rounded-lg border border-border bg-background p-5 text-foreground shadow-xl"
|
|
42
42
|
>
|
|
43
43
|
<form
|
|
44
44
|
onsubmit={(e) => {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
</h2>
|
|
53
53
|
{/if}
|
|
54
54
|
{#if item.label}
|
|
55
|
-
<label class="
|
|
55
|
+
<label class="mb-1 block text-sm text-muted-foreground" for="ff-prompt-{item.id}">
|
|
56
56
|
{resolveMessage(item.label)}
|
|
57
57
|
</label>
|
|
58
58
|
{/if}
|
|
@@ -60,22 +60,22 @@
|
|
|
60
60
|
id="ff-prompt-{item.id}"
|
|
61
61
|
bind:value
|
|
62
62
|
placeholder={item.placeholder}
|
|
63
|
-
class="
|
|
63
|
+
class="w-full rounded-md border border-border bg-card px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
|
64
64
|
/>
|
|
65
65
|
{#if item.hint}
|
|
66
|
-
<p class="
|
|
66
|
+
<p class="mt-1 text-xs text-muted-foreground">{item.hint(value)}</p>
|
|
67
67
|
{/if}
|
|
68
68
|
<div class="mt-5 flex justify-end gap-2">
|
|
69
69
|
<button
|
|
70
70
|
type="button"
|
|
71
|
-
class="
|
|
71
|
+
class="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted"
|
|
72
72
|
onclick={oncancel}
|
|
73
73
|
>
|
|
74
74
|
{resolveMessage(item.cancelLabel ?? cfg.messages.cancel)}
|
|
75
75
|
</button>
|
|
76
76
|
<button
|
|
77
77
|
type="submit"
|
|
78
|
-
class="
|
|
78
|
+
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
79
79
|
>
|
|
80
80
|
{resolveMessage(item.confirmLabel ?? cfg.messages.ok)}
|
|
81
81
|
</button>
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
// FIXME
|
|
2
|
-
// Why packages/ui pnpm check fails with goto and page?
|
|
3
|
-
// - OK it's not a sveltekit project... But this is never used! So why is it checking it ?!
|
|
4
|
-
// @ts-ignore
|
|
5
1
|
import { goto } from '$app/navigation';
|
|
6
|
-
// @ts-ignore
|
|
7
2
|
import { page } from '$app/state';
|
|
8
3
|
import { debounce } from '../helpers/debounce.js';
|
|
9
4
|
const CONFIG_DELIMITER = ';';
|
|
@@ -23,6 +18,7 @@ export class SP {
|
|
|
23
18
|
computing = $state(false);
|
|
24
19
|
// Store debounced toURL functions for each parameter
|
|
25
20
|
debouncedToURL = {};
|
|
21
|
+
lastNavigation;
|
|
26
22
|
// Shared debounce mechanism for all parameters
|
|
27
23
|
sharedDebouncePending = $state(false);
|
|
28
24
|
activeDebounces = $state({});
|
|
@@ -37,9 +33,8 @@ export class SP {
|
|
|
37
33
|
for (const key of Object.keys(this.paramValues)) {
|
|
38
34
|
this.debouncedValues[key] = this.paramValues[key];
|
|
39
35
|
}
|
|
40
|
-
//
|
|
36
|
+
// `computing` stays on until toURL's navigation lands
|
|
41
37
|
this.sharedDebouncePending = false;
|
|
42
|
-
this.computing = false;
|
|
43
38
|
this.longestDebounceTime = 0;
|
|
44
39
|
this.toURL();
|
|
45
40
|
};
|
|
@@ -393,16 +388,21 @@ export class SP {
|
|
|
393
388
|
}
|
|
394
389
|
const strSearch = params.toString() ? '?' + params.toString() : '';
|
|
395
390
|
// Don't do the goto if the search params haven't changed!
|
|
396
|
-
if (strSearch === window.location.search)
|
|
391
|
+
if (strSearch === window.location.search) {
|
|
392
|
+
this.computing = false;
|
|
397
393
|
return;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
394
|
+
}
|
|
395
|
+
const navigation = goto(`${window.location.pathname}${strSearch}`, {
|
|
396
|
+
replace: true,
|
|
397
|
+
reset: false,
|
|
402
398
|
...this.options.gotoOpts,
|
|
403
399
|
});
|
|
404
|
-
|
|
405
|
-
|
|
400
|
+
this.lastNavigation = navigation;
|
|
401
|
+
// A newer navigation or a pending debounce keeps `computing` on
|
|
402
|
+
void navigation.finally(() => {
|
|
403
|
+
if (this.lastNavigation === navigation && !this.sharedDebouncePending)
|
|
404
|
+
this.computing = false;
|
|
405
|
+
});
|
|
406
406
|
}
|
|
407
407
|
/**
|
|
408
408
|
* Reset all parameters to their default values
|
|
@@ -418,7 +418,6 @@ export class SP {
|
|
|
418
418
|
this.sharedDebouncePending = false;
|
|
419
419
|
this.longestDebounceTime = 0;
|
|
420
420
|
// Update URL immediately without debounce
|
|
421
|
-
this.computing = false;
|
|
422
421
|
this.toURL();
|
|
423
422
|
}
|
|
424
423
|
}
|
|
@@ -15,7 +15,7 @@ export { resolveMessage } from '../core/FF_Validators.js';
|
|
|
15
15
|
* cancelled/dismissed - no per-method `boolean` / `null` special-casing.
|
|
16
16
|
*
|
|
17
17
|
* ```svelte
|
|
18
|
-
* import { dialog } from '
|
|
18
|
+
* import { dialog } from 'firstly/svelte'
|
|
19
19
|
*
|
|
20
20
|
* {#snippet body(close)}
|
|
21
21
|
* <input bind:value={name} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object">
|
|
2
|
-
// PUBLISHED batteries-included grid — `import { FF_Grid } from '
|
|
2
|
+
// PUBLISHED batteries-included grid — `import { FF_Grid } from 'firstly/svelte'` and go, zero setup:
|
|
3
3
|
// it bundles a default input + a neutral default skin. Reads the entity `hub` (every prop overrides).
|
|
4
4
|
// For full control over markup + look, copy the boutique <App_Grid> instead (degit src/boutique/grid).
|
|
5
5
|
import { untrack } from 'svelte'
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import { errorMessage } from '../../core/helper.js'
|
|
11
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
12
|
import { ff } from '../ff.svelte.js'
|
|
15
13
|
import type { FF_Many, ManyStrategy } from '../ff.svelte.js'
|
|
14
|
+
import FF_Config from '../FF_Config.svelte'
|
|
15
|
+
import { ffConfig } from '../FF_Config.svelte.js'
|
|
16
16
|
import Icon from '../ui/Icon.svelte'
|
|
17
17
|
import { LibIcon_Add, LibIcon_Edit } from '../ui/LibIcon.js'
|
|
18
18
|
import { buildCells } from './buildCells.js'
|
|
@@ -61,7 +61,10 @@
|
|
|
61
61
|
const strategy = untrack(() => strategyProp ?? hub.strategy ?? 'paginate')
|
|
62
62
|
const pageSize = untrack(() => pageSizeProp ?? hub.pageSize ?? 25)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// fall back to the entity default so the header arrow shows what remult applies on landing
|
|
65
|
+
let sort = $state<EntityOrderBy<T> | undefined>(
|
|
66
|
+
untrack(() => orderBy ?? hub.orderBy ?? repo(entity).metadata.options.defaultOrderBy),
|
|
67
|
+
)
|
|
65
68
|
|
|
66
69
|
const m = untrack(() =>
|
|
67
70
|
ff(entity).many(
|
|
@@ -101,9 +104,9 @@
|
|
|
101
104
|
const newIcon = $derived(insertCfg !== false ? (insertCfg.icon ?? LibIcon_Add) : LibIcon_Add)
|
|
102
105
|
const editIcon = $derived(updateCfg !== false ? (updateCfg.icon ?? LibIcon_Edit) : LibIcon_Edit)
|
|
103
106
|
|
|
104
|
-
function toggleSort(key: string) {
|
|
107
|
+
function toggleSort(key: string, first: 'asc' | 'desc') {
|
|
105
108
|
const cur = (sort as Record<string, 'asc' | 'desc'> | undefined)?.[key]
|
|
106
|
-
sort = { [key]: cur === 'asc' ? 'desc' : 'asc' } as EntityOrderBy<T>
|
|
109
|
+
sort = { [key]: cur ? (cur === 'asc' ? 'desc' : 'asc') : first } as EntityOrderBy<T>
|
|
107
110
|
}
|
|
108
111
|
const sortDir = (key: string) => (sort as Record<string, string> | undefined)?.[key]
|
|
109
112
|
|
|
@@ -185,9 +188,9 @@
|
|
|
185
188
|
data-sortable={cell.sortable || undefined}
|
|
186
189
|
style:text-align={cell.align}
|
|
187
190
|
class={cell.class}
|
|
188
|
-
onclick={() => cell.sortable && cell.col && toggleSort(cell.col)}
|
|
191
|
+
onclick={() => cell.sortable && cell.col && toggleSort(cell.col, cell.firstSort)}
|
|
189
192
|
>
|
|
190
|
-
{cell.caption}{#if cell.col && sortDir(cell.col)}<span data-sort
|
|
193
|
+
{cell.caption}{#if cell.sortable && cell.col && sortDir(cell.col)}<span data-sort
|
|
191
194
|
>{sortDir(cell.col) === 'asc' ? ' ▲' : ' ▼'}</span
|
|
192
195
|
>{/if}
|
|
193
196
|
</th>
|
|
@@ -15,6 +15,11 @@ function alignFor(field, ui) {
|
|
|
15
15
|
return 'right';
|
|
16
16
|
return 'left';
|
|
17
17
|
}
|
|
18
|
+
// newest / biggest first is the useful first view for dates + numbers
|
|
19
|
+
function firstSortFor(field) {
|
|
20
|
+
const t = field?.valueType;
|
|
21
|
+
return t === Date || t === Number ? 'desc' : 'asc';
|
|
22
|
+
}
|
|
18
23
|
/** Resolve the render kind: explicit > href(field_link) > metaKind(relation/enum) > field. */
|
|
19
24
|
function resolveKind(field, explicit) {
|
|
20
25
|
if (explicit)
|
|
@@ -58,6 +63,7 @@ export function buildCells(meta, cells, opts) {
|
|
|
58
63
|
inputType: field ? getInputType(field, ui) : 'text',
|
|
59
64
|
align: alignFor(field, ui),
|
|
60
65
|
sortable,
|
|
66
|
+
firstSort: (isObj && item.firstSort) || firstSortFor(field),
|
|
61
67
|
class: isObj ? item.class : undefined,
|
|
62
68
|
cellSnippet: isObj ? item.cellSnippet : undefined,
|
|
63
69
|
component: isObj ? item.component : undefined,
|
|
@@ -41,6 +41,8 @@ export interface Cell<E = any> {
|
|
|
41
41
|
align: 'left' | 'center' | 'right';
|
|
42
42
|
/** Whether this column is sortable by header click (resolved; opt in via `sortable: true`). */
|
|
43
43
|
sortable: boolean;
|
|
44
|
+
/** Direction of the first header click on an unsorted column. */
|
|
45
|
+
firstSort: 'asc' | 'desc';
|
|
44
46
|
/** Tailwind/CSS passthrough (e.g. 'col-span-2'). */
|
|
45
47
|
class?: string;
|
|
46
48
|
cellSnippet?: Snippet<[{
|
|
@@ -62,6 +64,8 @@ export type CellInput<E> = (keyof E & string) | {
|
|
|
62
64
|
class?: string;
|
|
63
65
|
/** Set true to make this column sortable by header click (default: not sortable). */
|
|
64
66
|
sortable?: boolean;
|
|
67
|
+
/** First header click direction. Default: `desc` for Date/Number fields, else `asc`. */
|
|
68
|
+
firstSort?: 'asc' | 'desc';
|
|
65
69
|
cellSnippet?: Snippet<[{
|
|
66
70
|
row: E;
|
|
67
71
|
cell: Cell<E>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HandleClientError } from '@sveltejs/kit';
|
|
1
|
+
import type { HandleClientError } from '@sveltejs/kit/hooks';
|
|
2
2
|
export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleClientError;
|
|
3
3
|
/**
|
|
4
4
|
* Compose `handleError` middlewares into a single `hooks.client.js` handler.
|
|
@@ -8,7 +8,7 @@ export type HandleClientErrorMiddleware = (next: HandleClientError) => HandleCli
|
|
|
8
8
|
*
|
|
9
9
|
* ```js
|
|
10
10
|
* // src/hooks.client.js
|
|
11
|
-
* import { stackHandleClientError, withStaleDeployReload } from '
|
|
11
|
+
* import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
|
|
12
12
|
*
|
|
13
13
|
* export const handleError = stackHandleClientError(
|
|
14
14
|
* withStaleDeployReload(),
|
|
@@ -29,3 +29,9 @@ export declare function stackHandleClientError(...middlewares: HandleClientError
|
|
|
29
29
|
* falls through to `next`.
|
|
30
30
|
*/
|
|
31
31
|
export declare function withStaleDeployReload(): HandleClientErrorMiddleware;
|
|
32
|
+
/**
|
|
33
|
+
* Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
|
|
34
|
+
* only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
|
|
35
|
+
* at the top of `hooks.client.js`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function reloadOnStaleChunk(): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { navigating, updated } from '$app/state';
|
|
1
2
|
/**
|
|
2
3
|
* Compose `handleError` middlewares into a single `hooks.client.js` handler.
|
|
3
4
|
* Middlewares run outermost-first (the first argument wraps the rest). A middleware
|
|
@@ -6,7 +7,7 @@
|
|
|
6
7
|
*
|
|
7
8
|
* ```js
|
|
8
9
|
* // src/hooks.client.js
|
|
9
|
-
* import { stackHandleClientError, withStaleDeployReload } from '
|
|
10
|
+
* import { stackHandleClientError, withStaleDeployReload } from 'firstly/svelte'
|
|
10
11
|
*
|
|
11
12
|
* export const handleError = stackHandleClientError(
|
|
12
13
|
* withStaleDeployReload(),
|
|
@@ -22,6 +23,8 @@ export function stackHandleClientError(...middlewares) {
|
|
|
22
23
|
}
|
|
23
24
|
// Errors SvelteKit/Vite throw when a lazy chunk 404s after a deploy.
|
|
24
25
|
const CHUNK_LOAD_RE = /Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/;
|
|
26
|
+
// Kit recovers route chunks itself, and one failing on hover must not reload the page.
|
|
27
|
+
const ROUTE_CHUNK_RE = /\/immutable\/nodes\//;
|
|
25
28
|
const STORAGE_KEY = 'ff-reload';
|
|
26
29
|
// A URL that failed again within this window is a genuine break, not a stale deploy.
|
|
27
30
|
const RETRY_WINDOW_MS = 10_000;
|
|
@@ -35,12 +38,19 @@ const RETRY_WINDOW_MS = 10_000;
|
|
|
35
38
|
*/
|
|
36
39
|
export function withStaleDeployReload() {
|
|
37
40
|
return (next) => (input) => {
|
|
38
|
-
if (
|
|
41
|
+
if (input.kind === 'unknown' &&
|
|
42
|
+
CHUNK_LOAD_RE.test(String(input.error)) &&
|
|
43
|
+
isVisited(input.event.url) &&
|
|
44
|
+
reloadOnce(input.event.url.href)) {
|
|
39
45
|
return; // page is unloading; no error surfaces
|
|
40
46
|
}
|
|
41
47
|
return next(input);
|
|
42
48
|
};
|
|
43
49
|
}
|
|
50
|
+
// A hovered link preloads through handleError too, and must not navigate on its own.
|
|
51
|
+
function isVisited(url) {
|
|
52
|
+
return url.href === location.href || url.href === navigating.to?.url.href;
|
|
53
|
+
}
|
|
44
54
|
/** Hard-reload `href` unless it already failed within the guard window. Returns true if reloading. */
|
|
45
55
|
function reloadOnce(href) {
|
|
46
56
|
try {
|
|
@@ -60,3 +70,15 @@ function reloadOnce(href) {
|
|
|
60
70
|
return false; // no sessionStorage (private mode) -> don't risk a loop
|
|
61
71
|
}
|
|
62
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Recovers what Kit leaves out after a deploy: a lazy `import()` inside a component. Reloads
|
|
75
|
+
* only when `updated.check()` confirms a new version, so a flaky network can't loop. Call it
|
|
76
|
+
* at the top of `hooks.client.js`.
|
|
77
|
+
*/
|
|
78
|
+
export function reloadOnStaleChunk() {
|
|
79
|
+
addEventListener('vite:preloadError', (event) => {
|
|
80
|
+
if (ROUTE_CHUNK_RE.test(String(event.payload)))
|
|
81
|
+
return;
|
|
82
|
+
void updated.check().then((stale) => stale && location.reload());
|
|
83
|
+
});
|
|
84
|
+
}
|
package/esm/svelte/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { ff } from './ff.svelte.js';
|
|
|
3
3
|
export type { FF_Many, FF_One, FF_Builder, FF_RepoOptions, FF_OneOptions, FF_RepoLoading, FF_Issue, ManyStrategy, AggregateOptions, QueryOptionsHelper, } from './ff.svelte.js';
|
|
4
4
|
export { infiniteScroll } from './infiniteScroll.js';
|
|
5
5
|
export type { InfiniteScrollOptions } from './infiniteScroll.js';
|
|
6
|
-
export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
|
|
6
|
+
export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
|
|
7
7
|
export type { HandleClientErrorMiddleware } from './handleError.js';
|
|
8
8
|
export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
|
|
9
9
|
export type { DialogResult, DialogClose, DialogOptions, DialogItem, DialogRender, ConfirmItem, PromptItem, DialogShellArgs, DialogConfirmArgs, DialogPromptArgs, } from './dialog.svelte.js';
|
package/esm/svelte/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { errorMessage, isError } from '../core/helper.js';
|
|
2
2
|
export { ff } from './ff.svelte.js';
|
|
3
3
|
export { infiniteScroll } from './infiniteScroll.js';
|
|
4
|
-
export { stackHandleClientError, withStaleDeployReload } from './handleError.js';
|
|
4
|
+
export { reloadOnStaleChunk, stackHandleClientError, withStaleDeployReload } from './handleError.js';
|
|
5
5
|
export { dialog, ffAutofocus, ffTrapFocus, resolveMessage } from './dialog.svelte.js';
|
|
6
6
|
export { default as FF_DialogManager } from './FF_DialogManager.svelte';
|
|
7
7
|
export { default as FF_Config } from './FF_Config.svelte';
|