zinkee 0.1.24 → 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 +8 -1
- package/README.md +73 -5
- package/dist/index.js +551 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/automations.ts +1 -1
- package/src/api/displays.ts +48 -0
- package/src/api/navigation.ts +0 -1
- package/src/command-registry.ts +35 -0
- package/src/commands/automations.test.ts +32 -0
- package/src/commands/automations.ts +9 -3
- package/src/commands/displays.test.ts +311 -0
- package/src/commands/displays.ts +431 -7
- package/src/commands/navigation.test.ts +2 -5
- package/src/commands/navigation.ts +11 -11
- package/src/commands/teamspace.ts +19 -13
- package/src/utils/examples.test.ts +32 -4
- package/src/utils/examples.ts +208 -82
package/src/commands/displays.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type { Writable } from "node:stream";
|
|
|
3
3
|
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DisplaysApi,
|
|
8
|
+
type ApiDisplayFolder,
|
|
9
|
+
type DisplayResource,
|
|
10
|
+
type DisplaySummary,
|
|
11
|
+
} from "../api/displays.js";
|
|
7
12
|
import { ApiClient } from "../client.js";
|
|
8
13
|
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
9
14
|
import {
|
|
@@ -102,6 +107,36 @@ interface WidgetMutationOptions {
|
|
|
102
107
|
xAxisTimeSeries?: boolean;
|
|
103
108
|
yAxisSerie?: string[];
|
|
104
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;
|
|
105
140
|
raw?: string;
|
|
106
141
|
rawFile?: string;
|
|
107
142
|
}
|
|
@@ -290,6 +325,175 @@ function isEmptyObject(value: object): boolean {
|
|
|
290
325
|
return Object.keys(value).length === 0;
|
|
291
326
|
}
|
|
292
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
|
+
|
|
293
497
|
function displayRows(displays: readonly DisplaySummary[]): Record<string, unknown>[] {
|
|
294
498
|
return displays.map((display) => ({
|
|
295
499
|
id: display.id,
|
|
@@ -308,6 +512,40 @@ function getDisplaySubresource(value: unknown): Record<string, unknown>[] {
|
|
|
308
512
|
return value.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === "object");
|
|
309
513
|
}
|
|
310
514
|
|
|
515
|
+
function displayFolderRows(folders: readonly ApiDisplayFolder[]): Record<string, unknown>[] {
|
|
516
|
+
return folders.map((folder) => ({
|
|
517
|
+
id: folder.id ?? "",
|
|
518
|
+
name: folder.name ?? "",
|
|
519
|
+
isRoot: folder.isRoot ?? "",
|
|
520
|
+
displayCount: folder.displayCount ?? "",
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function resolveDisplayFolderId(
|
|
525
|
+
api: DisplaysApi,
|
|
526
|
+
selector: NormalizedUuidOrSlug,
|
|
527
|
+
): Promise<string> {
|
|
528
|
+
if (selector.kind === "uuid") {
|
|
529
|
+
return selector.value;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const folders = await api.listFolders();
|
|
533
|
+
const matches = folders.filter((folder) => folder.name === selector.value);
|
|
534
|
+
|
|
535
|
+
if (matches.length === 1 && typeof matches[0]?.id === "string" && matches[0].id.length > 0) {
|
|
536
|
+
return matches[0].id;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (matches.length > 1) {
|
|
540
|
+
throw new CliError(
|
|
541
|
+
"invalid_cli_usage",
|
|
542
|
+
`Folder "${selector.value}" is ambiguous. Found ${matches.length} matches.`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
throw new CliError("invalid_cli_usage", `Folder "${selector.value}" was not found.`);
|
|
547
|
+
}
|
|
548
|
+
|
|
311
549
|
function buildVariableBody(options: VariableMutationOptions): Record<string, unknown> {
|
|
312
550
|
const body = {
|
|
313
551
|
...readRawObject(options.raw, options.rawFile),
|
|
@@ -396,12 +634,37 @@ function buildWidgetBody(options: WidgetMutationOptions): Record<string, unknown
|
|
|
396
634
|
};
|
|
397
635
|
}
|
|
398
636
|
|
|
637
|
+
const viewPatch: Record<string, unknown> = {};
|
|
399
638
|
if (options.field && options.field.length > 0) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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 };
|
|
404
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
|
+
|
|
405
668
|
const queryMerge: Record<string, unknown> = {};
|
|
406
669
|
if (options.sort && options.sort.length > 0) queryMerge.sorts = options.sort;
|
|
407
670
|
if (options.filter && options.filter.length > 0) queryMerge.filters = options.filter.map(toDisplayFilter);
|
|
@@ -590,6 +853,109 @@ export function registerDisplaysCommands(
|
|
|
590
853
|
writeLine(stdout, `Deleted display "${display.value}".`);
|
|
591
854
|
});
|
|
592
855
|
|
|
856
|
+
const folders = displays
|
|
857
|
+
.command("folders")
|
|
858
|
+
.description("Manage display folders (flat: a folder only has a name, no order or properties)");
|
|
859
|
+
|
|
860
|
+
folders
|
|
861
|
+
.command("list")
|
|
862
|
+
.description("List display folders (non-root folders plus the root bucket, whose name is null)")
|
|
863
|
+
.action(async function action(this: Command) {
|
|
864
|
+
const { api, json } = createDisplaysApi(this, filePath);
|
|
865
|
+
const data = await api.listFolders();
|
|
866
|
+
if (json) {
|
|
867
|
+
emitJson(stdout, "displays folders list", data);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
printTable(displayFolderRows(data), ["id", "name", "isRoot", "displayCount"], stdout);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
folders
|
|
874
|
+
.command("create")
|
|
875
|
+
.description("Create a display folder")
|
|
876
|
+
.requiredOption("--name <name>", "folder name")
|
|
877
|
+
.action(async function action(this: Command, commandOptions: { name?: string }) {
|
|
878
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
879
|
+
ensureWritable("displays folders create", readOnly, "write");
|
|
880
|
+
if (commandOptions.name === undefined) {
|
|
881
|
+
throw new CliError("invalid_cli_usage", 'Provide folder name via "--name".');
|
|
882
|
+
}
|
|
883
|
+
const data = await api.createFolder({ name: commandOptions.name });
|
|
884
|
+
if (json) {
|
|
885
|
+
emitJson(stdout, "displays folders create", data);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
writeLine(stdout, `Created display folder "${String(data.name ?? commandOptions.name)}".`);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
folders
|
|
892
|
+
.command("update")
|
|
893
|
+
.description("Rename a display folder")
|
|
894
|
+
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
895
|
+
.requiredOption("--name <name>", "folder name")
|
|
896
|
+
.action(async function action(
|
|
897
|
+
this: Command,
|
|
898
|
+
folder: NormalizedUuidOrSlug,
|
|
899
|
+
commandOptions: { name?: string },
|
|
900
|
+
) {
|
|
901
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
902
|
+
ensureWritable("displays folders update", readOnly, "write");
|
|
903
|
+
if (commandOptions.name === undefined) {
|
|
904
|
+
throw new CliError("invalid_cli_usage", 'Provide folder name via "--name".');
|
|
905
|
+
}
|
|
906
|
+
const folderId = await resolveDisplayFolderId(api, folder);
|
|
907
|
+
const data = await api.updateFolder({ kind: "uuid", value: folderId }, { name: commandOptions.name });
|
|
908
|
+
if (json) {
|
|
909
|
+
emitJson(stdout, "displays folders update", data);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
writeLine(stdout, `Updated display folder "${String(data.name ?? folder.value)}".`);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
folders
|
|
916
|
+
.command("delete")
|
|
917
|
+
.description("Delete a display folder (its displays move back to the root folder)")
|
|
918
|
+
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
919
|
+
.action(async function action(this: Command, folder: NormalizedUuidOrSlug) {
|
|
920
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
921
|
+
ensureWritable("displays folders delete", readOnly, "destructive");
|
|
922
|
+
const folderId = await resolveDisplayFolderId(api, folder);
|
|
923
|
+
const data = await api.deleteFolder({ kind: "uuid", value: folderId });
|
|
924
|
+
if (json) {
|
|
925
|
+
emitJson(stdout, "displays folders delete", data);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
writeLine(stdout, `Deleted display folder "${folder.value}".`);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
folders
|
|
932
|
+
.command("move")
|
|
933
|
+
.description("Move a display into a folder")
|
|
934
|
+
.argument("<display>", "display UUID", parseUuidOrSlugSelector)
|
|
935
|
+
.requiredOption(
|
|
936
|
+
"--to <folder>",
|
|
937
|
+
"target folder UUID or exact name (use the root folder to move it out of any folder)",
|
|
938
|
+
parseUuidOrSlugSelector,
|
|
939
|
+
)
|
|
940
|
+
.action(async function action(
|
|
941
|
+
this: Command,
|
|
942
|
+
display: NormalizedUuidOrSlug,
|
|
943
|
+
commandOptions: { to?: NormalizedUuidOrSlug },
|
|
944
|
+
) {
|
|
945
|
+
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
946
|
+
ensureWritable("displays folders move", readOnly, "write");
|
|
947
|
+
if (!commandOptions.to) {
|
|
948
|
+
throw new CliError("invalid_cli_usage", 'Provide target folder via "--to".');
|
|
949
|
+
}
|
|
950
|
+
const targetFolderId = await resolveDisplayFolderId(api, commandOptions.to);
|
|
951
|
+
const data = await api.moveDisplay({ displayId: display.value, targetFolderId });
|
|
952
|
+
if (json) {
|
|
953
|
+
emitJson(stdout, "displays folders move", data);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
writeLine(stdout, `Moved display "${display.value}" to folder "${targetFolderId}".`);
|
|
957
|
+
});
|
|
958
|
+
|
|
593
959
|
const layout = displays.command("layout").description("Manage display layout");
|
|
594
960
|
layout
|
|
595
961
|
.command("get")
|
|
@@ -725,7 +1091,36 @@ export function registerDisplaysCommands(
|
|
|
725
1091
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
726
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")
|
|
727
1093
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
728
|
-
.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>")
|
|
729
1124
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: WidgetMutationOptions) {
|
|
730
1125
|
const { api, json, readOnly } = createDisplaysApi(this, filePath); ensureWritable("displays widgets create", readOnly, "write");
|
|
731
1126
|
const data = await api.createWidget(display, buildWidgetBody(commandOptions));
|
|
@@ -743,7 +1138,36 @@ export function registerDisplaysCommands(
|
|
|
743
1138
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
744
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")
|
|
745
1140
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
746
|
-
.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>")
|
|
747
1171
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, widgetId: string, commandOptions: WidgetMutationOptions) {
|
|
748
1172
|
const { api, json, readOnly } = createDisplaysApi(this, filePath); ensureWritable("displays widgets update", readOnly, "write");
|
|
749
1173
|
const data = await api.updateWidget(display, widgetId, buildWidgetBody(commandOptions));
|
|
@@ -157,7 +157,7 @@ describe("navigation commands", () => {
|
|
|
157
157
|
).rejects.toThrowError('Folder "Finance" is ambiguous. Found 2 matches.');
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("shapes move payloads using the
|
|
160
|
+
it("shapes move payloads using the resolved folder id (schemas only, no resource type)", async () => {
|
|
161
161
|
const configPath = createApiConfig();
|
|
162
162
|
const output = createStdoutBuffer();
|
|
163
163
|
const fetchMock = createFetchMock();
|
|
@@ -171,7 +171,7 @@ describe("navigation commands", () => {
|
|
|
171
171
|
{
|
|
172
172
|
id: "resource-1",
|
|
173
173
|
name: "Invoices",
|
|
174
|
-
type: "
|
|
174
|
+
type: "SCHEMA",
|
|
175
175
|
isInSpace: false,
|
|
176
176
|
},
|
|
177
177
|
],
|
|
@@ -189,8 +189,6 @@ describe("navigation commands", () => {
|
|
|
189
189
|
"resources",
|
|
190
190
|
"move",
|
|
191
191
|
"resource-1",
|
|
192
|
-
"--type",
|
|
193
|
-
"display",
|
|
194
192
|
"--to",
|
|
195
193
|
"Finance",
|
|
196
194
|
]);
|
|
@@ -207,7 +205,6 @@ describe("navigation commands", () => {
|
|
|
207
205
|
expect(moveInit?.method).toBe("POST");
|
|
208
206
|
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
209
207
|
resourceId: "resource-1",
|
|
210
|
-
resourceType: "DISPLAY",
|
|
211
208
|
targetFolderId: "aaaabbbb-cccc-dddd-eeee-ffffffffffff",
|
|
212
209
|
});
|
|
213
210
|
});
|
|
@@ -251,9 +251,9 @@ export function registerNavigationCommands(
|
|
|
251
251
|
.command("create")
|
|
252
252
|
.description("Create a navigation folder")
|
|
253
253
|
.requiredOption("--name <name>", "folder name")
|
|
254
|
-
.option("--order <n>", "
|
|
255
|
-
.option("--props <json>", "
|
|
256
|
-
.option("--props-file <path>", "
|
|
254
|
+
.option("--order <n>", "display order (lower values appear first)")
|
|
255
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
256
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
257
257
|
.action(async function action(this: Command, commandOptions: FolderMutationOptions) {
|
|
258
258
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
259
259
|
ensureWritable("navigation folders create", readOnly, "write");
|
|
@@ -272,9 +272,9 @@ export function registerNavigationCommands(
|
|
|
272
272
|
.description("Update a navigation folder")
|
|
273
273
|
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
274
274
|
.option("--name <name>", "folder name")
|
|
275
|
-
.option("--order <n>", "
|
|
276
|
-
.option("--props <json>", "
|
|
277
|
-
.option("--props-file <path>", "
|
|
275
|
+
.option("--order <n>", "display order (lower values appear first)")
|
|
276
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"color\":\"blue\",\"icon\":\"chart-line\"}")
|
|
277
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
278
278
|
.action(async function action(this: Command, folder: NormalizedUuidOrSlug, commandOptions: FolderMutationOptions) {
|
|
279
279
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
280
280
|
ensureWritable("navigation folders update", readOnly, "write");
|
|
@@ -319,14 +319,15 @@ export function registerNavigationCommands(
|
|
|
319
319
|
.command("resources")
|
|
320
320
|
.description("Manage navigation resources")
|
|
321
321
|
.command("move")
|
|
322
|
-
.description(
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
.description(
|
|
323
|
+
"Move a schema into a navigation folder. Only schemas can be moved (the navigation menu holds databases); schedules and sub-schemas follow their parent schema. Use the root folder UUID 00000000-0000-0000-0000-000000000000 to move it back to the workspace root.",
|
|
324
|
+
)
|
|
325
|
+
.argument("<resource>", "schema UUID")
|
|
325
326
|
.requiredOption("--to <folder>", "target folder UUID or exact name")
|
|
326
327
|
.action(async function action(
|
|
327
328
|
this: Command,
|
|
328
329
|
resourceId: string,
|
|
329
|
-
commandOptions: {
|
|
330
|
+
commandOptions: { to: string },
|
|
330
331
|
) {
|
|
331
332
|
const { api, json, readOnly } = createNavigationApi(this, filePath);
|
|
332
333
|
ensureWritable("navigation resources move", readOnly, "write");
|
|
@@ -334,7 +335,6 @@ export function registerNavigationCommands(
|
|
|
334
335
|
const resolved = resolveFolder(allFolders, parseUuidOrSlugSelector(commandOptions.to));
|
|
335
336
|
const data = await api.moveResource({
|
|
336
337
|
resourceId,
|
|
337
|
-
resourceType: commandOptions.type.toUpperCase(),
|
|
338
338
|
targetFolderId: resolved.id,
|
|
339
339
|
});
|
|
340
340
|
|
|
@@ -385,7 +385,7 @@ export function registerTeamspaceCommands(
|
|
|
385
385
|
folders
|
|
386
386
|
.command("list")
|
|
387
387
|
.description("List teamspace folders")
|
|
388
|
-
.option("--include-resources", "include
|
|
388
|
+
.option("--include-resources", "include the published resources inside each folder")
|
|
389
389
|
.action(async function action(this: Command, commandOptions: FolderListCommandOptions) {
|
|
390
390
|
const { api, json } = createTeamspaceApi(this, filePath);
|
|
391
391
|
const data = await api.listFolders(Boolean(commandOptions.includeResources));
|
|
@@ -402,7 +402,7 @@ export function registerTeamspaceCommands(
|
|
|
402
402
|
.command("get")
|
|
403
403
|
.description("Get a teamspace folder by exact name or UUID")
|
|
404
404
|
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
405
|
-
.option("--include-resources", "include
|
|
405
|
+
.option("--include-resources", "include the published resources inside each folder")
|
|
406
406
|
.action(async function action(
|
|
407
407
|
this: Command,
|
|
408
408
|
folder: NormalizedUuidOrSlug,
|
|
@@ -425,9 +425,9 @@ export function registerTeamspaceCommands(
|
|
|
425
425
|
.command("create")
|
|
426
426
|
.description("Create a teamspace folder")
|
|
427
427
|
.requiredOption("--name <name>", "folder name")
|
|
428
|
-
.option("--order <n>", "
|
|
429
|
-
.option("--props <json>", "
|
|
430
|
-
.option("--props-file <path>", "path to a
|
|
428
|
+
.option("--order <n>", "display order (lower values appear first)", parseNonNegativeInteger)
|
|
429
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"icon\":\"folder-users\",\"color\":\"gold\"}")
|
|
430
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
431
431
|
.action(async function action(
|
|
432
432
|
this: Command,
|
|
433
433
|
commandOptions: FolderCreateCommandOptions,
|
|
@@ -450,9 +450,9 @@ export function registerTeamspaceCommands(
|
|
|
450
450
|
.description("Update a teamspace folder")
|
|
451
451
|
.argument("<folder>", "folder UUID or exact name", parseUuidOrSlugSelector)
|
|
452
452
|
.option("--name <name>", "folder name")
|
|
453
|
-
.option("--order <n>", "
|
|
454
|
-
.option("--props <json>", "
|
|
455
|
-
.option("--props-file <path>", "path to a
|
|
453
|
+
.option("--order <n>", "display order (lower values appear first)", parseNonNegativeInteger)
|
|
454
|
+
.option("--props <json>", "additionalProperties JSON: free-form client metadata, e.g. {\"icon\":\"folder-users\",\"color\":\"gold\"}")
|
|
455
|
+
.option("--props-file <path>", "path to a file with the additionalProperties JSON")
|
|
456
456
|
.action(async function action(
|
|
457
457
|
this: Command,
|
|
458
458
|
folder: NormalizedUuidOrSlug,
|
|
@@ -495,9 +495,15 @@ export function registerTeamspaceCommands(
|
|
|
495
495
|
|
|
496
496
|
resources
|
|
497
497
|
.command("publish")
|
|
498
|
-
.description(
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
.description(
|
|
499
|
+
"Publish a resource to a teamspace folder. Publishing an already-published resource moves it to the new folder.",
|
|
500
|
+
)
|
|
501
|
+
.argument("<resource>", "schema UUID, schema slug, or display UUID", parseUuidOrSlugSelector)
|
|
502
|
+
.requiredOption(
|
|
503
|
+
"--to <folder>",
|
|
504
|
+
"target folder UUID or exact name (root folder UUID is 00000000-0000-0000-0000-000000000000)",
|
|
505
|
+
parseUuidOrSlugSelector,
|
|
506
|
+
)
|
|
501
507
|
.action(async function action(
|
|
502
508
|
this: Command,
|
|
503
509
|
resource: NormalizedUuidOrSlug,
|
|
@@ -522,8 +528,8 @@ export function registerTeamspaceCommands(
|
|
|
522
528
|
|
|
523
529
|
resources
|
|
524
530
|
.command("unpublish")
|
|
525
|
-
.description("Unpublish a resource from teamspace")
|
|
526
|
-
.argument("<resource>", "
|
|
531
|
+
.description("Unpublish a resource from teamspace (removes it from its shared folder)")
|
|
532
|
+
.argument("<resource>", "schema UUID, schema slug, or display UUID", parseUuidOrSlugSelector)
|
|
527
533
|
.action(async function action(this: Command, resource: NormalizedUuidOrSlug) {
|
|
528
534
|
const { api, json, readOnly } = createTeamspaceApi(this, filePath);
|
|
529
535
|
ensureWritable("teamspace resources unpublish", readOnly, "write");
|