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/package.json
CHANGED
package/src/api/automations.ts
CHANGED
|
@@ -180,7 +180,7 @@ export class AutomationsApi {
|
|
|
180
180
|
|
|
181
181
|
moveAutomation(input: MoveAutomationInput): Promise<Record<string, unknown>> {
|
|
182
182
|
return this.client.requestJson<Record<string, unknown>>(
|
|
183
|
-
|
|
183
|
+
"/api/v2/automation-folders/automations:move",
|
|
184
184
|
jsonOptions(input, "POST"),
|
|
185
185
|
);
|
|
186
186
|
}
|
package/src/api/displays.ts
CHANGED
|
@@ -45,6 +45,19 @@ export interface ApiDisplayNavigationTarget {
|
|
|
45
45
|
label?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface ApiDisplayFolder {
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
id?: string;
|
|
51
|
+
name?: string | null;
|
|
52
|
+
isRoot?: boolean;
|
|
53
|
+
displayCount?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MoveDisplayInput {
|
|
57
|
+
displayId: string;
|
|
58
|
+
targetFolderId: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
export type DisplaySummary = ApiDisplay;
|
|
49
62
|
export type DisplayResource = ApiDisplay;
|
|
50
63
|
|
|
@@ -273,4 +286,39 @@ export class DisplaysApi {
|
|
|
273
286
|
{ method: "DELETE" },
|
|
274
287
|
);
|
|
275
288
|
}
|
|
289
|
+
|
|
290
|
+
listFolders(): Promise<ApiDisplayFolder[]> {
|
|
291
|
+
return this.client.getJson<ApiDisplayFolder[]>("/api/v2/display-folders");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
createFolder(body: Record<string, unknown>): Promise<ApiDisplayFolder> {
|
|
295
|
+
return this.client.requestJson<ApiDisplayFolder>(
|
|
296
|
+
"/api/v2/display-folders",
|
|
297
|
+
jsonOptions(body, "POST"),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
updateFolder(
|
|
302
|
+
folder: NormalizedUuidOrSlug,
|
|
303
|
+
body: Record<string, unknown>,
|
|
304
|
+
): Promise<ApiDisplayFolder> {
|
|
305
|
+
return this.client.requestJson<ApiDisplayFolder>(
|
|
306
|
+
`/api/v2/display-folders/${selectorValue(folder)}`,
|
|
307
|
+
jsonOptions(body, "PATCH"),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
deleteFolder(folder: NormalizedUuidOrSlug): Promise<Record<string, unknown>> {
|
|
312
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
313
|
+
`/api/v2/display-folders/${selectorValue(folder)}`,
|
|
314
|
+
{ method: "DELETE" },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
moveDisplay(input: MoveDisplayInput): Promise<Record<string, unknown>> {
|
|
319
|
+
return this.client.requestJson<Record<string, unknown>>(
|
|
320
|
+
"/api/v2/display-folders/displays:move",
|
|
321
|
+
jsonOptions(input, "POST"),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
276
324
|
}
|
package/src/api/navigation.ts
CHANGED
package/src/command-registry.ts
CHANGED
|
@@ -280,6 +280,41 @@ export const commandRegistry = {
|
|
|
280
280
|
supportsRaw: false,
|
|
281
281
|
output: writeOutput,
|
|
282
282
|
},
|
|
283
|
+
"displays folders list": {
|
|
284
|
+
command: "displays folders list",
|
|
285
|
+
accessLevel: "read",
|
|
286
|
+
supportsExample: true,
|
|
287
|
+
supportsRaw: false,
|
|
288
|
+
output: readOutput,
|
|
289
|
+
},
|
|
290
|
+
"displays folders create": {
|
|
291
|
+
command: "displays folders create",
|
|
292
|
+
accessLevel: "write",
|
|
293
|
+
supportsExample: true,
|
|
294
|
+
supportsRaw: false,
|
|
295
|
+
output: writeOutput,
|
|
296
|
+
},
|
|
297
|
+
"displays folders update": {
|
|
298
|
+
command: "displays folders update",
|
|
299
|
+
accessLevel: "write",
|
|
300
|
+
supportsExample: true,
|
|
301
|
+
supportsRaw: false,
|
|
302
|
+
output: writeOutput,
|
|
303
|
+
},
|
|
304
|
+
"displays folders delete": {
|
|
305
|
+
command: "displays folders delete",
|
|
306
|
+
accessLevel: "destructive",
|
|
307
|
+
supportsExample: true,
|
|
308
|
+
supportsRaw: false,
|
|
309
|
+
output: writeOutput,
|
|
310
|
+
},
|
|
311
|
+
"displays folders move": {
|
|
312
|
+
command: "displays folders move",
|
|
313
|
+
accessLevel: "write",
|
|
314
|
+
supportsExample: true,
|
|
315
|
+
supportsRaw: false,
|
|
316
|
+
output: writeOutput,
|
|
317
|
+
},
|
|
283
318
|
"displays layout get": {
|
|
284
319
|
command: "displays layout get",
|
|
285
320
|
accessLevel: "read",
|
|
@@ -87,6 +87,38 @@ describe("automations commands", () => {
|
|
|
87
87
|
expect(program.commands.some((command) => command.name() === "automations")).toBe(true);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("moves an automation with the folder id in the body, not the path", async () => {
|
|
91
|
+
const configPath = createApiConfig();
|
|
92
|
+
const output = createStdoutBuffer();
|
|
93
|
+
const fetchMock = createFetchMock();
|
|
94
|
+
fetchMock.mockResolvedValueOnce(
|
|
95
|
+
jsonResponse({ id: "folder-1", name: "Ops", isRoot: false }),
|
|
96
|
+
);
|
|
97
|
+
const program = buildAutomationsProgram({ configPath, stdout: output.stdout });
|
|
98
|
+
|
|
99
|
+
await program.parseAsync([
|
|
100
|
+
"node",
|
|
101
|
+
"zinkee",
|
|
102
|
+
"--json",
|
|
103
|
+
"automations",
|
|
104
|
+
"folders",
|
|
105
|
+
"move",
|
|
106
|
+
"11111111-1111-4111-a111-111111111111",
|
|
107
|
+
"--to",
|
|
108
|
+
"22222222-2222-4222-a222-222222222222",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
112
|
+
"https://api.zinkee.test/api/v2/automation-folders/automations:move",
|
|
113
|
+
);
|
|
114
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
115
|
+
expect(init?.method).toBe("POST");
|
|
116
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
117
|
+
automationId: "11111111-1111-4111-a111-111111111111",
|
|
118
|
+
targetFolderId: "22222222-2222-4222-a222-222222222222",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
90
122
|
it("sets trigger payloads through the generic trigger flags", async () => {
|
|
91
123
|
const configPath = createApiConfig();
|
|
92
124
|
const output = createStdoutBuffer();
|
|
@@ -1408,11 +1408,13 @@ export function registerAutomationsCommands(
|
|
|
1408
1408
|
.command("automations")
|
|
1409
1409
|
.description("Manage automations, triggers, actions, and connections");
|
|
1410
1410
|
|
|
1411
|
-
const folders = automations
|
|
1411
|
+
const folders = automations
|
|
1412
|
+
.command("folders")
|
|
1413
|
+
.description("Manage automation folders (flat: a folder only has a name, no order or properties)");
|
|
1412
1414
|
|
|
1413
1415
|
folders
|
|
1414
1416
|
.command("list")
|
|
1415
|
-
.description("List automation folders")
|
|
1417
|
+
.description("List automation folders (non-root folders plus the root bucket, whose name is null)")
|
|
1416
1418
|
.action(async function action(this: Command) {
|
|
1417
1419
|
const { api, json } = createAutomationsApi(this, filePath);
|
|
1418
1420
|
const data = await api.listFolders();
|
|
@@ -1500,7 +1502,11 @@ export function registerAutomationsCommands(
|
|
|
1500
1502
|
.command("move")
|
|
1501
1503
|
.description("Move an automation into a folder")
|
|
1502
1504
|
.argument("<automation>", "automation UUID", parseUuidOrSlugSelector)
|
|
1503
|
-
.requiredOption(
|
|
1505
|
+
.requiredOption(
|
|
1506
|
+
"--to <folder>",
|
|
1507
|
+
"target folder UUID or exact name (use the root folder to move it out of any folder)",
|
|
1508
|
+
parseUuidOrSlugSelector,
|
|
1509
|
+
)
|
|
1504
1510
|
.action(async function action(
|
|
1505
1511
|
this: Command,
|
|
1506
1512
|
automation: NormalizedUuidOrSlug,
|
|
@@ -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();
|
|
@@ -486,4 +723,78 @@ describe("displays commands", () => {
|
|
|
486
723
|
|
|
487
724
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
488
725
|
});
|
|
726
|
+
|
|
727
|
+
it("creates a display folder with just a name", async () => {
|
|
728
|
+
const configPath = createApiConfig();
|
|
729
|
+
const output = createStdoutBuffer();
|
|
730
|
+
const fetchMock = createFetchMock();
|
|
731
|
+
fetchMock.mockResolvedValueOnce(
|
|
732
|
+
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 0 }),
|
|
733
|
+
);
|
|
734
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
735
|
+
|
|
736
|
+
await program.parseAsync([
|
|
737
|
+
"node",
|
|
738
|
+
"zinkee",
|
|
739
|
+
"--json",
|
|
740
|
+
"displays",
|
|
741
|
+
"folders",
|
|
742
|
+
"create",
|
|
743
|
+
"--name",
|
|
744
|
+
"Executive Dashboards",
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
748
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
749
|
+
expect(init?.method).toBe("POST");
|
|
750
|
+
expect(JSON.parse(String(init?.body))).toEqual({ name: "Executive Dashboards" });
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("moves a display into a folder resolved by name", async () => {
|
|
754
|
+
const configPath = createApiConfig();
|
|
755
|
+
const output = createStdoutBuffer();
|
|
756
|
+
const fetchMock = createFetchMock();
|
|
757
|
+
fetchMock.mockResolvedValueOnce(
|
|
758
|
+
jsonResponse([{ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 2 }]),
|
|
759
|
+
);
|
|
760
|
+
fetchMock.mockResolvedValueOnce(
|
|
761
|
+
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false }),
|
|
762
|
+
);
|
|
763
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
764
|
+
|
|
765
|
+
await program.parseAsync([
|
|
766
|
+
"node",
|
|
767
|
+
"zinkee",
|
|
768
|
+
"--json",
|
|
769
|
+
"displays",
|
|
770
|
+
"folders",
|
|
771
|
+
"move",
|
|
772
|
+
"0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
773
|
+
"--to",
|
|
774
|
+
"Executive Dashboards",
|
|
775
|
+
]);
|
|
776
|
+
|
|
777
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
778
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
779
|
+
"https://api.zinkee.test/api/v2/display-folders/displays:move",
|
|
780
|
+
);
|
|
781
|
+
const moveInit = fetchMock.mock.calls[1]?.[1];
|
|
782
|
+
expect(moveInit?.method).toBe("POST");
|
|
783
|
+
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
784
|
+
displayId: "0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
785
|
+
targetFolderId: "folder-1",
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("blocks destructive display folder commands in read-only mode", async () => {
|
|
790
|
+
const configPath = createApiConfig();
|
|
791
|
+
const fetchMock = createFetchMock();
|
|
792
|
+
const program = buildProgram({ configPath });
|
|
793
|
+
|
|
794
|
+
await expect(
|
|
795
|
+
program.parseAsync(["node", "zinkee", "--read-only", "displays", "folders", "delete", "folder-1"]),
|
|
796
|
+
).rejects.toThrowError('Command "displays folders delete" is not allowed in read-only mode.');
|
|
797
|
+
|
|
798
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
799
|
+
});
|
|
489
800
|
});
|