Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1811c48
feat: add fileTree shape type with inline editing and size restrictions
gustedeveloper Sep 23, 2025
1aad314
feat: integrate FileTree into canvas rendering and gallery
gustedeveloper Sep 23, 2025
0ab0526
feat: implement FileTree component
gustedeveloper Sep 23, 2025
997b8a0
fix(file-tree): Fix selection box size by constraining text elements …
gustedeveloper Sep 24, 2025
1cc1bd9
fix(file-tree): Add right padding to prevent text overflow
gustedeveloper Sep 24, 2025
7e17f3a
feat(file-tree): Add file-tree component to shape other props configu…
gustedeveloper Sep 24, 2025
9f44b4c
feat(file-tree): Sync icon colors with stroke color for visual consis…
gustedeveloper Sep 24, 2025
f5f3b80
feat(file-tree): Enable dynamic addition of file tree elements via sy…
gustedeveloper Sep 24, 2025
fffa03c
update(file-tree): update file tree default text to meet with dynamic…
gustedeveloper Sep 24, 2025
4a020ad
feat(file-tree): Implement dynamic height adaptation based on content
gustedeveloper Sep 24, 2025
d6fb800
create file tree svg
gustedeveloper Sep 24, 2025
0c5abc0
update file tree svg
gustedeveloper Sep 26, 2025
933ff76
update default file tree text to multiline format with indentation
gustedeveloper Sep 26, 2025
2394d44
feat(file-tree): implement multiline parsing with indentation support…
gustedeveloper Sep 26, 2025
e12cb2d
feat(file-tree): improve visual spacing in default text, 1 extra spac…
gustedeveloper Sep 29, 2025
4c24737
feat(file-tree): update level calculation to match new spacing and ad…
gustedeveloper Sep 29, 2025
f7c6f0e
feat: add generic size types and core model updates
gustedeveloper Sep 29, 2025
13ae182
feat: add size configuration system for icon and fileTree shapes
gustedeveloper Sep 29, 2025
47c3667
feat: create generic SelectSizeV2 component that manages both icon an…
gustedeveloper Sep 29, 2025
4e65c89
feat(file-tree): adjust shape restrictions, implement size property t…
gustedeveloper Sep 29, 2025
ac87418
feat(file-tree): refine sizing system for both available sizes
gustedeveloper Sep 30, 2025
8f8b05a
feat(file-tree): implement hook to get dynamic text editor height bas…
gustedeveloper Sep 30, 2025
736f35a
feat(file-tree): implement hook to get default width adjustment based…
gustedeveloper Sep 30, 2025
f748eed
update file tree svg
gustedeveloper Sep 30, 2025
feda3dc
chore(docs): document why SelectSize v2 exists and link to migration …
gustedeveloper Oct 2, 2025
31c8e40
refactor(file-tree): extract file tree resize hooks into hook file, w…
gustedeveloper Oct 2, 2025
f49e6bb
feat(file-tree): add dynamic width calculation to prevent icon overflow
gustedeveloper Oct 2, 2025
8a93fa9
feat(file-tree): improve resize behavior for content and ElementSize …
gustedeveloper Oct 2, 2025
efb9f20
fix(file-tree): remove restrictive minHeight to enable width resizing…
gustedeveloper Oct 2, 2025
5f0cbb9
fix(file-tree): correctly parse symbols with space but no text content
gustedeveloper Oct 3, 2025
3b08933
perf(file-tree): add memoization to optimize component re-renders
gustedeveloper Oct 3, 2025
13430d7
test(file-tree): add unit tests for parseFileTreeText
gustedeveloper Oct 3, 2025
0f02295
refactor(file-tree): extract interfaces to dedicated model file
gustedeveloper Oct 4, 2025
e32bb77
feat(file-tree): enable manual height resizing while preserving conte…
gustedeveloper Oct 4, 2025
6d5db61
Merge pull request #790 from Lemoncode/feature/#217-create-tree-compo…
brauliodiez Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions public/rich-components/file-tree.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ElementSize, Size } from '@/core/model';
import { useCanvasContext } from '@/core/providers';
import { useEffect, useRef } from 'react';
import { FileTreeItem, FileTreeSizeValues } from './file-tree.model';

// Hook to resize edition text area based on content
const useFileTreeResizeOnContentChange = (
id: string,
coords: { x: number; y: number },
text: string,
currentSize: Size,
calculatedSize: Size,
minHeight: number
) => {
const previousText = useRef(text);
const { updateShapeSizeAndPosition } = useCanvasContext();

useEffect(() => {
const textChanged = previousText.current !== text;

const finalHeight = Math.max(calculatedSize.height, minHeight);
const finalSize = { ...calculatedSize, height: finalHeight };

const sizeChanged =
finalHeight !== currentSize.height ||
calculatedSize.width !== currentSize.width;

if (textChanged && sizeChanged) {
previousText.current = text;
updateShapeSizeAndPosition(id, coords, finalSize, false);
} else if (sizeChanged) {
// If only the size has changed, also resize
updateShapeSizeAndPosition(id, coords, finalSize, false);
}
}, [
text,
calculatedSize.height,
calculatedSize.width,
currentSize.height,
currentSize.width,
id,
coords.x,
coords.y,
updateShapeSizeAndPosition,
]);
};

// Hook to force width change when ElementSize changes (XS ↔ S)
// This ensures that when dropping a component and changing from S to XS (or vice versa),
// the component doesn't maintain the previous width but forces the correct one:
// - XS: 150px width
// - S: 230px width

const useFileTreeResizeOnSizeChange = (
id: string,
coords: { x: number; y: number },
currentSize: Size,
treeItems: FileTreeItem[],
sizeValues: FileTreeSizeValues,
size?: ElementSize
) => {
const previousSize = useRef(size);
const { updateShapeSizeAndPosition } = useCanvasContext();

useEffect(() => {
// Only update if the size has changed
if (previousSize.current !== size) {
previousSize.current = size;

const newWidth = size === 'XS' ? 150 : 230;

const minContentHeight =
treeItems && sizeValues
? treeItems.length * sizeValues.elementHeight +
sizeValues.paddingY * 2
: currentSize.height;

if (
currentSize.width !== newWidth ||
currentSize.height !== minContentHeight
) {
updateShapeSizeAndPosition(
id,
coords,
{ width: newWidth, height: minContentHeight },
false
);
}
}
}, [
size,
currentSize.width,
currentSize.height,
id,
coords.x,
coords.y,
updateShapeSizeAndPosition,
treeItems,
sizeValues,
]);
};

export const useFileTreeResize = (
id: string,
coords: { x: number; y: number },
text: string,
currentSize: Size,
calculatedSize: Size,
minHeight: number,
treeItems: FileTreeItem[],
sizeValues: FileTreeSizeValues,
size?: ElementSize
) => {
useFileTreeResizeOnContentChange(
id,
coords,
text,
currentSize,
calculatedSize,
minHeight
);

useFileTreeResizeOnSizeChange(
id,
coords,
currentSize,

treeItems,
sizeValues,
size
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { parseFileTreeText } from './file-tree.business';

describe('parseFileTreeText', () => {
describe('Basic functionality', () => {
it.each([
['+ Documents', 'folder', 'Documents'],
['- Downloads', 'subfolder', 'Downloads'],
['* README.md', 'file', 'README.md'],
['Projects', 'folder', 'Projects'],
])(
'should parse %s as %s with text "%s"',
(text, expectedType, expectedText) => {
// Arrange

// Act
const result = parseFileTreeText(text);

// Assert
expect(result).toEqual([
{
type: expectedType,
text: expectedText,
level: 0,
},
]);
}
);
});

describe('Indentation levels', () => {
it.each<{ description: string; text: string; expectedLevel: number }>([
{
description: 'no spaces create level 0',
text: '+ Root',
expectedLevel: 0,
},
{
description: '3 spaces create level 1',
text: ' + Subfolder',
expectedLevel: 1,
},
{
description: '6 spaces create level 2',
text: ' * File',
expectedLevel: 2,
},
{
description: '9 spaces create level 3',
text: ' + Deep folder',
expectedLevel: 3,
},
])('$description', ({ text, expectedLevel }) => {
// Arrange

// Act
const result = parseFileTreeText(text);

// Assert
expect(result[0].level).toBe(expectedLevel);
});

it('should handle indentation with non-standard spacing', () => {
// Arrange
const text = ` + Two spaces (level 0)
+ Four spaces (level 1)
+ Five spaces (level 1)
+ Seven spaces (level 2)`;

// Act
const result = parseFileTreeText(text);

// Assert
expect(result[0].level).toBe(0); // 2/3 = 0
expect(result[1].level).toBe(1); // 4/3 = 1
expect(result[2].level).toBe(1); // 5/3 = 1
expect(result[3].level).toBe(2); // 7/3 = 2
});

it('should handle complex nested structure', () => {
// Arrange
const text = `+ Root
- Subfolder 1
* File 1
+ Subfolder 2
- Deep subfolder
* Deep file
+ Deep folder
- Deep subfolder 2
* Deep file 2`;

// Act
const result = parseFileTreeText(text);

// Assert
expect(result).toEqual([
{ type: 'folder', text: 'Root', level: 0 },
{ type: 'subfolder', text: 'Subfolder 1', level: 1 },
{ type: 'file', text: 'File 1', level: 2 },
{ type: 'folder', text: 'Subfolder 2', level: 1 },
{ type: 'subfolder', text: 'Deep subfolder', level: 1 },
{ type: 'file', text: 'Deep file', level: 2 },
{ type: 'folder', text: 'Deep folder', level: 3 },
{ type: 'subfolder', text: 'Deep subfolder 2', level: 5 },
{ type: 'file', text: 'Deep file 2', level: 6 },
]);
});
});

describe('Corner cases', () => {
it.each<{ description: string; input: string; expected: any[] }>([
{
description: 'return empty array for empty string',
input: '',
expected: [],
},
{
description:
'filter out lines with only newlines between valid content',
input: `

+ Folder

* File

`,
expected: [
{ type: 'folder', text: 'Folder', level: 0 },
{ type: 'file', text: 'File', level: 0 },
],
},
{
description: 'return empty array for text with only newlines',
input: '\n\n\n',
expected: [],
},
])('should $description', ({ input, expected }) => {
// Arrange

// Act
const result = parseFileTreeText(input);

// Assert
expect(result).toEqual(expected);
});
});

describe('Edge cases with symbols', () => {
it.each<{ text: string; expected: any[]; description: string }>([
{
description:
'ignore extra spaces after the symbol and keep correct type',
text: `+ Documents
- Downloads
* README.md`,
expected: [
{ type: 'folder', text: 'Documents', level: 0 },
{ type: 'subfolder', text: 'Downloads', level: 0 },
{ type: 'file', text: 'README.md', level: 0 },
],
},
{
description: 'handle symbols without space after as plain text',
text: `+
-
*`,
expected: [
{ type: 'folder', text: '+', level: 0 },
{ type: 'folder', text: '-', level: 0 },
{ type: 'folder', text: '*', level: 0 },
],
},
{
description: 'trim leading/trailing whitespace in text',
text: `+ Documents
- Downloads
* README.md `,
expected: [
{ type: 'folder', text: 'Documents', level: 0 },
{ type: 'subfolder', text: 'Downloads', level: 0 },
{ type: 'file', text: 'README.md', level: 0 },
],
},
{
description: 'recognize symbols with space but no text',
text: `+
-
* `,
expected: [
{ type: 'folder', text: '', level: 0 },
{ type: 'subfolder', text: '', level: 0 },
{ type: 'file', text: '', level: 0 },
],
},
{
description:
'handle lines starting with symbols but not followed by space, as folder and plain text',
text: `+Documents
-Downloads
*README.md`,
expected: [
{ type: 'folder', text: '+Documents', level: 0 },
{ type: 'folder', text: '-Downloads', level: 0 },
{ type: 'folder', text: '*README.md', level: 0 },
],
},
])('should $description', ({ text, expected }) => {
// Arrange

// Act
const result = parseFileTreeText(text);

// Assert
expect(result).toEqual(expected);
});
});

describe('Large indentation values', () => {
it('should handle 27 spaces creating level 9 indentation', () => {
// Arrange
const spaces = ' '; // 27 spaces

// Act
const text = `${spaces}+ Deep folder`;
const result = parseFileTreeText(text);

// Assert
expect(result[0].level).toBe(9);
});
});
});
Loading