@shwfed/nuxt 0.8.0 → 0.8.2

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,10 +1,15 @@
1
1
  <script setup>
2
2
  import { useSortable } from "@vueuse/integrations/useSortable";
3
3
  import { Icon } from "@iconify/vue";
4
- import { computed, nextTick, ref, watch } from "vue";
4
+ import { computed, nextTick, ref, toRaw, watch } from "vue";
5
5
  import { useI18n } from "vue-i18n";
6
+ import {
7
+ CalendarFieldC,
8
+ NumberFieldC,
9
+ SelectFieldC,
10
+ StringFieldC
11
+ } from "../fields/schema";
6
12
  import { cn } from "../../../utils/cn";
7
- import { getLocalizedText } from "../../../utils/coders";
8
13
  import { Button } from "../button";
9
14
  import {
10
15
  Dialog,
@@ -14,7 +19,18 @@ import {
14
19
  DialogHeader,
15
20
  DialogTitle
16
21
  } from "../dialog";
22
+ import {
23
+ DropdownMenu,
24
+ DropdownMenuContent,
25
+ DropdownMenuItem,
26
+ DropdownMenuTrigger
27
+ } from "../dropdown-menu";
28
+ import { IconPicker } from "../icon-picker";
29
+ import { Input } from "../input";
17
30
  import Locale from "../locale/Locale.vue";
31
+ import { NativeSelect, NativeSelectOption } from "../native-select";
32
+ import { Switch } from "../switch";
33
+ import { Textarea } from "../textarea";
18
34
  const props = defineProps({
19
35
  fields: { type: Array, required: true }
20
36
  });
@@ -22,41 +38,214 @@ const emit = defineEmits(["confirm"]);
22
38
  const open = defineModel("open", { type: Boolean, ...{
23
39
  default: false
24
40
  } });
25
- const { t, locale } = useI18n();
41
+ const { t } = useI18n();
26
42
  const selectedItemId = ref("general");
27
43
  const draftFields = ref([]);
28
44
  const sortableListRef = ref(null);
29
45
  const sortableItemIds = ref([]);
46
+ const validationErrors = ref({});
47
+ const fieldTypeOptions = computed(() => [
48
+ { type: "string", label: t("field-type-string") },
49
+ { type: "number", label: t("field-type-number") },
50
+ { type: "select", label: t("field-type-select") },
51
+ { type: "calendar", label: t("field-type-calendar") }
52
+ ]);
30
53
  const generalItem = computed(() => ({
31
54
  id: "general",
32
55
  label: t("general")
33
56
  }));
34
- function getFieldLabel(field) {
35
- return getLocalizedText(field.title, locale.value) ?? field.path;
57
+ const selectedField = computed(() => draftFields.value.find((field) => field.draftId === selectedItemId.value));
58
+ const selectedFieldValidationRules = computed(() => selectedField.value?.field.validation ?? []);
59
+ function createDraftId() {
60
+ return crypto.randomUUID();
61
+ }
62
+ function createDefaultLocaleValue() {
63
+ return [{ locale: "zh", message: "" }];
64
+ }
65
+ function createDraftField(field) {
66
+ return {
67
+ draftId: createDraftId(),
68
+ field: JSON.parse(JSON.stringify(toRaw(field)))
69
+ };
36
70
  }
37
- const fieldItems = computed(() => draftFields.value.map((field) => ({
38
- itemId: field.path,
39
- label: getFieldLabel(field),
40
- path: field.path
41
- })));
42
- const selectedField = computed(() => draftFields.value.find((field) => field.path === selectedItemId.value));
43
- const selectedItemLabel = computed(() => selectedField.value ? getFieldLabel(selectedField.value) : generalItem.value.label);
44
71
  function cloneFields(fields) {
45
- return fields.slice();
72
+ return fields.map(createDraftField);
73
+ }
74
+ function getFieldTypeLabel(type) {
75
+ return t(`field-type-${type}`);
76
+ }
77
+ function getUnnamedFieldLabel(field) {
78
+ return t("unnamed-field", {
79
+ type: getFieldTypeLabel(field.type)
80
+ });
81
+ }
82
+ function getFieldChineseTitle(field) {
83
+ const zhTitle = field.title.find((item) => item.locale === "zh");
84
+ if (!zhTitle) {
85
+ return void 0;
86
+ }
87
+ const message = zhTitle.message.trim();
88
+ return message.length > 0 ? message : void 0;
89
+ }
90
+ function getFieldListLabel(field) {
91
+ return getFieldChineseTitle(field) ?? getUnnamedFieldLabel(field);
92
+ }
93
+ const fieldItems = computed(() => draftFields.value.map((item) => ({
94
+ itemId: item.draftId,
95
+ label: getFieldListLabel(item.field),
96
+ path: item.field.path,
97
+ type: item.field.type
98
+ })));
99
+ const selectedItemLabel = computed(() => selectedField.value ? getFieldListLabel(selectedField.value.field) : generalItem.value.label);
100
+ const sortable = useSortable(sortableListRef, sortableItemIds);
101
+ function getFieldErrorKey(draftId, fieldKey) {
102
+ return `${draftId}:${fieldKey}`;
103
+ }
104
+ function getValidationRuleErrorKey(draftId, index, control) {
105
+ return `${draftId}:validation:${index}:${control}`;
106
+ }
107
+ function clearError(key) {
108
+ Reflect.deleteProperty(validationErrors.value, key);
109
+ }
110
+ function clearFieldError(draftId, fieldKey) {
111
+ clearError(getFieldErrorKey(draftId, fieldKey));
112
+ }
113
+ function clearValidationRuleError(draftId, index, control) {
114
+ clearError(getValidationRuleErrorKey(draftId, index, control));
115
+ }
116
+ function clearFieldErrors(draftId) {
117
+ for (const key of Object.keys(validationErrors.value)) {
118
+ if (key.startsWith(`${draftId}:`)) {
119
+ Reflect.deleteProperty(validationErrors.value, key);
120
+ }
121
+ }
122
+ }
123
+ function clearValidationRuleErrors(draftId) {
124
+ for (const key of Object.keys(validationErrors.value)) {
125
+ if (key.startsWith(`${draftId}:validation:`)) {
126
+ Reflect.deleteProperty(validationErrors.value, key);
127
+ }
128
+ }
129
+ }
130
+ function setError(errors, key, message) {
131
+ if (errors[key] === void 0) {
132
+ errors[key] = message;
133
+ }
134
+ }
135
+ function normalizeOptionalString(value) {
136
+ const normalizedValue = value.trim();
137
+ if (normalizedValue.length === 0) {
138
+ return void 0;
139
+ }
140
+ return normalizedValue;
141
+ }
142
+ function normalizeValidationRules(validation) {
143
+ if (!validation || validation.length === 0) {
144
+ return void 0;
145
+ }
146
+ return validation.map((rule) => ({
147
+ expression: rule.expression.trim(),
148
+ message: rule.message
149
+ }));
150
+ }
151
+ function normalizeField(field) {
152
+ switch (field.type) {
153
+ case "string":
154
+ return {
155
+ ...field,
156
+ path: field.path.trim(),
157
+ icon: normalizeOptionalString(field.icon ?? ""),
158
+ style: normalizeOptionalString(field.style ?? ""),
159
+ maxLength: normalizeOptionalString(field.maxLength ?? ""),
160
+ hidden: normalizeOptionalString(field.hidden ?? ""),
161
+ disabled: normalizeOptionalString(field.disabled ?? ""),
162
+ discardEmptyString: field.discardEmptyString ? true : void 0,
163
+ validation: normalizeValidationRules(field.validation)
164
+ };
165
+ case "number":
166
+ return {
167
+ ...field,
168
+ path: field.path.trim(),
169
+ icon: normalizeOptionalString(field.icon ?? ""),
170
+ style: normalizeOptionalString(field.style ?? ""),
171
+ min: normalizeOptionalString(field.min ?? ""),
172
+ max: normalizeOptionalString(field.max ?? ""),
173
+ step: normalizeOptionalString(field.step ?? ""),
174
+ hidden: normalizeOptionalString(field.hidden ?? ""),
175
+ disabled: normalizeOptionalString(field.disabled ?? ""),
176
+ validation: normalizeValidationRules(field.validation)
177
+ };
178
+ case "select":
179
+ return {
180
+ ...field,
181
+ path: field.path.trim(),
182
+ icon: normalizeOptionalString(field.icon ?? ""),
183
+ style: normalizeOptionalString(field.style ?? ""),
184
+ options: field.options.trim(),
185
+ label: field.label.trim(),
186
+ value: field.value.trim(),
187
+ key: field.key.trim(),
188
+ hidden: normalizeOptionalString(field.hidden ?? ""),
189
+ disabled: normalizeOptionalString(field.disabled ?? ""),
190
+ validation: normalizeValidationRules(field.validation)
191
+ };
192
+ case "calendar":
193
+ return {
194
+ ...field,
195
+ path: field.path.trim(),
196
+ icon: normalizeOptionalString(field.icon ?? ""),
197
+ style: normalizeOptionalString(field.style ?? ""),
198
+ display: normalizeOptionalString(field.display ?? ""),
199
+ value: field.value.trim(),
200
+ disableDate: normalizeOptionalString(field.disableDate ?? ""),
201
+ hidden: normalizeOptionalString(field.hidden ?? ""),
202
+ disabled: normalizeOptionalString(field.disabled ?? ""),
203
+ validation: normalizeValidationRules(field.validation)
204
+ };
205
+ }
206
+ }
207
+ function createField(type) {
208
+ const title = createDefaultLocaleValue();
209
+ switch (type) {
210
+ case "string":
211
+ return {
212
+ type,
213
+ path: "",
214
+ title
215
+ };
216
+ case "number":
217
+ return {
218
+ type,
219
+ path: "",
220
+ title
221
+ };
222
+ case "select":
223
+ return {
224
+ type,
225
+ path: "",
226
+ title,
227
+ options: "[]",
228
+ label: '""',
229
+ value: "option",
230
+ key: '""'
231
+ };
232
+ case "calendar":
233
+ return {
234
+ type,
235
+ path: "",
236
+ title,
237
+ mode: "date",
238
+ value: "yyyy-MM-dd"
239
+ };
240
+ }
46
241
  }
47
242
  function resetDraftFields() {
48
243
  draftFields.value = cloneFields(props.fields);
49
244
  selectedItemId.value = "general";
245
+ validationErrors.value = {};
50
246
  }
51
- watch(() => props.fields, (fields) => {
52
- if (!open.value) {
53
- draftFields.value = cloneFields(fields);
54
- selectedItemId.value = "general";
55
- }
56
- }, { immediate: true });
57
- const sortable = useSortable(sortableListRef, sortableItemIds);
58
247
  function syncSortableItemIds() {
59
- sortableItemIds.value = draftFields.value.map((field) => field.path);
248
+ sortableItemIds.value = draftFields.value.map((field) => field.draftId);
60
249
  }
61
250
  function moveField(fields, oldIndex, newIndex) {
62
251
  if (oldIndex < 0 || newIndex < 0 || oldIndex >= fields.length || newIndex >= fields.length) {
@@ -93,6 +282,13 @@ async function refreshSortable() {
93
282
  sortable.start();
94
283
  configureSortable();
95
284
  }
285
+ watch(() => props.fields, (fields) => {
286
+ if (!open.value) {
287
+ draftFields.value = cloneFields(fields);
288
+ selectedItemId.value = "general";
289
+ validationErrors.value = {};
290
+ }
291
+ }, { immediate: true });
96
292
  watch(draftFields, () => {
97
293
  syncSortableItemIds();
98
294
  }, { immediate: true });
@@ -103,6 +299,7 @@ watch(open, async (value) => {
103
299
  return;
104
300
  }
105
301
  sortable.stop();
302
+ validationErrors.value = {};
106
303
  }, { immediate: true });
107
304
  watch(fieldItems, async (items) => {
108
305
  if (selectedItemId.value === "general") {
@@ -132,27 +329,493 @@ function selectGeneral() {
132
329
  function selectItem(itemId) {
133
330
  selectedItemId.value = itemId;
134
331
  }
332
+ function updateDraftField(draftId, updater) {
333
+ draftFields.value = draftFields.value.map((item) => item.draftId === draftId ? {
334
+ draftId: item.draftId,
335
+ field: updater(item.field)
336
+ } : item);
337
+ }
338
+ function addField(type) {
339
+ const draftField = createDraftField(createField(type));
340
+ draftFields.value = [...draftFields.value, draftField];
341
+ selectedItemId.value = draftField.draftId;
342
+ clearFieldErrors(draftField.draftId);
343
+ }
135
344
  function deleteField(itemId) {
136
- const deleteIndex = draftFields.value.findIndex((field) => field.path === itemId);
345
+ const deleteIndex = draftFields.value.findIndex((field) => field.draftId === itemId);
137
346
  if (deleteIndex < 0) {
138
347
  return;
139
348
  }
140
- const nextFields = draftFields.value.filter((field) => field.path !== itemId);
349
+ const nextFields = draftFields.value.filter((field) => field.draftId !== itemId);
141
350
  draftFields.value = nextFields;
351
+ clearFieldErrors(itemId);
142
352
  if (selectedItemId.value !== itemId) {
143
353
  return;
144
354
  }
145
355
  const nextField = nextFields[deleteIndex] ?? nextFields[deleteIndex - 1];
146
- selectedItemId.value = nextField?.path ?? "general";
356
+ selectedItemId.value = nextField?.draftId ?? "general";
147
357
  }
148
358
  function updateSelectedFieldTitle(value) {
149
- if (!selectedField.value) {
359
+ const selected = selectedField.value;
360
+ if (!selected) {
361
+ return;
362
+ }
363
+ clearFieldError(selected.draftId, "title");
364
+ updateDraftField(selected.draftId, (field) => ({
365
+ ...field,
366
+ title: value
367
+ }));
368
+ }
369
+ function updateSelectedFieldPath(value) {
370
+ const selected = selectedField.value;
371
+ if (!selected) {
372
+ return;
373
+ }
374
+ clearFieldError(selected.draftId, "path");
375
+ updateDraftField(selected.draftId, (field) => ({
376
+ ...field,
377
+ path: String(value).trim()
378
+ }));
379
+ }
380
+ function updateSelectedFieldIcon(value) {
381
+ const selected = selectedField.value;
382
+ if (!selected) {
383
+ return;
384
+ }
385
+ clearFieldError(selected.draftId, "icon");
386
+ updateDraftField(selected.draftId, (field) => ({
387
+ ...field,
388
+ icon: normalizeOptionalString(String(value ?? ""))
389
+ }));
390
+ }
391
+ function updateSelectedFieldStyle(value) {
392
+ const selected = selectedField.value;
393
+ if (!selected) {
394
+ return;
395
+ }
396
+ clearFieldError(selected.draftId, "style");
397
+ updateDraftField(selected.draftId, (field) => ({
398
+ ...field,
399
+ style: normalizeOptionalString(String(value))
400
+ }));
401
+ }
402
+ function updateSelectedFieldHidden(value) {
403
+ const selected = selectedField.value;
404
+ if (!selected) {
405
+ return;
406
+ }
407
+ clearFieldError(selected.draftId, "hidden");
408
+ updateDraftField(selected.draftId, (field) => ({
409
+ ...field,
410
+ hidden: normalizeOptionalString(String(value))
411
+ }));
412
+ }
413
+ function updateSelectedFieldDisabled(value) {
414
+ const selected = selectedField.value;
415
+ if (!selected) {
416
+ return;
417
+ }
418
+ clearFieldError(selected.draftId, "disabled");
419
+ updateDraftField(selected.draftId, (field) => ({
420
+ ...field,
421
+ disabled: normalizeOptionalString(String(value))
422
+ }));
423
+ }
424
+ function updateSelectedStringDiscardEmpty(value) {
425
+ const selected = selectedField.value;
426
+ if (!selected || selected.field.type !== "string") {
427
+ return;
428
+ }
429
+ updateDraftField(selected.draftId, (field) => {
430
+ if (field.type !== "string") {
431
+ return field;
432
+ }
433
+ return {
434
+ ...field,
435
+ discardEmptyString: value ? true : void 0
436
+ };
437
+ });
438
+ }
439
+ function updateSelectedStringMaxLength(value) {
440
+ const selected = selectedField.value;
441
+ if (!selected || selected.field.type !== "string") {
442
+ return;
443
+ }
444
+ clearFieldError(selected.draftId, "maxLength");
445
+ updateDraftField(selected.draftId, (field) => {
446
+ if (field.type !== "string") {
447
+ return field;
448
+ }
449
+ return {
450
+ ...field,
451
+ maxLength: normalizeOptionalString(String(value))
452
+ };
453
+ });
454
+ }
455
+ function updateSelectedNumberMin(value) {
456
+ const selected = selectedField.value;
457
+ if (!selected || selected.field.type !== "number") {
458
+ return;
459
+ }
460
+ clearFieldError(selected.draftId, "min");
461
+ updateDraftField(selected.draftId, (field) => {
462
+ if (field.type !== "number") {
463
+ return field;
464
+ }
465
+ return {
466
+ ...field,
467
+ min: normalizeOptionalString(String(value))
468
+ };
469
+ });
470
+ }
471
+ function updateSelectedNumberMax(value) {
472
+ const selected = selectedField.value;
473
+ if (!selected || selected.field.type !== "number") {
474
+ return;
475
+ }
476
+ clearFieldError(selected.draftId, "max");
477
+ updateDraftField(selected.draftId, (field) => {
478
+ if (field.type !== "number") {
479
+ return field;
480
+ }
481
+ return {
482
+ ...field,
483
+ max: normalizeOptionalString(String(value))
484
+ };
485
+ });
486
+ }
487
+ function updateSelectedNumberStep(value) {
488
+ const selected = selectedField.value;
489
+ if (!selected || selected.field.type !== "number") {
490
+ return;
491
+ }
492
+ clearFieldError(selected.draftId, "step");
493
+ updateDraftField(selected.draftId, (field) => {
494
+ if (field.type !== "number") {
495
+ return field;
496
+ }
497
+ return {
498
+ ...field,
499
+ step: normalizeOptionalString(String(value))
500
+ };
501
+ });
502
+ }
503
+ function updateSelectedSelectOptions(value) {
504
+ const selected = selectedField.value;
505
+ if (!selected || selected.field.type !== "select") {
506
+ return;
507
+ }
508
+ clearFieldError(selected.draftId, "options");
509
+ updateDraftField(selected.draftId, (field) => {
510
+ if (field.type !== "select") {
511
+ return field;
512
+ }
513
+ return {
514
+ ...field,
515
+ options: String(value).trim()
516
+ };
517
+ });
518
+ }
519
+ function updateSelectedSelectLabel(value) {
520
+ const selected = selectedField.value;
521
+ if (!selected || selected.field.type !== "select") {
522
+ return;
523
+ }
524
+ clearFieldError(selected.draftId, "label");
525
+ updateDraftField(selected.draftId, (field) => {
526
+ if (field.type !== "select") {
527
+ return field;
528
+ }
529
+ return {
530
+ ...field,
531
+ label: String(value).trim()
532
+ };
533
+ });
534
+ }
535
+ function updateSelectedSelectValue(value) {
536
+ const selected = selectedField.value;
537
+ if (!selected || selected.field.type !== "select") {
538
+ return;
539
+ }
540
+ clearFieldError(selected.draftId, "value");
541
+ updateDraftField(selected.draftId, (field) => {
542
+ if (field.type !== "select") {
543
+ return field;
544
+ }
545
+ return {
546
+ ...field,
547
+ value: String(value).trim()
548
+ };
549
+ });
550
+ }
551
+ function updateSelectedSelectKey(value) {
552
+ const selected = selectedField.value;
553
+ if (!selected || selected.field.type !== "select") {
554
+ return;
555
+ }
556
+ clearFieldError(selected.draftId, "key");
557
+ updateDraftField(selected.draftId, (field) => {
558
+ if (field.type !== "select") {
559
+ return field;
560
+ }
561
+ return {
562
+ ...field,
563
+ key: String(value).trim()
564
+ };
565
+ });
566
+ }
567
+ function updateSelectedCalendarMode(value) {
568
+ const selected = selectedField.value;
569
+ if (!selected || selected.field.type !== "calendar") {
570
+ return;
571
+ }
572
+ const normalizedValue = String(value);
573
+ if (normalizedValue !== "year" && normalizedValue !== "month" && normalizedValue !== "date") {
574
+ return;
575
+ }
576
+ clearFieldError(selected.draftId, "mode");
577
+ updateDraftField(selected.draftId, (field) => {
578
+ if (field.type !== "calendar") {
579
+ return field;
580
+ }
581
+ return {
582
+ ...field,
583
+ mode: normalizedValue
584
+ };
585
+ });
586
+ }
587
+ function updateSelectedCalendarDisplay(value) {
588
+ const selected = selectedField.value;
589
+ if (!selected || selected.field.type !== "calendar") {
590
+ return;
591
+ }
592
+ clearFieldError(selected.draftId, "display");
593
+ updateDraftField(selected.draftId, (field) => {
594
+ if (field.type !== "calendar") {
595
+ return field;
596
+ }
597
+ return {
598
+ ...field,
599
+ display: normalizeOptionalString(String(value))
600
+ };
601
+ });
602
+ }
603
+ function updateSelectedCalendarValue(value) {
604
+ const selected = selectedField.value;
605
+ if (!selected || selected.field.type !== "calendar") {
606
+ return;
607
+ }
608
+ clearFieldError(selected.draftId, "value");
609
+ updateDraftField(selected.draftId, (field) => {
610
+ if (field.type !== "calendar") {
611
+ return field;
612
+ }
613
+ return {
614
+ ...field,
615
+ value: String(value).trim()
616
+ };
617
+ });
618
+ }
619
+ function updateSelectedCalendarDisableDate(value) {
620
+ const selected = selectedField.value;
621
+ if (!selected || selected.field.type !== "calendar") {
622
+ return;
623
+ }
624
+ clearFieldError(selected.draftId, "disableDate");
625
+ updateDraftField(selected.draftId, (field) => {
626
+ if (field.type !== "calendar") {
627
+ return field;
628
+ }
629
+ return {
630
+ ...field,
631
+ disableDate: normalizeOptionalString(String(value))
632
+ };
633
+ });
634
+ }
635
+ function addValidationRule() {
636
+ const selected = selectedField.value;
637
+ if (!selected) {
638
+ return;
639
+ }
640
+ updateDraftField(selected.draftId, (field) => ({
641
+ ...field,
642
+ validation: [
643
+ ...field.validation ?? [],
644
+ {
645
+ expression: "",
646
+ message: ""
647
+ }
648
+ ]
649
+ }));
650
+ }
651
+ function updateValidationRule(index, updater) {
652
+ const selected = selectedField.value;
653
+ if (!selected) {
654
+ return;
655
+ }
656
+ const validation = selected.field.validation ?? [];
657
+ const currentRule = validation[index];
658
+ if (!currentRule) {
659
+ return;
660
+ }
661
+ const nextValidation = validation.map((rule, ruleIndex) => ruleIndex === index ? updater(rule) : rule);
662
+ updateDraftField(selected.draftId, (field) => ({
663
+ ...field,
664
+ validation: nextValidation
665
+ }));
666
+ }
667
+ function updateSelectedValidationExpression(index, value) {
668
+ const selected = selectedField.value;
669
+ if (!selected) {
670
+ return;
671
+ }
672
+ clearValidationRuleError(selected.draftId, index, "expression");
673
+ updateValidationRule(index, (rule) => ({
674
+ ...rule,
675
+ expression: String(value).trim()
676
+ }));
677
+ }
678
+ function updateSelectedValidationMessage(index, value) {
679
+ const selected = selectedField.value;
680
+ if (!selected) {
681
+ return;
682
+ }
683
+ clearValidationRuleError(selected.draftId, index, "message");
684
+ updateValidationRule(index, (rule) => ({
685
+ ...rule,
686
+ message: String(value)
687
+ }));
688
+ }
689
+ function moveValidationRule(index, offset) {
690
+ const selected = selectedField.value;
691
+ if (!selected) {
692
+ return;
693
+ }
694
+ const validation = selected.field.validation ?? [];
695
+ const nextIndex = index + offset;
696
+ if (!validation[index] || !validation[nextIndex]) {
697
+ return;
698
+ }
699
+ const nextValidation = validation.slice();
700
+ const currentRule = nextValidation[index];
701
+ const targetRule = nextValidation[nextIndex];
702
+ if (!currentRule || !targetRule) {
703
+ return;
704
+ }
705
+ nextValidation[index] = targetRule;
706
+ nextValidation[nextIndex] = currentRule;
707
+ updateDraftField(selected.draftId, (field) => ({
708
+ ...field,
709
+ validation: nextValidation
710
+ }));
711
+ clearValidationRuleErrors(selected.draftId);
712
+ }
713
+ function deleteValidationRule(index) {
714
+ const selected = selectedField.value;
715
+ if (!selected) {
150
716
  return;
151
717
  }
152
- draftFields.value = draftFields.value.map((field) => field.path === selectedField.value?.path ? { ...field, title: value } : field);
718
+ const validation = selected.field.validation ?? [];
719
+ if (!validation[index]) {
720
+ return;
721
+ }
722
+ clearValidationRuleError(selected.draftId, index, "expression");
723
+ clearValidationRuleError(selected.draftId, index, "message");
724
+ updateDraftField(selected.draftId, (field) => {
725
+ const nextValidation = (field.validation ?? []).filter((_, ruleIndex) => ruleIndex !== index);
726
+ return {
727
+ ...field,
728
+ validation: nextValidation.length > 0 ? nextValidation : void 0
729
+ };
730
+ });
731
+ clearValidationRuleErrors(selected.draftId);
732
+ }
733
+ function getSchemaIssues(field) {
734
+ switch (field.type) {
735
+ case "string": {
736
+ const result = StringFieldC.safeParse(field);
737
+ return result.success ? [] : result.error.issues;
738
+ }
739
+ case "number": {
740
+ const result = NumberFieldC.safeParse(field);
741
+ return result.success ? [] : result.error.issues;
742
+ }
743
+ case "select": {
744
+ const result = SelectFieldC.safeParse(field);
745
+ return result.success ? [] : result.error.issues;
746
+ }
747
+ case "calendar": {
748
+ const result = CalendarFieldC.safeParse(field);
749
+ return result.success ? [] : result.error.issues;
750
+ }
751
+ }
752
+ }
753
+ function normalizeIssuePath(path) {
754
+ const normalizedPath = [];
755
+ for (const segment of path) {
756
+ if (typeof segment === "string" || typeof segment === "number") {
757
+ normalizedPath.push(segment);
758
+ }
759
+ }
760
+ return normalizedPath;
761
+ }
762
+ function getIssueErrorKey(draftId, path) {
763
+ const [head, second, third] = path;
764
+ if (head === "validation" && typeof second === "number" && (third === "expression" || third === "message")) {
765
+ return getValidationRuleErrorKey(draftId, second, third);
766
+ }
767
+ if (typeof head !== "string") {
768
+ return void 0;
769
+ }
770
+ return getFieldErrorKey(draftId, head);
771
+ }
772
+ function validateDraftFields() {
773
+ const errors = {};
774
+ const normalizedFields = draftFields.value.map((item) => ({
775
+ draftId: item.draftId,
776
+ field: normalizeField(item.field)
777
+ }));
778
+ const pathOwners = {};
779
+ let firstInvalidItemId;
780
+ for (const item of normalizedFields) {
781
+ const existingOwner = pathOwners[item.field.path];
782
+ if (item.field.path.length === 0) {
783
+ setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-required"));
784
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
785
+ } else if (existingOwner !== void 0) {
786
+ setError(errors, getFieldErrorKey(item.draftId, "path"), t("field-path-duplicate"));
787
+ setError(errors, getFieldErrorKey(existingOwner, "path"), t("field-path-duplicate"));
788
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
789
+ } else {
790
+ pathOwners[item.field.path] = item.draftId;
791
+ }
792
+ if (item.field.type === "calendar" && item.field.value.length === 0) {
793
+ setError(errors, getFieldErrorKey(item.draftId, "value"), t("calendar-value-required"));
794
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
795
+ }
796
+ for (const issue of getSchemaIssues(item.field)) {
797
+ const issueKey = getIssueErrorKey(item.draftId, normalizeIssuePath(issue.path));
798
+ if (!issueKey) {
799
+ continue;
800
+ }
801
+ setError(errors, issueKey, issue.message);
802
+ firstInvalidItemId = firstInvalidItemId ?? item.draftId;
803
+ }
804
+ }
805
+ validationErrors.value = errors;
806
+ if (firstInvalidItemId) {
807
+ selectedItemId.value = firstInvalidItemId;
808
+ return void 0;
809
+ }
810
+ return normalizedFields;
153
811
  }
154
812
  function confirmChanges() {
155
- emit("confirm", draftFields.value.slice());
813
+ const normalizedFields = validateDraftFields();
814
+ if (!normalizedFields) {
815
+ return;
816
+ }
817
+ draftFields.value = normalizedFields.map((item) => createDraftField(item.field));
818
+ emit("confirm", normalizedFields.map((item) => item.field));
156
819
  open.value = false;
157
820
  }
158
821
  </script>
@@ -209,6 +872,7 @@ function confirmChanges() {
209
872
  :key="item.itemId"
210
873
  data-slot="fields-configurator-field-item"
211
874
  :data-item-id="item.itemId"
875
+ :data-field-path="item.path"
212
876
  :data-selected="selectedItemId === item.itemId ? 'true' : 'false'"
213
877
  :class="cn(
214
878
  'flex w-full items-center gap-2 rounded-md border p-1 transition-colors',
@@ -260,6 +924,37 @@ function confirmChanges() {
260
924
  </p>
261
925
  </div>
262
926
  </div>
927
+
928
+ <div
929
+ data-slot="fields-configurator-add-container"
930
+ class="mt-4"
931
+ >
932
+ <DropdownMenu>
933
+ <DropdownMenuTrigger as-child>
934
+ <Button
935
+ type="button"
936
+ data-slot="fields-configurator-add"
937
+ size="sm"
938
+ variant="default"
939
+ class="w-full justify-center"
940
+ >
941
+ <Icon icon="fluent:add-20-regular" />
942
+ {{ t("add-field") }}
943
+ </Button>
944
+ </DropdownMenuTrigger>
945
+
946
+ <DropdownMenuContent align="start">
947
+ <DropdownMenuItem
948
+ v-for="option in fieldTypeOptions"
949
+ :key="option.type"
950
+ :data-slot="`fields-configurator-add-item-${option.type}`"
951
+ @select="addField(option.type)"
952
+ >
953
+ {{ option.label }}
954
+ </DropdownMenuItem>
955
+ </DropdownMenuContent>
956
+ </DropdownMenu>
957
+ </div>
263
958
  </section>
264
959
 
265
960
  <section class="flex min-h-0 flex-col overflow-y-auto px-6 py-6">
@@ -269,18 +964,19 @@ function confirmChanges() {
269
964
  >
270
965
  {{ selectedItemLabel }}
271
966
  </h3>
967
+
272
968
  <p
273
969
  v-if="selectedItemId === 'general'"
274
970
  data-slot="fields-configurator-detail-description"
275
971
  class="mt-2 text-sm text-zinc-500"
276
972
  >
277
- {{ t("general-placeholder") }}
973
+ {{ t("general-description") }}
278
974
  </p>
279
975
 
280
976
  <div
281
977
  v-if="selectedItemId === 'general'"
282
978
  data-slot="fields-configurator-general-placeholder"
283
- class="mt-6 flex min-h-48 items-center justify-center rounded-lg border border-dashed border-zinc-200 bg-zinc-50/60 px-6 text-center text-sm text-zinc-400"
979
+ class="mt-6 rounded-lg border border-dashed border-zinc-200 bg-zinc-50/60 px-6 py-8 text-sm text-zinc-500"
284
980
  >
285
981
  {{ t("general-empty") }}
286
982
  </div>
@@ -290,6 +986,47 @@ function confirmChanges() {
290
986
  data-slot="fields-configurator-field-main"
291
987
  class="mt-6 flex flex-col gap-6"
292
988
  >
989
+ <section class="grid gap-4 md:grid-cols-2">
990
+ <label
991
+ data-slot="fields-configurator-field-type-section"
992
+ class="flex flex-col gap-2"
993
+ >
994
+ <span class="text-xs font-medium text-zinc-500">
995
+ {{ t("field-type") }}
996
+ </span>
997
+ <div
998
+ data-slot="fields-configurator-field-type"
999
+ class="flex h-9 items-center rounded-md border border-zinc-200 bg-zinc-50 px-3 text-sm text-zinc-600"
1000
+ >
1001
+ {{ getFieldTypeLabel(selectedField.field.type) }}
1002
+ </div>
1003
+ </label>
1004
+
1005
+ <label
1006
+ data-slot="fields-configurator-field-path-section"
1007
+ class="flex flex-col gap-2"
1008
+ >
1009
+ <span class="text-xs font-medium text-zinc-500">
1010
+ {{ t("field-path") }}
1011
+ </span>
1012
+ <Input
1013
+ data-slot="fields-configurator-field-path-input"
1014
+ :model-value="selectedField.field.path"
1015
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'path')] ? 'true' : void 0"
1016
+ :placeholder="t('field-path-placeholder')"
1017
+ class="font-mono text-sm"
1018
+ @update:model-value="updateSelectedFieldPath"
1019
+ />
1020
+ <p
1021
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'path')]"
1022
+ data-slot="fields-configurator-field-path-error"
1023
+ class="text-xs text-red-500"
1024
+ >
1025
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "path")] }}
1026
+ </p>
1027
+ </label>
1028
+ </section>
1029
+
293
1030
  <section
294
1031
  data-slot="fields-configurator-field-label-section"
295
1032
  class="flex flex-col gap-2"
@@ -300,9 +1037,498 @@ function confirmChanges() {
300
1037
 
301
1038
  <Locale
302
1039
  data-slot="fields-configurator-field-title-locale"
303
- :model-value="selectedField.title"
1040
+ :model-value="selectedField.field.title"
304
1041
  @update:model-value="updateSelectedFieldTitle"
305
1042
  />
1043
+
1044
+ <p
1045
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'title')]"
1046
+ data-slot="fields-configurator-field-title-error"
1047
+ class="text-xs text-red-500"
1048
+ >
1049
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "title")] }}
1050
+ </p>
1051
+ </section>
1052
+
1053
+ <section
1054
+ data-slot="fields-configurator-field-general-options"
1055
+ class="grid gap-4 md:grid-cols-2"
1056
+ >
1057
+ <label class="flex flex-col gap-2">
1058
+ <span class="text-xs font-medium text-zinc-500">
1059
+ {{ t("field-icon") }}
1060
+ </span>
1061
+ <IconPicker
1062
+ data-slot="fields-configurator-field-icon-picker"
1063
+ :model-value="selectedField.field.icon ?? ''"
1064
+ :invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'icon')] !== void 0"
1065
+ :placeholder="t('field-icon-placeholder')"
1066
+ @update:model-value="updateSelectedFieldIcon"
1067
+ />
1068
+ <p
1069
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'icon')]"
1070
+ class="text-xs text-red-500"
1071
+ >
1072
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "icon")] }}
1073
+ </p>
1074
+ </label>
1075
+
1076
+ <label class="flex flex-col gap-2">
1077
+ <span class="text-xs font-medium text-zinc-500">
1078
+ {{ t("field-style") }}
1079
+ </span>
1080
+ <Textarea
1081
+ data-slot="fields-configurator-field-style-input"
1082
+ :model-value="selectedField.field.style ?? ''"
1083
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'style')] ? 'true' : void 0"
1084
+ :placeholder="t('field-style-placeholder')"
1085
+ class="min-h-20 font-mono text-sm"
1086
+ @update:model-value="updateSelectedFieldStyle"
1087
+ />
1088
+ <p
1089
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'style')]"
1090
+ class="text-xs text-red-500"
1091
+ >
1092
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "style")] }}
1093
+ </p>
1094
+ </label>
1095
+
1096
+ <label class="flex flex-col gap-2">
1097
+ <span class="text-xs font-medium text-zinc-500">
1098
+ {{ t("field-hidden") }}
1099
+ </span>
1100
+ <Textarea
1101
+ data-slot="fields-configurator-field-hidden-input"
1102
+ :model-value="selectedField.field.hidden ?? ''"
1103
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'hidden')] ? 'true' : void 0"
1104
+ :placeholder="t('field-hidden-placeholder')"
1105
+ class="min-h-20 font-mono text-sm"
1106
+ @update:model-value="updateSelectedFieldHidden"
1107
+ />
1108
+ <p
1109
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'hidden')]"
1110
+ class="text-xs text-red-500"
1111
+ >
1112
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "hidden")] }}
1113
+ </p>
1114
+ </label>
1115
+
1116
+ <label class="flex flex-col gap-2">
1117
+ <span class="text-xs font-medium text-zinc-500">
1118
+ {{ t("field-disabled") }}
1119
+ </span>
1120
+ <Textarea
1121
+ data-slot="fields-configurator-field-disabled-input"
1122
+ :model-value="selectedField.field.disabled ?? ''"
1123
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'disabled')] ? 'true' : void 0"
1124
+ :placeholder="t('field-disabled-placeholder')"
1125
+ class="min-h-20 font-mono text-sm"
1126
+ @update:model-value="updateSelectedFieldDisabled"
1127
+ />
1128
+ <p
1129
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'disabled')]"
1130
+ class="text-xs text-red-500"
1131
+ >
1132
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "disabled")] }}
1133
+ </p>
1134
+ </label>
1135
+ </section>
1136
+
1137
+ <section
1138
+ v-if="selectedField.field.type === 'string'"
1139
+ data-slot="fields-configurator-string-options"
1140
+ class="grid gap-4 md:grid-cols-2"
1141
+ >
1142
+ <label class="flex items-center justify-between gap-3 rounded-md border border-zinc-200 px-3 py-2">
1143
+ <div class="flex flex-col gap-1">
1144
+ <span class="text-sm font-medium text-zinc-800">{{ t("field-discard-empty-string") }}</span>
1145
+ <span class="text-xs text-zinc-500">{{ t("field-discard-empty-string-description") }}</span>
1146
+ </div>
1147
+ <Switch
1148
+ data-slot="fields-configurator-field-discard-empty-switch"
1149
+ :model-value="selectedField.field.discardEmptyString ?? false"
1150
+ @update:model-value="updateSelectedStringDiscardEmpty"
1151
+ />
1152
+ </label>
1153
+
1154
+ <label class="flex flex-col gap-2">
1155
+ <span class="text-xs font-medium text-zinc-500">
1156
+ {{ t("field-max-length") }}
1157
+ </span>
1158
+ <Textarea
1159
+ data-slot="fields-configurator-field-max-length-input"
1160
+ :model-value="selectedField.field.maxLength ?? ''"
1161
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'maxLength')] ? 'true' : void 0"
1162
+ :placeholder="t('field-max-length-placeholder')"
1163
+ class="min-h-20 font-mono text-sm"
1164
+ @update:model-value="updateSelectedStringMaxLength"
1165
+ />
1166
+ <p
1167
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'maxLength')]"
1168
+ class="text-xs text-red-500"
1169
+ >
1170
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "maxLength")] }}
1171
+ </p>
1172
+ </label>
1173
+ </section>
1174
+
1175
+ <section
1176
+ v-if="selectedField.field.type === 'number'"
1177
+ data-slot="fields-configurator-number-options"
1178
+ class="grid gap-4 md:grid-cols-3"
1179
+ >
1180
+ <label class="flex flex-col gap-2">
1181
+ <span class="text-xs font-medium text-zinc-500">
1182
+ {{ t("field-min") }}
1183
+ </span>
1184
+ <Textarea
1185
+ data-slot="fields-configurator-field-min-input"
1186
+ :model-value="selectedField.field.min ?? ''"
1187
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'min')] ? 'true' : void 0"
1188
+ :placeholder="t('field-min-placeholder')"
1189
+ class="min-h-20 font-mono text-sm"
1190
+ @update:model-value="updateSelectedNumberMin"
1191
+ />
1192
+ <p
1193
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'min')]"
1194
+ class="text-xs text-red-500"
1195
+ >
1196
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "min")] }}
1197
+ </p>
1198
+ </label>
1199
+
1200
+ <label class="flex flex-col gap-2">
1201
+ <span class="text-xs font-medium text-zinc-500">
1202
+ {{ t("field-max") }}
1203
+ </span>
1204
+ <Textarea
1205
+ data-slot="fields-configurator-field-max-input"
1206
+ :model-value="selectedField.field.max ?? ''"
1207
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'max')] ? 'true' : void 0"
1208
+ :placeholder="t('field-max-placeholder')"
1209
+ class="min-h-20 font-mono text-sm"
1210
+ @update:model-value="updateSelectedNumberMax"
1211
+ />
1212
+ <p
1213
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'max')]"
1214
+ class="text-xs text-red-500"
1215
+ >
1216
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "max")] }}
1217
+ </p>
1218
+ </label>
1219
+
1220
+ <label class="flex flex-col gap-2">
1221
+ <span class="text-xs font-medium text-zinc-500">
1222
+ {{ t("field-step") }}
1223
+ </span>
1224
+ <Textarea
1225
+ data-slot="fields-configurator-field-step-input"
1226
+ :model-value="selectedField.field.step ?? ''"
1227
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'step')] ? 'true' : void 0"
1228
+ :placeholder="t('field-step-placeholder')"
1229
+ class="min-h-20 font-mono text-sm"
1230
+ @update:model-value="updateSelectedNumberStep"
1231
+ />
1232
+ <p
1233
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'step')]"
1234
+ class="text-xs text-red-500"
1235
+ >
1236
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "step")] }}
1237
+ </p>
1238
+ </label>
1239
+ </section>
1240
+
1241
+ <section
1242
+ v-if="selectedField.field.type === 'select'"
1243
+ data-slot="fields-configurator-select-options"
1244
+ class="grid gap-4 md:grid-cols-2"
1245
+ >
1246
+ <label class="flex flex-col gap-2">
1247
+ <span class="text-xs font-medium text-zinc-500">
1248
+ {{ t("field-options") }}
1249
+ </span>
1250
+ <Textarea
1251
+ data-slot="fields-configurator-field-options-input"
1252
+ :model-value="selectedField.field.options"
1253
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'options')] ? 'true' : void 0"
1254
+ :placeholder="t('field-options-placeholder')"
1255
+ class="min-h-20 font-mono text-sm"
1256
+ @update:model-value="updateSelectedSelectOptions"
1257
+ />
1258
+ <p
1259
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'options')]"
1260
+ class="text-xs text-red-500"
1261
+ >
1262
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "options")] }}
1263
+ </p>
1264
+ </label>
1265
+
1266
+ <label class="flex flex-col gap-2">
1267
+ <span class="text-xs font-medium text-zinc-500">
1268
+ {{ t("field-option-label") }}
1269
+ </span>
1270
+ <Textarea
1271
+ data-slot="fields-configurator-field-option-label-input"
1272
+ :model-value="selectedField.field.label"
1273
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'label')] ? 'true' : void 0"
1274
+ :placeholder="t('field-option-label-placeholder')"
1275
+ class="min-h-20 font-mono text-sm"
1276
+ @update:model-value="updateSelectedSelectLabel"
1277
+ />
1278
+ <p
1279
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'label')]"
1280
+ class="text-xs text-red-500"
1281
+ >
1282
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "label")] }}
1283
+ </p>
1284
+ </label>
1285
+
1286
+ <label class="flex flex-col gap-2">
1287
+ <span class="text-xs font-medium text-zinc-500">
1288
+ {{ t("field-option-value") }}
1289
+ </span>
1290
+ <Textarea
1291
+ data-slot="fields-configurator-field-option-value-input"
1292
+ :model-value="selectedField.field.value"
1293
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'value')] ? 'true' : void 0"
1294
+ :placeholder="t('field-option-value-placeholder')"
1295
+ class="min-h-20 font-mono text-sm"
1296
+ @update:model-value="updateSelectedSelectValue"
1297
+ />
1298
+ <p
1299
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'value')]"
1300
+ class="text-xs text-red-500"
1301
+ >
1302
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "value")] }}
1303
+ </p>
1304
+ </label>
1305
+
1306
+ <label class="flex flex-col gap-2">
1307
+ <span class="text-xs font-medium text-zinc-500">
1308
+ {{ t("field-option-key") }}
1309
+ </span>
1310
+ <Textarea
1311
+ data-slot="fields-configurator-field-option-key-input"
1312
+ :model-value="selectedField.field.key"
1313
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'key')] ? 'true' : void 0"
1314
+ :placeholder="t('field-option-key-placeholder')"
1315
+ class="min-h-20 font-mono text-sm"
1316
+ @update:model-value="updateSelectedSelectKey"
1317
+ />
1318
+ <p
1319
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'key')]"
1320
+ class="text-xs text-red-500"
1321
+ >
1322
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "key")] }}
1323
+ </p>
1324
+ </label>
1325
+ </section>
1326
+
1327
+ <section
1328
+ v-if="selectedField.field.type === 'calendar'"
1329
+ data-slot="fields-configurator-calendar-options"
1330
+ class="grid gap-4 md:grid-cols-2"
1331
+ >
1332
+ <label class="flex flex-col gap-2">
1333
+ <span class="text-xs font-medium text-zinc-500">
1334
+ {{ t("field-calendar-mode") }}
1335
+ </span>
1336
+ <NativeSelect
1337
+ data-slot="fields-configurator-field-calendar-mode-select"
1338
+ :model-value="selectedField.field.mode"
1339
+ @update:model-value="updateSelectedCalendarMode"
1340
+ >
1341
+ <NativeSelectOption value="year">
1342
+ {{ t("field-calendar-mode-year") }}
1343
+ </NativeSelectOption>
1344
+ <NativeSelectOption value="month">
1345
+ {{ t("field-calendar-mode-month") }}
1346
+ </NativeSelectOption>
1347
+ <NativeSelectOption value="date">
1348
+ {{ t("field-calendar-mode-date") }}
1349
+ </NativeSelectOption>
1350
+ </NativeSelect>
1351
+ </label>
1352
+
1353
+ <label class="flex flex-col gap-2">
1354
+ <span class="text-xs font-medium text-zinc-500">
1355
+ {{ t("field-calendar-display") }}
1356
+ </span>
1357
+ <Input
1358
+ data-slot="fields-configurator-field-calendar-display-input"
1359
+ :model-value="selectedField.field.display ?? ''"
1360
+ :placeholder="t('field-calendar-display-placeholder')"
1361
+ @update:model-value="updateSelectedCalendarDisplay"
1362
+ />
1363
+ </label>
1364
+
1365
+ <label class="flex flex-col gap-2">
1366
+ <span class="text-xs font-medium text-zinc-500">
1367
+ {{ t("field-calendar-value") }}
1368
+ </span>
1369
+ <Input
1370
+ data-slot="fields-configurator-field-calendar-value-input"
1371
+ :model-value="selectedField.field.value"
1372
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'value')] ? 'true' : void 0"
1373
+ :placeholder="t('field-calendar-value-placeholder')"
1374
+ @update:model-value="updateSelectedCalendarValue"
1375
+ />
1376
+ <p
1377
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'value')]"
1378
+ class="text-xs text-red-500"
1379
+ >
1380
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "value")] }}
1381
+ </p>
1382
+ </label>
1383
+
1384
+ <label class="flex flex-col gap-2">
1385
+ <span class="text-xs font-medium text-zinc-500">
1386
+ {{ t("field-disable-date") }}
1387
+ </span>
1388
+ <Textarea
1389
+ data-slot="fields-configurator-field-disable-date-input"
1390
+ :model-value="selectedField.field.disableDate ?? ''"
1391
+ :aria-invalid="validationErrors[getFieldErrorKey(selectedField.draftId, 'disableDate')] ? 'true' : void 0"
1392
+ :placeholder="t('field-disable-date-placeholder')"
1393
+ class="min-h-20 font-mono text-sm"
1394
+ @update:model-value="updateSelectedCalendarDisableDate"
1395
+ />
1396
+ <p
1397
+ v-if="validationErrors[getFieldErrorKey(selectedField.draftId, 'disableDate')]"
1398
+ class="text-xs text-red-500"
1399
+ >
1400
+ {{ validationErrors[getFieldErrorKey(selectedField.draftId, "disableDate")] }}
1401
+ </p>
1402
+ </label>
1403
+ </section>
1404
+
1405
+ <section
1406
+ data-slot="fields-configurator-validation"
1407
+ class="flex flex-col gap-4"
1408
+ >
1409
+ <div class="flex items-center justify-between gap-3">
1410
+ <div>
1411
+ <p class="text-xs font-medium text-zinc-500">
1412
+ {{ t("field-validation") }}
1413
+ </p>
1414
+ <p class="text-xs text-zinc-400">
1415
+ {{ t("field-validation-description") }}
1416
+ </p>
1417
+ </div>
1418
+
1419
+ <Button
1420
+ type="button"
1421
+ data-slot="fields-configurator-validation-add"
1422
+ size="sm"
1423
+ variant="default"
1424
+ @click="addValidationRule"
1425
+ >
1426
+ <Icon icon="fluent:add-20-regular" />
1427
+ {{ t("add-validation-rule") }}
1428
+ </Button>
1429
+ </div>
1430
+
1431
+ <div
1432
+ v-if="selectedFieldValidationRules.length > 0"
1433
+ class="flex flex-col gap-4"
1434
+ >
1435
+ <div
1436
+ v-for="(rule, index) in selectedFieldValidationRules"
1437
+ :key="`${selectedField.draftId}:${index}`"
1438
+ data-slot="fields-configurator-validation-rule"
1439
+ class="rounded-lg border border-zinc-200 p-4"
1440
+ >
1441
+ <div class="mb-3 flex items-center justify-between gap-3">
1442
+ <p class="text-sm font-medium text-zinc-700">
1443
+ {{ t("validation-rule-title", { index: index + 1 }) }}
1444
+ </p>
1445
+
1446
+ <div class="flex items-center gap-1">
1447
+ <Button
1448
+ type="button"
1449
+ size="xs"
1450
+ variant="ghost"
1451
+ data-slot="fields-configurator-validation-move-up"
1452
+ :disabled="index === 0"
1453
+ @click="moveValidationRule(index, -1)"
1454
+ >
1455
+ <Icon icon="fluent:arrow-up-20-regular" />
1456
+ </Button>
1457
+
1458
+ <Button
1459
+ type="button"
1460
+ size="xs"
1461
+ variant="ghost"
1462
+ data-slot="fields-configurator-validation-move-down"
1463
+ :disabled="index === selectedFieldValidationRules.length - 1"
1464
+ @click="moveValidationRule(index, 1)"
1465
+ >
1466
+ <Icon icon="fluent:arrow-down-20-regular" />
1467
+ </Button>
1468
+
1469
+ <Button
1470
+ type="button"
1471
+ size="xs"
1472
+ variant="ghost"
1473
+ data-slot="fields-configurator-validation-delete"
1474
+ @click="deleteValidationRule(index)"
1475
+ >
1476
+ <Icon icon="fluent:delete-20-regular" />
1477
+ </Button>
1478
+ </div>
1479
+ </div>
1480
+
1481
+ <div class="grid gap-4">
1482
+ <label class="flex flex-col gap-2">
1483
+ <span class="text-xs font-medium text-zinc-500">
1484
+ {{ t("validation-expression") }}
1485
+ </span>
1486
+ <Textarea
1487
+ data-slot="fields-configurator-validation-expression-input"
1488
+ :model-value="rule.expression"
1489
+ :aria-invalid="validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, 'expression')] ? 'true' : void 0"
1490
+ :placeholder="t('validation-expression-placeholder')"
1491
+ class="min-h-20 font-mono text-sm"
1492
+ @update:model-value="updateSelectedValidationExpression(index, $event)"
1493
+ />
1494
+ <p
1495
+ v-if="validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, 'expression')]"
1496
+ class="text-xs text-red-500"
1497
+ >
1498
+ {{ validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, "expression")] }}
1499
+ </p>
1500
+ </label>
1501
+
1502
+ <label class="flex flex-col gap-2">
1503
+ <span class="text-xs font-medium text-zinc-500">
1504
+ {{ t("validation-message") }}
1505
+ </span>
1506
+ <Textarea
1507
+ data-slot="fields-configurator-validation-message-input"
1508
+ :model-value="rule.message"
1509
+ :aria-invalid="validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, 'message')] ? 'true' : void 0"
1510
+ :placeholder="t('validation-message-placeholder')"
1511
+ class="min-h-20 text-sm"
1512
+ @update:model-value="updateSelectedValidationMessage(index, $event)"
1513
+ />
1514
+ <p
1515
+ v-if="validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, 'message')]"
1516
+ class="text-xs text-red-500"
1517
+ >
1518
+ {{ validationErrors[getValidationRuleErrorKey(selectedField.draftId, index, "message")] }}
1519
+ </p>
1520
+ </label>
1521
+ </div>
1522
+ </div>
1523
+ </div>
1524
+
1525
+ <p
1526
+ v-else
1527
+ data-slot="fields-configurator-validation-empty"
1528
+ class="rounded-lg border border-dashed border-zinc-200 px-4 py-6 text-sm text-zinc-400"
1529
+ >
1530
+ {{ t("no-validation-rules") }}
1531
+ </p>
306
1532
  </section>
307
1533
  </div>
308
1534
  </section>
@@ -337,10 +1563,68 @@ function confirmChanges() {
337
1563
  "zh": {
338
1564
  "configure-fields": "配置字段",
339
1565
  "configure-fields-description": "在这里浏览通用项和字段配置项。",
1566
+ "field-list": "字段列表",
340
1567
  "general": "通用",
341
- "general-placeholder": "这里会放置字段集合的通用配置。",
342
- "general-empty": "通用配置区域预留中。",
343
- "field-label": "Label",
1568
+ "general-description": "当前没有可保存的字段集合通用配置。",
1569
+ "general-empty": "字段集合的通用配置目前由宿主组件决定,这里只配置字段自身。",
1570
+ "add-field": "新增字段",
1571
+ "field-type": "字段类型",
1572
+ "field-type-string": "文本",
1573
+ "field-type-number": "数字",
1574
+ "field-type-select": "选择",
1575
+ "field-type-calendar": "日期",
1576
+ "field-path": "字段路径",
1577
+ "field-path-placeholder": "例如 profile.name",
1578
+ "field-path-required": "字段路径不能为空",
1579
+ "field-path-duplicate": "字段路径不能重复",
1580
+ "field-label": "字段标题",
1581
+ "field-icon": "图标",
1582
+ "field-icon-placeholder": "例如 fluent:person-20-regular",
1583
+ "field-style": "样式表达式",
1584
+ "field-style-placeholder": "例如 width: 100%",
1585
+ "field-hidden": "隐藏条件",
1586
+ "field-hidden-placeholder": "返回 true 时隐藏字段",
1587
+ "field-disabled": "禁用条件",
1588
+ "field-disabled-placeholder": "返回 true 时禁用字段",
1589
+ "field-discard-empty-string": "保留空字符串",
1590
+ "field-discard-empty-string-description": "开启后,清空输入时仍保存空字符串。",
1591
+ "field-max-length": "最大长度",
1592
+ "field-max-length-placeholder": "例如 100",
1593
+ "field-min": "最小值",
1594
+ "field-min-placeholder": "例如 0.0",
1595
+ "field-max": "最大值",
1596
+ "field-max-placeholder": "例如 100.0",
1597
+ "field-step": "步长",
1598
+ "field-step-placeholder": "例如 0.5",
1599
+ "field-options": "候选项表达式",
1600
+ "field-options-placeholder": "例如 users",
1601
+ "field-option-label": "选项标题表达式",
1602
+ "field-option-label-placeholder": "例如 option.name",
1603
+ "field-option-value": "选项值表达式",
1604
+ "field-option-value-placeholder": "例如 option.id",
1605
+ "field-option-key": "选项 key 表达式",
1606
+ "field-option-key-placeholder": "例如 string(option.id)",
1607
+ "field-calendar-mode": "日期精度",
1608
+ "field-calendar-mode-year": "年",
1609
+ "field-calendar-mode-month": "年月",
1610
+ "field-calendar-mode-date": "年月日",
1611
+ "field-calendar-display": "展示格式",
1612
+ "field-calendar-display-placeholder": "例如 yyyy年MM月dd日",
1613
+ "field-calendar-value": "存储格式",
1614
+ "field-calendar-value-placeholder": "例如 yyyy-MM-dd",
1615
+ "calendar-value-required": "日期存储格式不能为空",
1616
+ "field-disable-date": "禁用日期条件",
1617
+ "field-disable-date-placeholder": "返回 true 时禁用该日期",
1618
+ "field-validation": "校验规则",
1619
+ "field-validation-description": "字段失焦时按顺序执行,命中第一个失败规则后停止。",
1620
+ "add-validation-rule": "新增规则",
1621
+ "validation-rule-title": "规则 {index}",
1622
+ "validation-expression": "校验表达式",
1623
+ "validation-expression-placeholder": "返回 false 时展示下面的消息",
1624
+ "validation-message": "失败消息",
1625
+ "validation-message-placeholder": "支持 Markdown 与双花括号表达式",
1626
+ "no-validation-rules": "暂未配置校验规则。",
1627
+ "unnamed-field": "未命名{type}字段",
344
1628
  "no-fields": "还没有字段。",
345
1629
  "drag-field": "拖拽调整字段顺序:{field}",
346
1630
  "delete-field": "删除字段:{field}",
@@ -350,10 +1634,68 @@ function confirmChanges() {
350
1634
  "ja": {
351
1635
  "configure-fields": "フィールドを設定",
352
1636
  "configure-fields-description": "共通項目とフィールド設定をここで確認できます。",
1637
+ "field-list": "フィールド一覧",
353
1638
  "general": "共通",
354
- "general-placeholder": "ここにはフィールド群の共通設定を配置します。",
355
- "general-empty": "共通設定エリアはまだ準備中です。",
356
- "field-label": "Label",
1639
+ "general-description": "現在、保存できるフィールド群の共通設定はありません。",
1640
+ "general-empty": "フィールド群の共通設定はホスト側で管理され、この画面では各フィールドのみ編集できます。",
1641
+ "add-field": "フィールドを追加",
1642
+ "field-type": "フィールド種別",
1643
+ "field-type-string": "テキスト",
1644
+ "field-type-number": "数値",
1645
+ "field-type-select": "選択",
1646
+ "field-type-calendar": "日付",
1647
+ "field-path": "フィールドパス",
1648
+ "field-path-placeholder": "例: profile.name",
1649
+ "field-path-required": "フィールドパスは必須です",
1650
+ "field-path-duplicate": "フィールドパスは重複できません",
1651
+ "field-label": "フィールドラベル",
1652
+ "field-icon": "アイコン",
1653
+ "field-icon-placeholder": "例: fluent:person-20-regular",
1654
+ "field-style": "スタイル式",
1655
+ "field-style-placeholder": "例: width: 100%",
1656
+ "field-hidden": "非表示条件",
1657
+ "field-hidden-placeholder": "true を返すと非表示になります",
1658
+ "field-disabled": "無効化条件",
1659
+ "field-disabled-placeholder": "true を返すと無効になります",
1660
+ "field-discard-empty-string": "空文字列を保持",
1661
+ "field-discard-empty-string-description": "有効にすると、入力を空にしても空文字列を保存します。",
1662
+ "field-max-length": "最大長",
1663
+ "field-max-length-placeholder": "例: 100",
1664
+ "field-min": "最小値",
1665
+ "field-min-placeholder": "例: 0.0",
1666
+ "field-max": "最大値",
1667
+ "field-max-placeholder": "例: 100.0",
1668
+ "field-step": "ステップ",
1669
+ "field-step-placeholder": "例: 0.5",
1670
+ "field-options": "候補式",
1671
+ "field-options-placeholder": "例: users",
1672
+ "field-option-label": "候補ラベル式",
1673
+ "field-option-label-placeholder": "例: option.name",
1674
+ "field-option-value": "候補値式",
1675
+ "field-option-value-placeholder": "例: option.id",
1676
+ "field-option-key": "候補 key 式",
1677
+ "field-option-key-placeholder": "例: string(option.id)",
1678
+ "field-calendar-mode": "日付粒度",
1679
+ "field-calendar-mode-year": "年",
1680
+ "field-calendar-mode-month": "年月",
1681
+ "field-calendar-mode-date": "年月日",
1682
+ "field-calendar-display": "表示形式",
1683
+ "field-calendar-display-placeholder": "例: yyyy年MM月dd日",
1684
+ "field-calendar-value": "保存形式",
1685
+ "field-calendar-value-placeholder": "例: yyyy-MM-dd",
1686
+ "calendar-value-required": "日付の保存形式は必須です",
1687
+ "field-disable-date": "無効日条件",
1688
+ "field-disable-date-placeholder": "true を返す日付を無効化します",
1689
+ "field-validation": "検証ルール",
1690
+ "field-validation-description": "フォーカスを外したときに順番に評価し、最初の失敗で停止します。",
1691
+ "add-validation-rule": "ルールを追加",
1692
+ "validation-rule-title": "ルール {index}",
1693
+ "validation-expression": "検証式",
1694
+ "validation-expression-placeholder": "false を返すと下のメッセージを表示します",
1695
+ "validation-message": "失敗メッセージ",
1696
+ "validation-message-placeholder": "Markdown と二重波括弧式を利用できます",
1697
+ "no-validation-rules": "検証ルールはまだありません。",
1698
+ "unnamed-field": "未命名の{type}フィールド",
357
1699
  "no-fields": "フィールドがありません。",
358
1700
  "drag-field": "{field} の順序をドラッグで変更",
359
1701
  "delete-field": "{field} を削除",
@@ -363,10 +1705,68 @@ function confirmChanges() {
363
1705
  "en": {
364
1706
  "configure-fields": "Configure Fields",
365
1707
  "configure-fields-description": "Browse the shared and field-level settings here.",
1708
+ "field-list": "Field list",
366
1709
  "general": "General",
367
- "general-placeholder": "Shared settings for this field group will live here.",
368
- "general-empty": "The shared settings area is reserved for now.",
369
- "field-label": "Label",
1710
+ "general-description": "There is no shared field-group configuration to save here.",
1711
+ "general-empty": "Field-group level settings are owned by the host. This dialog only edits fields.",
1712
+ "add-field": "Add field",
1713
+ "field-type": "Field type",
1714
+ "field-type-string": "Text",
1715
+ "field-type-number": "Number",
1716
+ "field-type-select": "Select",
1717
+ "field-type-calendar": "Date",
1718
+ "field-path": "Field path",
1719
+ "field-path-placeholder": "For example profile.name",
1720
+ "field-path-required": "Field path is required",
1721
+ "field-path-duplicate": "Field path must be unique",
1722
+ "field-label": "Field label",
1723
+ "field-icon": "Icon",
1724
+ "field-icon-placeholder": "For example fluent:person-20-regular",
1725
+ "field-style": "Style expression",
1726
+ "field-style-placeholder": "For example width: 100%",
1727
+ "field-hidden": "Hidden expression",
1728
+ "field-hidden-placeholder": "Return true to hide the field",
1729
+ "field-disabled": "Disabled expression",
1730
+ "field-disabled-placeholder": "Return true to disable the field",
1731
+ "field-discard-empty-string": "Keep empty string",
1732
+ "field-discard-empty-string-description": "When enabled, clearing the input still stores an empty string.",
1733
+ "field-max-length": "Max length",
1734
+ "field-max-length-placeholder": "For example 100",
1735
+ "field-min": "Min",
1736
+ "field-min-placeholder": "For example 0.0",
1737
+ "field-max": "Max",
1738
+ "field-max-placeholder": "For example 100.0",
1739
+ "field-step": "Step",
1740
+ "field-step-placeholder": "For example 0.5",
1741
+ "field-options": "Options expression",
1742
+ "field-options-placeholder": "For example users",
1743
+ "field-option-label": "Option label expression",
1744
+ "field-option-label-placeholder": "For example option.name",
1745
+ "field-option-value": "Option value expression",
1746
+ "field-option-value-placeholder": "For example option.id",
1747
+ "field-option-key": "Option key expression",
1748
+ "field-option-key-placeholder": "For example string(option.id)",
1749
+ "field-calendar-mode": "Date precision",
1750
+ "field-calendar-mode-year": "Year",
1751
+ "field-calendar-mode-month": "Month",
1752
+ "field-calendar-mode-date": "Date",
1753
+ "field-calendar-display": "Display format",
1754
+ "field-calendar-display-placeholder": "For example yyyy-MM-dd",
1755
+ "field-calendar-value": "Storage format",
1756
+ "field-calendar-value-placeholder": "For example yyyy-MM-dd",
1757
+ "calendar-value-required": "Storage format is required",
1758
+ "field-disable-date": "Disabled date expression",
1759
+ "field-disable-date-placeholder": "Return true to disable the date",
1760
+ "field-validation": "Validation rules",
1761
+ "field-validation-description": "Rules run on blur in order and stop at the first failure.",
1762
+ "add-validation-rule": "Add rule",
1763
+ "validation-rule-title": "Rule {index}",
1764
+ "validation-expression": "Validation expression",
1765
+ "validation-expression-placeholder": "Return false to show the message below",
1766
+ "validation-message": "Failure message",
1767
+ "validation-message-placeholder": "Supports Markdown and double-curly expressions",
1768
+ "no-validation-rules": "No validation rules yet.",
1769
+ "unnamed-field": "Untitled {type} field",
370
1770
  "no-fields": "No fields yet.",
371
1771
  "drag-field": "Drag to reorder field {field}",
372
1772
  "delete-field": "Delete field {field}",