@antelopejs/dms 0.4.1 → 0.4.3
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/CHANGELOG.md +39 -0
- package/dist/pages/settings/users/invites.js +6 -5
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.d.ts +1 -0
- package/dist/pages/settings/users/members.js +12 -3
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/permission-tree-nodes.d.ts +3 -0
- package/dist/pages/settings/users/permission-tree-nodes.js +22 -0
- package/dist/pages/settings/users/permission-tree-nodes.js.map +1 -0
- package/dist/pages/settings/users/roles.js +2 -16
- package/dist/pages/settings/users/roles.js.map +1 -1
- package/dist/test/unit/pages/settings/permission-tree-nodes.test.d.ts +1 -0
- package/dist/test/unit/pages/settings/permission-tree-nodes.test.js +68 -0
- package/dist/test/unit/pages/settings/permission-tree-nodes.test.js.map +1 -0
- package/frontend-vue/dms.frontend.ts +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +4 -2
- package/frontend-vue/layers/dms-layout/app/composables/general/types/index.ts +7 -0
- package/frontend-vue/layers/dms-layout/app/composables/general/useColorModePreference.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/appearance.vue +6 -6
- package/frontend-vue/layers/dms-layout/app/plugins/color-mode.ts +100 -0
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +41 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/Calendar.vue +49 -26
- package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +77 -32
- package/frontend-vue/layers/dms-ui/app/composables/chart/useThemeRevision.ts +0 -22
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +23 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +7 -33
- package/frontend-vue/tests/color-mode-server-render.test.ts +84 -0
- package/frontend-vue/tests/color-mode-sync.test.ts +320 -0
- package/frontend-vue/tests/form-required-fields.test.ts +44 -0
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { FormProps } from "../../composables/form/types";
|
|
3
3
|
import type { FormFieldValue } from "../../composables/form/types/value";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
cloneFormValue,
|
|
6
|
+
isFieldMarkedRequired,
|
|
7
|
+
} from "../../composables/form/useForm";
|
|
5
8
|
import {
|
|
6
9
|
FORM_VALIDATOR_KEY,
|
|
7
10
|
type FormValidator,
|
|
@@ -38,6 +41,7 @@ const {
|
|
|
38
41
|
allFields,
|
|
39
42
|
disabledFields,
|
|
40
43
|
hiddenFields,
|
|
44
|
+
requiredFields,
|
|
41
45
|
isFieldGroup,
|
|
42
46
|
submitSucceeded,
|
|
43
47
|
resolvedFetchUrl,
|
|
@@ -200,6 +204,23 @@ function isGroupVisible(group: { fields: FormField[] }): boolean {
|
|
|
200
204
|
return group.fields.some((field) => !isFieldHidden(field));
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
function isFieldRequired(field: FormField): boolean {
|
|
208
|
+
return isFieldMarkedRequired(
|
|
209
|
+
field,
|
|
210
|
+
disabledFields.value,
|
|
211
|
+
hiddenFields.value,
|
|
212
|
+
requiredFields.value,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isGroupRequired(group: { fields: FormField[] }): boolean {
|
|
217
|
+
return group.fields.some(isFieldRequired);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const hasRequiredFields = computed(() =>
|
|
221
|
+
toValue(allFields).some(isFieldRequired),
|
|
222
|
+
);
|
|
223
|
+
|
|
203
224
|
function shouldShowDisplay(field: FormField): boolean {
|
|
204
225
|
return isFieldDisabled(field) && !!field.type;
|
|
205
226
|
}
|
|
@@ -379,6 +400,13 @@ onUnmounted(async () => {
|
|
|
379
400
|
class="text-default block text-base font-semibold sm:text-sm"
|
|
380
401
|
>
|
|
381
402
|
{{ processI18n(item.label || "") }}
|
|
403
|
+
<span
|
|
404
|
+
v-if="isGroupRequired(item)"
|
|
405
|
+
class="text-error ms-0.5"
|
|
406
|
+
aria-hidden="true"
|
|
407
|
+
>
|
|
408
|
+
*
|
|
409
|
+
</span>
|
|
382
410
|
</label>
|
|
383
411
|
<p v-if="item.description" class="text-dimmed text-xs">
|
|
384
412
|
{{ processI18n(item.description || "") }}
|
|
@@ -453,6 +481,13 @@ onUnmounted(async () => {
|
|
|
453
481
|
class="text-default block text-base font-semibold sm:text-sm"
|
|
454
482
|
>
|
|
455
483
|
{{ processI18n(item.label || "") }}
|
|
484
|
+
<span
|
|
485
|
+
v-if="isFieldRequired(item)"
|
|
486
|
+
class="text-error ms-0.5"
|
|
487
|
+
aria-hidden="true"
|
|
488
|
+
>
|
|
489
|
+
*
|
|
490
|
+
</span>
|
|
456
491
|
</label>
|
|
457
492
|
<p v-if="item.description" class="text-dimmed text-xs">
|
|
458
493
|
{{ processI18n(item.description || "") }}
|
|
@@ -509,6 +544,11 @@ onUnmounted(async () => {
|
|
|
509
544
|
</template>
|
|
510
545
|
</div>
|
|
511
546
|
|
|
547
|
+
<p v-if="hasRequiredFields" class="text-dimmed mt-6 text-xs">
|
|
548
|
+
<span class="text-error" aria-hidden="true">*</span>
|
|
549
|
+
{{ $t("dms.form.required_legend") }}
|
|
550
|
+
</p>
|
|
551
|
+
|
|
512
552
|
<template v-if="showActions">
|
|
513
553
|
<section class="mt-6 flex justify-end gap-2">
|
|
514
554
|
<UButton
|
|
@@ -11,27 +11,39 @@ import {
|
|
|
11
11
|
type CalendarDate,
|
|
12
12
|
} from "@internationalized/date";
|
|
13
13
|
|
|
14
|
+
// Mid-selection, the range calendar emits a range whose end is still unset.
|
|
14
15
|
interface StrictDateRange {
|
|
15
|
-
start: CalendarDate;
|
|
16
|
-
end: CalendarDate;
|
|
16
|
+
start: CalendarDate | undefined;
|
|
17
|
+
end: CalendarDate | undefined;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
interface StringDateRange {
|
|
20
|
-
start: string;
|
|
21
|
-
end: string;
|
|
21
|
+
start: string | undefined;
|
|
22
|
+
end: string | undefined;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface NativeDateRange {
|
|
25
|
-
start: Date;
|
|
26
|
-
end: Date;
|
|
26
|
+
start: Date | undefined;
|
|
27
|
+
end: Date | undefined;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
type DateValue =
|
|
30
|
+
type DateValue =
|
|
31
|
+
| CalendarDate
|
|
32
|
+
| StrictDateRange
|
|
33
|
+
| string
|
|
34
|
+
| (string | Date | CalendarDate)[]
|
|
35
|
+
| null
|
|
36
|
+
| undefined;
|
|
30
37
|
|
|
31
38
|
const props = defineProps<CalendarProps<R, M>>();
|
|
32
39
|
const emits = defineEmits<{
|
|
33
40
|
"update:modelValue": [
|
|
34
|
-
value:
|
|
41
|
+
value:
|
|
42
|
+
| string
|
|
43
|
+
| { start: string | undefined; end: string | undefined }
|
|
44
|
+
| string[]
|
|
45
|
+
| null
|
|
46
|
+
| undefined,
|
|
35
47
|
];
|
|
36
48
|
"update:placeholder": [date: CalendarDate];
|
|
37
49
|
"update:startValue": [date: CalendarDate | undefined];
|
|
@@ -42,22 +54,25 @@ function convertIsoStringToCalendarDate(value: string): CalendarDate {
|
|
|
42
54
|
return parseDate(value.split("T")[0]!);
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
):
|
|
48
|
-
if (isString(
|
|
49
|
-
return
|
|
50
|
-
start: convertIsoStringToCalendarDate(rangeValue.start),
|
|
51
|
-
end: convertIsoStringToCalendarDate(rangeValue.end),
|
|
52
|
-
};
|
|
57
|
+
function convertToCalendarDate(
|
|
58
|
+
value: string | Date | CalendarDate | undefined,
|
|
59
|
+
): CalendarDate | undefined {
|
|
60
|
+
if (isString(value)) {
|
|
61
|
+
return convertIsoStringToCalendarDate(value);
|
|
53
62
|
}
|
|
54
|
-
if (
|
|
55
|
-
return
|
|
56
|
-
start: toCalendarDate(fromDate(rangeValue.start, "UTC")),
|
|
57
|
-
end: toCalendarDate(fromDate(rangeValue.end, "UTC")),
|
|
58
|
-
};
|
|
63
|
+
if (value instanceof Date) {
|
|
64
|
+
return toCalendarDate(fromDate(value, "UTC"));
|
|
59
65
|
}
|
|
60
|
-
return
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function convertDateRangeValue(
|
|
70
|
+
rangeValue: StringDateRange | StrictDateRange | NativeDateRange,
|
|
71
|
+
): StrictDateRange {
|
|
72
|
+
return {
|
|
73
|
+
start: convertToCalendarDate(rangeValue.start),
|
|
74
|
+
end: convertToCalendarDate(rangeValue.end),
|
|
75
|
+
};
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
function isDateRangeValue(
|
|
@@ -73,7 +88,10 @@ function convertModelValue(value: DateValue): DateValue {
|
|
|
73
88
|
return convertIsoStringToCalendarDate(value);
|
|
74
89
|
}
|
|
75
90
|
if (isDateRangeValue(value)) {
|
|
76
|
-
return convertDateRangeValue(value)
|
|
91
|
+
return convertDateRangeValue(value);
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.map(convertToCalendarDate) as CalendarDate[];
|
|
77
95
|
}
|
|
78
96
|
return value;
|
|
79
97
|
}
|
|
@@ -88,7 +106,12 @@ type EmitValue = unknown;
|
|
|
88
106
|
|
|
89
107
|
const wrappedEmits = (event: EmitEvent, value: EmitValue) => {
|
|
90
108
|
if (event === "update:modelValue" && value) {
|
|
91
|
-
if (
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
const isoStrings = (value as CalendarDate[]).map((date) =>
|
|
111
|
+
date.toString(),
|
|
112
|
+
);
|
|
113
|
+
emits("update:modelValue", isoStrings);
|
|
114
|
+
} else if (
|
|
92
115
|
isObject(value) &&
|
|
93
116
|
"year" in value &&
|
|
94
117
|
"month" in value &&
|
|
@@ -98,8 +121,8 @@ const wrappedEmits = (event: EmitEvent, value: EmitValue) => {
|
|
|
98
121
|
emits("update:modelValue", isoString);
|
|
99
122
|
} else if (isObject(value) && "start" in value && "end" in value) {
|
|
100
123
|
const dateRange = value as unknown as StrictDateRange;
|
|
101
|
-
const startIso = dateRange.start
|
|
102
|
-
const endIso = dateRange.end
|
|
124
|
+
const startIso = dateRange.start?.toString();
|
|
125
|
+
const endIso = dateRange.end?.toString();
|
|
103
126
|
emits("update:modelValue", { start: startIso, end: endIso });
|
|
104
127
|
} else {
|
|
105
128
|
emits("update:modelValue", value as string | null | undefined);
|
|
@@ -13,29 +13,41 @@ import {
|
|
|
13
13
|
} from "@internationalized/date";
|
|
14
14
|
import { reactivePick } from "@vueuse/core";
|
|
15
15
|
|
|
16
|
+
// Mid-selection, the range calendar emits a range whose end is still unset.
|
|
16
17
|
interface StrictDateRange {
|
|
17
|
-
start: CalendarDate;
|
|
18
|
-
end: CalendarDate;
|
|
18
|
+
start: CalendarDate | undefined;
|
|
19
|
+
end: CalendarDate | undefined;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
interface StringDateRange {
|
|
22
|
-
start: string;
|
|
23
|
-
end: string;
|
|
23
|
+
start: string | undefined;
|
|
24
|
+
end: string | undefined;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface NativeDateRange {
|
|
27
|
-
start: Date;
|
|
28
|
-
end: Date;
|
|
28
|
+
start: Date | undefined;
|
|
29
|
+
end: Date | undefined;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
type DateValue =
|
|
32
|
+
type DateValue =
|
|
33
|
+
| CalendarDate
|
|
34
|
+
| StrictDateRange
|
|
35
|
+
| string
|
|
36
|
+
| (string | Date | CalendarDate)[]
|
|
37
|
+
| null
|
|
38
|
+
| undefined;
|
|
32
39
|
|
|
33
40
|
type DatePickerProps = Omit<CalendarProps<R, M>, "modelValue">;
|
|
34
41
|
|
|
35
42
|
const props = defineProps<DatePickerProps>();
|
|
36
43
|
const emits = defineEmits<{
|
|
37
44
|
"update:modelValue": [
|
|
38
|
-
value:
|
|
45
|
+
value:
|
|
46
|
+
| string
|
|
47
|
+
| { start: string | undefined; end: string | undefined }
|
|
48
|
+
| string[]
|
|
49
|
+
| null
|
|
50
|
+
| undefined,
|
|
39
51
|
];
|
|
40
52
|
"update:placeholder": [date: CalendarDate];
|
|
41
53
|
"update:startValue": [date: CalendarDate | undefined];
|
|
@@ -50,22 +62,25 @@ function convertIsoStringToCalendarDate(value: string): CalendarDate {
|
|
|
50
62
|
return parseDate(value.split("T")[0]!);
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
):
|
|
56
|
-
if (isString(
|
|
57
|
-
return
|
|
58
|
-
start: convertIsoStringToCalendarDate(rangeValue.start),
|
|
59
|
-
end: convertIsoStringToCalendarDate(rangeValue.end),
|
|
60
|
-
};
|
|
65
|
+
function convertToCalendarDate(
|
|
66
|
+
value: string | Date | CalendarDate | undefined,
|
|
67
|
+
): CalendarDate | undefined {
|
|
68
|
+
if (isString(value)) {
|
|
69
|
+
return convertIsoStringToCalendarDate(value);
|
|
61
70
|
}
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
64
|
-
start: toCalendarDate(fromDate(rangeValue.start, "UTC")),
|
|
65
|
-
end: toCalendarDate(fromDate(rangeValue.end, "UTC")),
|
|
66
|
-
};
|
|
71
|
+
if (value instanceof Date) {
|
|
72
|
+
return toCalendarDate(fromDate(value, "UTC"));
|
|
67
73
|
}
|
|
68
|
-
return
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function convertDateRangeValue(
|
|
78
|
+
rangeValue: StringDateRange | StrictDateRange | NativeDateRange,
|
|
79
|
+
): StrictDateRange {
|
|
80
|
+
return {
|
|
81
|
+
start: convertToCalendarDate(rangeValue.start),
|
|
82
|
+
end: convertToCalendarDate(rangeValue.end),
|
|
83
|
+
};
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
function isDateRangeValue(
|
|
@@ -81,7 +96,10 @@ function convertModelValue(value: DateValue): DateValue {
|
|
|
81
96
|
return convertIsoStringToCalendarDate(value);
|
|
82
97
|
}
|
|
83
98
|
if (isDateRangeValue(value)) {
|
|
84
|
-
return convertDateRangeValue(value)
|
|
99
|
+
return convertDateRangeValue(value);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return value.map(convertToCalendarDate) as CalendarDate[];
|
|
85
103
|
}
|
|
86
104
|
return value;
|
|
87
105
|
}
|
|
@@ -96,7 +114,12 @@ type EmitValue = unknown;
|
|
|
96
114
|
|
|
97
115
|
const wrappedEmits = (event: EmitEvent, value: EmitValue) => {
|
|
98
116
|
if (event === "update:modelValue" && value) {
|
|
99
|
-
if (
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
const isoStrings = (value as CalendarDate[]).map((date) =>
|
|
119
|
+
date.toString(),
|
|
120
|
+
);
|
|
121
|
+
emits("update:modelValue", isoStrings);
|
|
122
|
+
} else if (
|
|
100
123
|
isObject(value) &&
|
|
101
124
|
"year" in value &&
|
|
102
125
|
"month" in value &&
|
|
@@ -106,8 +129,8 @@ const wrappedEmits = (event: EmitEvent, value: EmitValue) => {
|
|
|
106
129
|
emits("update:modelValue", isoString);
|
|
107
130
|
} else if (isObject(value) && "start" in value && "end" in value) {
|
|
108
131
|
const dateRange = value as unknown as StrictDateRange;
|
|
109
|
-
const startIso = dateRange.start
|
|
110
|
-
const endIso = dateRange.end
|
|
132
|
+
const startIso = dateRange.start?.toString();
|
|
133
|
+
const endIso = dateRange.end?.toString();
|
|
111
134
|
emits("update:modelValue", { start: startIso, end: endIso });
|
|
112
135
|
} else {
|
|
113
136
|
emits("update:modelValue", value as string | null | undefined);
|
|
@@ -123,18 +146,40 @@ const forwarded = useForwardPropsEmits(
|
|
|
123
146
|
wrappedEmits as typeof emits,
|
|
124
147
|
);
|
|
125
148
|
const buttonProps = reactivePick(props, "disabled");
|
|
149
|
+
|
|
150
|
+
function formatDateLabel(value: unknown): string | undefined {
|
|
151
|
+
if (value === undefined || value === null) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
// A CalendarDate stringifies to its ISO date.
|
|
155
|
+
const date = value instanceof Date ? value : String(value);
|
|
156
|
+
return formatDate(date, locale.value) ?? undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Range mode holds a { start, end } object and multiple mode an array, which
|
|
160
|
+
// formatDate cannot read as a single date.
|
|
161
|
+
const label = computed(() => {
|
|
162
|
+
const value: unknown = modelValue.value;
|
|
163
|
+
if (Array.isArray(value)) {
|
|
164
|
+
return value.map(formatDateLabel).filter(Boolean).join(", ") || undefined;
|
|
165
|
+
}
|
|
166
|
+
if (isDateRangeValue(value)) {
|
|
167
|
+
return (
|
|
168
|
+
[formatDateLabel(value.start), formatDateLabel(value.end)]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join(" – ") || undefined
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return formatDateLabel(value);
|
|
174
|
+
});
|
|
126
175
|
</script>
|
|
127
176
|
|
|
128
177
|
<template>
|
|
129
178
|
<UPopover>
|
|
130
179
|
<UButton
|
|
131
|
-
:label="
|
|
132
|
-
modelValue
|
|
133
|
-
? (formatDate(modelValue, locale) ?? undefined)
|
|
134
|
-
: t('dms.form.select_date')
|
|
135
|
-
"
|
|
180
|
+
:label="label ?? t('dms.form.select_date')"
|
|
136
181
|
:ui="{
|
|
137
|
-
label:
|
|
182
|
+
label: label ? 'text-default' : 'text-dimmed',
|
|
138
183
|
}"
|
|
139
184
|
v-bind="buttonProps"
|
|
140
185
|
variant="outline"
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
import { onBeforeUnmount, onMounted, ref, type Ref } from "vue";
|
|
2
2
|
import { clearChartColorCache } from "./useChartTheme";
|
|
3
3
|
|
|
4
|
-
interface ColorModeLike {
|
|
5
|
-
preference?: string;
|
|
6
|
-
value?: string;
|
|
7
|
-
$subscribe?: (cb: () => void) => () => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type ColorModeFactory = () => ColorModeLike;
|
|
11
|
-
|
|
12
4
|
const THEME_OBSERVED_ATTRIBUTES = ["class", "data-theme", "style"];
|
|
13
5
|
|
|
14
|
-
function getColorModeFactory(): ColorModeFactory | undefined {
|
|
15
|
-
const fn = (globalThis as unknown as { useColorMode?: ColorModeFactory })
|
|
16
|
-
.useColorMode;
|
|
17
|
-
return typeof fn === "function" ? fn : undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function trySubscribeColorMode(bump: () => void) {
|
|
21
|
-
const factory = getColorModeFactory();
|
|
22
|
-
if (!factory) return;
|
|
23
|
-
const mode = factory();
|
|
24
|
-
mode?.$subscribe?.(bump);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
6
|
function createThemeObserver(bump: () => void): MutationObserver {
|
|
28
7
|
const observer = new MutationObserver(bump);
|
|
29
8
|
observer.observe(document.documentElement, {
|
|
@@ -47,7 +26,6 @@ export function useThemeRevision(): Ref<number> {
|
|
|
47
26
|
|
|
48
27
|
onMounted(() => {
|
|
49
28
|
observer = createThemeObserver(bump);
|
|
50
|
-
trySubscribeColorMode(bump);
|
|
51
29
|
});
|
|
52
30
|
|
|
53
31
|
onBeforeUnmount(() => {
|
|
@@ -258,6 +258,28 @@ export function buildValidationSchema(
|
|
|
258
258
|
return z.object(newShape).passthrough();
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// A switch always holds a value (off is `false`): it can never be left
|
|
262
|
+
// missing, so marking it required would only suggest an action that does not
|
|
263
|
+
// exist.
|
|
264
|
+
const ALWAYS_FILLED_FIELD_TYPES = new Set(["boolean"]);
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Whether the form marks a field as required: declared so, or made so by a
|
|
268
|
+
* watch action, and neither disabled nor hidden, since `buildValidationSchema`
|
|
269
|
+
* does not validate inactive fields, nor a type that always holds a value.
|
|
270
|
+
*/
|
|
271
|
+
export function isFieldMarkedRequired(
|
|
272
|
+
field: Pick<FormField, "id" | "required" | "disabled" | "type">,
|
|
273
|
+
disabled: Set<string> | undefined,
|
|
274
|
+
hidden: Set<string> | undefined,
|
|
275
|
+
required: Set<string> | undefined,
|
|
276
|
+
): boolean {
|
|
277
|
+
if (field.disabled || disabled?.has(field.id) || hidden?.has(field.id))
|
|
278
|
+
return false;
|
|
279
|
+
if (field.type && ALWAYS_FILLED_FIELD_TYPES.has(field.type)) return false;
|
|
280
|
+
return !!field.required || (required?.has(field.id) ?? false);
|
|
281
|
+
}
|
|
282
|
+
|
|
261
283
|
function watchFieldChanges(
|
|
262
284
|
state: Ref<Record<string, unknown>>,
|
|
263
285
|
componentId: string | undefined,
|
|
@@ -546,6 +568,7 @@ export const useForm = (props: FormProps) => {
|
|
|
546
568
|
allFields,
|
|
547
569
|
disabledFields,
|
|
548
570
|
hiddenFields,
|
|
571
|
+
requiredFields,
|
|
549
572
|
isFieldGroup,
|
|
550
573
|
submitSucceeded,
|
|
551
574
|
resolvedFetchUrl,
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
"row_added_success": "The item has been successfully added",
|
|
170
170
|
"error_missing_fields": "Missing required fields",
|
|
171
171
|
"error_fill_all_fields": "Please fill in all required fields",
|
|
172
|
+
"required_legend": "Required fields",
|
|
172
173
|
"fetch_error_title": "Failed to fetch data",
|
|
173
174
|
"fetch_error_unknown": "An unexpected error occurred. Please try again later.",
|
|
174
175
|
"select_date": "Select date",
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
"row_added_success": "L'élément a été ajouté avec succès",
|
|
170
170
|
"error_missing_fields": "Champs requis manquants",
|
|
171
171
|
"error_fill_all_fields": "Veuillez remplir tous les champs requis",
|
|
172
|
+
"required_legend": "Champs requis",
|
|
172
173
|
"error_not_submitted": "Le formulaire n'a pas été soumis",
|
|
173
174
|
"fetch_error_title": "Échec de la récupération des données",
|
|
174
175
|
"fetch_error_unknown": "Une erreur inattendue s'est produite. Veuillez réessayer plus tard.",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@vue-flow/controls": "^1.1.3",
|
|
40
40
|
"@vue-flow/core": "^1.48.2",
|
|
41
41
|
"@vue-flow/minimap": "^1.5.4",
|
|
42
|
-
"@vueuse/core": "^
|
|
42
|
+
"@vueuse/core": "^14.3.0",
|
|
43
43
|
"@vueuse/integrations": "^14.1.0",
|
|
44
44
|
"apexcharts": "^4.4.0",
|
|
45
45
|
"defu": "^6.1.4",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@antelopejs/core": ">=1.9.1 <2",
|
|
62
|
-
"@antelopejs/dms-frontend": ">=0.2.
|
|
62
|
+
"@antelopejs/dms-frontend": ">=0.2.8 <1.0.0",
|
|
63
63
|
"@eslint/js": "^8.57.0",
|
|
64
64
|
"@inertiajs/vue3": "^3.7.0",
|
|
65
65
|
"@nuxt/ui": "4.10.0",
|
|
@@ -66,8 +66,8 @@ importers:
|
|
|
66
66
|
specifier: ^1.5.4
|
|
67
67
|
version: 1.5.4(@vue-flow/core@1.48.2(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
|
|
68
68
|
'@vueuse/core':
|
|
69
|
-
specifier: ^
|
|
70
|
-
version:
|
|
69
|
+
specifier: ^14.3.0
|
|
70
|
+
version: 14.4.0(vue@3.5.25(typescript@5.9.3))
|
|
71
71
|
'@vueuse/integrations':
|
|
72
72
|
specifier: ^14.1.0
|
|
73
73
|
version: 14.1.0(focus-trap@7.8.0)(fuse.js@7.5.0)(sortablejs@1.14.0)(vue@3.5.25(typescript@5.9.3))
|
|
@@ -124,8 +124,8 @@ importers:
|
|
|
124
124
|
specifier: '>=1.9.1 <2'
|
|
125
125
|
version: 1.9.1(@types/node@24.8.1)
|
|
126
126
|
'@antelopejs/dms-frontend':
|
|
127
|
-
specifier: '>=0.2.
|
|
128
|
-
version: 0.2.
|
|
127
|
+
specifier: '>=0.2.8 <1.0.0'
|
|
128
|
+
version: 0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))
|
|
129
129
|
'@eslint/js':
|
|
130
130
|
specifier: ^8.57.0
|
|
131
131
|
version: 8.57.0
|
|
@@ -200,8 +200,8 @@ packages:
|
|
|
200
200
|
resolution: {integrity: sha512-MPLJak0rPluV4lgsdsO5ba03sgTxIGSncajBcoaoppuppQmksdpaemojIHiVLJ4LiG0iiH21Roaed8XXy1XkYQ==}
|
|
201
201
|
hasBin: true
|
|
202
202
|
|
|
203
|
-
'@antelopejs/dms-frontend@0.2.
|
|
204
|
-
resolution: {integrity: sha512-
|
|
203
|
+
'@antelopejs/dms-frontend@0.2.8':
|
|
204
|
+
resolution: {integrity: sha512-7qrlf2M3J26DZxGZVvM/3qjPDeDfoRMrE7HEPFxWav8Pe1QFct7LtgY7NmKup9XdrRyUw4nqckxvW8UZDSjpeg==}
|
|
205
205
|
engines: {node: '>=20.12.0'}
|
|
206
206
|
hasBin: true
|
|
207
207
|
peerDependencies:
|
|
@@ -1753,11 +1753,6 @@ packages:
|
|
|
1753
1753
|
'@vueuse/core@12.8.2':
|
|
1754
1754
|
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
|
1755
1755
|
|
|
1756
|
-
'@vueuse/core@13.9.0':
|
|
1757
|
-
resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==}
|
|
1758
|
-
peerDependencies:
|
|
1759
|
-
vue: ^3.5.0
|
|
1760
|
-
|
|
1761
1756
|
'@vueuse/core@14.1.0':
|
|
1762
1757
|
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
|
|
1763
1758
|
peerDependencies:
|
|
@@ -1858,9 +1853,6 @@ packages:
|
|
|
1858
1853
|
'@vueuse/metadata@12.8.2':
|
|
1859
1854
|
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
|
1860
1855
|
|
|
1861
|
-
'@vueuse/metadata@13.9.0':
|
|
1862
|
-
resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==}
|
|
1863
|
-
|
|
1864
1856
|
'@vueuse/metadata@14.1.0':
|
|
1865
1857
|
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
|
|
1866
1858
|
|
|
@@ -1873,11 +1865,6 @@ packages:
|
|
|
1873
1865
|
'@vueuse/shared@12.8.2':
|
|
1874
1866
|
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
|
1875
1867
|
|
|
1876
|
-
'@vueuse/shared@13.9.0':
|
|
1877
|
-
resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==}
|
|
1878
|
-
peerDependencies:
|
|
1879
|
-
vue: ^3.5.0
|
|
1880
|
-
|
|
1881
1868
|
'@vueuse/shared@14.1.0':
|
|
1882
1869
|
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
|
|
1883
1870
|
peerDependencies:
|
|
@@ -4344,7 +4331,7 @@ snapshots:
|
|
|
4344
4331
|
transitivePeerDependencies:
|
|
4345
4332
|
- '@types/node'
|
|
4346
4333
|
|
|
4347
|
-
'@antelopejs/dms-frontend@0.2.
|
|
4334
|
+
'@antelopejs/dms-frontend@0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))':
|
|
4348
4335
|
dependencies:
|
|
4349
4336
|
boxen: 5.1.2
|
|
4350
4337
|
chalk: 4.1.2
|
|
@@ -5944,13 +5931,6 @@ snapshots:
|
|
|
5944
5931
|
transitivePeerDependencies:
|
|
5945
5932
|
- typescript
|
|
5946
5933
|
|
|
5947
|
-
'@vueuse/core@13.9.0(vue@3.5.25(typescript@5.9.3))':
|
|
5948
|
-
dependencies:
|
|
5949
|
-
'@types/web-bluetooth': 0.0.21
|
|
5950
|
-
'@vueuse/metadata': 13.9.0
|
|
5951
|
-
'@vueuse/shared': 13.9.0(vue@3.5.25(typescript@5.9.3))
|
|
5952
|
-
vue: 3.5.25(typescript@5.9.3)
|
|
5953
|
-
|
|
5954
5934
|
'@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))':
|
|
5955
5935
|
dependencies:
|
|
5956
5936
|
'@types/web-bluetooth': 0.0.21
|
|
@@ -5989,8 +5969,6 @@ snapshots:
|
|
|
5989
5969
|
|
|
5990
5970
|
'@vueuse/metadata@12.8.2': {}
|
|
5991
5971
|
|
|
5992
|
-
'@vueuse/metadata@13.9.0': {}
|
|
5993
|
-
|
|
5994
5972
|
'@vueuse/metadata@14.1.0': {}
|
|
5995
5973
|
|
|
5996
5974
|
'@vueuse/metadata@14.4.0': {}
|
|
@@ -6008,10 +5986,6 @@ snapshots:
|
|
|
6008
5986
|
transitivePeerDependencies:
|
|
6009
5987
|
- typescript
|
|
6010
5988
|
|
|
6011
|
-
'@vueuse/shared@13.9.0(vue@3.5.25(typescript@5.9.3))':
|
|
6012
|
-
dependencies:
|
|
6013
|
-
vue: 3.5.25(typescript@5.9.3)
|
|
6014
|
-
|
|
6015
5989
|
'@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))':
|
|
6016
5990
|
dependencies:
|
|
6017
5991
|
vue: 3.5.25(typescript@5.9.3)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The color-mode plugin as the server runs it: the `<html>` tag of the rendered
|
|
3
|
+
* document already carries an explicit preference, so the first paint is right
|
|
4
|
+
* without waiting for JavaScript, and `system` is left to the pre-paint script.
|
|
5
|
+
*/
|
|
6
|
+
import { useHead } from "@unhead/vue";
|
|
7
|
+
import { createHead, renderSSRHead } from "@unhead/vue/server";
|
|
8
|
+
import { createSSRApp, ref } from "vue";
|
|
9
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
interface CookieOptions {
|
|
12
|
+
default: () => unknown;
|
|
13
|
+
maxAge?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const COLOR_MODE_COOKIE = "dms-color-mode";
|
|
17
|
+
const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
|
|
18
|
+
const requestCookies = new Map<string, unknown>();
|
|
19
|
+
const cookieReads = vi.fn();
|
|
20
|
+
|
|
21
|
+
vi.stubGlobal("defineDmsPlugin", (setup: unknown) => setup);
|
|
22
|
+
vi.stubGlobal("useHead", useHead);
|
|
23
|
+
vi.stubGlobal("useDmsCookie", (name: string, options: CookieOptions) => {
|
|
24
|
+
cookieReads(name, options);
|
|
25
|
+
return ref(
|
|
26
|
+
requestCookies.has(name) ? requestCookies.get(name) : options.default(),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { default: colorModePlugin } = await import(
|
|
31
|
+
"../layers/dms-layout/app/plugins/color-mode"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
async function renderHead(preference?: string) {
|
|
35
|
+
if (preference !== undefined)
|
|
36
|
+
requestCookies.set(COLOR_MODE_COOKIE, preference);
|
|
37
|
+
const app = createSSRApp({ render: () => null });
|
|
38
|
+
const head = createHead();
|
|
39
|
+
app.use(head);
|
|
40
|
+
await app.runWithContext(() => colorModePlugin({} as never));
|
|
41
|
+
return renderSSRHead(head);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
requestCookies.clear();
|
|
46
|
+
cookieReads.mockClear();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each(["light", "dark"])(
|
|
50
|
+
"renders the %s class on <html> for an explicit preference",
|
|
51
|
+
async (preference) => {
|
|
52
|
+
const { htmlAttrs } = await renderHead(preference);
|
|
53
|
+
|
|
54
|
+
expect(htmlAttrs).toContain(` class="${preference}"`);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
it.each([undefined, "system", "sepia"])(
|
|
59
|
+
"leaves the class to the browser for a %s preference",
|
|
60
|
+
async (preference) => {
|
|
61
|
+
const { htmlAttrs } = await renderHead(preference);
|
|
62
|
+
|
|
63
|
+
expect(htmlAttrs).not.toContain("class");
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
it("reads the dms-color-mode cookie, kept a year and defaulting to system", async () => {
|
|
68
|
+
await renderHead();
|
|
69
|
+
|
|
70
|
+
expect(cookieReads).toHaveBeenCalledOnce();
|
|
71
|
+
const [name, options] = cookieReads.mock.calls[0] as [string, CookieOptions];
|
|
72
|
+
expect(name).toBe(COLOR_MODE_COOKIE);
|
|
73
|
+
expect(options.maxAge).toBe(ONE_YEAR_IN_SECONDS);
|
|
74
|
+
expect(options.default()).toBe("system");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ships the pre-paint script as a classic inline script in <head>", async () => {
|
|
78
|
+
// No src, type, defer or async: the browser runs it while it parses <head>,
|
|
79
|
+
// before the deferred entry module that installs Nuxt UI.
|
|
80
|
+
const { headTags, bodyTags, bodyTagsOpen } = await renderHead("dark");
|
|
81
|
+
|
|
82
|
+
expect(headTags).toMatch(/<script id="dms-color-mode">\(\(\) => \{/);
|
|
83
|
+
expect(bodyTags + bodyTagsOpen).toBe("");
|
|
84
|
+
});
|