@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.
@@ -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] = useState<boolean | "success" | "failed">(false);
134
- const [fresh, setFresh] = useState<boolean>(true);
135
- const [mapGroups, setMapGroups] = useState<Record<string, number[]>>({});
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
- if (defaultValue) {
205
- setDefaultValues(defaultValue);
206
-
207
- const derivedGroups = GroupsFromDefaults(defaultValue);
208
- setMapGroups(derivedGroups);
209
-
210
- resetFresh();
211
- } else {
212
- setDefaultValues(null);
213
- setMapGroups({});
214
- resetFresh();
215
- }
216
- }, [defaultValue]);
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
- if (form?.onHide?.(values)) return null;
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 group = mapGroups[groupKey] || [0];
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 = () => setMapGroups((prev) => ({ ...prev, [groupKey]: [...group, group.length] }));
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, `${mapName}[${gIndex}]`))}
447
+ {innerForms.map((inner, i) => renderInput(inner, i, `${groupKey}[${gIndex}]`))}
288
448
  </div>
289
449
 
290
- <ButtonComponent
291
- icon={"solid/times"}
292
- paint="danger"
293
- variant="outline"
294
- size="xs"
295
- className={cn("absolute top-10 right-2 translate-x-[50%] -translate-y-[50%]", wrap && "translate-x-0 -translate-y-0 top-1 right-1")}
296
- onClick={() => removeGroup(gIndex)}
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
- // autoFocus={key === 0}
491
+ disabled={ws?.disabled}
492
+ readOnly={ws?.readonly}
493
+ name={name}
330
494
  />
331
495
  </div>
332
496
  );