@supercycle/cli 0.1.0
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/README.md +107 -0
- package/dist/cli.js +9 -0
- package/dist/commands/comments/delete.js +48 -0
- package/dist/commands/comments/index.js +3 -0
- package/dist/commands/conditions/index.js +3 -0
- package/dist/commands/conditions/list.js +20 -0
- package/dist/commands/custom-fields/definition.js +58 -0
- package/dist/commands/custom-fields/definitions.js +25 -0
- package/dist/commands/custom-fields/delete.js +45 -0
- package/dist/commands/custom-fields/index.js +3 -0
- package/dist/commands/custom-fields/set.js +43 -0
- package/dist/commands/custom-fields/update.js +19 -0
- package/dist/commands/cycles/comment.js +29 -0
- package/dist/commands/cycles/fulfil.js +24 -0
- package/dist/commands/cycles/get.js +50 -0
- package/dist/commands/cycles/index.js +3 -0
- package/dist/commands/cycles/list.js +124 -0
- package/dist/commands/cycles/overdue.js +25 -0
- package/dist/commands/cycles/pack.js +21 -0
- package/dist/commands/cycles/reassign.js +28 -0
- package/dist/commands/cycles/receive.js +23 -0
- package/dist/commands/cycles/reschedule.js +74 -0
- package/dist/commands/cycles/return.js +52 -0
- package/dist/commands/cycles/tag.js +45 -0
- package/dist/commands/cycles/to-fulfill.js +9 -0
- package/dist/commands/cycles/to-receive.js +9 -0
- package/dist/commands/cycles/today.js +18 -0
- package/dist/commands/items/availability.js +75 -0
- package/dist/commands/items/comment.js +29 -0
- package/dist/commands/items/create.js +57 -0
- package/dist/commands/items/get.js +50 -0
- package/dist/commands/items/index.js +3 -0
- package/dist/commands/items/list.js +25 -0
- package/dist/commands/items/update.js +60 -0
- package/dist/commands/locations/index.js +3 -0
- package/dist/commands/locations/list.js +19 -0
- package/dist/commands/login.js +76 -0
- package/dist/commands/products/import.js +65 -0
- package/dist/commands/products/index.js +3 -0
- package/dist/commands/products/list.js +19 -0
- package/dist/commands/returns/comment.js +29 -0
- package/dist/commands/returns/get.js +50 -0
- package/dist/commands/returns/index.js +3 -0
- package/dist/commands/returns/list.js +27 -0
- package/dist/commands/returns/update.js +68 -0
- package/dist/components/CommentResultView.js +40 -0
- package/dist/components/CustomFieldResultView.js +46 -0
- package/dist/components/CycleActionView.js +36 -0
- package/dist/components/CycleDetail.js +60 -0
- package/dist/components/CycleTable.js +68 -0
- package/dist/components/CyclesListView.js +30 -0
- package/dist/components/ErrorMessage.js +13 -0
- package/dist/components/ExitError.js +13 -0
- package/dist/components/ItemActionView.js +36 -0
- package/dist/components/ItemDetail.js +48 -0
- package/dist/components/ItemTable.js +58 -0
- package/dist/components/ItemsListView.js +30 -0
- package/dist/components/ItemsResultView.js +37 -0
- package/dist/components/JsonOutput.js +12 -0
- package/dist/components/Loading.js +11 -0
- package/dist/components/ProductTable.js +49 -0
- package/dist/components/ProductsListView.js +30 -0
- package/dist/components/ReferenceListView.js +49 -0
- package/dist/components/ReturnActionView.js +36 -0
- package/dist/components/ReturnDetail.js +55 -0
- package/dist/components/ReturnResultView.js +49 -0
- package/dist/components/ReturnTable.js +58 -0
- package/dist/components/ReturnsListView.js +30 -0
- package/dist/components/Status.js +33 -0
- package/dist/hooks/useAsync.js +32 -0
- package/dist/lib/client.js +302 -0
- package/dist/lib/config.js +38 -0
- package/dist/lib/format.js +49 -0
- package/dist/lib/listViewOptions.js +39 -0
- package/dist/lib/resolveClient.js +11 -0
- package/dist/lib/types.js +3 -0
- package/package.json +44 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Box, Text, useApp } from "ink";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
3
|
+
import { option } from "pastel";
|
|
4
|
+
import React, { useState, useEffect } from "react";
|
|
5
|
+
import zod from "zod";
|
|
6
|
+
import ErrorMessage from "../components/ErrorMessage.js";
|
|
7
|
+
import Loading from "../components/Loading.js";
|
|
8
|
+
import { SupercycleClient } from "../lib/client.js";
|
|
9
|
+
import { saveConfig, getBaseUrl, configPath } from "../lib/config.js";
|
|
10
|
+
export const description = "Store and validate your Supercycle API key";
|
|
11
|
+
export const options = zod.object({
|
|
12
|
+
key: zod
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe(option({
|
|
16
|
+
description: "API key (skips the interactive prompt)",
|
|
17
|
+
})),
|
|
18
|
+
});
|
|
19
|
+
export default function Login({ options }) {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const [value, setValue] = useState("");
|
|
22
|
+
const [phase, setPhase] = useState(options.key ? { name: "validating" } : { name: "prompt" });
|
|
23
|
+
async function validateAndSave(key) {
|
|
24
|
+
const trimmed = key.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
setPhase({ name: "error", message: "No API key provided." });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setPhase({ name: "validating" });
|
|
30
|
+
const client = new SupercycleClient({
|
|
31
|
+
token: trimmed,
|
|
32
|
+
baseUrl: getBaseUrl(),
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
await client.listCycles({ limit: 1 });
|
|
36
|
+
saveConfig({ apiKey: trimmed, baseUrl: getBaseUrl() });
|
|
37
|
+
setPhase({ name: "done" });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
setPhase({
|
|
41
|
+
name: "error",
|
|
42
|
+
message: error instanceof Error ? error.message : String(error),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// If a key was passed as a flag, validate it immediately.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (options.key) {
|
|
49
|
+
void validateAndSave(options.key);
|
|
50
|
+
}
|
|
51
|
+
}, [options]);
|
|
52
|
+
// Exit (with the right code) once we reach a terminal phase.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (phase.name === "done") {
|
|
55
|
+
exit();
|
|
56
|
+
}
|
|
57
|
+
else if (phase.name === "error") {
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
exit();
|
|
60
|
+
}
|
|
61
|
+
}, [phase, exit]);
|
|
62
|
+
if (phase.name === "prompt") {
|
|
63
|
+
return (React.createElement(Box, null,
|
|
64
|
+
React.createElement(Text, null, "Enter your Supercycle API key: "),
|
|
65
|
+
React.createElement(TextInput, { value: value, onChange: setValue, onSubmit: validateAndSave, mask: "*" })));
|
|
66
|
+
}
|
|
67
|
+
if (phase.name === "validating") {
|
|
68
|
+
return React.createElement(Loading, { label: "Validating API key\u2026" });
|
|
69
|
+
}
|
|
70
|
+
if (phase.name === "error") {
|
|
71
|
+
return React.createElement(ErrorMessage, { message: phase.message });
|
|
72
|
+
}
|
|
73
|
+
return React.createElement(Text, { color: "green" },
|
|
74
|
+
"\u2714 API key saved to ",
|
|
75
|
+
configPath());
|
|
76
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { option } from "pastel";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import zod from "zod";
|
|
5
|
+
import ErrorMessage from "../../components/ErrorMessage.js";
|
|
6
|
+
import ExitError from "../../components/ExitError.js";
|
|
7
|
+
import JsonOutput from "../../components/JsonOutput.js";
|
|
8
|
+
import Loading from "../../components/Loading.js";
|
|
9
|
+
import { useAsync } from "../../hooks/useAsync.js";
|
|
10
|
+
import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
|
|
11
|
+
export const description = "Import Shopify products into Supercycle by ID";
|
|
12
|
+
export const options = zod.object({
|
|
13
|
+
ids: zod
|
|
14
|
+
.string()
|
|
15
|
+
.describe(option({ description: "Comma-separated Shopify product IDs to import (required)" })),
|
|
16
|
+
json: zod
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe(option({ description: "Output raw JSON" })),
|
|
20
|
+
});
|
|
21
|
+
function parseIds(raw) {
|
|
22
|
+
const ids = [];
|
|
23
|
+
for (const part of raw.split(",")) {
|
|
24
|
+
const trimmed = part.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
continue;
|
|
27
|
+
const id = Number(trimmed);
|
|
28
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
29
|
+
return null;
|
|
30
|
+
ids.push(id);
|
|
31
|
+
}
|
|
32
|
+
return ids.length > 0 ? ids : null;
|
|
33
|
+
}
|
|
34
|
+
export default function Import({ options }) {
|
|
35
|
+
const ids = parseIds(options.ids);
|
|
36
|
+
const resolved = resolveClient();
|
|
37
|
+
const state = useAsync(async () => {
|
|
38
|
+
if (!ids)
|
|
39
|
+
throw new Error("Provide one or more numeric product IDs via --ids.");
|
|
40
|
+
if (!resolved.client)
|
|
41
|
+
throw new Error(resolved.error);
|
|
42
|
+
return resolved.client.importProducts(ids);
|
|
43
|
+
}, []);
|
|
44
|
+
if (state.loading) {
|
|
45
|
+
return options.json ? null : React.createElement(Loading, { label: "Importing products\u2026" });
|
|
46
|
+
}
|
|
47
|
+
if (state.error) {
|
|
48
|
+
if (options.json)
|
|
49
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
50
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
51
|
+
}
|
|
52
|
+
if (options.json) {
|
|
53
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
54
|
+
}
|
|
55
|
+
const imported = state.data.productIds ?? [];
|
|
56
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
57
|
+
React.createElement(Text, { color: "green" },
|
|
58
|
+
"\u2714 Imported ",
|
|
59
|
+
imported.length,
|
|
60
|
+
" product",
|
|
61
|
+
imported.length === 1 ? "" : "s"),
|
|
62
|
+
imported.length > 0 ? React.createElement(Text, { dimColor: true },
|
|
63
|
+
"IDs: ",
|
|
64
|
+
imported.join(", ")) : null));
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import zod from "zod";
|
|
3
|
+
import ProductsListView from "../../components/ProductsListView.js";
|
|
4
|
+
import { listViewShape } from "../../lib/listViewOptions.js";
|
|
5
|
+
export const description = "List products, optionally filtered by search";
|
|
6
|
+
export const options = zod.object({
|
|
7
|
+
...listViewShape(),
|
|
8
|
+
});
|
|
9
|
+
function buildFilters(options) {
|
|
10
|
+
const view = options;
|
|
11
|
+
return {
|
|
12
|
+
search: view.search,
|
|
13
|
+
limit: view.limit,
|
|
14
|
+
page: view.page,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export default function List({ options }) {
|
|
18
|
+
return React.createElement(ProductsListView, { filters: buildFilters(options), all: options.all, json: options.json });
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { argument, option } from "pastel";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import zod from "zod";
|
|
4
|
+
import CommentResultView from "../../components/CommentResultView.js";
|
|
5
|
+
import ErrorMessage from "../../components/ErrorMessage.js";
|
|
6
|
+
export const description = "Add a comment to a return's timeline";
|
|
7
|
+
export const args = zod.tuple([
|
|
8
|
+
zod.string().describe(argument({ name: "id", description: "Numeric return ID" })),
|
|
9
|
+
zod.string().describe(argument({ name: "message", description: "Comment text" })),
|
|
10
|
+
]);
|
|
11
|
+
export const options = zod.object({
|
|
12
|
+
json: zod
|
|
13
|
+
.boolean()
|
|
14
|
+
.default(false)
|
|
15
|
+
.describe(option({ description: "Output raw JSON" })),
|
|
16
|
+
});
|
|
17
|
+
export default function Comment({ args, options }) {
|
|
18
|
+
const [id, message] = args;
|
|
19
|
+
if (!message.trim()) {
|
|
20
|
+
return React.createElement(ErrorMessage, { message: "Comment message cannot be empty." });
|
|
21
|
+
}
|
|
22
|
+
return (React.createElement(CommentResultView, { loadingLabel: `Adding comment to return ${id}…`, action: (client) => client.createTimelineComment({
|
|
23
|
+
timelineEvent: {
|
|
24
|
+
eventableId: Number(id),
|
|
25
|
+
eventableType: "ReturnOrder",
|
|
26
|
+
message,
|
|
27
|
+
},
|
|
28
|
+
}), json: options.json }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { argument, option } from "pastel";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import zod from "zod";
|
|
4
|
+
import ErrorMessage from "../../components/ErrorMessage.js";
|
|
5
|
+
import ExitError from "../../components/ExitError.js";
|
|
6
|
+
import JsonOutput from "../../components/JsonOutput.js";
|
|
7
|
+
import Loading from "../../components/Loading.js";
|
|
8
|
+
import ReturnDetail from "../../components/ReturnDetail.js";
|
|
9
|
+
import { useAsync } from "../../hooks/useAsync.js";
|
|
10
|
+
import { resolveClient, NO_TOKEN_HINT } from "../../lib/resolveClient.js";
|
|
11
|
+
export const description = "Retrieve a single return by ID";
|
|
12
|
+
export const args = zod.tuple([
|
|
13
|
+
zod.string().describe(argument({
|
|
14
|
+
name: "id",
|
|
15
|
+
description: "Numeric ID of the return",
|
|
16
|
+
})),
|
|
17
|
+
]);
|
|
18
|
+
export const options = zod.object({
|
|
19
|
+
include: zod
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(option({
|
|
23
|
+
description: "Relations to include, e.g. timelineEvents",
|
|
24
|
+
})),
|
|
25
|
+
json: zod
|
|
26
|
+
.boolean()
|
|
27
|
+
.default(false)
|
|
28
|
+
.describe(option({ description: "Output raw JSON" })),
|
|
29
|
+
});
|
|
30
|
+
export default function Get({ args, options }) {
|
|
31
|
+
const [id] = args;
|
|
32
|
+
const resolved = resolveClient();
|
|
33
|
+
const state = useAsync(async () => {
|
|
34
|
+
if (!resolved.client)
|
|
35
|
+
throw new Error(resolved.error);
|
|
36
|
+
return resolved.client.getReturn(id, { include: options.include });
|
|
37
|
+
}, []);
|
|
38
|
+
if (state.loading) {
|
|
39
|
+
return options.json ? null : React.createElement(Loading, { label: `Fetching return ${id}…` });
|
|
40
|
+
}
|
|
41
|
+
if (state.error) {
|
|
42
|
+
if (options.json)
|
|
43
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
44
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
45
|
+
}
|
|
46
|
+
if (options.json) {
|
|
47
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
48
|
+
}
|
|
49
|
+
return React.createElement(ReturnDetail, { order: state.data });
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { option } from "pastel";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import zod from "zod";
|
|
4
|
+
import ReturnsListView from "../../components/ReturnsListView.js";
|
|
5
|
+
import { listViewShape } from "../../lib/listViewOptions.js";
|
|
6
|
+
export const description = "List returns, optionally filtered";
|
|
7
|
+
export const options = zod.object({
|
|
8
|
+
...listViewShape(),
|
|
9
|
+
status: zod
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(option({
|
|
13
|
+
description: "Return status (comma-separated): requested,expected,received,in_progress,completed,cancelled",
|
|
14
|
+
})),
|
|
15
|
+
});
|
|
16
|
+
function buildFilters(options) {
|
|
17
|
+
const view = options;
|
|
18
|
+
return {
|
|
19
|
+
search: view.search,
|
|
20
|
+
limit: view.limit,
|
|
21
|
+
page: view.page,
|
|
22
|
+
status: options.status,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export default function List({ options }) {
|
|
26
|
+
return React.createElement(ReturnsListView, { filters: buildFilters(options), all: options.all, json: options.json });
|
|
27
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { argument, option } from "pastel";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import zod from "zod";
|
|
4
|
+
import ErrorMessage from "../../components/ErrorMessage.js";
|
|
5
|
+
import ReturnActionView from "../../components/ReturnActionView.js";
|
|
6
|
+
export const description = "Update a return's status and/or line statuses";
|
|
7
|
+
const lineStatus = ["awaiting", "received", "missing"];
|
|
8
|
+
export const args = zod.tuple([
|
|
9
|
+
zod.string().describe(argument({ name: "id", description: "Numeric return ID" })),
|
|
10
|
+
]);
|
|
11
|
+
export const options = zod.object({
|
|
12
|
+
status: zod
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe(option({
|
|
16
|
+
description: "New return status: requested|expected|received|in_progress|completed|cancelled",
|
|
17
|
+
})),
|
|
18
|
+
line: zod
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe(option({
|
|
22
|
+
description: 'Set line statuses as "lineId:status" pairs, comma-separated (status: awaiting|received|missing). Line IDs come from `returns get <id>`.',
|
|
23
|
+
})),
|
|
24
|
+
json: zod
|
|
25
|
+
.boolean()
|
|
26
|
+
.default(false)
|
|
27
|
+
.describe(option({ description: "Output raw JSON" })),
|
|
28
|
+
});
|
|
29
|
+
// Parse a "12:received,13:missing" string into returnLines entries.
|
|
30
|
+
function parseLines(raw) {
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const part of raw.split(",")) {
|
|
33
|
+
const trimmed = part.trim();
|
|
34
|
+
if (!trimmed)
|
|
35
|
+
continue;
|
|
36
|
+
const [idPart, statusPart] = trimmed.split(":");
|
|
37
|
+
const id = Number(idPart);
|
|
38
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
39
|
+
return { ok: false, error: `Invalid line ID in "${trimmed}". Expected "lineId:status".` };
|
|
40
|
+
}
|
|
41
|
+
if (!statusPart || !lineStatus.includes(statusPart)) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: `Invalid status in "${trimmed}". Expected one of: ${lineStatus.join(", ")}.`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
lines.push({ id, status: statusPart });
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, lines };
|
|
50
|
+
}
|
|
51
|
+
export default function Update({ args, options }) {
|
|
52
|
+
const [id] = args;
|
|
53
|
+
if (!options.status && !options.line) {
|
|
54
|
+
return React.createElement(ErrorMessage, { message: "Nothing to update. Pass --status and/or --line." });
|
|
55
|
+
}
|
|
56
|
+
const body = {};
|
|
57
|
+
if (options.status)
|
|
58
|
+
body.status = options.status;
|
|
59
|
+
if (options.line) {
|
|
60
|
+
const parsed = parseLines(options.line);
|
|
61
|
+
if (!parsed.ok) {
|
|
62
|
+
return React.createElement(ErrorMessage, { message: parsed.error });
|
|
63
|
+
}
|
|
64
|
+
if (parsed.lines.length > 0)
|
|
65
|
+
body.returnLines = parsed.lines;
|
|
66
|
+
}
|
|
67
|
+
return (React.createElement(ReturnActionView, { loadingLabel: `Updating return ${id}…`, action: (client) => client.updateReturn(id, body), successLabel: (o) => `Return #${o.sequentialId ?? o.id} updated`, json: options.json }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useAsync } from "../hooks/useAsync.js";
|
|
4
|
+
import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
|
|
5
|
+
import ErrorMessage from "./ErrorMessage.js";
|
|
6
|
+
import ExitError from "./ExitError.js";
|
|
7
|
+
import JsonOutput from "./JsonOutput.js";
|
|
8
|
+
import Loading from "./Loading.js";
|
|
9
|
+
// Runs a create-timeline-comment action and reports the new comment.
|
|
10
|
+
export default function CommentResultView({ action, loadingLabel = "Adding comment…", json, }) {
|
|
11
|
+
const resolved = resolveClient();
|
|
12
|
+
const state = useAsync(async () => {
|
|
13
|
+
if (!resolved.client)
|
|
14
|
+
throw new Error(resolved.error);
|
|
15
|
+
return action(resolved.client);
|
|
16
|
+
}, []);
|
|
17
|
+
if (state.loading) {
|
|
18
|
+
return json ? null : React.createElement(Loading, { label: loadingLabel });
|
|
19
|
+
}
|
|
20
|
+
if (state.error) {
|
|
21
|
+
if (json)
|
|
22
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
23
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
24
|
+
}
|
|
25
|
+
if (json) {
|
|
26
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
27
|
+
}
|
|
28
|
+
const comment = state.data;
|
|
29
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
30
|
+
React.createElement(Text, { color: "green" },
|
|
31
|
+
"\u2714 Comment added to ",
|
|
32
|
+
comment.eventableType,
|
|
33
|
+
" ",
|
|
34
|
+
comment.eventableId,
|
|
35
|
+
" (comment ID ",
|
|
36
|
+
comment.id,
|
|
37
|
+
")"),
|
|
38
|
+
React.createElement(Box, { marginTop: 1 },
|
|
39
|
+
React.createElement(Text, { dimColor: true }, comment.message))));
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useAsync } from "../hooks/useAsync.js";
|
|
4
|
+
import { dash } from "../lib/format.js";
|
|
5
|
+
import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
|
|
6
|
+
import ErrorMessage from "./ErrorMessage.js";
|
|
7
|
+
import ExitError from "./ExitError.js";
|
|
8
|
+
import JsonOutput from "./JsonOutput.js";
|
|
9
|
+
import Loading from "./Loading.js";
|
|
10
|
+
// Runs a custom-field create/update action, then shows a confirmation line plus
|
|
11
|
+
// the resulting field's key/value/owner.
|
|
12
|
+
export default function CustomFieldResultView({ action, successLabel, loadingLabel = "Saving custom field…", json, }) {
|
|
13
|
+
const resolved = resolveClient();
|
|
14
|
+
const state = useAsync(async () => {
|
|
15
|
+
if (!resolved.client)
|
|
16
|
+
throw new Error(resolved.error);
|
|
17
|
+
return action(resolved.client);
|
|
18
|
+
}, []);
|
|
19
|
+
if (state.loading) {
|
|
20
|
+
return json ? null : React.createElement(Loading, { label: loadingLabel });
|
|
21
|
+
}
|
|
22
|
+
if (state.error) {
|
|
23
|
+
if (json)
|
|
24
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
25
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
26
|
+
}
|
|
27
|
+
if (json) {
|
|
28
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
29
|
+
}
|
|
30
|
+
const field = state.data;
|
|
31
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
32
|
+
React.createElement(Text, { color: "green" },
|
|
33
|
+
"\u2714 ",
|
|
34
|
+
successLabel(field)),
|
|
35
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
36
|
+
React.createElement(Text, { dimColor: true },
|
|
37
|
+
dash(field.key),
|
|
38
|
+
" = ",
|
|
39
|
+
dash(field.value)),
|
|
40
|
+
React.createElement(Text, { dimColor: true },
|
|
41
|
+
field.ownerType,
|
|
42
|
+
" ",
|
|
43
|
+
field.ownerId,
|
|
44
|
+
" \u00B7 field ID ",
|
|
45
|
+
field.id))));
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useAsync } from "../hooks/useAsync.js";
|
|
4
|
+
import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
|
|
5
|
+
import CycleDetail from "./CycleDetail.js";
|
|
6
|
+
import ErrorMessage from "./ErrorMessage.js";
|
|
7
|
+
import ExitError from "./ExitError.js";
|
|
8
|
+
import JsonOutput from "./JsonOutput.js";
|
|
9
|
+
import Loading from "./Loading.js";
|
|
10
|
+
// Runs a single cycle-mutating action, then shows a confirmation line plus the
|
|
11
|
+
// updated cycle (or raw JSON). Used by fulfil/receive/pack/reschedule/etc.
|
|
12
|
+
export default function CycleActionView({ action, successLabel, loadingLabel = "Updating cycle…", json, }) {
|
|
13
|
+
const resolved = resolveClient();
|
|
14
|
+
const state = useAsync(async () => {
|
|
15
|
+
if (!resolved.client)
|
|
16
|
+
throw new Error(resolved.error);
|
|
17
|
+
return action(resolved.client);
|
|
18
|
+
}, []);
|
|
19
|
+
if (state.loading) {
|
|
20
|
+
return json ? null : React.createElement(Loading, { label: loadingLabel });
|
|
21
|
+
}
|
|
22
|
+
if (state.error) {
|
|
23
|
+
if (json)
|
|
24
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
25
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
26
|
+
}
|
|
27
|
+
if (json) {
|
|
28
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
29
|
+
}
|
|
30
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
31
|
+
React.createElement(Text, { color: "green" },
|
|
32
|
+
"\u2714 ",
|
|
33
|
+
successLabel(state.data)),
|
|
34
|
+
React.createElement(Box, { marginTop: 1 },
|
|
35
|
+
React.createElement(CycleDetail, { cycle: state.data }))));
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { customerName, formatDateTime, titleCase, dash, EMPTY } from "../lib/format.js";
|
|
4
|
+
import Status from "./Status.js";
|
|
5
|
+
function Section({ title, children }) {
|
|
6
|
+
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
7
|
+
React.createElement(Text, { bold: true, underline: true }, title),
|
|
8
|
+
children));
|
|
9
|
+
}
|
|
10
|
+
function Field({ label, children }) {
|
|
11
|
+
return (React.createElement(Box, null,
|
|
12
|
+
React.createElement(Box, { width: 18 },
|
|
13
|
+
React.createElement(Text, { dimColor: true }, label)),
|
|
14
|
+
React.createElement(Box, null, typeof children === "string" ? React.createElement(Text, null, children) : children)));
|
|
15
|
+
}
|
|
16
|
+
export default function CycleDetail({ cycle }) {
|
|
17
|
+
const { item, timelineEvents, tags, customFields } = cycle;
|
|
18
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
19
|
+
React.createElement(Section, { title: `Cycle #${cycle.sequentialId ?? cycle.id}` },
|
|
20
|
+
React.createElement(Field, { label: "ID" }, String(cycle.id)),
|
|
21
|
+
React.createElement(Field, { label: "Order" }, dash(cycle.shopifyOrderName)),
|
|
22
|
+
React.createElement(Field, { label: "Method" }, titleCase(cycle.methodType)),
|
|
23
|
+
React.createElement(Field, { label: "Status" },
|
|
24
|
+
React.createElement(Status, { value: cycle.status })),
|
|
25
|
+
React.createElement(Field, { label: "Fulfillment" },
|
|
26
|
+
React.createElement(Status, { value: cycle.fulfillmentStatus })),
|
|
27
|
+
React.createElement(Field, { label: "Receival" },
|
|
28
|
+
React.createElement(Status, { value: cycle.receivalStatus })),
|
|
29
|
+
React.createElement(Field, { label: "Packing" },
|
|
30
|
+
React.createElement(Status, { value: cycle.packingStatus }))),
|
|
31
|
+
React.createElement(Section, { title: "Customer" },
|
|
32
|
+
React.createElement(Field, { label: "Name" }, customerName(cycle.customer)),
|
|
33
|
+
React.createElement(Field, { label: "Email" }, dash(cycle.customer?.email))),
|
|
34
|
+
item ? (React.createElement(Section, { title: "Item" },
|
|
35
|
+
React.createElement(Field, { label: "Product" }, dash(item.productTitle)),
|
|
36
|
+
React.createElement(Field, { label: "Variant" }, dash(item.variantTitle)),
|
|
37
|
+
React.createElement(Field, { label: "Serial" }, dash(item.serial)),
|
|
38
|
+
React.createElement(Field, { label: "Visibility" },
|
|
39
|
+
React.createElement(Status, { value: item.visibility })),
|
|
40
|
+
React.createElement(Field, { label: "Pick location" }, dash(item.pickLocation)))) : null,
|
|
41
|
+
React.createElement(Section, { title: "Dates" },
|
|
42
|
+
React.createElement(Field, { label: "Fulfill due" }, formatDateTime(cycle.fulfillAt)),
|
|
43
|
+
React.createElement(Field, { label: "Fulfilled" }, formatDateTime(cycle.fulfilledAt)),
|
|
44
|
+
React.createElement(Field, { label: "Rental start" }, formatDateTime(cycle.rentalStart)),
|
|
45
|
+
React.createElement(Field, { label: "Rental end" }, formatDateTime(cycle.rentalEnd)),
|
|
46
|
+
React.createElement(Field, { label: "Receive due" }, formatDateTime(cycle.receiveAt)),
|
|
47
|
+
React.createElement(Field, { label: "Received" }, formatDateTime(cycle.receivedAt)),
|
|
48
|
+
React.createElement(Field, { label: "Restock by" }, formatDateTime(cycle.restockBy)),
|
|
49
|
+
React.createElement(Field, { label: "Created" }, formatDateTime(cycle.createdAt)),
|
|
50
|
+
React.createElement(Field, { label: "Updated" }, formatDateTime(cycle.updatedAt))),
|
|
51
|
+
tags && tags.length > 0 ? (React.createElement(Section, { title: "Tags" },
|
|
52
|
+
React.createElement(Text, null, tags.join(", ")))) : null,
|
|
53
|
+
customFields && customFields.length > 0 ? (React.createElement(Section, { title: "Custom fields" }, customFields.map((field) => (React.createElement(Field, { key: field.id, label: field.key }, dash(field.value)))))) : null,
|
|
54
|
+
timelineEvents && timelineEvents.length > 0 ? (React.createElement(Section, { title: "Timeline" }, timelineEvents.map((event) => (React.createElement(Box, { key: event.id },
|
|
55
|
+
React.createElement(Box, { width: 20 },
|
|
56
|
+
React.createElement(Text, { dimColor: true, wrap: "truncate" }, formatDateTime(event.createdAt))),
|
|
57
|
+
React.createElement(Box, { width: 28 },
|
|
58
|
+
React.createElement(Text, { wrap: "truncate" }, titleCase(event.eventType))),
|
|
59
|
+
React.createElement(Text, { dimColor: true }, event.author ?? EMPTY)))))) : null));
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { customerName, formatDate, titleCase, dash } from "../lib/format.js";
|
|
4
|
+
import Status from "./Status.js";
|
|
5
|
+
const COLUMNS = {
|
|
6
|
+
id: 10,
|
|
7
|
+
cycle: 7,
|
|
8
|
+
order: 10,
|
|
9
|
+
customer: 18,
|
|
10
|
+
method: 12,
|
|
11
|
+
status: 11,
|
|
12
|
+
fulfillment: 12,
|
|
13
|
+
receival: 10,
|
|
14
|
+
start: 12,
|
|
15
|
+
};
|
|
16
|
+
// `noShrink` keeps a column at full width even on narrow terminals — used for
|
|
17
|
+
// the identifier columns so they stay readable for follow-up commands.
|
|
18
|
+
function Cell({ width, noShrink, children, }) {
|
|
19
|
+
return (React.createElement(Box, { width: width, marginRight: 1, flexShrink: noShrink ? 0 : 1 }, children));
|
|
20
|
+
}
|
|
21
|
+
function HeaderCell({ width, label, noShrink, }) {
|
|
22
|
+
return (React.createElement(Cell, { width: width, noShrink: noShrink },
|
|
23
|
+
React.createElement(Text, { bold: true, dimColor: true, wrap: "truncate" }, label)));
|
|
24
|
+
}
|
|
25
|
+
export default function CycleTable({ cycles, nextPage }) {
|
|
26
|
+
if (cycles.length === 0) {
|
|
27
|
+
return React.createElement(Text, { dimColor: true }, "No cycles matched your filters.");
|
|
28
|
+
}
|
|
29
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
30
|
+
React.createElement(Box, null,
|
|
31
|
+
React.createElement(HeaderCell, { width: COLUMNS.id, label: "ID", noShrink: true }),
|
|
32
|
+
React.createElement(HeaderCell, { width: COLUMNS.cycle, label: "Cycle #", noShrink: true }),
|
|
33
|
+
React.createElement(HeaderCell, { width: COLUMNS.order, label: "Order" }),
|
|
34
|
+
React.createElement(HeaderCell, { width: COLUMNS.customer, label: "Customer" }),
|
|
35
|
+
React.createElement(HeaderCell, { width: COLUMNS.method, label: "Method" }),
|
|
36
|
+
React.createElement(HeaderCell, { width: COLUMNS.status, label: "Status" }),
|
|
37
|
+
React.createElement(HeaderCell, { width: COLUMNS.fulfillment, label: "Fulfillment" }),
|
|
38
|
+
React.createElement(HeaderCell, { width: COLUMNS.receival, label: "Receival" }),
|
|
39
|
+
React.createElement(HeaderCell, { width: COLUMNS.start, label: "Rental start" })),
|
|
40
|
+
cycles.map((cycle) => (React.createElement(Box, { key: cycle.id },
|
|
41
|
+
React.createElement(Cell, { width: COLUMNS.id, noShrink: true },
|
|
42
|
+
React.createElement(Text, { wrap: "truncate" }, cycle.id)),
|
|
43
|
+
React.createElement(Cell, { width: COLUMNS.cycle, noShrink: true },
|
|
44
|
+
React.createElement(Text, { dimColor: true, wrap: "truncate" }, cycle.sequentialId == null ? "—" : `#${cycle.sequentialId}`)),
|
|
45
|
+
React.createElement(Cell, { width: COLUMNS.order },
|
|
46
|
+
React.createElement(Text, { wrap: "truncate" }, dash(cycle.shopifyOrderName))),
|
|
47
|
+
React.createElement(Cell, { width: COLUMNS.customer },
|
|
48
|
+
React.createElement(Text, { wrap: "truncate" }, customerName(cycle.customer))),
|
|
49
|
+
React.createElement(Cell, { width: COLUMNS.method },
|
|
50
|
+
React.createElement(Text, { wrap: "truncate" }, titleCase(cycle.methodType))),
|
|
51
|
+
React.createElement(Cell, { width: COLUMNS.status },
|
|
52
|
+
React.createElement(Status, { value: cycle.status })),
|
|
53
|
+
React.createElement(Cell, { width: COLUMNS.fulfillment },
|
|
54
|
+
React.createElement(Status, { value: cycle.fulfillmentStatus })),
|
|
55
|
+
React.createElement(Cell, { width: COLUMNS.receival },
|
|
56
|
+
React.createElement(Status, { value: cycle.receivalStatus })),
|
|
57
|
+
React.createElement(Cell, { width: COLUMNS.start },
|
|
58
|
+
React.createElement(Text, { wrap: "truncate" }, formatDate(cycle.rentalStart)))))),
|
|
59
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
60
|
+
React.createElement(Text, { dimColor: true },
|
|
61
|
+
cycles.length,
|
|
62
|
+
" cycle",
|
|
63
|
+
cycles.length === 1 ? "" : "s",
|
|
64
|
+
" shown \u00B7 use the ID column with `cycles get <id>`"),
|
|
65
|
+
nextPage ? React.createElement(Text, { dimColor: true },
|
|
66
|
+
"Next page: --page ",
|
|
67
|
+
nextPage) : null)));
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useAsync } from "../hooks/useAsync.js";
|
|
3
|
+
import { resolveClient, NO_TOKEN_HINT } from "../lib/resolveClient.js";
|
|
4
|
+
import CycleTable from "./CycleTable.js";
|
|
5
|
+
import ErrorMessage from "./ErrorMessage.js";
|
|
6
|
+
import ExitError from "./ExitError.js";
|
|
7
|
+
import JsonOutput from "./JsonOutput.js";
|
|
8
|
+
import Loading from "./Loading.js";
|
|
9
|
+
// Fetch a list of cycles (optionally following every page) and render it as a
|
|
10
|
+
// table or raw JSON. Shared by `list` and the read-convenience commands.
|
|
11
|
+
export default function CyclesListView({ filters, all, json }) {
|
|
12
|
+
const resolved = resolveClient();
|
|
13
|
+
const state = useAsync(async () => {
|
|
14
|
+
if (!resolved.client)
|
|
15
|
+
throw new Error(resolved.error);
|
|
16
|
+
return all ? resolved.client.listAllCycles(filters) : resolved.client.listCycles(filters);
|
|
17
|
+
}, []);
|
|
18
|
+
if (state.loading) {
|
|
19
|
+
return json ? null : React.createElement(Loading, { label: "Fetching cycles\u2026" });
|
|
20
|
+
}
|
|
21
|
+
if (state.error) {
|
|
22
|
+
if (json)
|
|
23
|
+
return React.createElement(ExitError, { message: state.error.message });
|
|
24
|
+
return (React.createElement(ErrorMessage, { message: state.error.message, hint: resolved.error ? NO_TOKEN_HINT : undefined }));
|
|
25
|
+
}
|
|
26
|
+
if (json) {
|
|
27
|
+
return React.createElement(JsonOutput, { data: state.data });
|
|
28
|
+
}
|
|
29
|
+
return React.createElement(CycleTable, { cycles: state.data.data, nextPage: state.data.nextPage });
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React, { useEffect } from "react";
|
|
3
|
+
export default function ErrorMessage({ message, hint }) {
|
|
4
|
+
// Rendering an error always means the command failed.
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
}, []);
|
|
8
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
9
|
+
React.createElement(Text, { color: "red" },
|
|
10
|
+
"\u2716 ",
|
|
11
|
+
message),
|
|
12
|
+
hint ? React.createElement(Text, { dimColor: true }, hint) : null));
|
|
13
|
+
}
|