Skip to content

Commit 86ae359

Browse files
authored
Merge branch 'develop' into fix/User-Model
2 parents 0fa9d75 + 3363148 commit 86ae359

35 files changed

Lines changed: 1656 additions & 103 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent, waitFor } from '../../test-utils';
3+
import { DropdownMenu, DropdownMenuAlignment } from './DropdownMenu';
4+
5+
describe('DropdownMenu', () => {
6+
const renderDropdown = () => {
7+
render(
8+
<DropdownMenu
9+
aria-label="More options"
10+
align={DropdownMenuAlignment.RIGHT}
11+
>
12+
<li role="menuitem">Item One</li>
13+
<li role="menuitem">Item Two</li>
14+
<li role="menuitem">Item Three</li>
15+
</DropdownMenu>
16+
);
17+
};
18+
19+
it('should render the dropdown button', () => {
20+
renderDropdown();
21+
22+
const button = screen.getByRole('button', { name: 'More options' });
23+
expect(button).toBeInTheDocument();
24+
25+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
26+
});
27+
28+
it('should open dropdown and render menu items when button is clicked', () => {
29+
renderDropdown();
30+
31+
const button = screen.getByRole('button', { name: 'More options' });
32+
33+
fireEvent.click(button);
34+
35+
expect(screen.getByRole('menu')).toBeInTheDocument();
36+
37+
expect(screen.getByText('Item One')).toBeInTheDocument();
38+
expect(screen.getByText('Item Two')).toBeInTheDocument();
39+
expect(screen.getByText('Item Three')).toBeInTheDocument();
40+
});
41+
42+
it('should close the menu after selecting an item', async () => {
43+
renderDropdown();
44+
45+
const button = screen.getByRole('button', { name: 'More options' });
46+
fireEvent.click(button);
47+
48+
const item = screen.getByText('Item One');
49+
fireEvent.mouseUp(item);
50+
51+
await waitFor(() => {
52+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
53+
});
54+
});
55+
});

client/i18n.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
uk,
1919
sv,
2020
tr,
21-
enIN
21+
enIN,
22+
ne
2223
} from 'date-fns/locale';
2324

2425
import { getPreferredLanguage } from './utils/language-utils';
@@ -41,7 +42,8 @@ export const availableLanguages = [
4142
'zh-CN',
4243
'zh-TW',
4344
'tr',
44-
'ur'
45+
'ur',
46+
'ne'
4547
];
4648

4749
const detectedLanguage = getPreferredLanguage(
@@ -76,7 +78,8 @@ export function languageKeyToLabel(lang) {
7678
'zh-CN': '简体中文',
7779
'zh-TW': '正體中文',
7880
tr: 'Türkçe',
79-
ur: 'اردو'
81+
ur: 'اردو',
82+
ne: 'नेपाली'
8083
};
8184
return languageMap[lang];
8285
}
@@ -98,7 +101,8 @@ export function languageKeyToDateLocale(lang) {
98101
'zh-CN': zhCN,
99102
'zh-TW': zhTW,
100103
tr,
101-
ur: enIN
104+
ur: enIN,
105+
ne
102106
};
103107
return languageMap[lang];
104108
}

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
});

client/modules/IDE/actions/uploader.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ export function dropzoneCompleteCallback(file) {
8282
content: file.content
8383
};
8484
dispatch(handleCreateFile(formParams, false));
85+
} else if (file.status === 'error' || file.xhr.status >= 400) {
86+
let uploadFileErrorMessage = 'Uploading file to AWS failed.';
87+
if (file.xhr?.response) {
88+
const parser = new DOMParser();
89+
const xmlDoc = parser.parseFromString(file.xhr.response, 'text/xml');
90+
const message = xmlDoc.getElementsByTagName('Message')[0]?.textContent;
91+
const code = xmlDoc.getElementsByTagName('Code')[0]?.textContent;
92+
uploadFileErrorMessage = `${code}: ${message}`;
93+
}
94+
file.previewElement.classList.add('dz-error');
95+
file.previewElement.classList.remove('dz-success');
96+
const dzErrorMessageElement = file.previewElement?.querySelector(
97+
'[data-dz-errormessage]'
98+
);
99+
if (dzErrorMessageElement) {
100+
dzErrorMessageElement.textContent = uploadFileErrorMessage;
101+
}
85102
}
86103
};
87104
}

client/modules/IDE/components/FileUploader.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,36 @@ const StyledUploader = styled.div`
2323
.dz-preview.dz-image-preview {
2424
background-color: transparent;
2525
}
26+
.dz-image img {
27+
width: 100%;
28+
height: auto;
29+
}
30+
.dz-details .dz-filename:hover {
31+
overflow: hidden;
32+
}
33+
.dz-error-message span {
34+
width: 100%;
35+
height: auto;
36+
overflow: hidden;
37+
display: block;
38+
}
39+
.dz-error-mark:hover {
40+
cursor: pointer !important;
41+
}
2642
`;
2743

2844
function FileUploader() {
2945
const { t } = useTranslation();
3046
const dispatch = useDispatch();
3147
const userId = useSelector((state) => state.user.id);
32-
48+
const deleteUploadErrorFiles = (uploader, file) => {
49+
if (file.status === 'error') {
50+
file.previewElement.addEventListener('click', (e) => {
51+
e.stopPropagation();
52+
uploader.removeFile(file);
53+
});
54+
}
55+
};
3356
useEffect(() => {
3457
const uploader = new Dropzone('div#uploader', {
3558
url: s3BucketHttps,
@@ -52,6 +75,7 @@ function FileUploader() {
5275
});
5376
uploader.on('complete', (file) => {
5477
dispatch(dropzoneCompleteCallback(file));
78+
deleteUploadErrorFiles(uploader, file);
5579
});
5680
return () => {
5781
uploader.destroy();

client/modules/IDE/components/NewFileForm.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ function NewFileForm() {
6464
</Field>
6565
</div>
6666
{touched.submitButton && errors.name && (
67-
<span className="form-error">{errors.name}</span>
67+
<span className="form-error" aria-live="polite">
68+
{errors.name}
69+
</span>
6870
)}
6971
</form>
7072
)}

client/modules/IDE/components/NewFolderForm.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ function NewFolderForm() {
6161
</Field>
6262
</div>
6363
{touched.submitButton && errors.name && (
64-
<span className="form-error">{errors.name}</span>
64+
<span className="form-error" aria-live="polite">
65+
{errors.name}
66+
</span>
6567
)}
6668
</form>
6769
)}
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+
});

0 commit comments

Comments
 (0)