firstly 0.5.0 → 0.5.1
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 +8 -0
- package/esm/svelte/DemoGrid.svelte +167 -0
- package/esm/svelte/DemoGrid.svelte.d.ts +40 -0
- package/esm/svelte/FF_Repo.svelte.d.ts +37 -30
- package/esm/svelte/FF_Repo.svelte.js +28 -35
- package/esm/svelte/index.d.ts +1 -0
- package/esm/svelte/index.js +1 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# firstly
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#282](https://github.com/jycouet/firstly/pull/282) [`a93d4c8`](https://github.com/jycouet/firstly/commit/a93d4c8815da30f2c4d701ddd7e3d8cdf39fd8f5) Thanks [@jycouet](https://github.com/jycouet)! - ffRepo (svelte): leaner surface. Rename `firstOnce` → `onFirst`; remove `draft`, `first`, `insert`, `update`, `deleteMany`. List handles (`load`/`listen`/`paginate`) are now read-only - write via `.repo` (+ `addItem`/`updateItem`/`removeItem` to reconcile). Editing lives on `one`/`create()` with argless `save()`/`delete()`.
|
|
8
|
+
|
|
9
|
+
New `DemoGrid` (from `firstly/svelte`): a generic inline-CRUD table over any entity - props `entity` + `fields`, headers/placeholders from each field's `caption`.
|
|
10
|
+
|
|
3
11
|
## 0.5.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends { id: string }">
|
|
2
|
+
import type { ClassType, EntityFilter, MembersOnly } from 'remult'
|
|
3
|
+
|
|
4
|
+
import { ffRepo } from './FF_Repo.svelte.js'
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
/** The remult entity class to CRUD. */
|
|
8
|
+
entity: ClassType<T>
|
|
9
|
+
/** Fields to show as columns / edit inputs. Headers come from each field's `caption`. */
|
|
10
|
+
fields: (keyof T & string)[]
|
|
11
|
+
}
|
|
12
|
+
let { entity, fields }: Props = $props()
|
|
13
|
+
|
|
14
|
+
// live list - any write re-emits it, so no reconcile code (entity is static config)
|
|
15
|
+
const list = ffRepo(entity).listen(() => ({}))
|
|
16
|
+
|
|
17
|
+
// the row being edited (by id), or a fresh draft for "new" - one reactive slot
|
|
18
|
+
let editingId = $state<string | null>(null)
|
|
19
|
+
const editor = ffRepo(entity).one(() => ({
|
|
20
|
+
where: { id: editingId ?? '' } as EntityFilter<T>,
|
|
21
|
+
enabled: !!editingId,
|
|
22
|
+
}))
|
|
23
|
+
const creating = $derived(!!editor.item && !editor.item.id)
|
|
24
|
+
|
|
25
|
+
// dynamic read/write of a field by key (v1: inputs are text)
|
|
26
|
+
const get = (row: T, f: keyof T & string) => (row as Record<string, unknown>)[f]
|
|
27
|
+
function setField(f: keyof T & string, value: string) {
|
|
28
|
+
if (editor.item) (editor.item as Record<string, unknown>)[f] = value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function edit(id: string) {
|
|
32
|
+
editingId = id // → editor.item loads that row
|
|
33
|
+
}
|
|
34
|
+
function add() {
|
|
35
|
+
editingId = null
|
|
36
|
+
editor.create() // blank draft into editor.item
|
|
37
|
+
}
|
|
38
|
+
function cancel() {
|
|
39
|
+
editingId = null
|
|
40
|
+
editor.item = undefined // drop the draft / stop editing
|
|
41
|
+
}
|
|
42
|
+
async function save() {
|
|
43
|
+
await editor.save() // insert (a draft) or update (the loaded row); the live list self-syncs
|
|
44
|
+
cancel()
|
|
45
|
+
}
|
|
46
|
+
async function remove(row: T) {
|
|
47
|
+
await list.repo.delete(row as Partial<MembersOnly<T>>) // raw delete via .repo; the live list drops the row
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
{#snippet editRow(draft: T)}
|
|
52
|
+
{#each fields as f (f)}
|
|
53
|
+
<td>
|
|
54
|
+
<input
|
|
55
|
+
placeholder={list.meta.fields.find(f)?.caption ?? f}
|
|
56
|
+
value={String(get(draft, f) ?? '')}
|
|
57
|
+
oninput={(e) => setField(f, e.currentTarget.value)}
|
|
58
|
+
/>
|
|
59
|
+
</td>
|
|
60
|
+
{/each}
|
|
61
|
+
<td class="actions">
|
|
62
|
+
<button disabled={editor.loading.saving} onclick={save}>Save</button>
|
|
63
|
+
<button onclick={cancel}>Cancel</button>
|
|
64
|
+
</td>
|
|
65
|
+
{/snippet}
|
|
66
|
+
|
|
67
|
+
<div class="crud">
|
|
68
|
+
<button class="new" onclick={add}>+ New</button>
|
|
69
|
+
|
|
70
|
+
<table>
|
|
71
|
+
<thead>
|
|
72
|
+
<tr>
|
|
73
|
+
{#each fields as f (f)}<th>{list.meta.fields.find(f)?.caption ?? f}</th>{/each}
|
|
74
|
+
<th></th>
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
{#if creating && editor.item}
|
|
79
|
+
<tr class="editing">{@render editRow(editor.item)}</tr>
|
|
80
|
+
{/if}
|
|
81
|
+
{#each list.items as row (row.id)}
|
|
82
|
+
<tr class:editing={editingId === row.id}>
|
|
83
|
+
{#if editingId === row.id && editor.item}
|
|
84
|
+
{@render editRow(editor.item)}
|
|
85
|
+
{:else}
|
|
86
|
+
{#each fields as f (f)}<td>{get(row, f)}</td>{/each}
|
|
87
|
+
<td class="actions">
|
|
88
|
+
<button onclick={() => edit(row.id)}>Edit</button>
|
|
89
|
+
<button onclick={() => remove(row)}>Delete</button>
|
|
90
|
+
</td>
|
|
91
|
+
{/if}
|
|
92
|
+
</tr>
|
|
93
|
+
{/each}
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
|
|
97
|
+
{#if list.loading.init}
|
|
98
|
+
<p class="muted">Loading…</p>
|
|
99
|
+
{:else if list.items.length === 0 && !creating}
|
|
100
|
+
<p class="muted">Nothing yet - hit “+ New”.</p>
|
|
101
|
+
{/if}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.crud {
|
|
106
|
+
font-size: 14px;
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-direction: column;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
align-items: start;
|
|
111
|
+
}
|
|
112
|
+
table {
|
|
113
|
+
border-collapse: collapse;
|
|
114
|
+
width: 100%;
|
|
115
|
+
}
|
|
116
|
+
th,
|
|
117
|
+
td {
|
|
118
|
+
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
|
119
|
+
padding: 7px 10px;
|
|
120
|
+
text-align: left;
|
|
121
|
+
vertical-align: middle;
|
|
122
|
+
}
|
|
123
|
+
th {
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
background: color-mix(in srgb, currentColor 7%, transparent);
|
|
126
|
+
}
|
|
127
|
+
tr.editing {
|
|
128
|
+
background: color-mix(in srgb, currentColor 5%, transparent);
|
|
129
|
+
}
|
|
130
|
+
td.actions {
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
text-align: right;
|
|
133
|
+
}
|
|
134
|
+
td.actions button + button {
|
|
135
|
+
margin-left: 6px;
|
|
136
|
+
}
|
|
137
|
+
input {
|
|
138
|
+
width: 100%;
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
padding: 5px 7px;
|
|
141
|
+
font: inherit;
|
|
142
|
+
color: inherit;
|
|
143
|
+
background: transparent;
|
|
144
|
+
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
|
145
|
+
border-radius: 6px;
|
|
146
|
+
}
|
|
147
|
+
button {
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
font: inherit;
|
|
150
|
+
padding: 4px 11px;
|
|
151
|
+
color: inherit;
|
|
152
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
153
|
+
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
transition: background 0.12s ease;
|
|
156
|
+
}
|
|
157
|
+
button:hover {
|
|
158
|
+
background: color-mix(in srgb, currentColor 14%, transparent);
|
|
159
|
+
}
|
|
160
|
+
button:disabled {
|
|
161
|
+
opacity: 0.45;
|
|
162
|
+
cursor: not-allowed;
|
|
163
|
+
}
|
|
164
|
+
.muted {
|
|
165
|
+
opacity: 0.6;
|
|
166
|
+
}
|
|
167
|
+
</style>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ClassType } from 'remult';
|
|
2
|
+
declare function $$render<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}>(): {
|
|
5
|
+
props: {
|
|
6
|
+
/** The remult entity class to CRUD. */
|
|
7
|
+
entity: ClassType<T>;
|
|
8
|
+
/** Fields to show as columns / edit inputs. Headers come from each field's `caption`. */
|
|
9
|
+
fields: (keyof T & string)[];
|
|
10
|
+
};
|
|
11
|
+
exports: {};
|
|
12
|
+
bindings: "";
|
|
13
|
+
slots: {};
|
|
14
|
+
events: {};
|
|
15
|
+
};
|
|
16
|
+
declare class __sveltets_Render<T extends {
|
|
17
|
+
id: string;
|
|
18
|
+
}> {
|
|
19
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
20
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
21
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
22
|
+
bindings(): "";
|
|
23
|
+
exports(): {};
|
|
24
|
+
}
|
|
25
|
+
interface $$IsomorphicComponent {
|
|
26
|
+
new <T extends {
|
|
27
|
+
id: string;
|
|
28
|
+
}>(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']>> & {
|
|
29
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
30
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
31
|
+
<T extends {
|
|
32
|
+
id: string;
|
|
33
|
+
}>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
34
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
35
|
+
}
|
|
36
|
+
declare const DemoGrid: $$IsomorphicComponent;
|
|
37
|
+
type DemoGrid<T extends {
|
|
38
|
+
id: string;
|
|
39
|
+
}> = InstanceType<typeof DemoGrid<T>>;
|
|
40
|
+
export default DemoGrid;
|
|
@@ -29,12 +29,12 @@ import { type ClassType, type EntityFilter, type EntityMetadata, type EntityOrde
|
|
|
29
29
|
* the moment it flips true - use it for search-min-length, tab visibility,
|
|
30
30
|
* dependent queries, or a manual button trigger.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* Writes: only the record handle (`one` / `create()`) writes - argless `save()` / `delete()`
|
|
33
|
+
* act on its `item` and re-sync it. List handles (`load` / `listen` / `paginate`) don't write;
|
|
34
|
+
* go through `.repo` (the plain remult repo: `insert` / `update` / `save` / `delete` /
|
|
35
|
+
* `deleteMany`), then reflect it in a `load` / `paginate` list with the local reconcilers
|
|
36
|
+
* (`addItem` / `updateItem` / `removeItem`) - a `listen` list re-syncs itself via the liveQuery.
|
|
37
|
+
* A failed write fills `error` and re-throws.
|
|
38
38
|
*
|
|
39
39
|
* The factory's return type is mode-specific, so e.g. `.more()` doesn't exist on
|
|
40
40
|
* a `listen()` handle. Methods also throw if reached via a cast in the wrong mode.
|
|
@@ -113,29 +113,36 @@ declare class FF_RepoHandle<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOp
|
|
|
113
113
|
hasNextPage: boolean;
|
|
114
114
|
/** Aggregations for the whole query (paginate mode). `aggregates.$count` is the total row count. */
|
|
115
115
|
aggregates: ExtractAggregateResult<Entity, O> | undefined;
|
|
116
|
-
first: Entity | null;
|
|
117
116
|
constructor(r: Repository<Entity>, opts: Getter<Entity, O>, mode: Mode);
|
|
118
117
|
/** Re-run the current query (load/paginate/one), back to the first page. */
|
|
119
118
|
refresh(): Promise<void>;
|
|
120
119
|
/** Load and append the next page (paginate mode). */
|
|
121
120
|
more(): Promise<void>;
|
|
122
121
|
/**
|
|
123
|
-
* Run `fn` once
|
|
124
|
-
*
|
|
125
|
-
*
|
|
122
|
+
* Run `fn` once - the first time a row exists (`items[0]`).
|
|
123
|
+
*
|
|
124
|
+
* The point: seed editable UI state from the latest row WITHOUT a live query
|
|
125
|
+
* clobbering in-progress edits. It fires a single time, on the first non-empty
|
|
126
|
+
* result, and never again - later ticks (an edit, a delete, a re-sort) are
|
|
127
|
+
* ignored. Empty snapshots are skipped (a liveQuery often emits one before the
|
|
128
|
+
* data lands; there is nothing to seed from an empty result).
|
|
129
|
+
*
|
|
130
|
+
* For pure derived state prefer `$derived`; reach for `onFirst` only when the
|
|
131
|
+
* seed must become independently editable (a draft the user then mutates).
|
|
132
|
+
*
|
|
133
|
+
* ```svelte
|
|
134
|
+
* const list = ffRepo(Plan).listen(() => ({ where: { ownerDid } }))
|
|
135
|
+
* let draft = $state({ title: '' })
|
|
136
|
+
* list.onFirst((latest) => (draft.title = latest.title)) // seed once, then edit freely
|
|
137
|
+
* ```
|
|
126
138
|
*/
|
|
127
|
-
|
|
128
|
-
/** Reactive, bindable editor state seeded once from the latest row. */
|
|
129
|
-
draft<D extends Record<string, unknown>>(seed: (latest: Entity | null) => D): D;
|
|
139
|
+
onFirst(fn: (latest: Entity) => void): void;
|
|
130
140
|
/** Create a new unsaved entity into the `item` slot (for an edit form). */
|
|
131
141
|
create(...args: Parameters<Repository<Entity>['create']>): Entity;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
/** Delete. With no argument, deletes the current `item`. */
|
|
137
|
-
delete(...args: [] | Parameters<Repository<Entity>['delete']>): Promise<void>;
|
|
138
|
-
deleteMany(...args: Parameters<Repository<Entity>['deleteMany']>): Promise<number>;
|
|
142
|
+
/** Save the current `item` (from `one` / `create()`). To save a specific row, use `.repo.save(row)`. */
|
|
143
|
+
save(): Promise<Entity>;
|
|
144
|
+
/** Delete the current `item`. To delete a specific row/id, use `.repo.delete(idOrRow)`. */
|
|
145
|
+
delete(): Promise<void>;
|
|
139
146
|
/** Insert into `items` at `top` (default) / `bottom` / an index (`-1` = last). +1 to `$count`. */
|
|
140
147
|
addItem(item: Entity, options?: {
|
|
141
148
|
at?: 'top' | 'bottom' | number;
|
|
@@ -154,20 +161,20 @@ declare class FF_RepoHandle<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOp
|
|
|
154
161
|
/** Escape hatch to the underlying repo (count, findId, upsert, projections, ...). */
|
|
155
162
|
get repo(): Repository<Entity>;
|
|
156
163
|
}
|
|
157
|
-
/** load: one-shot list
|
|
158
|
-
export type FF_RepoLoad<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'more' | 'hasNextPage' | 'aggregates'>;
|
|
159
|
-
/** live: reactive subscription, auto-updates. No
|
|
160
|
-
export type FF_RepoLive<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'refresh' | 'more' | 'hasNextPage' | 'aggregates' | 'addItem' | 'updateItem' | 'removeItem'>;
|
|
161
|
-
/** paginate: `more()` / `hasNextPage` / `aggregates
|
|
162
|
-
export type FF_RepoPaginate<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, '
|
|
163
|
-
/** one: a single reactive record in `item
|
|
164
|
+
/** load: one-shot list (`refresh()` to re-run) - a read+reconcile view. No paging/aggregates, and no `item`/`save`/`delete`/`create` (edit via `one`, write via `.repo`). */
|
|
165
|
+
export type FF_RepoLoad<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'more' | 'hasNextPage' | 'aggregates' | 'item' | 'save' | 'delete' | 'create'>;
|
|
166
|
+
/** live: reactive subscription, auto-updates - a read view. No refresh/paging/aggregates/reconcilers (the liveQuery does it), and no `item`/`save`/`delete`/`create`. */
|
|
167
|
+
export type FF_RepoLive<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'refresh' | 'more' | 'hasNextPage' | 'aggregates' | 'addItem' | 'updateItem' | 'removeItem' | 'item' | 'save' | 'delete' | 'create'>;
|
|
168
|
+
/** paginate: `more()` / `hasNextPage` / `aggregates` - a read+reconcile view. No `onFirst` (paged ≠ latest), and no `item`/`save`/`delete`/`create`. */
|
|
169
|
+
export type FF_RepoPaginate<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'onFirst' | 'item' | 'save' | 'delete' | 'create'>;
|
|
170
|
+
/** one: a single reactive record in `item`. No paging / aggregates / list reconcilers. */
|
|
164
171
|
export type FF_RepoOne<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = Omit<FF_RepoHandle<Entity, O>, 'more' | 'hasNextPage' | 'aggregates' | 'addItem' | 'updateItem' | 'removeItem'>;
|
|
165
172
|
/**
|
|
166
173
|
* Umbrella handle type - any mode. Use for a component prop that accepts a
|
|
167
174
|
* `load`/`listen`/`paginate`/`one` handle (`r: FF_Repo<T>`). It exposes the surface
|
|
168
|
-
* common to every mode (`items`/`
|
|
169
|
-
*
|
|
170
|
-
* `
|
|
175
|
+
* common to every mode (`items`/`loading`/`error`/`meta`/`repo`); mode-specific members
|
|
176
|
+
* (`item`/`save`/`delete`/`create` on `one`; `more`/`hasNextPage`/`aggregates`/`refresh`/
|
|
177
|
+
* `onFirst`/`addItem`/`updateItem`/`removeItem`) require the matching per-mode type.
|
|
171
178
|
*/
|
|
172
179
|
export type FF_Repo<Entity, O extends FF_RepoOptions<Entity> = FF_RepoOptions<Entity>> = FF_RepoLoad<Entity, O> | FF_RepoLive<Entity, O> | FF_RepoPaginate<Entity, O> | FF_RepoOne<Entity, O>;
|
|
173
180
|
type StrictGetter<Entity, O extends FF_RepoOptions<Entity>> = () => O & Record<Exclude<keyof O, keyof FF_RepoOptions<Entity>>, never>;
|
|
@@ -25,7 +25,6 @@ class FF_RepoHandle {
|
|
|
25
25
|
hasNextPage = $state(false);
|
|
26
26
|
/** Aggregations for the whole query (paginate mode). `aggregates.$count` is the total row count. */
|
|
27
27
|
aggregates = $state(undefined);
|
|
28
|
-
first = $derived(this.items[0] ?? null);
|
|
29
28
|
constructor(r, opts, mode) {
|
|
30
29
|
this.#repo = r;
|
|
31
30
|
this.#opts = opts;
|
|
@@ -39,7 +38,7 @@ class FF_RepoHandle {
|
|
|
39
38
|
}
|
|
40
39
|
if (mode === 'live') {
|
|
41
40
|
// Pass orderBy so liveQuery re-sorts incrementally-added rows too;
|
|
42
|
-
// without it a freshly inserted row is appended and `
|
|
41
|
+
// without it a freshly inserted row is appended and `items[0]` (the latest) goes stale.
|
|
43
42
|
const unsub = this.#repo
|
|
44
43
|
.liveQuery({ where: o.where, orderBy: o.orderBy, limit: o.limit, include: o.include })
|
|
45
44
|
.subscribe({
|
|
@@ -146,28 +145,35 @@ class FF_RepoHandle {
|
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
/**
|
|
149
|
-
* Run `fn` once
|
|
150
|
-
*
|
|
151
|
-
*
|
|
148
|
+
* Run `fn` once - the first time a row exists (`items[0]`).
|
|
149
|
+
*
|
|
150
|
+
* The point: seed editable UI state from the latest row WITHOUT a live query
|
|
151
|
+
* clobbering in-progress edits. It fires a single time, on the first non-empty
|
|
152
|
+
* result, and never again - later ticks (an edit, a delete, a re-sort) are
|
|
153
|
+
* ignored. Empty snapshots are skipped (a liveQuery often emits one before the
|
|
154
|
+
* data lands; there is nothing to seed from an empty result).
|
|
155
|
+
*
|
|
156
|
+
* For pure derived state prefer `$derived`; reach for `onFirst` only when the
|
|
157
|
+
* seed must become independently editable (a draft the user then mutates).
|
|
158
|
+
*
|
|
159
|
+
* ```svelte
|
|
160
|
+
* const list = ffRepo(Plan).listen(() => ({ where: { ownerDid } }))
|
|
161
|
+
* let draft = $state({ title: '' })
|
|
162
|
+
* list.onFirst((latest) => (draft.title = latest.title)) // seed once, then edit freely
|
|
163
|
+
* ```
|
|
152
164
|
*/
|
|
153
|
-
|
|
165
|
+
onFirst(fn) {
|
|
154
166
|
let done = false;
|
|
155
167
|
$effect(() => {
|
|
156
168
|
if (done)
|
|
157
169
|
return;
|
|
158
|
-
const latest = this.
|
|
170
|
+
const latest = this.items[0];
|
|
159
171
|
if (latest == null)
|
|
160
172
|
return;
|
|
161
173
|
fn(latest);
|
|
162
174
|
done = true;
|
|
163
175
|
});
|
|
164
176
|
}
|
|
165
|
-
/** Reactive, bindable editor state seeded once from the latest row. */
|
|
166
|
-
draft(seed) {
|
|
167
|
-
const values = $state(seed(null));
|
|
168
|
-
this.firstOnce((latest) => Object.assign(values, seed(latest)));
|
|
169
|
-
return values;
|
|
170
|
-
}
|
|
171
177
|
/** Create a new unsaved entity into the `item` slot (for an edit form). */
|
|
172
178
|
create(...args) {
|
|
173
179
|
this.item = this.#repo.create(...args);
|
|
@@ -194,24 +200,14 @@ class FF_RepoHandle {
|
|
|
194
200
|
this.loading[flag] = false;
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
update(...args) {
|
|
201
|
-
return this.#write('saving', () => this.#repo.update(...args), () => this.#resync());
|
|
203
|
+
/** Save the current `item` (from `one` / `create()`). To save a specific row, use `.repo.save(row)`. */
|
|
204
|
+
save() {
|
|
205
|
+
return this.#write('saving', () => this.#repo.save(this.#requireItem()), () => this.#resync());
|
|
202
206
|
}
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/** Delete. With no argument, deletes the current `item`. */
|
|
208
|
-
delete(...args) {
|
|
209
|
-
let target;
|
|
210
|
-
return this.#write('deleting', () => {
|
|
211
|
-
const a = (args.length ? args : [this.#requireItem()]);
|
|
212
|
-
target = a[0];
|
|
213
|
-
return this.#repo.delete(...a);
|
|
214
|
-
}, () => {
|
|
207
|
+
/** Delete the current `item`. To delete a specific row/id, use `.repo.delete(idOrRow)`. */
|
|
208
|
+
delete() {
|
|
209
|
+
const target = this.#requireItem();
|
|
210
|
+
return this.#write('deleting', () => this.#repo.delete(target), () => {
|
|
215
211
|
// live: liveQuery removes it. one: re-fetch (likely empty now).
|
|
216
212
|
// load/paginate: drop it locally (no refetch).
|
|
217
213
|
if (this.#mode === 'live')
|
|
@@ -221,15 +217,12 @@ class FF_RepoHandle {
|
|
|
221
217
|
this.#removeLocal(target);
|
|
222
218
|
});
|
|
223
219
|
}
|
|
224
|
-
/** The current `item` (or throw) - backs the
|
|
220
|
+
/** The current `item` (or throw) - backs the argless `save()`/`delete()`. */
|
|
225
221
|
#requireItem() {
|
|
226
222
|
if (this.item === undefined)
|
|
227
|
-
throw new Error('FF_Repo: no `item` to save/delete -
|
|
223
|
+
throw new Error('FF_Repo: no `item` to save/delete - load one first (`one` mode or `create()`), or write a specific row through `.repo`.');
|
|
228
224
|
return this.item;
|
|
229
225
|
}
|
|
230
|
-
deleteMany(...args) {
|
|
231
|
-
return this.#write('deleting', () => this.#repo.deleteMany(...args), () => this.#resync());
|
|
232
|
-
}
|
|
233
226
|
// Client-side list reconcilers (no server I/O) - reflect a change you made
|
|
234
227
|
// elsewhere (e.g. via `.repo`) in the reactive `items`. `load`/`paginate` only;
|
|
235
228
|
// `listen` reconciles itself via the liveQuery. `add`/`remove` also adjust
|
package/esm/svelte/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export type { InfiniteScrollOptions } from './infiniteScroll.js';
|
|
|
5
5
|
export { SP } from './class/SP.svelte';
|
|
6
6
|
export type { ParamDefinition } from './class/SP.svelte';
|
|
7
7
|
export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
|
|
8
|
+
export { default as DemoGrid } from './DemoGrid.svelte';
|
|
8
9
|
export { default as Icon } from './ui/Icon.svelte';
|
|
9
10
|
export { LibIcon_Empty, LibIcon_Forbidden, LibIcon_ChevronDown, LibIcon_ChevronUp, LibIcon_ChevronLeft, LibIcon_ChevronRight, LibIcon_Search, LibIcon_Check, LibIcon_MultiCheck, LibIcon_Add, LibIcon_MultiAdd, LibIcon_Edit, LibIcon_Eye, LibIcon_EyeOff, LibIcon_Delete, LibIcon_Cross, LibIcon_Save, LibIcon_Man, LibIcon_Woman, LibIcon_Send, LibIcon_Load, LibIcon_Settings, LibIcon_Sort, LibIcon_SortAsc, LibIcon_SortDesc, } from './ui/LibIcon.js';
|
package/esm/svelte/index.js
CHANGED
|
@@ -2,5 +2,6 @@ export { ffRepo } from './FF_Repo.svelte.js';
|
|
|
2
2
|
export { infiniteScroll } from './infiniteScroll.js';
|
|
3
3
|
export { SP } from './class/SP.svelte';
|
|
4
4
|
export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
|
|
5
|
+
export { default as DemoGrid } from './DemoGrid.svelte';
|
|
5
6
|
export { default as Icon } from './ui/Icon.svelte';
|
|
6
7
|
export { LibIcon_Empty, LibIcon_Forbidden, LibIcon_ChevronDown, LibIcon_ChevronUp, LibIcon_ChevronLeft, LibIcon_ChevronRight, LibIcon_Search, LibIcon_Check, LibIcon_MultiCheck, LibIcon_Add, LibIcon_MultiAdd, LibIcon_Edit, LibIcon_Eye, LibIcon_EyeOff, LibIcon_Delete, LibIcon_Cross, LibIcon_Save, LibIcon_Man, LibIcon_Woman, LibIcon_Send, LibIcon_Load, LibIcon_Settings, LibIcon_Sort, LibIcon_SortAsc, LibIcon_SortDesc, } from './ui/LibIcon.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firstly",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Firstly, an opinionated Remult setup!",
|
|
6
6
|
"funding": "https://github.com/sponsors/jycouet",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"nodemailer": "8.0.5",
|
|
41
41
|
"tailwind-merge": "3.5.0",
|
|
42
42
|
"tailwindcss": "4.2.2",
|
|
43
|
-
"vite-plugin-kit-routes": "1.0.
|
|
44
|
-
"vite-plugin-stripper": "0.10.
|
|
43
|
+
"vite-plugin-kit-routes": "1.0.6",
|
|
44
|
+
"vite-plugin-stripper": "0.10.4"
|
|
45
45
|
},
|
|
46
46
|
"sideEffects": false,
|
|
47
47
|
"exports": {
|