firstly 0.5.0 → 0.6.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 +28 -0
- package/esm/core/FF_Filter.d.ts +2 -2
- package/esm/core/FF_Filter.js +2 -2
- package/esm/core/FF_Validators.d.ts +2 -0
- package/esm/core/FF_Validators.js +8 -10
- package/esm/core/containsWords.d.ts +2 -2
- package/esm/core/containsWords.js +2 -2
- package/esm/core/tailwind.d.ts +3 -4
- package/esm/core/tailwind.js +3 -4
- package/esm/svelte/DemoForm.svelte +121 -0
- package/esm/svelte/DemoForm.svelte.d.ts +42 -0
- package/esm/svelte/DemoGrid.svelte +258 -0
- package/esm/svelte/DemoGrid.svelte.d.ts +49 -0
- package/esm/svelte/DialogOpenTest.svelte +10 -0
- package/esm/svelte/DialogOpenTest.svelte.d.ts +8 -0
- package/esm/svelte/FF_Config.svelte +13 -0
- package/esm/svelte/FF_Config.svelte.d.ts +3 -0
- package/esm/svelte/FF_Config.svelte.js +38 -0
- package/esm/svelte/FF_DialogManager.svelte +251 -0
- package/esm/svelte/FF_DialogManager.svelte.d.ts +13 -0
- package/esm/svelte/FF_PromptDefault.svelte +85 -0
- package/esm/svelte/FF_PromptDefault.svelte.d.ts +9 -0
- package/esm/svelte/FF_ToastHtml.svelte +9 -0
- package/esm/svelte/FF_ToastHtml.svelte.d.ts +6 -0
- package/esm/svelte/FF_ToastManager.svelte +22 -0
- package/esm/svelte/FF_ToastManager.svelte.d.ts +4 -0
- package/esm/svelte/dialog.svelte.d.ts +209 -0
- package/esm/svelte/dialog.svelte.js +243 -0
- package/esm/svelte/ff.svelte.d.ts +294 -0
- package/esm/svelte/ff.svelte.js +599 -0
- package/esm/svelte/index.d.ts +14 -2
- package/esm/svelte/index.js +9 -1
- package/esm/svelte/infiniteScroll.d.ts +1 -1
- package/esm/svelte/infiniteScroll.js +1 -1
- package/esm/svelte/toast.d.ts +59 -0
- package/esm/svelte/toast.js +92 -0
- package/esm/virtual/StateDemoEnum.js +1 -1
- package/package.json +4 -3
- package/esm/svelte/FF_Repo.svelte.d.ts +0 -191
- package/esm/svelte/FF_Repo.svelte.js +0 -312
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { repo as remultRepo, } from 'remult';
|
|
2
|
-
/**
|
|
3
|
-
* The reactive handle implementation. Not exported directly - consumers use a per-mode
|
|
4
|
-
* alias (`FF_RepoLoad`/`FF_RepoLive`/`FF_RepoPaginate`/`FF_RepoOne`) or the umbrella
|
|
5
|
-
* union `FF_Repo` (any mode). Each verb returns the Omit'd per-mode view of this.
|
|
6
|
-
*/
|
|
7
|
-
class FF_RepoHandle {
|
|
8
|
-
#repo;
|
|
9
|
-
#opts;
|
|
10
|
-
#mode;
|
|
11
|
-
#defaultOrderBy;
|
|
12
|
-
#paginator;
|
|
13
|
-
#seq = 0;
|
|
14
|
-
items = $state([]);
|
|
15
|
-
/** Single-record slot: the loaded row in `one` mode, or a create/edit draft (see `create`). */
|
|
16
|
-
item = $state(undefined);
|
|
17
|
-
loading = $state({
|
|
18
|
-
init: true,
|
|
19
|
-
fetching: false,
|
|
20
|
-
more: false,
|
|
21
|
-
saving: false,
|
|
22
|
-
deleting: false,
|
|
23
|
-
});
|
|
24
|
-
error = $state(undefined);
|
|
25
|
-
hasNextPage = $state(false);
|
|
26
|
-
/** Aggregations for the whole query (paginate mode). `aggregates.$count` is the total row count. */
|
|
27
|
-
aggregates = $state(undefined);
|
|
28
|
-
first = $derived(this.items[0] ?? null);
|
|
29
|
-
constructor(r, opts, mode) {
|
|
30
|
-
this.#repo = r;
|
|
31
|
-
this.#opts = opts;
|
|
32
|
-
this.#mode = mode;
|
|
33
|
-
this.#defaultOrderBy = r.metadata.options.defaultOrderBy;
|
|
34
|
-
$effect(() => {
|
|
35
|
-
const o = this.#resolve();
|
|
36
|
-
if (o.enabled === false) {
|
|
37
|
-
this.loading.init = false;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (mode === 'live') {
|
|
41
|
-
// Pass orderBy so liveQuery re-sorts incrementally-added rows too;
|
|
42
|
-
// without it a freshly inserted row is appended and `first` goes stale.
|
|
43
|
-
const unsub = this.#repo
|
|
44
|
-
.liveQuery({ where: o.where, orderBy: o.orderBy, limit: o.limit, include: o.include })
|
|
45
|
-
.subscribe({
|
|
46
|
-
next: (info) => {
|
|
47
|
-
this.items = info.items;
|
|
48
|
-
this.loading.init = false;
|
|
49
|
-
},
|
|
50
|
-
error: (e) => {
|
|
51
|
-
this.error = e instanceof Error ? e.message : String(e);
|
|
52
|
-
this.loading.init = false;
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
return () => unsub();
|
|
56
|
-
}
|
|
57
|
-
// load | paginate | one: (re)fetch; a newer opts() invalidates older responses.
|
|
58
|
-
void this.#load(o, ++this.#seq);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
#resolve() {
|
|
62
|
-
const o = this.#opts();
|
|
63
|
-
return { ...o, orderBy: o.orderBy ?? this.#defaultOrderBy };
|
|
64
|
-
}
|
|
65
|
-
async #load(o, seq, keepCount) {
|
|
66
|
-
this.loading.fetching = true;
|
|
67
|
-
this.error = undefined;
|
|
68
|
-
try {
|
|
69
|
-
if (this.#mode === 'paginate') {
|
|
70
|
-
// One request returns the page AND the aggregates ($count always, plus any
|
|
71
|
-
// requested): on the client REST proxy remult fetches both together. It sets
|
|
72
|
-
// `hasNextPage` from whether a full page came back (no count probe); `more()`
|
|
73
|
-
// then fetches the next page via a keyset cursor (orderBy + PK).
|
|
74
|
-
const p = await this.#repo
|
|
75
|
-
.query({
|
|
76
|
-
where: o.where,
|
|
77
|
-
orderBy: o.orderBy,
|
|
78
|
-
pageSize: keepCount ?? o.pageSize ?? 25,
|
|
79
|
-
include: o.include,
|
|
80
|
-
aggregate: { ...o.aggregate },
|
|
81
|
-
})
|
|
82
|
-
.paginator();
|
|
83
|
-
if (seq !== this.#seq)
|
|
84
|
-
return;
|
|
85
|
-
this.#paginator = p;
|
|
86
|
-
this.items = p.items;
|
|
87
|
-
this.hasNextPage = p.hasNextPage;
|
|
88
|
-
// `aggregates` is only on the paginator type when the aggregate is non-empty,
|
|
89
|
-
// but remult returns `$count` for the empty case too - so read it through a cast.
|
|
90
|
-
this.aggregates = p.aggregates;
|
|
91
|
-
}
|
|
92
|
-
else if (this.#mode === 'one') {
|
|
93
|
-
const found = await this.#repo.findFirst(o.where, {
|
|
94
|
-
orderBy: o.orderBy,
|
|
95
|
-
include: o.include,
|
|
96
|
-
});
|
|
97
|
-
if (seq !== this.#seq)
|
|
98
|
-
return;
|
|
99
|
-
this.item = found ?? undefined;
|
|
100
|
-
this.items = found ? [found] : [];
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
const items = await this.#repo.find({
|
|
104
|
-
where: o.where,
|
|
105
|
-
orderBy: o.orderBy,
|
|
106
|
-
limit: o.limit,
|
|
107
|
-
include: o.include,
|
|
108
|
-
});
|
|
109
|
-
if (seq !== this.#seq)
|
|
110
|
-
return;
|
|
111
|
-
this.items = items;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
if (seq === this.#seq)
|
|
116
|
-
this.error = e instanceof Error ? e.message : String(e);
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
if (seq === this.#seq) {
|
|
120
|
-
this.loading.init = false;
|
|
121
|
-
this.loading.fetching = false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/** Re-run the current query (load/paginate/one), back to the first page. */
|
|
126
|
-
async refresh() {
|
|
127
|
-
if (this.#mode === 'live')
|
|
128
|
-
throw new Error('FF_Repo: refresh() is not available in live mode');
|
|
129
|
-
await this.#load(this.#resolve(), ++this.#seq);
|
|
130
|
-
}
|
|
131
|
-
/** Load and append the next page (paginate mode). */
|
|
132
|
-
async more() {
|
|
133
|
-
if (this.#mode !== 'paginate')
|
|
134
|
-
throw new Error('FF_Repo: more() requires paginate mode');
|
|
135
|
-
if (!this.#paginator || this.loading.more || !this.hasNextPage)
|
|
136
|
-
return;
|
|
137
|
-
this.loading.more = true;
|
|
138
|
-
try {
|
|
139
|
-
const next = await this.#paginator.nextPage();
|
|
140
|
-
this.#paginator = next;
|
|
141
|
-
this.items = [...this.items, ...next.items];
|
|
142
|
-
this.hasNextPage = next.hasNextPage;
|
|
143
|
-
}
|
|
144
|
-
finally {
|
|
145
|
-
this.loading.more = false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Run `fn` once, the first time a row is known (first non-null `first`).
|
|
150
|
-
* Never fires while the result is empty - so it's robust to liveQuery emitting
|
|
151
|
-
* an empty snapshot before the data lands. Nothing to seed from an empty result.
|
|
152
|
-
*/
|
|
153
|
-
firstOnce(fn) {
|
|
154
|
-
let done = false;
|
|
155
|
-
$effect(() => {
|
|
156
|
-
if (done)
|
|
157
|
-
return;
|
|
158
|
-
const latest = this.first;
|
|
159
|
-
if (latest == null)
|
|
160
|
-
return;
|
|
161
|
-
fn(latest);
|
|
162
|
-
done = true;
|
|
163
|
-
});
|
|
164
|
-
}
|
|
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
|
-
/** Create a new unsaved entity into the `item` slot (for an edit form). */
|
|
172
|
-
create(...args) {
|
|
173
|
-
this.item = this.#repo.create(...args);
|
|
174
|
-
return this.item;
|
|
175
|
-
}
|
|
176
|
-
// Mutations: run `op`, then the post-write sync on SUCCESS only (a failed write
|
|
177
|
-
// leaves the result untouched). On failure we fill `error` AND re-throw - the
|
|
178
|
-
// caller still gets the rejection (not silenced); `error` is for a reactive
|
|
179
|
-
// UI that wants it. `finally` only flips `loading` (no `await`, which would
|
|
180
|
-
// mask the original error).
|
|
181
|
-
async #write(flag, op, after) {
|
|
182
|
-
this.loading[flag] = true;
|
|
183
|
-
this.error = undefined;
|
|
184
|
-
try {
|
|
185
|
-
const res = await op();
|
|
186
|
-
await after();
|
|
187
|
-
return res;
|
|
188
|
-
}
|
|
189
|
-
catch (e) {
|
|
190
|
-
this.error = e instanceof Error ? e.message : String(e);
|
|
191
|
-
throw e;
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
this.loading[flag] = false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
insert(...args) {
|
|
198
|
-
return this.#write('saving', () => this.#repo.insert(...args), () => this.#resync());
|
|
199
|
-
}
|
|
200
|
-
update(...args) {
|
|
201
|
-
return this.#write('saving', () => this.#repo.update(...args), () => this.#resync());
|
|
202
|
-
}
|
|
203
|
-
/** Save. With no argument, saves the current `item` (e.g. a bound `one`/`create` form). */
|
|
204
|
-
save(...args) {
|
|
205
|
-
return this.#write('saving', () => this.#repo.save(...(args.length ? args : [this.#requireItem()])), () => this.#resync());
|
|
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
|
-
}, () => {
|
|
215
|
-
// live: liveQuery removes it. one: re-fetch (likely empty now).
|
|
216
|
-
// load/paginate: drop it locally (no refetch).
|
|
217
|
-
if (this.#mode === 'live')
|
|
218
|
-
return;
|
|
219
|
-
if (this.#mode === 'one')
|
|
220
|
-
return this.#resync();
|
|
221
|
-
this.#removeLocal(target);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
/** The current `item` (or throw) - backs the no-arg `save()`/`delete()` forms. */
|
|
225
|
-
#requireItem() {
|
|
226
|
-
if (this.item === undefined)
|
|
227
|
-
throw new Error('FF_Repo: no `item` to save/delete - pass an explicit argument, or load one first (`one` mode or `create()`).');
|
|
228
|
-
return this.item;
|
|
229
|
-
}
|
|
230
|
-
deleteMany(...args) {
|
|
231
|
-
return this.#write('deleting', () => this.#repo.deleteMany(...args), () => this.#resync());
|
|
232
|
-
}
|
|
233
|
-
// Client-side list reconcilers (no server I/O) - reflect a change you made
|
|
234
|
-
// elsewhere (e.g. via `.repo`) in the reactive `items`. `load`/`paginate` only;
|
|
235
|
-
// `listen` reconciles itself via the liveQuery. `add`/`remove` also adjust
|
|
236
|
-
// `aggregates.$count` (not the other aggregates). For authoritative state, call
|
|
237
|
-
// `refresh()` (it re-pulls and, for paginate, resets to the first page).
|
|
238
|
-
/** Insert into `items` at `top` (default) / `bottom` / an index (`-1` = last). +1 to `$count`. */
|
|
239
|
-
addItem(item, options) {
|
|
240
|
-
const at = options?.at ?? 'top';
|
|
241
|
-
const list = this.items;
|
|
242
|
-
const idx = at === 'top'
|
|
243
|
-
? 0
|
|
244
|
-
: at === 'bottom'
|
|
245
|
-
? list.length
|
|
246
|
-
: at < 0
|
|
247
|
-
? Math.max(0, list.length + at + 1)
|
|
248
|
-
: Math.min(at, list.length);
|
|
249
|
-
this.items = [...list.slice(0, idx), item, ...list.slice(idx)];
|
|
250
|
-
if (this.aggregates)
|
|
251
|
-
this.aggregates.$count += 1;
|
|
252
|
-
}
|
|
253
|
-
/** Replace the row whose id matches `item`'s id (no `$count` change). */
|
|
254
|
-
updateItem(item) {
|
|
255
|
-
const id = this.#repo.metadata.idMetadata.getId(item);
|
|
256
|
-
this.items = this.items.map((x) => (this.#repo.metadata.idMetadata.getId(x) === id ? item : x));
|
|
257
|
-
}
|
|
258
|
-
/** Drop the matching row (pass an id or the item). -1 to `$count`. */
|
|
259
|
-
removeItem(idOrItem) {
|
|
260
|
-
this.#removeLocal(idOrItem);
|
|
261
|
-
}
|
|
262
|
-
#removeLocal(idOrItem) {
|
|
263
|
-
const id = idOrItem != null && typeof idOrItem === 'object'
|
|
264
|
-
? this.#repo.metadata.idMetadata.getId(idOrItem)
|
|
265
|
-
: idOrItem;
|
|
266
|
-
this.items = this.items.filter((i) => this.#repo.metadata.idMetadata.getId(i) !== id);
|
|
267
|
-
if (this.aggregates)
|
|
268
|
-
this.aggregates.$count = Math.max(0, this.aggregates.$count - 1);
|
|
269
|
-
}
|
|
270
|
-
/** After insert/update (or a `one` delete) in a non-live mode, re-fetch keeping the current count. */
|
|
271
|
-
async #resync() {
|
|
272
|
-
if (this.#mode === 'live')
|
|
273
|
-
return;
|
|
274
|
-
const keepCount = this.#mode === 'paginate' ? this.items.length || undefined : undefined;
|
|
275
|
-
await this.#load(this.#resolve(), ++this.#seq, keepCount);
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* The entity's remult metadata - the single escape hatch for everything not on
|
|
279
|
-
* this handle: permissions (`apiInsertAllowed()`, `apiUpdateAllowed(item)`,
|
|
280
|
-
* `apiDeleteAllowed(item)`, `apiReadAllowed`), `fields`, `idMetadata`, `options`,
|
|
281
|
-
* `key`. Reflects the current `remult.user`.
|
|
282
|
-
*/
|
|
283
|
-
get meta() {
|
|
284
|
-
return this.#repo.metadata;
|
|
285
|
-
}
|
|
286
|
-
/** Escape hatch to the underlying repo (count, findId, upsert, projections, ...). */
|
|
287
|
-
get repo() {
|
|
288
|
-
return this.#repo;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
export function ffRepo(entity) {
|
|
292
|
-
const r = remultRepo(entity);
|
|
293
|
-
const builder = {
|
|
294
|
-
load(o) {
|
|
295
|
-
return new FF_RepoHandle(r, o, 'load');
|
|
296
|
-
},
|
|
297
|
-
listen(o) {
|
|
298
|
-
return new FF_RepoHandle(r, o, 'live');
|
|
299
|
-
},
|
|
300
|
-
paginate(o) {
|
|
301
|
-
return new FF_RepoHandle(r, o, 'paginate');
|
|
302
|
-
},
|
|
303
|
-
one(o) {
|
|
304
|
-
return new FF_RepoHandle(r, o, 'one');
|
|
305
|
-
},
|
|
306
|
-
get meta() {
|
|
307
|
-
return r.metadata;
|
|
308
|
-
},
|
|
309
|
-
repo: r,
|
|
310
|
-
};
|
|
311
|
-
return builder;
|
|
312
|
-
}
|