zinkee 0.1.37 → 0.1.39
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/dist/index.js +298 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/displays.ts +2 -2
- package/src/command-registry.ts +1 -1
- package/src/commands/displays.test.ts +206 -0
- package/src/commands/displays.ts +209 -19
- package/src/utils/examples.ts +172 -36
package/package.json
CHANGED
package/src/api/displays.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ApiDisplay {
|
|
|
15
15
|
variables?: Array<Record<string, unknown>>;
|
|
16
16
|
widgets?: Array<Record<string, unknown>>;
|
|
17
17
|
navigationTargets?: Array<Record<string, unknown>>;
|
|
18
|
-
crossWidgetFilter?: Record<string, unknown
|
|
18
|
+
crossWidgetFilter?: Array<Record<string, unknown>> | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface ApiDisplayTab {
|
|
@@ -272,7 +272,7 @@ export class DisplaysApi {
|
|
|
272
272
|
|
|
273
273
|
setCrossWidgetFilter(
|
|
274
274
|
display: NormalizedUuidOrSlug,
|
|
275
|
-
body:
|
|
275
|
+
body: unknown[],
|
|
276
276
|
): Promise<Record<string, unknown>> {
|
|
277
277
|
return this.client.requestJson<Record<string, unknown>>(
|
|
278
278
|
`/api/v2/displays/${selectorValue(display)}/cross-widget-filter`,
|
package/src/command-registry.ts
CHANGED
|
@@ -466,7 +466,7 @@ export const commandRegistry = {
|
|
|
466
466
|
command: "displays cross-widget-filter set",
|
|
467
467
|
accessLevel: "write",
|
|
468
468
|
supportsExample: true,
|
|
469
|
-
supportsRaw:
|
|
469
|
+
supportsRaw: true,
|
|
470
470
|
output: writeOutput,
|
|
471
471
|
},
|
|
472
472
|
"displays cross-widget-filter delete": {
|
|
@@ -489,6 +489,147 @@ describe("displays commands", () => {
|
|
|
489
489
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
+
it("translates --lock-* flags into behavior.lockedFields", async () => {
|
|
493
|
+
const configPath = createApiConfig();
|
|
494
|
+
const output = createStdoutBuffer();
|
|
495
|
+
const fetchMock = createFetchMock();
|
|
496
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-l1" }));
|
|
497
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
498
|
+
|
|
499
|
+
await program.parseAsync([
|
|
500
|
+
"node",
|
|
501
|
+
"zinkee",
|
|
502
|
+
"--json",
|
|
503
|
+
"displays",
|
|
504
|
+
"widgets",
|
|
505
|
+
"create",
|
|
506
|
+
"display-1",
|
|
507
|
+
"--type",
|
|
508
|
+
"table",
|
|
509
|
+
"--origin-resource",
|
|
510
|
+
"schema-1",
|
|
511
|
+
"--lock-field",
|
|
512
|
+
"contact-name",
|
|
513
|
+
"--lock-condition",
|
|
514
|
+
"status:invoice-number:is_empty",
|
|
515
|
+
"--lock-condition",
|
|
516
|
+
"status:amount:gte:500",
|
|
517
|
+
"--lock-message-before",
|
|
518
|
+
"status:Status locks until the invoice number is set.",
|
|
519
|
+
"--lock-message-after",
|
|
520
|
+
"status:Status is locked because the invoice number is missing.",
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
524
|
+
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
525
|
+
type: "TABLE_OR_SUBTABLE",
|
|
526
|
+
behavior: {
|
|
527
|
+
lockedFields: [
|
|
528
|
+
{
|
|
529
|
+
field: { fieldId: "contact-name" },
|
|
530
|
+
conditions: [],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
field: { fieldId: "status" },
|
|
534
|
+
conditions: [
|
|
535
|
+
{
|
|
536
|
+
origin: { fieldId: "invoice-number" },
|
|
537
|
+
comparisonOperator: "IS_EMPTY",
|
|
538
|
+
values: [],
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
origin: { fieldId: "amount" },
|
|
542
|
+
comparisonOperator: "GREATER_OR_EQUAL",
|
|
543
|
+
values: ["500"],
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
messageBeforeLock: "Status locks until the invoice number is set.",
|
|
547
|
+
messageAfterLock: "Status is locked because the invoice number is missing.",
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("clears locked fields with --clear-locked-fields", async () => {
|
|
555
|
+
const configPath = createApiConfig();
|
|
556
|
+
const output = createStdoutBuffer();
|
|
557
|
+
const fetchMock = createFetchMock();
|
|
558
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-l2" }));
|
|
559
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
560
|
+
|
|
561
|
+
await program.parseAsync([
|
|
562
|
+
"node",
|
|
563
|
+
"zinkee",
|
|
564
|
+
"--json",
|
|
565
|
+
"displays",
|
|
566
|
+
"widgets",
|
|
567
|
+
"update",
|
|
568
|
+
"display-1",
|
|
569
|
+
"widget-l2",
|
|
570
|
+
"--clear-locked-fields",
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
574
|
+
expect(init?.method).toBe("PATCH");
|
|
575
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
576
|
+
behavior: { lockedFields: [] },
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("rejects --clear-locked-fields combined with --lock-* flags", async () => {
|
|
581
|
+
const configPath = createApiConfig();
|
|
582
|
+
const output = createStdoutBuffer();
|
|
583
|
+
const fetchMock = createFetchMock();
|
|
584
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
585
|
+
|
|
586
|
+
await expect(
|
|
587
|
+
program.parseAsync([
|
|
588
|
+
"node",
|
|
589
|
+
"zinkee",
|
|
590
|
+
"--json",
|
|
591
|
+
"displays",
|
|
592
|
+
"widgets",
|
|
593
|
+
"update",
|
|
594
|
+
"display-1",
|
|
595
|
+
"widget-l3",
|
|
596
|
+
"--clear-locked-fields",
|
|
597
|
+
"--lock-field",
|
|
598
|
+
"status",
|
|
599
|
+
]),
|
|
600
|
+
).rejects.toThrow(/not both/i);
|
|
601
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("rejects a --lock-condition without an expression", async () => {
|
|
605
|
+
const configPath = createApiConfig();
|
|
606
|
+
const output = createStdoutBuffer();
|
|
607
|
+
const fetchMock = createFetchMock();
|
|
608
|
+
const errorSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
609
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
610
|
+
|
|
611
|
+
// Option arg parser errors are handled by commander, which prints the
|
|
612
|
+
// InvalidArgumentError message to stderr and exits with code 1 (same
|
|
613
|
+
// behavior as an invalid --filter expression).
|
|
614
|
+
await expect(
|
|
615
|
+
program.parseAsync([
|
|
616
|
+
"node",
|
|
617
|
+
"zinkee",
|
|
618
|
+
"--json",
|
|
619
|
+
"displays",
|
|
620
|
+
"widgets",
|
|
621
|
+
"create",
|
|
622
|
+
"display-1",
|
|
623
|
+
"--type",
|
|
624
|
+
"table",
|
|
625
|
+
"--lock-condition",
|
|
626
|
+
"status",
|
|
627
|
+
]),
|
|
628
|
+
).rejects.toThrow(/process.exit/);
|
|
629
|
+
expect(errorSpy.mock.calls.flat().join("\n")).toMatch(/lockedField/i);
|
|
630
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
631
|
+
});
|
|
632
|
+
|
|
492
633
|
it("translates --filter into the structured display filter contract", async () => {
|
|
493
634
|
const configPath = createApiConfig();
|
|
494
635
|
const output = createStdoutBuffer();
|
|
@@ -797,4 +938,69 @@ describe("displays commands", () => {
|
|
|
797
938
|
|
|
798
939
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
799
940
|
});
|
|
941
|
+
|
|
942
|
+
it("wraps a flag-built cross widget filter configuration in an array", async () => {
|
|
943
|
+
const configPath = createApiConfig();
|
|
944
|
+
const output = createStdoutBuffer();
|
|
945
|
+
const fetchMock = createFetchMock();
|
|
946
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
947
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
948
|
+
|
|
949
|
+
await program.parseAsync([
|
|
950
|
+
"node",
|
|
951
|
+
"zinkee",
|
|
952
|
+
"--json",
|
|
953
|
+
"displays",
|
|
954
|
+
"cross-widget-filter",
|
|
955
|
+
"set",
|
|
956
|
+
"display-1",
|
|
957
|
+
"--source-widget",
|
|
958
|
+
"w-source",
|
|
959
|
+
"--source-field",
|
|
960
|
+
"f-source",
|
|
961
|
+
"--target",
|
|
962
|
+
"w-target:f-target",
|
|
963
|
+
]);
|
|
964
|
+
|
|
965
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
966
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
967
|
+
"https://api.zinkee.test/api/v2/displays/display-1/cross-widget-filter",
|
|
968
|
+
);
|
|
969
|
+
expect(init?.method).toBe("PUT");
|
|
970
|
+
expect(JSON.parse(String(init?.body))).toEqual([
|
|
971
|
+
{
|
|
972
|
+
source: { widgetId: "w-source", fieldId: "f-source" },
|
|
973
|
+
targets: [{ widgetId: "w-target", fieldId: "f-target" }],
|
|
974
|
+
},
|
|
975
|
+
]);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("sends the raw array of cross widget filter configurations verbatim", async () => {
|
|
979
|
+
const configPath = createApiConfig();
|
|
980
|
+
const output = createStdoutBuffer();
|
|
981
|
+
const fetchMock = createFetchMock();
|
|
982
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
983
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
984
|
+
|
|
985
|
+
const rawArray = [
|
|
986
|
+
{ source: { widgetId: "w1", fieldId: "f1" }, targets: [{ widgetId: "t1", fieldId: "f1" }] },
|
|
987
|
+
{ source: { widgetId: "w2", fieldId: "f2" }, targets: [{ widgetId: "t2", fieldId: "f2" }] },
|
|
988
|
+
];
|
|
989
|
+
|
|
990
|
+
await program.parseAsync([
|
|
991
|
+
"node",
|
|
992
|
+
"zinkee",
|
|
993
|
+
"--json",
|
|
994
|
+
"displays",
|
|
995
|
+
"cross-widget-filter",
|
|
996
|
+
"set",
|
|
997
|
+
"display-1",
|
|
998
|
+
"--raw",
|
|
999
|
+
JSON.stringify(rawArray),
|
|
1000
|
+
]);
|
|
1001
|
+
|
|
1002
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
1003
|
+
expect(init?.method).toBe("PUT");
|
|
1004
|
+
expect(JSON.parse(String(init?.body))).toEqual(rawArray);
|
|
1005
|
+
});
|
|
800
1006
|
});
|
package/src/commands/displays.ts
CHANGED
|
@@ -98,6 +98,11 @@ interface WidgetMutationOptions {
|
|
|
98
98
|
hideSortButton?: boolean;
|
|
99
99
|
hideFilterButton?: boolean;
|
|
100
100
|
hideSeeButton?: boolean;
|
|
101
|
+
lockField?: string[];
|
|
102
|
+
lockCondition?: LockConditionOption[];
|
|
103
|
+
lockMessageBefore?: LockMessageOption[];
|
|
104
|
+
lockMessageAfter?: LockMessageOption[];
|
|
105
|
+
clearLockedFields?: boolean;
|
|
101
106
|
calc?: string;
|
|
102
107
|
calcField?: string;
|
|
103
108
|
kpiConfig?: string;
|
|
@@ -141,6 +146,16 @@ interface WidgetMutationOptions {
|
|
|
141
146
|
rawFile?: string;
|
|
142
147
|
}
|
|
143
148
|
|
|
149
|
+
interface LockConditionOption {
|
|
150
|
+
lockedField: string;
|
|
151
|
+
expression: ParsedExpression;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface LockMessageOption {
|
|
155
|
+
lockedField: string;
|
|
156
|
+
message: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
interface NavigationTargetMutationOptions {
|
|
145
160
|
sourceWidget?: string;
|
|
146
161
|
targetDisplay?: string;
|
|
@@ -149,9 +164,12 @@ interface NavigationTargetMutationOptions {
|
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
interface CrossWidgetFilterSetOptions {
|
|
152
|
-
sourceWidget
|
|
153
|
-
sourceField
|
|
154
|
-
|
|
167
|
+
sourceWidget?: string;
|
|
168
|
+
sourceField?: string;
|
|
169
|
+
sourceSchema?: string;
|
|
170
|
+
target?: string[];
|
|
171
|
+
raw?: string;
|
|
172
|
+
rawFile?: string;
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
@@ -272,6 +290,115 @@ function collectFilters(value: string, previous: ParsedExpression[] = []): Parse
|
|
|
272
290
|
return [...previous, parseFilterExpression(value)];
|
|
273
291
|
}
|
|
274
292
|
|
|
293
|
+
function parseLockConditionOption(input: string): LockConditionOption {
|
|
294
|
+
const trimmed = input.trim();
|
|
295
|
+
let lockedField: string;
|
|
296
|
+
let expressionText: string;
|
|
297
|
+
|
|
298
|
+
if (trimmed.includes(":") && !trimmed.includes(" ")) {
|
|
299
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
300
|
+
lockedField = trimmed.slice(0, separatorIndex).trim();
|
|
301
|
+
expressionText = trimmed.slice(separatorIndex + 1).trim();
|
|
302
|
+
} else {
|
|
303
|
+
const parts = trimmed.split(/\s+/);
|
|
304
|
+
lockedField = parts[0]?.trim() ?? "";
|
|
305
|
+
expressionText = parts.slice(1).join(" ").trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!lockedField || !expressionText) {
|
|
309
|
+
throw new InvalidArgumentError(
|
|
310
|
+
'Expected "<lockedField>:<field>:<comparator>[:<value>]" (or space-separated), e.g. "status:invoice-number:is_empty".',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { lockedField, expression: parseFilterExpression(expressionText) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function collectLockConditions(value: string, previous: LockConditionOption[] = []): LockConditionOption[] {
|
|
318
|
+
return [...previous, parseLockConditionOption(value)];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseLockMessageOption(input: string): LockMessageOption {
|
|
322
|
+
const separatorIndex = input.indexOf(":");
|
|
323
|
+
const lockedField = separatorIndex > 0 ? input.slice(0, separatorIndex).trim() : "";
|
|
324
|
+
const message = separatorIndex > 0 ? input.slice(separatorIndex + 1).trim() : "";
|
|
325
|
+
|
|
326
|
+
if (!lockedField || !message) {
|
|
327
|
+
throw new InvalidArgumentError('Expected "<lockedField>:<message>", e.g. "status:Locked once approved.".');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { lockedField, message };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function collectLockMessages(value: string, previous: LockMessageOption[] = []): LockMessageOption[] {
|
|
334
|
+
return [...previous, parseLockMessageOption(value)];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Builds behavior.lockedFields from the --lock-* flags. Every flag names its
|
|
339
|
+
* locked field explicitly, so ordering between flags never matters. A field
|
|
340
|
+
* with no conditions is locked permanently; with conditions it locks when all
|
|
341
|
+
* of them match (AND). Returns [] for --clear-locked-fields and undefined when
|
|
342
|
+
* no lock flag was used.
|
|
343
|
+
*/
|
|
344
|
+
function buildLockedFields(options: WidgetMutationOptions): Array<Record<string, unknown>> | undefined {
|
|
345
|
+
const hasLockFlags =
|
|
346
|
+
(options.lockField?.length ?? 0) > 0 ||
|
|
347
|
+
(options.lockCondition?.length ?? 0) > 0 ||
|
|
348
|
+
(options.lockMessageBefore?.length ?? 0) > 0 ||
|
|
349
|
+
(options.lockMessageAfter?.length ?? 0) > 0;
|
|
350
|
+
|
|
351
|
+
if (options.clearLockedFields) {
|
|
352
|
+
if (hasLockFlags) {
|
|
353
|
+
throw new CliError(
|
|
354
|
+
"invalid_cli_usage",
|
|
355
|
+
'Use either "--clear-locked-fields" or the "--lock-*" flags, not both.',
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!hasLockFlags) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
interface LockRule {
|
|
366
|
+
conditions: Array<Record<string, unknown>>;
|
|
367
|
+
messageBeforeLock?: string;
|
|
368
|
+
messageAfterLock?: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const rules = new Map<string, LockRule>();
|
|
372
|
+
const ensureRule = (lockedField: string): LockRule => {
|
|
373
|
+
let rule = rules.get(lockedField);
|
|
374
|
+
if (!rule) {
|
|
375
|
+
rule = { conditions: [] };
|
|
376
|
+
rules.set(lockedField, rule);
|
|
377
|
+
}
|
|
378
|
+
return rule;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
for (const lockedField of options.lockField ?? []) {
|
|
382
|
+
ensureRule(lockedField);
|
|
383
|
+
}
|
|
384
|
+
for (const { lockedField, expression } of options.lockCondition ?? []) {
|
|
385
|
+
ensureRule(lockedField).conditions.push(toDisplayFilter(expression));
|
|
386
|
+
}
|
|
387
|
+
for (const { lockedField, message } of options.lockMessageBefore ?? []) {
|
|
388
|
+
ensureRule(lockedField).messageBeforeLock = message;
|
|
389
|
+
}
|
|
390
|
+
for (const { lockedField, message } of options.lockMessageAfter ?? []) {
|
|
391
|
+
ensureRule(lockedField).messageAfterLock = message;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return [...rules.entries()].map(([lockedField, rule]) => ({
|
|
395
|
+
field: { fieldId: lockedField },
|
|
396
|
+
conditions: rule.conditions,
|
|
397
|
+
...(rule.messageBeforeLock !== undefined ? { messageBeforeLock: rule.messageBeforeLock } : {}),
|
|
398
|
+
...(rule.messageAfterLock !== undefined ? { messageAfterLock: rule.messageAfterLock } : {}),
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
275
402
|
function parseBooleanFlag(input: string): boolean {
|
|
276
403
|
const normalized = input.trim().toLowerCase();
|
|
277
404
|
if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
|
|
@@ -694,6 +821,14 @@ function buildWidgetBody(options: WidgetMutationOptions): Record<string, unknown
|
|
|
694
821
|
};
|
|
695
822
|
}
|
|
696
823
|
|
|
824
|
+
const lockedFields = buildLockedFields(options);
|
|
825
|
+
if (lockedFields !== undefined) {
|
|
826
|
+
body.behavior = {
|
|
827
|
+
...(typeof body.behavior === "object" && body.behavior ? body.behavior as Record<string, unknown> : {}),
|
|
828
|
+
lockedFields,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
697
832
|
if (options.calc || options.calcField || options.kpiConfig) {
|
|
698
833
|
body.kpi = {
|
|
699
834
|
...(typeof body.kpi === "object" && body.kpi ? body.kpi as Record<string, unknown> : {}),
|
|
@@ -744,20 +879,60 @@ function buildNavigationTargetBody(options: NavigationTargetMutationOptions): Re
|
|
|
744
879
|
return body;
|
|
745
880
|
}
|
|
746
881
|
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
882
|
+
function readRawArray(raw?: string, rawFile?: string): unknown[] {
|
|
883
|
+
if (raw && rawFile) {
|
|
884
|
+
throw new CliError("invalid_cli_usage", 'Use either "--raw" or "--raw-file", not both.');
|
|
885
|
+
}
|
|
886
|
+
const source = rawFile ? readFileSync(rawFile, "utf8") : raw;
|
|
887
|
+
if (!source) {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const parsed = JSON.parse(source) as unknown;
|
|
892
|
+
if (!Array.isArray(parsed)) {
|
|
893
|
+
throw new CliError("invalid_cli_usage", "Raw cross widget filter payload must be a JSON array of configurations.");
|
|
894
|
+
}
|
|
895
|
+
return parsed;
|
|
896
|
+
} catch (error) {
|
|
897
|
+
if (error instanceof CliError) throw error;
|
|
898
|
+
throw new CliError("invalid_cli_usage", "Raw JSON payload is invalid.");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// The endpoint replaces the whole array of configurations. With flags a single configuration is
|
|
903
|
+
// built and wrapped in an array; use --raw/--raw-file to send several configurations at once.
|
|
904
|
+
function buildCrossWidgetFilterBody(options: CrossWidgetFilterSetOptions): unknown[] {
|
|
905
|
+
if (options.raw !== undefined || options.rawFile !== undefined) {
|
|
906
|
+
return readRawArray(options.raw, options.rawFile);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (!options.sourceWidget || !options.sourceField) {
|
|
910
|
+
throw new CliError(
|
|
911
|
+
"invalid_cli_usage",
|
|
912
|
+
"Provide --source-widget and --source-field, or --raw/--raw-file with the full configuration array.",
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const source: Record<string, unknown> = {
|
|
917
|
+
widgetId: options.sourceWidget,
|
|
918
|
+
fieldId: options.sourceField,
|
|
760
919
|
};
|
|
920
|
+
if (options.sourceSchema) {
|
|
921
|
+
source.schemaId = options.sourceSchema;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return [
|
|
925
|
+
{
|
|
926
|
+
source,
|
|
927
|
+
targets: (options.target ?? []).map((entry) => {
|
|
928
|
+
const [widgetId, fieldId] = entry.split(":");
|
|
929
|
+
if (!widgetId || !fieldId) {
|
|
930
|
+
throw new CliError("invalid_cli_usage", `Invalid --target entry "${entry}". Use <widgetId:fieldId>.`);
|
|
931
|
+
}
|
|
932
|
+
return { widgetId, fieldId };
|
|
933
|
+
}),
|
|
934
|
+
},
|
|
935
|
+
];
|
|
761
936
|
}
|
|
762
937
|
|
|
763
938
|
export function registerDisplaysCommands(
|
|
@@ -1088,6 +1263,11 @@ export function registerDisplaysCommands(
|
|
|
1088
1263
|
.option("--sort <sort>", "repeatable sort", (v, p: string[] = []) => [...p, v], [])
|
|
1089
1264
|
.option("--filter <expr>", "repeatable filter", collectFilters, [])
|
|
1090
1265
|
.option("--widget-read-only").option("--hide-add-button").option("--hide-delete-button").option("--hide-sort-button").option("--hide-filter-button").option("--hide-see-button")
|
|
1266
|
+
.option("--lock-field <field>", "lock a field permanently in this widget (repeatable; field UUID or slug)", (v, p: string[] = []) => [...p, v], [])
|
|
1267
|
+
.option("--lock-condition <expr>", 'conditional lock "<lockedField>:<field>:<comparator>[:<value>]"; locks <lockedField> when all its conditions match (AND, repeatable)', collectLockConditions, [])
|
|
1268
|
+
.option("--lock-message-before <field:text>", "message shown while a conditional lock is not met yet (repeatable)", collectLockMessages, [])
|
|
1269
|
+
.option("--lock-message-after <field:text>", "message shown once the field is locked (repeatable)", collectLockMessages, [])
|
|
1270
|
+
.option("--clear-locked-fields", "clear the widget locked-fields configuration")
|
|
1091
1271
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
1092
1272
|
.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")
|
|
1093
1273
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
@@ -1135,6 +1315,11 @@ export function registerDisplaysCommands(
|
|
|
1135
1315
|
.option("--sort <sort>", "repeatable sort", (v, p: string[] = []) => [...p, v], [])
|
|
1136
1316
|
.option("--filter <expr>", "repeatable filter", collectFilters, [])
|
|
1137
1317
|
.option("--widget-read-only").option("--hide-add-button").option("--hide-delete-button").option("--hide-sort-button").option("--hide-filter-button").option("--hide-see-button")
|
|
1318
|
+
.option("--lock-field <field>", "lock a field permanently in this widget (repeatable; field UUID or slug)", (v, p: string[] = []) => [...p, v], [])
|
|
1319
|
+
.option("--lock-condition <expr>", 'conditional lock "<lockedField>:<field>:<comparator>[:<value>]"; locks <lockedField> when all its conditions match (AND, repeatable)', collectLockConditions, [])
|
|
1320
|
+
.option("--lock-message-before <field:text>", "message shown while a conditional lock is not met yet (repeatable)", collectLockMessages, [])
|
|
1321
|
+
.option("--lock-message-after <field:text>", "message shown once the field is locked (repeatable)", collectLockMessages, [])
|
|
1322
|
+
.option("--clear-locked-fields", "clear the widget locked-fields configuration")
|
|
1138
1323
|
.option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
|
|
1139
1324
|
.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")
|
|
1140
1325
|
.option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
|
|
@@ -1212,10 +1397,15 @@ export function registerDisplaysCommands(
|
|
|
1212
1397
|
const cross = displays.command("cross-widget-filter").description("Manage cross widget filter");
|
|
1213
1398
|
cross.command("get").description("Get cross widget filter").argument("<display>", "display uuid or slug", parseUuidOrSlugSelector).action(async function action(this: Command, display: NormalizedUuidOrSlug) {
|
|
1214
1399
|
const { api, json } = createDisplaysApi(this, filePath); const data = await api.getDisplay(display); const item = data.crossWidgetFilter ?? null;
|
|
1215
|
-
if (json) { emitJson(stdout, "displays cross-widget-filter get", item); return; } printTable(item
|
|
1400
|
+
if (json) { emitJson(stdout, "displays cross-widget-filter get", item); return; } printTable(item ?? [], undefined, stdout);
|
|
1216
1401
|
});
|
|
1217
|
-
cross.command("set").description("Set cross widget filter").argument("<display>", "display uuid or slug", parseUuidOrSlugSelector)
|
|
1218
|
-
.
|
|
1402
|
+
cross.command("set").description("Set cross widget filter (replaces the whole array of configurations)").argument("<display>", "display uuid or slug", parseUuidOrSlugSelector)
|
|
1403
|
+
.option("--source-widget <id>", "source widget id (single-configuration mode)")
|
|
1404
|
+
.option("--source-field <id>", "source field id (single-configuration mode)")
|
|
1405
|
+
.option("--source-schema <id>", "source schema id, only for HIERARCHY source widgets")
|
|
1406
|
+
.option("--target <widget:field>", "repeatable target (single-configuration mode)", (v, p: string[] = []) => [...p, v], [])
|
|
1407
|
+
.option("--raw <json>", "full array of configurations as JSON (for several configurations)")
|
|
1408
|
+
.option("--raw-file <path>", "path to a JSON file with the full array of configurations")
|
|
1219
1409
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: CrossWidgetFilterSetOptions) {
|
|
1220
1410
|
const { api, json, readOnly } = createDisplaysApi(this, filePath); ensureWritable("displays cross-widget-filter set", readOnly, "write");
|
|
1221
1411
|
const data = await api.setCrossWidgetFilter(display, buildCrossWidgetFilterBody(commandOptions));
|