5htp-core 0.2.5-2 → 0.2.6
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/package.json +1 -1
- package/src/client/components/Dialog/Manager.tsx +4 -5
- package/src/client/components/Form.ts +88 -36
- package/src/client/components/Form_old/index.tsx +17 -17
- package/src/client/components/Form_old/index.tsx.old +17 -17
- package/src/client/components/Select/ChoiceSelector.tsx +172 -0
- package/src/client/components/Select/index.tsx +27 -138
- package/src/client/components/containers/Popover/getPosition.ts +14 -11
- package/src/client/components/containers/Popover/index.tsx +52 -35
- package/src/client/components/containers/Popover/popover.less +7 -1
- package/src/client/components/dropdown/index.tsx +1 -1
- package/src/client/components/index.ts +1 -1
- package/src/client/components/input/Number/index.tsx +2 -2
- package/src/client/components/inputv3/{string/index.tsx → index.tsx} +6 -2
- package/src/client/pages/_messages/403.tsx +1 -1
- package/src/client/pages/_messages/500.tsx +1 -1
- package/src/client/pages/bug.tsx +3 -3
- package/src/client/utils/dom.ts +12 -1
- package/src/common/validation/schema.ts +26 -27
- package/src/common/validation/validator.ts +14 -5
- package/src/server/app/index.ts +6 -2
- package/src/server/services/schema/request.ts +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.6",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
import { ComponentChild } from 'preact';
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Core
|
|
10
10
|
import useContext from '@/client/context';
|
|
11
|
+
import { blurable, deepContains, focusContent } from '@client/utils/dom';
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
+
// Specific
|
|
13
14
|
import type Application from '../../app';
|
|
14
15
|
import Card, { Props as CardInfos } from './card';
|
|
15
16
|
import Button from '../button';
|
|
@@ -242,9 +243,7 @@ export default () => {
|
|
|
242
243
|
|
|
243
244
|
// Focus
|
|
244
245
|
const lastToast = modals[ modals.length - 1 ];
|
|
245
|
-
|
|
246
|
-
console.log('Element to focus', toFocus);
|
|
247
|
-
toFocus.focus();
|
|
246
|
+
focusContent( lastToast );
|
|
248
247
|
|
|
249
248
|
// Backdrop color
|
|
250
249
|
const header = lastToast.querySelector('header');
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
8
|
// Core
|
|
9
|
+
import { InputError } from '@common/errors';
|
|
9
10
|
import type { Schema } from '@common/validation';
|
|
11
|
+
import type { TValidationResult } from '@common/validation/schema';
|
|
12
|
+
import useContext from '@/client/context';
|
|
10
13
|
|
|
11
14
|
/*----------------------------------
|
|
12
15
|
- TYPES
|
|
13
16
|
----------------------------------*/
|
|
14
17
|
type TFormOptions<TFormData extends {}> = {
|
|
15
18
|
data?: Partial<TFormData>,
|
|
16
|
-
submit?: (data: TFormData) => Promise<void
|
|
19
|
+
submit?: (data: TFormData) => Promise<void>,
|
|
20
|
+
autoValidateOnly?: (keyof TFormData)[],
|
|
21
|
+
autoSave?: {
|
|
22
|
+
id: string
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
type FieldsAttrs<TFormData extends {}> = {
|
|
@@ -21,87 +28,134 @@ type FieldsAttrs<TFormData extends {}> = {
|
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export type Form<TFormData extends {} = {}> = {
|
|
31
|
+
fields: FieldsAttrs<TFormData>,
|
|
24
32
|
data: TFormData,
|
|
33
|
+
options: TFormOptions<TFormData>,
|
|
34
|
+
validate: (data: TFormData) => TValidationResult<{}>,
|
|
25
35
|
set: (data: Partial<TFormData>) => void,
|
|
26
36
|
submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
|
|
27
|
-
fields: FieldsAttrs<TFormData>,
|
|
28
37
|
} & FormState
|
|
29
38
|
|
|
30
39
|
type FormState = {
|
|
31
40
|
isLoading: boolean,
|
|
32
41
|
errorsCount: number,
|
|
33
|
-
errors: {[fieldName: string]: string[]},
|
|
34
|
-
changed: boolean
|
|
42
|
+
errors: { [fieldName: string]: string[] },
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/*----------------------------------
|
|
38
46
|
- HOOK
|
|
39
47
|
----------------------------------*/
|
|
40
|
-
export default function useForm<TFormData extends {}>(
|
|
48
|
+
export default function useForm<TFormData extends {}>(
|
|
49
|
+
schema: Schema<TFormData>,
|
|
50
|
+
options: TFormOptions<TFormData>
|
|
51
|
+
) {
|
|
52
|
+
|
|
53
|
+
const context = useContext();
|
|
41
54
|
|
|
42
55
|
/*----------------------------------
|
|
43
56
|
- INIT
|
|
44
57
|
----------------------------------*/
|
|
45
|
-
|
|
58
|
+
let initialData: any;
|
|
59
|
+
if (options.autoSave && typeof window !== 'undefined') {
|
|
60
|
+
const autosaved = localStorage.getItem('form.' + options.autoSave.id);
|
|
61
|
+
if (autosaved !== null) {
|
|
62
|
+
try {
|
|
63
|
+
console.log('[form] Parse autosaved from json:', autosaved);
|
|
64
|
+
initialData = JSON.parse(autosaved);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('[form] Failed to decode autosaved data from json:', autosaved);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (initialData === undefined)
|
|
71
|
+
initialData = options.data || {};
|
|
46
72
|
|
|
47
|
-
const
|
|
73
|
+
const fields = React.useRef<FieldsAttrs<TFormData>>(null);
|
|
74
|
+
const [data, setData] = React.useState<TFormData>(initialData);
|
|
48
75
|
const [state, setState] = React.useState<FormState>({
|
|
49
76
|
isLoading: false,
|
|
50
77
|
errorsCount: 0,
|
|
51
|
-
errors: {}
|
|
52
|
-
changed: false
|
|
78
|
+
errors: {}
|
|
53
79
|
});
|
|
54
80
|
|
|
55
81
|
// Validate data when it changes
|
|
56
82
|
React.useEffect(() => {
|
|
57
|
-
|
|
83
|
+
|
|
84
|
+
// Validate
|
|
85
|
+
validate(data, false);
|
|
86
|
+
|
|
87
|
+
// Autosave
|
|
88
|
+
if (options.autoSave !== undefined)
|
|
89
|
+
saveLocally(data, options.autoSave);
|
|
90
|
+
|
|
58
91
|
}, [data]);
|
|
59
92
|
|
|
60
93
|
/*----------------------------------
|
|
61
94
|
- ACTIONS
|
|
62
95
|
----------------------------------*/
|
|
63
|
-
const validate = (allData: TFormData) => {
|
|
96
|
+
const validate = (allData: TFormData = data, validateAll: boolean = true) => {
|
|
64
97
|
|
|
65
|
-
const validated = schema.validate(allData, allData
|
|
98
|
+
const validated = schema.validate(allData, allData, {}, {
|
|
99
|
+
// Ignore the fields where the vlaue has not been changed
|
|
100
|
+
// if the validation was triggered via onChange
|
|
101
|
+
ignoreMissing: !validateAll,
|
|
102
|
+
// The list of fields we should only validate
|
|
103
|
+
only: options.autoValidateOnly
|
|
104
|
+
});
|
|
66
105
|
|
|
67
106
|
// Update errors
|
|
68
|
-
if (validated.
|
|
69
|
-
rebuildFieldsAttrs({
|
|
70
|
-
errorsCount: validated.
|
|
107
|
+
if (validated.errorsCount !== state.errorsCount) {
|
|
108
|
+
rebuildFieldsAttrs({
|
|
109
|
+
errorsCount: validated.errorsCount,
|
|
71
110
|
errors: validated.erreurs,
|
|
72
111
|
});
|
|
73
112
|
}
|
|
74
|
-
|
|
113
|
+
|
|
75
114
|
return validated;
|
|
76
115
|
}
|
|
77
116
|
|
|
78
|
-
const submit = (additionnalData: Partial<TFormData> = {}) => {
|
|
117
|
+
const submit = async (additionnalData: Partial<TFormData> = {}) => {
|
|
79
118
|
|
|
80
119
|
const allData = { ...data, ...additionnalData }
|
|
81
120
|
|
|
82
121
|
// Validation
|
|
83
122
|
const validated = validate(allData);
|
|
84
|
-
if (validated.
|
|
123
|
+
if (validated.errorsCount !== 0) {
|
|
124
|
+
context.app.handleError(
|
|
125
|
+
new InputError("You have " + validated.errorsCount + " errors in the form.")
|
|
126
|
+
);
|
|
85
127
|
return;
|
|
128
|
+
}
|
|
86
129
|
|
|
87
130
|
// Callback
|
|
131
|
+
let submitResult: any;
|
|
88
132
|
if (options.submit)
|
|
89
|
-
|
|
133
|
+
submitResult = await options.submit(allData);
|
|
134
|
+
|
|
135
|
+
// Reset autosaved data
|
|
136
|
+
if (options.autoSave)
|
|
137
|
+
localStorage.removeItem(options.autoSave.id);
|
|
138
|
+
|
|
139
|
+
return submitResult;
|
|
90
140
|
}
|
|
91
141
|
|
|
92
142
|
const rebuildFieldsAttrs = (newState: Partial<FormState> = {}) => {
|
|
93
143
|
// Force rebuilding the fields definition on the next state change
|
|
94
|
-
fields.current = null;
|
|
144
|
+
fields.current = null;
|
|
95
145
|
// Force state change
|
|
96
|
-
setState(
|
|
146
|
+
setState(old => ({
|
|
97
147
|
...old,
|
|
98
|
-
...newState
|
|
99
|
-
changed: true
|
|
148
|
+
...newState
|
|
100
149
|
}));
|
|
101
150
|
}
|
|
102
151
|
|
|
152
|
+
const saveLocally = (data: TFormData, id: string) => {
|
|
153
|
+
console.log('[form] Autosave data for form:', id, ':', data);
|
|
154
|
+
localStorage.setItem('form.' + id, JSON.stringify(data));
|
|
155
|
+
}
|
|
156
|
+
|
|
103
157
|
// Rebuild the fields attrs when the schema changes
|
|
104
|
-
if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')){
|
|
158
|
+
if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')) {
|
|
105
159
|
fields.current = {}
|
|
106
160
|
for (const fieldName in schema.fields) {
|
|
107
161
|
fields.current[fieldName] = {
|
|
@@ -109,13 +163,9 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
109
163
|
// Value control
|
|
110
164
|
value: data[fieldName],
|
|
111
165
|
onChange: (val) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}));
|
|
116
|
-
setData( old => {
|
|
117
|
-
return {
|
|
118
|
-
...old,
|
|
166
|
+
setData(old => {
|
|
167
|
+
return {
|
|
168
|
+
...old,
|
|
119
169
|
[fieldName]: typeof val === 'function'
|
|
120
170
|
? val(old[fieldName])
|
|
121
171
|
: val
|
|
@@ -131,9 +181,9 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
131
181
|
},
|
|
132
182
|
|
|
133
183
|
// Error
|
|
134
|
-
errors: state.errors[
|
|
135
|
-
required: schema.fields[
|
|
136
|
-
validator: schema.fields[
|
|
184
|
+
errors: state.errors[fieldName],
|
|
185
|
+
required: schema.fields[fieldName].options?.opt !== true,
|
|
186
|
+
validator: schema.fields[fieldName]
|
|
137
187
|
}
|
|
138
188
|
}
|
|
139
189
|
}
|
|
@@ -143,12 +193,14 @@ export default function useForm<TFormData extends {}>( schema: Schema<TFormData>
|
|
|
143
193
|
----------------------------------*/
|
|
144
194
|
|
|
145
195
|
const form = {
|
|
196
|
+
fields: fields.current,
|
|
146
197
|
data,
|
|
147
198
|
set: setData,
|
|
199
|
+
validate,
|
|
148
200
|
submit,
|
|
149
|
-
|
|
201
|
+
options,
|
|
150
202
|
...state
|
|
151
203
|
}
|
|
152
|
-
|
|
204
|
+
|
|
153
205
|
return [form, fields.current]
|
|
154
206
|
}
|
|
@@ -85,13 +85,13 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
85
85
|
const [state, setState] = React.useState<{
|
|
86
86
|
donnees: Partial<TDonnees>,
|
|
87
87
|
erreurs: TListeErreursSaisie,
|
|
88
|
-
|
|
88
|
+
errorsCount: number,
|
|
89
89
|
progression: false | number,
|
|
90
90
|
changed: Partial<TDonnees> // Nom des champs changés depuis le dernier enregistrement
|
|
91
91
|
}>({
|
|
92
92
|
donnees: props.donnees || {},
|
|
93
93
|
erreurs: {},
|
|
94
|
-
|
|
94
|
+
errorsCount: 0,
|
|
95
95
|
progression: false,
|
|
96
96
|
changed: {}
|
|
97
97
|
});
|
|
@@ -129,7 +129,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
129
129
|
async function onChange(
|
|
130
130
|
valeursInit: Partial<TDonnees>,
|
|
131
131
|
newState: Partial<typeof state> = {},
|
|
132
|
-
): Promise<{ erreurs: TListeErreursSaisie,
|
|
132
|
+
): Promise<{ erreurs: TListeErreursSaisie, errorsCount: number }> {
|
|
133
133
|
|
|
134
134
|
// RAPPEL: donnees = anciennes données
|
|
135
135
|
|
|
@@ -142,7 +142,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
142
142
|
|
|
143
143
|
if (debug) console.log(`[form][saisie] onChange`, changees);
|
|
144
144
|
|
|
145
|
-
let
|
|
145
|
+
let errorsCount: number = 0;
|
|
146
146
|
let erreurs: TListeErreursSaisie = {}
|
|
147
147
|
if (Object.keys(changees).length !== 0) {
|
|
148
148
|
|
|
@@ -151,14 +151,14 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
151
151
|
let nouvellesDonnees: Partial<TDonnees>;
|
|
152
152
|
({
|
|
153
153
|
valeurs: nouvellesDonnees,
|
|
154
|
-
|
|
154
|
+
errorsCount,
|
|
155
155
|
erreurs
|
|
156
156
|
} = await valider(valeursInit));
|
|
157
157
|
|
|
158
|
-
if (debug &&
|
|
158
|
+
if (debug && errorsCount !== 0) console.log(`[form][saisie] erreurs`, erreurs);
|
|
159
159
|
|
|
160
160
|
newState.erreurs = erreurs;
|
|
161
|
-
newState.
|
|
161
|
+
newState.errorsCount = errorsCount;
|
|
162
162
|
|
|
163
163
|
// Validation & mapping personnalisé
|
|
164
164
|
/*if (props.filtres?.after)
|
|
@@ -177,8 +177,8 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
177
177
|
...newState
|
|
178
178
|
}));
|
|
179
179
|
|
|
180
|
-
if (props.onChange &&
|
|
181
|
-
props.onChange(donneesCompletes, { valeurs: nouvellesDonnees,
|
|
180
|
+
if (props.onChange && errorsCount === 0)
|
|
181
|
+
props.onChange(donneesCompletes, { valeurs: nouvellesDonnees, errorsCount, erreurs, changed });
|
|
182
182
|
|
|
183
183
|
/*if (valider && props.autosave === true) {
|
|
184
184
|
|
|
@@ -191,7 +191,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
191
191
|
}*/
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
return { erreurs,
|
|
194
|
+
return { erreurs, errorsCount };
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
async function valider(
|
|
@@ -206,7 +206,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
206
206
|
});
|
|
207
207
|
|
|
208
208
|
// Focus sur le premier champ ayant déclenché une erreur
|
|
209
|
-
if (retour.
|
|
209
|
+
if (retour.errorsCount !== 0) {
|
|
210
210
|
|
|
211
211
|
const cheminChamp = Object.keys(retour.erreurs)[0]
|
|
212
212
|
const champ = chemin.get(schema, cheminChamp);
|
|
@@ -233,15 +233,15 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
233
233
|
if (props.progression !== undefined && props.progression !== false)
|
|
234
234
|
return false;
|
|
235
235
|
|
|
236
|
-
if (state.
|
|
236
|
+
if (state.errorsCount !== 0)
|
|
237
237
|
return false;
|
|
238
238
|
|
|
239
239
|
console.log(`[form][saisie] Envoyer`, donnees);
|
|
240
240
|
|
|
241
241
|
// Validation de l'ensemble des champs
|
|
242
|
-
const { erreurs,
|
|
243
|
-
if (
|
|
244
|
-
setState((stateA) => ({ ...stateA, erreurs,
|
|
242
|
+
const { erreurs, errorsCount, valeurs } = await valider(donnees);
|
|
243
|
+
if (errorsCount !== 0) {
|
|
244
|
+
setState((stateA) => ({ ...stateA, erreurs, errorsCount }));
|
|
245
245
|
console.error('Erreurs formulaire', erreurs);
|
|
246
246
|
return false;
|
|
247
247
|
}
|
|
@@ -369,10 +369,10 @@ export const useForm = <TDonnees extends TObjetDonnees>(
|
|
|
369
369
|
});
|
|
370
370
|
|
|
371
371
|
// Application des changements
|
|
372
|
-
const {
|
|
372
|
+
const { errorsCount } = await onChange(nouvellesDonnees, {});
|
|
373
373
|
|
|
374
374
|
// Si aucune erreur, onChange propre au champ + gestion erreurs
|
|
375
|
-
if (propsChamp.onChange !== undefined &&
|
|
375
|
+
if (propsChamp.onChange !== undefined && errorsCount === 0)
|
|
376
376
|
await propsChamp.onChange(nouvelleValeur).catch((e) => {
|
|
377
377
|
setState((stateA) => ({
|
|
378
378
|
...stateA,
|
|
@@ -78,13 +78,13 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
78
78
|
const [state, setState] = React.useState<{
|
|
79
79
|
donnees: Partial<TDonnees>,
|
|
80
80
|
erreurs: TListeErreursSaisie,
|
|
81
|
-
|
|
81
|
+
errorsCount: number,
|
|
82
82
|
progression: false | number,
|
|
83
83
|
changed: Partial<TDonnees> // Nom des champs changés depuis le dernier enregistrement
|
|
84
84
|
}>({
|
|
85
85
|
donnees: props.donnees || {},
|
|
86
86
|
erreurs: {},
|
|
87
|
-
|
|
87
|
+
errorsCount: 0,
|
|
88
88
|
progression: false,
|
|
89
89
|
changed: {}
|
|
90
90
|
});
|
|
@@ -109,7 +109,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
109
109
|
valeursInit: Partial<TDonnees>,
|
|
110
110
|
newState: Partial<typeof state> = {},
|
|
111
111
|
validerMaintenant: boolean = true
|
|
112
|
-
): Promise<{ erreurs: TListeErreursSaisie,
|
|
112
|
+
): Promise<{ erreurs: TListeErreursSaisie, errorsCount: number }> {
|
|
113
113
|
|
|
114
114
|
// RAPPEL: donnees = anciennes données
|
|
115
115
|
|
|
@@ -118,20 +118,20 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
118
118
|
// Validation des données changées
|
|
119
119
|
// TODO: conserver erreurs des champs n'ayant pas été changés
|
|
120
120
|
let nouvellesDonnees: Partial<TDonnees>;
|
|
121
|
-
let
|
|
121
|
+
let errorsCount: number = 0;
|
|
122
122
|
let erreurs: TListeErreursSaisie = {}
|
|
123
123
|
if (validerMaintenant) {
|
|
124
124
|
|
|
125
125
|
({
|
|
126
126
|
valeurs: nouvellesDonnees,
|
|
127
|
-
|
|
127
|
+
errorsCount,
|
|
128
128
|
erreurs
|
|
129
129
|
} = await valider(valeursInit));
|
|
130
130
|
|
|
131
|
-
if (debug &&
|
|
131
|
+
if (debug && errorsCount !== 0) console.log(`[form][saisie] erreurs`, erreurs);
|
|
132
132
|
|
|
133
133
|
newState.erreurs = erreurs;
|
|
134
|
-
newState.
|
|
134
|
+
newState.errorsCount = errorsCount;
|
|
135
135
|
|
|
136
136
|
} else
|
|
137
137
|
nouvellesDonnees = valeursInit;
|
|
@@ -152,8 +152,8 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
152
152
|
...newState
|
|
153
153
|
}));
|
|
154
154
|
|
|
155
|
-
if (props.onChange &&
|
|
156
|
-
props.onChange(donneesCompletes, validerMaintenant ? { valeurs: nouvellesDonnees,
|
|
155
|
+
if (props.onChange && errorsCount === 0)
|
|
156
|
+
props.onChange(donneesCompletes, validerMaintenant ? { valeurs: nouvellesDonnees, errorsCount, erreurs } : false);
|
|
157
157
|
|
|
158
158
|
/*if (valider && props.autosave === true) {
|
|
159
159
|
|
|
@@ -165,7 +165,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
165
165
|
|
|
166
166
|
}*/
|
|
167
167
|
|
|
168
|
-
return { erreurs,
|
|
168
|
+
return { erreurs, errorsCount };
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
async function valider(
|
|
@@ -180,7 +180,7 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
// Focus sur le premier champ ayant déclenché une erreur
|
|
183
|
-
if (retour.
|
|
183
|
+
if (retour.errorsCount !== 0) {
|
|
184
184
|
|
|
185
185
|
const cheminChamp = Object.keys(retour.erreurs)[0]
|
|
186
186
|
const champ = schema.get(cheminChamp);
|
|
@@ -215,15 +215,15 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
215
215
|
if (props.progression !== undefined && props.progression !== false)
|
|
216
216
|
return false;
|
|
217
217
|
|
|
218
|
-
if (state.
|
|
218
|
+
if (state.errorsCount !== 0)
|
|
219
219
|
return false;
|
|
220
220
|
|
|
221
221
|
console.log(`[form][saisie] Envoyer`, donnees);
|
|
222
222
|
|
|
223
223
|
// Validation de l'ensemble des champs
|
|
224
|
-
const { erreurs,
|
|
225
|
-
if (
|
|
226
|
-
setState((stateA) => ({ ...stateA, erreurs,
|
|
224
|
+
const { erreurs, errorsCount, valeurs } = await valider(donnees);
|
|
225
|
+
if (errorsCount !== 0) {
|
|
226
|
+
setState((stateA) => ({ ...stateA, erreurs, errorsCount }));
|
|
227
227
|
console.error('Erreurs formulaire', erreurs);
|
|
228
228
|
return false;
|
|
229
229
|
}
|
|
@@ -361,10 +361,10 @@ export const useForm = <TDonnees extends TObjetDonnees>(props: TPropsHook<TDonne
|
|
|
361
361
|
});
|
|
362
362
|
|
|
363
363
|
// Application des changements
|
|
364
|
-
const {
|
|
364
|
+
const { errorsCount } = await onChange(nouvellesDonnees, {}, validerMaintenant);
|
|
365
365
|
|
|
366
366
|
// Si aucune erreur, onChange propre au champ + gestion erreurs
|
|
367
|
-
if (propsChamp.onChange !== undefined &&
|
|
367
|
+
if (propsChamp.onChange !== undefined && errorsCount === 0)
|
|
368
368
|
await propsChamp.onChange(nouvelleValeur, true).catch((e) => {
|
|
369
369
|
if (validerMaintenant)
|
|
370
370
|
setState((stateA) => ({
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import type { ComponentChild, RefObject } from 'preact';
|
|
8
|
+
import type { StateUpdater } from 'preact/hooks';
|
|
9
|
+
|
|
10
|
+
// Core
|
|
11
|
+
import Button from '@client/components/button';
|
|
12
|
+
import Input from '@client/components/inputv3';
|
|
13
|
+
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- TYPES
|
|
16
|
+
----------------------------------*/
|
|
17
|
+
|
|
18
|
+
export type Choice = { label: ComponentChild, value: string }
|
|
19
|
+
|
|
20
|
+
export type Choices = Choice[]
|
|
21
|
+
|
|
22
|
+
type ChoicesFunc = (search: string) => Promise<Choices>
|
|
23
|
+
|
|
24
|
+
export type Props = (
|
|
25
|
+
{
|
|
26
|
+
multiple: true,
|
|
27
|
+
value?: Choice[],
|
|
28
|
+
onChange: StateUpdater<Choice[]>,
|
|
29
|
+
validator?: ArrayValidator
|
|
30
|
+
}
|
|
31
|
+
|
|
|
32
|
+
{
|
|
33
|
+
multiple?: false,
|
|
34
|
+
value?: Choice,
|
|
35
|
+
onChange: StateUpdater<Choice>,
|
|
36
|
+
validator?: StringValidator
|
|
37
|
+
}
|
|
38
|
+
) & {
|
|
39
|
+
choices: Choices | ChoicesFunc,
|
|
40
|
+
enableSearch?: boolean,
|
|
41
|
+
inline?: boolean,
|
|
42
|
+
required?: boolean,
|
|
43
|
+
noneSelection?: false | string,
|
|
44
|
+
currentList: Choice[],
|
|
45
|
+
refPopover?: RefObject<HTMLElement>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*----------------------------------
|
|
49
|
+
- COMPONENT
|
|
50
|
+
----------------------------------*/
|
|
51
|
+
/*
|
|
52
|
+
We crezte the ChoiceSelector separately from the Selector component because:
|
|
53
|
+
- we don't want the selector to be rendered before the dropdown content is dhown
|
|
54
|
+
- this component is called multiple time
|
|
55
|
+
*/
|
|
56
|
+
export default ({
|
|
57
|
+
choices: initChoices,
|
|
58
|
+
validator,
|
|
59
|
+
required,
|
|
60
|
+
noneSelection,
|
|
61
|
+
enableSearch,
|
|
62
|
+
value: current,
|
|
63
|
+
onChange,
|
|
64
|
+
inline,
|
|
65
|
+
multiple,
|
|
66
|
+
currentList,
|
|
67
|
+
refPopover
|
|
68
|
+
}: Props) => {
|
|
69
|
+
|
|
70
|
+
/*----------------------------------
|
|
71
|
+
- INIT
|
|
72
|
+
----------------------------------*/
|
|
73
|
+
|
|
74
|
+
const choicesViaFunc = typeof initChoices === 'function';
|
|
75
|
+
if (choicesViaFunc && enableSearch === undefined)
|
|
76
|
+
enableSearch = true;
|
|
77
|
+
|
|
78
|
+
const [search, setSearch] = React.useState<{
|
|
79
|
+
keywords: string,
|
|
80
|
+
loading: boolean
|
|
81
|
+
}>({
|
|
82
|
+
keywords: '',
|
|
83
|
+
loading: choicesViaFunc
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const [choices, setChoices] = React.useState<Choices>( choicesViaFunc ? [] : initChoices );
|
|
87
|
+
|
|
88
|
+
/*----------------------------------
|
|
89
|
+
- ACTIONS
|
|
90
|
+
----------------------------------*/
|
|
91
|
+
|
|
92
|
+
React.useEffect(() => {
|
|
93
|
+
if (choicesViaFunc) {
|
|
94
|
+
initChoices(search.keywords).then((searchResults) => {
|
|
95
|
+
setSearch(s => ({ ...s, loading: false }))
|
|
96
|
+
setChoices(searchResults);
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}, [initChoices, search.keywords]);
|
|
100
|
+
|
|
101
|
+
/*----------------------------------
|
|
102
|
+
- RENDER
|
|
103
|
+
----------------------------------*/
|
|
104
|
+
return (
|
|
105
|
+
<div class={(inline ? '' : 'card ') + "col al-top"} ref={refPopover}>
|
|
106
|
+
|
|
107
|
+
{enableSearch && (
|
|
108
|
+
<Input icon="search"
|
|
109
|
+
title="Search"
|
|
110
|
+
value={search.keywords}
|
|
111
|
+
onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
|
|
112
|
+
iconR={'spin'}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{currentList.length !== 0 && (
|
|
117
|
+
<ul class="col menu">
|
|
118
|
+
{currentList.map(choice => (
|
|
119
|
+
<Button size="s" onClick={() => {
|
|
120
|
+
onChange( current => multiple
|
|
121
|
+
? current.filter(c => c.value !== choice.value)
|
|
122
|
+
: undefined
|
|
123
|
+
);
|
|
124
|
+
}} suffix={<i src="check" class="fg primary" />}>
|
|
125
|
+
{choice.label}
|
|
126
|
+
</Button>
|
|
127
|
+
))}
|
|
128
|
+
</ul>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{choices === null ? (
|
|
132
|
+
<div class="row h-3 al-center">
|
|
133
|
+
<i src="spin" />
|
|
134
|
+
</div>
|
|
135
|
+
) : (
|
|
136
|
+
<ul class="col menu">
|
|
137
|
+
{choices.map( choice => {
|
|
138
|
+
const isCurrent = currentList.some(c => c.value === choice.value);
|
|
139
|
+
return !isCurrent && (
|
|
140
|
+
<li>
|
|
141
|
+
<Button size="s" onClick={() => {
|
|
142
|
+
onChange( current => {
|
|
143
|
+
return multiple
|
|
144
|
+
? [...(current || []), choice]
|
|
145
|
+
: choice
|
|
146
|
+
});
|
|
147
|
+
}}>
|
|
148
|
+
{/*search.keywords ? (
|
|
149
|
+
<span>
|
|
150
|
+
|
|
151
|
+
<strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
|
|
152
|
+
|
|
153
|
+
</span>
|
|
154
|
+
) : */choice.label}
|
|
155
|
+
</Button>
|
|
156
|
+
</li>
|
|
157
|
+
)
|
|
158
|
+
})}
|
|
159
|
+
|
|
160
|
+
{((!required || !validator?.options.min) && noneSelection) && (
|
|
161
|
+
<li>
|
|
162
|
+
<Button size="s" onClick={() => onChange(multiple ? [] : undefined)}
|
|
163
|
+
suffix={(current === undefined || (multiple && current.length === 0)) && <i src="check" class="fg primary" />}>
|
|
164
|
+
{noneSelection}
|
|
165
|
+
</Button>
|
|
166
|
+
</li>
|
|
167
|
+
)}
|
|
168
|
+
</ul>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|