zinkee 0.1.25 → 0.1.26
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/.claude/settings.local.json +4 -1
- package/README.md +45 -1
- package/dist/index.js +281 -38
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/displays.test.ts +237 -0
- package/src/commands/displays.ts +288 -6
- package/src/utils/examples.test.ts +19 -0
- package/src/utils/examples.ts +113 -32
package/package.json
CHANGED
|
@@ -252,6 +252,243 @@ describe("displays commands", () => {
|
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
it("builds KANBAN viewState fieldMapping, cardConfig and aggregation from flags", async () => {
|
|
256
|
+
const configPath = createApiConfig();
|
|
257
|
+
const output = createStdoutBuffer();
|
|
258
|
+
const fetchMock = createFetchMock();
|
|
259
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-k1" }));
|
|
260
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
261
|
+
|
|
262
|
+
await program.parseAsync([
|
|
263
|
+
"node",
|
|
264
|
+
"zinkee",
|
|
265
|
+
"--json",
|
|
266
|
+
"displays",
|
|
267
|
+
"widgets",
|
|
268
|
+
"create",
|
|
269
|
+
"display-1",
|
|
270
|
+
"--type",
|
|
271
|
+
"kanban",
|
|
272
|
+
"--field",
|
|
273
|
+
"name-uuid",
|
|
274
|
+
"--column-field",
|
|
275
|
+
"status-uuid",
|
|
276
|
+
"--swimlane-field",
|
|
277
|
+
"owner-uuid",
|
|
278
|
+
"--card-title-field",
|
|
279
|
+
"name-uuid",
|
|
280
|
+
"--card-category3-field",
|
|
281
|
+
"amount-uuid",
|
|
282
|
+
"--aggregation-method",
|
|
283
|
+
"sum",
|
|
284
|
+
"--aggregation-field",
|
|
285
|
+
"amount-uuid",
|
|
286
|
+
"--view-state",
|
|
287
|
+
'{"showNoValueColumn":true,"hideEmptyColumns":false}',
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
291
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
292
|
+
type: "KANBAN",
|
|
293
|
+
view: {
|
|
294
|
+
fields: ["name-uuid"],
|
|
295
|
+
viewAdditionalProperties: {
|
|
296
|
+
viewState: {
|
|
297
|
+
showNoValueColumn: true,
|
|
298
|
+
hideEmptyColumns: false,
|
|
299
|
+
fieldMapping: {
|
|
300
|
+
columnField: "status-uuid",
|
|
301
|
+
swimlaneField: "owner-uuid",
|
|
302
|
+
cardConfig: { titleField: "name-uuid", category3Field: "amount-uuid" },
|
|
303
|
+
aggregationConfig: { method: "sum", fieldId: "amount-uuid" },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("builds TIMELINE viewState fieldMapping with category4 from flags", async () => {
|
|
312
|
+
const configPath = createApiConfig();
|
|
313
|
+
const output = createStdoutBuffer();
|
|
314
|
+
const fetchMock = createFetchMock();
|
|
315
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-t1" }));
|
|
316
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
317
|
+
|
|
318
|
+
await program.parseAsync([
|
|
319
|
+
"node",
|
|
320
|
+
"zinkee",
|
|
321
|
+
"--json",
|
|
322
|
+
"displays",
|
|
323
|
+
"widgets",
|
|
324
|
+
"create",
|
|
325
|
+
"display-1",
|
|
326
|
+
"--type",
|
|
327
|
+
"timeline",
|
|
328
|
+
"--start-date-field",
|
|
329
|
+
"start-uuid",
|
|
330
|
+
"--end-date-field",
|
|
331
|
+
"end-uuid",
|
|
332
|
+
"--label-field",
|
|
333
|
+
"name-uuid",
|
|
334
|
+
"--card-category4-field",
|
|
335
|
+
"amount-uuid",
|
|
336
|
+
"--view-state",
|
|
337
|
+
'{"granularity":"month","sidebarWidth":300}',
|
|
338
|
+
]);
|
|
339
|
+
|
|
340
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
341
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
342
|
+
type: "TIMELINE",
|
|
343
|
+
view: {
|
|
344
|
+
viewAdditionalProperties: {
|
|
345
|
+
viewState: {
|
|
346
|
+
granularity: "month",
|
|
347
|
+
sidebarWidth: 300,
|
|
348
|
+
fieldMapping: {
|
|
349
|
+
startDateField: "start-uuid",
|
|
350
|
+
endDateField: "end-uuid",
|
|
351
|
+
labelField: "name-uuid",
|
|
352
|
+
cardConfig: { category4Field: "amount-uuid" },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("builds HIERARCHY hierarchyConfig from columns, root and child flags", async () => {
|
|
361
|
+
const configPath = createApiConfig();
|
|
362
|
+
const output = createStdoutBuffer();
|
|
363
|
+
const fetchMock = createFetchMock();
|
|
364
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-h1" }));
|
|
365
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
366
|
+
|
|
367
|
+
await program.parseAsync([
|
|
368
|
+
"node",
|
|
369
|
+
"zinkee",
|
|
370
|
+
"--json",
|
|
371
|
+
"displays",
|
|
372
|
+
"widgets",
|
|
373
|
+
"create",
|
|
374
|
+
"display-1",
|
|
375
|
+
"--type",
|
|
376
|
+
"hierarchy",
|
|
377
|
+
"--hierarchy-label",
|
|
378
|
+
"Name",
|
|
379
|
+
"--hierarchy-info",
|
|
380
|
+
"col-info-status:Status",
|
|
381
|
+
"--hierarchy-number",
|
|
382
|
+
"col-num-amount:Total amount:sum",
|
|
383
|
+
"--hierarchy-root-name",
|
|
384
|
+
"Customers",
|
|
385
|
+
"--hierarchy-root-label-field",
|
|
386
|
+
"name-uuid",
|
|
387
|
+
"--hierarchy-root-info",
|
|
388
|
+
"col-info-status:status-uuid",
|
|
389
|
+
"--hierarchy-root-number",
|
|
390
|
+
"col-num-amount:amount-uuid",
|
|
391
|
+
"--hierarchy-child",
|
|
392
|
+
'{"name":"Projects","schemaId":"projects-uuid","childFieldId":"child-uuid","labelFieldId":"project-name-uuid","infoMappings":[],"numberMappings":[{"columnId":"col-num-amount","fieldId":"project-amount-uuid"}]}',
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
396
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
397
|
+
type: "HIERARCHY",
|
|
398
|
+
view: {
|
|
399
|
+
hierarchyConfig: {
|
|
400
|
+
columns: {
|
|
401
|
+
label: "Name",
|
|
402
|
+
infos: [{ id: "col-info-status", name: "Status" }],
|
|
403
|
+
numbers: [{ id: "col-num-amount", name: "Total amount", aggregation: "SUM" }],
|
|
404
|
+
},
|
|
405
|
+
rootLevel: {
|
|
406
|
+
name: "Customers",
|
|
407
|
+
labelFieldId: "name-uuid",
|
|
408
|
+
infoMappings: [{ columnId: "col-info-status", fieldId: "status-uuid" }],
|
|
409
|
+
numberMappings: [{ columnId: "col-num-amount", fieldId: "amount-uuid" }],
|
|
410
|
+
},
|
|
411
|
+
childLevels: [
|
|
412
|
+
{
|
|
413
|
+
name: "Projects",
|
|
414
|
+
schemaId: "projects-uuid",
|
|
415
|
+
childFieldId: "child-uuid",
|
|
416
|
+
labelFieldId: "project-name-uuid",
|
|
417
|
+
infoMappings: [],
|
|
418
|
+
numberMappings: [{ columnId: "col-num-amount", fieldId: "project-amount-uuid" }],
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("deep merges KANBAN flags over view state coming from --raw", async () => {
|
|
427
|
+
const configPath = createApiConfig();
|
|
428
|
+
const output = createStdoutBuffer();
|
|
429
|
+
const fetchMock = createFetchMock();
|
|
430
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-k2" }));
|
|
431
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
432
|
+
|
|
433
|
+
await program.parseAsync([
|
|
434
|
+
"node",
|
|
435
|
+
"zinkee",
|
|
436
|
+
"--json",
|
|
437
|
+
"displays",
|
|
438
|
+
"widgets",
|
|
439
|
+
"create",
|
|
440
|
+
"display-1",
|
|
441
|
+
"--type",
|
|
442
|
+
"kanban",
|
|
443
|
+
"--raw",
|
|
444
|
+
'{"view":{"viewAdditionalProperties":{"viewState":{"columnOrder":["a","b"],"fieldMapping":{"columnField":"old-uuid","cardConfig":{"titleField":"name-uuid"}}}}}}',
|
|
445
|
+
"--column-field",
|
|
446
|
+
"status-uuid",
|
|
447
|
+
"--card-category1-field",
|
|
448
|
+
"status-uuid",
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
452
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
453
|
+
type: "KANBAN",
|
|
454
|
+
view: {
|
|
455
|
+
viewAdditionalProperties: {
|
|
456
|
+
viewState: {
|
|
457
|
+
columnOrder: ["a", "b"],
|
|
458
|
+
fieldMapping: {
|
|
459
|
+
columnField: "status-uuid",
|
|
460
|
+
cardConfig: { titleField: "name-uuid", category1Field: "status-uuid" },
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("rejects --hierarchy-number with an invalid aggregation", async () => {
|
|
469
|
+
const configPath = createApiConfig();
|
|
470
|
+
const output = createStdoutBuffer();
|
|
471
|
+
const fetchMock = createFetchMock();
|
|
472
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
473
|
+
|
|
474
|
+
await expect(
|
|
475
|
+
program.parseAsync([
|
|
476
|
+
"node",
|
|
477
|
+
"zinkee",
|
|
478
|
+
"--json",
|
|
479
|
+
"displays",
|
|
480
|
+
"widgets",
|
|
481
|
+
"create",
|
|
482
|
+
"display-1",
|
|
483
|
+
"--type",
|
|
484
|
+
"hierarchy",
|
|
485
|
+
"--hierarchy-number",
|
|
486
|
+
"col-num-amount:Total:BOGUS",
|
|
487
|
+
]),
|
|
488
|
+
).rejects.toThrow(/aggregation/i);
|
|
489
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
490
|
+
});
|
|
491
|
+
|
|
255
492
|
it("translates --filter into the structured display filter contract", async () => {
|
|
256
493
|
const configPath = createApiConfig();
|
|
257
494
|
const output = createStdoutBuffer();
|
package/src/commands/displays.ts
CHANGED
|
@@ -107,6 +107,36 @@ interface WidgetMutationOptions {
|
|
|
107
107
|
xAxisTimeSeries?: boolean;
|
|
108
108
|
yAxisSerie?: string[];
|
|
109
109
|
legend?: string;
|
|
110
|
+
// KANBAN / TIMELINE viewState.fieldMapping (UUIDs only; slugs are not resolved here)
|
|
111
|
+
columnField?: string;
|
|
112
|
+
swimlaneField?: string;
|
|
113
|
+
startDateField?: string;
|
|
114
|
+
endDateField?: string;
|
|
115
|
+
labelField?: string;
|
|
116
|
+
groupField?: string;
|
|
117
|
+
subgroupField?: string;
|
|
118
|
+
progressField?: string;
|
|
119
|
+
cardTitleField?: string;
|
|
120
|
+
cardCategory1Field?: string;
|
|
121
|
+
cardCategory2Field?: string;
|
|
122
|
+
cardCategory3Field?: string;
|
|
123
|
+
cardCategory4Field?: string;
|
|
124
|
+
cardDescriptionField?: string;
|
|
125
|
+
aggregationMethod?: string;
|
|
126
|
+
aggregationField?: string;
|
|
127
|
+
viewState?: string;
|
|
128
|
+
viewStateFile?: string;
|
|
129
|
+
// HIERARCHY view.hierarchyConfig (UUIDs only; slugs are not resolved here)
|
|
130
|
+
hierarchyLabel?: string;
|
|
131
|
+
hierarchyInfo?: string[];
|
|
132
|
+
hierarchyNumber?: string[];
|
|
133
|
+
hierarchyRootName?: string;
|
|
134
|
+
hierarchyRootLabelField?: string;
|
|
135
|
+
hierarchyRootInfo?: string[];
|
|
136
|
+
hierarchyRootNumber?: string[];
|
|
137
|
+
hierarchyChild?: string[];
|
|
138
|
+
hierarchyConfig?: string;
|
|
139
|
+
hierarchyConfigFile?: string;
|
|
110
140
|
raw?: string;
|
|
111
141
|
rawFile?: string;
|
|
112
142
|
}
|
|
@@ -295,6 +325,175 @@ function isEmptyObject(value: object): boolean {
|
|
|
295
325
|
return Object.keys(value).length === 0;
|
|
296
326
|
}
|
|
297
327
|
|
|
328
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
329
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function deepMerge(
|
|
333
|
+
target: Record<string, unknown>,
|
|
334
|
+
source: Record<string, unknown>,
|
|
335
|
+
): Record<string, unknown> {
|
|
336
|
+
const out: Record<string, unknown> = { ...target };
|
|
337
|
+
for (const [key, value] of Object.entries(source)) {
|
|
338
|
+
const existing = out[key];
|
|
339
|
+
out[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value;
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function assignDefined(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
345
|
+
if (value !== undefined) target[key] = value;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readNamedJsonObject(
|
|
349
|
+
raw: string | undefined,
|
|
350
|
+
rawFile: string | undefined,
|
|
351
|
+
inlineFlag: string,
|
|
352
|
+
fileFlag: string,
|
|
353
|
+
): Record<string, unknown> | undefined {
|
|
354
|
+
const parsed = readNamedJsonOption(raw, rawFile, inlineFlag, fileFlag);
|
|
355
|
+
if (parsed === undefined) return undefined;
|
|
356
|
+
if (!isPlainObject(parsed)) {
|
|
357
|
+
throw new CliError("invalid_cli_usage", `${inlineFlag} payload must be a JSON object.`);
|
|
358
|
+
}
|
|
359
|
+
return parsed;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const HIERARCHY_AGGREGATIONS = new Set([
|
|
363
|
+
"SUM",
|
|
364
|
+
"AVERAGE",
|
|
365
|
+
"MINIMUM",
|
|
366
|
+
"MAXIMUM",
|
|
367
|
+
"COUNT",
|
|
368
|
+
"COUNT_DISTINCT",
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
function parseHierarchyInfo(value: string): Record<string, unknown> {
|
|
372
|
+
const idx = value.indexOf(":");
|
|
373
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
374
|
+
throw new CliError("invalid_cli_usage", `--hierarchy-info "${value}" must use the format <id:name>.`);
|
|
375
|
+
}
|
|
376
|
+
return { id: value.slice(0, idx).trim(), name: value.slice(idx + 1).trim() };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseHierarchyNumber(value: string): Record<string, unknown> {
|
|
380
|
+
const parts = value.split(":");
|
|
381
|
+
if (parts.length < 3) {
|
|
382
|
+
throw new CliError(
|
|
383
|
+
"invalid_cli_usage",
|
|
384
|
+
`--hierarchy-number "${value}" must use the format <id:name:aggregation>.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const id = parts[0]?.trim() ?? "";
|
|
388
|
+
const aggregation = (parts[parts.length - 1]?.trim() ?? "").toUpperCase();
|
|
389
|
+
const name = parts.slice(1, -1).join(":").trim();
|
|
390
|
+
if (!id || !name) {
|
|
391
|
+
throw new CliError(
|
|
392
|
+
"invalid_cli_usage",
|
|
393
|
+
`--hierarchy-number "${value}" must use the format <id:name:aggregation>.`,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!HIERARCHY_AGGREGATIONS.has(aggregation)) {
|
|
397
|
+
throw new CliError(
|
|
398
|
+
"invalid_cli_usage",
|
|
399
|
+
`--hierarchy-number aggregation "${aggregation}" must be one of SUM, AVERAGE, MINIMUM, MAXIMUM, COUNT, COUNT_DISTINCT.`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return { id, name, aggregation };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function parseHierarchyMapping(value: string, flag: string): Record<string, unknown> {
|
|
406
|
+
const idx = value.indexOf(":");
|
|
407
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
408
|
+
throw new CliError("invalid_cli_usage", `${flag} "${value}" must use the format <columnId:fieldId>.`);
|
|
409
|
+
}
|
|
410
|
+
return { columnId: value.slice(0, idx).trim(), fieldId: value.slice(idx + 1).trim() };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function buildWidgetFieldMapping(options: WidgetMutationOptions): Record<string, unknown> | undefined {
|
|
414
|
+
const mapping: Record<string, unknown> = {};
|
|
415
|
+
assignDefined(mapping, "columnField", options.columnField);
|
|
416
|
+
assignDefined(mapping, "swimlaneField", options.swimlaneField);
|
|
417
|
+
assignDefined(mapping, "startDateField", options.startDateField);
|
|
418
|
+
assignDefined(mapping, "endDateField", options.endDateField);
|
|
419
|
+
assignDefined(mapping, "labelField", options.labelField);
|
|
420
|
+
assignDefined(mapping, "groupField", options.groupField);
|
|
421
|
+
assignDefined(mapping, "subgroupField", options.subgroupField);
|
|
422
|
+
assignDefined(mapping, "progressField", options.progressField);
|
|
423
|
+
|
|
424
|
+
const cardConfig: Record<string, unknown> = {};
|
|
425
|
+
assignDefined(cardConfig, "titleField", options.cardTitleField);
|
|
426
|
+
assignDefined(cardConfig, "category1Field", options.cardCategory1Field);
|
|
427
|
+
assignDefined(cardConfig, "category2Field", options.cardCategory2Field);
|
|
428
|
+
assignDefined(cardConfig, "category3Field", options.cardCategory3Field);
|
|
429
|
+
assignDefined(cardConfig, "category4Field", options.cardCategory4Field);
|
|
430
|
+
assignDefined(cardConfig, "descriptionField", options.cardDescriptionField);
|
|
431
|
+
if (Object.keys(cardConfig).length > 0) mapping.cardConfig = cardConfig;
|
|
432
|
+
|
|
433
|
+
const aggregationConfig: Record<string, unknown> = {};
|
|
434
|
+
assignDefined(aggregationConfig, "method", options.aggregationMethod);
|
|
435
|
+
assignDefined(aggregationConfig, "fieldId", options.aggregationField);
|
|
436
|
+
if (Object.keys(aggregationConfig).length > 0) mapping.aggregationConfig = aggregationConfig;
|
|
437
|
+
|
|
438
|
+
return Object.keys(mapping).length > 0 ? mapping : undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildHierarchyConfig(options: WidgetMutationOptions): Record<string, unknown> | undefined {
|
|
442
|
+
const base = readNamedJsonObject(
|
|
443
|
+
options.hierarchyConfig,
|
|
444
|
+
options.hierarchyConfigFile,
|
|
445
|
+
"--hierarchy-config",
|
|
446
|
+
"--hierarchy-config-file",
|
|
447
|
+
);
|
|
448
|
+
const patch: Record<string, unknown> = {};
|
|
449
|
+
|
|
450
|
+
const columns: Record<string, unknown> = {};
|
|
451
|
+
assignDefined(columns, "label", options.hierarchyLabel);
|
|
452
|
+
if (options.hierarchyInfo && options.hierarchyInfo.length > 0) {
|
|
453
|
+
columns.infos = options.hierarchyInfo.map(parseHierarchyInfo);
|
|
454
|
+
}
|
|
455
|
+
if (options.hierarchyNumber && options.hierarchyNumber.length > 0) {
|
|
456
|
+
columns.numbers = options.hierarchyNumber.map(parseHierarchyNumber);
|
|
457
|
+
}
|
|
458
|
+
if (Object.keys(columns).length > 0) patch.columns = columns;
|
|
459
|
+
|
|
460
|
+
const rootLevel: Record<string, unknown> = {};
|
|
461
|
+
assignDefined(rootLevel, "name", options.hierarchyRootName);
|
|
462
|
+
assignDefined(rootLevel, "labelFieldId", options.hierarchyRootLabelField);
|
|
463
|
+
if (options.hierarchyRootInfo && options.hierarchyRootInfo.length > 0) {
|
|
464
|
+
rootLevel.infoMappings = options.hierarchyRootInfo.map((entry) =>
|
|
465
|
+
parseHierarchyMapping(entry, "--hierarchy-root-info"),
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (options.hierarchyRootNumber && options.hierarchyRootNumber.length > 0) {
|
|
469
|
+
rootLevel.numberMappings = options.hierarchyRootNumber.map((entry) =>
|
|
470
|
+
parseHierarchyMapping(entry, "--hierarchy-root-number"),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
if (Object.keys(rootLevel).length > 0) patch.rootLevel = rootLevel;
|
|
474
|
+
|
|
475
|
+
if (options.hierarchyChild && options.hierarchyChild.length > 0) {
|
|
476
|
+
patch.childLevels = options.hierarchyChild.map((entry, index) => {
|
|
477
|
+
let parsed: unknown;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(entry);
|
|
480
|
+
} catch {
|
|
481
|
+
throw new CliError("invalid_cli_usage", `--hierarchy-child payload #${index + 1} is invalid JSON.`);
|
|
482
|
+
}
|
|
483
|
+
if (!isPlainObject(parsed)) {
|
|
484
|
+
throw new CliError(
|
|
485
|
+
"invalid_cli_usage",
|
|
486
|
+
`--hierarchy-child payload #${index + 1} must be a JSON object.`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return parsed;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (base === undefined && Object.keys(patch).length === 0) return undefined;
|
|
494
|
+
return deepMerge(base ?? {}, patch);
|
|
495
|
+
}
|
|
496
|
+
|
|
298
497
|
function displayRows(displays: readonly DisplaySummary[]): Record<string, unknown>[] {
|
|
299
498
|
return displays.map((display) => ({
|
|
300
499
|
id: display.id,
|
|
@@ -435,12 +634,37 @@ function buildWidgetBody(options: WidgetMutationOptions): Record<string, unknown
|
|
|
435
634
|
};
|
|
436
635
|
}
|
|
437
636
|
|
|
637
|
+
const viewPatch: Record<string, unknown> = {};
|
|
438
638
|
if (options.field && options.field.length > 0) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
639
|
+
viewPatch.fields = options.field;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const fieldMapping = buildWidgetFieldMapping(options);
|
|
643
|
+
const viewStateBase = readNamedJsonObject(
|
|
644
|
+
options.viewState,
|
|
645
|
+
options.viewStateFile,
|
|
646
|
+
"--view-state",
|
|
647
|
+
"--view-state-file",
|
|
648
|
+
);
|
|
649
|
+
let viewState: Record<string, unknown> | undefined = viewStateBase ? { ...viewStateBase } : undefined;
|
|
650
|
+
if (fieldMapping) {
|
|
651
|
+
viewState = deepMerge(viewState ?? {}, { fieldMapping });
|
|
652
|
+
}
|
|
653
|
+
if (viewState !== undefined) {
|
|
654
|
+
viewPatch.viewAdditionalProperties = { viewState };
|
|
443
655
|
}
|
|
656
|
+
|
|
657
|
+
const hierarchyConfig = buildHierarchyConfig(options);
|
|
658
|
+
if (hierarchyConfig !== undefined) {
|
|
659
|
+
viewPatch.hierarchyConfig = hierarchyConfig;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (Object.keys(viewPatch).length > 0) {
|
|
663
|
+
const existingView =
|
|
664
|
+
typeof body.view === "object" && body.view ? (body.view as Record<string, unknown>) : {};
|
|
665
|
+
body.view = deepMerge(existingView, viewPatch);
|
|
666
|
+
}
|
|
667
|
+
|
|
444
668
|
const queryMerge: Record<string, unknown> = {};
|
|
445
669
|
if (options.sort && options.sort.length > 0) queryMerge.sorts = options.sort;
|
|
446
670
|
if (options.filter && options.filter.length > 0) queryMerge.filters = options.filter.map(toDisplayFilter);
|
|
@@ -867,7 +1091,36 @@ export function registerDisplaysCommands(
|
|
|
867
1091
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
868
1092
|
.option("--chart-type <type>", "chart type (lines, bars, stackedBars, combined)").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>", "time granularity (DATE, MONTH_YEAR, QUARTER_YEAR, YEAR)").option("--x-axis-time-series")
|
|
869
1093
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
870
|
-
.option("--legend <json>")
|
|
1094
|
+
.option("--legend <json>")
|
|
1095
|
+
.option("--column-field <uuid>", "KANBAN column field (UUID, not slug)")
|
|
1096
|
+
.option("--swimlane-field <uuid>", "KANBAN swimlane field (UUID, not slug)")
|
|
1097
|
+
.option("--start-date-field <uuid>", "TIMELINE start date field (UUID, not slug)")
|
|
1098
|
+
.option("--end-date-field <uuid>", "TIMELINE end date field (UUID, not slug)")
|
|
1099
|
+
.option("--label-field <uuid>", "TIMELINE label field (UUID, not slug)")
|
|
1100
|
+
.option("--group-field <uuid>", "TIMELINE group field (UUID, not slug)")
|
|
1101
|
+
.option("--subgroup-field <uuid>", "TIMELINE subgroup field (UUID, not slug)")
|
|
1102
|
+
.option("--progress-field <uuid>", "TIMELINE progress field (UUID, not slug)")
|
|
1103
|
+
.option("--card-title-field <uuid>", "KANBAN/TIMELINE card title field (UUID, not slug)")
|
|
1104
|
+
.option("--card-category1-field <uuid>", "KANBAN/TIMELINE card category 1 field (UUID, not slug)")
|
|
1105
|
+
.option("--card-category2-field <uuid>", "KANBAN/TIMELINE card category 2 field (UUID, not slug)")
|
|
1106
|
+
.option("--card-category3-field <uuid>", "KANBAN card category 3 field (UUID, not slug)")
|
|
1107
|
+
.option("--card-category4-field <uuid>", "TIMELINE card category 4 field (UUID, not slug)")
|
|
1108
|
+
.option("--card-description-field <uuid>", "KANBAN/TIMELINE card description field (UUID, not slug)")
|
|
1109
|
+
.option("--aggregation-method <method>", "KANBAN card aggregation method (e.g. sum)")
|
|
1110
|
+
.option("--aggregation-field <uuid>", "KANBAN card aggregation field (UUID, not slug)")
|
|
1111
|
+
.option("--view-state <json>", "KANBAN/TIMELINE viewState UI scalars (columnOrder, granularity, sidebarWidth, show* ...)")
|
|
1112
|
+
.option("--view-state-file <path>", "read --view-state JSON from a file")
|
|
1113
|
+
.option("--hierarchy-label <name>", "HIERARCHY label column name")
|
|
1114
|
+
.option("--hierarchy-info <id:name>", "HIERARCHY info column; id is a unique opaque column id you choose (e.g. col_1779975207355_zfndknslw), not the name, and is reused as columnId in mappings (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1115
|
+
.option("--hierarchy-number <id:name:aggregation>", "HIERARCHY number column; id is a unique opaque column id (e.g. col_1779975207355_zfndknslw), not the name; aggregation SUM|AVERAGE|MINIMUM|MAXIMUM|COUNT|COUNT_DISTINCT (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1116
|
+
.option("--hierarchy-root-name <name>", "HIERARCHY root level name")
|
|
1117
|
+
.option("--hierarchy-root-label-field <uuid>", "HIERARCHY root level label field (UUID, not slug)")
|
|
1118
|
+
.option("--hierarchy-root-info <columnId:fieldId>", "HIERARCHY root info mapping; columnId must match a --hierarchy-info id (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1119
|
+
.option("--hierarchy-root-number <columnId:fieldId>", "HIERARCHY root number mapping; columnId must match a --hierarchy-number id (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1120
|
+
.option("--hierarchy-child <json>", "HIERARCHY child level JSON {name,schemaId,childFieldId,labelFieldId,infoMappings,numberMappings} (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1121
|
+
.option("--hierarchy-config <json>", "HIERARCHY full hierarchyConfig JSON (escape hatch)")
|
|
1122
|
+
.option("--hierarchy-config-file <path>", "read --hierarchy-config JSON from a file")
|
|
1123
|
+
.option("--raw <json>").option("--raw-file <path>")
|
|
871
1124
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: WidgetMutationOptions) {
|
|
872
1125
|
const { api, json, readOnly } = createDisplaysApi(this, filePath); ensureWritable("displays widgets create", readOnly, "write");
|
|
873
1126
|
const data = await api.createWidget(display, buildWidgetBody(commandOptions));
|
|
@@ -885,7 +1138,36 @@ export function registerDisplaysCommands(
|
|
|
885
1138
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
886
1139
|
.option("--chart-type <type>", "chart type (lines, bars, stackedBars, combined)").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>", "time granularity (DATE, MONTH_YEAR, QUARTER_YEAR, YEAR)").option("--x-axis-time-series")
|
|
887
1140
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
888
|
-
.option("--legend <json>")
|
|
1141
|
+
.option("--legend <json>")
|
|
1142
|
+
.option("--column-field <uuid>", "KANBAN column field (UUID, not slug)")
|
|
1143
|
+
.option("--swimlane-field <uuid>", "KANBAN swimlane field (UUID, not slug)")
|
|
1144
|
+
.option("--start-date-field <uuid>", "TIMELINE start date field (UUID, not slug)")
|
|
1145
|
+
.option("--end-date-field <uuid>", "TIMELINE end date field (UUID, not slug)")
|
|
1146
|
+
.option("--label-field <uuid>", "TIMELINE label field (UUID, not slug)")
|
|
1147
|
+
.option("--group-field <uuid>", "TIMELINE group field (UUID, not slug)")
|
|
1148
|
+
.option("--subgroup-field <uuid>", "TIMELINE subgroup field (UUID, not slug)")
|
|
1149
|
+
.option("--progress-field <uuid>", "TIMELINE progress field (UUID, not slug)")
|
|
1150
|
+
.option("--card-title-field <uuid>", "KANBAN/TIMELINE card title field (UUID, not slug)")
|
|
1151
|
+
.option("--card-category1-field <uuid>", "KANBAN/TIMELINE card category 1 field (UUID, not slug)")
|
|
1152
|
+
.option("--card-category2-field <uuid>", "KANBAN/TIMELINE card category 2 field (UUID, not slug)")
|
|
1153
|
+
.option("--card-category3-field <uuid>", "KANBAN card category 3 field (UUID, not slug)")
|
|
1154
|
+
.option("--card-category4-field <uuid>", "TIMELINE card category 4 field (UUID, not slug)")
|
|
1155
|
+
.option("--card-description-field <uuid>", "KANBAN/TIMELINE card description field (UUID, not slug)")
|
|
1156
|
+
.option("--aggregation-method <method>", "KANBAN card aggregation method (e.g. sum)")
|
|
1157
|
+
.option("--aggregation-field <uuid>", "KANBAN card aggregation field (UUID, not slug)")
|
|
1158
|
+
.option("--view-state <json>", "KANBAN/TIMELINE viewState UI scalars (columnOrder, granularity, sidebarWidth, show* ...)")
|
|
1159
|
+
.option("--view-state-file <path>", "read --view-state JSON from a file")
|
|
1160
|
+
.option("--hierarchy-label <name>", "HIERARCHY label column name")
|
|
1161
|
+
.option("--hierarchy-info <id:name>", "HIERARCHY info column; id is a unique opaque column id you choose (e.g. col_1779975207355_zfndknslw), not the name, and is reused as columnId in mappings (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1162
|
+
.option("--hierarchy-number <id:name:aggregation>", "HIERARCHY number column; id is a unique opaque column id (e.g. col_1779975207355_zfndknslw), not the name; aggregation SUM|AVERAGE|MINIMUM|MAXIMUM|COUNT|COUNT_DISTINCT (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1163
|
+
.option("--hierarchy-root-name <name>", "HIERARCHY root level name")
|
|
1164
|
+
.option("--hierarchy-root-label-field <uuid>", "HIERARCHY root level label field (UUID, not slug)")
|
|
1165
|
+
.option("--hierarchy-root-info <columnId:fieldId>", "HIERARCHY root info mapping; columnId must match a --hierarchy-info id (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1166
|
+
.option("--hierarchy-root-number <columnId:fieldId>", "HIERARCHY root number mapping; columnId must match a --hierarchy-number id (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1167
|
+
.option("--hierarchy-child <json>", "HIERARCHY child level JSON {name,schemaId,childFieldId,labelFieldId,infoMappings,numberMappings} (repeatable)", (v, p: string[] = []) => [...p, v], [])
|
|
1168
|
+
.option("--hierarchy-config <json>", "HIERARCHY full hierarchyConfig JSON (escape hatch)")
|
|
1169
|
+
.option("--hierarchy-config-file <path>", "read --hierarchy-config JSON from a file")
|
|
1170
|
+
.option("--raw <json>").option("--raw-file <path>")
|
|
889
1171
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, widgetId: string, commandOptions: WidgetMutationOptions) {
|
|
890
1172
|
const { api, json, readOnly } = createDisplaysApi(this, filePath); ensureWritable("displays widgets update", readOnly, "write");
|
|
891
1173
|
const data = await api.updateWidget(display, widgetId, buildWidgetBody(commandOptions));
|
|
@@ -200,6 +200,25 @@ describe("getCommandExamples", () => {
|
|
|
200
200
|
}
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
+
it("does not teach legacy system columns in display widget examples", () => {
|
|
204
|
+
const legacyColumns = ["id", "createdat", "updatedat", "createdby", "updatedby"];
|
|
205
|
+
const commands = ["displays widgets create", "displays widgets update", "displays list", "displays get"];
|
|
206
|
+
|
|
207
|
+
for (const command of commands) {
|
|
208
|
+
for (const example of getCommandExamples(command)) {
|
|
209
|
+
for (const column of legacyColumns) {
|
|
210
|
+
expect(example.command).not.toContain(`--field ${column}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const examplePayloads = [example.raw, example.response].filter(Boolean);
|
|
214
|
+
for (const payload of examplePayloads) {
|
|
215
|
+
expect(payload).not.toMatch(/"fields"\s*:\s*\[[^\]]*"(id|createdat|updatedat|createdby|updatedby)"/);
|
|
216
|
+
expect(payload).not.toMatch(/"fieldCalc"\s*:\s*"(id|createdat|updatedat|createdby|updatedby)"/);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
203
222
|
it("covers REFERENCE, MEMBER and DATE variants in displays variables create", () => {
|
|
204
223
|
const examples = getCommandExamples("displays variables create");
|
|
205
224
|
const rawPayloads = examples.map((example) => example.raw ?? "");
|