sveltekit-admin 0.6.0 → 0.9.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/README.md +61 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/server/adapters/drizzle/dataAdapter.js +196 -42
- package/dist/server/adapters/drizzle/index.d.ts +10 -1
- package/dist/server/adapters/drizzle/index.js +4 -0
- package/dist/server/adapters/prisma/dataAdapter.js +71 -11
- package/dist/server/adapters/prisma/handler.d.ts +14 -0
- package/dist/server/adapters/prisma/handler.js +37 -0
- package/dist/server/adapters/retry.d.ts +27 -0
- package/dist/server/adapters/retry.js +53 -0
- package/dist/server/adapters/types.d.ts +42 -3
- package/dist/server/audit.d.ts +65 -0
- package/dist/server/audit.js +106 -0
- package/dist/server/csrf.d.ts +33 -0
- package/dist/server/csrf.js +55 -0
- package/dist/server/data.d.ts +18 -2
- package/dist/server/data.js +39 -8
- package/dist/server/errors.d.ts +47 -0
- package/dist/server/errors.js +90 -0
- package/dist/server/handler.d.ts +92 -26
- package/dist/server/handler.js +263 -560
- package/dist/server/introspection/parser.d.ts +15 -0
- package/dist/server/introspection/parser.js +17 -0
- package/dist/server/mutations.d.ts +13 -0
- package/dist/server/mutations.js +476 -0
- package/dist/server/plugin.d.ts +47 -0
- package/dist/server/plugin.js +1 -0
- package/dist/server/pluginAccess.d.ts +7 -0
- package/dist/server/pluginAccess.js +79 -0
- package/dist/server/pluginRegistry.d.ts +12 -0
- package/dist/server/pluginRegistry.js +72 -0
- package/dist/server/query/listColumns.d.ts +19 -0
- package/dist/server/query/listColumns.js +43 -0
- package/dist/server/query/listQuery.d.ts +1 -1
- package/dist/server/query/pageSize.d.ts +19 -0
- package/dist/server/query/pageSize.js +26 -0
- package/dist/server/query/sortQuery.d.ts +31 -0
- package/dist/server/query/sortQuery.js +33 -0
- package/dist/server/query/urls.js +9 -1
- package/dist/server/relationLoaders.d.ts +42 -0
- package/dist/server/relationLoaders.js +188 -0
- package/dist/server/router.d.ts +10 -0
- package/dist/server/router.js +42 -19
- package/dist/server/runtime.d.ts +51 -0
- package/dist/server/runtime.js +263 -0
- package/dist/server/search.d.ts +14 -0
- package/dist/server/search.js +78 -0
- package/dist/server/submitted.d.ts +20 -0
- package/dist/server/submitted.js +54 -0
- package/dist/server/views/FieldInput.svelte +114 -8
- package/dist/server/views/FieldInput.svelte.d.ts +7 -0
- package/dist/server/views/Form.svelte +96 -5
- package/dist/server/views/Form.svelte.d.ts +19 -1
- package/dist/server/views/Layout.svelte +32 -4
- package/dist/server/views/Layout.svelte.d.ts +2 -0
- package/dist/server/views/List.svelte +156 -26
- package/dist/server/views/List.svelte.d.ts +7 -2
- package/dist/server/views/RelationCheckboxes.svelte +26 -5
- package/dist/server/views/RelationCheckboxes.svelte.d.ts +9 -0
- package/dist/server/views/RelationSelect.svelte +14 -3
- package/dist/server/views/RelationSelect.svelte.d.ts +2 -0
- package/dist/server/views/html.d.ts +19 -0
- package/dist/server/views/html.js +25 -0
- package/dist/server/views/pagination.d.ts +9 -0
- package/dist/server/views/pagination.js +31 -0
- package/dist/server/views/theme.js +147 -3
- package/dist/server/views/types.d.ts +15 -0
- package/package.json +24 -21
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PrismaField } from '../introspection/parser.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { SubmittedForm } from '../submitted.js';
|
|
4
|
+
import { toLabel, fieldErrorAttrs } from './html.js';
|
|
4
5
|
|
|
5
|
-
let {
|
|
6
|
-
|
|
6
|
+
let {
|
|
7
|
+
field,
|
|
8
|
+
value,
|
|
9
|
+
isReadonly,
|
|
10
|
+
submitted,
|
|
11
|
+
errorMessage,
|
|
12
|
+
enums = new Map<string, string[]>()
|
|
13
|
+
}: {
|
|
14
|
+
field: PrismaField;
|
|
15
|
+
value: any;
|
|
16
|
+
isReadonly: boolean;
|
|
17
|
+
/** Voir `Form.svelte` : défini seulement sur un re-rendu après échec. */
|
|
18
|
+
submitted?: SubmittedForm;
|
|
19
|
+
/** Défini seulement si c'est ce champ que l'erreur désigne. */
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
/** Valeurs de chaque enum du schéma, indexées par nom de type. */
|
|
22
|
+
enums?: Map<string, string[]>;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const err = $derived(fieldErrorAttrs(field.name, errorMessage));
|
|
7
26
|
|
|
8
27
|
const label = $derived(toLabel(field.name));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `submitted` défini signifie « ce formulaire vient d'être posté et refusé ».
|
|
31
|
+
* On teste sa présence, pas celle de la clé : une checkbox décochée et un
|
|
32
|
+
* champ sensible non ré-émis n'envoient rien, et retomber sur la valeur en
|
|
33
|
+
* base rallumerait à l'écran un booléen que l'utilisateur vient d'éteindre.
|
|
34
|
+
*
|
|
35
|
+
* Un champ readonly est exclu : la ligne en base fait foi puisqu'il n'est pas
|
|
36
|
+
* éditable. Un navigateur soumet bien un `readonly` (contrairement à un
|
|
37
|
+
* `disabled`), mais rien ne l'y oblige — un POST forgé ou un client partiel
|
|
38
|
+
* viderait alors l'ID et les horodatages à l'écran, sans que ce soit une
|
|
39
|
+
* donnée que l'utilisateur ait saisie.
|
|
40
|
+
*/
|
|
41
|
+
const hasSubmission = $derived(submitted !== undefined && !isReadonly);
|
|
42
|
+
const submittedValue = $derived(submitted?.values[field.name]);
|
|
9
43
|
const required = $derived(field.isRequired && !field.hasDefault && !isReadonly);
|
|
10
44
|
|
|
11
45
|
const lower = $derived(field.name.toLowerCase());
|
|
@@ -29,35 +63,107 @@
|
|
|
29
63
|
});
|
|
30
64
|
|
|
31
65
|
const inputValue = $derived.by(() => {
|
|
66
|
+
// Rendue telle qu'elle a été tapée : la re-normaliser (un `datetime-local`
|
|
67
|
+
// incomplet, un nombre en cours de frappe) transformerait la valeur refusée
|
|
68
|
+
// en une autre, et l'utilisateur ne reconnaîtrait plus ce qu'il a saisi.
|
|
69
|
+
if (hasSubmission) return submittedValue ?? '';
|
|
32
70
|
if (field.type === 'DateTime' && value) {
|
|
33
71
|
return new Date(value).toISOString().slice(0, 16);
|
|
34
72
|
}
|
|
35
73
|
return value ?? '';
|
|
36
74
|
});
|
|
37
75
|
|
|
38
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Valeur retenue par le `<select>` d'un enum : même arbitrage que
|
|
78
|
+
* `inputValue` (ce qui vient d'être soumis prime sur la ligne en base),
|
|
79
|
+
* ramené à une chaîne pour se comparer aux valeurs déclarées de l'enum.
|
|
80
|
+
*/
|
|
81
|
+
const enumValue = $derived(String(inputValue));
|
|
82
|
+
|
|
83
|
+
const jsonValue = $derived.by(() => {
|
|
84
|
+
if (hasSubmission) return submittedValue ?? '';
|
|
85
|
+
return value ? JSON.stringify(value, null, 2) : '';
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Absence = décochée : c'est ce que le navigateur envoie, et `formDataToPrisma`
|
|
89
|
+
// l'interprète déjà comme `false` côté écriture.
|
|
90
|
+
const isChecked = $derived(
|
|
91
|
+
hasSubmission ? submittedValue !== undefined : !!value
|
|
92
|
+
);
|
|
39
93
|
</script>
|
|
40
94
|
|
|
41
95
|
{#if field.type === 'Boolean'}
|
|
42
96
|
<div class="ska-field">
|
|
43
97
|
<label class="ska-checkbox-wrap">
|
|
44
|
-
<input
|
|
98
|
+
<input
|
|
99
|
+
type="checkbox"
|
|
100
|
+
name={field.name}
|
|
101
|
+
class="ska-checkbox"
|
|
102
|
+
checked={isChecked}
|
|
103
|
+
disabled={isReadonly}
|
|
104
|
+
aria-invalid={err.ariaInvalid}
|
|
105
|
+
aria-describedby={err.describedBy}
|
|
106
|
+
/>
|
|
45
107
|
<span class="ska-label">{label}</span>
|
|
46
108
|
</label>
|
|
109
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
110
|
+
{@html err.html}
|
|
47
111
|
</div>
|
|
48
112
|
{:else if field.type === 'Json'}
|
|
49
113
|
<div class="ska-field">
|
|
50
114
|
<label class="ska-label" for={field.name}>{label}{required ? ' *' : ''}</label>
|
|
51
|
-
<textarea id={field.name} name={field.name} class="ska-input" rows="4" readonly={isReadonly} required={required}>{jsonValue}</textarea>
|
|
115
|
+
<textarea id={field.name} name={field.name} class="ska-input" rows="4" readonly={isReadonly} required={required} aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy}>{jsonValue}</textarea>
|
|
116
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
117
|
+
{@html err.html}
|
|
52
118
|
</div>
|
|
53
119
|
{:else if isLongText}
|
|
54
120
|
<div class="ska-field">
|
|
55
121
|
<label class="ska-label" for={field.name}>{label}{required ? ' *' : ''}</label>
|
|
56
|
-
<textarea id={field.name} name={field.name} class="ska-input" rows="4" readonly={isReadonly} required={required}>{inputValue}</textarea>
|
|
122
|
+
<textarea id={field.name} name={field.name} class="ska-input" rows="4" readonly={isReadonly} required={required} aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy}>{inputValue}</textarea>
|
|
123
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
124
|
+
{@html err.html}
|
|
125
|
+
</div>
|
|
126
|
+
{:else if field.isEnum && enums.has(field.type)}
|
|
127
|
+
<!--
|
|
128
|
+
Calqué sur `RelationSelect.svelte` (même structure, même « — aucun — »), à
|
|
129
|
+
deux écarts près, tous deux volontaires :
|
|
130
|
+
|
|
131
|
+
1. Un placeholder désactivé quand la colonne est non nullable et n'a pas
|
|
132
|
+
encore de valeur. Sans lui le navigateur présélectionne la première
|
|
133
|
+
valeur de l'enum, et une création écrit un choix que l'utilisateur n'a
|
|
134
|
+
jamais fait. `RelationSelect` a le même défaut pour une FK obligatoire —
|
|
135
|
+
il n'est pas corrigé ici, ce serait un autre comportement dans une autre
|
|
136
|
+
PR.
|
|
137
|
+
2. `disabled` et non `readonly` : un `<select>` n'a pas de `readonly`. Le
|
|
138
|
+
champ sort donc du POST, `formDataToPrisma` ignore la clé absente et la
|
|
139
|
+
colonne n'est pas réécrite — exactement ce que « lecture seule » veut
|
|
140
|
+
dire, et plus sûr que les branches texte qui, elles, réémettent la valeur.
|
|
141
|
+
|
|
142
|
+
L'option vide se décide sur `field.isRequired` (la colonne accepte-t-elle
|
|
143
|
+
NULL) et non sur `required` (l'attribut du widget, faux dès qu'il y a un
|
|
144
|
+
`@default`) : une colonne non nullable à défaut n'accepte toujours pas
|
|
145
|
+
« aucun ».
|
|
146
|
+
-->
|
|
147
|
+
<div class="ska-field">
|
|
148
|
+
<label class="ska-label" for={field.name}>{label}{required ? ' *' : ''}</label>
|
|
149
|
+
<select id={field.name} name={field.name} class="ska-input" disabled={isReadonly} required={required} aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy}>
|
|
150
|
+
{#if !field.isRequired}
|
|
151
|
+
<option value="">— aucun —</option>
|
|
152
|
+
{:else if !enumValue}
|
|
153
|
+
<option value="" disabled selected>Select…</option>
|
|
154
|
+
{/if}
|
|
155
|
+
{#each enums.get(field.type)! as v (v)}
|
|
156
|
+
<option value={v} selected={v === enumValue}>{v}</option>
|
|
157
|
+
{/each}
|
|
158
|
+
</select>
|
|
159
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
160
|
+
{@html err.html}
|
|
57
161
|
</div>
|
|
58
162
|
{:else}
|
|
59
163
|
<div class="ska-field">
|
|
60
164
|
<label class="ska-label" for={field.name}>{label}{required ? ' *' : ''}</label>
|
|
61
|
-
<input id={field.name} type={inputType} name={field.name} value={inputValue} class="ska-input" readonly={isReadonly} required={required} />
|
|
165
|
+
<input id={field.name} type={inputType} name={field.name} value={inputValue} class="ska-input" readonly={isReadonly} required={required} aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy} />
|
|
166
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
167
|
+
{@html err.html}
|
|
62
168
|
</div>
|
|
63
169
|
{/if}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { PrismaField } from '../introspection/parser.js';
|
|
2
|
+
import type { SubmittedForm } from '../submitted.js';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
field: PrismaField;
|
|
4
5
|
value: any;
|
|
5
6
|
isReadonly: boolean;
|
|
7
|
+
/** Voir `Form.svelte` : défini seulement sur un re-rendu après échec. */
|
|
8
|
+
submitted?: SubmittedForm;
|
|
9
|
+
/** Défini seulement si c'est ce champ que l'erreur désigne. */
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
/** Valeurs de chaque enum du schéma, indexées par nom de type. */
|
|
12
|
+
enums?: Map<string, string[]>;
|
|
6
13
|
};
|
|
7
14
|
declare const FieldInput: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
15
|
type FieldInput = ReturnType<typeof FieldInput>;
|
|
@@ -1,35 +1,77 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { AdminHandlerConfig } from '../handler.js';
|
|
3
|
-
import type { ViewModel } from './types.js';
|
|
3
|
+
import type { RecordAction, ViewModel } from './types.js';
|
|
4
|
+
import type { SubmittedForm } from '../submitted.js';
|
|
5
|
+
import { isSensitiveStringField } from '../introspection/parser.js';
|
|
4
6
|
import FieldInput from './FieldInput.svelte';
|
|
5
7
|
import RelationSelect from './RelationSelect.svelte';
|
|
6
8
|
import RelationCheckboxes from './RelationCheckboxes.svelte';
|
|
7
9
|
import RelatedBlock from './RelatedBlock.svelte';
|
|
10
|
+
import { escapeHtml } from './html.js';
|
|
8
11
|
|
|
9
12
|
let {
|
|
10
13
|
mode,
|
|
11
14
|
model,
|
|
12
15
|
basePath,
|
|
13
16
|
config,
|
|
14
|
-
item
|
|
17
|
+
item,
|
|
18
|
+
recordActions = [],
|
|
19
|
+
submitted,
|
|
20
|
+
mutationError
|
|
15
21
|
}: {
|
|
16
22
|
mode: 'create' | 'edit';
|
|
17
23
|
model: ViewModel;
|
|
18
24
|
basePath: string;
|
|
19
25
|
config: AdminHandlerConfig;
|
|
20
26
|
item?: any;
|
|
27
|
+
recordActions?: RecordAction[];
|
|
28
|
+
/**
|
|
29
|
+
* Ce que le POST venait de soumettre, quand la mutation a échoué. Défini
|
|
30
|
+
* uniquement sur un re-rendu d'erreur : `undefined` veut dire « affichage
|
|
31
|
+
* normal », et c'est cette distinction — pas le contenu — qui permet à un
|
|
32
|
+
* booléen décoché de rester décoché.
|
|
33
|
+
*/
|
|
34
|
+
submitted?: SubmittedForm;
|
|
35
|
+
/**
|
|
36
|
+
* L'échec qui a provoqué ce re-rendu. `field` est absent quand la cause ne
|
|
37
|
+
* désigne pas un champ (conflit d'unicité, message générique) : le message
|
|
38
|
+
* ne vit alors que dans la bannière de page, posée par `handler.ts`.
|
|
39
|
+
*/
|
|
40
|
+
mutationError?: { message: string; field?: string };
|
|
21
41
|
} = $props();
|
|
22
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Le message n'est rendu qu'au widget que l'erreur nomme. Un champ absent du
|
|
45
|
+
* formulaire (masqué, ou un scalaire sans widget propre) ne fait rien
|
|
46
|
+
* apparaître : la bannière reste le canal de repli.
|
|
47
|
+
*/
|
|
48
|
+
const errorFor = (...names: Array<string | undefined>) =>
|
|
49
|
+
mutationError?.field !== undefined && names.includes(mutationError.field)
|
|
50
|
+
? mutationError.message
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
23
53
|
const modelConfig = $derived(config.models?.[model.name] || {});
|
|
24
54
|
const hidden = $derived(modelConfig.hidden || []);
|
|
25
55
|
const readonly = $derived(modelConfig.readonly || []);
|
|
26
56
|
const listPath = $derived(`${basePath}/${model.name.toLowerCase()}`);
|
|
27
57
|
|
|
58
|
+
/**
|
|
59
|
+
* `Bytes` n'a pas de widget : rendue dans la branche texte générique, la
|
|
60
|
+
* colonne renvoyait au pilote une chaîne là où il attend un `Uint8Array`,
|
|
61
|
+
* donc aucune écriture n'a jamais pu aboutir. L'exclure rend un modèle à
|
|
62
|
+
* colonne `Bytes` obligatoire incréable depuis l'admin — c'est déjà le cas
|
|
63
|
+
* en pratique, à ceci près que l'échec arrive maintenant avant le formulaire
|
|
64
|
+
* plutôt qu'après le POST. La liste l'écarte déjà pareillement
|
|
65
|
+
* (`List.svelte`), c'est la même décision au même endroit du schéma.
|
|
66
|
+
*/
|
|
67
|
+
const isEditableType = (f: { type: string }) => f.type !== 'Bytes';
|
|
68
|
+
|
|
28
69
|
const formFields = $derived(
|
|
29
70
|
mode === 'create'
|
|
30
71
|
? model.fields.filter(
|
|
31
72
|
(f) =>
|
|
32
73
|
!hidden.includes(f.name) &&
|
|
74
|
+
isEditableType(f) &&
|
|
33
75
|
!f.isId &&
|
|
34
76
|
!f.isCreatedAt &&
|
|
35
77
|
!f.isUpdatedAt &&
|
|
@@ -41,7 +83,15 @@
|
|
|
41
83
|
: model.fields.filter(
|
|
42
84
|
(f) =>
|
|
43
85
|
!hidden.includes(f.name) &&
|
|
86
|
+
isEditableType(f) &&
|
|
44
87
|
!f.relation &&
|
|
88
|
+
// Exclu à l'édition seulement : il existe ici une valeur stockée,
|
|
89
|
+
// donc quelque chose à exposer dans le HTML et à écraser au
|
|
90
|
+
// prochain enregistrement. Le formulaire de création le garde — il
|
|
91
|
+
// n'y a rien à fuiter avant que la ligne existe, et le retirer
|
|
92
|
+
// rendrait un modèle à colonne obligatoire incréable depuis
|
|
93
|
+
// l'admin. `mutations.ts` refuse le vide de ce côté-là.
|
|
94
|
+
!isSensitiveStringField(f) &&
|
|
45
95
|
!model.relationGraph?.scalarToRelation.has(f.name)
|
|
46
96
|
)
|
|
47
97
|
);
|
|
@@ -62,7 +112,14 @@
|
|
|
62
112
|
: []
|
|
63
113
|
);
|
|
64
114
|
|
|
65
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Sur un re-rendu d'erreur, le scalaire soumis fait foi ; sinon la ligne en
|
|
117
|
+
* base. Le `??` ne suffirait pas : une relation optionnelle qu'on vient de
|
|
118
|
+
* vider arrive comme `''`, ce qui doit rester `''` et non retomber sur
|
|
119
|
+
* l'ancienne cible.
|
|
120
|
+
*/
|
|
121
|
+
const currentValueOf = (scalarName: string) =>
|
|
122
|
+
submitted ? (submitted.values[scalarName] ?? '') : item ? item[scalarName] : null;
|
|
66
123
|
|
|
67
124
|
const relationCheckboxGroups = $derived(
|
|
68
125
|
model.relationGraph
|
|
@@ -77,6 +134,24 @@
|
|
|
77
134
|
: []
|
|
78
135
|
);
|
|
79
136
|
|
|
137
|
+
// Built as a single string (rather than {#if}/{#each}) so an empty/create-mode
|
|
138
|
+
// render stays a single @html call: Svelte 5's SSR wraps every {#if}/{#each} node
|
|
139
|
+
// in its own hydration-boundary comment regardless of the branch/array taken, so
|
|
140
|
+
// nesting recordActions in its own control-flow blocks would add bytes to every
|
|
141
|
+
// edit-form render even when recordActions is []. `label` and `href` are both
|
|
142
|
+
// escaped manually since this goes through @html instead of Svelte's
|
|
143
|
+
// auto-escaped text/attributes.
|
|
144
|
+
const recordActionsHtml = $derived(
|
|
145
|
+
mode === 'edit' && recordActions.length > 0
|
|
146
|
+
? `<div class="ska-record-actions">${recordActions
|
|
147
|
+
.map(
|
|
148
|
+
(action) =>
|
|
149
|
+
`<a href="${escapeHtml(action.href)}" class="ska-btn ska-btn--secondary ska-btn--sm">${escapeHtml(action.label)}</a>`
|
|
150
|
+
)
|
|
151
|
+
.join('')}</div>`
|
|
152
|
+
: ''
|
|
153
|
+
);
|
|
154
|
+
|
|
80
155
|
const inverseEdges = $derived(
|
|
81
156
|
model.relationGraph
|
|
82
157
|
? [...model.relationGraph.edges.values()].filter(
|
|
@@ -92,11 +167,21 @@
|
|
|
92
167
|
<p class="ska-subtitle">ID: {item[model.primaryKey]}</p>
|
|
93
168
|
{/if}
|
|
94
169
|
|
|
170
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- recordActionsHtml escapes both action.label and action.href via escapeHtml -->
|
|
171
|
+
{@html recordActionsHtml}
|
|
172
|
+
|
|
95
173
|
<div class="ska-card">
|
|
96
174
|
<form method="POST" class="ska-form">
|
|
97
175
|
<input type="hidden" name="_action" value={mode === 'create' ? 'create' : 'update'} />
|
|
98
176
|
{#each formFields as f (f.name)}
|
|
99
|
-
<FieldInput
|
|
177
|
+
<FieldInput
|
|
178
|
+
field={f}
|
|
179
|
+
value={item ? item[f.name] : null}
|
|
180
|
+
isReadonly={isFieldReadonly(f)}
|
|
181
|
+
{submitted}
|
|
182
|
+
errorMessage={errorFor(f.name)}
|
|
183
|
+
enums={model.enums}
|
|
184
|
+
/>
|
|
100
185
|
{/each}
|
|
101
186
|
{#each relationSelects as edge (edge.field)}
|
|
102
187
|
<RelationSelect
|
|
@@ -104,10 +189,16 @@
|
|
|
104
189
|
meta={model.relationOptions!.get(`${edge.model}.${edge.field}`)!}
|
|
105
190
|
currentValue={currentValueOf(edge.scalarFields[0])}
|
|
106
191
|
{config}
|
|
192
|
+
errorMessage={errorFor(edge.field, edge.scalarFields[0])}
|
|
107
193
|
/>
|
|
108
194
|
{/each}
|
|
109
195
|
{#each relationCheckboxGroups as edge (edge.field)}
|
|
110
|
-
<RelationCheckboxes
|
|
196
|
+
<RelationCheckboxes
|
|
197
|
+
{edge}
|
|
198
|
+
meta={model.relationOptions!.get(`${edge.model}.${edge.field}`)!}
|
|
199
|
+
submittedIds={submitted?.m2m[edge.field]}
|
|
200
|
+
errorMessage={errorFor(edge.field)}
|
|
201
|
+
/>
|
|
111
202
|
{/each}
|
|
112
203
|
<div class="ska-form__actions">
|
|
113
204
|
<button type="submit" class="ska-btn ska-btn--primary">{mode === 'create' ? 'Create' : 'Save Changes'}</button>
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type { AdminHandlerConfig } from '../handler.js';
|
|
2
|
-
import type { ViewModel } from './types.js';
|
|
2
|
+
import type { RecordAction, ViewModel } from './types.js';
|
|
3
|
+
import type { SubmittedForm } from '../submitted.js';
|
|
3
4
|
type $$ComponentProps = {
|
|
4
5
|
mode: 'create' | 'edit';
|
|
5
6
|
model: ViewModel;
|
|
6
7
|
basePath: string;
|
|
7
8
|
config: AdminHandlerConfig;
|
|
8
9
|
item?: any;
|
|
10
|
+
recordActions?: RecordAction[];
|
|
11
|
+
/**
|
|
12
|
+
* Ce que le POST venait de soumettre, quand la mutation a échoué. Défini
|
|
13
|
+
* uniquement sur un re-rendu d'erreur : `undefined` veut dire « affichage
|
|
14
|
+
* normal », et c'est cette distinction — pas le contenu — qui permet à un
|
|
15
|
+
* booléen décoché de rester décoché.
|
|
16
|
+
*/
|
|
17
|
+
submitted?: SubmittedForm;
|
|
18
|
+
/**
|
|
19
|
+
* L'échec qui a provoqué ce re-rendu. `field` est absent quand la cause ne
|
|
20
|
+
* désigne pas un champ (conflit d'unicité, message générique) : le message
|
|
21
|
+
* ne vit alors que dans la bannière de page, posée par `handler.ts`.
|
|
22
|
+
*/
|
|
23
|
+
mutationError?: {
|
|
24
|
+
message: string;
|
|
25
|
+
field?: string;
|
|
26
|
+
};
|
|
9
27
|
};
|
|
10
28
|
declare const Form: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
29
|
type Form = ReturnType<typeof Form>;
|
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
content,
|
|
7
7
|
config,
|
|
8
8
|
modelList,
|
|
9
|
-
currentModel
|
|
9
|
+
currentModel,
|
|
10
|
+
extraStyles = '',
|
|
11
|
+
extraScripts = ''
|
|
10
12
|
}: {
|
|
11
13
|
content: string;
|
|
12
14
|
config: AdminHandlerConfig;
|
|
13
15
|
modelList: Array<{ name: string; label: string }>;
|
|
14
16
|
currentModel?: string;
|
|
17
|
+
extraStyles?: string;
|
|
18
|
+
extraScripts?: string;
|
|
15
19
|
} = $props();
|
|
16
20
|
|
|
17
21
|
const branding = $derived(config.branding ?? {});
|
|
@@ -21,6 +25,25 @@
|
|
|
21
25
|
// No button at all if `logout` isn't configured — an admin that never
|
|
22
26
|
// opted into this option looks exactly as it did before it existed.
|
|
23
27
|
const showLogout = $derived(Boolean(config.logout));
|
|
28
|
+
|
|
29
|
+
// extraStyles is concatenated into the SAME @html expression as the theme <style>
|
|
30
|
+
// below, rather than a sibling {#if}/{@html} block: Svelte 5's SSR unconditionally
|
|
31
|
+
// wraps every {#if}/{#each}/{@html} node in its own hydration-boundary comment, even
|
|
32
|
+
// for a false/empty branch (see svelte/internal/server's `html()` helper) — a sibling
|
|
33
|
+
// block would add bytes to every render regardless of extraStyles being set.
|
|
34
|
+
// Concatenating keeps this ONE @html call, byte-identical to the pre-plugin-slots
|
|
35
|
+
// template when extraStyles is ''. extraScripts (bottom of <body>) has no such
|
|
36
|
+
// pre-existing @html call to fold into, so it stays its own @html — the smallest
|
|
37
|
+
// achievable footprint, though it still adds a fixed hydration-boundary comment
|
|
38
|
+
// pair even when empty (see task-6-report.md fix-round-1 notes).
|
|
39
|
+
//
|
|
40
|
+
// Built as a $derived here (rather than a nested template literal inline in the
|
|
41
|
+
// markup below) so tooling that tag-sniffs {@html} expressions for literal
|
|
42
|
+
// <style>/<script> text doesn't misparse the nested backticks.
|
|
43
|
+
const headStyleHtml = $derived(
|
|
44
|
+
`<style>${styles(primaryColor)}</style>${extraStyles ? `<style>${extraStyles}</style>` : ''}`
|
|
45
|
+
);
|
|
46
|
+
const bodyScriptHtml = $derived(extraScripts ? '<script>' + extraScripts + '</scr' + 'ipt>' : '');
|
|
24
47
|
</script>
|
|
25
48
|
|
|
26
49
|
<!doctype html>
|
|
@@ -31,14 +54,17 @@
|
|
|
31
54
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
32
55
|
<title>{title}</title>
|
|
33
56
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- CSS injected as raw text; a literal <style> block can't take a dynamic value; primaryColor is developer-supplied config, not request/database data, and this raw interpolation is unchanged from the original layout.ts implementation, not a new injection point introduced by this migration -->
|
|
34
|
-
{@html
|
|
57
|
+
{@html headStyleHtml}
|
|
35
58
|
</head>
|
|
36
59
|
<!-- eslint-disable-next-line svelte/no-raw-special-elements -- server-only full-document template, never mounted client-side -->
|
|
37
60
|
<body>
|
|
61
|
+
<!-- Premier élément focusable de la page : sans lui, atteindre le contenu au
|
|
62
|
+
clavier demande de retraverser toute la nav latérale à chaque page. -->
|
|
63
|
+
<a href="#ska-content" class="ska-skip">Skip to content</a>
|
|
38
64
|
<div class="ska-layout">
|
|
39
65
|
<aside class="ska-sidebar">
|
|
40
66
|
<a href={basePath} class="ska-logo">{title}</a>
|
|
41
|
-
<nav>
|
|
67
|
+
<nav aria-label="Main">
|
|
42
68
|
<ul class="ska-nav">
|
|
43
69
|
<li class="ska-nav__item">
|
|
44
70
|
<a href={basePath} class="ska-nav__link" class:ska-nav__link--active={!currentModel}>
|
|
@@ -69,10 +95,12 @@
|
|
|
69
95
|
</form>
|
|
70
96
|
{/if}
|
|
71
97
|
</aside>
|
|
72
|
-
<main class="ska-main">
|
|
98
|
+
<main class="ska-main" id="ska-content" tabindex="-1">
|
|
73
99
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- content is pre-rendered HTML from sibling view components / the handler's own escaped error string -->
|
|
74
100
|
{@html content}
|
|
75
101
|
</main>
|
|
76
102
|
</div>
|
|
103
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- plugin JS is developer-supplied, same trust as branding.primaryColor -->
|
|
104
|
+
{@html bodyScriptHtml}
|
|
77
105
|
</body>
|
|
78
106
|
</html>
|