@uipath/tasks-tool 1.201.0-preview.115 → 1.201.0-preview.121
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/browser.json +2 -1
- package/dist/folder-picker-interactive-2kj361mt.js +1850 -0
- package/dist/folder-resolver-htdmzdhy.js +92 -0
- package/dist/index.js +5 -3
- package/dist/tool-0n9mj887.js +2448 -0
- package/dist/tool-rsk1xt86.js +6026 -0
- package/dist/{tool-vw6gha5d.js → tool-ww671age.js} +7469 -10469
- package/dist/tool.js +3 -2
- package/package.json +2 -2
- package/src/commands/_shared/catalog-options.spec.ts +114 -0
- package/src/commands/_shared/catalog-options.ts +122 -0
- package/src/commands/_shared/connect.spec.ts +52 -0
- package/src/commands/_shared/connect.ts +31 -0
- package/src/commands/_shared/constants.ts +1 -0
- package/src/commands/_shared/folder-picker-interactive.spec.ts +92 -0
- package/src/commands/_shared/folder-picker-interactive.ts +76 -0
- package/src/commands/_shared/folder-picker.spec.ts +159 -0
- package/src/commands/_shared/folder-picker.ts +113 -0
- package/src/commands/_shared/folder-resolver.spec.ts +152 -0
- package/src/commands/_shared/folder-resolver.ts +113 -0
- package/src/commands/_shared/parse.spec.ts +69 -0
- package/src/commands/_shared/parse.ts +43 -0
- package/src/commands/catalogs.ts +277 -0
- package/src/commands/comments.ts +160 -0
- package/src/commands/data.ts +137 -0
- package/src/commands/labels.ts +93 -0
- package/src/commands/metadata.ts +139 -0
- package/src/commands/tasks.spec.ts +1557 -2
- package/src/commands/tasks.ts +191 -280
- package/src/tool.ts +10 -0
- package/src/utils/sdk-client.ts +5 -0
- package/tests/assignable-users.e2e.test.ts +58 -4
- package/tests/catalogs.e2e.test.ts +393 -0
- package/tests/comments.e2e.test.ts +223 -0
- package/tests/complete.e2e.test.ts +9 -9
- package/tests/data.e2e.test.ts +187 -0
- package/tests/get.e2e.test.ts +4 -2
- package/tests/labels.e2e.test.ts +184 -0
- package/tests/list.e2e.test.ts +110 -0
- package/tests/metadata.e2e.test.ts +186 -0
package/dist/tool.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
metadata,
|
|
3
3
|
registerCommands
|
|
4
|
-
} from "./tool-
|
|
4
|
+
} from "./tool-ww671age.js";
|
|
5
|
+
import"./tool-rsk1xt86.js";
|
|
5
6
|
import"./tool-9qecd4wb.js";
|
|
6
7
|
import"./tool-7eva0peq.js";
|
|
7
8
|
import"./tool-5arsyj36.js";
|
|
@@ -12,4 +13,4 @@ export {
|
|
|
12
13
|
metadata
|
|
13
14
|
};
|
|
14
15
|
|
|
15
|
-
//# debugId=
|
|
16
|
+
//# debugId=106E34AF1A233DCE64756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uipath/tasks-tool",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.201.0-preview.
|
|
4
|
+
"version": "1.201.0-preview.121",
|
|
5
5
|
"description": "Manage Action Center tasks.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/tool.js",
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"registry": "https://registry.npmjs.org/"
|
|
16
16
|
},
|
|
17
|
-
"gitHead": "
|
|
17
|
+
"gitHead": "c70ccfc0b12e637441d67df1d212b71d7784b5f8"
|
|
18
18
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
4
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
5
|
+
return {
|
|
6
|
+
...actual,
|
|
7
|
+
OutputFormatter: { success: vi.fn(), error: vi.fn() },
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
import { OutputFormatter } from "@uipath/common";
|
|
12
|
+
import {
|
|
13
|
+
buildCatalogCreateOptions,
|
|
14
|
+
buildCatalogUpdateOptions,
|
|
15
|
+
} from "./catalog-options";
|
|
16
|
+
|
|
17
|
+
describe("buildCatalogCreateOptions", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.resetAllMocks();
|
|
20
|
+
process.exitCode = undefined;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("builds folderId only", () => {
|
|
24
|
+
expect(
|
|
25
|
+
buildCatalogCreateOptions({ folderId: "42", name: "x" }, 42),
|
|
26
|
+
).toEqual({ folderId: 42 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("includes description, encryption, and retention", () => {
|
|
30
|
+
const result = buildCatalogCreateOptions(
|
|
31
|
+
{
|
|
32
|
+
folderId: "42",
|
|
33
|
+
name: "x",
|
|
34
|
+
description: "desc",
|
|
35
|
+
encrypted: true,
|
|
36
|
+
retentionAction: "Archive",
|
|
37
|
+
retentionPeriod: "30",
|
|
38
|
+
retentionBucketId: "7",
|
|
39
|
+
},
|
|
40
|
+
42,
|
|
41
|
+
);
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
folderId: 42,
|
|
44
|
+
description: "desc",
|
|
45
|
+
encrypted: true,
|
|
46
|
+
retentionAction: "Archive",
|
|
47
|
+
retentionPeriod: 30,
|
|
48
|
+
retentionBucketId: 7,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("rejects an invalid retention action", () => {
|
|
53
|
+
const result = buildCatalogCreateOptions(
|
|
54
|
+
{ folderId: "42", name: "x", retentionAction: "Purge" },
|
|
55
|
+
42,
|
|
56
|
+
);
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
Message: "Invalid --retention-action 'Purge'",
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it.each([
|
|
66
|
+
["retentionPeriod", "--retention-period"],
|
|
67
|
+
["retentionBucketId", "--retention-bucket-id"],
|
|
68
|
+
])("rejects an invalid %s", (field, flag) => {
|
|
69
|
+
const result = buildCatalogCreateOptions(
|
|
70
|
+
{ folderId: "42", name: "x", [field]: "abc" },
|
|
71
|
+
42,
|
|
72
|
+
);
|
|
73
|
+
expect(result).toBeNull();
|
|
74
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
75
|
+
expect.objectContaining({ Message: `Invalid ${flag}: abc` }),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("buildCatalogUpdateOptions", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.resetAllMocks();
|
|
83
|
+
process.exitCode = undefined;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("includes name when provided", () => {
|
|
87
|
+
expect(
|
|
88
|
+
buildCatalogUpdateOptions({ folderId: "42", name: "renamed" }, 42),
|
|
89
|
+
).toEqual({ folderId: 42, name: "renamed" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("omits name when not provided and never sets encrypted", () => {
|
|
93
|
+
const result = buildCatalogUpdateOptions(
|
|
94
|
+
{ folderId: "42", description: "d" },
|
|
95
|
+
42,
|
|
96
|
+
);
|
|
97
|
+
expect(result).toEqual({ folderId: 42, description: "d" });
|
|
98
|
+
expect(result).not.toHaveProperty("name");
|
|
99
|
+
expect(result).not.toHaveProperty("encrypted");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("rejects an invalid retention action", () => {
|
|
103
|
+
const result = buildCatalogUpdateOptions(
|
|
104
|
+
{ folderId: "42", retentionAction: "Nope" },
|
|
105
|
+
42,
|
|
106
|
+
);
|
|
107
|
+
expect(result).toBeNull();
|
|
108
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
Message: "Invalid --retention-action 'Nope'",
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { OutputFormatter, processContext, RESULTS } from "@uipath/common";
|
|
2
|
+
import type {
|
|
3
|
+
TaskCatalogCreateOptions,
|
|
4
|
+
TaskCatalogUpdateOptions,
|
|
5
|
+
} from "@uipath/uipath-typescript";
|
|
6
|
+
import { TaskCatalogRetentionAction } from "@uipath/uipath-typescript";
|
|
7
|
+
|
|
8
|
+
import type { FolderScopeOptions } from "./folder-picker";
|
|
9
|
+
import { parsePositiveInt, reportInvalidInt } from "./parse";
|
|
10
|
+
|
|
11
|
+
// None is a read-only state, not a settable action, so the CLI only offers Delete and Archive.
|
|
12
|
+
const RETENTION_ACTIONS: TaskCatalogRetentionAction[] = [
|
|
13
|
+
TaskCatalogRetentionAction.Delete,
|
|
14
|
+
TaskCatalogRetentionAction.Archive,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export interface CatalogListOptions extends FolderScopeOptions {
|
|
18
|
+
limit?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CatalogGetOptions = FolderScopeOptions;
|
|
22
|
+
|
|
23
|
+
export interface CatalogCreateOptions extends FolderScopeOptions {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
encrypted?: boolean;
|
|
27
|
+
retentionAction?: string;
|
|
28
|
+
retentionPeriod?: string;
|
|
29
|
+
retentionBucketId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CatalogUpdateOptions extends FolderScopeOptions {
|
|
33
|
+
name?: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
retentionAction?: string;
|
|
36
|
+
retentionPeriod?: string;
|
|
37
|
+
retentionBucketId?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Common retention/description fields shared by create and update. Returns
|
|
41
|
+
// false after emitting an error + exit when a value is invalid.
|
|
42
|
+
function applyCommonFields(
|
|
43
|
+
target: {
|
|
44
|
+
description?: string;
|
|
45
|
+
retentionAction?: TaskCatalogRetentionAction;
|
|
46
|
+
retentionPeriod?: number;
|
|
47
|
+
retentionBucketId?: number;
|
|
48
|
+
},
|
|
49
|
+
options: {
|
|
50
|
+
description?: string;
|
|
51
|
+
retentionAction?: string;
|
|
52
|
+
retentionPeriod?: string;
|
|
53
|
+
retentionBucketId?: string;
|
|
54
|
+
},
|
|
55
|
+
): boolean {
|
|
56
|
+
if (options.description !== undefined) {
|
|
57
|
+
target.description = options.description;
|
|
58
|
+
}
|
|
59
|
+
if (options.retentionAction !== undefined) {
|
|
60
|
+
if (
|
|
61
|
+
!RETENTION_ACTIONS.includes(
|
|
62
|
+
options.retentionAction as TaskCatalogRetentionAction,
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
OutputFormatter.error({
|
|
66
|
+
Result: RESULTS.Failure,
|
|
67
|
+
Message: `Invalid --retention-action '${options.retentionAction}'`,
|
|
68
|
+
Instructions: `Valid values: ${RETENTION_ACTIONS.join(", ")}`,
|
|
69
|
+
});
|
|
70
|
+
processContext.exit(1);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
target.retentionAction =
|
|
74
|
+
options.retentionAction as TaskCatalogRetentionAction;
|
|
75
|
+
}
|
|
76
|
+
if (options.retentionPeriod !== undefined) {
|
|
77
|
+
const parsed = parsePositiveInt(
|
|
78
|
+
options.retentionPeriod,
|
|
79
|
+
"--retention-period",
|
|
80
|
+
);
|
|
81
|
+
if (!parsed.valid) {
|
|
82
|
+
reportInvalidInt(parsed);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
target.retentionPeriod = parsed.value;
|
|
86
|
+
}
|
|
87
|
+
if (options.retentionBucketId !== undefined) {
|
|
88
|
+
const parsed = parsePositiveInt(
|
|
89
|
+
options.retentionBucketId,
|
|
90
|
+
"--retention-bucket-id",
|
|
91
|
+
);
|
|
92
|
+
if (!parsed.valid) {
|
|
93
|
+
reportInvalidInt(parsed);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
target.retentionBucketId = parsed.value;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildCatalogCreateOptions(
|
|
102
|
+
options: CatalogCreateOptions,
|
|
103
|
+
folderId: number,
|
|
104
|
+
): TaskCatalogCreateOptions | null {
|
|
105
|
+
const result: TaskCatalogCreateOptions = { folderId };
|
|
106
|
+
if (options.encrypted) {
|
|
107
|
+
result.encrypted = true;
|
|
108
|
+
}
|
|
109
|
+
return applyCommonFields(result, options) ? result : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildCatalogUpdateOptions(
|
|
113
|
+
options: CatalogUpdateOptions,
|
|
114
|
+
folderId: number,
|
|
115
|
+
): TaskCatalogUpdateOptions | null {
|
|
116
|
+
// `encrypted` is immutable on update, so it is intentionally not set here.
|
|
117
|
+
const result: TaskCatalogUpdateOptions = { folderId };
|
|
118
|
+
if (options.name !== undefined) {
|
|
119
|
+
result.name = options.name;
|
|
120
|
+
}
|
|
121
|
+
return applyCommonFields(result, options) ? result : null;
|
|
122
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../../utils/sdk-client", () => ({
|
|
4
|
+
createTasksClient: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
OutputFormatter: { success: vi.fn(), error: vi.fn() },
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
import { OutputFormatter } from "@uipath/common";
|
|
16
|
+
import { createTasksClient } from "../../utils/sdk-client";
|
|
17
|
+
import { connectTasksClient } from "./connect";
|
|
18
|
+
|
|
19
|
+
describe("connectTasksClient", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
process.exitCode = undefined;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns the client on success", async () => {
|
|
26
|
+
const sdk = { tasks: {} };
|
|
27
|
+
vi.mocked(createTasksClient).mockResolvedValue(sdk as never);
|
|
28
|
+
|
|
29
|
+
const result = await connectTasksClient("MyTenant");
|
|
30
|
+
|
|
31
|
+
expect(result).toBe(sdk);
|
|
32
|
+
expect(createTasksClient).toHaveBeenCalledWith("MyTenant");
|
|
33
|
+
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reports, exits, and returns null on failure", async () => {
|
|
37
|
+
vi.mocked(createTasksClient).mockRejectedValue(
|
|
38
|
+
new Error("Not logged in"),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const result = await connectTasksClient();
|
|
42
|
+
|
|
43
|
+
expect(result).toBeNull();
|
|
44
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
45
|
+
expect.objectContaining({
|
|
46
|
+
Result: "Failure",
|
|
47
|
+
Message: "Error connecting to Action Center",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
expect(process.exitCode).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
catchError,
|
|
3
|
+
extractErrorMessage,
|
|
4
|
+
OutputFormatter,
|
|
5
|
+
processContext,
|
|
6
|
+
RESULTS,
|
|
7
|
+
} from "@uipath/common";
|
|
8
|
+
import type { UiPath } from "@uipath/uipath-typescript";
|
|
9
|
+
|
|
10
|
+
import { createTasksClient } from "../../utils/sdk-client";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Connects to Action Center for the given tenant. On failure, emits the
|
|
14
|
+
* standard error, exits with code 1, and returns null so the caller can
|
|
15
|
+
* `return` early. Keeps the connect boilerplate in one place.
|
|
16
|
+
*/
|
|
17
|
+
export async function connectTasksClient(
|
|
18
|
+
tenant?: string,
|
|
19
|
+
): Promise<UiPath | null> {
|
|
20
|
+
const [clientError, sdk] = await catchError(createTasksClient(tenant));
|
|
21
|
+
if (clientError) {
|
|
22
|
+
OutputFormatter.error({
|
|
23
|
+
Result: RESULTS.Failure,
|
|
24
|
+
Message: "Error connecting to Action Center",
|
|
25
|
+
Instructions: await extractErrorMessage(clientError),
|
|
26
|
+
});
|
|
27
|
+
processContext.exit(1);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return sdk;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_PAGE_SIZE = 100;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@inquirer/prompts", () => ({ select: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
vi.mock("@uipath/orchestrator-sdk", () => ({
|
|
6
|
+
createApiClient: vi.fn(),
|
|
7
|
+
FoldersApi: class {},
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
OutputFormatter: { success: vi.fn(), error: vi.fn() },
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
import { select } from "@inquirer/prompts";
|
|
19
|
+
import { OutputFormatter } from "@uipath/common";
|
|
20
|
+
import { createApiClient } from "@uipath/orchestrator-sdk";
|
|
21
|
+
import { pickFolderInteractively } from "./folder-picker-interactive";
|
|
22
|
+
|
|
23
|
+
function mockFolders(pageItems: unknown[]) {
|
|
24
|
+
vi.mocked(createApiClient).mockResolvedValue({
|
|
25
|
+
foldersGetAllForCurrentUser: vi.fn().mockResolvedValue({ pageItems }),
|
|
26
|
+
} as never);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("pickFolderInteractively", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.resetAllMocks();
|
|
32
|
+
process.exitCode = undefined;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("lists folders and returns the selected id", async () => {
|
|
36
|
+
mockFolders([
|
|
37
|
+
{ id: 7, displayName: "Finance" },
|
|
38
|
+
{ id: 9, displayName: "HR" },
|
|
39
|
+
]);
|
|
40
|
+
vi.mocked(select).mockResolvedValue(7 as never);
|
|
41
|
+
|
|
42
|
+
expect(await pickFolderInteractively()).toBe(7);
|
|
43
|
+
expect(select).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("passes the tenant through to the folder client", async () => {
|
|
47
|
+
mockFolders([{ id: 7, displayName: "Finance" }]);
|
|
48
|
+
vi.mocked(select).mockResolvedValue(7 as never);
|
|
49
|
+
|
|
50
|
+
expect(await pickFolderInteractively("my-tenant")).toBe(7);
|
|
51
|
+
expect(createApiClient).toHaveBeenCalledWith(expect.anything(), {
|
|
52
|
+
tenant: "my-tenant",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("reports a connection error", async () => {
|
|
57
|
+
vi.mocked(createApiClient).mockRejectedValue(new Error("boom"));
|
|
58
|
+
|
|
59
|
+
expect(await pickFolderInteractively()).toBeNull();
|
|
60
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
61
|
+
expect.objectContaining({
|
|
62
|
+
Message: "Error connecting to Action Center",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
expect(process.exitCode).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reports a listing error", async () => {
|
|
69
|
+
vi.mocked(createApiClient).mockResolvedValue({
|
|
70
|
+
foldersGetAllForCurrentUser: vi
|
|
71
|
+
.fn()
|
|
72
|
+
.mockRejectedValue(new Error("nope")),
|
|
73
|
+
} as never);
|
|
74
|
+
|
|
75
|
+
expect(await pickFolderInteractively()).toBeNull();
|
|
76
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
77
|
+
expect.objectContaining({ Message: "Error listing folders" }),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("fails when no selectable folders are returned", async () => {
|
|
82
|
+
mockFolders([{ displayName: "no id here" }]);
|
|
83
|
+
|
|
84
|
+
expect(await pickFolderInteractively()).toBeNull();
|
|
85
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
Message: "No folders available to select",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
expect(select).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { select } from "@inquirer/prompts";
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
extractErrorMessage,
|
|
5
|
+
OutputFormatter,
|
|
6
|
+
processContext,
|
|
7
|
+
RESULTS,
|
|
8
|
+
} from "@uipath/common";
|
|
9
|
+
import {
|
|
10
|
+
type CurrentUserFolderDto,
|
|
11
|
+
createApiClient,
|
|
12
|
+
FoldersApi,
|
|
13
|
+
} from "@uipath/orchestrator-sdk";
|
|
14
|
+
|
|
15
|
+
const FOLDER_PAGE_SIZE = 200;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Interactive folder picker. Pulls @inquirer/prompts, which is Node-only
|
|
19
|
+
* (TTY prompts), so it is excluded from the browser bundle
|
|
20
|
+
* (browser.json -> excludedImports) and only imported on a TTY.
|
|
21
|
+
* (@uipath/orchestrator-sdk itself is browser-safe — it ships a browser
|
|
22
|
+
* export and reaches the browser bundle through folder-resolver.)
|
|
23
|
+
*
|
|
24
|
+
* Returns null when listing/selection fails (after emitting an error + exit).
|
|
25
|
+
*/
|
|
26
|
+
export async function pickFolderInteractively(
|
|
27
|
+
tenant?: string,
|
|
28
|
+
): Promise<number | null> {
|
|
29
|
+
const [clientError, api] = await catchError(
|
|
30
|
+
createApiClient(FoldersApi, { tenant }),
|
|
31
|
+
);
|
|
32
|
+
if (clientError) {
|
|
33
|
+
OutputFormatter.error({
|
|
34
|
+
Result: RESULTS.Failure,
|
|
35
|
+
Message: "Error connecting to Action Center",
|
|
36
|
+
Instructions: await extractErrorMessage(clientError),
|
|
37
|
+
});
|
|
38
|
+
processContext.exit(1);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const [listError, result] = await catchError(
|
|
43
|
+
api.foldersGetAllForCurrentUser({ take: FOLDER_PAGE_SIZE }),
|
|
44
|
+
);
|
|
45
|
+
if (listError) {
|
|
46
|
+
OutputFormatter.error({
|
|
47
|
+
Result: RESULTS.Failure,
|
|
48
|
+
Message: "Error listing folders",
|
|
49
|
+
Instructions: await extractErrorMessage(listError),
|
|
50
|
+
});
|
|
51
|
+
processContext.exit(1);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const folders = (result.pageItems ?? []).filter(
|
|
56
|
+
(f: CurrentUserFolderDto): f is CurrentUserFolderDto & { id: number } =>
|
|
57
|
+
typeof f.id === "number",
|
|
58
|
+
);
|
|
59
|
+
if (folders.length === 0) {
|
|
60
|
+
OutputFormatter.error({
|
|
61
|
+
Result: RESULTS.Failure,
|
|
62
|
+
Message: "No folders available to select",
|
|
63
|
+
Instructions: "Pass --folder-id <id> instead.",
|
|
64
|
+
});
|
|
65
|
+
processContext.exit(1);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return select<number>({
|
|
70
|
+
message: "Select a folder",
|
|
71
|
+
choices: folders.map((f) => ({
|
|
72
|
+
name: f.fullyQualifiedName || f.displayName || String(f.id),
|
|
73
|
+
value: f.id,
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./folder-picker-interactive", () => ({
|
|
4
|
+
pickFolderInteractively: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("./folder-resolver", () => ({
|
|
8
|
+
resolveFolderIdByKey: vi.fn(),
|
|
9
|
+
resolveFolderIdByPath: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
OutputFormatter: { success: vi.fn(), error: vi.fn() },
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
import { OutputFormatter } from "@uipath/common";
|
|
21
|
+
import { resolveFolderId, resolveFolderScope } from "./folder-picker";
|
|
22
|
+
import { pickFolderInteractively } from "./folder-picker-interactive";
|
|
23
|
+
import { resolveFolderIdByKey, resolveFolderIdByPath } from "./folder-resolver";
|
|
24
|
+
|
|
25
|
+
const FOLDER_KEY = "12345678-1234-1234-1234-123456789abc";
|
|
26
|
+
|
|
27
|
+
describe("resolveFolderScope", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetAllMocks();
|
|
30
|
+
process.exitCode = undefined;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("parses and returns the id when --folder-id is provided", async () => {
|
|
34
|
+
expect(await resolveFolderScope({ folderId: "42" })).toBe(42);
|
|
35
|
+
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
36
|
+
expect(resolveFolderIdByKey).not.toHaveBeenCalled();
|
|
37
|
+
expect(resolveFolderIdByPath).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reports an invalid --folder-id", async () => {
|
|
41
|
+
expect(await resolveFolderScope({ folderId: "abc" })).toBeNull();
|
|
42
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
43
|
+
expect.objectContaining({ Message: "Invalid --folder-id: abc" }),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("resolves --folder-key through the folder resolver", async () => {
|
|
48
|
+
vi.mocked(resolveFolderIdByKey).mockResolvedValue(55);
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
await resolveFolderScope({ folderKey: FOLDER_KEY, tenant: "t1" }),
|
|
52
|
+
).toBe(55);
|
|
53
|
+
expect(resolveFolderIdByKey).toHaveBeenCalledWith(FOLDER_KEY, "t1");
|
|
54
|
+
expect(resolveFolderIdByPath).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("resolves --folder-path through the folder resolver", async () => {
|
|
58
|
+
vi.mocked(resolveFolderIdByPath).mockResolvedValue(77);
|
|
59
|
+
|
|
60
|
+
expect(await resolveFolderScope({ folderPath: "Shared/Finance" })).toBe(
|
|
61
|
+
77,
|
|
62
|
+
);
|
|
63
|
+
expect(resolveFolderIdByPath).toHaveBeenCalledWith(
|
|
64
|
+
"Shared/Finance",
|
|
65
|
+
undefined,
|
|
66
|
+
);
|
|
67
|
+
expect(resolveFolderIdByKey).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("passes a resolver failure through as null", async () => {
|
|
71
|
+
vi.mocked(resolveFolderIdByKey).mockResolvedValue(null);
|
|
72
|
+
|
|
73
|
+
expect(await resolveFolderScope({ folderKey: FOLDER_KEY })).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns undefined when no folder option is passed", async () => {
|
|
77
|
+
expect(await resolveFolderScope({})).toBeUndefined();
|
|
78
|
+
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("resolveFolderId", () => {
|
|
83
|
+
const originalTTY = process.stdout.isTTY;
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
vi.resetAllMocks();
|
|
87
|
+
process.exitCode = undefined;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
process.stdout.isTTY = originalTTY;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("parses and returns the id when --folder-id is provided", async () => {
|
|
95
|
+
expect(await resolveFolderId({ folderId: "42" })).toBe(42);
|
|
96
|
+
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
97
|
+
expect(pickFolderInteractively).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("resolves --folder-path without falling back to the picker", async () => {
|
|
101
|
+
vi.mocked(resolveFolderIdByPath).mockResolvedValue(77);
|
|
102
|
+
|
|
103
|
+
expect(await resolveFolderId({ folderPath: "Shared" })).toBe(77);
|
|
104
|
+
expect(pickFolderInteractively).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("fails loud when omitted on a non-interactive terminal", async () => {
|
|
108
|
+
process.stdout.isTTY = false;
|
|
109
|
+
|
|
110
|
+
expect(await resolveFolderId({})).toBeNull();
|
|
111
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
Message: "A folder is required",
|
|
114
|
+
Instructions: expect.stringContaining("--folder-id <id>"),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
expect(process.exitCode).toBe(1);
|
|
118
|
+
expect(pickFolderInteractively).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("uses the caller's instructions when the command's folder options differ", async () => {
|
|
122
|
+
process.stdout.isTTY = false;
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
await resolveFolderId({}, "Pass the folder-id argument."),
|
|
126
|
+
).toBeNull();
|
|
127
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
Message: "A folder is required",
|
|
130
|
+
Instructions: "Pass the folder-id argument.",
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
expect(process.exitCode).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("delegates to the interactive picker when omitted on a TTY", async () => {
|
|
137
|
+
process.stdout.isTTY = true;
|
|
138
|
+
vi.mocked(pickFolderInteractively).mockResolvedValue(7);
|
|
139
|
+
|
|
140
|
+
expect(await resolveFolderId({})).toBe(7);
|
|
141
|
+
expect(pickFolderInteractively).toHaveBeenCalledOnce();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("passes the tenant to the interactive picker", async () => {
|
|
145
|
+
process.stdout.isTTY = true;
|
|
146
|
+
vi.mocked(pickFolderInteractively).mockResolvedValue(7);
|
|
147
|
+
|
|
148
|
+
expect(await resolveFolderId({ tenant: "my-tenant" })).toBe(7);
|
|
149
|
+
expect(pickFolderInteractively).toHaveBeenCalledWith("my-tenant");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does not fall back to the picker when resolution fails", async () => {
|
|
153
|
+
process.stdout.isTTY = true;
|
|
154
|
+
vi.mocked(resolveFolderIdByKey).mockResolvedValue(null);
|
|
155
|
+
|
|
156
|
+
expect(await resolveFolderId({ folderKey: FOLDER_KEY })).toBeNull();
|
|
157
|
+
expect(pickFolderInteractively).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|