@ramathibodi/nuxt-commons 4.0.12 → 4.0.14
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/dialog/ImportResult.d.vue.ts +28 -0
- package/dist/runtime/components/dialog/ImportResult.vue +122 -0
- package/dist/runtime/components/dialog/ImportResult.vue.d.ts +28 -0
- package/dist/runtime/components/document/TemplateBuilder.vue +112 -7
- package/dist/runtime/components/model/Pad.vue +2 -1
- package/dist/runtime/components/model/Table.d.vue.ts +104 -28
- package/dist/runtime/components/model/Table.vue +118 -3
- package/dist/runtime/components/model/Table.vue.d.ts +104 -28
- package/dist/runtime/components/model/iterator.d.vue.ts +146 -49
- package/dist/runtime/components/model/iterator.vue +129 -5
- package/dist/runtime/components/model/iterator.vue.d.ts +146 -49
- package/dist/runtime/composables/api.d.ts +7 -0
- package/dist/runtime/composables/api.js +3 -2
- package/dist/runtime/composables/apiModel.d.ts +31 -3
- package/dist/runtime/composables/apiModel.js +30 -21
- package/dist/runtime/composables/apiModelOperation.d.ts +17 -3
- package/dist/runtime/composables/apiModelOperation.js +6 -6
- package/dist/runtime/composables/document/template.d.ts +61 -0
- package/dist/runtime/composables/document/template.js +59 -0
- package/dist/runtime/composables/document/validateTemplate.d.ts +62 -0
- package/dist/runtime/composables/document/validateTemplate.js +378 -0
- package/dist/runtime/composables/graphql.d.ts +3 -3
- package/dist/runtime/composables/graphql.js +6 -6
- package/dist/runtime/composables/graphqlModel.d.ts +31 -3
- package/dist/runtime/composables/graphqlModel.js +28 -20
- package/dist/runtime/composables/graphqlModelOperation.d.ts +1 -0
- package/dist/runtime/composables/graphqlOperation.d.ts +2 -1
- package/dist/runtime/composables/graphqlOperation.js +3 -3
- package/dist/runtime/composables/importProgress.d.ts +58 -0
- package/dist/runtime/composables/importProgress.js +130 -0
- package/dist/runtime/types/graphqlOperation.d.ts +12 -1
- package/dist/runtime/utils/virtualize.d.ts +15 -0
- package/dist/runtime/utils/virtualize.js +10 -0
- package/package.json +2 -1
- package/scripts/validate-document-template.mjs +158 -0
- package/templates/.codegen/plugin-schema-object.cjs +10 -13
|
@@ -25,11 +25,39 @@ export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T):
|
|
|
25
25
|
canCreate: import("vue").ComputedRef<boolean>;
|
|
26
26
|
canUpdate: import("vue").ComputedRef<boolean>;
|
|
27
27
|
canDelete: import("vue").ComputedRef<boolean>;
|
|
28
|
-
createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean) => Promise<void>;
|
|
29
|
-
importItems: (
|
|
30
|
-
updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<void>;
|
|
28
|
+
createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean, idempotencySalt?: string | number) => Promise<void>;
|
|
29
|
+
importItems: (importData: Record<string, any>[], callback?: FormDialogCallback) => void;
|
|
30
|
+
updateItem: (item: Record<string, any>, callback?: FormDialogCallback, idempotencySalt?: string | number) => Promise<void>;
|
|
31
31
|
deleteItem: (item: Record<string, any>, callback?: FormDialogCallback) => Promise<any>;
|
|
32
32
|
loadItems: (options: any) => Promise<void> | undefined;
|
|
33
33
|
reload: () => Promise<void> | undefined;
|
|
34
34
|
isLoading: import("vue").Ref<boolean, boolean>;
|
|
35
|
+
importProgress: {
|
|
36
|
+
isImporting: import("vue").Ref<boolean, boolean>;
|
|
37
|
+
total: import("vue").Ref<number, number>;
|
|
38
|
+
processed: import("vue").Ref<number, number>;
|
|
39
|
+
succeeded: import("vue").Ref<number, number>;
|
|
40
|
+
failed: import("vue").Ref<number, number>;
|
|
41
|
+
errors: import("vue").Ref<{
|
|
42
|
+
index: number;
|
|
43
|
+
item: any;
|
|
44
|
+
errorType: import("./importProgress.js").ImportErrorType;
|
|
45
|
+
message: string;
|
|
46
|
+
detail?: any;
|
|
47
|
+
}[], import("./importProgress.js").ImportError[] | {
|
|
48
|
+
index: number;
|
|
49
|
+
item: any;
|
|
50
|
+
errorType: import("./importProgress.js").ImportErrorType;
|
|
51
|
+
message: string;
|
|
52
|
+
detail?: any;
|
|
53
|
+
}[]>;
|
|
54
|
+
percent: import("vue").ComputedRef<number>;
|
|
55
|
+
resultVisible: import("vue").Ref<boolean, boolean>;
|
|
56
|
+
reset: () => void;
|
|
57
|
+
run: <T_1 = any>(items: T_1[], worker: import("./importProgress.js").ImportWorker<T_1>, options?: {
|
|
58
|
+
concurrency?: number;
|
|
59
|
+
}) => Promise<import("./importProgress.js").ImportSummary>;
|
|
60
|
+
retry: (indices?: number[]) => Promise<import("./importProgress.js").ImportSummary>;
|
|
61
|
+
dismissResult: () => void;
|
|
62
|
+
};
|
|
35
63
|
};
|
|
@@ -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(".");
|
|
@@ -56,43 +57,49 @@ export function useGraphqlModel(props) {
|
|
|
56
57
|
const canDelete = computed(() => {
|
|
57
58
|
return !!operationDelete.value;
|
|
58
59
|
});
|
|
59
|
-
function createItem(item, callback, importing = false) {
|
|
60
|
+
function createItem(item, callback, importing = false, idempotencySalt) {
|
|
60
61
|
isLoading.value = true;
|
|
61
|
-
return operationCreate.value?.call(fields.value, { input: item }).then((result) => {
|
|
62
|
+
return operationCreate.value?.call(fields.value, { input: item }, void 0, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
|
|
62
63
|
if (canServerPageable.value) {
|
|
63
64
|
if (!importing) loadItems(currentOptions.value);
|
|
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, _index, salt) => {
|
|
83
|
+
const createAsNew = () => createItem(Object.assign({}, props.initialData, item), void 0, true, salt);
|
|
84
|
+
return item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }, void 0, salt != null ? { idempotencySalt: salt } : void 0).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
|
+
importProgress.resultVisible.value = true;
|
|
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();
|
|
91
98
|
});
|
|
92
99
|
}
|
|
93
|
-
function updateItem(item, callback) {
|
|
100
|
+
function updateItem(item, callback, idempotencySalt) {
|
|
94
101
|
isLoading.value = true;
|
|
95
|
-
return operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
|
|
102
|
+
return operationUpdate.value?.call(fields.value, { input: item }, void 0, idempotencySalt != null ? { idempotencySalt } : void 0).then((result) => {
|
|
96
103
|
if (canServerPageable.value) loadItems(currentOptions.value);
|
|
97
104
|
else {
|
|
98
105
|
const index = items.value.findIndex((item2) => item2[props.modelKey || "id"] === result[props.modelKey || "id"]);
|
|
@@ -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>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ClassConstructor } from '../utils/object.js';
|
|
2
2
|
import type { graphqlTypeObject, graphqlVariable } from '../types/graphqlOperation.js';
|
|
3
|
+
import type { ApiIdempotencyOption } from './api.js';
|
|
3
4
|
export declare function buildFields(operationFields: graphqlTypeObject, fields?: Array<string | Object> | ClassConstructor<any>, depth?: number): Array<string | Object>;
|
|
4
5
|
export declare function buildVariables(outputVariables?: graphqlVariable[], inputVariables?: {
|
|
5
6
|
[p: string]: any;
|
|
@@ -7,4 +8,4 @@ export declare function buildVariables(outputVariables?: graphqlVariable[], inpu
|
|
|
7
8
|
export declare function buildRequiredInputFields(variables: graphqlVariable[] | undefined, inputField?: string): string[];
|
|
8
9
|
export declare function useGraphQlOperation<T>(operationType: string, operation: string, fields?: Array<string | Object> | ClassConstructor<any>, variables?: {
|
|
9
10
|
[p: string]: any;
|
|
10
|
-
}, cache?: boolean | number): Promise<T>;
|
|
11
|
+
}, cache?: boolean | number, options?: ApiIdempotencyOption): Promise<T>;
|
|
@@ -93,7 +93,7 @@ export function buildRequiredInputFields(variables, inputField = "input") {
|
|
|
93
93
|
return [];
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
export function useGraphQlOperation(operationType, operation, fields, variables, cache = false) {
|
|
96
|
+
export function useGraphQlOperation(operationType, operation, fields, variables, cache = false, options) {
|
|
97
97
|
const { graphqlOperation } = useGraphqlBridge();
|
|
98
98
|
return new Promise((resolve, reject) => {
|
|
99
99
|
let rejectReason = void 0;
|
|
@@ -107,8 +107,8 @@ export function useGraphQlOperation(operationType, operation, fields, variables,
|
|
|
107
107
|
}
|
|
108
108
|
if (graphqlOperation[operation]?.fields) {
|
|
109
109
|
fields = buildFields(graphqlOperation[operation]?.fields, fields, 0);
|
|
110
|
-
if (operationType == "Query") useGraphQl().queryPromise(operation, fields, variables, cache).then((result) => resolve(result)).catch((error) => reject(error));
|
|
111
|
-
else if (operationType == "Mutation") useGraphQl().mutationPromise(operation, fields, variables).then((result) => resolve(result)).catch((error) => reject(error));
|
|
110
|
+
if (operationType == "Query") useGraphQl().queryPromise(operation, fields, variables, cache, options).then((result) => resolve(result)).catch((error) => reject(error));
|
|
111
|
+
else if (operationType == "Mutation") useGraphQl().mutationPromise(operation, fields, variables, options).then((result) => resolve(result)).catch((error) => reject(error));
|
|
112
112
|
else reject("Invalid Operation Type");
|
|
113
113
|
} else {
|
|
114
114
|
reject("No schema of return data available");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type ImportErrorType = 'network' | 'timeout' | 'server' | 'unknown';
|
|
2
|
+
export interface ImportError {
|
|
3
|
+
index: number;
|
|
4
|
+
item: any;
|
|
5
|
+
errorType: ImportErrorType;
|
|
6
|
+
message: string;
|
|
7
|
+
detail?: any;
|
|
8
|
+
}
|
|
9
|
+
export interface ImportSummary {
|
|
10
|
+
succeeded: number;
|
|
11
|
+
failed: number;
|
|
12
|
+
errors: ImportError[];
|
|
13
|
+
}
|
|
14
|
+
export type ImportWorker<T = any> = (item: T, index: number, salt: string) => Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
* classifyImportError buckets a thrown import error into a coarse type and
|
|
17
|
+
* extracts a short message + raw detail, normalising ofetch FetchError and
|
|
18
|
+
* Apollo error shapes so callers can separate connection failures from
|
|
19
|
+
* server-returned errors.
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyImportError(error: any): {
|
|
22
|
+
errorType: ImportErrorType;
|
|
23
|
+
message: string;
|
|
24
|
+
detail?: any;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* useImportProgress drives a concurrency-limited bulk import while exposing
|
|
28
|
+
* reactive progress. The runner continues on per-row errors and never rejects;
|
|
29
|
+
* callers read the resolved summary for the success/failure breakdown.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useImportProgress(): {
|
|
32
|
+
isImporting: import("vue").Ref<boolean, boolean>;
|
|
33
|
+
total: import("vue").Ref<number, number>;
|
|
34
|
+
processed: import("vue").Ref<number, number>;
|
|
35
|
+
succeeded: import("vue").Ref<number, number>;
|
|
36
|
+
failed: import("vue").Ref<number, number>;
|
|
37
|
+
errors: import("vue").Ref<{
|
|
38
|
+
index: number;
|
|
39
|
+
item: any;
|
|
40
|
+
errorType: ImportErrorType;
|
|
41
|
+
message: string;
|
|
42
|
+
detail?: any;
|
|
43
|
+
}[], ImportError[] | {
|
|
44
|
+
index: number;
|
|
45
|
+
item: any;
|
|
46
|
+
errorType: ImportErrorType;
|
|
47
|
+
message: string;
|
|
48
|
+
detail?: any;
|
|
49
|
+
}[]>;
|
|
50
|
+
percent: import("vue").ComputedRef<number>;
|
|
51
|
+
resultVisible: import("vue").Ref<boolean, boolean>;
|
|
52
|
+
reset: () => void;
|
|
53
|
+
run: <T = any>(items: T[], worker: ImportWorker<T>, options?: {
|
|
54
|
+
concurrency?: number;
|
|
55
|
+
}) => Promise<ImportSummary>;
|
|
56
|
+
retry: (indices?: number[]) => Promise<ImportSummary>;
|
|
57
|
+
dismissResult: () => void;
|
|
58
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import pLimit from "p-limit";
|
|
3
|
+
export function classifyImportError(error) {
|
|
4
|
+
const isString = typeof error === "string";
|
|
5
|
+
const rawMessage = error?.message || (isString ? error : String(error));
|
|
6
|
+
const name = error?.name;
|
|
7
|
+
const status = error?.status ?? error?.statusCode ?? error?.response?.status;
|
|
8
|
+
const graphQLErrors = error?.graphQLErrors;
|
|
9
|
+
const networkError = error?.networkError;
|
|
10
|
+
if (name === "AbortError" || name === "TimeoutError" || /timeout|timed out|aborted/i.test(rawMessage)) {
|
|
11
|
+
return { errorType: "timeout", message: rawMessage, detail: error?.data };
|
|
12
|
+
}
|
|
13
|
+
if (networkError || /failed to fetch|fetch failed|network ?error|load failed/i.test(rawMessage)) {
|
|
14
|
+
return { errorType: "network", message: rawMessage, detail: networkError ?? error?.data };
|
|
15
|
+
}
|
|
16
|
+
if (status || Array.isArray(graphQLErrors) && graphQLErrors.length > 0 || isString) {
|
|
17
|
+
const detail = error?.data ?? (graphQLErrors?.length ? graphQLErrors : isString ? error : error?.response?._data);
|
|
18
|
+
const message = error?.data?.message || graphQLErrors?.[0]?.message || rawMessage || `Server error ${status ?? ""}`.trim();
|
|
19
|
+
return { errorType: "server", message, detail };
|
|
20
|
+
}
|
|
21
|
+
return { errorType: "unknown", message: rawMessage, detail: error?.data };
|
|
22
|
+
}
|
|
23
|
+
let _importRunSeq = 0;
|
|
24
|
+
export function useImportProgress() {
|
|
25
|
+
const isImporting = ref(false);
|
|
26
|
+
const total = ref(0);
|
|
27
|
+
const processed = ref(0);
|
|
28
|
+
const succeeded = ref(0);
|
|
29
|
+
const failed = ref(0);
|
|
30
|
+
const errors = ref([]);
|
|
31
|
+
const resultVisible = ref(false);
|
|
32
|
+
let _items = [];
|
|
33
|
+
let _worker = null;
|
|
34
|
+
let _concurrency = 3;
|
|
35
|
+
let _runId = 0;
|
|
36
|
+
const percent = computed(
|
|
37
|
+
() => total.value ? Math.round(processed.value / total.value * 100) : 0
|
|
38
|
+
);
|
|
39
|
+
function reset() {
|
|
40
|
+
total.value = 0;
|
|
41
|
+
processed.value = 0;
|
|
42
|
+
succeeded.value = 0;
|
|
43
|
+
failed.value = 0;
|
|
44
|
+
errors.value = [];
|
|
45
|
+
resultVisible.value = false;
|
|
46
|
+
}
|
|
47
|
+
async function run(items, worker, options = {}) {
|
|
48
|
+
if (isImporting.value) {
|
|
49
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
50
|
+
}
|
|
51
|
+
reset();
|
|
52
|
+
total.value = items.length;
|
|
53
|
+
isImporting.value = true;
|
|
54
|
+
_items = items;
|
|
55
|
+
_worker = worker;
|
|
56
|
+
_concurrency = options.concurrency && options.concurrency > 0 ? options.concurrency : 3;
|
|
57
|
+
_runId = ++_importRunSeq;
|
|
58
|
+
const limit = pLimit(_concurrency);
|
|
59
|
+
try {
|
|
60
|
+
await Promise.all(
|
|
61
|
+
items.map(
|
|
62
|
+
(item, index) => limit(async () => {
|
|
63
|
+
try {
|
|
64
|
+
await worker(item, index, `import:${_runId}:${index}`);
|
|
65
|
+
succeeded.value++;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const info = classifyImportError(error);
|
|
68
|
+
console.error("[import] row failed", { index, errorType: info.errorType, item, message: info.message, detail: info.detail });
|
|
69
|
+
failed.value++;
|
|
70
|
+
errors.value.push({ index, item, ...info });
|
|
71
|
+
} finally {
|
|
72
|
+
processed.value++;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
isImporting.value = false;
|
|
79
|
+
}
|
|
80
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
81
|
+
}
|
|
82
|
+
function dismissResult() {
|
|
83
|
+
resultVisible.value = false;
|
|
84
|
+
}
|
|
85
|
+
async function retry(indices) {
|
|
86
|
+
if (isImporting.value || !_worker) {
|
|
87
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
88
|
+
}
|
|
89
|
+
const requested = indices && indices.length ? indices : errors.value.map((e) => e.index);
|
|
90
|
+
const targets = requested.filter((i) => errors.value.some((e) => e.index === i));
|
|
91
|
+
if (targets.length === 0) {
|
|
92
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
93
|
+
}
|
|
94
|
+
isImporting.value = true;
|
|
95
|
+
const limit = pLimit(_concurrency);
|
|
96
|
+
try {
|
|
97
|
+
await Promise.all(
|
|
98
|
+
targets.map(
|
|
99
|
+
(index) => limit(async () => {
|
|
100
|
+
const item = _items[index];
|
|
101
|
+
try {
|
|
102
|
+
await _worker(item, index, `import:${_runId}:${index}`);
|
|
103
|
+
const pos = errors.value.findIndex((e) => e.index === index);
|
|
104
|
+
if (pos !== -1) {
|
|
105
|
+
errors.value.splice(pos, 1);
|
|
106
|
+
failed.value = Math.max(0, failed.value - 1);
|
|
107
|
+
succeeded.value++;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const info = classifyImportError(error);
|
|
111
|
+
console.error("[import] row failed", { index, errorType: info.errorType, item, message: info.message, detail: info.detail });
|
|
112
|
+
const next = { index, item, ...info };
|
|
113
|
+
const pos = errors.value.findIndex((e) => e.index === index);
|
|
114
|
+
if (pos !== -1) errors.value[pos] = next;
|
|
115
|
+
else {
|
|
116
|
+
errors.value.push(next);
|
|
117
|
+
failed.value++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
processed.value = succeeded.value + failed.value;
|
|
125
|
+
isImporting.value = false;
|
|
126
|
+
}
|
|
127
|
+
return { succeeded: succeeded.value, failed: failed.value, errors: [...errors.value] };
|
|
128
|
+
}
|
|
129
|
+
return { isImporting, total, processed, succeeded, failed, errors, percent, resultVisible, reset, run, retry, dismissResult };
|
|
130
|
+
}
|
|
@@ -12,7 +12,18 @@ export interface graphqlOperationObject<T, U> {
|
|
|
12
12
|
fields?: graphqlTypeObject
|
|
13
13
|
operationType: 'Query' | 'Mutation'
|
|
14
14
|
name: string
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Execute this operation.
|
|
17
|
+
*
|
|
18
|
+
* The signature is shared by Query and Mutation operations:
|
|
19
|
+
* - `cache` applies to **Query** operations only (localStorage TTL / fetch policy).
|
|
20
|
+
* It is **ignored for Mutation** operations — mutations are never cached, so a
|
|
21
|
+
* value passed here has no effect.
|
|
22
|
+
* - `options.idempotencySalt` applies to **both** kinds: it is folded into the
|
|
23
|
+
* request's `Idempotency-Key` (`SHA-256(<second>|<username>|<body>|<salt>)`).
|
|
24
|
+
* Pass `options.idempotent: false` to skip the header for this call.
|
|
25
|
+
*/
|
|
26
|
+
call: (fields?: Array<string | Object>, variables?: U, cache?: boolean | number, options?: { idempotent?: boolean, idempotencySalt?: string | number }) => Promise<T>
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
export interface graphqlTypeObject {
|
|
@@ -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.14",
|
|
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",
|
|
@@ -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
|
+
}
|
|
@@ -104,19 +104,22 @@ module.exports = {
|
|
|
104
104
|
return `
|
|
105
105
|
import * as graphQlClass from "~/types/graphql"
|
|
106
106
|
import { useGraphQlOperation } from "#imports";
|
|
107
|
+
import type { ApiIdempotencyOption } from "#imports";
|
|
107
108
|
import type {
|
|
108
109
|
graphqlInputObject,
|
|
109
110
|
graphqlOperationObject,
|
|
110
111
|
graphqlTypeObject,
|
|
111
112
|
graphqlVariable
|
|
112
113
|
} from "#build/types/graphqlOperation";
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
export const scalarType = ${JSON.stringify(scalarType)}
|
|
115
|
-
|
|
116
|
-
function operationCall${'<' + 'T,U>'}(operationType:string, operation: string,fields?: ${'Array' + '<string | Object>'},variables?: U,cache: boolean | number = false
|
|
117
|
-
|
|
116
|
+
|
|
117
|
+
function operationCall${'<' + 'T,U>'}(operationType:string, operation: string,fields?: ${'Array' + '<string | Object>'},variables?: U,cache: boolean | number = false,
|
|
118
|
+
options?: ApiIdempotencyOption) {
|
|
119
|
+
return useGraphQlOperation${'<' + 'T>'}(operationType,operation,fields,variables as {[p: string] : any} | undefined,cache,
|
|
120
|
+
options)
|
|
118
121
|
}
|
|
119
|
-
|
|
122
|
+
|
|
120
123
|
function createGraphQLOperation${'<' + 'T,U>'}(operationType: "Query" | "Mutation",name: string,variables?: graphqlVariable[],fields?: graphqlTypeObject): graphqlOperationObject${'<' + 'T,U>'} {
|
|
121
124
|
let returnGraphqlOperation : ${'Record' + '<string,any>'} = {
|
|
122
125
|
operationType,
|
|
@@ -124,14 +127,8 @@ module.exports = {
|
|
|
124
127
|
variables,
|
|
125
128
|
fields,
|
|
126
129
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return operationCall(operationType,name, fields, variables);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U,cache: boolean | number = false): Promise${'<' + 'T>'} {
|
|
133
|
-
return operationCall(operationType,name, fields, variables,cache);
|
|
134
|
-
}
|
|
130
|
+
returnGraphqlOperation['call'] = async function(fields?: ${'Array' + '<string | Object>'}, variables?: U, cache: boolean | number = false, options?: ApiIdempotencyOption): Promise${'<' + 'T>'} {
|
|
131
|
+
return operationCall(operationType,name, fields, variables, cache, options);
|
|
135
132
|
}
|
|
136
133
|
return returnGraphqlOperation as graphqlOperationObject${'<' + 'T,U>'}
|
|
137
134
|
}
|