@mk-kit/ui 0.37.0 → 0.39.0
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/README.md +16 -6
- package/fesm2022/mk-kit-ui-core.mjs +2 -0
- package/fesm2022/mk-kit-ui-core.mjs.map +1 -1
- package/fesm2022/mk-kit-ui-dynamic-form.mjs +578 -0
- package/fesm2022/mk-kit-ui-dynamic-form.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-testing.mjs +834 -0
- package/fesm2022/mk-kit-ui-testing.mjs.map +1 -0
- package/fesm2022/mk-kit-ui.mjs +1 -0
- package/fesm2022/mk-kit-ui.mjs.map +1 -1
- package/package.json +9 -1
- package/schematics/collection.json +5 -0
- package/schematics/migrate-primeng/index.js +76 -0
- package/schematics/migrate-primeng/mapping.js +289 -0
- package/schematics/migrate-primeng/schema.json +25 -0
- package/schematics/migrate-primeng/transform.js +325 -0
- package/types/mk-kit-ui-core.d.ts +4 -0
- package/types/mk-kit-ui-dynamic-form.d.ts +305 -0
- package/types/mk-kit-ui-testing.d.ts +344 -0
- package/types/mk-kit-ui.d.ts +1 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MK_STYLE_PATH = void 0;
|
|
4
|
+
exports.transformTypeScript = transformTypeScript;
|
|
5
|
+
exports.transformTemplate = transformTemplate;
|
|
6
|
+
exports.transformAngularJson = transformAngularJson;
|
|
7
|
+
exports.transformPackageJson = transformPackageJson;
|
|
8
|
+
exports.renderReport = renderReport;
|
|
9
|
+
/**
|
|
10
|
+
* Pure string transforms behind the `migrate-primeng` schematic. No devkit
|
|
11
|
+
* types here so they can be unit-tested on fixtures and reused by other
|
|
12
|
+
* tooling. Every function returns the new text plus what it did.
|
|
13
|
+
*/
|
|
14
|
+
const mapping_1 = require("./mapping");
|
|
15
|
+
const MODULE_BY_PATH = new Map(mapping_1.MODULES.map((m) => [m.path, m]));
|
|
16
|
+
function escapeRe(s) {
|
|
17
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
function docsUrl(route) {
|
|
20
|
+
return route ? `${mapping_1.DOCS_BASE}${route}` : undefined;
|
|
21
|
+
}
|
|
22
|
+
/* ------------------------------------------------------------------------ */
|
|
23
|
+
/* TypeScript */
|
|
24
|
+
/* ------------------------------------------------------------------------ */
|
|
25
|
+
const IMPORT_RE = /import\s*(type\s+)?\{([^}]*)\}\s*from\s*['"]primeng\/([\w-]+)['"];?[ \t]*\r?\n?/g;
|
|
26
|
+
/**
|
|
27
|
+
* Rewrites `primeng/*` imports to `@mk-kit/ui`, renames the imported
|
|
28
|
+
* identifiers everywhere in the file, keeps unmapped symbols on their
|
|
29
|
+
* original import, and dedupes `imports: [...]` arrays.
|
|
30
|
+
*/
|
|
31
|
+
function transformTypeScript(source) {
|
|
32
|
+
const findings = [];
|
|
33
|
+
const mkNames = new Set();
|
|
34
|
+
const renames = new Map();
|
|
35
|
+
const keep = [];
|
|
36
|
+
let text = source;
|
|
37
|
+
let firstImportIndex = -1;
|
|
38
|
+
text = text.replace(IMPORT_RE, (whole, isType, names, path, offset) => {
|
|
39
|
+
const rule = MODULE_BY_PATH.get(path);
|
|
40
|
+
if (firstImportIndex === -1)
|
|
41
|
+
firstImportIndex = offset;
|
|
42
|
+
const symbols = names
|
|
43
|
+
.split(',')
|
|
44
|
+
.map((n) => n.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
const unmapped = [];
|
|
47
|
+
for (const raw of symbols) {
|
|
48
|
+
const [name, alias] = raw.split(/\s+as\s+/).map((s) => s.trim());
|
|
49
|
+
const target = rule?.symbols[name];
|
|
50
|
+
if (target) {
|
|
51
|
+
mkNames.add(target);
|
|
52
|
+
renames.set(alias ?? name, target);
|
|
53
|
+
add(findings, `import:primeng/${path}`, `${name} → ${target}`, 'rewrite', rule?.docs);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
unmapped.push(raw);
|
|
57
|
+
add(findings, `import:primeng/${path}`, `${name} (primeng/${path}) has no drop-in equivalent — kept`, rule ? 'manual' : 'unmapped', rule?.docs);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (unmapped.length)
|
|
61
|
+
keep.push(`import ${isType ?? ''}{ ${unmapped.join(', ')} } from 'primeng/${path}';\n`);
|
|
62
|
+
if (rule?.note && symbols.some((s) => rule.symbols[s.split(/\s+as\s+/)[0].trim()])) {
|
|
63
|
+
add(findings, `note:primeng/${path}`, rule.note, 'manual', rule.docs);
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
});
|
|
67
|
+
if (firstImportIndex === -1)
|
|
68
|
+
return { text: source, changed: false, findings };
|
|
69
|
+
// Rename identifiers (whole words) — includes `imports: [...]` entries and injected services.
|
|
70
|
+
for (const [from, to] of renames) {
|
|
71
|
+
if (from === to)
|
|
72
|
+
continue;
|
|
73
|
+
text = text.replace(new RegExp(`\\b${escapeRe(from)}\\b`, 'g'), to);
|
|
74
|
+
}
|
|
75
|
+
// Dedupe entries inside `imports: [ ... ]` arrays (several PrimeNG symbols map to one class).
|
|
76
|
+
text = text.replace(/imports:\s*\[([^\]]*)\]/g, (whole, inner) => {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const items = inner
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((s) => s.trim())
|
|
81
|
+
.filter((s) => s && !seen.has(s) && seen.add(s));
|
|
82
|
+
const multiline = inner.includes('\n');
|
|
83
|
+
return multiline ? `imports: [\n ${items.join(',\n ')},\n ]` : `imports: [${items.join(', ')}]`;
|
|
84
|
+
});
|
|
85
|
+
// Insert the mk-kit import where the first primeng import was, merging with an existing one.
|
|
86
|
+
const header = [...(keep.length ? keep : [])];
|
|
87
|
+
if (mkNames.size) {
|
|
88
|
+
const existing = /import\s*\{([^}]*)\}\s*from\s*['"]@mk-kit\/ui['"];?[ \t]*\r?\n?/;
|
|
89
|
+
const m = existing.exec(text);
|
|
90
|
+
if (m) {
|
|
91
|
+
const merged = new Set(m[1].split(',').map((s) => s.trim()).filter(Boolean));
|
|
92
|
+
for (const n of mkNames)
|
|
93
|
+
merged.add(n);
|
|
94
|
+
text = text.replace(existing, `import { ${[...merged].join(', ')} } from '@mk-kit/ui';\n`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
header.unshift(`import { ${[...mkNames].join(', ')} } from '@mk-kit/ui';\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
text = text.slice(0, firstImportIndex) + header.join('') + text.slice(firstImportIndex);
|
|
101
|
+
// Service usage hints (MessageService / ConfirmationService bodies are not mechanical).
|
|
102
|
+
if (renames.has('MessageService')) {
|
|
103
|
+
text = text.replace(/(\n[ \t]*)([^\n]*\.add\(\s*\{[^\n]*severity[^\n]*)/g, '$1// mk-kit: MessageService.add({severity, summary, detail}) → toast.<severity>(detail, { title: summary })$1$2');
|
|
104
|
+
}
|
|
105
|
+
if (renames.has('ConfirmationService')) {
|
|
106
|
+
text = text.replace(/(\n[ \t]*)([^\n]*\.confirm\(\s*\{[^\n]*)/g, '$1// mk-kit: ConfirmationService.confirm({...}) → await dialog.confirm({ title, message, tone }) returns boolean$1$2');
|
|
107
|
+
}
|
|
108
|
+
// Inline templates get the template pass too.
|
|
109
|
+
const tpl = transformTemplate(text, true);
|
|
110
|
+
text = tpl.text;
|
|
111
|
+
findings.push(...tpl.findings);
|
|
112
|
+
return { text, changed: text !== source, findings };
|
|
113
|
+
}
|
|
114
|
+
/* ------------------------------------------------------------------------ */
|
|
115
|
+
/* Templates */
|
|
116
|
+
/* ------------------------------------------------------------------------ */
|
|
117
|
+
/** Longest selectors first so `p-inputgroupaddon` is not eaten by `p-inputgroup`. */
|
|
118
|
+
const ELEMENT_RULES = mapping_1.SELECTORS.filter((r) => r.kind === 'element').sort((a, b) => b.from.length - a.from.length);
|
|
119
|
+
const ATTRIBUTE_RULES = mapping_1.SELECTORS.filter((r) => r.kind === 'attribute').sort((a, b) => b.from.length - a.from.length);
|
|
120
|
+
function noteComment(rule, inline) {
|
|
121
|
+
const text = `mk-kit: ${rule.from} → ${rule.to ?? 'manual'} — ${rule.note ?? ''}`.trim();
|
|
122
|
+
// Inside a TS template literal `-->` is fine; keep one comment style everywhere.
|
|
123
|
+
return inline ? `<!-- ${text} -->` : `<!-- ${text} -->`;
|
|
124
|
+
}
|
|
125
|
+
function renameAttrs(openTag, attrs) {
|
|
126
|
+
if (!attrs)
|
|
127
|
+
return openTag;
|
|
128
|
+
let out = openTag;
|
|
129
|
+
for (const [from, to] of Object.entries(attrs)) {
|
|
130
|
+
const re = new RegExp(`(\\s)${escapeRe(from)}(?=[\\s=>/])`, 'g');
|
|
131
|
+
out = out.replace(re, (_m, ws) => (to === null ? ws.trimEnd() : `${ws}${to}`));
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Rewrites PrimeNG selectors in a template (an .html file, or a whole .ts file
|
|
137
|
+
* when `inline` — only `<p-…` tags and known attribute tokens are touched).
|
|
138
|
+
*/
|
|
139
|
+
function transformTemplate(source, inline = false) {
|
|
140
|
+
const findings = [];
|
|
141
|
+
let text = source;
|
|
142
|
+
for (const rule of ELEMENT_RULES) {
|
|
143
|
+
const open = new RegExp(`<${escapeRe(rule.from)}(?=[\\s>/])`, 'g');
|
|
144
|
+
const close = new RegExp(`</${escapeRe(rule.from)}\\s*>`, 'g');
|
|
145
|
+
const matches = text.match(open);
|
|
146
|
+
if (!matches)
|
|
147
|
+
continue;
|
|
148
|
+
const n = matches.length;
|
|
149
|
+
if (rule.to) {
|
|
150
|
+
// Rename open/close tags, then attributes on each opening tag.
|
|
151
|
+
text = text.replace(open, `<${rule.to}`).replace(close, `</${rule.to}>`);
|
|
152
|
+
if (rule.attrs) {
|
|
153
|
+
const tagRe = new RegExp(`<${escapeRe(rule.to)}(\\s[^>]*)?>`, 'g');
|
|
154
|
+
text = text.replace(tagRe, (tag) => renameAttrs(tag, rule.attrs));
|
|
155
|
+
}
|
|
156
|
+
add(findings, `element:${rule.from}`, `<${rule.from}> → <${rule.to}>`, 'rewrite', rule.docs, n);
|
|
157
|
+
if (rule.manual && rule.note) {
|
|
158
|
+
text = insertNote(text, `<${rule.to}`, noteComment(rule, inline));
|
|
159
|
+
add(findings, `manual:${rule.from}`, rule.note, 'manual', rule.docs, n);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
text = insertNote(text, `<${rule.from}`, noteComment(rule, inline));
|
|
164
|
+
add(findings, `element:${rule.from}`, rule.note ?? 'no drop-in equivalent', 'unmapped', rule.docs, n);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const rule of ATTRIBUTE_RULES) {
|
|
168
|
+
// Attribute token forms: `pButton`, `[pTooltip]="…"`, `pTooltip="…"`.
|
|
169
|
+
const token = new RegExp(`(?<=[\\s\\[])${escapeRe(rule.from)}(?=[\\s=\\]>/])`, 'g');
|
|
170
|
+
const matches = text.match(token);
|
|
171
|
+
if (!matches)
|
|
172
|
+
continue;
|
|
173
|
+
const n = matches.length;
|
|
174
|
+
if (rule.to) {
|
|
175
|
+
text = text.replace(token, rule.to);
|
|
176
|
+
if (rule.attrs) {
|
|
177
|
+
const tagRe = new RegExp(`<[a-zA-Z][^>]*\\b${escapeRe(rule.to)}\\b[^>]*>`, 'g');
|
|
178
|
+
text = text.replace(tagRe, (tag) => renameAttrs(tag, rule.attrs));
|
|
179
|
+
}
|
|
180
|
+
add(findings, `attr:${rule.from}`, `${rule.from} → ${rule.to}`, 'rewrite', rule.docs, n);
|
|
181
|
+
if (rule.manual && rule.note) {
|
|
182
|
+
text = insertNoteBeforeTagWith(text, rule.to, noteComment(rule, inline));
|
|
183
|
+
add(findings, `manual:${rule.from}`, rule.note, 'manual', rule.docs, n);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
text = insertNoteBeforeTagWith(text, rule.from, noteComment(rule, inline));
|
|
188
|
+
add(findings, `attr:${rule.from}`, rule.note ?? 'no drop-in equivalent', 'unmapped', rule.docs, n);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { text, changed: text !== source, findings };
|
|
192
|
+
}
|
|
193
|
+
/** Insert `note` once, right before the first occurrence of `needle` (keeps indentation). */
|
|
194
|
+
function insertNote(text, needle, note) {
|
|
195
|
+
if (text.includes(note))
|
|
196
|
+
return text;
|
|
197
|
+
const i = text.indexOf(needle);
|
|
198
|
+
if (i === -1)
|
|
199
|
+
return text;
|
|
200
|
+
const lineStart = text.lastIndexOf('\n', i) + 1;
|
|
201
|
+
const indent = text.slice(lineStart, i).match(/^[ \t]*$/) ? text.slice(lineStart, i) : '';
|
|
202
|
+
return indent ? text.slice(0, lineStart) + indent + note + '\n' + text.slice(lineStart) : text.slice(0, i) + note + ' ' + text.slice(i);
|
|
203
|
+
}
|
|
204
|
+
/** Insert `note` once before the first tag that carries `attr`. */
|
|
205
|
+
function insertNoteBeforeTagWith(text, attr, note) {
|
|
206
|
+
if (text.includes(note))
|
|
207
|
+
return text;
|
|
208
|
+
const re = new RegExp(`<[a-zA-Z][^>]*(?<=[\\s\\[])${escapeRe(attr)}(?=[\\s=\\]>/])`);
|
|
209
|
+
const m = re.exec(text);
|
|
210
|
+
if (!m)
|
|
211
|
+
return text;
|
|
212
|
+
return insertNote(text, m[0], note);
|
|
213
|
+
}
|
|
214
|
+
/* ------------------------------------------------------------------------ */
|
|
215
|
+
/* Workspace files */
|
|
216
|
+
/* ------------------------------------------------------------------------ */
|
|
217
|
+
exports.MK_STYLE_PATH = 'node_modules/@mk-kit/ui/styles/mk-kit.css';
|
|
218
|
+
/** Remove PrimeNG / primeicons / primeflex style entries from every project's `styles`; add the mk-kit theme. */
|
|
219
|
+
function transformAngularJson(source) {
|
|
220
|
+
const findings = [];
|
|
221
|
+
let json;
|
|
222
|
+
try {
|
|
223
|
+
json = JSON.parse(source);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return { text: source, changed: false, findings };
|
|
227
|
+
}
|
|
228
|
+
let changed = false;
|
|
229
|
+
for (const project of Object.values(json.projects ?? {})) {
|
|
230
|
+
const options = project?.architect?.build?.options;
|
|
231
|
+
if (!options || !Array.isArray(options.styles))
|
|
232
|
+
continue;
|
|
233
|
+
const before = options.styles.length;
|
|
234
|
+
options.styles = options.styles.filter((s) => {
|
|
235
|
+
const p = typeof s === 'string' ? s : s?.input ?? '';
|
|
236
|
+
return !/prime(ng|icons|flex|uix)/i.test(p);
|
|
237
|
+
});
|
|
238
|
+
if (options.styles.length !== before) {
|
|
239
|
+
changed = true;
|
|
240
|
+
add(findings, 'styles:primeng', `removed ${before - options.styles.length} PrimeNG/primeicons style entries`, 'rewrite', '/theming', before - options.styles.length);
|
|
241
|
+
}
|
|
242
|
+
const has = options.styles.some((s) => (typeof s === 'string' ? s : s?.input ?? '').includes('@mk-kit/ui/styles'));
|
|
243
|
+
if (!has) {
|
|
244
|
+
options.styles.unshift(exports.MK_STYLE_PATH);
|
|
245
|
+
changed = true;
|
|
246
|
+
add(findings, 'styles:mk-kit', `added ${exports.MK_STYLE_PATH}`, 'rewrite', '/theming');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { text: changed ? JSON.stringify(json, null, 2) + '\n' : source, changed, findings };
|
|
250
|
+
}
|
|
251
|
+
/** Drop PrimeNG packages from dependencies and make sure @mk-kit/ui is present. */
|
|
252
|
+
function transformPackageJson(source, packages, mkVersion = '^0.38.0') {
|
|
253
|
+
const findings = [];
|
|
254
|
+
let json;
|
|
255
|
+
try {
|
|
256
|
+
json = JSON.parse(source);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return { text: source, changed: false, findings };
|
|
260
|
+
}
|
|
261
|
+
let changed = false;
|
|
262
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
263
|
+
const deps = json[field];
|
|
264
|
+
if (!deps)
|
|
265
|
+
continue;
|
|
266
|
+
for (const p of packages) {
|
|
267
|
+
if (p in deps) {
|
|
268
|
+
delete deps[p];
|
|
269
|
+
changed = true;
|
|
270
|
+
add(findings, 'package:remove', `removed ${p} from ${field}`, 'rewrite');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
json.dependencies ??= {};
|
|
275
|
+
if (!json.dependencies['@mk-kit/ui']) {
|
|
276
|
+
json.dependencies['@mk-kit/ui'] = mkVersion;
|
|
277
|
+
changed = true;
|
|
278
|
+
add(findings, 'package:add', `added @mk-kit/ui ${mkVersion}`, 'rewrite');
|
|
279
|
+
}
|
|
280
|
+
return { text: changed ? JSON.stringify(json, null, 2) + '\n' : source, changed, findings };
|
|
281
|
+
}
|
|
282
|
+
function add(findings, rule, message, kind, docs, count = 1) {
|
|
283
|
+
const existing = findings.find((f) => f.rule === rule && f.message === message);
|
|
284
|
+
if (existing)
|
|
285
|
+
existing.count += count;
|
|
286
|
+
else
|
|
287
|
+
findings.push({ rule, message, kind, docs: docsUrl(docs), count });
|
|
288
|
+
}
|
|
289
|
+
/** Markdown report of a whole run. */
|
|
290
|
+
function renderReport(files, options) {
|
|
291
|
+
const all = files.flatMap((f) => f.findings.map((x) => ({ ...x, file: f.path })));
|
|
292
|
+
const changed = files.filter((f) => f.changed);
|
|
293
|
+
const byKind = (kind) => all.filter((f) => f.kind === kind);
|
|
294
|
+
const sum = (list) => list.reduce((n, f) => n + f.count, 0);
|
|
295
|
+
const lines = [];
|
|
296
|
+
lines.push('# PrimeNG → mk-kit migration report', '');
|
|
297
|
+
lines.push(`${options.dryRun ? '**Dry run** — nothing was written. ' : ''}Scanned ${options.scanned} files, ${options.dryRun ? 'would change' : 'changed'} ${changed.length}.`, '');
|
|
298
|
+
lines.push(`- Automatic rewrites: **${sum(byKind('rewrite'))}**`);
|
|
299
|
+
lines.push(`- Needs a manual touch: **${sum(byKind('manual'))}**`);
|
|
300
|
+
lines.push(`- No mk-kit equivalent: **${sum(byKind('unmapped'))}**`, '');
|
|
301
|
+
lines.push('## Rewrites', '');
|
|
302
|
+
const rewrites = group(byKind('rewrite'));
|
|
303
|
+
lines.push(...(rewrites.length ? rewrites.map(([msg, n]) => `- ${msg} ×${n}`) : ['- (none)']), '');
|
|
304
|
+
lines.push('## Manual steps', '', 'Look for `<!-- mk-kit: … -->` and `// mk-kit:` comments in the changed files.', '');
|
|
305
|
+
const manual = group(byKind('manual'), true);
|
|
306
|
+
lines.push(...(manual.length ? manual.map(([msg, n, docs]) => `- ${msg} ×${n}${docs ? ` — [docs](${docs})` : ''}`) : ['- (none)']), '');
|
|
307
|
+
lines.push('## Not available in mk-kit', '');
|
|
308
|
+
const unmapped = group(byKind('unmapped'), true);
|
|
309
|
+
lines.push(...(unmapped.length ? unmapped.map(([msg, n, docs]) => `- ${msg} ×${n}${docs ? ` — closest: [docs](${docs})` : ''}`) : ['- (none)']), '');
|
|
310
|
+
lines.push('## Files', '');
|
|
311
|
+
lines.push(...(changed.length ? changed.map((f) => `- \`${f.path}\` — ${f.findings.map((x) => `${x.rule}×${x.count}`).join(', ')}`) : ['- (none)']), '');
|
|
312
|
+
lines.push('## Next', '');
|
|
313
|
+
lines.push('1. Run `ng build` and fix the remaining template errors — most are the `<!-- mk-kit -->` notes above.', '2. Replace `MessageService.add(...)` calls with `MkToastService` and `ConfirmationService.confirm(...)` with `await MkDialogService.confirm(...)`.', '3. Icons: `pi pi-*` classes → `<mk-icon name="…" />` (see https://mk-kit.dev/components/icon).', `4. Theme: \`--mk-*\` tokens replace the PrimeNG preset — https://mk-kit.dev/theming.`, '');
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
function group(list, withDocs = false) {
|
|
317
|
+
const map = new Map();
|
|
318
|
+
for (const f of list) {
|
|
319
|
+
const key = f.message;
|
|
320
|
+
const cur = map.get(key) ?? [0, f.docs];
|
|
321
|
+
cur[0] += f.count;
|
|
322
|
+
map.set(key, cur);
|
|
323
|
+
}
|
|
324
|
+
return [...map.entries()].map(([msg, [n, docs]]) => [msg, n, withDocs ? docs : undefined]);
|
|
325
|
+
}
|
|
@@ -973,6 +973,10 @@ interface MkI18nStrings {
|
|
|
973
973
|
chatToolDone: string;
|
|
974
974
|
/** Chat: tool call failed. */
|
|
975
975
|
chatToolError: string;
|
|
976
|
+
/** Dynamic form: add an item to an array field. */
|
|
977
|
+
dynamicFormAdd: string;
|
|
978
|
+
/** Dynamic form: remove one item of an array field (index is 1-based). */
|
|
979
|
+
dynamicFormRemove: (index: number) => string;
|
|
976
980
|
/** Query builder: accessible name of the whole builder. */
|
|
977
981
|
queryBuilderLabel: string;
|
|
978
982
|
queryAddRule: string;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { ValidatorFn, AbstractControl, FormGroup, FormControl, FormArray } from '@angular/forms';
|
|
2
|
+
import * as _mk_kit_ui_core from '@mk-kit/ui/core';
|
|
3
|
+
import * as _angular_core from '@angular/core';
|
|
4
|
+
import { TemplateRef } from '@angular/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Field kinds `mk-dynamic-form` renders out of the box. Each maps to one
|
|
8
|
+
* mk-kit control; `custom` renders a projected `mkDynamicField` template.
|
|
9
|
+
*/
|
|
10
|
+
type MkDynamicFieldType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'textarea' | 'number' | 'currency' | 'date' | 'time' | 'datetime' | 'select' | 'multi-select' | 'autocomplete' | 'radio' | 'toggle' | 'checkbox' | 'switch' | 'slider' | 'rating' | 'color' | 'tags' | 'phone' | 'file' | 'code' | 'custom';
|
|
11
|
+
/** One option of a select / radio / toggle / autocomplete field. */
|
|
12
|
+
interface MkDynamicOption {
|
|
13
|
+
label: string;
|
|
14
|
+
value: unknown;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Declarative validators — plain data so a schema can be stored as JSON.
|
|
19
|
+
* `custom` takes Angular validator functions for anything else.
|
|
20
|
+
*/
|
|
21
|
+
interface MkDynamicValidators {
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
minLength?: number;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
/** A RegExp or its source string. */
|
|
27
|
+
pattern?: string | RegExp;
|
|
28
|
+
email?: boolean;
|
|
29
|
+
/** Angular validator functions appended after the declarative ones. */
|
|
30
|
+
custom?: ValidatorFn[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A serialisable condition over the form value (dotted paths reach into
|
|
34
|
+
* groups: `address.country`). Combine with `and` / `or`; a function is the
|
|
35
|
+
* escape hatch for TypeScript-only schemas.
|
|
36
|
+
*/
|
|
37
|
+
type MkDynamicCondition = {
|
|
38
|
+
field: string;
|
|
39
|
+
eq?: unknown;
|
|
40
|
+
neq?: unknown;
|
|
41
|
+
in?: unknown[];
|
|
42
|
+
notIn?: unknown[];
|
|
43
|
+
truthy?: boolean;
|
|
44
|
+
empty?: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
and: MkDynamicCondition[];
|
|
47
|
+
} | {
|
|
48
|
+
or: MkDynamicCondition[];
|
|
49
|
+
} | {
|
|
50
|
+
not: MkDynamicCondition;
|
|
51
|
+
} | ((value: Record<string, unknown>) => boolean);
|
|
52
|
+
/** Properties shared by every field that holds a value. */
|
|
53
|
+
interface MkDynamicFieldBase {
|
|
54
|
+
/** Key in the form value. */
|
|
55
|
+
key: string;
|
|
56
|
+
type: MkDynamicFieldType;
|
|
57
|
+
label?: string;
|
|
58
|
+
hint?: string;
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
required?: boolean;
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** Initial value when the form (or a new array item) is created. */
|
|
63
|
+
default?: unknown;
|
|
64
|
+
validators?: MkDynamicValidators;
|
|
65
|
+
/** Options for select / multi-select / radio / toggle / autocomplete. */
|
|
66
|
+
options?: readonly MkDynamicOption[];
|
|
67
|
+
/**
|
|
68
|
+
* Extra inputs forwarded to the underlying control — the subset each type
|
|
69
|
+
* understands (`rows`, `min`, `max`, `step`, `currency`, `accept`,
|
|
70
|
+
* `multiple`, `swatches`, `language`, …). See the docs table.
|
|
71
|
+
*/
|
|
72
|
+
props?: Record<string, unknown>;
|
|
73
|
+
/** Grid columns (1–12) the field spans. Default: the form's `columns` split. */
|
|
74
|
+
span?: number;
|
|
75
|
+
/** Render (and enable) only when the condition holds. Hidden fields are excluded from the value. */
|
|
76
|
+
showWhen?: MkDynamicCondition;
|
|
77
|
+
/** Disable while the condition holds. */
|
|
78
|
+
disabledWhen?: MkDynamicCondition;
|
|
79
|
+
}
|
|
80
|
+
/** A nested object: its fields become a child `FormGroup` under `key`. */
|
|
81
|
+
interface MkDynamicGroup {
|
|
82
|
+
type: 'group';
|
|
83
|
+
key: string;
|
|
84
|
+
label?: string;
|
|
85
|
+
hint?: string;
|
|
86
|
+
fields: MkDynamicField[];
|
|
87
|
+
span?: number;
|
|
88
|
+
/** Columns for the group's own grid (default: inherits the form's). */
|
|
89
|
+
columns?: number;
|
|
90
|
+
showWhen?: MkDynamicCondition;
|
|
91
|
+
disabledWhen?: MkDynamicCondition;
|
|
92
|
+
}
|
|
93
|
+
/** A list of objects: a `FormArray` of groups with add / remove controls. */
|
|
94
|
+
interface MkDynamicArray {
|
|
95
|
+
type: 'array';
|
|
96
|
+
key: string;
|
|
97
|
+
label?: string;
|
|
98
|
+
hint?: string;
|
|
99
|
+
/** Fields of one item. */
|
|
100
|
+
fields: MkDynamicField[];
|
|
101
|
+
min?: number;
|
|
102
|
+
max?: number;
|
|
103
|
+
addLabel?: string;
|
|
104
|
+
/** Initial items (each is patched over the item defaults). */
|
|
105
|
+
default?: Record<string, unknown>[];
|
|
106
|
+
span?: number;
|
|
107
|
+
columns?: number;
|
|
108
|
+
showWhen?: MkDynamicCondition;
|
|
109
|
+
disabledWhen?: MkDynamicCondition;
|
|
110
|
+
}
|
|
111
|
+
/** A heading + description with no value of its own. */
|
|
112
|
+
interface MkDynamicSection {
|
|
113
|
+
type: 'section';
|
|
114
|
+
label: string;
|
|
115
|
+
hint?: string;
|
|
116
|
+
span?: number;
|
|
117
|
+
showWhen?: MkDynamicCondition;
|
|
118
|
+
}
|
|
119
|
+
type MkDynamicField = MkDynamicFieldBase | MkDynamicGroup | MkDynamicArray | MkDynamicSection;
|
|
120
|
+
/** The whole form. */
|
|
121
|
+
interface MkDynamicSchema {
|
|
122
|
+
fields: MkDynamicField[];
|
|
123
|
+
/** Default number of columns fields are laid out in (1–12). Default 1. */
|
|
124
|
+
columns?: number;
|
|
125
|
+
}
|
|
126
|
+
declare function mkIsGroup(f: MkDynamicField): f is MkDynamicGroup;
|
|
127
|
+
declare function mkIsArray(f: MkDynamicField): f is MkDynamicArray;
|
|
128
|
+
declare function mkIsSection(f: MkDynamicField): f is MkDynamicSection;
|
|
129
|
+
declare function mkIsValueField(f: MkDynamicField): f is MkDynamicFieldBase;
|
|
130
|
+
|
|
131
|
+
/** The empty value a field type starts with when no `default` is given. */
|
|
132
|
+
declare function mkDynamicEmptyValue(field: MkDynamicFieldBase): unknown;
|
|
133
|
+
/** Default value object of a field list (groups nested, arrays as item lists). */
|
|
134
|
+
declare function mkDynamicDefaults(fields: readonly MkDynamicField[]): Record<string, unknown>;
|
|
135
|
+
/** Angular validators for one value field, from its declarative `validators` + `required`. */
|
|
136
|
+
declare function mkDynamicValidators(field: MkDynamicFieldBase): ValidatorFn[];
|
|
137
|
+
/** Build the `FormControl` / `FormGroup` / `FormArray` for one field. */
|
|
138
|
+
declare function mkDynamicControl(field: MkDynamicField, value?: unknown): AbstractControl;
|
|
139
|
+
/** A `FormGroup` for a field list; `value` (partial) overrides the defaults. */
|
|
140
|
+
declare function mkDynamicGroup(fields: readonly MkDynamicField[], value?: Record<string, unknown>): FormGroup;
|
|
141
|
+
/** Build the reactive form of a whole schema. */
|
|
142
|
+
declare function mkDynamicForm(schema: MkDynamicSchema, value?: Record<string, unknown>): FormGroup;
|
|
143
|
+
/**
|
|
144
|
+
* Evaluate a condition against a form value. `value` is normally the whole
|
|
145
|
+
* form value; inside an array item the item's own value is used, with the
|
|
146
|
+
* root available under `$root`.
|
|
147
|
+
*/
|
|
148
|
+
declare function mkDynamicCondition(cond: MkDynamicCondition | undefined, value: Record<string, unknown>): boolean;
|
|
149
|
+
/** Depth-first list of every value field with its dotted path. */
|
|
150
|
+
declare function mkDynamicFlatten(fields: readonly MkDynamicField[], prefix?: string): Array<{
|
|
151
|
+
path: string;
|
|
152
|
+
field: MkDynamicFieldBase | MkDynamicGroup | MkDynamicArray;
|
|
153
|
+
}>;
|
|
154
|
+
/** Column span a field takes in a grid of `columns`. */
|
|
155
|
+
declare function mkDynamicSpan(field: MkDynamicField, columns: number): number;
|
|
156
|
+
|
|
157
|
+
/** Context handed to a custom field template. */
|
|
158
|
+
interface MkDynamicFieldContext {
|
|
159
|
+
$implicit: MkDynamicFieldBase;
|
|
160
|
+
field: MkDynamicFieldBase;
|
|
161
|
+
/** The field's `FormControl` — bind it with `[formControl]`. */
|
|
162
|
+
control: FormControl;
|
|
163
|
+
/** The current value of the whole form (or array item). */
|
|
164
|
+
value: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Registers a renderer for a custom field type:
|
|
168
|
+
*
|
|
169
|
+
* ```html
|
|
170
|
+
* <mk-dynamic-form [schema]="schema">
|
|
171
|
+
* <ng-template mkDynamicField="signature" let-field let-control="control">
|
|
172
|
+
* <mk-signature-pad [formControl]="control" />
|
|
173
|
+
* </ng-template>
|
|
174
|
+
* </mk-dynamic-form>
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* A field `{ type: 'custom', key: 'sig', props: { renderer: 'signature' } }`
|
|
178
|
+
* (or any built-in `type` you want to override) then renders this template
|
|
179
|
+
* inside the usual `mk-form-field`.
|
|
180
|
+
*/
|
|
181
|
+
declare class MkDynamicFieldDef {
|
|
182
|
+
/** The type (or `props.renderer` name) this template renders. */
|
|
183
|
+
readonly mkDynamicField: _angular_core.InputSignal<string>;
|
|
184
|
+
readonly template: TemplateRef<MkDynamicFieldContext>;
|
|
185
|
+
static ngTemplateContextGuard(_dir: MkDynamicFieldDef, ctx: unknown): ctx is MkDynamicFieldContext;
|
|
186
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MkDynamicFieldDef, never>;
|
|
187
|
+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<MkDynamicFieldDef, "ng-template[mkDynamicField]", never, { "mkDynamicField": { "alias": "mkDynamicField"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Renders one field list (the root, a group, or one array item) as a grid.
|
|
191
|
+
* Internal — projected by {@link MkDynamicForm}; recursive for groups/arrays.
|
|
192
|
+
*/
|
|
193
|
+
declare class MkDynamicFields {
|
|
194
|
+
private readonly root;
|
|
195
|
+
protected readonly i18n: _mk_kit_ui_core.MkI18nStrings;
|
|
196
|
+
readonly fields: _angular_core.InputSignal<readonly MkDynamicField[]>;
|
|
197
|
+
readonly group: _angular_core.InputSignal<FormGroup<any>>;
|
|
198
|
+
readonly columns: _angular_core.InputSignalWithTransform<number, unknown>;
|
|
199
|
+
/** Value the conditions of this level are evaluated against. */
|
|
200
|
+
readonly scope: _angular_core.InputSignal<Record<string, unknown>>;
|
|
201
|
+
protected readonly labelPosition: _angular_core.Signal<any>;
|
|
202
|
+
protected readonly size: _angular_core.Signal<any>;
|
|
203
|
+
protected control(f: MkDynamicField): AbstractControl;
|
|
204
|
+
protected formGroup(f: MkDynamicGroup): FormGroup;
|
|
205
|
+
protected formArray(f: MkDynamicArray): FormArray<FormGroup>;
|
|
206
|
+
protected isVisible(f: MkDynamicField): boolean;
|
|
207
|
+
protected span(f: MkDynamicField): number;
|
|
208
|
+
protected columnsOf(f: MkDynamicGroup | MkDynamicArray): number;
|
|
209
|
+
protected isGroup: typeof mkIsGroup;
|
|
210
|
+
protected isArray: typeof mkIsArray;
|
|
211
|
+
protected isSection: typeof mkIsSection;
|
|
212
|
+
protected isValue: typeof mkIsValueField;
|
|
213
|
+
/** Custom template for a value field, if one is registered. */
|
|
214
|
+
protected customTemplate(f: MkDynamicFieldBase): TemplateRef<MkDynamicFieldContext> | null;
|
|
215
|
+
protected customContext(f: MkDynamicFieldBase): MkDynamicFieldContext;
|
|
216
|
+
/** `props.x` with a typed default. */
|
|
217
|
+
protected p(f: MkDynamicFieldBase, key: string, fallback: unknown): any;
|
|
218
|
+
protected inputType(f: MkDynamicFieldBase): string;
|
|
219
|
+
protected itemScope(f: MkDynamicArray, index: number): Record<string, unknown>;
|
|
220
|
+
protected canAdd(f: MkDynamicArray): boolean;
|
|
221
|
+
protected canRemove(f: MkDynamicArray): boolean;
|
|
222
|
+
protected addItem(f: MkDynamicArray): void;
|
|
223
|
+
protected removeItem(f: MkDynamicArray, index: number): void;
|
|
224
|
+
protected trackItem(index: number, item: FormGroup): FormGroup;
|
|
225
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MkDynamicFields, never>;
|
|
226
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<MkDynamicFields, "mk-dynamic-fields", never, { "fields": { "alias": "fields"; "required": true; "isSignal": true; }; "group": { "alias": "group"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": false; "isSignal": true; }; "scope": { "alias": "scope"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Dynamic form — renders a form from a JSON schema and manages one reactive
|
|
230
|
+
* `FormGroup` for it. Every value field renders inside `mk-form-field`, so
|
|
231
|
+
* labels, hints, required marks and localised validation messages come from
|
|
232
|
+
* the schema alone.
|
|
233
|
+
*
|
|
234
|
+
* ```html
|
|
235
|
+
* <mk-dynamic-form [schema]="schema" [(value)]="user" (formSubmit)="save($event)">
|
|
236
|
+
* <button mkButton type="submit">Save</button>
|
|
237
|
+
* </mk-dynamic-form>
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* schema: MkDynamicSchema = {
|
|
242
|
+
* columns: 2,
|
|
243
|
+
* fields: [
|
|
244
|
+
* { key: 'name', type: 'text', label: 'Name', required: true },
|
|
245
|
+
* { key: 'role', type: 'select', label: 'Role', options: roles },
|
|
246
|
+
* { key: 'company', type: 'text', label: 'Company', showWhen: { field: 'role', eq: 'b2b' } },
|
|
247
|
+
* ],
|
|
248
|
+
* };
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* - Hidden fields (`showWhen` false) are disabled, so `value` only carries
|
|
252
|
+
* what the user can see; `form.getRawValue()` has everything.
|
|
253
|
+
* - `form` is the live `FormGroup` for anything the schema does not cover.
|
|
254
|
+
* - Custom field types: project an `ng-template[mkDynamicField]`.
|
|
255
|
+
*/
|
|
256
|
+
declare class MkDynamicForm {
|
|
257
|
+
private readonly destroyRef;
|
|
258
|
+
/** The schema. Changing it rebuilds the form (values of surviving keys are kept). */
|
|
259
|
+
readonly schema: _angular_core.InputSignal<MkDynamicSchema>;
|
|
260
|
+
/** Two-way form value (visible, enabled fields only). */
|
|
261
|
+
readonly value: _angular_core.ModelSignal<Record<string, unknown>>;
|
|
262
|
+
/** Disable every control. */
|
|
263
|
+
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
|
|
264
|
+
/** Label placement forwarded to every `mk-form-field`. */
|
|
265
|
+
readonly labelPosition: _angular_core.InputSignal<"top" | "float">;
|
|
266
|
+
/** Control size forwarded to the fields. */
|
|
267
|
+
readonly size: _angular_core.InputSignal<"sm" | "md" | "lg">;
|
|
268
|
+
/** Emits the value when the form is submitted and valid. */
|
|
269
|
+
readonly formSubmit: _angular_core.OutputEmitterRef<Record<string, unknown>>;
|
|
270
|
+
/** Emits when a submit is attempted while invalid (every control is marked touched). */
|
|
271
|
+
readonly invalidSubmit: _angular_core.OutputEmitterRef<FormGroup<any>>;
|
|
272
|
+
private readonly defs;
|
|
273
|
+
private readonly formSignal;
|
|
274
|
+
private readonly formValue;
|
|
275
|
+
private subscription;
|
|
276
|
+
private applying;
|
|
277
|
+
/** The live reactive form. */
|
|
278
|
+
get form(): FormGroup;
|
|
279
|
+
protected readonly formRef: _angular_core.Signal<FormGroup<any>>;
|
|
280
|
+
/** Value including disabled fields — what conditions are evaluated against. */
|
|
281
|
+
protected readonly scope: _angular_core.Signal<Record<string, unknown>>;
|
|
282
|
+
protected readonly columns: _angular_core.Signal<number>;
|
|
283
|
+
protected readonly fields: _angular_core.Signal<MkDynamicField[]>;
|
|
284
|
+
/** `true` while every control passes validation. */
|
|
285
|
+
readonly valid: _angular_core.Signal<boolean>;
|
|
286
|
+
constructor();
|
|
287
|
+
private attach;
|
|
288
|
+
/** Re-evaluate conditions and push the form value to `value`. @internal */
|
|
289
|
+
sync(): void;
|
|
290
|
+
private applyConditions;
|
|
291
|
+
/** Template registered for a custom type / renderer name. @internal */
|
|
292
|
+
templateFor(name: string): TemplateRef<MkDynamicFieldContext> | null;
|
|
293
|
+
/** Patch part of the value. */
|
|
294
|
+
patch(value: Record<string, unknown>): void;
|
|
295
|
+
/** Reset to the schema defaults (or the given value). */
|
|
296
|
+
reset(value?: Record<string, unknown>): void;
|
|
297
|
+
/** Mark every control touched so validation messages show. */
|
|
298
|
+
touchAll(): void;
|
|
299
|
+
protected onSubmit(event: Event): void;
|
|
300
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MkDynamicForm, never>;
|
|
301
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<MkDynamicForm, "mk-dynamic-form", never, { "schema": { "alias": "schema"; "required": true; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "labelPosition": { "alias": "labelPosition"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "formSubmit": "formSubmit"; "invalidSubmit": "invalidSubmit"; }, ["defs"], ["*"], true, never>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { MkDynamicFieldDef, MkDynamicFields, MkDynamicForm, mkDynamicCondition, mkDynamicControl, mkDynamicDefaults, mkDynamicEmptyValue, mkDynamicFlatten, mkDynamicForm, mkDynamicGroup, mkDynamicSpan, mkDynamicValidators, mkIsArray, mkIsGroup, mkIsSection, mkIsValueField };
|
|
305
|
+
export type { MkDynamicArray, MkDynamicCondition, MkDynamicField, MkDynamicFieldBase, MkDynamicFieldContext, MkDynamicFieldType, MkDynamicGroup, MkDynamicGroup as MkDynamicGroupField, MkDynamicOption, MkDynamicSchema, MkDynamicSection, MkDynamicValidators };
|