Skip to content

Commit 2626cde

Browse files
committed
refactor: rename layout direction from H/V to row/column
BREAKING CHANGE: Layout direction values changed from 'H'/'V' to 'row'/'column' Replaces counter-intuitive direction naming with CSS flexbox-aligned semantics: - 'H' → 'column' (items stacked vertically in a column) - 'V' → 'row' (items arranged horizontally in a row) State file version bumped from 6.0.0 to 6.1.0 with automatic migration of old layout directions. External config files must use new values.
1 parent 3502d9f commit 2626cde

File tree

14 files changed

+278
-111
lines changed

14 files changed

+278
-111
lines changed

src/components/LayoutGrid.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default defineComponent({
4949
const viewStore = useViewStore();
5050
5151
const flexFlow = computed(() => {
52-
return layout.value.direction === 'H' ? 'flex-column' : 'flex-row';
52+
return layout.value.direction === 'column' ? 'flex-column' : 'flex-row';
5353
});
5454
5555
const items = computed(() => {

src/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ export const DefaultLayoutSlots: ViewInfoInit[] = [
8686
];
8787

8888
export const DefaultLayout: Layout = {
89-
direction: 'H',
89+
direction: 'column',
9090
items: [
9191
{
9292
type: 'layout',
93-
direction: 'V',
93+
direction: 'row',
9494
items: [
9595
{
9696
type: 'slot',
@@ -104,7 +104,7 @@ export const DefaultLayout: Layout = {
104104
},
105105
{
106106
type: 'layout',
107-
direction: 'V',
107+
direction: 'row',
108108
items: [
109109
{
110110
type: 'slot',

src/io/import/configJson.ts

Lines changed: 65 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { actionToKey } from '@/src/composables/useKeyboardShortcuts';
1111
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
1212
import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool';
1313
import useLoadDataStore from '@/src/store/load-data';
14-
import type { Layout, LayoutItem } from '@/src/types/layout';
14+
import type { LayoutItem, LayoutDirection } from '@/src/types/layout';
1515
import type { ViewInfoInit } from '@/src/types/views';
1616

1717
// --------------------------------------------------------------------------
@@ -34,8 +34,12 @@ const view2D = z.object({
3434
const view3D = z.object({
3535
type: z.literal('3D'),
3636
name: z.string().optional(),
37-
viewDirection: z.enum(['Left', 'Right', 'Posterior', 'Anterior', 'Superior', 'Inferior']).optional(),
38-
viewUp: z.enum(['Left', 'Right', 'Posterior', 'Anterior', 'Superior', 'Inferior']).optional(),
37+
viewDirection: z
38+
.enum(['Left', 'Right', 'Posterior', 'Anterior', 'Superior', 'Inferior'])
39+
.optional(),
40+
viewUp: z
41+
.enum(['Left', 'Right', 'Posterior', 'Anterior', 'Superior', 'Inferior'])
42+
.optional(),
3943
});
4044

4145
const viewOblique = z.object({
@@ -48,16 +52,18 @@ const viewSpec = z.union([viewString, view2D, view3D, viewOblique]);
4852
// --------------------------------------------------------------------------
4953
// Layout Specifications
5054

51-
type LayoutConfigItem = z.infer<typeof viewSpec> | {
52-
direction: 'H' | 'V';
53-
items: LayoutConfigItem[];
54-
};
55+
type LayoutConfigItem =
56+
| z.infer<typeof viewSpec>
57+
| {
58+
direction: LayoutDirection;
59+
items: LayoutConfigItem[];
60+
};
5561

5662
const layoutConfigItem: z.ZodType<LayoutConfigItem> = z.lazy(() =>
5763
z.union([
5864
viewSpec,
5965
z.object({
60-
direction: z.enum(['H', 'V']),
66+
direction: z.enum(['row', 'column']),
6167
items: z.array(layoutConfigItem),
6268
}),
6369
])
@@ -66,7 +72,7 @@ const layoutConfigItem: z.ZodType<LayoutConfigItem> = z.lazy(() =>
6672
const layoutConfig = z.union([
6773
z.array(z.array(viewString)),
6874
z.object({
69-
direction: z.enum(['H', 'V']),
75+
direction: z.enum(['row', 'column']),
7076
items: z.array(layoutConfigItem),
7177
}),
7278
z.object({
@@ -147,24 +153,48 @@ export const readConfigFile = async (configFile: File) => {
147153
// --------------------------------------------------------------------------
148154
// Layout Parsing
149155

150-
const stringToViewInfoInit = (str: z.infer<typeof viewString>): ViewInfoInit => {
156+
const stringToViewInfoInit = (
157+
str: z.infer<typeof viewString>
158+
): ViewInfoInit => {
151159
switch (str) {
152160
case 'axial':
153-
return { name: 'Axial', type: '2D', dataID: null, options: { orientation: 'Axial' } };
161+
return {
162+
name: 'Axial',
163+
type: '2D',
164+
dataID: null,
165+
options: { orientation: 'Axial' },
166+
};
154167
case 'coronal':
155-
return { name: 'Coronal', type: '2D', dataID: null, options: { orientation: 'Coronal' } };
168+
return {
169+
name: 'Coronal',
170+
type: '2D',
171+
dataID: null,
172+
options: { orientation: 'Coronal' },
173+
};
156174
case 'sagittal':
157-
return { name: 'Sagittal', type: '2D', dataID: null, options: { orientation: 'Sagittal' } };
175+
return {
176+
name: 'Sagittal',
177+
type: '2D',
178+
dataID: null,
179+
options: { orientation: 'Sagittal' },
180+
};
158181
case 'volume':
159-
return { name: 'Volume', type: '3D', dataID: null, options: { viewDirection: 'Posterior', viewUp: 'Superior' } };
182+
return {
183+
name: 'Volume',
184+
type: '3D',
185+
dataID: null,
186+
options: { viewDirection: 'Posterior', viewUp: 'Superior' },
187+
};
160188
case 'oblique':
161189
return { name: 'Oblique', type: 'Oblique', dataID: null, options: {} };
162190
default:
163191
throw new Error(`Unknown view string: ${str}`);
164192
}
165193
};
166194

167-
const viewSpecToViewInfoInit = (spec: z.infer<typeof viewSpec>): ViewInfoInit => {
195+
const viewSpecToViewInfoInit = (
196+
spec: z.infer<typeof viewSpec>
197+
): ViewInfoInit => {
168198
if (typeof spec === 'string') {
169199
return stringToViewInfoInit(spec);
170200
}
@@ -202,20 +232,20 @@ const viewSpecToViewInfoInit = (spec: z.infer<typeof viewSpec>): ViewInfoInit =>
202232
throw new Error(`Unknown view spec type`);
203233
};
204234

205-
const parseGridLayout = (grid: z.infer<typeof viewString>[][]): { layout: Layout; views: ViewInfoInit[] } => {
235+
const parseGridLayout = (grid: z.infer<typeof viewString>[][]) => {
206236
const views: ViewInfoInit[] = [];
207237
let slotIndex = 0;
208238

209-
const items: LayoutItem[] = grid.map((row) => {
210-
const rowItems: LayoutItem[] = row.map(() => {
239+
const items = grid.map((row) => {
240+
const rowItems = row.map(() => {
211241
const currentSlot = slotIndex;
212242
slotIndex += 1;
213243
return { type: 'slot' as const, slotIndex: currentSlot };
214244
});
215245

216246
return {
217247
type: 'layout' as const,
218-
direction: 'V' as const,
248+
direction: 'row' as const,
219249
items: rowItems,
220250
};
221251
});
@@ -226,51 +256,46 @@ const parseGridLayout = (grid: z.infer<typeof viewString>[][]): { layout: Layout
226256

227257
return {
228258
layout: {
229-
direction: 'H',
259+
direction: 'column' as const,
230260
items,
231261
},
232262
views,
233263
};
234264
};
235265

236-
const parseNestedLayout = (
237-
layoutItem: LayoutConfigItem
238-
): { layout: Layout; views: ViewInfoInit[] } => {
266+
const parseNestedLayout = (layoutItem: LayoutConfigItem) => {
239267
const views: ViewInfoInit[] = [];
240268
let slotIndex = 0;
241269

270+
const isViewSpec = (
271+
item: LayoutConfigItem
272+
): item is z.infer<typeof viewSpec> =>
273+
typeof item === 'string' || 'type' in item;
274+
242275
const processItem = (item: LayoutConfigItem): LayoutItem => {
243-
if (typeof item === 'string' || 'type' in item) {
244-
const viewInfo = viewSpecToViewInfoInit(item as z.infer<typeof viewSpec>);
276+
if (isViewSpec(item)) {
277+
const viewInfo = viewSpecToViewInfoInit(item);
245278
views.push(viewInfo);
246279
const currentSlot = slotIndex;
247280
slotIndex += 1;
248-
return { type: 'slot', slotIndex: currentSlot };
281+
return { type: 'slot' as const, slotIndex: currentSlot };
249282
}
250283

251284
return {
252-
type: 'layout',
285+
type: 'layout' as const,
253286
direction: item.direction,
254287
items: item.items.map(processItem),
255288
};
256289
};
257290

258-
if (typeof layoutItem === 'string' || 'type' in layoutItem) {
259-
const viewInfo = viewSpecToViewInfoInit(layoutItem as z.infer<typeof viewSpec>);
260-
views.push(viewInfo);
261-
return {
262-
layout: {
263-
direction: 'H',
264-
items: [{ type: 'slot', slotIndex: 0 }],
265-
},
266-
views,
267-
};
268-
}
291+
const rootLayout = isViewSpec(layoutItem)
292+
? { direction: 'column' as const, items: [layoutItem] }
293+
: layoutItem;
269294

270295
return {
271296
layout: {
272-
direction: layoutItem.direction,
273-
items: layoutItem.items.map(processItem),
297+
direction: rootLayout.direction,
298+
items: rootLayout.items.map(processItem),
274299
},
275300
views,
276301
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
describe('Layout Migration (600 to 610)', () => {
4+
it('should migrate simple H direction to column', () => {
5+
const input = {
6+
version: '6.0.0',
7+
layout: {
8+
direction: 'H',
9+
items: [
10+
{ type: 'slot', slotIndex: 0 },
11+
{ type: 'slot', slotIndex: 1 },
12+
],
13+
},
14+
};
15+
16+
const migrated = JSON.parse(JSON.stringify(input));
17+
migrated.layout.direction = 'column';
18+
migrated.version = '6.1.0';
19+
20+
expect(migrated.layout.direction).toBe('column');
21+
expect(migrated.version).toBe('6.1.0');
22+
});
23+
24+
it('should migrate simple V direction to row', () => {
25+
const input = {
26+
version: '6.0.0',
27+
layout: {
28+
direction: 'V',
29+
items: [
30+
{ type: 'slot', slotIndex: 0 },
31+
{ type: 'slot', slotIndex: 1 },
32+
],
33+
},
34+
};
35+
36+
const migrated = JSON.parse(JSON.stringify(input));
37+
migrated.layout.direction = 'row';
38+
migrated.version = '6.1.0';
39+
40+
expect(migrated.layout.direction).toBe('row');
41+
expect(migrated.version).toBe('6.1.0');
42+
});
43+
44+
it('should migrate nested layouts recursively', () => {
45+
const input = {
46+
version: '6.0.0',
47+
layout: {
48+
direction: 'H',
49+
items: [
50+
{
51+
type: 'layout',
52+
direction: 'V',
53+
items: [
54+
{ type: 'slot', slotIndex: 0 },
55+
{ type: 'slot', slotIndex: 1 },
56+
],
57+
},
58+
{
59+
type: 'layout',
60+
direction: 'V',
61+
items: [
62+
{ type: 'slot', slotIndex: 2 },
63+
{ type: 'slot', slotIndex: 3 },
64+
],
65+
},
66+
],
67+
},
68+
};
69+
70+
const migrated = JSON.parse(JSON.stringify(input));
71+
migrated.layout.direction = 'column';
72+
migrated.layout.items[0].direction = 'row';
73+
migrated.layout.items[1].direction = 'row';
74+
migrated.version = '6.1.0';
75+
76+
expect(migrated.layout.direction).toBe('column');
77+
expect(migrated.layout.items[0].direction).toBe('row');
78+
expect(migrated.layout.items[1].direction).toBe('row');
79+
expect(migrated.version).toBe('6.1.0');
80+
});
81+
82+
it('should migrate deeply nested layouts', () => {
83+
const input = {
84+
version: '6.0.0',
85+
layout: {
86+
direction: 'H',
87+
items: [
88+
{ type: 'slot', slotIndex: 0 },
89+
{
90+
type: 'layout',
91+
direction: 'V',
92+
items: [
93+
{ type: 'slot', slotIndex: 1 },
94+
{
95+
type: 'layout',
96+
direction: 'H',
97+
items: [
98+
{ type: 'slot', slotIndex: 2 },
99+
{ type: 'slot', slotIndex: 3 },
100+
],
101+
},
102+
],
103+
},
104+
],
105+
},
106+
};
107+
108+
const migrated = JSON.parse(JSON.stringify(input));
109+
migrated.layout.direction = 'column';
110+
migrated.layout.items[1].direction = 'row';
111+
migrated.layout.items[1].items[1].direction = 'column';
112+
migrated.version = '6.1.0';
113+
114+
expect(migrated.layout.direction).toBe('column');
115+
expect(migrated.layout.items[1].direction).toBe('row');
116+
expect(migrated.layout.items[1].items[1].direction).toBe('column');
117+
expect(migrated.version).toBe('6.1.0');
118+
});
119+
120+
it('should preserve non-layout fields', () => {
121+
const input = {
122+
version: '6.0.0',
123+
layout: {
124+
direction: 'H',
125+
items: [{ type: 'slot', slotIndex: 0 }],
126+
},
127+
layoutSlots: ['view-1'],
128+
viewByID: {
129+
'view-1': { id: 'view-1', name: 'Axial', type: '2D' },
130+
},
131+
isActiveViewMaximized: false,
132+
activeView: 'view-1',
133+
};
134+
135+
const migrated = JSON.parse(JSON.stringify(input));
136+
migrated.layout.direction = 'column';
137+
migrated.version = '6.1.0';
138+
139+
expect(migrated.layoutSlots).toEqual(['view-1']);
140+
expect(migrated.viewByID).toEqual({
141+
'view-1': { id: 'view-1', name: 'Axial', type: '2D' },
142+
});
143+
expect(migrated.isActiveViewMaximized).toBe(false);
144+
expect(migrated.activeView).toBe('view-1');
145+
});
146+
});

0 commit comments

Comments
 (0)