@olenbetong/appframe-cli 4.3.1 → 4.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/cli/af-apply.js +1 -1
  3. package/cli/af-deploy.js +1 -1
  4. package/cli/editor/TransactionsEditor.js +134 -158
  5. package/cli/editor/TransactionsPreviewDialog.js +175 -104
  6. package/cli/editor/tableFormatting.js +9 -80
  7. package/cli/editor/useCommand.js +148 -0
  8. package/cli/editor/useScrollableText.js +28 -0
  9. package/cli/editor/useTransactionEdits.js +142 -0
  10. package/cli/editor/useTransactionText.js +113 -0
  11. package/cli/editor/useTransactions.js +53 -0
  12. package/cli/editor/useTransactionsSelection.js +41 -0
  13. package/cli/editor/useTransactionsTableLayout.js +54 -0
  14. package/cli/editor/{useTransactionsTableViewport.js → useVirtualScrolling.js} +62 -24
  15. package/cli/editor/useVirtualText.js +18 -0
  16. package/package.json +2 -2
  17. package/src/af-apply.ts +1 -1
  18. package/src/af-deploy.ts +1 -1
  19. package/src/editor/TransactionsEditor.tsx +155 -180
  20. package/src/editor/TransactionsPreviewDialog.tsx +196 -146
  21. package/src/editor/tableFormatting.ts +8 -94
  22. package/src/editor/useCommand.ts +191 -0
  23. package/src/editor/useScrollableText.ts +48 -0
  24. package/src/editor/useTransactionEdits.ts +183 -0
  25. package/src/editor/useTransactionText.ts +153 -0
  26. package/src/editor/useTransactions.ts +75 -0
  27. package/src/editor/useTransactionsSelection.ts +54 -0
  28. package/src/editor/useTransactionsTableLayout.ts +84 -0
  29. package/src/editor/{useTransactionsTableViewport.ts → useVirtualScrolling.ts} +87 -38
  30. package/src/editor/useVirtualText.ts +32 -0
  31. package/tsconfig.build.tsbuildinfo +1 -1
  32. package/cli/editor/useTransactionsEditorData.js +0 -206
  33. package/cli/editor/useTransactionsEditorInput.js +0 -109
  34. package/src/editor/useTransactionsEditorData.ts +0 -245
  35. package/src/editor/useTransactionsEditorInput.ts +0 -147
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8e102e3: add support for editing transactions to deploy in af deploy
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [8e102e3]
12
+ - @olenbetong/appframe-updater@0.4.0
13
+
3
14
  ## 4.3.1
4
15
 
5
16
  ### Patch Changes
package/cli/af-apply.js CHANGED
@@ -3,8 +3,8 @@ import { listTransactions, transactionStatuses } from "@olenbetong/appframe-upda
3
3
  import chalk from "chalk";
4
4
  import inquirer from "inquirer";
5
5
  import open from "open";
6
- import { Command } from "./lib/Command.js";
7
6
  import { renderTransactionsEditor } from "./editor/TransactionsEditor.js";
7
+ import { Command } from "./lib/Command.js";
8
8
  import { importJson } from "./lib/importJson.js";
9
9
  import { Server } from "./lib/Server.js";
10
10
  const isInteractive = process.stdout.isTTY;
package/cli/af-deploy.js CHANGED
@@ -40,7 +40,7 @@ async function deployTransactions(namespaceArg, options) {
40
40
  if (action === "no") {
41
41
  process.exit(0);
42
42
  }
43
- let instance = await renderTransactionsEditor({ server, namespace });
43
+ let instance = await renderTransactionsEditor({ filter: "deployEdit", server, namespace });
44
44
  await instance.waitUntilExit();
45
45
  }
46
46
  await server.deploy(namespace);
@@ -3,28 +3,24 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { Box, Text, useApp } from "ink";
4
4
  import { useCallback, useEffect, useMemo, useState } from "react";
5
5
  import { TransactionsPreviewDialog } from "./TransactionsPreviewDialog.js";
6
+ import { createStatusLine } from "./tableFormatting.js";
7
+ import { useCommand } from "./useCommand.js";
6
8
  import { useScreenSize } from "./useScreenSize.js";
7
- import { useTransactionsEditorInput } from "./useTransactionsEditorInput.js";
8
- import { useTransactionsEditorData } from "./useTransactionsEditorData.js";
9
+ import { useTransactionEdits } from "./useTransactionEdits.js";
10
+ import { useTransactions } from "./useTransactions.js";
11
+ import { useTransactionsSelection } from "./useTransactionsSelection.js";
12
+ import { useTransactionsTableLayout } from "./useTransactionsTableLayout.js";
13
+ import { CURSOR_PREFIX_WIDTH, HEADER_PREFIX, useVirtualScrolling } from "./useVirtualScrolling.js";
9
14
  import { withFullScreen } from "./withFullScreen.js";
10
- import {
11
- DEFAULT_TABLE_SEPARATOR,
12
- ERROR_COLUMN_MAX_LINES,
13
- calculateColumnWidths,
14
- createStatusLine,
15
- createTransactionColumns,
16
- formatRowLines,
17
- mapTransactionsToRows
18
- } from "./tableFormatting.js";
19
- import { HEADER_PREFIX, CURSOR_PREFIX_WIDTH, useTransactionsTableViewport } from "./useTransactionsTableViewport.js";
20
15
  const HEADER_LINES = 2;
21
16
  const BASE_FOOTER_LINES = 1;
22
17
  const STATUS_LINES = 1;
23
18
  const FRAME_BORDER_LINES = 2;
24
- export function TransactionsEditor({ server, namespace, showErrorColumn = true, onReady }) {
19
+ const SAVE_SHORTCUTS = ["ctrl+s", "meta+s"];
20
+ const EXIT_SHORTCUTS = ["ctrl+x", "meta+x"];
21
+ export function TransactionsEditor({ filter, server, namespace, showErrorColumn = true }) {
25
22
  let { width: terminalColumns, height: terminalRows } = useScreenSize();
26
23
  let { exit } = useApp();
27
- let [offset, setOffset] = useState(0);
28
24
  let [cursor, setCursor] = useState(0);
29
25
  let [actionMessage, setActionMessage] = useState(null);
30
26
  let [actionColor, setActionColor] = useState("gray");
@@ -37,45 +33,41 @@ export function TransactionsEditor({ server, namespace, showErrorColumn = true,
37
33
  transactions,
38
34
  loading,
39
35
  error,
40
- refresh,
36
+ refresh: fetchTransactions,
41
37
  normalizedTransactions,
42
- transactionRecordByPrimKey,
43
- displayTransactions,
44
- modifiedPrimKeys,
38
+ transactionMap
39
+ } = useTransactions({ server, namespace, filter });
40
+ let [lastRefreshOptions, setLastRefreshOptions] = useState({
41
+ preserveViewport: false,
42
+ anchorPrimKey: null
43
+ });
44
+ let refresh = useCallback(
45
+ async (options = {}) => {
46
+ let { preserveViewport = false, anchorPrimKey = null } = options;
47
+ setLastRefreshOptions({ preserveViewport, anchorPrimKey });
48
+ await fetchTransactions(options);
49
+ },
50
+ [fetchTransactions]
51
+ );
52
+ let { selectedPrimKeys, toggleSelection } = useTransactionsSelection(transactions);
53
+ let { pendingStatuses, modifiedPrimKeys, cycleStatus, save } = useTransactionEdits({
54
+ normalizedTransactions,
55
+ transactionRecordMap: transactionMap,
45
56
  selectedPrimKeys,
46
- setSelectedPrimKeys,
47
- cycleStatus,
48
- handleSave,
49
- lastRefreshOptions
50
- } = useTransactionsEditorData({ server, namespace, showErrorColumn, onActionMessage });
51
- useEffect(() => {
52
- onReady?.({ refresh });
53
- }, [onReady, refresh]);
54
- useEffect(() => {
55
- void refresh();
56
- }, [refresh]);
57
- useEffect(() => {
58
- let { preserveViewport = false, anchorPrimKey = null } = lastRefreshOptions;
59
- if (!preserveViewport) {
60
- setOffset(0);
61
- setCursor(0);
62
- return;
63
- }
64
- if (anchorPrimKey) {
65
- let index = transactions.findIndex((transaction) => transaction.PrimKey === anchorPrimKey);
66
- if (index >= 0) {
67
- setCursor(index);
68
- }
69
- }
70
- }, [lastRefreshOptions, transactions]);
71
- let rows = useMemo(
72
- () => mapTransactionsToRows(displayTransactions, showErrorColumn),
73
- [displayTransactions, showErrorColumn]
57
+ server,
58
+ onActionMessage,
59
+ refresh
60
+ });
61
+ let displayTransactions = useMemo(
62
+ () => normalizedTransactions.map((transaction) => ({
63
+ ...transaction,
64
+ Status: pendingStatuses[transaction.PrimKey] ?? transaction.Status
65
+ })),
66
+ [normalizedTransactions, pendingStatuses]
74
67
  );
75
- let messageLineCount = (error ? 1 : 0) + (loading ? 1 : 0);
76
68
  let instructionsLineCount = BASE_FOOTER_LINES;
77
69
  let frameHeight = Math.max(
78
- terminalRows - instructionsLineCount - messageLineCount,
70
+ terminalRows - instructionsLineCount,
79
71
  FRAME_BORDER_LINES + HEADER_LINES + STATUS_LINES + 1
80
72
  );
81
73
  let innerHeight = Math.max(frameHeight - FRAME_BORDER_LINES, 0);
@@ -83,139 +75,113 @@ export function TransactionsEditor({ server, namespace, showErrorColumn = true,
83
75
  let frameWidth = Math.max(terminalColumns, FRAME_BORDER_LINES + CURSOR_PREFIX_WIDTH + 1);
84
76
  let innerWidth = Math.max(frameWidth - FRAME_BORDER_LINES, 0);
85
77
  let tableWidth = Math.max(innerWidth - CURSOR_PREFIX_WIDTH, 0);
86
- let baseColumns = useMemo(() => createTransactionColumns(showErrorColumn), [showErrorColumn]);
87
- let columns = useMemo(
88
- () => baseColumns.map((column) => {
89
- if (!column.wrap) {
90
- return column;
91
- }
92
- let maxLines = column.maxWrapLines ?? ERROR_COLUMN_MAX_LINES;
93
- let limited = Math.max(1, Math.min(maxLines, dataAreaHeight || maxLines));
94
- if (limited === maxLines) {
95
- return column;
96
- }
97
- return { ...column, maxWrapLines: limited };
98
- }),
99
- [baseColumns, dataAreaHeight]
100
- );
101
- let widths = useMemo(
102
- () => calculateColumnWidths({
103
- columns,
104
- rows,
105
- totalWidth: tableWidth,
106
- separator: DEFAULT_TABLE_SEPARATOR
107
- }),
108
- [columns, rows, tableWidth]
109
- );
110
- let headerRow = useMemo(
111
- () => Object.fromEntries(columns.map((column) => [column.key, column.header])),
112
- [columns]
113
- );
114
- let headerLines = useMemo(
115
- () => formatRowLines({ row: headerRow, columns, widths, separator: DEFAULT_TABLE_SEPARATOR }),
116
- [columns, headerRow, widths]
117
- );
118
- let { tableContentWidth, lineWidth, bodyLines, displayedRowCount, pageStep, getHeightBetween, totalRowCount } = useTransactionsTableViewport({
119
- rows,
78
+ let { columns, rows, widths, tableContentWidth, lineWidth, header, divider } = useTransactionsTableLayout({
79
+ displayTransactions,
80
+ showErrorColumn,
81
+ tableWidth
82
+ });
83
+ let { bodyLines, pageStep, stats, setOffset } = useVirtualScrolling({
120
84
  cursor,
121
- offset,
85
+ setCursor,
122
86
  dataAreaHeight,
87
+ rows,
123
88
  columns,
124
89
  widths,
90
+ tableContentWidth,
91
+ lineWidth,
125
92
  normalizedTransactions,
126
93
  selectedPrimKeys,
127
94
  modifiedPrimKeys,
128
95
  loading
129
96
  });
130
- let header = useMemo(
131
- () => `${HEADER_PREFIX}${(headerLines[0] ?? "").padEnd(tableContentWidth, " ")}`,
132
- [headerLines, tableContentWidth]
133
- );
134
- let divider = useMemo(() => {
135
- let separator = "\u2500".repeat(DEFAULT_TABLE_SEPARATOR.length);
136
- let content = columns.map((_, index) => "\u2500".repeat(widths[index])).join(separator);
137
- return `${HEADER_PREFIX}${content.padEnd(tableContentWidth, " ")}`;
138
- }, [columns, tableContentWidth, widths]);
139
97
  useEffect(() => {
140
- setOffset((current) => {
141
- if (totalRowCount === 0) {
142
- return 0;
98
+ let { preserveViewport = false, anchorPrimKey = null } = lastRefreshOptions;
99
+ if (!preserveViewport) {
100
+ setOffset(0);
101
+ setCursor(0);
102
+ return;
103
+ }
104
+ if (anchorPrimKey) {
105
+ let index = transactions.findIndex((transaction) => transaction.PrimKey === anchorPrimKey);
106
+ if (index >= 0) {
107
+ setCursor(index);
143
108
  }
144
- let clamped = Math.max(Math.min(current, totalRowCount - 1), 0);
145
- return clamped;
146
- });
147
- }, [totalRowCount]);
148
- useEffect(() => {
109
+ }
110
+ }, [lastRefreshOptions, setOffset, transactions]);
111
+ function handlePreview() {
112
+ let focused = normalizedTransactions[cursor];
113
+ if (!focused) {
114
+ onActionMessage("No transaction focused for preview.", "yellow");
115
+ return;
116
+ }
117
+ setPreviewOpen(true);
118
+ }
119
+ let totalRowCount = stats.totalRowCount;
120
+ function focusPrevious() {
121
+ setCursor((current) => totalRowCount === 0 ? 0 : Math.max(current - 1, 0));
122
+ }
123
+ function focusNext() {
149
124
  setCursor((current) => {
150
125
  if (totalRowCount === 0) {
151
126
  return 0;
152
127
  }
153
- return Math.min(current, totalRowCount - 1);
128
+ return Math.min(current + 1, totalRowCount - 1);
154
129
  });
155
- }, [totalRowCount]);
156
- useEffect(() => {
157
- if (dataAreaHeight <= 0) {
158
- return;
159
- }
160
- setOffset((current) => {
130
+ }
131
+ function pageUpCursor() {
132
+ setCursor((current) => Math.max(current - pageStep, 0));
133
+ }
134
+ function pageDownCursor() {
135
+ setCursor((current) => {
161
136
  if (totalRowCount === 0) {
162
137
  return 0;
163
138
  }
164
- let next = Math.max(Math.min(current, totalRowCount - 1), 0);
165
- if (cursor < next) {
166
- return cursor;
167
- }
168
- let height = getHeightBetween(next, cursor);
169
- while (height > dataAreaHeight && next < cursor) {
170
- next += 1;
171
- height = getHeightBetween(next, cursor);
172
- }
173
- return next;
139
+ return Math.min(current + pageStep, totalRowCount - 1);
174
140
  });
175
- }, [cursor, dataAreaHeight, getHeightBetween, totalRowCount]);
176
- let handlePreview = useCallback(() => {
177
- let focused = normalizedTransactions[cursor];
178
- if (!focused) {
179
- onActionMessage("No transaction focused for preview.", "yellow");
180
- return;
181
- }
182
- let record = transactionRecordByPrimKey.get(focused.PrimKey);
183
- if (!record) {
184
- onActionMessage("Unable to resolve focused transaction for preview.", "red");
185
- return;
141
+ }
142
+ let focusedPrimKey = normalizedTransactions[cursor]?.PrimKey ?? null;
143
+ function handlePreviewClose() {
144
+ if (previewOpen) {
145
+ setPreviewOpen(false);
146
+ refresh({ preserveViewport: true, anchorPrimKey: normalizedTransactions[cursor]?.PrimKey ?? null });
186
147
  }
187
- setPreviewOpen(true);
188
- }, [cursor, normalizedTransactions, transactionRecordByPrimKey, onActionMessage]);
189
- useTransactionsEditorInput({
190
- setCursor,
191
- setSelectedPrimKeys,
192
- totalRowCount,
193
- pageStep,
194
- normalizedTransactions,
195
- cursor,
196
- cycleStatus: () => cycleStatus(normalizedTransactions[cursor]?.PrimKey ?? null),
197
- handleSave,
198
- previewOpen,
199
- handlePreview,
200
- onPreviewClose: useCallback(() => setPreviewOpen(false), []),
201
- refresh,
202
- exit
203
- });
204
- let startRow = totalRowCount === 0 ? 0 : offset + 1;
205
- let stats = [`${totalRowCount} rows`, `showing ${displayedRowCount}`, `starting at ${startRow}`];
148
+ }
149
+ let tableCommandsEnabled = !previewOpen;
150
+ useCommand("up", focusPrevious, tableCommandsEnabled);
151
+ useCommand("down", focusNext, tableCommandsEnabled);
152
+ useCommand("pageup", pageUpCursor, tableCommandsEnabled);
153
+ useCommand("pagedown", pageDownCursor, tableCommandsEnabled);
154
+ useCommand("home", () => setCursor(0), tableCommandsEnabled);
155
+ useCommand("end", () => setCursor(Math.max(totalRowCount - 1, 0)), tableCommandsEnabled);
156
+ useCommand(SAVE_SHORTCUTS, save, tableCommandsEnabled);
157
+ useCommand("space", () => toggleSelection(focusedPrimKey), tableCommandsEnabled);
158
+ useCommand("s", () => cycleStatus(focusedPrimKey), tableCommandsEnabled);
159
+ useCommand("r", () => refresh({ preserveViewport: true, anchorPrimKey: focusedPrimKey }), tableCommandsEnabled);
160
+ useCommand("p", handlePreview, tableCommandsEnabled);
161
+ useCommand("escape", handlePreviewClose, true);
162
+ useCommand(EXIT_SHORTCUTS, exit, true);
163
+ let statusParts = [`showing ${stats.firstVisibleRow}-${stats.lastVisibleRow} of ${stats.totalRowCount}`];
206
164
  if (selectedPrimKeys.size > 0) {
207
- stats.push(`${selectedPrimKeys.size} selected`);
165
+ statusParts.push(`${selectedPrimKeys.size} selected`);
208
166
  }
209
167
  if (modifiedPrimKeys.size > 0) {
210
- stats.push(`${modifiedPrimKeys.size} modified`);
168
+ statusParts.push(`${modifiedPrimKeys.size} modified`);
169
+ }
170
+ let statsText = statusParts.length > 0 ? `(${statusParts.join(" - ")})` : "";
171
+ let statusMessage = "";
172
+ let statusColor = "gray";
173
+ if (error) {
174
+ statusMessage = error.message.replace(/\s+/g, " ").trim();
175
+ statusColor = "red";
176
+ } else if (loading) {
177
+ statusMessage = "Loading transactions\u2026";
178
+ statusColor = "blue";
179
+ } else if (actionMessage) {
180
+ statusMessage = actionMessage.replace(/\s+/g, " ").trim();
181
+ statusColor = actionColor;
211
182
  }
212
- let statsText = stats.length > 0 ? `(${stats.join(" - ")})` : "";
213
- let statusLine = createStatusLine(actionMessage ?? "", statsText, lineWidth, HEADER_PREFIX);
214
- let statusColor = actionMessage ? actionColor : "gray";
215
- let focusedTransaction = transactionRecordByPrimKey.get(normalizedTransactions[cursor]?.PrimKey ?? null);
183
+ let statusLine = createStatusLine(statusMessage, statsText, lineWidth, HEADER_PREFIX);
216
184
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: frameWidth, height: terminalRows, position: "relative", children: [
217
- error ? /* @__PURE__ */ jsx(Text, { color: "red", wrap: "truncate", children: error.message }) : null,
218
- loading ? /* @__PURE__ */ jsx(Text, { color: "blue", wrap: "truncate", children: "Loading transactions\u2026" }) : null,
219
185
  /* @__PURE__ */ jsxs(
220
186
  Box,
221
187
  {
@@ -234,11 +200,21 @@ export function TransactionsEditor({ server, namespace, showErrorColumn = true,
234
200
  }
235
201
  ),
236
202
  /* @__PURE__ */ jsx(Text, { color: "gray", wrap: "truncate", children: "\u2191/\u2193 move \xB7 PgUp/PgDn jump \xB7 Space toggle select \xB7 s cycle status \xB7 ctrl+s save \xB7 ctrl+x exit \xB7 r refresh \xB7 p preview \xB7 esc close preview" }),
237
- previewOpen && focusedTransaction ? /* @__PURE__ */ jsx(TransactionsPreviewDialog, { record: focusedTransaction, frameWidth }) : null
203
+ previewOpen ? /* @__PURE__ */ jsx(
204
+ TransactionsPreviewDialog,
205
+ {
206
+ server,
207
+ namespace,
208
+ frameWidth,
209
+ focusedPrimKey,
210
+ focusNext,
211
+ focusPrevious
212
+ }
213
+ ) : null
238
214
  ] });
239
215
  }
240
- export async function renderTransactionsEditor(props) {
241
- const ink = withFullScreen(/* @__PURE__ */ jsx(TransactionsEditor, { ...props }));
216
+ export async function renderTransactionsEditor({ filter = "edit", ...props }) {
217
+ const ink = withFullScreen(/* @__PURE__ */ jsx(TransactionsEditor, { ...props, filter }));
242
218
  await ink.start();
243
219
  return ink;
244
220
  }