Skip to content

Commit 34b7f66

Browse files
committed
v0.0.4
1 parent dd5ca49 commit 34b7f66

19 files changed

Lines changed: 337 additions & 62 deletions

‎assets/src/api.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe('CSRFトークンの解決', () => {
166166
});
167167

168168
describe('fetchSnapshots', () => {
169-
it('blogIdをパラメータに含めてPOSTし、成功時はレスポンスをそのまま返す', async () => {
169+
it('blogIdをパラメータに含めてPOSTし、成功時はレスポンスをそのまま返す。includeChildrenは省略時"0"として送信する', async () => {
170170
mockedPost.mockReturnValue(respond({ success: true, snapshots: [] }));
171171

172172
const result = await fetchSnapshots(5);
@@ -175,6 +175,16 @@ describe('fetchSnapshots', () => {
175175
const [, params] = mockedPost.mock.calls[0] as [string, URLSearchParams];
176176
expect(params.get('ACMS_POST_ModuleMigrationSnapshots')).toBe('post');
177177
expect(params.get('blogId')).toBe('5');
178+
expect(params.get('includeChildren')).toBe('0');
179+
});
180+
181+
it('includeChildren:trueを渡した場合は"1"として送信する', async () => {
182+
mockedPost.mockReturnValue(respond({ success: true, snapshots: [] }));
183+
184+
await fetchSnapshots(5, true);
185+
186+
const [, params] = mockedPost.mock.calls[0] as [string, URLSearchParams];
187+
expect(params.get('includeChildren')).toBe('1');
178188
});
179189

180190
it('success:falseの場合、messageをそのままエラーにして投げる', async () => {

‎assets/src/api.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ export function rollbackMigration(blogId: number, snapshotId: number): Promise<M
7070
return postAcms<MigrationRollbackResponse>('ACMS_POST_ModuleMigrationRollback', { blogId, snapshotId });
7171
}
7272

73-
export function fetchSnapshots(blogId: number): Promise<MigrationSnapshotsResponse> {
74-
return postAcms<MigrationSnapshotsResponse>('ACMS_POST_ModuleMigrationSnapshots', { blogId });
73+
export function fetchSnapshots(blogId: number, includeChildren: boolean = false): Promise<MigrationSnapshotsResponse> {
74+
return postAcms<MigrationSnapshotsResponse>('ACMS_POST_ModuleMigrationSnapshots', {
75+
blogId,
76+
includeChildren: includeChildren ? '1' : '0',
77+
});
7578
}

‎assets/src/blog-label.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* 「配下のブログを含める」表示(モジュール一覧・移行履歴一覧)で、所属ブログ名とIDを
3+
* 併記した表示文字列を組み立てる。blogNameが無い(取得できなかった)場合はIDのみ表示する。
4+
*/
5+
export function blogLabel(blogName: string | null, blogId: number): string {
6+
return blogName !== null ? `${blogName} (${blogId})` : `(${blogId})`;
7+
}

‎assets/src/components/migration-history.test.tsx‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ function summary(overrides: Partial<SnapshotSummary> = {}): SnapshotSummary {
1919
moduleName: 'Plugin_Schedule',
2020
snapshotDatetime: '2026-08-30 12:00:00',
2121
userId: 1,
22+
blogId: 1,
23+
blogName: null,
2224
...overrides,
2325
};
2426
}
@@ -51,6 +53,54 @@ describe('MigrationHistory', () => {
5153
expect(loadSnapshots).toHaveBeenCalledTimes(1);
5254
});
5355

56+
it('includeChildrenを省略した場合、falseでマウント時の一覧取得を呼ぶ', () => {
57+
const loadSnapshots = vi.fn();
58+
mockedUseMigrationHistory.mockReturnValue(makeHookState({ loadSnapshots }));
59+
60+
render(<MigrationHistory blogId={1} />);
61+
62+
expect(loadSnapshots).toHaveBeenCalledWith(false);
63+
});
64+
65+
it('includeChildren:trueを渡すと、trueで一覧取得する', () => {
66+
const loadSnapshots = vi.fn();
67+
mockedUseMigrationHistory.mockReturnValue(makeHookState({ loadSnapshots }));
68+
69+
render(<MigrationHistory blogId={1} includeChildren />);
70+
71+
expect(loadSnapshots).toHaveBeenCalledWith(true);
72+
});
73+
74+
it('includeChildrenが変化すると一覧を取得し直す', () => {
75+
const loadSnapshots = vi.fn();
76+
mockedUseMigrationHistory.mockReturnValue(makeHookState({ loadSnapshots }));
77+
78+
const { rerender } = render(<MigrationHistory blogId={1} includeChildren={false} />);
79+
expect(loadSnapshots).toHaveBeenLastCalledWith(false);
80+
81+
rerender(<MigrationHistory blogId={1} includeChildren />);
82+
83+
expect(loadSnapshots).toHaveBeenLastCalledWith(true);
84+
});
85+
86+
it('スナップショットの所属ブログ名とブログIDを表示する', () => {
87+
mockedUseMigrationHistory.mockReturnValue(
88+
makeHookState({ snapshots: [summary({ blogName: '子ブログA', blogId: 5 })] })
89+
);
90+
91+
render(<MigrationHistory blogId={1} />);
92+
93+
expect(screen.getByText('子ブログA (5)')).toBeInTheDocument();
94+
});
95+
96+
it('ブログ名が無い場合はブログIDのみ表示する', () => {
97+
mockedUseMigrationHistory.mockReturnValue(makeHookState({ snapshots: [summary({ blogName: null, blogId: 5 })] }));
98+
99+
render(<MigrationHistory blogId={1} />);
100+
101+
expect(screen.getByText('(5)')).toBeInTheDocument();
102+
});
103+
54104
it('移行履歴が無い場合はその旨を表示する', () => {
55105
mockedUseMigrationHistory.mockReturnValue(makeHookState());
56106

@@ -77,17 +127,17 @@ describe('MigrationHistory', () => {
77127
expect(screen.getByRole('alert')).toHaveTextContent('移行履歴の取得に失敗しました。');
78128
});
79129

80-
it('ロールバックボタンを押し確認してOKした場合、rollback()を呼び成功したらアラートを出す', async () => {
130+
it('ロールバックボタンを押し確認してOKした場合、snapshotIdと所属ブログIDでrollback()を呼び成功したらアラートを出す', async () => {
81131
const user = userEvent.setup();
82132
const rollback = vi.fn().mockResolvedValue(true);
83133
mockedConfirm.mockResolvedValue(true);
84-
mockedUseMigrationHistory.mockReturnValue(makeHookState({ snapshots: [summary()], rollback }));
134+
mockedUseMigrationHistory.mockReturnValue(makeHookState({ snapshots: [summary({ blogId: 7 })], rollback }));
85135

86136
render(<MigrationHistory blogId={1} />);
87137
await user.click(screen.getByRole('button', { name: 'ロールバック' }));
88138

89139
expect(mockedConfirm).toHaveBeenCalledTimes(1);
90-
expect(rollback).toHaveBeenCalledWith(10);
140+
expect(rollback).toHaveBeenCalledWith(10, 7);
91141
await waitFor(() => expect(mockedAlert).toHaveBeenCalledWith('ロールバックしました'));
92142
});
93143

‎assets/src/components/migration-history.tsx‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
11
import Button from '@ablogcms/components/button';
22
import Spinner from '@ablogcms/components/spinner';
33
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ablogcms/components/table';
4-
import { useEffectOnce } from '@ablogcms/react-hooks';
4+
import { useEffect } from 'react';
5+
import { blogLabel } from '../blog-label';
56
import { useMigrationHistory } from '../hooks/use-migration-history';
67

78
interface MigrationHistoryProps {
89
blogId: number;
10+
includeChildren?: boolean;
911
onRollbackSuccess?: () => void;
1012
}
1113

12-
function MigrationHistory({ blogId, onRollbackSuccess }: MigrationHistoryProps) {
14+
function MigrationHistory({ blogId, includeChildren = false, onRollbackSuccess }: MigrationHistoryProps) {
1315
const { snapshots, isLoading, error, isRollingBack, loadSnapshots, rollback } = useMigrationHistory(blogId, {
1416
onRollbackSuccess,
1517
});
1618

17-
useEffectOnce(() => {
18-
void loadSnapshots();
19-
});
19+
// includeChildrenは呼び出し元(ModuleMigrationAdmin)の「配下のブログを含める」チェックボックスと
20+
// 連動する。マウント時・チェック状態が変わるたびに、その範囲で一覧を取得し直す。
21+
useEffect(() => {
22+
void loadSnapshots(includeChildren);
23+
}, [includeChildren, loadSnapshots]);
2024

21-
const handleRollback = async (snapshotId: number) => {
25+
const handleRollback = async (snapshotId: number, snapshotBlogId: number) => {
2226
const confirmed = await ACMS.Library.dialog.confirm('このスナップショットの内容へ元に戻します。よろしいですか?');
2327
if (!confirmed) {
2428
return;
2529
}
26-
const succeeded = await rollback(snapshotId);
30+
const succeeded = await rollback(snapshotId, snapshotBlogId);
2731
if (succeeded) {
2832
await ACMS.Library.dialog.alert('ロールバックしました');
2933
}
@@ -44,6 +48,7 @@ function MigrationHistory({ blogId, onRollbackSuccess }: MigrationHistoryProps)
4448
<Table>
4549
<TableHeader>
4650
<TableRow>
51+
<TableHead>ブログ</TableHead>
4752
<TableHead>モジュールID</TableHead>
4853
<TableHead>移行前のモジュール</TableHead>
4954
<TableHead>適用日時</TableHead>
@@ -53,12 +58,13 @@ function MigrationHistory({ blogId, onRollbackSuccess }: MigrationHistoryProps)
5358
<TableBody>
5459
{snapshots.map((snapshot) => (
5560
<TableRow key={snapshot.snapshotId}>
61+
<TableCell>{blogLabel(snapshot.blogName, snapshot.blogId)}</TableCell>
5662
<TableCell>{snapshot.moduleId}</TableCell>
5763
<TableCell>{snapshot.moduleName}</TableCell>
5864
<TableCell>{snapshot.snapshotDatetime}</TableCell>
5965
<TableCell>
6066
<Button
61-
onClick={() => void handleRollback(snapshot.snapshotId)}
67+
onClick={() => void handleRollback(snapshot.snapshotId, snapshot.blogId)}
6268
type="button"
6369
disabled={isRollingBack}
6470
>

‎assets/src/components/module-migration-admin.test.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ describe('ModuleMigrationAdmin', () => {
216216

217217
expect(loadModules).toHaveBeenLastCalledWith(true);
218218
});
219+
220+
it('チェック状態は移行履歴一覧(MigrationHistory)にも連動する', async () => {
221+
const user = userEvent.setup();
222+
const loadSnapshots = vi.fn();
223+
mockedUseModuleMigration.mockReturnValue(makeHookState());
224+
mockedUseMigrationHistory.mockReturnValue({
225+
snapshots: [],
226+
isLoading: false,
227+
error: null,
228+
isRollingBack: false,
229+
loadSnapshots,
230+
rollback: vi.fn(),
231+
});
232+
233+
render(<ModuleMigrationAdmin blogId={1} />);
234+
expect(loadSnapshots).toHaveBeenLastCalledWith(true);
235+
236+
await user.click(screen.getByRole('checkbox', { name: '配下のブログを含める' }));
237+
238+
expect(loadSnapshots).toHaveBeenLastCalledWith(false);
239+
});
219240
});
220241

221242
describe('ロールバック', () => {

‎assets/src/components/module-migration-admin.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Spinner from '@ablogcms/components/spinner';
55
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ablogcms/components/table';
66
import { useEffectOnce } from '@ablogcms/react-hooks';
77
import { useState } from 'react';
8+
import { blogLabel } from '../blog-label';
89
import { useModuleMigration } from '../hooks/use-module-migration';
910
import type { MigrationRank, ModuleMigrationCandidate } from '../types';
1011
import DiffPreviewModal from './diff-preview-modal';
@@ -27,10 +28,6 @@ function rankBadgeVariant(rank: MigrationRank | null): 'success' | 'warning' | '
2728
return 'info';
2829
}
2930

30-
function blogLabel(blogName: string | null, blogId: number): string {
31-
return blogName !== null ? `${blogName} (${blogId})` : `(${blogId})`;
32-
}
33-
3431
function rankLabel(rank: MigrationRank | null): string {
3532
if (rank === 'A') {
3633
return 'A: 自動移行可能';
@@ -199,7 +196,11 @@ function ModuleMigrationAdmin({ blogId }: ModuleMigrationAdminProps) {
199196
isApplying={isApplying}
200197
/>
201198

202-
<MigrationHistory blogId={blogId} onRollbackSuccess={() => void loadModules()} />
199+
<MigrationHistory
200+
blogId={blogId}
201+
includeChildren={includeChildren}
202+
onRollbackSuccess={() => void loadModules()}
203+
/>
203204
</div>
204205
);
205206
}

‎assets/src/hooks/use-migration-history.test.ts‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ function summary(overrides: Partial<SnapshotSummary> = {}): SnapshotSummary {
1616
moduleName: 'Plugin_Schedule',
1717
snapshotDatetime: '2026-08-30 12:00:00',
1818
userId: 1,
19+
blogId: 1,
20+
blogName: null,
1921
...overrides,
2022
};
2123
}
@@ -67,6 +69,31 @@ describe('loadSnapshots', () => {
6769
expect(result.current.error).toBe('権限がありません。');
6870
expect(result.current.snapshots).toEqual([]);
6971
});
72+
73+
it('includeChildren:trueを渡すとfetchSnapshotsにそのまま渡す', async () => {
74+
mockedFetchSnapshots.mockResolvedValue({ success: true, snapshots: [] });
75+
const { result } = renderHook(() => useMigrationHistory(1));
76+
77+
await act(async () => {
78+
await result.current.loadSnapshots(true);
79+
});
80+
81+
expect(mockedFetchSnapshots).toHaveBeenCalledWith(1, true);
82+
});
83+
84+
it('引数を省略した場合、直近に指定したincludeChildrenを維持する', async () => {
85+
mockedFetchSnapshots.mockResolvedValue({ success: true, snapshots: [] });
86+
const { result } = renderHook(() => useMigrationHistory(1));
87+
88+
await act(async () => {
89+
await result.current.loadSnapshots(true);
90+
});
91+
await act(async () => {
92+
await result.current.loadSnapshots();
93+
});
94+
95+
expect(mockedFetchSnapshots).toHaveBeenLastCalledWith(1, true);
96+
});
7097
});
7198

7299
describe('rollback', () => {
@@ -78,8 +105,8 @@ describe('rollback', () => {
78105

79106
let secondResult: boolean | null = null;
80107
act(() => {
81-
void result.current.rollback(10);
82-
void result.current.rollback(10).then((r) => {
108+
void result.current.rollback(10, 1);
109+
void result.current.rollback(10, 1).then((r) => {
83110
secondResult = r;
84111
});
85112
});
@@ -94,13 +121,25 @@ describe('rollback', () => {
94121
expect(secondResult).toBe(false);
95122
});
96123

124+
it('スナップショットの所有ブログIDでrollbackMigration()を呼ぶ(hookのblogIdとは独立)', async () => {
125+
mockedRollback.mockResolvedValue({ success: true });
126+
mockedFetchSnapshots.mockResolvedValue({ success: true, snapshots: [] });
127+
const { result } = renderHook(() => useMigrationHistory(1));
128+
129+
await act(async () => {
130+
await result.current.rollback(10, 5);
131+
});
132+
133+
expect(mockedRollback).toHaveBeenCalledWith(5, 10);
134+
});
135+
97136
it('成功したら一覧を再取得する', async () => {
98137
mockedRollback.mockResolvedValue({ success: true });
99138
mockedFetchSnapshots.mockResolvedValue({ success: true, snapshots: [summary()] });
100139
const { result } = renderHook(() => useMigrationHistory(1));
101140

102141
await act(async () => {
103-
await result.current.rollback(10);
142+
await result.current.rollback(10, 1);
104143
});
105144

106145
expect(mockedFetchSnapshots).toHaveBeenCalledTimes(1);
@@ -114,7 +153,7 @@ describe('rollback', () => {
114153
const { result } = renderHook(() => useMigrationHistory(1, { onRollbackSuccess }));
115154

116155
await act(async () => {
117-
await result.current.rollback(10);
156+
await result.current.rollback(10, 1);
118157
});
119158

120159
expect(onRollbackSuccess).toHaveBeenCalledTimes(1);
@@ -124,7 +163,7 @@ describe('rollback', () => {
124163
mockedRollback.mockRejectedValue(new Error('ロールバックに失敗しました。'));
125164
const { result } = renderHook(() => useMigrationHistory(1));
126165

127-
const succeeded = await act(async () => result.current.rollback(10));
166+
const succeeded = await act(async () => result.current.rollback(10, 1));
128167

129168
expect(succeeded).toBe(false);
130169
expect(result.current.error).toBe('ロールバックに失敗しました。');

0 commit comments

Comments
 (0)