From ecc4a1a6ca1d884ae5c7d4f8ba9e31195ea5b41e Mon Sep 17 00:00:00 2001 From: Kenichiro NOGI Date: Sat, 19 Sep 2026 14:11:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(app-portal):=20n8n=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=81=AE=E3=83=AF=E3=83=BC=E3=82=AF=E3=83=95=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E5=90=8D=E3=82=BD=E3=83=BC=E3=83=88=E3=83=BBJST=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=83=BB=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=82=B9=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit タグ内ワークフローを名前昇順に、実行履歴の開始日時をJST変換、 success/error等のステータスバッジを日本語ラベルに変更。 --- NodeSrv/apps/app-portal/src/n8nTree.js | 3 +++ NodeSrv/apps/app-portal/src/n8nView.js | 26 +++++++++++++++++--- NodeSrv/apps/app-portal/test/n8nTree.test.js | 12 +++++++++ NodeSrv/apps/app-portal/test/n8nView.test.js | 14 +++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/NodeSrv/apps/app-portal/src/n8nTree.js b/NodeSrv/apps/app-portal/src/n8nTree.js index 0a1ac65e..3c8597d0 100644 --- a/NodeSrv/apps/app-portal/src/n8nTree.js +++ b/NodeSrv/apps/app-portal/src/n8nTree.js @@ -22,6 +22,9 @@ function buildTagTree(workflows) { } const result = [...groups.values()]; + for (const group of result) { + group.children.sort((a, b) => a.name.localeCompare(b.name)); + } result.sort((a, b) => { if (a.id === UNTAGGED_ID) return 1; if (b.id === UNTAGGED_ID) return -1; diff --git a/NodeSrv/apps/app-portal/src/n8nView.js b/NodeSrv/apps/app-portal/src/n8nView.js index b6169825..98987483 100644 --- a/NodeSrv/apps/app-portal/src/n8nView.js +++ b/NodeSrv/apps/app-portal/src/n8nView.js @@ -37,6 +37,25 @@ const EXECUTION_BADGE = { unknown: 'bg-secondary', }; +const EXECUTION_STATUS_LABEL = { + success: '成功', + error: 'エラー', + crashed: 'クラッシュ', + running: '実行中', + waiting: '待機中', + canceled: 'キャンセル', + new: '新規', + unknown: '不明', +}; + +function formatJst(isoString) { + if (!isoString) return ''; + const utc = new Date(isoString); + const jst = new Date(utc.getTime() + 9 * 60 * 60 * 1000); + const pad = (n) => String(n).padStart(2, '0'); + return `${jst.getUTCFullYear()}-${pad(jst.getUTCMonth() + 1)}-${pad(jst.getUTCDate())} ${pad(jst.getUTCHours())}:${pad(jst.getUTCMinutes())}:${pad(jst.getUTCSeconds())}`; +} + function renderExecutionList(executions) { if (!executions || executions.length === 0) { return '

実行履歴なし

'; @@ -44,14 +63,15 @@ function renderExecutionList(executions) { const rows = executions .map((e) => { const badgeClass = EXECUTION_BADGE[e.status] || 'bg-secondary'; + const statusLabel = EXECUTION_STATUS_LABEL[e.status] || e.status; return ` - ${escapeHtml(e.startedAt || '')} - ${escapeHtml(e.status)} + ${escapeHtml(formatJst(e.startedAt))} + ${escapeHtml(statusLabel)} `; }) .join('\n'); return ` - + ${rows}
開始日時状態
開始日時(JST)状態
`; } diff --git a/NodeSrv/apps/app-portal/test/n8nTree.test.js b/NodeSrv/apps/app-portal/test/n8nTree.test.js index 256d4219..81cac4e8 100644 --- a/NodeSrv/apps/app-portal/test/n8nTree.test.js +++ b/NodeSrv/apps/app-portal/test/n8nTree.test.js @@ -43,6 +43,18 @@ test('buildTagTree puts a multi-tagged workflow into every one of its tag groups assert.strictEqual(tree[1].children.length, 1); }); +test('buildTagTree sorts workflows within a group alphabetically by name', () => { + const workflows = [ + { id: 'w2', name: 'Zワークフロー', active: true, tags: [{ name: 'タグA' }] }, + { id: 'w1', name: 'Aワークフロー', active: true, tags: [{ name: 'タグA' }] }, + ]; + const tree = buildTagTree(workflows); + assert.deepStrictEqual( + tree[0].children.map((w) => w.name), + ['Aワークフロー', 'Zワークフロー'] + ); +}); + test('buildTagTree sorts tag groups alphabetically and puts "未分類" last', () => { const workflows = [ { id: 'w1', name: 'A', active: true, tags: [{ name: 'Zタグ' }] }, diff --git a/NodeSrv/apps/app-portal/test/n8nView.test.js b/NodeSrv/apps/app-portal/test/n8nView.test.js index 38eedb08..9193c7ad 100644 --- a/NodeSrv/apps/app-portal/test/n8nView.test.js +++ b/NodeSrv/apps/app-portal/test/n8nView.test.js @@ -49,6 +49,20 @@ test('renderExecutionList renders a row per execution with a status badge', () = assert.ok(html.includes('bg-danger')); }); +test('renderExecutionList shows the started time converted to JST, not raw UTC', () => { + const html = renderExecutionList([{ id: 'e1', status: 'success', startedAt: '2026-09-19T00:00:00.000Z' }]); + assert.ok(html.includes('2026-09-19 09:00:00')); + assert.ok(!html.includes('2026-09-19T00:00:00.000Z')); +}); + +test('renderExecutionList shows a Japanese label for each status', () => { + const statuses = ['success', 'error', 'running', 'waiting', 'canceled']; + for (const status of statuses) { + const html = renderExecutionList([{ id: 'e1', status, startedAt: '2026-09-19T00:00:00.000Z' }]); + assert.ok(!html.includes(`>${status}<`), `expected status "${status}" to be translated`); + } +}); + test('renderExecutionList shows a message when there are no executions', () => { const html = renderExecutionList([]); assert.ok(html.includes('実行履歴なし'));