@omnitend/dashboard-for-laravel 0.33.1 → 0.35.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/dist/charts.js +82 -90
- package/dist/charts.js.map +1 -1
- package/dist/charts.umd.cjs +1 -1
- package/dist/charts.umd.cjs.map +1 -1
- package/dist/components/base/DButton.vue.d.ts +1 -1
- package/dist/components/charts/DXBarChart.vue.d.ts +3 -18
- package/dist/components/charts/DXLineChart.vue.d.ts +3 -18
- package/dist/components/charts/useThemedChart.d.ts +35 -0
- package/dist/components/extended/DXChoiceField.vue.d.ts +26 -0
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +1 -1
- package/dist/components/extended/DXField.vue.d.ts +10 -113
- package/dist/components/extended/DXFieldShell.vue.d.ts +73 -0
- package/dist/components/extended/DXForm.vue.d.ts +28 -6
- package/dist/components/extended/DXNumericField.vue.d.ts +23 -0
- package/dist/components/extended/DXRepeater.vue.d.ts +4 -4
- package/dist/components/extended/DXRepeaterCards.vue.d.ts +45 -0
- package/dist/components/extended/DXTable.vue.d.ts +107 -2
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +22 -2
- package/dist/components/extended/DXUserAvatar.vue.d.ts +1 -1
- package/dist/composables/useAsyncOptions.d.ts +20 -0
- package/dist/composables/useContainerWidth.d.ts +96 -0
- package/dist/composables/useResourceEditor.d.ts +2 -1
- package/dist/dashboard-for-laravel.js +10513 -10443
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/style.css +1 -1
- package/dist/types/index.d.ts +146 -0
- package/dist/utils/formSchema.d.ts +59 -0
- package/docs/public/api-reference.json +494 -33
- package/docs/public/docs-map.md +1 -1
- package/docs/public/llms.txt +5 -2
- package/package.json +1 -1
- package/resources/css/theme.scss +80 -2
- package/resources/js/components/charts/DXBarChart.vue +4 -43
- package/resources/js/components/charts/DXLineChart.vue +4 -43
- package/resources/js/components/charts/useThemedChart.ts +69 -0
- package/resources/js/components/extended/DXChoiceField.vue +114 -0
- package/resources/js/components/extended/DXDashboardSidebar.vue +1 -1
- package/resources/js/components/extended/DXField.vue +328 -615
- package/resources/js/components/extended/DXFieldShell.vue +124 -0
- package/resources/js/components/extended/DXForm.vue +92 -14
- package/resources/js/components/extended/DXNumericField.vue +144 -0
- package/resources/js/components/extended/DXRepeater.vue +53 -175
- package/resources/js/components/extended/DXRepeaterCards.vue +133 -0
- package/resources/js/components/extended/DXTable.vue +418 -43
- package/resources/js/components/extended/DXTableEditorModal.vue +39 -2
- package/resources/js/components/extended/DXTablePagination.vue +19 -3
- package/resources/js/components/extended/DXTableShell.vue +70 -1
- package/resources/js/composables/defineForm.ts +8 -44
- package/resources/js/composables/useAsyncOptions.ts +61 -0
- package/resources/js/composables/useContainerWidth.ts +303 -0
- package/resources/js/composables/useResourceEditor.ts +24 -38
- package/resources/js/index.ts +40 -0
- package/resources/js/types/index.ts +231 -0
- package/resources/js/utils/formSchema.ts +126 -0
|
@@ -2,6 +2,12 @@ import { computed, ref, type Ref } from "vue";
|
|
|
2
2
|
import { api } from "../utils/api";
|
|
3
3
|
import { useForm } from "./useForm";
|
|
4
4
|
import { useToast } from "./useToast";
|
|
5
|
+
import type { FieldDefinition } from "../types";
|
|
6
|
+
import {
|
|
7
|
+
resolveFieldDefault,
|
|
8
|
+
isFieldVisible as isFieldVisibleFor,
|
|
9
|
+
isSubmittableField,
|
|
10
|
+
} from "../utils/formSchema";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* The subset of `DXTable`'s props the resource editor reads. Declared
|
|
@@ -11,7 +17,7 @@ import { useToast } from "./useToast";
|
|
|
11
17
|
*/
|
|
12
18
|
export interface ResourceEditorProps<T = any> {
|
|
13
19
|
/** Field definitions for the edit/create modal (enables edit-on-row-click). */
|
|
14
|
-
editFields?:
|
|
20
|
+
editFields?: FieldDefinition[];
|
|
15
21
|
/** Tab definitions — only their `key` is read here (DXForm consumes the rest). */
|
|
16
22
|
editTabs?: Array<{ key: string }>;
|
|
17
23
|
/** Modal title, or a function of the edited row. */
|
|
@@ -108,58 +114,38 @@ export function useResourceEditor<T = any>(
|
|
|
108
114
|
// and auto-switching to the first tab with a validation error.
|
|
109
115
|
|
|
110
116
|
/*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* too, so the create and edit paths agree about the same field.
|
|
118
|
-
*/
|
|
119
|
-
const defaultValueFor = (field: any): any =>
|
|
120
|
-
Object.prototype.hasOwnProperty.call(field, 'default') ? field.default : '';
|
|
121
|
-
|
|
122
|
-
/*
|
|
123
|
-
* Seeding an EDIT form from a row (#117).
|
|
124
|
-
*
|
|
125
|
-
* `row[key] ?? field.default` is wrong: it can't tell "the row has no such key"
|
|
126
|
-
* from "the row's value IS null", so an explicitly-null column gets overwritten
|
|
127
|
-
* by the field's default — a value the user never saw, on a field they may not
|
|
128
|
-
* even be able to see. Presence, not nullishness, decides.
|
|
117
|
+
* Seeding an EDIT form from a row (#117): use the row's own value when it
|
|
118
|
+
* HAS the key (presence, not nullishness — an explicitly-null column must
|
|
119
|
+
* not be overwritten by the field's default), else the field's own default.
|
|
120
|
+
* The default comes from the shared `resolveFieldDefault` rule, so create
|
|
121
|
+
* and edit seeding agree with defineForm/DXRepeater (#134) — this path is
|
|
122
|
+
* now type-aware (an array field falls back to `[]`, not `''`).
|
|
129
123
|
*/
|
|
130
124
|
const seedValueFor = (field: any, row: T): any => {
|
|
131
125
|
if (Object.prototype.hasOwnProperty.call(row as any, field.key)) {
|
|
132
126
|
return (row as any)[field.key];
|
|
133
127
|
}
|
|
134
|
-
return
|
|
128
|
+
return resolveFieldDefault(field);
|
|
135
129
|
};
|
|
136
130
|
|
|
137
131
|
/*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* Kept in step with DXForm.isFieldVisible.
|
|
132
|
+
* The model field/tab predicates evaluate against: the edited row widened
|
|
133
|
+
* with the live form data (context + data), same as DXForm's `model`.
|
|
141
134
|
*/
|
|
142
135
|
const editPredicateModel = computed(() => ({
|
|
143
136
|
...((selectedItem.value as any) ?? {}),
|
|
144
137
|
...(editForm.value?.data ?? {}),
|
|
145
138
|
}));
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
? true
|
|
152
|
-
: typeof when === 'function'
|
|
153
|
-
? when(editPredicateModel.value)
|
|
154
|
-
: when;
|
|
155
|
-
const showOk = field.show ? field.show() : true;
|
|
156
|
-
return whenOk && showOk;
|
|
157
|
-
};
|
|
140
|
+
// Visibility by the one shared rule (`when` + legacy `show`), against the
|
|
141
|
+
// edit model — see formSchema.isFieldVisible (#134).
|
|
142
|
+
const isEditFieldVisible = (field: any): boolean =>
|
|
143
|
+
isFieldVisibleFor(field, editPredicateModel.value);
|
|
158
144
|
|
|
159
145
|
// Fields whose value actually belongs in the payload. A presentational field
|
|
160
146
|
// (submit: false) renders but holds no data — see FieldDefinition.submit.
|
|
161
147
|
const submittableEditFields = computed(() =>
|
|
162
|
-
(props.editFields ?? []).filter(
|
|
148
|
+
(props.editFields ?? []).filter(isSubmittableField),
|
|
163
149
|
);
|
|
164
150
|
|
|
165
151
|
// Enforced at SUBMIT, not just at seeding: the modal still renders every field,
|
|
@@ -172,7 +158,7 @@ export function useResourceEditor<T = any>(
|
|
|
172
158
|
.filter(
|
|
173
159
|
(field) =>
|
|
174
160
|
// Presentational: lays the form out, holds no data (#110).
|
|
175
|
-
field
|
|
161
|
+
!isSubmittableField(field) ||
|
|
176
162
|
// Hidden by `when` at submit time (#117). Submitting a field
|
|
177
163
|
// the user cannot see is a silent write — and with `default`
|
|
178
164
|
// set it writes a value they never chose. Omitting the key
|
|
@@ -299,13 +285,13 @@ export function useResourceEditor<T = any>(
|
|
|
299
285
|
if (!editForm.value) {
|
|
300
286
|
const formData: Record<string, any> = {};
|
|
301
287
|
submittableEditFields.value.forEach(field => {
|
|
302
|
-
formData[field.key] =
|
|
288
|
+
formData[field.key] = resolveFieldDefault(field);
|
|
303
289
|
});
|
|
304
290
|
editForm.value = useForm(formData);
|
|
305
291
|
} else {
|
|
306
292
|
// Reset existing form to defaults
|
|
307
293
|
submittableEditFields.value.forEach(field => {
|
|
308
|
-
editForm.value.data[field.key] =
|
|
294
|
+
editForm.value.data[field.key] = resolveFieldDefault(field);
|
|
309
295
|
});
|
|
310
296
|
editForm.value.clearErrors();
|
|
311
297
|
}
|
package/resources/js/index.ts
CHANGED
|
@@ -110,9 +110,20 @@ export { default as DTooltip } from "./components/base/DTooltip.vue";
|
|
|
110
110
|
export { useForm } from "./composables/useForm";
|
|
111
111
|
export { defineForm } from "./composables/defineForm";
|
|
112
112
|
export { useToast } from "./composables/useToast";
|
|
113
|
+
export { useContainerWidth } from "./composables/useContainerWidth";
|
|
113
114
|
|
|
114
115
|
// Utils
|
|
115
116
|
export { api } from "./utils/api";
|
|
117
|
+
// Pure form-schema helpers (#134) — the single source of defaulting, cloning,
|
|
118
|
+
// predicate resolution, visibility and submission-eligibility.
|
|
119
|
+
export {
|
|
120
|
+
defaultValueForType,
|
|
121
|
+
cloneDefault,
|
|
122
|
+
resolveFieldDefault,
|
|
123
|
+
resolvePredicate,
|
|
124
|
+
isFieldVisible,
|
|
125
|
+
isSubmittableField,
|
|
126
|
+
} from "./utils/formSchema";
|
|
116
127
|
|
|
117
128
|
// Types
|
|
118
129
|
export type {
|
|
@@ -122,6 +133,27 @@ export type {
|
|
|
122
133
|
FormTab,
|
|
123
134
|
MaybeFn,
|
|
124
135
|
OptionsLoader,
|
|
136
|
+
// Strict, discriminated field types (#131) — opt-in; canonical in a future
|
|
137
|
+
// major. `FieldDef` is the union; the rest are its per-type members.
|
|
138
|
+
FieldDef,
|
|
139
|
+
BaseFieldDef,
|
|
140
|
+
TextFieldDef,
|
|
141
|
+
PasswordFieldDef,
|
|
142
|
+
NumberFieldDef,
|
|
143
|
+
DateFieldDef,
|
|
144
|
+
CurrencyFieldDef,
|
|
145
|
+
PercentageFieldDef,
|
|
146
|
+
TextareaFieldDef,
|
|
147
|
+
SelectFieldDef,
|
|
148
|
+
AutocompleteFieldDef,
|
|
149
|
+
CheckboxFieldDef,
|
|
150
|
+
CheckboxGroupFieldDef,
|
|
151
|
+
SwitchFieldDef,
|
|
152
|
+
SwitchListFieldDef,
|
|
153
|
+
RadioFieldDef,
|
|
154
|
+
FileFieldDef,
|
|
155
|
+
ComponentFieldDef,
|
|
156
|
+
RepeaterFieldDef,
|
|
125
157
|
} from "./types";
|
|
126
158
|
|
|
127
159
|
export type {
|
|
@@ -137,6 +169,12 @@ export type {
|
|
|
137
169
|
DefineFormReturn,
|
|
138
170
|
} from "./composables/defineForm";
|
|
139
171
|
|
|
172
|
+
export type {
|
|
173
|
+
ContainerWidthTarget,
|
|
174
|
+
UseContainerWidthOptions,
|
|
175
|
+
UseContainerWidthReturn,
|
|
176
|
+
} from "./composables/useContainerWidth";
|
|
177
|
+
|
|
140
178
|
export type {
|
|
141
179
|
ApiError,
|
|
142
180
|
ApiResponse,
|
|
@@ -149,6 +187,8 @@ export type {
|
|
|
149
187
|
BTableProvider,
|
|
150
188
|
EditTab,
|
|
151
189
|
BTableProviderContext,
|
|
190
|
+
DXTableApiAdapter,
|
|
191
|
+
DXTableSource,
|
|
152
192
|
} from "./components/extended/DXTable.vue";
|
|
153
193
|
|
|
154
194
|
export type {
|
|
@@ -380,6 +380,237 @@ export interface FieldDefinition {
|
|
|
380
380
|
show?: () => boolean;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
384
|
+
// Strict, discriminated field types (#131)
|
|
385
|
+
//
|
|
386
|
+
// The permissive `FieldDefinition` above lets every option appear on every
|
|
387
|
+
// field type: `currencySymbol` compiles on a checkbox, `options` on a text
|
|
388
|
+
// input, `component` without `type: "component"`. The types below are the
|
|
389
|
+
// strict, `type`-keyed replacement — each variant permits only its own
|
|
390
|
+
// options, so the compiler catches an invalid field config at the call site.
|
|
391
|
+
//
|
|
392
|
+
// ADDITIVE for now: `FieldDefinition` stays canonical and these are opt-in
|
|
393
|
+
// (`import type { FieldDef } from "@omnitend/dashboard-for-laravel"`). A future
|
|
394
|
+
// major (#131 Phase 3) makes `FieldDef` canonical and removes the permissive
|
|
395
|
+
// interface with a codemod. **Until then, keep the two in step** — a prop added
|
|
396
|
+
// to `FieldDefinition` must be added to the matching variant(s) below. The doc
|
|
397
|
+
// comments live on `FieldDefinition`; variants stay terse to keep them aligned.
|
|
398
|
+
//
|
|
399
|
+
// Known limitation: the union rejects invalid FRESH object literals (excess
|
|
400
|
+
// property checking — the common `fields: [{ type: "checkbox", … }]` call site),
|
|
401
|
+
// but a PRE-DECLARED object with excess props (`const f = { type: "checkbox",
|
|
402
|
+
// currencySymbol: "£" }`) stays structurally assignable. Full exclusivity would
|
|
403
|
+
// need `never`-typed exclusion keys on every variant (or a StrictUnion helper) —
|
|
404
|
+
// heavy machinery with confusing errors for marginal gain on an opt-in type, so
|
|
405
|
+
// it's deliberately not done here.
|
|
406
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/** Options common to every field variant (the non-`type`-specific props). */
|
|
409
|
+
export interface BaseFieldDef {
|
|
410
|
+
key: string;
|
|
411
|
+
label?: MaybeFn<string>;
|
|
412
|
+
required?: boolean;
|
|
413
|
+
help?: string;
|
|
414
|
+
hint?: MaybeFn<string>;
|
|
415
|
+
info?: MaybeFn<string>;
|
|
416
|
+
class?: string;
|
|
417
|
+
inputProps?: Record<string, any>;
|
|
418
|
+
span?: boolean;
|
|
419
|
+
layout?: "vertical" | "horizontal";
|
|
420
|
+
labelCols?: LabelCols;
|
|
421
|
+
hideLabel?: boolean;
|
|
422
|
+
default?: any;
|
|
423
|
+
disabled?: MaybeFn<boolean>;
|
|
424
|
+
disabledWhen?: (model: any) => boolean;
|
|
425
|
+
readonly?: MaybeFn<boolean>;
|
|
426
|
+
plaintext?: MaybeFn<boolean>;
|
|
427
|
+
submit?: boolean;
|
|
428
|
+
when?: MaybeFn<boolean>;
|
|
429
|
+
show?: () => boolean;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Mixins shared by several variants (not exported — internal composition). */
|
|
433
|
+
interface WithPlaceholder {
|
|
434
|
+
placeholder?: string;
|
|
435
|
+
}
|
|
436
|
+
interface WithNumericBounds {
|
|
437
|
+
step?: number | string;
|
|
438
|
+
min?: number | string;
|
|
439
|
+
max?: number | string;
|
|
440
|
+
}
|
|
441
|
+
interface WithSelectableOptions {
|
|
442
|
+
options?: FieldOption[];
|
|
443
|
+
optionsLoader?: OptionsLoader;
|
|
444
|
+
reloadOptionsOnChange?: boolean;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** `text` / `email` / `url` / `tel` — a plain text-like `<input>`. */
|
|
448
|
+
export interface TextFieldDef extends BaseFieldDef, WithPlaceholder {
|
|
449
|
+
type: "text" | "email" | "url" | "tel";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** `password` — text input with an optional reveal (eye) toggle. */
|
|
453
|
+
export interface PasswordFieldDef extends BaseFieldDef, WithPlaceholder {
|
|
454
|
+
type: "password";
|
|
455
|
+
revealable?: boolean;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** `number` — numeric input with optional bounds. */
|
|
459
|
+
export interface NumberFieldDef
|
|
460
|
+
extends BaseFieldDef,
|
|
461
|
+
WithPlaceholder,
|
|
462
|
+
WithNumericBounds {
|
|
463
|
+
type: "number";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** `date` / `datetime` / `datetime-local` / `time` — native date/time controls.
|
|
467
|
+
* `min`/`max`/`step` are valid on these inputs (e.g. `min="2024-01-01"`) and
|
|
468
|
+
* DXField forwards them, so the variant permits them. */
|
|
469
|
+
export interface DateFieldDef extends BaseFieldDef, WithNumericBounds {
|
|
470
|
+
type: "date" | "datetime" | "datetime-local" | "time";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** `currency` — numeric input with a symbol affix and minor-unit handling. */
|
|
474
|
+
export interface CurrencyFieldDef
|
|
475
|
+
extends BaseFieldDef,
|
|
476
|
+
WithPlaceholder,
|
|
477
|
+
WithNumericBounds {
|
|
478
|
+
type: "currency";
|
|
479
|
+
currencySymbol?: string;
|
|
480
|
+
decimals?: number;
|
|
481
|
+
minorUnits?: boolean;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** `percentage` — numeric input shown as a 0–100 percentage. */
|
|
485
|
+
export interface PercentageFieldDef
|
|
486
|
+
extends BaseFieldDef,
|
|
487
|
+
WithPlaceholder,
|
|
488
|
+
WithNumericBounds {
|
|
489
|
+
type: "percentage";
|
|
490
|
+
asFraction?: boolean;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** `textarea` — multi-line text input. */
|
|
494
|
+
export interface TextareaFieldDef extends BaseFieldDef, WithPlaceholder {
|
|
495
|
+
type: "textarea";
|
|
496
|
+
rows?: number;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** `select` — dropdown; `searchable` upgrades it to a filterable select. */
|
|
500
|
+
export interface SelectFieldDef
|
|
501
|
+
extends BaseFieldDef,
|
|
502
|
+
WithPlaceholder,
|
|
503
|
+
WithSelectableOptions {
|
|
504
|
+
type: "select";
|
|
505
|
+
searchable?: boolean;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** `autocomplete` — free-text input with datalist suggestions. */
|
|
509
|
+
export interface AutocompleteFieldDef
|
|
510
|
+
extends BaseFieldDef,
|
|
511
|
+
WithPlaceholder,
|
|
512
|
+
WithSelectableOptions {
|
|
513
|
+
type: "autocomplete";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** `checkbox` — a single boolean checkbox. */
|
|
517
|
+
export interface CheckboxFieldDef extends BaseFieldDef {
|
|
518
|
+
type: "checkbox";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** `checkbox-group` — multiple checkboxes; model is an array of values. */
|
|
522
|
+
export interface CheckboxGroupFieldDef
|
|
523
|
+
extends BaseFieldDef,
|
|
524
|
+
WithSelectableOptions {
|
|
525
|
+
type: "checkbox-group";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** `switch` — a toggle with contextual on/off text and on-state colour. */
|
|
529
|
+
export interface SwitchFieldDef extends BaseFieldDef {
|
|
530
|
+
type: "switch";
|
|
531
|
+
textWhenTrue?: MaybeFn<string>;
|
|
532
|
+
textWhenFalse?: MaybeFn<string>;
|
|
533
|
+
switchVariant?: "success" | "neutral";
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** `switch-list` — a labelled toggle row per option; model is an array. */
|
|
537
|
+
export interface SwitchListFieldDef
|
|
538
|
+
extends BaseFieldDef,
|
|
539
|
+
WithSelectableOptions {
|
|
540
|
+
type: "switch-list";
|
|
541
|
+
switchVariant?: "success" | "neutral";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** `radio` — a radio group. */
|
|
545
|
+
export interface RadioFieldDef extends BaseFieldDef, WithSelectableOptions {
|
|
546
|
+
type: "radio";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** `image` / `file` — file input (`image` also shows a preview). */
|
|
550
|
+
export interface FileFieldDef extends BaseFieldDef {
|
|
551
|
+
type: "image" | "file";
|
|
552
|
+
accept?: string;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** `component` — escape hatch rendering `field.component`. */
|
|
556
|
+
export interface ComponentFieldDef extends BaseFieldDef {
|
|
557
|
+
type: "component";
|
|
558
|
+
/** Required on the strict variant: a `component` field with no component
|
|
559
|
+
* falls through to the generic input branch, so the union enforces it. */
|
|
560
|
+
component: Component;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** `repeater` — a nested, repeatable sub-form driven by `fields`. */
|
|
564
|
+
export interface RepeaterFieldDef extends BaseFieldDef {
|
|
565
|
+
type: "repeater";
|
|
566
|
+
fields?: FieldDef[];
|
|
567
|
+
addLabel?: string;
|
|
568
|
+
minItems?: number;
|
|
569
|
+
maxItems?: number;
|
|
570
|
+
repeaterLayout?: "cards" | "table";
|
|
571
|
+
showRowIndex?: boolean;
|
|
572
|
+
softDeleteKey?: string;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Strict, discriminated replacement for `FieldDefinition`. Keyed on `type`,
|
|
577
|
+
* so each field only permits its own options. Opt-in today; canonical in a
|
|
578
|
+
* future major (#131). See the banner above.
|
|
579
|
+
*/
|
|
580
|
+
export type FieldDef =
|
|
581
|
+
| TextFieldDef
|
|
582
|
+
| PasswordFieldDef
|
|
583
|
+
| NumberFieldDef
|
|
584
|
+
| DateFieldDef
|
|
585
|
+
| CurrencyFieldDef
|
|
586
|
+
| PercentageFieldDef
|
|
587
|
+
| TextareaFieldDef
|
|
588
|
+
| SelectFieldDef
|
|
589
|
+
| AutocompleteFieldDef
|
|
590
|
+
| CheckboxFieldDef
|
|
591
|
+
| CheckboxGroupFieldDef
|
|
592
|
+
| SwitchFieldDef
|
|
593
|
+
| SwitchListFieldDef
|
|
594
|
+
| RadioFieldDef
|
|
595
|
+
| FileFieldDef
|
|
596
|
+
| ComponentFieldDef
|
|
597
|
+
| RepeaterFieldDef;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Compile-time drift guard (#131): every strict variant must remain assignable
|
|
601
|
+
* to the permissive `FieldDefinition`. If a `FieldDef` variant grows a prop that
|
|
602
|
+
* `FieldDefinition` doesn't have (or types incompatibly), this fails to satisfy
|
|
603
|
+
* its `true` constraint and the build goes red. Purely type-level — no runtime.
|
|
604
|
+
* (The reverse direction — a prop added to `FieldDefinition` but forgotten on a
|
|
605
|
+
* variant — can't be asserted this way; the banner above is the reminder.)
|
|
606
|
+
*/
|
|
607
|
+
type _AssertTrue<T extends true> = T;
|
|
608
|
+
// Exported (under an internal underscore name, not re-exported by the barrel)
|
|
609
|
+
// only so `noUnusedLocals` treats it as used — its value is the constraint check.
|
|
610
|
+
export type _FieldDefStaysASubset = _AssertTrue<
|
|
611
|
+
FieldDef extends FieldDefinition ? true : false
|
|
612
|
+
>;
|
|
613
|
+
|
|
383
614
|
/**
|
|
384
615
|
* A tab in a tabbed form. Groups a subset of fields and can be shown
|
|
385
616
|
* conditionally or lazily mounted.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure form-schema semantics — defaulting, cloning, predicate resolution,
|
|
3
|
+
* visibility, and submission-eligibility — extracted so the rule is defined
|
|
4
|
+
* ONCE and tested once (#134).
|
|
5
|
+
*
|
|
6
|
+
* These lived, drifted, in three-plus places: `defineForm`, `useResourceEditor`,
|
|
7
|
+
* `DXForm`, and `DXRepeater` each re-implemented seeding/visibility and were
|
|
8
|
+
* "kept in step" by hand — the exact shape of bug behind #117/#122/#125. Each
|
|
9
|
+
* copy had drifted: `DXRepeater`'s type map was missing the array-valued
|
|
10
|
+
* `checkbox-group`/`switch-list` cases, and `useResourceEditor` wasn't
|
|
11
|
+
* type-aware at all (a flat `""` fallback). Consolidating here fixes that drift.
|
|
12
|
+
*
|
|
13
|
+
* Everything is a plain function of its inputs (no Vue reactivity), so it unit
|
|
14
|
+
* tests directly.
|
|
15
|
+
*/
|
|
16
|
+
import { toRaw } from "vue";
|
|
17
|
+
import type { FieldType, MaybeFn } from "../types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The canonical starting value for a field TYPE, used when a field declares no
|
|
21
|
+
* explicit `default`. Superset of the previous copies:
|
|
22
|
+
* - `checkbox` / `switch` → `false`
|
|
23
|
+
* - `number` / `currency` / `percentage` → `0`
|
|
24
|
+
* - `repeater` / `checkbox-group` / `switch-list` → `[]` (array-valued models)
|
|
25
|
+
* - `image` / `file` → `null`
|
|
26
|
+
* - everything else (text family, `select`, `radio`, `autocomplete`, …) → `""`
|
|
27
|
+
*/
|
|
28
|
+
export function defaultValueForType(type: FieldType): any {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "checkbox":
|
|
31
|
+
case "switch":
|
|
32
|
+
return false;
|
|
33
|
+
case "number":
|
|
34
|
+
case "currency":
|
|
35
|
+
case "percentage":
|
|
36
|
+
return 0;
|
|
37
|
+
case "repeater":
|
|
38
|
+
case "checkbox-group":
|
|
39
|
+
case "switch-list":
|
|
40
|
+
return [];
|
|
41
|
+
case "image":
|
|
42
|
+
case "file":
|
|
43
|
+
return null;
|
|
44
|
+
default:
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Deep-clone a default so object/array defaults aren't shared BY REFERENCE
|
|
51
|
+
* across form instances or repeater rows (mutating one row would otherwise
|
|
52
|
+
* mutate the field's declared default and every sibling row).
|
|
53
|
+
*
|
|
54
|
+
* Mirrors `useForm`'s `deepClone`: `toRaw` unwraps a Vue reactive proxy, then
|
|
55
|
+
* `structuredClone` (which preserves Date/File/Map — a plain JSON round-trip
|
|
56
|
+
* would flatten a `Date` default to a string) with a JSON fallback for a
|
|
57
|
+
* residual nested proxy or otherwise-uncloneable value. This matters because
|
|
58
|
+
* `defineForm`/`useResourceEditor` now clone here BEFORE the value reaches
|
|
59
|
+
* `useForm`, so a lossy clone would corrupt a rich default the raw path used
|
|
60
|
+
* to preserve (#134 review).
|
|
61
|
+
*/
|
|
62
|
+
export function cloneDefault<TValue>(value: TValue): TValue {
|
|
63
|
+
if (value === null || typeof value !== "object") return value;
|
|
64
|
+
const raw = toRaw(value);
|
|
65
|
+
if (typeof structuredClone === "function") {
|
|
66
|
+
try {
|
|
67
|
+
return structuredClone(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
// Reactive proxy or otherwise-uncloneable — fall through to JSON.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return JSON.parse(JSON.stringify(raw));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A field's starting value: its explicit `default` (deep-cloned) when one is
|
|
77
|
+
* DEFINED, else the type default. Definedness — not nullishness — decides, so a
|
|
78
|
+
* legitimate `null` default (e.g. a "none" select option whose value is `null`)
|
|
79
|
+
* survives instead of being replaced by the type default (#122/#125). An absent
|
|
80
|
+
* or explicitly-`undefined` default falls back to the type default rather than
|
|
81
|
+
* seeding `undefined`, which would drop the key from the JSON payload.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveFieldDefault(field: {
|
|
84
|
+
type: FieldType;
|
|
85
|
+
default?: any;
|
|
86
|
+
}): any {
|
|
87
|
+
return field.default !== undefined
|
|
88
|
+
? cloneDefault(field.default)
|
|
89
|
+
: defaultValueForType(field.type);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a `MaybeFn<boolean>` predicate (`when` on a field or tab) against the
|
|
94
|
+
* live model. `undefined` yields `fallback` (the field/tab is unconditioned).
|
|
95
|
+
*/
|
|
96
|
+
export function resolvePredicate(
|
|
97
|
+
when: MaybeFn<boolean> | undefined,
|
|
98
|
+
model: any,
|
|
99
|
+
fallback: boolean,
|
|
100
|
+
): boolean {
|
|
101
|
+
if (when === undefined) return fallback;
|
|
102
|
+
return typeof when === "function" ? when(model) : when;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a field is VISIBLE against a model, by the one rule every renderer
|
|
107
|
+
* shares: the `when` predicate (default visible) AND the legacy no-arg `show`
|
|
108
|
+
* predicate. A field is visible only if both pass.
|
|
109
|
+
*/
|
|
110
|
+
export function isFieldVisible(
|
|
111
|
+
field: { when?: MaybeFn<boolean>; show?: () => boolean },
|
|
112
|
+
model: any,
|
|
113
|
+
): boolean {
|
|
114
|
+
const whenOk = resolvePredicate(field.when, model, true);
|
|
115
|
+
const showOk = field.show ? field.show() : true;
|
|
116
|
+
return whenOk && showOk;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether a field's value belongs in the submitted payload. A presentational
|
|
121
|
+
* field (`submit: false`) — a header, an alert, an explanatory `span` block —
|
|
122
|
+
* renders but holds no data and must not be POSTed just because it was declared.
|
|
123
|
+
*/
|
|
124
|
+
export function isSubmittableField(field: { submit?: boolean }): boolean {
|
|
125
|
+
return field.submit !== false;
|
|
126
|
+
}
|