@ramathibodi/nuxt-commons 4.0.11 → 4.0.13
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/module.json +1 -1
- package/dist/runtime/components/dialog/ImportProgress.d.vue.ts +35 -0
- package/dist/runtime/components/dialog/ImportProgress.vue +53 -0
- package/dist/runtime/components/dialog/ImportProgress.vue.d.ts +35 -0
- package/dist/runtime/components/document/TemplateBuilder.d.vue.ts +2 -2
- package/dist/runtime/components/document/TemplateBuilder.vue +113 -8
- package/dist/runtime/components/document/TemplateBuilder.vue.d.ts +2 -2
- package/dist/runtime/components/form/ActionPad.vue +1 -0
- package/dist/runtime/components/form/Birthdate.d.vue.ts +3 -3
- package/dist/runtime/components/form/Birthdate.vue.d.ts +3 -3
- package/dist/runtime/components/form/Date.vue +11 -6
- package/dist/runtime/components/form/Dialog.d.vue.ts +1 -5
- package/dist/runtime/components/form/Dialog.vue +1 -0
- package/dist/runtime/components/form/Dialog.vue.d.ts +1 -5
- package/dist/runtime/components/form/EditPad.vue +1 -0
- package/dist/runtime/components/form/Pad.d.vue.ts +24 -0
- package/dist/runtime/components/form/Pad.vue +11 -6
- package/dist/runtime/components/form/Pad.vue.d.ts +24 -0
- package/dist/runtime/components/form/Time.vue +10 -5
- package/dist/runtime/components/form/images/Edit.d.vue.ts +1 -3
- package/dist/runtime/components/form/images/Edit.vue.d.ts +1 -3
- package/dist/runtime/components/model/AutoRefreshChip.d.vue.ts +16 -0
- package/dist/runtime/components/model/AutoRefreshChip.vue +34 -0
- package/dist/runtime/components/model/AutoRefreshChip.vue.d.ts +16 -0
- package/dist/runtime/components/model/Pad.vue +2 -1
- package/dist/runtime/components/model/Table.d.vue.ts +158 -61
- package/dist/runtime/components/model/Table.vue +129 -7
- package/dist/runtime/components/model/Table.vue.d.ts +158 -61
- package/dist/runtime/components/model/iterator.d.vue.ts +198 -78
- package/dist/runtime/components/model/iterator.vue +140 -9
- package/dist/runtime/components/model/iterator.vue.d.ts +198 -78
- package/dist/runtime/composables/apiModel.d.ts +22 -3
- package/dist/runtime/composables/apiModel.js +27 -19
- package/dist/runtime/composables/autoRefresh.d.ts +42 -0
- package/dist/runtime/composables/autoRefresh.js +57 -0
- package/dist/runtime/composables/document/template.d.ts +61 -0
- package/dist/runtime/composables/document/template.js +60 -1
- package/dist/runtime/composables/document/validateTemplate.d.ts +62 -0
- package/dist/runtime/composables/document/validateTemplate.js +378 -0
- package/dist/runtime/composables/graphqlModel.d.ts +22 -3
- package/dist/runtime/composables/graphqlModel.js +27 -19
- package/dist/runtime/composables/graphqlModelOperation.d.ts +1 -0
- package/dist/runtime/composables/importProgress.d.ts +34 -0
- package/dist/runtime/composables/importProgress.js +50 -0
- package/dist/runtime/composables/modelAutoRefresh.d.ts +29 -0
- package/dist/runtime/composables/modelAutoRefresh.js +16 -0
- package/dist/runtime/composables/utils/validation.d.ts +4 -0
- package/dist/runtime/composables/utils/validation.js +2 -0
- package/dist/runtime/utils/virtualize.d.ts +15 -0
- package/dist/runtime/utils/virtualize.js +10 -0
- package/package.json +3 -2
- package/scripts/validate-document-template.mjs +158 -0
|
@@ -3,7 +3,7 @@ import { watchDebounced } from "@vueuse/core";
|
|
|
3
3
|
import { useAlert } from "./alert.js";
|
|
4
4
|
import { buildRequiredInputFields } from "./graphqlOperation.js";
|
|
5
5
|
import { useGraphqlModelOperation } from "./graphqlModelOperation.js";
|
|
6
|
-
import
|
|
6
|
+
import { useImportProgress } from "./importProgress.js";
|
|
7
7
|
import { arrayWrap } from "../utils/array.js";
|
|
8
8
|
export function useGraphqlModel(props) {
|
|
9
9
|
const alert = useAlert();
|
|
@@ -15,6 +15,7 @@ export function useGraphqlModel(props) {
|
|
|
15
15
|
search.value = keyword;
|
|
16
16
|
}
|
|
17
17
|
const isLoading = ref(false);
|
|
18
|
+
const importProgress = useImportProgress();
|
|
18
19
|
const { operationCreate, operationUpdate, operationDelete, operationRead, operationReadPageable, operationSearch } = useGraphqlModelOperation(props);
|
|
19
20
|
function keyToField(key) {
|
|
20
21
|
const parts = key.split(".");
|
|
@@ -64,27 +65,33 @@ export function useGraphqlModel(props) {
|
|
|
64
65
|
} else items.value.push(result);
|
|
65
66
|
if (callback && callback.setData) callback.setData(result);
|
|
66
67
|
}).catch((error) => {
|
|
68
|
+
if (importing) throw error;
|
|
67
69
|
alert?.addAlert({ alertType: "error", message: error });
|
|
68
70
|
}).finally(() => {
|
|
69
71
|
if (!importing) isLoading.value = false;
|
|
70
72
|
if (callback) callback.done();
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
|
-
function importItems(
|
|
75
|
+
function importItems(importData, callback) {
|
|
76
|
+
if (importProgress.isImporting.value) return;
|
|
77
|
+
if (importData.length === 0) {
|
|
78
|
+
if (callback) callback.done();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
74
81
|
isLoading.value = true;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
(item) =>
|
|
78
|
-
()
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
const worker = (item) => {
|
|
83
|
+
const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true);
|
|
84
|
+
return item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
|
|
85
|
+
if (!result) return createAsNew();
|
|
86
|
+
}) : createAsNew();
|
|
87
|
+
};
|
|
88
|
+
importProgress.run(importData, worker, { concurrency: props.importConcurrency }).then(({ succeeded, failed }) => {
|
|
89
|
+
if (failed > 0) {
|
|
90
|
+
alert?.addAlert({ alertType: "warning", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23, \u0E25\u0E49\u0E21\u0E40\u0E2B\u0E25\u0E27 ${failed} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
|
|
91
|
+
} else {
|
|
92
|
+
alert?.addAlert({ alertType: "success", message: `\u0E19\u0E33\u0E40\u0E02\u0E49\u0E32\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 ${succeeded} \u0E23\u0E32\u0E22\u0E01\u0E32\u0E23` });
|
|
93
|
+
}
|
|
94
|
+
}).finally(() => {
|
|
88
95
|
isLoading.value = false;
|
|
89
96
|
reload();
|
|
90
97
|
if (callback) callback.done();
|
|
@@ -127,7 +134,7 @@ export function useGraphqlModel(props) {
|
|
|
127
134
|
sortBy: options.sortBy
|
|
128
135
|
};
|
|
129
136
|
isLoading.value = true;
|
|
130
|
-
operationReadPageable.value?.call([{ data: fields.value }, "meta"], Object.assign({}, props.modelBy, { pageable: pageableVariable })).then((result) => {
|
|
137
|
+
return operationReadPageable.value?.call([{ data: fields.value }, "meta"], Object.assign({}, props.modelBy, { pageable: pageableVariable })).then((result) => {
|
|
131
138
|
items.value = result.data;
|
|
132
139
|
itemsLength.value = result.meta.totalItems;
|
|
133
140
|
}).catch((error) => {
|
|
@@ -141,10 +148,10 @@ export function useGraphqlModel(props) {
|
|
|
141
148
|
}
|
|
142
149
|
function reload() {
|
|
143
150
|
if (canServerPageable.value) {
|
|
144
|
-
if (currentOptions.value) loadItems(currentOptions.value);
|
|
151
|
+
if (currentOptions.value) return loadItems(currentOptions.value);
|
|
145
152
|
} else {
|
|
146
153
|
isLoading.value = true;
|
|
147
|
-
operationRead.value?.call(fields.value, props.modelBy).then((result) => {
|
|
154
|
+
return operationRead.value?.call(fields.value, props.modelBy).then((result) => {
|
|
148
155
|
items.value = arrayWrap(result);
|
|
149
156
|
}).catch((error) => {
|
|
150
157
|
items.value = [];
|
|
@@ -184,6 +191,7 @@ export function useGraphqlModel(props) {
|
|
|
184
191
|
deleteItem,
|
|
185
192
|
loadItems,
|
|
186
193
|
reload,
|
|
187
|
-
isLoading
|
|
194
|
+
isLoading,
|
|
195
|
+
importProgress
|
|
188
196
|
};
|
|
189
197
|
}
|
|
@@ -10,6 +10,7 @@ export interface GraphqlModelConfigProps {
|
|
|
10
10
|
operationReadPageable?: graphqlOperationObject<any, any> | string;
|
|
11
11
|
operationSearch?: graphqlOperationObject<any, any> | string;
|
|
12
12
|
fields?: Array<string | object>;
|
|
13
|
+
importConcurrency?: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function useGraphqlModelOperation<T extends GraphqlModelConfigProps>(props: T): {
|
|
15
16
|
operationCreate: import("vue").ComputedRef<graphqlOperationObject<any, any>>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ImportError {
|
|
2
|
+
index: number;
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ImportSummary {
|
|
6
|
+
succeeded: number;
|
|
7
|
+
failed: number;
|
|
8
|
+
errors: ImportError[];
|
|
9
|
+
}
|
|
10
|
+
export type ImportWorker<T = any> = (item: T, index: number) => Promise<any>;
|
|
11
|
+
/**
|
|
12
|
+
* useImportProgress drives a concurrency-limited bulk import while exposing
|
|
13
|
+
* reactive progress. The runner continues on per-row errors and never rejects;
|
|
14
|
+
* callers read the resolved summary for the success/failure breakdown.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useImportProgress(): {
|
|
17
|
+
isImporting: import("vue").Ref<boolean, boolean>;
|
|
18
|
+
total: import("vue").Ref<number, number>;
|
|
19
|
+
processed: import("vue").Ref<number, number>;
|
|
20
|
+
succeeded: import("vue").Ref<number, number>;
|
|
21
|
+
failed: import("vue").Ref<number, number>;
|
|
22
|
+
errors: import("vue").Ref<{
|
|
23
|
+
index: number;
|
|
24
|
+
message: string;
|
|
25
|
+
}[], ImportError[] | {
|
|
26
|
+
index: number;
|
|
27
|
+
message: string;
|
|
28
|
+
}[]>;
|
|
29
|
+
percent: import("vue").ComputedRef<number>;
|
|
30
|
+
reset: () => void;
|
|
31
|
+
run: <T = any>(items: T[], worker: ImportWorker<T>, options?: {
|
|
32
|
+
concurrency?: number;
|
|
33
|
+
}) => Promise<ImportSummary>;
|
|
34
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import pLimit from "p-limit";
|
|
3
|
+
export function useImportProgress() {
|
|
4
|
+
const isImporting = ref(false);
|
|
5
|
+
const total = ref(0);
|
|
6
|
+
const processed = ref(0);
|
|
7
|
+
const succeeded = ref(0);
|
|
8
|
+
const failed = ref(0);
|
|
9
|
+
const errors = ref([]);
|
|
10
|
+
const percent = computed(
|
|
11
|
+
() => total.value ? Math.round(processed.value / total.value * 100) : 0
|
|
12
|
+
);
|
|
13
|
+
function reset() {
|
|
14
|
+
total.value = 0;
|
|
15
|
+
processed.value = 0;
|
|
16
|
+
succeeded.value = 0;
|
|
17
|
+
failed.value = 0;
|
|
18
|
+
errors.value = [];
|
|
19
|
+
}
|
|
20
|
+
async function run(items, worker, options = {}) {
|
|
21
|
+
if (isImporting.value) {
|
|
22
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
23
|
+
}
|
|
24
|
+
reset();
|
|
25
|
+
total.value = items.length;
|
|
26
|
+
isImporting.value = true;
|
|
27
|
+
const limit = pLimit(options.concurrency && options.concurrency > 0 ? options.concurrency : 3);
|
|
28
|
+
try {
|
|
29
|
+
await Promise.all(
|
|
30
|
+
items.map(
|
|
31
|
+
(item, index) => limit(async () => {
|
|
32
|
+
try {
|
|
33
|
+
await worker(item, index);
|
|
34
|
+
succeeded.value++;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
failed.value++;
|
|
37
|
+
errors.value.push({ index, message: error?.message || String(error) });
|
|
38
|
+
} finally {
|
|
39
|
+
processed.value++;
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
} finally {
|
|
45
|
+
isImporting.value = false;
|
|
46
|
+
}
|
|
47
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
48
|
+
}
|
|
49
|
+
return { isImporting, total, processed, succeeded, failed, errors, percent, reset, run };
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared auto-refresh wiring for `<ModelTable>` and `<ModelIterator>`.
|
|
3
|
+
*
|
|
4
|
+
* Both components expose the same `autoRefresh` / `autoRefreshDefault` / `autoRefreshControl`
|
|
5
|
+
* props and gate polling on the same signals (dialog open + in-flight loading). This helper
|
|
6
|
+
* keeps that contract in one place: pass the model's `reload`/`isLoading` and the dialog-open
|
|
7
|
+
* ref, get back the `useAutoRefresh` handle plus a `manualReload` that reloads and restarts
|
|
8
|
+
* the countdown (used by the toolbar refresh icon).
|
|
9
|
+
*/
|
|
10
|
+
import { type Ref } from 'vue';
|
|
11
|
+
import { type UseAutoRefreshHandle } from './autoRefresh.js';
|
|
12
|
+
export interface ModelAutoRefreshProps {
|
|
13
|
+
autoRefresh?: number | boolean;
|
|
14
|
+
autoRefreshDefault?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface UseModelAutoRefreshDeps {
|
|
17
|
+
/** The model composable's reload(). */
|
|
18
|
+
reload: () => void | Promise<void>;
|
|
19
|
+
/** The model composable's loading flag. */
|
|
20
|
+
isLoading: Ref<boolean>;
|
|
21
|
+
/** Component ref that is true while a create/edit dialog is open. */
|
|
22
|
+
isDialogOpen: Ref<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export interface UseModelAutoRefreshHandle {
|
|
25
|
+
autoRefresh: UseAutoRefreshHandle;
|
|
26
|
+
/** Reload now and restart the countdown — wired to the toolbar refresh icon. */
|
|
27
|
+
manualReload: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function useModelAutoRefresh(props: ModelAutoRefreshProps, deps: UseModelAutoRefreshDeps): UseModelAutoRefreshHandle;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useAutoRefresh } from "./autoRefresh.js";
|
|
3
|
+
export function useModelAutoRefresh(props, deps) {
|
|
4
|
+
const autoRefresh = useAutoRefresh({
|
|
5
|
+
interval: () => props.autoRefresh,
|
|
6
|
+
defaultSeconds: props.autoRefreshDefault,
|
|
7
|
+
reload: deps.reload,
|
|
8
|
+
isLoading: deps.isLoading,
|
|
9
|
+
paused: computed(() => deps.isDialogOpen.value)
|
|
10
|
+
});
|
|
11
|
+
function manualReload() {
|
|
12
|
+
deps.reload();
|
|
13
|
+
autoRefresh.reset();
|
|
14
|
+
}
|
|
15
|
+
return { autoRefresh, manualReload };
|
|
16
|
+
}
|
|
@@ -3,6 +3,7 @@ export declare function useRules(): {
|
|
|
3
3
|
requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
4
4
|
requireTrue: (customError?: string) => (value: any) => string | true;
|
|
5
5
|
requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
6
|
+
requireNotEmpty: (customError?: string) => (value: any) => string | true;
|
|
6
7
|
numeric: (customError?: string) => (value: any) => string | true;
|
|
7
8
|
range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
|
|
8
9
|
integer: (customError?: string) => (value: any) => string | true;
|
|
@@ -31,6 +32,7 @@ export declare function useRules(): {
|
|
|
31
32
|
requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
32
33
|
requireTrue: (customError?: string) => (value: any) => string | true;
|
|
33
34
|
requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
35
|
+
requireNotEmpty: (customError?: string) => (value: any) => string | true;
|
|
34
36
|
numeric: (customError?: string) => (value: any) => string | true;
|
|
35
37
|
range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
|
|
36
38
|
integer: (customError?: string) => (value: any) => string | true;
|
|
@@ -59,6 +61,7 @@ export declare function useRules(): {
|
|
|
59
61
|
requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
60
62
|
requireTrue: (customError?: string) => (value: any) => string | true;
|
|
61
63
|
requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
64
|
+
requireNotEmpty: (customError?: string) => (value: any) => string | true;
|
|
62
65
|
numeric: (customError?: string) => (value: any) => string | true;
|
|
63
66
|
range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
|
|
64
67
|
integer: (customError?: string) => (value: any) => string | true;
|
|
@@ -87,6 +90,7 @@ export declare function useRules(): {
|
|
|
87
90
|
requireIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
88
91
|
requireTrue: (customError?: string) => (value: any) => string | true;
|
|
89
92
|
requireTrueIf: (conditionIf: boolean, customError?: string) => (value: any) => string | true;
|
|
93
|
+
requireNotEmpty: (customError?: string) => (value: any) => string | true;
|
|
90
94
|
numeric: (customError?: string) => (value: any) => string | true;
|
|
91
95
|
range: (minValue: number, maxValue: number, customError?: string) => (value: any) => string | true;
|
|
92
96
|
integer: (customError?: string) => (value: any) => string | true;
|
|
@@ -7,6 +7,7 @@ export function useRules() {
|
|
|
7
7
|
const requireIf = (conditionIf, customError = "This field is required") => (value) => condition(!!value || value === false || value === 0 || !conditionIf, customError);
|
|
8
8
|
const requireTrue = (customError = "This field must be true") => (value) => condition(!!value, customError);
|
|
9
9
|
const requireTrueIf = (conditionIf, customError = "This field must be true") => (value) => condition(!!value || !conditionIf, customError);
|
|
10
|
+
const requireNotEmpty = (customError = "This field is required") => (value) => condition(Array.isArray(value) ? value.length > 0 : !!value || value === false || value === 0, customError);
|
|
10
11
|
const numeric = (customError = "This field must be a number") => (value) => condition(!value || !isNaN(Number(value)), customError);
|
|
11
12
|
const range = (minValue, maxValue, customError = `Value is out of range (${minValue}-${maxValue})`) => (value) => condition(!value || value >= minValue && value <= maxValue, customError);
|
|
12
13
|
const integer = (customError = "This field must be an integer") => (value) => condition(!value || isInteger(value) || /^\+?-?\d+$/.test(value), customError);
|
|
@@ -35,6 +36,7 @@ export function useRules() {
|
|
|
35
36
|
requireIf,
|
|
36
37
|
requireTrue,
|
|
37
38
|
requireTrueIf,
|
|
39
|
+
requireNotEmpty,
|
|
38
40
|
numeric,
|
|
39
41
|
range,
|
|
40
42
|
integer,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the ModelTable/ModelIterator virtualization layer (#246).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Decide whether to render rows virtualized.
|
|
6
|
+
* @param count rendered-row count (items.length)
|
|
7
|
+
* @param override tri-state `virtual` prop: true/false force, undefined = auto
|
|
8
|
+
* @param threshold auto-virtualize above this count (default 500)
|
|
9
|
+
*/
|
|
10
|
+
export declare function shouldVirtualize(count: number, override: boolean | undefined, threshold: number | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Cards-per-row for a Vuetify grid column span (out of 12).
|
|
13
|
+
* span 12 -> 1, 6 -> 2, 4 -> 3, 2 -> 6. Falsy / out-of-range -> 1 (full width).
|
|
14
|
+
*/
|
|
15
|
+
export declare function itemsPerRowForSpan(span: number | string | boolean | undefined): number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function shouldVirtualize(count, override, threshold) {
|
|
2
|
+
if (override === true) return true;
|
|
3
|
+
if (override === false) return false;
|
|
4
|
+
return count > (threshold ?? 500);
|
|
5
|
+
}
|
|
6
|
+
export function itemsPerRowForSpan(span) {
|
|
7
|
+
const n = Number(span);
|
|
8
|
+
if (!Number.isFinite(n) || n <= 0 || n > 12) return 1;
|
|
9
|
+
return Math.max(1, Math.floor(12 / n));
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramathibodi/nuxt-commons",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.13",
|
|
4
4
|
"description": "Ramathibodi Nuxt modules for common components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"dev:build": "nuxt build playground",
|
|
50
50
|
"dev:generate": "nuxt generate playground",
|
|
51
51
|
"playground:scaffold": "node scripts/scaffold-playground-pages.mjs",
|
|
52
|
+
"template:validate": "node scripts/validate-document-template.mjs",
|
|
52
53
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
53
54
|
"docs:api:components": "vue-docgen -c docs/vue-docgen.config.cjs && npm run docs:ai:summary && node scripts/enrich-vue-docs-from-ai.mjs",
|
|
54
55
|
"docs:api:composables": "typedoc --options docs/typedoc.json",
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
"@techstark/opencv-js": "4.11.0-release.1",
|
|
89
90
|
"@thumbmarkjs/thumbmarkjs": "^1.7.4",
|
|
90
91
|
"@vue/apollo-composable": "^4.2.2",
|
|
91
|
-
"@vuepic/vue-datepicker": "^
|
|
92
|
+
"@vuepic/vue-datepicker": "^12.1.0",
|
|
92
93
|
"@vueuse/integrations": "^14.2.1",
|
|
93
94
|
"@zxing/browser": "^0.1.5",
|
|
94
95
|
"cropperjs": "^1.6.2",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Codegen-style validator for Document Template JSON.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/validate-document-template.mjs [file|glob ...] [options]
|
|
7
|
+
* cat template.json | node scripts/validate-document-template.mjs
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --strict Promote builder-unsafe warnings to errors (exit 1 on them too).
|
|
11
|
+
* --json Emit machine-readable JSON: [{ file, result }].
|
|
12
|
+
* --quiet Print errors only (suppress warnings and infos).
|
|
13
|
+
* -h, --help Show this help.
|
|
14
|
+
*
|
|
15
|
+
* Exit code: 1 if any input has errors (warnings count as errors under --strict),
|
|
16
|
+
* otherwise 0.
|
|
17
|
+
*
|
|
18
|
+
* Shares the exact validation core used by the `useDocumentTemplate` runtime via
|
|
19
|
+
* jiti (the TypeScript loader Nuxt already ships), so the CLI and the composable
|
|
20
|
+
* can never diverge.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from 'node:fs'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import { dirname, resolve, relative } from 'node:path'
|
|
25
|
+
import process from 'node:process'
|
|
26
|
+
import { createJiti } from 'jiti'
|
|
27
|
+
|
|
28
|
+
const dir = dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
const repoRoot = resolve(dir, '..')
|
|
30
|
+
|
|
31
|
+
const jiti = createJiti(import.meta.url)
|
|
32
|
+
const { validateDocumentTemplate } = await jiti.import(
|
|
33
|
+
resolve(repoRoot, 'src/runtime/composables/document/validateTemplate.ts'),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// --- arg parsing -----------------------------------------------------------
|
|
37
|
+
const argv = process.argv.slice(2)
|
|
38
|
+
const flags = new Set(argv.filter(a => a.startsWith('-')))
|
|
39
|
+
const files = argv.filter(a => !a.startsWith('-'))
|
|
40
|
+
|
|
41
|
+
if (flags.has('-h') || flags.has('--help')) {
|
|
42
|
+
printHelp()
|
|
43
|
+
process.exit(0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const strict = flags.has('--strict')
|
|
47
|
+
const asJson = flags.has('--json')
|
|
48
|
+
const quiet = flags.has('--quiet')
|
|
49
|
+
|
|
50
|
+
// --- input collection ------------------------------------------------------
|
|
51
|
+
/** @type {{ file: string, content: string }[]} */
|
|
52
|
+
const inputs = []
|
|
53
|
+
|
|
54
|
+
if (files.length === 0) {
|
|
55
|
+
const stdin = readFileSync(0, 'utf8')
|
|
56
|
+
if (!stdin.trim()) {
|
|
57
|
+
console.error('No input: pass file paths/globs or pipe JSON via stdin.')
|
|
58
|
+
process.exit(2)
|
|
59
|
+
}
|
|
60
|
+
inputs.push({ file: '<stdin>', content: stdin })
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const resolved = await expandGlobs(files)
|
|
64
|
+
if (resolved.length === 0) {
|
|
65
|
+
console.error(`No files matched: ${files.join(', ')}`)
|
|
66
|
+
process.exit(2)
|
|
67
|
+
}
|
|
68
|
+
for (const f of resolved) {
|
|
69
|
+
try {
|
|
70
|
+
inputs.push({ file: f, content: readFileSync(f, 'utf8') })
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.error(`Cannot read ${f}: ${e.message}`)
|
|
74
|
+
process.exit(2)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- validation ------------------------------------------------------------
|
|
80
|
+
const results = inputs.map(({ file, content }) => ({
|
|
81
|
+
file,
|
|
82
|
+
result: validateDocumentTemplate(content, { strict }),
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
const hadErrors = results.some(r => !r.result.valid)
|
|
86
|
+
|
|
87
|
+
if (asJson) {
|
|
88
|
+
const rel = f => (f === '<stdin>' ? f : relative(repoRoot, resolve(f)))
|
|
89
|
+
console.log(JSON.stringify(results.map(r => ({ file: rel(r.file), result: r.result })), null, 2))
|
|
90
|
+
process.exit(hadErrors ? 1 : 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- human report ----------------------------------------------------------
|
|
94
|
+
let totalErrors = 0
|
|
95
|
+
let totalWarnings = 0
|
|
96
|
+
let totalInfos = 0
|
|
97
|
+
|
|
98
|
+
for (const { file, result } of results) {
|
|
99
|
+
const shown = quiet ? result.errors : result.issues
|
|
100
|
+
totalErrors += result.errors.length
|
|
101
|
+
totalWarnings += result.warnings.length
|
|
102
|
+
totalInfos += result.infos.length
|
|
103
|
+
|
|
104
|
+
const status = result.valid ? (result.warnings.length ? 'WARN' : 'OK') : 'FAIL'
|
|
105
|
+
console.log(`\n${status} ${file}`)
|
|
106
|
+
if (shown.length === 0) {
|
|
107
|
+
if (!quiet) console.log(' no issues')
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
for (const issue of shown) {
|
|
111
|
+
const tag = issue.severity.toUpperCase().padEnd(7)
|
|
112
|
+
const loc = issue.path || '(root)'
|
|
113
|
+
console.log(` ${tag} ${loc} ${issue.code} ${issue.message}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
`\n${results.length} file(s): ${totalErrors} error(s), ${totalWarnings} warning(s), ${totalInfos} info(s)`
|
|
119
|
+
+ (strict ? ' [strict]' : ''),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
process.exit(hadErrors ? 1 : 0)
|
|
123
|
+
|
|
124
|
+
// --- helpers ---------------------------------------------------------------
|
|
125
|
+
async function expandGlobs(patterns) {
|
|
126
|
+
const out = []
|
|
127
|
+
for (const p of patterns) {
|
|
128
|
+
if (/[*?[\]{}]/.test(p)) {
|
|
129
|
+
try {
|
|
130
|
+
const { glob } = await import('node:fs/promises')
|
|
131
|
+
for await (const entry of glob(p)) out.push(entry)
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
console.error(`Globbing not supported in this Node version for "${p}"; pass explicit file paths.`)
|
|
135
|
+
process.exit(2)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
out.push(p)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// De-dupe while preserving order.
|
|
143
|
+
return [...new Set(out)]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function printHelp() {
|
|
147
|
+
console.log(`Validate Document Template JSON.
|
|
148
|
+
|
|
149
|
+
Usage:
|
|
150
|
+
node scripts/validate-document-template.mjs [file|glob ...] [options]
|
|
151
|
+
cat template.json | node scripts/validate-document-template.mjs
|
|
152
|
+
|
|
153
|
+
Options:
|
|
154
|
+
--strict Promote builder-unsafe warnings to errors.
|
|
155
|
+
--json Emit machine-readable JSON.
|
|
156
|
+
--quiet Print errors only.
|
|
157
|
+
-h, --help Show this help.`)
|
|
158
|
+
}
|