@ramathibodi/nuxt-commons 4.0.12 → 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.
Files changed (27) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/dialog/ImportProgress.d.vue.ts +35 -0
  3. package/dist/runtime/components/dialog/ImportProgress.vue +53 -0
  4. package/dist/runtime/components/dialog/ImportProgress.vue.d.ts +35 -0
  5. package/dist/runtime/components/document/TemplateBuilder.vue +112 -7
  6. package/dist/runtime/components/model/Pad.vue +2 -1
  7. package/dist/runtime/components/model/Table.d.vue.ts +79 -12
  8. package/dist/runtime/components/model/Table.vue +106 -3
  9. package/dist/runtime/components/model/Table.vue.d.ts +79 -12
  10. package/dist/runtime/components/model/iterator.d.vue.ts +117 -29
  11. package/dist/runtime/components/model/iterator.vue +117 -5
  12. package/dist/runtime/components/model/iterator.vue.d.ts +117 -29
  13. package/dist/runtime/composables/apiModel.d.ts +20 -1
  14. package/dist/runtime/composables/apiModel.js +24 -16
  15. package/dist/runtime/composables/document/template.d.ts +61 -0
  16. package/dist/runtime/composables/document/template.js +59 -0
  17. package/dist/runtime/composables/document/validateTemplate.d.ts +62 -0
  18. package/dist/runtime/composables/document/validateTemplate.js +378 -0
  19. package/dist/runtime/composables/graphqlModel.d.ts +20 -1
  20. package/dist/runtime/composables/graphqlModel.js +24 -16
  21. package/dist/runtime/composables/graphqlModelOperation.d.ts +1 -0
  22. package/dist/runtime/composables/importProgress.d.ts +34 -0
  23. package/dist/runtime/composables/importProgress.js +50 -0
  24. package/dist/runtime/utils/virtualize.d.ts +15 -0
  25. package/dist/runtime/utils/virtualize.js +10 -0
  26. package/package.json +2 -1
  27. package/scripts/validate-document-template.mjs +158 -0
@@ -0,0 +1,378 @@
1
+ import { computed, toValue } from "vue";
2
+ import { isPlainObject } from "lodash-es";
3
+ import { validationRulesRegex } from "./template.js";
4
+ import {
5
+ getDocumentTemplateInputTypeEntry
6
+ } from "./templateInputTypes.js";
7
+ const CHOICE_STRING_REGEX = /^[^'",]+(,[^'",]+)*$/;
8
+ const VARIABLE_NAME_REGEX = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
9
+ const KNOWN_ITEM_KEYS = /* @__PURE__ */ new Set([
10
+ "inputType",
11
+ "width",
12
+ "inputLabel",
13
+ "variableName",
14
+ "validationRules",
15
+ "inputOptions",
16
+ "inputAttributes",
17
+ "inputCustomCode",
18
+ "columnAttributes",
19
+ "conditionalDisplay",
20
+ "computedValue",
21
+ "retrievedValue",
22
+ "customClass",
23
+ "customStyle",
24
+ "printConfig"
25
+ ]);
26
+ export function validateDocumentTemplate(items, options = {}) {
27
+ const raw = [];
28
+ const push = (path, code, message, base) => raw.push({ path, code, message, base });
29
+ let parsed = items;
30
+ if (typeof items === "string") {
31
+ try {
32
+ parsed = JSON.parse(items);
33
+ } catch {
34
+ push("", "NOT_ARRAY", "Template is a string but not valid JSON; expected an array of items.", "error");
35
+ return finalize(raw, options.strict);
36
+ }
37
+ }
38
+ validateItemArray(parsed, "", push);
39
+ return finalize(raw, options.strict);
40
+ }
41
+ function validateItemArray(node, basePath, push) {
42
+ if (!Array.isArray(node) || !node.every((it) => isPlainObject(it))) {
43
+ push(basePath || "", "NOT_ARRAY", `Expected an array of template item objects at ${basePath || "top level"}.`, "error");
44
+ return;
45
+ }
46
+ if (node.length === 0) {
47
+ push(basePath || "", "EMPTY_TEMPLATE", "Template array is empty; it renders an empty container.", "info");
48
+ return;
49
+ }
50
+ const seen = /* @__PURE__ */ new Map();
51
+ for (const item of node) {
52
+ const v = typeof item.variableName === "string" ? item.variableName.trim() : "";
53
+ if (v) seen.set(v, (seen.get(v) ?? 0) + 1);
54
+ }
55
+ node.forEach((item, index) => {
56
+ validateItem(item, `${basePath}[${index}]`, seen, push);
57
+ });
58
+ }
59
+ function validateItem(item, path, siblingVarCounts, push) {
60
+ for (const key of Object.keys(item)) {
61
+ if (!KNOWN_ITEM_KEYS.has(key)) {
62
+ push(`${path}.${key}`, "UNKNOWN_KEY", `Unknown key "${key}" is ignored by the renderer (possible typo).`, "info");
63
+ }
64
+ }
65
+ if (typeof item.inputType !== "string" || item.inputType.trim() === "") {
66
+ push(`${path}.inputType`, "INPUT_TYPE_MISSING", "inputType is required and must be a non-empty string.", "error");
67
+ return;
68
+ }
69
+ const inputType = item.inputType;
70
+ const entry = getDocumentTemplateInputTypeEntry(inputType);
71
+ if (!entry) {
72
+ push(
73
+ `${path}.inputType`,
74
+ "INPUT_TYPE_UNKNOWN",
75
+ `inputType "${inputType}" is not a built-in type; it will render as a raw <${inputType}> component tag.`,
76
+ "warning"
77
+ );
78
+ }
79
+ const needsVariableName = entry?.needsVariableName !== false;
80
+ const needsLabel = entry?.needsLabel !== false;
81
+ const needsWidth = entry?.needsWidth !== false;
82
+ const requiresOptions = entry?.requiresOptions === true;
83
+ const optionsAsChoice = entry?.optionsAsChoice === true;
84
+ const hasSpecificOptionEditor = entry?.hasSpecificOptionEditor === true;
85
+ const variableName = typeof item.variableName === "string" ? item.variableName.trim() : "";
86
+ if (needsVariableName) {
87
+ if (!variableName) {
88
+ push(`${path}.variableName`, "VARIABLE_NAME_REQUIRED", `${inputType} requires a variableName.`, "warning");
89
+ } else if (!VARIABLE_NAME_REGEX.test(variableName)) {
90
+ push(
91
+ `${path}.variableName`,
92
+ "VARIABLE_NAME_MALFORMED",
93
+ `variableName "${variableName}" is not a valid data path; it breaks v-model="data.${variableName}".`,
94
+ "error"
95
+ );
96
+ } else if ((siblingVarCounts.get(variableName) ?? 0) > 1) {
97
+ push(
98
+ `${path}.variableName`,
99
+ "VARIABLE_NAME_DUPLICATE",
100
+ `variableName "${variableName}" is used by more than one sibling field; they bind to the same data property.`,
101
+ "warning"
102
+ );
103
+ }
104
+ }
105
+ if (needsLabel && (typeof item.inputLabel !== "string" || item.inputLabel.trim() === "")) {
106
+ push(`${path}.inputLabel`, "LABEL_REQUIRED", `${inputType} expects an inputLabel.`, "warning");
107
+ }
108
+ if (needsWidth) {
109
+ if (item.width === void 0 || item.width === null || item.width === "") {
110
+ push(`${path}.width`, "WIDTH_REQUIRED", `${inputType} expects a width (1\u201312).`, "warning");
111
+ } else {
112
+ const n = Number(item.width);
113
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 12) {
114
+ push(`${path}.width`, "WIDTH_OUT_OF_RANGE", `width "${item.width}" should be an integer from 1 to 12.`, "warning");
115
+ }
116
+ }
117
+ }
118
+ if (item.validationRules !== void 0 && item.validationRules !== null && item.validationRules !== "") {
119
+ if (typeof item.validationRules !== "string") {
120
+ push(`${path}.validationRules`, "RULES_INVALID", "validationRules must be a string.", "warning");
121
+ } else {
122
+ const stripped = item.validationRules.replace(/^\[|]$/g, "").trim();
123
+ if (stripped && !validationRulesRegex.test(stripped)) {
124
+ push(
125
+ `${path}.validationRules`,
126
+ "RULES_INVALID",
127
+ `validationRules "${item.validationRules}" fail the rules regex; the renderer drops them silently.`,
128
+ "warning"
129
+ );
130
+ }
131
+ }
132
+ }
133
+ if (item.computedValue && !variableName) {
134
+ push(
135
+ `${path}.computedValue`,
136
+ "COMPUTED_VALUE_IGNORED",
137
+ "computedValue is ignored without a variableName (no hidden field is appended).",
138
+ "info"
139
+ );
140
+ }
141
+ if (item.retrievedValue) {
142
+ push(`${path}.retrievedValue`, "RETRIEVED_VALUE_UNUSED", "retrievedValue is not consumed by the renderer.", "info");
143
+ }
144
+ validatePrintConfig(item.printConfig, `${path}.printConfig`, push);
145
+ validateInputOptions(item, path, { entry, requiresOptions, optionsAsChoice, hasSpecificOptionEditor }, push);
146
+ }
147
+ function validatePrintConfig(printConfig, path, push) {
148
+ if (printConfig === void 0 || printConfig === null) return;
149
+ if (typeof printConfig === "string") {
150
+ if (printConfig.trim() === "") {
151
+ push(path, "PRINTCONFIG_EMPTY", "printConfig is an empty string; nothing is appended to the placeholder.", "info");
152
+ }
153
+ return;
154
+ }
155
+ if (!isPlainObject(printConfig)) {
156
+ push(
157
+ path,
158
+ "PRINTCONFIG_INVALID_TYPE",
159
+ "printConfig must be an object map of primitive values or a raw string; other shapes are ignored by the print renderer.",
160
+ "info"
161
+ );
162
+ return;
163
+ }
164
+ const entries = Object.entries(printConfig);
165
+ if (entries.length === 0) {
166
+ push(path, "PRINTCONFIG_EMPTY", "printConfig is an empty object; nothing is appended to the placeholder.", "info");
167
+ return;
168
+ }
169
+ for (const [key, value] of entries) {
170
+ const t = typeof value;
171
+ if (t !== "string" && t !== "number" && t !== "boolean") {
172
+ push(
173
+ `${path}.${key}`,
174
+ "PRINTCONFIG_VALUE_INVALID",
175
+ `printConfig.${key} must be a string, number, or boolean; the print renderer cannot append a ${Array.isArray(value) ? "array" : t}.`,
176
+ "info"
177
+ );
178
+ }
179
+ }
180
+ }
181
+ function validateInputOptions(item, path, ctx, push) {
182
+ const { requiresOptions, optionsAsChoice, hasSpecificOptionEditor } = ctx;
183
+ const inputType = item.inputType;
184
+ const opts = item.inputOptions;
185
+ const optsPath = `${path}.inputOptions`;
186
+ const hasOptions = opts !== void 0 && opts !== null && opts !== "";
187
+ if (requiresOptions && !hasSpecificOptionEditor && !hasOptions) {
188
+ push(optsPath, "OPTIONS_REQUIRED", `${inputType} requires inputOptions.`, "warning");
189
+ }
190
+ switch (inputType) {
191
+ case "CustomCode":
192
+ if (typeof item.inputCustomCode !== "string" || item.inputCustomCode.trim() === "") {
193
+ push(`${path}.inputCustomCode`, "CUSTOMCODE_EMPTY", "CustomCode requires inputCustomCode; otherwise it renders nothing.", "error");
194
+ }
195
+ return;
196
+ case "MasterAutocomplete":
197
+ if (hasOptions && (typeof opts !== "string" || opts.trim() === "")) {
198
+ push(optsPath, "OPTIONS_NOT_STRING", "MasterAutocomplete inputOptions must be a non-empty group-key string.", "warning");
199
+ }
200
+ return;
201
+ case "DocumentForm":
202
+ if (hasOptions && (typeof opts !== "string" || opts.trim() === "")) {
203
+ push(optsPath, "OPTIONS_NOT_STRING", "DocumentForm inputOptions must be the child template code (a non-empty string).", "warning");
204
+ }
205
+ return;
206
+ case "Header":
207
+ if (hasOptions && typeof opts !== "string") {
208
+ push(optsPath, "OPTIONS_NOT_STRING", "Header inputOptions should be a class string; non-strings are ignored.", "warning");
209
+ }
210
+ return;
211
+ case "FormHidden":
212
+ if (hasOptions && !isPlainObject(opts)) {
213
+ push(optsPath, "OPTIONS_NOT_OBJECT", "FormHidden inputOptions must be an object { itemValue?, hook? }.", "error");
214
+ } else if (isPlainObject(opts)) {
215
+ const o = opts;
216
+ if (o.hook !== void 0 && typeof o.hook !== "string") {
217
+ push(`${optsPath}.hook`, "OPTIONS_NOT_STRING", "FormHidden inputOptions.hook must be a string.", "warning");
218
+ }
219
+ }
220
+ return;
221
+ case "FormTable":
222
+ validateFormTableOptions(opts, optsPath, push);
223
+ return;
224
+ case "FormTableData":
225
+ validateFormTableDataOptions(opts, optsPath, push);
226
+ return;
227
+ default:
228
+ if (optionsAsChoice && hasOptions) {
229
+ validateChoiceOptions(opts, optsPath, push);
230
+ }
231
+ return;
232
+ }
233
+ }
234
+ function validateChoiceOptions(opts, path, push) {
235
+ if (typeof opts === "string") {
236
+ if (!CHOICE_STRING_REGEX.test(opts.trim())) {
237
+ push(
238
+ path,
239
+ "CHOICE_OPTIONS_INVALID",
240
+ "Choice inputOptions string is malformed (quotes or empty commas); it parses to zero options.",
241
+ "error"
242
+ );
243
+ }
244
+ return;
245
+ }
246
+ if (Array.isArray(opts)) {
247
+ if (!opts.every((it) => isPlainObject(it))) {
248
+ push(path, "CHOICE_OPTIONS_INVALID", "Choice inputOptions array must contain only objects.", "error");
249
+ return;
250
+ }
251
+ opts.forEach((it, i) => {
252
+ const o = it;
253
+ const hasLabel = o.label !== void 0 && o.label !== "";
254
+ const hasValue = o.value !== void 0 && o.value !== "";
255
+ if (!hasLabel && !hasValue) {
256
+ push(`${path}[${i}]`, "CHOICE_ITEM_EMPTY", "Choice item has neither label nor value; it renders blank.", "info");
257
+ }
258
+ });
259
+ return;
260
+ }
261
+ push(path, "CHOICE_OPTIONS_INVALID", "Choice inputOptions must be a compact string or an array of { label, value } objects.", "error");
262
+ }
263
+ function validateFormTableOptions(opts, path, push) {
264
+ if (!isPlainObject(opts)) {
265
+ push(path, "OPTIONS_NOT_OBJECT", "FormTable inputOptions must be an object with a formTemplate.", "error");
266
+ return;
267
+ }
268
+ const o = opts;
269
+ const formTemplate = coerceTemplateArray(o.formTemplate);
270
+ if (formTemplate === void 0) {
271
+ push(
272
+ `${path}.formTemplate`,
273
+ "FORMTABLE_NO_FORMTEMPLATE",
274
+ "FormTable requires a formTemplate (array of items or a JSON string) for the row editor.",
275
+ "error"
276
+ );
277
+ } else {
278
+ validateItemArray(formTemplate, `${path}.formTemplate`, push);
279
+ }
280
+ validateTableHeaders(o.headers, `${path}.headers`, push);
281
+ }
282
+ function validateFormTableDataOptions(opts, path, push) {
283
+ if (!isPlainObject(opts)) {
284
+ push(path, "OPTIONS_NOT_OBJECT", "FormTableData inputOptions must be an object with a dataTemplate.", "error");
285
+ return;
286
+ }
287
+ const o = opts;
288
+ const dataTemplate = coerceTemplateArray(o.dataTemplate);
289
+ const dataVars = /* @__PURE__ */ new Set();
290
+ if (dataTemplate === void 0) {
291
+ push(
292
+ `${path}.dataTemplate`,
293
+ "FORMTABLEDATA_NO_DATATEMPLATE",
294
+ "FormTableData has no dataTemplate; no editable columns are rendered.",
295
+ "warning"
296
+ );
297
+ } else {
298
+ validateItemArray(dataTemplate, `${path}.dataTemplate`, push);
299
+ for (const it of dataTemplate) {
300
+ if (typeof it.variableName === "string" && it.variableName.trim()) dataVars.add(it.variableName.trim());
301
+ }
302
+ }
303
+ const itemTemplate = coerceTemplateArray(o.itemTemplate);
304
+ if (itemTemplate !== void 0) validateItemArray(itemTemplate, `${path}.itemTemplate`, push);
305
+ validateTableHeaders(o.headers, `${path}.headers`, push);
306
+ if (o.disableApplyToAll !== void 0 && o.disableApplyToAll !== "") {
307
+ const val = o.disableApplyToAll;
308
+ if (val !== "false" && val !== "true" && val !== "partial") {
309
+ push(`${path}.disableApplyToAll`, "APPLYTOALL_INVALID", 'disableApplyToAll must be "false", "true", or "partial".', "warning");
310
+ } else if (val === "partial") {
311
+ const keys = typeof o.disableApplyToAllPartial === "string" ? o.disableApplyToAllPartial.split(",").map((s) => s.trim()).filter(Boolean) : [];
312
+ if (keys.length === 0) {
313
+ push(
314
+ `${path}.disableApplyToAllPartial`,
315
+ "APPLYTOALL_INVALID",
316
+ 'disableApplyToAll is "partial" but disableApplyToAllPartial lists no columns.',
317
+ "warning"
318
+ );
319
+ } else if (dataTemplate !== void 0) {
320
+ for (const k of keys) {
321
+ if (!dataVars.has(k)) {
322
+ push(
323
+ `${path}.disableApplyToAllPartial`,
324
+ "APPLYTOALL_KEY_UNKNOWN",
325
+ `disableApplyToAllPartial key "${k}" is not a variableName in dataTemplate.`,
326
+ "warning"
327
+ );
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+ }
334
+ function validateTableHeaders(headers, path, push) {
335
+ if (headers === void 0 || headers === null) return;
336
+ if (!Array.isArray(headers)) {
337
+ push(path, "OPTIONS_NOT_OBJECT", "headers must be an array.", "warning");
338
+ return;
339
+ }
340
+ headers.forEach((h, i) => {
341
+ if (!isPlainObject(h)) {
342
+ push(`${path}[${i}]`, "OPTIONS_NOT_OBJECT", "header entry must be an object.", "warning");
343
+ return;
344
+ }
345
+ const key = h.key;
346
+ if (key === void 0 || key === "") {
347
+ push(`${path}[${i}].key`, "HEADER_NO_KEY", "header entry has no key; the column cannot bind to a row property.", "warning");
348
+ }
349
+ });
350
+ }
351
+ function coerceTemplateArray(value) {
352
+ if (value === void 0 || value === null || value === "") return void 0;
353
+ if (Array.isArray(value)) return value;
354
+ if (typeof value === "string") {
355
+ try {
356
+ const parsed = JSON.parse(value);
357
+ return Array.isArray(parsed) ? parsed : void 0;
358
+ } catch {
359
+ return void 0;
360
+ }
361
+ }
362
+ return void 0;
363
+ }
364
+ function finalize(raw, strict) {
365
+ const issues = raw.map((r) => ({
366
+ path: r.path,
367
+ code: r.code,
368
+ message: r.message,
369
+ severity: strict && r.base === "warning" ? "error" : r.base
370
+ }));
371
+ const errors = issues.filter((i) => i.severity === "error");
372
+ const warnings = issues.filter((i) => i.severity === "warning");
373
+ const infos = issues.filter((i) => i.severity === "info");
374
+ return { valid: errors.length === 0, errors, warnings, infos, issues };
375
+ }
376
+ export function useDocumentTemplateValidate(source, options = {}) {
377
+ return computed(() => validateDocumentTemplate(toValue(source), options));
378
+ }
@@ -26,10 +26,29 @@ export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T):
26
26
  canUpdate: import("vue").ComputedRef<boolean>;
27
27
  canDelete: import("vue").ComputedRef<boolean>;
28
28
  createItem: (item: Record<string, any>, callback?: FormDialogCallback, importing?: boolean) => Promise<void>;
29
- importItems: (importItems: Record<string, any>[], callback?: FormDialogCallback) => void;
29
+ importItems: (importData: Record<string, any>[], callback?: FormDialogCallback) => void;
30
30
  updateItem: (item: Record<string, any>, callback?: FormDialogCallback) => 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
+ message: string;
44
+ }[], import("./importProgress.js").ImportError[] | {
45
+ index: number;
46
+ message: string;
47
+ }[]>;
48
+ percent: import("vue").ComputedRef<number>;
49
+ reset: () => void;
50
+ run: <T_1 = any>(items: T_1[], worker: import("./importProgress.js").ImportWorker<T_1>, options?: {
51
+ concurrency?: number;
52
+ }) => Promise<import("./importProgress.js").ImportSummary>;
53
+ };
35
54
  };
@@ -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 pLimit from "p-limit";
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(importItems2, callback) {
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 limit = pLimit(50);
76
- const importPromises = importItems2.map(
77
- (item) => limit(
78
- () => (item[props.modelKey || "id"] ? operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
79
- if (!result) {
80
- return createItem(Object.assign({}, props.initialData, item), void 0, true);
81
- }
82
- }) : createItem(Object.assign({}, props.initialData, item), void 0, true)).catch((error) => {
83
- alert?.addAlert({ alertType: "error", message: error });
84
- })
85
- )
86
- );
87
- Promise.all(importPromises).finally(() => {
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();
@@ -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,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.12",
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",