@unifylib/ui-lib 1.1.64 → 1.1.67
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/cli/generators/dashboard.js +147 -0
- package/cli/generators/form-screen.js +95 -0
- package/cli/generators/line-editor.js +110 -0
- package/cli/generators/permission-matrix.js +115 -0
- package/cli/generators/section-form.js +103 -0
- package/cli/generators/table-screen.js +181 -0
- package/cli/index.js +185 -0
- package/cli/utils.js +228 -0
- package/esm2022/lib/components/action-comment/action-comment.component.mjs +6 -5
- package/esm2022/lib/components/advanced-filter/filter-builder/filter-builder.component.mjs +8 -3
- package/esm2022/lib/components/audit-log-list/audit-log.component.mjs +3 -3
- package/esm2022/lib/components/auto-complete/auto-complete.component.mjs +11 -3
- package/esm2022/lib/components/base-form-canvas/base-form-canvas.component.mjs +50 -4
- package/esm2022/lib/components/base-table/base-table.component.mjs +73 -9
- package/esm2022/lib/components/button-actions/button-actions.component.mjs +3 -3
- package/esm2022/lib/components/editable-base-table/editable-base-table.component.mjs +3 -3
- package/esm2022/lib/components/equation-builder/equation-builder.component.mjs +9 -4
- package/esm2022/lib/components/multi-auto-complete/multi-auto-complete.component.mjs +20 -4
- package/esm2022/lib/components/paginator/paginator.component.mjs +26 -6
- package/esm2022/lib/components/report-details-dialog/report-details-dialog.component.mjs +3 -3
- package/esm2022/lib/components/report-form/report-form.component.mjs +3 -3
- package/esm2022/lib/components/search-bar/search-bar.component.mjs +5 -4
- package/esm2022/lib/components/section-form-canvas/section-form-canvas.component.mjs +3 -3
- package/esm2022/lib/components/shared/attachment-uploader/attachment-uploader.component.mjs +5 -3
- package/esm2022/lib/components/snackbar-static/snackbar-static.component.mjs +3 -3
- package/esm2022/lib/components/title-bar/title-bar.component.mjs +3 -3
- package/fesm2022/iq-ui-lib.mjs +233 -72
- package/fesm2022/iq-ui-lib.mjs.map +1 -1
- package/fesm2022/unifylib-ui-lib.mjs +233 -72
- package/fesm2022/unifylib-ui-lib.mjs.map +1 -1
- package/lib/components/advanced-filter/filter-builder/filter-builder.component.d.ts +2 -0
- package/lib/components/auto-complete/auto-complete.component.d.ts +2 -1
- package/lib/components/base-form-canvas/base-form-canvas.component.d.ts +4 -0
- package/lib/components/base-table/base-table.component.d.ts +10 -1
- package/lib/components/multi-auto-complete/multi-auto-complete.component.d.ts +3 -1
- package/lib/components/paginator/paginator.component.d.ts +6 -1
- package/lib/components/shared/attachment-uploader/attachment-uploader.component.d.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {{
|
|
5
|
+
* kebab: string,
|
|
6
|
+
* pascal: string,
|
|
7
|
+
* labelsSection: string,
|
|
8
|
+
* apiUri: string,
|
|
9
|
+
* importPath: string,
|
|
10
|
+
* }} ctx
|
|
11
|
+
*/
|
|
12
|
+
function generate(ctx) {
|
|
13
|
+
const fileBase = ctx.kebab;
|
|
14
|
+
|
|
15
|
+
const settings = `import { Validators } from "@angular/forms";
|
|
16
|
+
import {
|
|
17
|
+
ButtonActionSettings,
|
|
18
|
+
FieldInfo,
|
|
19
|
+
FilterRequest,
|
|
20
|
+
PageInfo,
|
|
21
|
+
ReportRequest,
|
|
22
|
+
TableColumn,
|
|
23
|
+
} from "${ctx.importPath}";
|
|
24
|
+
|
|
25
|
+
export const PAGE_INFO: PageInfo = {
|
|
26
|
+
id: "${ctx.kebab}",
|
|
27
|
+
labelsSection: "${ctx.labelsSection}",
|
|
28
|
+
hasAddButton: true,
|
|
29
|
+
apiUri: "${ctx.apiUri}",
|
|
30
|
+
selfRouting: true,
|
|
31
|
+
listRoutePath: "/${ctx.kebab}",
|
|
32
|
+
draftSupported: true,
|
|
33
|
+
pageSize: 10,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const COLUMNS: TableColumn<Record<string, unknown>>[] = [
|
|
37
|
+
{ property: "id", type: "text", enableSorting: true, visible: true, label: "id" },
|
|
38
|
+
{ property: "name", type: "text", enableSorting: true, visible: true, label: "name" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const FILTER_FIELDS: FieldInfo[] = [
|
|
42
|
+
new FieldInfo({
|
|
43
|
+
property: "name",
|
|
44
|
+
row: 1,
|
|
45
|
+
type: "text",
|
|
46
|
+
label: "name",
|
|
47
|
+
advancedFilter: true,
|
|
48
|
+
filterOperator: "LIKE",
|
|
49
|
+
validators: [Validators.maxLength(30)],
|
|
50
|
+
maxLength: 30,
|
|
51
|
+
} as FieldInfo),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function buildReportRequest(): ReportRequest {
|
|
55
|
+
const filterRequest = new FilterRequest(PAGE_INFO.pageSize ?? 10);
|
|
56
|
+
filterRequest.columns = COLUMNS.filter((c) => c.visible !== false).map((c) => c.property);
|
|
57
|
+
return new ReportRequest({ filterRequest });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const REPORT_REQUEST: ReportRequest = buildReportRequest();
|
|
61
|
+
|
|
62
|
+
export const LIST_ACTIONS: ButtonActionSettings[] = [
|
|
63
|
+
new ButtonActionSettings({
|
|
64
|
+
key: 1,
|
|
65
|
+
id: "view",
|
|
66
|
+
label: "VIEW",
|
|
67
|
+
linkHref: (row: unknown) => {
|
|
68
|
+
const id = (row as { id?: number }).id;
|
|
69
|
+
return id != null ? \`/${ctx.kebab}/view/\${id}\` : undefined;
|
|
70
|
+
},
|
|
71
|
+
} as ButtonActionSettings),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export const TOOLBAR_ACTIONS: ButtonActionSettings[] = [
|
|
75
|
+
new ButtonActionSettings({
|
|
76
|
+
key: 1,
|
|
77
|
+
id: "create",
|
|
78
|
+
label: "ADD_NEW",
|
|
79
|
+
linkHref: "/${ctx.kebab}/new",
|
|
80
|
+
} as ButtonActionSettings),
|
|
81
|
+
];
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const componentTs = `import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
|
|
85
|
+
import { Router } from "@angular/router";
|
|
86
|
+
import { TranslateModule } from "@ngx-translate/core";
|
|
87
|
+
import {
|
|
88
|
+
BaseTableComponent,
|
|
89
|
+
ButtonActionSettings,
|
|
90
|
+
Filter,
|
|
91
|
+
} from "${ctx.importPath}";
|
|
92
|
+
import {
|
|
93
|
+
COLUMNS,
|
|
94
|
+
FILTER_FIELDS,
|
|
95
|
+
LIST_ACTIONS,
|
|
96
|
+
PAGE_INFO,
|
|
97
|
+
REPORT_REQUEST,
|
|
98
|
+
TOOLBAR_ACTIONS,
|
|
99
|
+
} from "./${fileBase}.settings";
|
|
100
|
+
|
|
101
|
+
@Component({
|
|
102
|
+
selector: "app-${fileBase}",
|
|
103
|
+
standalone: true,
|
|
104
|
+
imports: [BaseTableComponent, TranslateModule],
|
|
105
|
+
templateUrl: "./${fileBase}.component.html",
|
|
106
|
+
styleUrls: ["./${fileBase}.component.scss"],
|
|
107
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
108
|
+
})
|
|
109
|
+
export class ${ctx.pascal}Component {
|
|
110
|
+
private readonly router = inject(Router);
|
|
111
|
+
|
|
112
|
+
readonly pageInfo = PAGE_INFO;
|
|
113
|
+
readonly columnsDefinition = COLUMNS;
|
|
114
|
+
readonly filterFieldDefinitions = FILTER_FIELDS;
|
|
115
|
+
readonly reportRequest = REPORT_REQUEST;
|
|
116
|
+
readonly filters: Filter[] = [];
|
|
117
|
+
readonly listActions = LIST_ACTIONS;
|
|
118
|
+
readonly toolbarActions = TOOLBAR_ACTIONS;
|
|
119
|
+
readonly isPending = false;
|
|
120
|
+
|
|
121
|
+
onListActionClicked(event: {
|
|
122
|
+
action: ButtonActionSettings;
|
|
123
|
+
row: unknown;
|
|
124
|
+
isPending: boolean;
|
|
125
|
+
}): void {
|
|
126
|
+
const href =
|
|
127
|
+
typeof event.action.linkHref === "function"
|
|
128
|
+
? event.action.linkHref(event.row)
|
|
129
|
+
: event.action.linkHref;
|
|
130
|
+
if (href) {
|
|
131
|
+
void this.router.navigateByUrl(href);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onToolbarButton(action: ButtonActionSettings): void {
|
|
136
|
+
if (typeof action.linkHref === "string") {
|
|
137
|
+
void this.router.navigateByUrl(action.linkHref);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const componentHtml = `<app-base-table
|
|
144
|
+
[pageInfo]="pageInfo"
|
|
145
|
+
[columns]="columnsDefinition"
|
|
146
|
+
[reportRequest]="reportRequest"
|
|
147
|
+
[filterFields]="filterFieldDefinitions"
|
|
148
|
+
[isPending]="isPending"
|
|
149
|
+
[filters]="filters"
|
|
150
|
+
[listAction]="listActions"
|
|
151
|
+
[extraActions]="toolbarActions"
|
|
152
|
+
[buttonsDisplayMode]="'list'"
|
|
153
|
+
(listActionClicked)="onListActionClicked($event)"
|
|
154
|
+
(buttonClicked)="onToolbarButton($event)">
|
|
155
|
+
</app-base-table>
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const componentScss = `:host {
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
{ relative: `${fileBase}.settings.ts`, content: settings },
|
|
165
|
+
{ relative: `${fileBase}.component.ts`, content: componentTs },
|
|
166
|
+
{ relative: `${fileBase}.component.html`, content: componentHtml },
|
|
167
|
+
{ relative: `${fileBase}.component.scss`, content: componentScss },
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getTranslations(ctx) {
|
|
172
|
+
return {
|
|
173
|
+
[`${ctx.labelsSection}.list_title`]: `${ctx.pascal} List`,
|
|
174
|
+
[`${ctx.labelsSection}.id`]: "ID",
|
|
175
|
+
[`${ctx.labelsSection}.name`]: "Name",
|
|
176
|
+
VIEW: "View",
|
|
177
|
+
ADD_NEW: "Add new",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { generate, getTranslations };
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const {
|
|
6
|
+
kebabCase,
|
|
7
|
+
pascalCase,
|
|
8
|
+
upperSnakeFromKebab,
|
|
9
|
+
lastPathSegment,
|
|
10
|
+
writeFiles,
|
|
11
|
+
upsertI18nEntries,
|
|
12
|
+
} = require("./utils");
|
|
13
|
+
|
|
14
|
+
const GENERATORS = {
|
|
15
|
+
"form-screen": require("./generators/form-screen.js"),
|
|
16
|
+
"table-screen": require("./generators/table-screen.js"),
|
|
17
|
+
"section-form": require("./generators/section-form.js"),
|
|
18
|
+
"line-editor": require("./generators/line-editor.js"),
|
|
19
|
+
dashboard: require("./generators/dashboard.js"),
|
|
20
|
+
"permission-matrix": require("./generators/permission-matrix.js"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
console.log(`
|
|
25
|
+
iq-ui — scaffold Angular screens that use @unifylib/ui-lib
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
iq-ui -g <type> <destination> [options]
|
|
29
|
+
|
|
30
|
+
Types:
|
|
31
|
+
form-screen app-base-form-canvas + settings
|
|
32
|
+
table-screen app-base-table + settings
|
|
33
|
+
section-form lib-section-form-canvas + settings
|
|
34
|
+
line-editor app-item-line-editor + settings
|
|
35
|
+
dashboard lib-action-card grid (no settings file)
|
|
36
|
+
permission-matrix lib-permission-matrix + settings
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--name <n> File/class base name (default: last segment of destination)
|
|
40
|
+
--labels-section <s> i18n labelsSection (default: UPPER_SNAKE from name)
|
|
41
|
+
--api-uri <u> API path for PageInfo.apiUri (default: /<name>)
|
|
42
|
+
--fields-count <n> Placeholder fields (default: 6; form-screen, section-form, line-editor)
|
|
43
|
+
--import-path <p> Library import (default: @iq/ui-lib)
|
|
44
|
+
--i18n-path <p> Optional translation file/folder (example: src/assets/en.json or src/assets)
|
|
45
|
+
--dry-run Print files that would be written
|
|
46
|
+
-h, --help Show this help
|
|
47
|
+
|
|
48
|
+
Examples (after npm install @unifylib/ui-lib, use the package binary; do not run \`npx iq-ui\`, which looks up a wrong package on npm):
|
|
49
|
+
npx -p @unifylib/ui-lib iq-ui -g form-screen src/app/registration --i18n-path src/assets/en.json
|
|
50
|
+
npx -p @unifylib/ui-lib iq-ui -g table-screen src/app/devices --labels-section DEVICE --api-uri /devices/list
|
|
51
|
+
npx -p @unifylib/ui-lib iq-ui -g dashboard src/app/home --import-path @unifylib/ui-lib
|
|
52
|
+
|
|
53
|
+
Local dev / linked build:
|
|
54
|
+
node node_modules/@unifylib/ui-lib/cli/index.js -g form-screen src/app/registration
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {{
|
|
60
|
+
* type?: string,
|
|
61
|
+
* destination?: string,
|
|
62
|
+
* name?: string,
|
|
63
|
+
* labelsSection?: string,
|
|
64
|
+
* apiUri?: string,
|
|
65
|
+
* fieldsCount: number,
|
|
66
|
+
* importPath: string,
|
|
67
|
+
* i18nPath?: string,
|
|
68
|
+
* dryRun: boolean,
|
|
69
|
+
* help: boolean,
|
|
70
|
+
* }}
|
|
71
|
+
*/
|
|
72
|
+
function parseArgs(argv) {
|
|
73
|
+
const out = {
|
|
74
|
+
fieldsCount: 6,
|
|
75
|
+
importPath: "@iq/ui-lib",
|
|
76
|
+
dryRun: false,
|
|
77
|
+
help: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (let i = 2; i < argv.length; i++) {
|
|
81
|
+
const a = argv[i];
|
|
82
|
+
if (a === "-h" || a === "--help") {
|
|
83
|
+
out.help = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (a === "--dry-run") {
|
|
87
|
+
out.dryRun = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (a === "-g" || a === "--generate") {
|
|
91
|
+
out.type = argv[++i];
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (a === "--name") {
|
|
95
|
+
out.name = argv[++i];
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (a === "--labels-section") {
|
|
99
|
+
out.labelsSection = argv[++i];
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (a === "--api-uri") {
|
|
103
|
+
out.apiUri = argv[++i];
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (a === "--fields-count") {
|
|
107
|
+
out.fieldsCount = parseInt(argv[++i], 10) || 6;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (a === "--import-path") {
|
|
111
|
+
out.importPath = argv[++i];
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (a === "--i18n-path") {
|
|
115
|
+
out.i18nPath = argv[++i];
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (a.startsWith("-")) {
|
|
119
|
+
console.error("Unknown option:", a);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
out.help = true;
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
if (!out.destination) {
|
|
125
|
+
out.destination = a;
|
|
126
|
+
} else {
|
|
127
|
+
console.error("Unexpected argument:", a);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
out.help = true;
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function main() {
|
|
137
|
+
const args = parseArgs(process.argv);
|
|
138
|
+
if (args.help) {
|
|
139
|
+
printHelp();
|
|
140
|
+
process.exit(process.exitCode || 0);
|
|
141
|
+
}
|
|
142
|
+
if (!args.type || !args.destination) {
|
|
143
|
+
console.error("Missing required: -g <type> and <destination>\n");
|
|
144
|
+
printHelp();
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const gen = GENERATORS[args.type];
|
|
149
|
+
if (!gen || typeof gen.generate !== "function") {
|
|
150
|
+
console.error("Unknown generator type:", args.type);
|
|
151
|
+
printHelp();
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rawName = args.name || lastPathSegment(args.destination);
|
|
156
|
+
const kebab = kebabCase(rawName);
|
|
157
|
+
const pascal = pascalCase(rawName);
|
|
158
|
+
const labelsSection = args.labelsSection || upperSnakeFromKebab(kebab);
|
|
159
|
+
const apiUri = args.apiUri != null ? args.apiUri : `/${kebab}`;
|
|
160
|
+
|
|
161
|
+
const destDir = path.isAbsolute(args.destination)
|
|
162
|
+
? args.destination
|
|
163
|
+
: path.join(process.cwd(), args.destination);
|
|
164
|
+
|
|
165
|
+
const ctx = {
|
|
166
|
+
kebab,
|
|
167
|
+
pascal,
|
|
168
|
+
labelsSection,
|
|
169
|
+
apiUri,
|
|
170
|
+
importPath: args.importPath,
|
|
171
|
+
fieldsCount: args.fieldsCount,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const files = gen.generate(ctx);
|
|
175
|
+
writeFiles({ destDir, dryRun: args.dryRun }, files);
|
|
176
|
+
|
|
177
|
+
if (args.i18nPath && typeof gen.getTranslations === "function") {
|
|
178
|
+
const translations = gen.getTranslations(ctx);
|
|
179
|
+
if (translations && Object.keys(translations).length > 0) {
|
|
180
|
+
upsertI18nEntries(args.i18nPath, translations, args.dryRun);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main();
|
package/cli/utils.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} destPath
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function lastPathSegment(destPath) {
|
|
11
|
+
const normalized = destPath.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
12
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
13
|
+
return parts[parts.length - 1] || "feature";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} str
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function kebabCase(str) {
|
|
21
|
+
return String(str)
|
|
22
|
+
.replace(/([a-z])([A-Z])/g, "$1-$2")
|
|
23
|
+
.replace(/[\s_]+/g, "-")
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
26
|
+
.replace(/-+/g, "-")
|
|
27
|
+
.replace(/^-|-$/g, "") || "feature";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} str
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function pascalCase(str) {
|
|
35
|
+
return kebabCase(str)
|
|
36
|
+
.split("-")
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} kebab
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function upperSnakeFromKebab(kebab) {
|
|
47
|
+
return kebabCase(kebab)
|
|
48
|
+
.split("-")
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((s) => s.toUpperCase())
|
|
51
|
+
.join("_");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} destDir
|
|
56
|
+
* @param {boolean} dryRun
|
|
57
|
+
*/
|
|
58
|
+
function ensureDir(destDir, dryRun) {
|
|
59
|
+
if (dryRun) return;
|
|
60
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} filePath
|
|
65
|
+
* @param {string} content
|
|
66
|
+
* @param {boolean} dryRun
|
|
67
|
+
*/
|
|
68
|
+
function writeFile(filePath, content, dryRun) {
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
console.log(`[dry-run] would write ${filePath} (${content.length} bytes)`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
74
|
+
console.log(`Wrote ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {{ destDir: string, dryRun: boolean }} opts
|
|
79
|
+
* @param {Array<{ relative: string, content: string }>} files
|
|
80
|
+
*/
|
|
81
|
+
function writeFiles(opts, files) {
|
|
82
|
+
ensureDir(opts.destDir, opts.dryRun);
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
const filePath = path.join(opts.destDir, f.relative);
|
|
85
|
+
writeFile(filePath, f.content, opts.dryRun);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {Record<string, unknown>} target
|
|
91
|
+
* @param {string[]} pathParts
|
|
92
|
+
* @param {string} value
|
|
93
|
+
*/
|
|
94
|
+
function setNested(target, pathParts, value) {
|
|
95
|
+
let cursor = target;
|
|
96
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
97
|
+
const part = pathParts[i];
|
|
98
|
+
const isLeaf = i === pathParts.length - 1;
|
|
99
|
+
if (isLeaf) {
|
|
100
|
+
cursor[part] = value;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (typeof cursor[part] !== "object" || cursor[part] === null || Array.isArray(cursor[part])) {
|
|
104
|
+
cursor[part] = {};
|
|
105
|
+
}
|
|
106
|
+
cursor = /** @type {Record<string, unknown>} */ (cursor[part]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} targetPath
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function resolveJsonTargets(targetPath) {
|
|
115
|
+
const fullPath = path.isAbsolute(targetPath) ? targetPath : path.join(process.cwd(), targetPath);
|
|
116
|
+
if (!fs.existsSync(fullPath)) {
|
|
117
|
+
return [fullPath];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const stat = fs.statSync(fullPath);
|
|
121
|
+
if (stat.isFile()) {
|
|
122
|
+
return [fullPath];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!stat.isDirectory()) {
|
|
126
|
+
return [fullPath];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @type {string[]} */
|
|
130
|
+
const out = [];
|
|
131
|
+
/** @param {string} fileName */
|
|
132
|
+
function isTranslationFileName(fileName) {
|
|
133
|
+
const lower = fileName.toLowerCase();
|
|
134
|
+
// Common translation file patterns
|
|
135
|
+
// - en.json / ar.json
|
|
136
|
+
// - en-us.json / ar-QA.json
|
|
137
|
+
// - messages.en.json / messages-ar.json
|
|
138
|
+
return (
|
|
139
|
+
/^(en|ar)([-_][a-z0-9]+)*\.json$/i.test(lower) ||
|
|
140
|
+
/\.((en|ar)([-_][a-z0-9]+)*)\.json$/i.test(lower) ||
|
|
141
|
+
lower.includes("i18n")
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
/** @param {string} dir */
|
|
145
|
+
function walk(dir) {
|
|
146
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const entryPath = path.join(dir, entry.name);
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
walk(entryPath);
|
|
151
|
+
} else if (entry.isFile() && isTranslationFileName(entry.name)) {
|
|
152
|
+
out.push(entryPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
walk(fullPath);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} i18nPath
|
|
163
|
+
* @param {Record<string, string>} entries
|
|
164
|
+
* @param {boolean} dryRun
|
|
165
|
+
*/
|
|
166
|
+
function upsertI18nEntries(i18nPath, entries, dryRun) {
|
|
167
|
+
const targets = resolveJsonTargets(i18nPath);
|
|
168
|
+
if (targets.length === 0) {
|
|
169
|
+
console.log(`No JSON files found in i18n target: ${i18nPath}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const fullPath of targets) {
|
|
174
|
+
if (dryRun) {
|
|
175
|
+
console.log(`[dry-run] would update i18n file ${fullPath} with ${Object.keys(entries).length} keys`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let data = {};
|
|
180
|
+
if (fs.existsSync(fullPath)) {
|
|
181
|
+
const raw = fs.readFileSync(fullPath, "utf8").trim();
|
|
182
|
+
data = raw ? JSON.parse(raw) : {};
|
|
183
|
+
} else {
|
|
184
|
+
const parent = path.dirname(fullPath);
|
|
185
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
189
|
+
const parts = key.split(".").filter(Boolean);
|
|
190
|
+
if (parts.length === 0) continue;
|
|
191
|
+
setNested(data, parts, value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(fullPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
195
|
+
console.log(`Updated i18n ${fullPath}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Generate placeholder FieldInfo rows (2 per row by default).
|
|
201
|
+
* @param {number} count
|
|
202
|
+
* @param {number} perRow
|
|
203
|
+
* @param {string} indent
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function buildFieldInfoLines(count, perRow, indent) {
|
|
207
|
+
const lines = [];
|
|
208
|
+
for (let i = 1; i <= count; i++) {
|
|
209
|
+
const row = Math.ceil(i / perRow);
|
|
210
|
+
lines.push(
|
|
211
|
+
`${indent}new FieldInfo({ row: ${row}, property: "field${i}", label: "field${i}", required: false, type: "text", validators: [Validators.maxLength(100)], validationMessages: { maxlength: "VALIDATION_MAX_100" } })`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return lines.join(",\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
lastPathSegment,
|
|
219
|
+
kebabCase,
|
|
220
|
+
pascalCase,
|
|
221
|
+
upperSnakeFromKebab,
|
|
222
|
+
ensureDir,
|
|
223
|
+
writeFile,
|
|
224
|
+
writeFiles,
|
|
225
|
+
buildFieldInfoLines,
|
|
226
|
+
resolveJsonTargets,
|
|
227
|
+
upsertI18nEntries,
|
|
228
|
+
};
|