@skalfa/skalfa-component 1.0.23 → 1.0.25
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/dist/input/Input.component.js +1 -1
- package/dist/input/InputCheckbox.component.js +1 -1
- package/dist/input/InputCurrency.component.js +1 -1
- package/dist/input/InputDate.component.js +1 -1
- package/dist/input/InputDatetime.component.js +1 -1
- package/dist/input/InputDocument.component.js +1 -1
- package/dist/input/InputImage.component.js +1 -1
- package/dist/input/InputNumber.component.js +1 -1
- package/dist/input/InputPassword.component.js +1 -1
- package/dist/input/InputRadio.component.js +1 -1
- package/dist/input/InputTime.component.js +1 -1
- package/dist/input/Select.component.d.ts +5 -3
- package/dist/input/Select.component.js +155 -54
- package/dist/supervision/FormSupervision.component.d.ts +16 -1
- package/dist/supervision/FormSupervision.component.js +129 -10
- package/package.json +1 -1
- package/src/input/Input.component.tsx +1 -1
- package/src/input/InputCheckbox.component.tsx +1 -1
- package/src/input/InputCurrency.component.tsx +1 -1
- package/src/input/InputDate.component.tsx +1 -1
- package/src/input/InputDatetime.component.tsx +1 -1
- package/src/input/InputDocument.component.tsx +1 -1
- package/src/input/InputImage.component.tsx +1 -1
- package/src/input/InputNumber.component.tsx +1 -1
- package/src/input/InputPassword.component.tsx +1 -1
- package/src/input/InputRadio.component.tsx +1 -1
- package/src/input/InputTime.component.tsx +1 -1
- package/src/input/Select.component.tsx +195 -35
- package/src/supervision/FormSupervision.component.tsx +196 -32
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { ReactNode, useEffect, useState } from "react";
|
|
3
|
+
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { ApiType, cn, pcn, FormErrorType, FormRegisterType, FormValueType, useForm, ValidationRules, DBSchema } from "@utils";
|
|
5
5
|
import {
|
|
6
6
|
InputCheckboxComponent,
|
|
@@ -65,6 +65,7 @@ type ClusterConstruction = {
|
|
|
65
65
|
tip : string;
|
|
66
66
|
fields : FormType[];
|
|
67
67
|
wrap : boolean;
|
|
68
|
+
min ?: number;
|
|
68
69
|
|
|
69
70
|
/** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
|
|
70
71
|
className : string;
|
|
@@ -89,12 +90,28 @@ type ConstructionMap = {
|
|
|
89
90
|
|
|
90
91
|
type TypeKeys = keyof ConstructionMap;
|
|
91
92
|
|
|
93
|
+
export type WatchContext = {
|
|
94
|
+
values : Record<string, any>
|
|
95
|
+
self : string
|
|
96
|
+
prev : WatchAction
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type WatchAction = {
|
|
100
|
+
disabled ?: boolean
|
|
101
|
+
hidden ?: boolean
|
|
102
|
+
value ?: any
|
|
103
|
+
required ?: boolean
|
|
104
|
+
readonly ?: boolean
|
|
105
|
+
reset ?: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
export interface FormType<T extends TypeKeys = keyof ConstructionMap> {
|
|
93
109
|
col ?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | string;
|
|
94
110
|
className ?: string;
|
|
95
111
|
construction ?: ConstructionMap[T];
|
|
96
112
|
type ?: T;
|
|
97
|
-
onHide ?: (values: any) => boolean;
|
|
113
|
+
onHide ?: (values: Record<string, any>) => boolean;
|
|
114
|
+
onWatch ?: (ctx: WatchContext) => WatchAction | undefined;
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
export interface formSupervisionProps {
|
|
@@ -130,14 +147,17 @@ export function FormSupervisionComponent({
|
|
|
130
147
|
}: formSupervisionProps) {
|
|
131
148
|
const l = useLang();
|
|
132
149
|
|
|
133
|
-
const [modal, setModal]
|
|
134
|
-
const [fresh, setFresh]
|
|
135
|
-
const [mapGroups, setMapGroups]
|
|
150
|
+
const [modal, setModal] = useState<boolean | "success" | "failed">(false);
|
|
151
|
+
const [fresh, setFresh] = useState<boolean>(true);
|
|
152
|
+
const [mapGroups, setMapGroups] = useState<Record<string, number[]>>({});
|
|
153
|
+
const [watchState, setWatchState] = useState<Record<string, WatchAction>>({});
|
|
154
|
+
const watchRef = useRef<Record<string, WatchAction>>({});
|
|
136
155
|
|
|
137
156
|
const [
|
|
138
157
|
{
|
|
139
158
|
formControl,
|
|
140
159
|
setRegister,
|
|
160
|
+
setUnregister,
|
|
141
161
|
values,
|
|
142
162
|
setValues,
|
|
143
163
|
errors,
|
|
@@ -164,6 +184,95 @@ export function FormSupervisionComponent({
|
|
|
164
184
|
}
|
|
165
185
|
);
|
|
166
186
|
|
|
187
|
+
// ==============================>
|
|
188
|
+
// ## Watch: collect watchers from fields
|
|
189
|
+
// ==============================>
|
|
190
|
+
const collectWatchers = (fieldList: FormType[], prefix?: string): { name: string, onWatch: NonNullable<FormType['onWatch']>, construction: any }[] => {
|
|
191
|
+
const result: { name: string, onWatch: NonNullable<FormType['onWatch']>, construction: any }[] = [];
|
|
192
|
+
|
|
193
|
+
for (const f of fieldList) {
|
|
194
|
+
const inputType = f.type || "default";
|
|
195
|
+
const name = prefix ? `${prefix}.${f.construction?.name}` : f.construction?.name || "";
|
|
196
|
+
|
|
197
|
+
if (inputType === "cluster") {
|
|
198
|
+
const cluster = f.construction as ClusterConstruction;
|
|
199
|
+
const groupKey = prefix ? `${prefix}.${cluster.name}` : cluster.name;
|
|
200
|
+
const group = mapGroups[groupKey] || [];
|
|
201
|
+
|
|
202
|
+
for (const gIndex of group) {
|
|
203
|
+
result.push(...collectWatchers(cluster.fields, `${cluster.name}[${gIndex}]`));
|
|
204
|
+
}
|
|
205
|
+
} else if (f.onWatch) {
|
|
206
|
+
result.push({ name, onWatch: f.onWatch, construction: f.construction });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return result;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ==============================>
|
|
214
|
+
// ## Watch: execute watchers on value change
|
|
215
|
+
// ==============================>
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
const watchers = collectWatchers(fields);
|
|
218
|
+
if (watchers.length === 0) {
|
|
219
|
+
if (Object.keys(watchRef.current).length > 0) {
|
|
220
|
+
watchRef.current = {};
|
|
221
|
+
setWatchState({});
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const valMap: Record<string, any> = {};
|
|
227
|
+
values.forEach((v) => { valMap[v.name] = v.value; });
|
|
228
|
+
|
|
229
|
+
const nextState : Record<string, WatchAction> = {};
|
|
230
|
+
const valueUpdates : FormValueType[] = [];
|
|
231
|
+
|
|
232
|
+
for (const { name, onWatch, construction } of watchers) {
|
|
233
|
+
const prev = watchRef.current[name] || {};
|
|
234
|
+
const action = onWatch({ values: valMap, self: name, prev });
|
|
235
|
+
|
|
236
|
+
if (!action) continue;
|
|
237
|
+
|
|
238
|
+
nextState[name] = action;
|
|
239
|
+
|
|
240
|
+
if (action.hidden && !prev.hidden) setUnregister(name);
|
|
241
|
+
|
|
242
|
+
if (action.required !== prev.required) {
|
|
243
|
+
const baseValidations = Array.isArray(construction?.validations) ? [...construction.validations] : [];
|
|
244
|
+
const newValidations = action.required ? (baseValidations.includes("required") ? baseValidations : [...baseValidations, "required"]) : baseValidations.filter((v: string) => v !== "required");
|
|
245
|
+
|
|
246
|
+
setRegister({ name, validations: newValidations });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (action.reset) {
|
|
250
|
+
const cur = valMap[name];
|
|
251
|
+
|
|
252
|
+
if (cur != null && cur !== "") valueUpdates.push({ name, value: "" });
|
|
253
|
+
} else if (action.value !== undefined && action.value !== valMap[name]) {
|
|
254
|
+
valueUpdates.push({ name, value: action.value });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (JSON.stringify(watchRef.current) !== JSON.stringify(nextState)) {
|
|
259
|
+
watchRef.current = nextState;
|
|
260
|
+
setWatchState(nextState);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (valueUpdates.length > 0) {
|
|
264
|
+
const merged = [...values];
|
|
265
|
+
|
|
266
|
+
for (const upd of valueUpdates) {
|
|
267
|
+
const idx = merged.findIndex(v => v.name === upd.name);
|
|
268
|
+
if (idx >= 0) merged[idx] = upd;
|
|
269
|
+
else merged.push(upd);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
setValues(merged);
|
|
273
|
+
}
|
|
274
|
+
}, [values, fields, mapGroups]);
|
|
275
|
+
|
|
167
276
|
const GroupsFromDefaults = (defaults: Record<string, any>): Record<string, number[]> => {
|
|
168
277
|
const groups: Record<string, Set<number>> = {};
|
|
169
278
|
|
|
@@ -201,19 +310,46 @@ export function FormSupervisionComponent({
|
|
|
201
310
|
}, [fields]);
|
|
202
311
|
|
|
203
312
|
useEffect(() => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
313
|
+
const minGroups: Record<string, number[]> = {};
|
|
314
|
+
const initialValues: FormValueType[] = [];
|
|
315
|
+
|
|
316
|
+
const processMinClusters = (formList: FormType[], prefix?: string) => {
|
|
317
|
+
formList.forEach((form) => {
|
|
318
|
+
if (form.type === "cluster" && form.construction) {
|
|
319
|
+
const { name: mapName, fields: innerForms, min } = form.construction as ClusterConstruction;
|
|
320
|
+
const groupKey = prefix ? `${prefix}.${mapName}` : mapName;
|
|
321
|
+
const minCount = Math.max(0, min || 0);
|
|
322
|
+
|
|
323
|
+
if (minCount > 0) {
|
|
324
|
+
minGroups[groupKey] = Array.from({ length: minCount }, (_, i) => i);
|
|
325
|
+
|
|
326
|
+
for (let gIndex = 0; gIndex < minCount; gIndex++) {
|
|
327
|
+
innerForms.forEach((inner) => {
|
|
328
|
+
const fieldName = `${groupKey}[${gIndex}].${inner.construction?.name}`;
|
|
329
|
+
initialValues.push({ name: fieldName, value: "" });
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
processMinClusters(fields);
|
|
338
|
+
|
|
339
|
+
if (defaultValue) {
|
|
340
|
+
setDefaultValues(defaultValue);
|
|
341
|
+
const derivedGroups = GroupsFromDefaults(defaultValue);
|
|
342
|
+
setMapGroups({ ...minGroups, ...derivedGroups });
|
|
343
|
+
resetFresh();
|
|
344
|
+
} else {
|
|
345
|
+
setDefaultValues(null);
|
|
346
|
+
setMapGroups(minGroups);
|
|
347
|
+
if (initialValues.length > 0) {
|
|
348
|
+
setValues(initialValues);
|
|
349
|
+
}
|
|
350
|
+
resetFresh();
|
|
351
|
+
}
|
|
352
|
+
}, [defaultValue, fields]);
|
|
217
353
|
|
|
218
354
|
const generateColClass = (col: string | number) => String(col).split(" ").map((c) => (c.includes(":") ? `${c.replace(":", ":col-span-")}` : `col-span-${c}`)).join(" ");
|
|
219
355
|
|
|
@@ -238,15 +374,35 @@ export function FormSupervisionComponent({
|
|
|
238
374
|
const inputType = form.type || "default";
|
|
239
375
|
const name = prefix ? `${prefix}.${form.construction?.name}` : form.construction?.name || "input_name";
|
|
240
376
|
|
|
241
|
-
|
|
377
|
+
const valMap: Record<string, any> = {};
|
|
378
|
+
values.forEach((v) => { valMap[v.name] = v.value; });
|
|
379
|
+
|
|
380
|
+
if (form?.onHide?.(valMap)) return null;
|
|
381
|
+
|
|
382
|
+
const ws = watchState[name];
|
|
383
|
+
if (ws?.hidden) return null;
|
|
242
384
|
|
|
243
385
|
if (inputType === "cluster") {
|
|
244
|
-
const { name: mapName, fields: innerForms, label, tip, wrap, className } = form.construction as ClusterConstruction;
|
|
386
|
+
const { name: mapName, fields: innerForms, label, tip, wrap, className, min } = form.construction as ClusterConstruction;
|
|
387
|
+
const minCount = Math.max(0, min || 0);
|
|
245
388
|
|
|
246
389
|
const groupKey = prefix ? `${prefix}.${mapName}` : mapName;
|
|
247
|
-
const
|
|
390
|
+
const defaultGroup = minCount > 0 ? Array.from({ length: minCount }, (_, i) => i) : [];
|
|
391
|
+
const group = mapGroups[groupKey] ?? defaultGroup;
|
|
392
|
+
|
|
393
|
+
const showDeleteButton = group.length > minCount;
|
|
248
394
|
|
|
249
|
-
const addGroup = () =>
|
|
395
|
+
const addGroup = () => {
|
|
396
|
+
const nextIndex = group.length;
|
|
397
|
+
setMapGroups((prev) => ({ ...prev, [groupKey]: [...group, nextIndex] }));
|
|
398
|
+
|
|
399
|
+
const newGroupValues = innerForms.map((inner) => ({
|
|
400
|
+
name: `${groupKey}[${nextIndex}].${inner.construction?.name}`,
|
|
401
|
+
value: "",
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
setValues([...values, ...newGroupValues]);
|
|
405
|
+
};
|
|
250
406
|
|
|
251
407
|
const removeGroup = (index: number) => {
|
|
252
408
|
const filteredGroup = group.filter((_, i) => i !== index);
|
|
@@ -275,8 +431,12 @@ export function FormSupervisionComponent({
|
|
|
275
431
|
setValues(updatedValues);
|
|
276
432
|
};
|
|
277
433
|
|
|
434
|
+
const clusterError = errors?.find((err: FormErrorType) => err.name === groupKey || err.name === mapName)?.error;
|
|
435
|
+
|
|
278
436
|
return (
|
|
279
437
|
<div key={key} className={cn("flex flex-col gap-4", generateColClass(form.col || "12"))}>
|
|
438
|
+
{label && <p className="input-label">{label}</p>}
|
|
439
|
+
{clusterError && <small className="input-error-message">{clusterError}</small>}
|
|
280
440
|
{group.map((gIndex) => (
|
|
281
441
|
<div key={gIndex} className={cn("relative pr-8", wrap && "p-4 rounded border", className)}>
|
|
282
442
|
{label && <p className="input-label">{label} {gIndex + 1}</p>}
|
|
@@ -284,17 +444,19 @@ export function FormSupervisionComponent({
|
|
|
284
444
|
{(label || tip) && <div className="mb-2"></div>}
|
|
285
445
|
|
|
286
446
|
<div className="w-full grid grid-cols-12 gap-4">
|
|
287
|
-
{innerForms.map((inner, i) => renderInput(inner, i, `${
|
|
447
|
+
{innerForms.map((inner, i) => renderInput(inner, i, `${groupKey}[${gIndex}]`))}
|
|
288
448
|
</div>
|
|
289
449
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
450
|
+
{showDeleteButton && (
|
|
451
|
+
<ButtonComponent
|
|
452
|
+
icon={"solid/times"}
|
|
453
|
+
paint="danger"
|
|
454
|
+
variant="outline"
|
|
455
|
+
size="xs"
|
|
456
|
+
className={cn("absolute top-10 right-2 translate-x-[50%] -translate-y-[50%]", wrap && "translate-x-0 -translate-y-0 top-1 right-1")}
|
|
457
|
+
onClick={() => removeGroup(gIndex)}
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
298
460
|
</div>
|
|
299
461
|
))}
|
|
300
462
|
|
|
@@ -326,7 +488,9 @@ export function FormSupervisionComponent({
|
|
|
326
488
|
<Component
|
|
327
489
|
{...(form.construction as any)}
|
|
328
490
|
{...formControl(name)}
|
|
329
|
-
|
|
491
|
+
disabled={ws?.disabled}
|
|
492
|
+
readOnly={ws?.readonly}
|
|
493
|
+
name={name}
|
|
330
494
|
/>
|
|
331
495
|
</div>
|
|
332
496
|
);
|