firstly 0.4.5 → 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 +26 -0
- package/esm/core/FF_Filter.d.ts +17 -2
- package/esm/core/FF_Filter.js +18 -2
- package/esm/core/containsWords.d.ts +13 -0
- package/esm/core/containsWords.js +27 -0
- package/esm/core/httpClientStack.d.ts +18 -0
- package/esm/core/httpClientStack.js +26 -0
- package/esm/formats/index.d.ts +1 -1
- package/esm/formats/index.js +1 -1
- package/esm/formats/strings.d.ts +7 -0
- package/esm/formats/strings.js +12 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +1 -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 +178 -56
- package/esm/svelte/FF_Repo.svelte.js +273 -161
- package/esm/svelte/index.d.ts +5 -1
- package/esm/svelte/index.js +3 -1
- package/esm/svelte/infiniteScroll.d.ts +31 -0
- package/esm/svelte/infiniteScroll.js +40 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.5.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- [#274](https://github.com/jycouet/firstly/pull/274) [`6114148`](https://github.com/jycouet/firstly/commit/61141482a06f0a006ae148f0645146c073a2fb3c) Thanks [@jycouet](https://github.com/jycouet)! - **BREAKING (svelte): `FF_Repo` class -> `ffRepo()` factory.** The reactive repo wrapper now takes a reactive options getter and a mode (`load` / `listen` / `paginate` / `one`), with built-in mutations (no-arg `save()`/`delete()` target the loaded `item`), client-side list reconcilers (`addItem`/`updateItem`/`removeItem`, with `addItem` positioning), `firstOnce`/`draft`, and permissions via `r.meta`. The old `new FF_Repo(E, { findOptions })` class is removed.
|
|
16
|
+
|
|
17
|
+
One rule: anything not under `.repo` is reactive; every imperative read/write lives on `.repo` (the plain remult repo). The builder no longer hoists `findFirst`/`findId`/`insert`/... - use `ffRepo(E).repo.*`.
|
|
18
|
+
|
|
19
|
+
Also new: `infiniteScroll` (svelte attachment, pairs with `paginate`), `stackHttpClient`/`withHeader` (core), `FF_Filter.containsWords` (multi-field search filter), `splitTrim` (formats), and exported types including the umbrella `FF_Repo` handle plus `FF_RepoBuilder`/`FF_RepoLoad`/`FF_RepoLive`/`FF_RepoPaginate`/`FF_RepoOne`/`QueryOptionsHelper`/`AggregateOptions`.
|
|
20
|
+
|
|
21
|
+
Migration (see `/docs/svelte/ff-repo`):
|
|
22
|
+
- `new FF_Repo(E, { findOptions: { where } })` -> `ffRepo(E).load(() => ({ where }))`
|
|
23
|
+
- `new FF_Repo(E, { queryOptions })` + `.query()/.queryMore()/.queryRefresh()` -> `ffRepo(E).paginate(() => ({ ... }))` + `.more()/.refresh()`
|
|
24
|
+
- `r.globalError` -> `r.error`
|
|
25
|
+
- `r.fields` -> `r.meta.fields`; `r.metadata.apiInsertAllowed()` -> `r.meta.apiInsertAllowed()`
|
|
26
|
+
- `repo(r.ent).update(...)` / `.insert(...)` -> `r.update(...)` / `r.insert(...)`
|
|
27
|
+
- `r.aggregates.$count` unchanged; `skipAutoFetch` -> `enabled: false`
|
|
28
|
+
|
|
3
29
|
## 0.4.5
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/esm/core/FF_Filter.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Filter helpers that build a remult `EntityFilter`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `owner` / `ownerOr` are prefilters (for `apiPrefilter`, `backendPrefilter`) -
|
|
5
|
+
* pair with `FF_Allow` (the equivalent for `allowApi*` row checks). `containsWords`
|
|
6
|
+
* is a search-box helper (pairs with `ffRepo(...).load/paginate`).
|
|
5
7
|
*
|
|
6
8
|
* Pass the entity type as a generic (`FF_Filter.owner<Task>('userId')`) to get
|
|
7
9
|
* autocompletion and type-safety on the column name. Without a generic the
|
|
@@ -52,4 +54,17 @@ export declare const FF_Filter: {
|
|
|
52
54
|
col?: keyof T & string;
|
|
53
55
|
roles: string[];
|
|
54
56
|
}) => any;
|
|
57
|
+
/**
|
|
58
|
+
* Build a search filter where every word must match (AND) and each word may
|
|
59
|
+
* match any of the given fields (OR), case-insensitive `$contains`. Word order
|
|
60
|
+
* and which field holds which word don't matter - handy for "NOM Prénom" search.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const r = ffRepo(User).load(() => ({
|
|
65
|
+
* where: FF_Filter.containsWords([repo(User).fields.name, repo(User).fields.sesa], q),
|
|
66
|
+
* }))
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
containsWords: <Entity>(fields: import("remult").FieldMetadata<unknown, Entity>[], search: string) => import("remult").EntityFilter<Entity>;
|
|
55
70
|
};
|
package/esm/core/FF_Filter.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { remult } from 'remult';
|
|
2
|
+
import { containsWords } from './containsWords.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Filter helpers that build a remult `EntityFilter`.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
+
* `owner` / `ownerOr` are prefilters (for `apiPrefilter`, `backendPrefilter`) -
|
|
7
|
+
* pair with `FF_Allow` (the equivalent for `allowApi*` row checks). `containsWords`
|
|
8
|
+
* is a search-box helper (pairs with `ffRepo(...).load/paginate`).
|
|
6
9
|
*
|
|
7
10
|
* Pass the entity type as a generic (`FF_Filter.owner<Task>('userId')`) to get
|
|
8
11
|
* autocompletion and type-safety on the column name. Without a generic the
|
|
@@ -54,4 +57,17 @@ export const FF_Filter = {
|
|
|
54
57
|
return {};
|
|
55
58
|
return { [col]: [remult.user?.id] };
|
|
56
59
|
},
|
|
60
|
+
/**
|
|
61
|
+
* Build a search filter where every word must match (AND) and each word may
|
|
62
|
+
* match any of the given fields (OR), case-insensitive `$contains`. Word order
|
|
63
|
+
* and which field holds which word don't matter - handy for "NOM Prénom" search.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const r = ffRepo(User).load(() => ({
|
|
68
|
+
* where: FF_Filter.containsWords([repo(User).fields.name, repo(User).fields.sesa], q),
|
|
69
|
+
* }))
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
containsWords,
|
|
57
73
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityFilter, FieldMetadata } from 'remult';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a filter where every word in `search` must match (AND), and each word
|
|
4
|
+
* may match any of the given fields (OR), using case-insensitive `$contains`.
|
|
5
|
+
*
|
|
6
|
+
* E.g. "dupont marie" over [name, sesa] =>
|
|
7
|
+
* (name~dupont OR sesa~dupont) AND (name~marie OR sesa~marie)
|
|
8
|
+
*
|
|
9
|
+
* So word order and which field holds which word don't matter - handy for
|
|
10
|
+
* "NOM Prénom" style search across several columns. Pairs with `ffRepo`:
|
|
11
|
+
* `ffRepo(User).load(() => ({ where: containsWords([f.name, f.sesa], q) }))`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const containsWords: <Entity>(fields: FieldMetadata<unknown, Entity>[], search: string) => EntityFilter<Entity>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a filter where every word in `search` must match (AND), and each word
|
|
3
|
+
* may match any of the given fields (OR), using case-insensitive `$contains`.
|
|
4
|
+
*
|
|
5
|
+
* E.g. "dupont marie" over [name, sesa] =>
|
|
6
|
+
* (name~dupont OR sesa~dupont) AND (name~marie OR sesa~marie)
|
|
7
|
+
*
|
|
8
|
+
* So word order and which field holds which word don't matter - handy for
|
|
9
|
+
* "NOM Prénom" style search across several columns. Pairs with `ffRepo`:
|
|
10
|
+
* `ffRepo(User).load(() => ({ where: containsWords([f.name, f.sesa], q) }))`.
|
|
11
|
+
*/
|
|
12
|
+
export const containsWords = (fields, search) => {
|
|
13
|
+
const words = (search ?? '').split(' ').filter((s) => s.length > 0);
|
|
14
|
+
if (words.length === 0 || fields.length === 0) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
if (fields.length === 1) {
|
|
18
|
+
return {
|
|
19
|
+
$and: words.map((s) => ({ [fields[0].key]: { $contains: s } })),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
$and: words.map((s) => ({
|
|
24
|
+
$or: fields.map((f) => ({ [f.key]: { $contains: s } })),
|
|
25
|
+
})),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type HttpClientFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
export type HttpClientMiddleware = (next: HttpClientFetch) => HttpClientFetch;
|
|
3
|
+
/**
|
|
4
|
+
* Compose `fetch` middlewares into a single `fetch`-shaped function. Middlewares
|
|
5
|
+
* run outermost-first (the first argument wraps the rest). Useful to inject auth
|
|
6
|
+
* headers, correlation ids, retries, logging, ... around a base `fetch`.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const client = stackHttpClient(
|
|
10
|
+
* withHeader('x-correlation-id', () => crypto.randomUUID()),
|
|
11
|
+
* withHeader('authorization', () => `Bearer ${token}`),
|
|
12
|
+
* )
|
|
13
|
+
* await client('/api/...')
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function stackHttpClient(...middlewares: HttpClientMiddleware[]): HttpClientFetch;
|
|
17
|
+
/** Middleware that sets a request header from `getValue()` when it returns a value. */
|
|
18
|
+
export declare function withHeader(name: string, getValue: () => string | undefined | null): HttpClientMiddleware;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose `fetch` middlewares into a single `fetch`-shaped function. Middlewares
|
|
3
|
+
* run outermost-first (the first argument wraps the rest). Useful to inject auth
|
|
4
|
+
* headers, correlation ids, retries, logging, ... around a base `fetch`.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const client = stackHttpClient(
|
|
8
|
+
* withHeader('x-correlation-id', () => crypto.randomUUID()),
|
|
9
|
+
* withHeader('authorization', () => `Bearer ${token}`),
|
|
10
|
+
* )
|
|
11
|
+
* await client('/api/...')
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function stackHttpClient(...middlewares) {
|
|
15
|
+
return middlewares.reduceRight((next, mw) => mw(next), (input, init) => fetch(input, init));
|
|
16
|
+
}
|
|
17
|
+
/** Middleware that sets a request header from `getValue()` when it returns a value. */
|
|
18
|
+
export function withHeader(name, getValue) {
|
|
19
|
+
return (next) => async (input, init) => {
|
|
20
|
+
const headers = new Headers(init?.headers);
|
|
21
|
+
const value = getValue();
|
|
22
|
+
if (value)
|
|
23
|
+
headers.set(name, value);
|
|
24
|
+
return next(input, { ...init, headers });
|
|
25
|
+
};
|
|
26
|
+
}
|
package/esm/formats/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { displayCurrency, displayCurrencyK, displayPercent, displayCurrencyWOSuffix, } from './numbers.js';
|
|
2
|
-
export { formatNumber, extractMailInfo, slugify, nameify, displayPhone, suffixWithS, arrToStr, mask, toTitleCase, } from './strings.js';
|
|
2
|
+
export { formatNumber, extractMailInfo, slugify, nameify, displayPhone, suffixWithS, arrToStr, mask, toTitleCase, splitTrim, } from './strings.js';
|
|
3
3
|
export { offsetedToPlainDate, plainDateCompare, isBetween, dateISOToPlainDate } from './dates.js';
|
|
4
4
|
export type { KitPlainDateRange } from './dates.js';
|
package/esm/formats/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { displayCurrency, displayCurrencyK, displayPercent, displayCurrencyWOSuffix, } from './numbers.js';
|
|
2
|
-
export { formatNumber, extractMailInfo, slugify, nameify, displayPhone, suffixWithS, arrToStr, mask, toTitleCase, } from './strings.js';
|
|
2
|
+
export { formatNumber, extractMailInfo, slugify, nameify, displayPhone, suffixWithS, arrToStr, mask, toTitleCase, splitTrim, } from './strings.js';
|
|
3
3
|
export { offsetedToPlainDate, plainDateCompare, isBetween, dateISOToPlainDate } from './dates.js';
|
package/esm/formats/strings.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a string into a flat list of trimmed, non-empty tokens. The default
|
|
3
|
+
* separator handles comma- and newline-separated input.
|
|
4
|
+
*
|
|
5
|
+
* `"a, b\n,c"` -> `["a", "b", "c"]`
|
|
6
|
+
*/
|
|
7
|
+
export declare const splitTrim: (input?: string | null, separator?: string | RegExp) => string[];
|
|
1
8
|
export declare const formatNumber: (number: number, digitNumber?: number) => string;
|
|
2
9
|
export declare function displayPhone(_entity: any, value: string | undefined): string;
|
|
3
10
|
export declare const arrToStr: (arr: string | undefined | (string | undefined)[]) => string;
|
package/esm/formats/strings.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** Default separator for `splitTrim`: any run of commas and/or newlines. */
|
|
2
|
+
const COMMA_OR_NEWLINE = /[,\n]+/;
|
|
3
|
+
/**
|
|
4
|
+
* Split a string into a flat list of trimmed, non-empty tokens. The default
|
|
5
|
+
* separator handles comma- and newline-separated input.
|
|
6
|
+
*
|
|
7
|
+
* `"a, b\n,c"` -> `["a", "b", "c"]`
|
|
8
|
+
*/
|
|
9
|
+
export const splitTrim = (input, separator = COMMA_OR_NEWLINE) => (input ?? '')
|
|
10
|
+
.split(separator)
|
|
11
|
+
.map((s) => s.trim())
|
|
12
|
+
.filter(Boolean);
|
|
1
13
|
export const formatNumber = (number, digitNumber = 2) => {
|
|
2
14
|
if (number === undefined || number === null || isNaN(number)) {
|
|
3
15
|
const value = 0;
|
package/esm/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { errorMessage, isError } from './core/helper.js';
|
|
|
15
15
|
export { tryCatch, tryCatchSync } from './core/tryCatch.js';
|
|
16
16
|
export type { ResolvedType, UnArray, RecursivePartial } from './core/types.js';
|
|
17
17
|
export { tw } from './core/tailwind.js';
|
|
18
|
+
export { stackHttpClient, withHeader } from './core/httpClientStack.js';
|
|
19
|
+
export type { HttpClientFetch, HttpClientMiddleware } from './core/httpClientStack.js';
|
|
18
20
|
export { FF_LogToConsole } from './SqlDatabase/FF_LogToConsole.js';
|
|
19
21
|
export { FilterEntity } from './virtual/FilterEntity.js';
|
|
20
22
|
export { UIEntity } from './virtual/UIEntity.js';
|
package/esm/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { FF_Validators, createValidators } from './core/FF_Validators.js';
|
|
|
15
15
|
export { errorMessage, isError } from './core/helper.js';
|
|
16
16
|
export { tryCatch, tryCatchSync } from './core/tryCatch.js';
|
|
17
17
|
export { tw } from './core/tailwind.js';
|
|
18
|
+
export { stackHttpClient, withHeader } from './core/httpClientStack.js';
|
|
18
19
|
// Misc primitives still exposed from the root.
|
|
19
20
|
export { FF_LogToConsole } from './SqlDatabase/FF_LogToConsole.js';
|
|
20
21
|
export { FilterEntity } from './virtual/FilterEntity.js';
|
|
@@ -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;
|