Skip to content

Commit 276b7f2

Browse files
authored
Merge branch 'develop' into nityam/fix-aria-live-login-error-#3874
2 parents f7d7449 + bc88c20 commit 276b7f2

File tree

8 files changed

+491
-45
lines changed

8 files changed

+491
-45
lines changed

client/modules/IDE/actions/project.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,12 @@ export function changeVisibility(projectId, projectName, visibility, t) {
420420
.patch('/project/visibility', { projectId, visibility })
421421
.then((response) => {
422422
if (response.status === 200) {
423-
const { visibility: newVisibility, updatedAt } = response.data;
423+
const { visibility: newVisibility, updatedAt, id } = response.data;
424424

425425
dispatch({
426426
type: ActionTypes.CHANGE_VISIBILITY,
427427
payload: {
428-
id: response.data.id,
428+
id,
429429
visibility: newVisibility
430430
}
431431
});
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import files from './files';
2+
import * as ActionTypes from '../../../constants';
3+
4+
// Helper to build test state without dispatching actions
5+
// Keeps DELETE_FILE tests isolated
6+
function createTestState(fileDescriptors) {
7+
return fileDescriptors.map((f) => ({
8+
id: f.id,
9+
_id: f.id,
10+
name: f.name,
11+
content: f.content ?? '',
12+
fileType: f.fileType ?? 'file',
13+
children: f.children ?? [],
14+
parentId: f.parentId ?? null
15+
}));
16+
}
17+
18+
describe('files reducer', () => {
19+
describe('DELETE_FILE action', () => {
20+
it('removes file and updates parent children array', () => {
21+
const state = createTestState([
22+
{
23+
id: 'root',
24+
name: 'root',
25+
fileType: 'folder',
26+
children: ['sketch', 'html', 'css']
27+
},
28+
{
29+
id: 'sketch',
30+
name: 'sketch.js',
31+
content: 'draw()',
32+
parentId: 'root'
33+
},
34+
{ id: 'html', name: 'index.html', content: '<html>', parentId: 'root' },
35+
{ id: 'css', name: 'style.css', content: 'body {}', parentId: 'root' }
36+
]);
37+
38+
const action = {
39+
type: ActionTypes.DELETE_FILE,
40+
id: 'sketch',
41+
parentId: 'root'
42+
};
43+
44+
const newState = files(state, action);
45+
46+
// File should be removed from state
47+
expect(newState.find((f) => f.id === 'sketch')).toBeUndefined();
48+
expect(newState).toHaveLength(state.length - 1);
49+
50+
// Parent's children array should be updated
51+
const root = newState.find((f) => f.id === 'root');
52+
expect(root.children).not.toContain('sketch');
53+
expect(root.children).toHaveLength(2);
54+
55+
// Siblings should still exist with unchanged content
56+
const htmlFile = newState.find((f) => f.id === 'html');
57+
const cssFile = newState.find((f) => f.id === 'css');
58+
expect(htmlFile).toBeDefined();
59+
expect(cssFile).toBeDefined();
60+
expect(htmlFile.content).toBe('<html>');
61+
expect(htmlFile.name).toBe('index.html');
62+
expect(cssFile.content).toBe('body {}');
63+
});
64+
65+
it('recursively deletes folder and all descendants', () => {
66+
const state = createTestState([
67+
{
68+
id: 'root',
69+
name: 'root',
70+
fileType: 'folder',
71+
children: ['components']
72+
},
73+
{
74+
id: 'components',
75+
name: 'components',
76+
fileType: 'folder',
77+
children: ['button', 'input'],
78+
parentId: 'root'
79+
},
80+
{ id: 'button', name: 'Button.jsx', parentId: 'components' },
81+
{ id: 'input', name: 'Input.jsx', parentId: 'components' }
82+
]);
83+
84+
const action = {
85+
type: ActionTypes.DELETE_FILE,
86+
id: 'components',
87+
parentId: 'root'
88+
};
89+
90+
const newState = files(state, action);
91+
92+
// All three items should be deleted
93+
expect(newState.find((f) => f.id === 'components')).toBeUndefined();
94+
expect(newState.find((f) => f.id === 'button')).toBeUndefined();
95+
expect(newState.find((f) => f.id === 'input')).toBeUndefined();
96+
expect(newState).toHaveLength(state.length - 3);
97+
98+
// Parent cleanup
99+
const root = newState.find((f) => f.id === 'root');
100+
expect(root.children).not.toContain('components');
101+
102+
// No orphaned files, every non root file should be referenced in some parent's children array
103+
const referencedIds = new Set();
104+
newState.forEach((file) => {
105+
if (file.children) {
106+
file.children.forEach((childId) => referencedIds.add(childId));
107+
}
108+
});
109+
110+
const nonRootFiles = newState.filter((f) => f.name !== 'root');
111+
nonRootFiles.forEach((file) => {
112+
expect(referencedIds.has(file.id)).toBe(true);
113+
});
114+
});
115+
116+
it('handles deeply nested folder hierarchies', () => {
117+
const state = createTestState([
118+
{
119+
id: 'root',
120+
name: 'root',
121+
fileType: 'folder',
122+
children: ['src']
123+
},
124+
{
125+
id: 'src',
126+
name: 'src',
127+
fileType: 'folder',
128+
children: ['utils'],
129+
parentId: 'root'
130+
},
131+
{
132+
id: 'utils',
133+
name: 'utils',
134+
fileType: 'folder',
135+
children: ['helper'],
136+
parentId: 'src'
137+
},
138+
{ id: 'helper', name: 'helper.js', parentId: 'utils' }
139+
]);
140+
141+
const action = {
142+
type: ActionTypes.DELETE_FILE,
143+
id: 'src',
144+
parentId: 'root'
145+
};
146+
147+
const newState = files(state, action);
148+
149+
// All three nested items should be deleted
150+
expect(newState.find((f) => f.id === 'src')).toBeUndefined();
151+
expect(newState.find((f) => f.id === 'utils')).toBeUndefined();
152+
expect(newState.find((f) => f.id === 'helper')).toBeUndefined();
153+
expect(newState).toHaveLength(state.length - 3);
154+
});
155+
156+
it('handles empty folder deletion', () => {
157+
const state = createTestState([
158+
{
159+
id: 'root',
160+
name: 'root',
161+
fileType: 'folder',
162+
children: ['docs']
163+
},
164+
{
165+
id: 'docs',
166+
name: 'docs',
167+
fileType: 'folder',
168+
children: [],
169+
parentId: 'root'
170+
}
171+
]);
172+
173+
const action = {
174+
type: ActionTypes.DELETE_FILE,
175+
id: 'docs',
176+
parentId: 'root'
177+
};
178+
179+
const newState = files(state, action);
180+
181+
// Folder should be removed
182+
expect(newState.find((f) => f.id === 'docs')).toBeUndefined();
183+
184+
// Parent should be updated
185+
const root = newState.find((f) => f.id === 'root');
186+
expect(root.children).not.toContain('docs');
187+
});
188+
});
189+
});

client/modules/IDE/reducers/projects.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,30 @@ const sketches = (state = [], action) => {
77
case ActionTypes.DELETE_PROJECT:
88
return state.projects.filter((sketch) => sketch.id !== action.id);
99
case ActionTypes.CHANGE_VISIBILITY: {
10-
return state.map((sketch) => {
11-
if (sketch.id === action.payload.id) {
12-
return { ...sketch, visibility: action.payload.visibility };
13-
}
14-
return sketch;
15-
});
10+
const updatedProjects = state.projects.map((sketch) =>
11+
sketch.id === action.payload.id
12+
? { ...sketch, visibility: action.payload.visibility }
13+
: sketch
14+
);
15+
16+
return {
17+
...state,
18+
projects: updatedProjects
19+
};
1620
}
21+
1722
case ActionTypes.RENAME_PROJECT: {
18-
return state.map((sketch) => {
23+
const updatedproject = state.projects.map((sketch) => {
1924
if (sketch.id === action.payload.id) {
2025
return { ...sketch, name: action.payload.name };
2126
}
2227
return sketch;
2328
});
29+
30+
return {
31+
...state,
32+
projects: updatedproject
33+
};
2434
}
2535
default:
2636
return state;

common/p5Versions.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export const currentP5Version = '1.11.11'; // Don't update to 2.x until 2026
55
// JSON.stringify([...document.querySelectorAll('._132722c7')].map(n => n.innerText), null, 2)
66
// TODO: use their API for this to grab these at build time?
77
export const p5Versions = [
8-
{ version: '2.2.1', label: '(Beta)' },
8+
{ version: '2.2.2', label: '(Beta)' },
9+
'2.2.1',
910
'2.2.0',
1011
'2.1.2',
1112
'2.1.1',

0 commit comments

Comments
 (0)