Skip to content

Commit 7bc30de

Browse files
authored
Merge pull request #792 from Lemoncode/dev
quick Mock - tree view
2 parents 0dd29a5 + 6d5db61 commit 7bc30de

File tree

22 files changed

+934
-3
lines changed

22 files changed

+934
-3
lines changed
Lines changed: 28 additions & 0 deletions
Loading
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { ElementSize, Size } from '@/core/model';
2+
import { useCanvasContext } from '@/core/providers';
3+
import { useEffect, useRef } from 'react';
4+
import { FileTreeItem, FileTreeSizeValues } from './file-tree.model';
5+
6+
// Hook to resize edition text area based on content
7+
const useFileTreeResizeOnContentChange = (
8+
id: string,
9+
coords: { x: number; y: number },
10+
text: string,
11+
currentSize: Size,
12+
calculatedSize: Size,
13+
minHeight: number
14+
) => {
15+
const previousText = useRef(text);
16+
const { updateShapeSizeAndPosition } = useCanvasContext();
17+
18+
useEffect(() => {
19+
const textChanged = previousText.current !== text;
20+
21+
const finalHeight = Math.max(calculatedSize.height, minHeight);
22+
const finalSize = { ...calculatedSize, height: finalHeight };
23+
24+
const sizeChanged =
25+
finalHeight !== currentSize.height ||
26+
calculatedSize.width !== currentSize.width;
27+
28+
if (textChanged && sizeChanged) {
29+
previousText.current = text;
30+
updateShapeSizeAndPosition(id, coords, finalSize, false);
31+
} else if (sizeChanged) {
32+
// If only the size has changed, also resize
33+
updateShapeSizeAndPosition(id, coords, finalSize, false);
34+
}
35+
}, [
36+
text,
37+
calculatedSize.height,
38+
calculatedSize.width,
39+
currentSize.height,
40+
currentSize.width,
41+
id,
42+
coords.x,
43+
coords.y,
44+
updateShapeSizeAndPosition,
45+
]);
46+
};
47+
48+
// Hook to force width change when ElementSize changes (XS ↔ S)
49+
// This ensures that when dropping a component and changing from S to XS (or vice versa),
50+
// the component doesn't maintain the previous width but forces the correct one:
51+
// - XS: 150px width
52+
// - S: 230px width
53+
54+
const useFileTreeResizeOnSizeChange = (
55+
id: string,
56+
coords: { x: number; y: number },
57+
currentSize: Size,
58+
treeItems: FileTreeItem[],
59+
sizeValues: FileTreeSizeValues,
60+
size?: ElementSize
61+
) => {
62+
const previousSize = useRef(size);
63+
const { updateShapeSizeAndPosition } = useCanvasContext();
64+
65+
useEffect(() => {
66+
// Only update if the size has changed
67+
if (previousSize.current !== size) {
68+
previousSize.current = size;
69+
70+
const newWidth = size === 'XS' ? 150 : 230;
71+
72+
const minContentHeight =
73+
treeItems && sizeValues
74+
? treeItems.length * sizeValues.elementHeight +
75+
sizeValues.paddingY * 2
76+
: currentSize.height;
77+
78+
if (
79+
currentSize.width !== newWidth ||
80+
currentSize.height !== minContentHeight
81+
) {
82+
updateShapeSizeAndPosition(
83+
id,
84+
coords,
85+
{ width: newWidth, height: minContentHeight },
86+
false
87+
);
88+
}
89+
}
90+
}, [
91+
size,
92+
currentSize.width,
93+
currentSize.height,
94+
id,
95+
coords.x,
96+
coords.y,
97+
updateShapeSizeAndPosition,
98+
treeItems,
99+
sizeValues,
100+
]);
101+
};
102+
103+
export const useFileTreeResize = (
104+
id: string,
105+
coords: { x: number; y: number },
106+
text: string,
107+
currentSize: Size,
108+
calculatedSize: Size,
109+
minHeight: number,
110+
treeItems: FileTreeItem[],
111+
sizeValues: FileTreeSizeValues,
112+
size?: ElementSize
113+
) => {
114+
useFileTreeResizeOnContentChange(
115+
id,
116+
coords,
117+
text,
118+
currentSize,
119+
calculatedSize,
120+
minHeight
121+
);
122+
123+
useFileTreeResizeOnSizeChange(
124+
id,
125+
coords,
126+
currentSize,
127+
128+
treeItems,
129+
sizeValues,
130+
size
131+
);
132+
};
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { parseFileTreeText } from './file-tree.business';
2+
3+
describe('parseFileTreeText', () => {
4+
describe('Basic functionality', () => {
5+
it.each([
6+
['+ Documents', 'folder', 'Documents'],
7+
['- Downloads', 'subfolder', 'Downloads'],
8+
['* README.md', 'file', 'README.md'],
9+
['Projects', 'folder', 'Projects'],
10+
])(
11+
'should parse %s as %s with text "%s"',
12+
(text, expectedType, expectedText) => {
13+
// Arrange
14+
15+
// Act
16+
const result = parseFileTreeText(text);
17+
18+
// Assert
19+
expect(result).toEqual([
20+
{
21+
type: expectedType,
22+
text: expectedText,
23+
level: 0,
24+
},
25+
]);
26+
}
27+
);
28+
});
29+
30+
describe('Indentation levels', () => {
31+
it.each<{ description: string; text: string; expectedLevel: number }>([
32+
{
33+
description: 'no spaces create level 0',
34+
text: '+ Root',
35+
expectedLevel: 0,
36+
},
37+
{
38+
description: '3 spaces create level 1',
39+
text: ' + Subfolder',
40+
expectedLevel: 1,
41+
},
42+
{
43+
description: '6 spaces create level 2',
44+
text: ' * File',
45+
expectedLevel: 2,
46+
},
47+
{
48+
description: '9 spaces create level 3',
49+
text: ' + Deep folder',
50+
expectedLevel: 3,
51+
},
52+
])('$description', ({ text, expectedLevel }) => {
53+
// Arrange
54+
55+
// Act
56+
const result = parseFileTreeText(text);
57+
58+
// Assert
59+
expect(result[0].level).toBe(expectedLevel);
60+
});
61+
62+
it('should handle indentation with non-standard spacing', () => {
63+
// Arrange
64+
const text = ` + Two spaces (level 0)
65+
+ Four spaces (level 1)
66+
+ Five spaces (level 1)
67+
+ Seven spaces (level 2)`;
68+
69+
// Act
70+
const result = parseFileTreeText(text);
71+
72+
// Assert
73+
expect(result[0].level).toBe(0); // 2/3 = 0
74+
expect(result[1].level).toBe(1); // 4/3 = 1
75+
expect(result[2].level).toBe(1); // 5/3 = 1
76+
expect(result[3].level).toBe(2); // 7/3 = 2
77+
});
78+
79+
it('should handle complex nested structure', () => {
80+
// Arrange
81+
const text = `+ Root
82+
- Subfolder 1
83+
* File 1
84+
+ Subfolder 2
85+
- Deep subfolder
86+
* Deep file
87+
+ Deep folder
88+
- Deep subfolder 2
89+
* Deep file 2`;
90+
91+
// Act
92+
const result = parseFileTreeText(text);
93+
94+
// Assert
95+
expect(result).toEqual([
96+
{ type: 'folder', text: 'Root', level: 0 },
97+
{ type: 'subfolder', text: 'Subfolder 1', level: 1 },
98+
{ type: 'file', text: 'File 1', level: 2 },
99+
{ type: 'folder', text: 'Subfolder 2', level: 1 },
100+
{ type: 'subfolder', text: 'Deep subfolder', level: 1 },
101+
{ type: 'file', text: 'Deep file', level: 2 },
102+
{ type: 'folder', text: 'Deep folder', level: 3 },
103+
{ type: 'subfolder', text: 'Deep subfolder 2', level: 5 },
104+
{ type: 'file', text: 'Deep file 2', level: 6 },
105+
]);
106+
});
107+
});
108+
109+
describe('Corner cases', () => {
110+
it.each<{ description: string; input: string; expected: any[] }>([
111+
{
112+
description: 'return empty array for empty string',
113+
input: '',
114+
expected: [],
115+
},
116+
{
117+
description:
118+
'filter out lines with only newlines between valid content',
119+
input: `
120+
121+
+ Folder
122+
123+
* File
124+
125+
`,
126+
expected: [
127+
{ type: 'folder', text: 'Folder', level: 0 },
128+
{ type: 'file', text: 'File', level: 0 },
129+
],
130+
},
131+
{
132+
description: 'return empty array for text with only newlines',
133+
input: '\n\n\n',
134+
expected: [],
135+
},
136+
])('should $description', ({ input, expected }) => {
137+
// Arrange
138+
139+
// Act
140+
const result = parseFileTreeText(input);
141+
142+
// Assert
143+
expect(result).toEqual(expected);
144+
});
145+
});
146+
147+
describe('Edge cases with symbols', () => {
148+
it.each<{ text: string; expected: any[]; description: string }>([
149+
{
150+
description:
151+
'ignore extra spaces after the symbol and keep correct type',
152+
text: `+ Documents
153+
- Downloads
154+
* README.md`,
155+
expected: [
156+
{ type: 'folder', text: 'Documents', level: 0 },
157+
{ type: 'subfolder', text: 'Downloads', level: 0 },
158+
{ type: 'file', text: 'README.md', level: 0 },
159+
],
160+
},
161+
{
162+
description: 'handle symbols without space after as plain text',
163+
text: `+
164+
-
165+
*`,
166+
expected: [
167+
{ type: 'folder', text: '+', level: 0 },
168+
{ type: 'folder', text: '-', level: 0 },
169+
{ type: 'folder', text: '*', level: 0 },
170+
],
171+
},
172+
{
173+
description: 'trim leading/trailing whitespace in text',
174+
text: `+ Documents
175+
- Downloads
176+
* README.md `,
177+
expected: [
178+
{ type: 'folder', text: 'Documents', level: 0 },
179+
{ type: 'subfolder', text: 'Downloads', level: 0 },
180+
{ type: 'file', text: 'README.md', level: 0 },
181+
],
182+
},
183+
{
184+
description: 'recognize symbols with space but no text',
185+
text: `+
186+
-
187+
* `,
188+
expected: [
189+
{ type: 'folder', text: '', level: 0 },
190+
{ type: 'subfolder', text: '', level: 0 },
191+
{ type: 'file', text: '', level: 0 },
192+
],
193+
},
194+
{
195+
description:
196+
'handle lines starting with symbols but not followed by space, as folder and plain text',
197+
text: `+Documents
198+
-Downloads
199+
*README.md`,
200+
expected: [
201+
{ type: 'folder', text: '+Documents', level: 0 },
202+
{ type: 'folder', text: '-Downloads', level: 0 },
203+
{ type: 'folder', text: '*README.md', level: 0 },
204+
],
205+
},
206+
])('should $description', ({ text, expected }) => {
207+
// Arrange
208+
209+
// Act
210+
const result = parseFileTreeText(text);
211+
212+
// Assert
213+
expect(result).toEqual(expected);
214+
});
215+
});
216+
217+
describe('Large indentation values', () => {
218+
it('should handle 27 spaces creating level 9 indentation', () => {
219+
// Arrange
220+
const spaces = ' '; // 27 spaces
221+
222+
// Act
223+
const text = `${spaces}+ Deep folder`;
224+
const result = parseFileTreeText(text);
225+
226+
// Assert
227+
expect(result[0].level).toBe(9);
228+
});
229+
});
230+
});

0 commit comments

Comments
 (0)