mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge f3e20dd820 into e19048f5dd
This commit is contained in:
commit
524f48ea91
13 changed files with 860 additions and 287 deletions
113
client/package-lock.json
generated
113
client/package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
|
@ -50,6 +51,7 @@
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-latex": "^2.0.3",
|
"@types/react-latex": "^2.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||||
|
|
@ -4512,6 +4514,15 @@
|
||||||
"@types/unist": "*"
|
"@types/unist": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
||||||
|
"integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||||
|
|
@ -4651,6 +4662,15 @@
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
|
||||||
|
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-dom": {
|
"node_modules/@types/react-dom": {
|
||||||
"version": "18.3.5",
|
"version": "18.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
||||||
|
|
@ -4671,6 +4691,17 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-redux": {
|
||||||
|
"version": "7.1.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||||
|
"integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hoist-non-react-statics": "^3.3.0",
|
||||||
|
"@types/react": "*",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"redux": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-transition-group": {
|
"node_modules/@types/react-transition-group": {
|
||||||
"version": "4.4.12",
|
"version": "4.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||||
|
|
@ -5918,6 +5949,14 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-box-model": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css.escape": {
|
"node_modules/css.escape": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
|
|
@ -9871,6 +9910,11 @@
|
||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
|
@ -11119,6 +11163,11 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/raf-schd": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
|
|
@ -11131,6 +11180,25 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-beautiful-dnd": {
|
||||||
|
"version": "13.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||||
|
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
|
||||||
|
"deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.9.2",
|
||||||
|
"css-box-model": "^1.2.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"raf-schd": "^4.0.2",
|
||||||
|
"react-redux": "^7.2.0",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"use-memo-one": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
|
@ -11172,6 +11240,35 @@
|
||||||
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
|
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "7.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||||
|
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.15.4",
|
||||||
|
"@types/react-redux": "^7.1.20",
|
||||||
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^17.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.3 || ^17 || ^18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-redux/node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.30.0",
|
"version": "6.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
|
||||||
|
|
@ -11247,6 +11344,14 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
|
|
@ -12778,6 +12883,14 @@
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-memo-one": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "11.1.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-modal": "^3.16.3",
|
"react-modal": "^3.16.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
|
|
@ -54,6 +55,7 @@
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/react-latex": "^2.0.3",
|
"@types/react-latex": "^2.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,78 @@
|
||||||
// Editor.test.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, fireEvent, screen } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import Editor from 'src/components/Editor/Editor';
|
import Editor from '../../../components/Editor/newEditor';
|
||||||
|
|
||||||
describe('Editor Component', () => {
|
describe('Editor Component', () => {
|
||||||
const mockOnEditorChange = jest.fn();
|
const mockOnValuesChange = jest.fn();
|
||||||
|
const mockOnFocusQuestion = jest.fn();
|
||||||
|
|
||||||
const sampleProps = {
|
const sampleProps = {
|
||||||
label: 'Sample Label',
|
label: 'Sample Label',
|
||||||
initialValue: 'Sample Initial Value',
|
values: ['Question 1', 'Question 2'],
|
||||||
onEditorChange: mockOnEditorChange
|
onValuesChange: mockOnValuesChange,
|
||||||
|
onFocusQuestion: mockOnFocusQuestion,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockOnValuesChange.mockClear();
|
||||||
|
mockOnFocusQuestion.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the label correctly', () => {
|
||||||
render(<Editor {...sampleProps} />);
|
render(<Editor {...sampleProps} />);
|
||||||
|
const label = screen.getByText('Sample Label');
|
||||||
|
expect(label).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with initial value', () => {
|
test('renders the correct number of questions', () => {
|
||||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
render(<Editor {...sampleProps} />);
|
||||||
expect(editorTextarea).toBeInTheDocument();
|
const questions = screen.getAllByRole('textbox');
|
||||||
expect(editorTextarea.value).toBe('Sample Initial Value');
|
expect(questions.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onEditorChange callback when editor value changes', () => {
|
test('calls onValuesChange with updated values when a question is changed', () => {
|
||||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
render(<Editor {...sampleProps} />);
|
||||||
fireEvent.change(editorTextarea, { target: { value: 'Updated Value' } });
|
const questionInput = screen.getAllByRole('textbox')[0];
|
||||||
expect(mockOnEditorChange).toHaveBeenCalledWith('Updated Value');
|
fireEvent.change(questionInput, { target: { value: 'Updated Question 1' } });
|
||||||
|
expect(mockOnValuesChange).toHaveBeenCalledWith(['Updated Question 1', 'Question 2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates editor value when initialValue prop changes', () => {
|
test('calls onValuesChange with updated values when an empty question is deleted', () => {
|
||||||
const updatedProps = {
|
const sampleProps = {
|
||||||
label: 'Updated Label',
|
label: 'Test Editor',
|
||||||
initialValue: 'Updated Initial Value',
|
values: [''],
|
||||||
onEditorChange: mockOnEditorChange
|
onValuesChange: mockOnValuesChange,
|
||||||
};
|
};
|
||||||
|
render(<Editor {...sampleProps} />);
|
||||||
render(<Editor {...updatedProps} />);
|
const deleteButton = screen.getAllByLabelText('delete')[0];
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
expect(mockOnValuesChange).toHaveBeenCalledWith([]);
|
||||||
const editorTextarea = editorTextareas[1];
|
|
||||||
|
|
||||||
expect(editorTextarea.value).toBe('Updated Initial Value');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call change text with the correct value on textarea change', () => {
|
test('renders delete buttons for each question', () => {
|
||||||
const updatedProps = {
|
render(<Editor {...sampleProps} />);
|
||||||
label: 'Updated Label',
|
const deleteButtons = screen.getAllByLabelText('delete');
|
||||||
initialValue: 'Updated Initial Value',
|
expect(deleteButtons.length).toBe(2);
|
||||||
onEditorChange: mockOnEditorChange
|
|
||||||
};
|
|
||||||
|
|
||||||
render(<Editor {...updatedProps} />);
|
|
||||||
|
|
||||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
|
||||||
const editorTextarea = editorTextareas[1];
|
|
||||||
fireEvent.change(editorTextarea, { target: { value: 'New value' } });
|
|
||||||
|
|
||||||
expect(editorTextarea.value).toBe('New value');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call onEditorChange with an empty string if textarea value is falsy', () => {
|
test('calls onFocusQuestion with correct index when focus button is clicked', () => {
|
||||||
const updatedProps = {
|
render(<Editor {...sampleProps} />);
|
||||||
label: 'Updated Label',
|
const focusButton = screen.getAllByLabelText('focus question')[1];
|
||||||
initialValue: 'Updated Initial Value',
|
fireEvent.click(focusButton);
|
||||||
onEditorChange: mockOnEditorChange
|
expect(mockOnFocusQuestion).toHaveBeenCalledWith(1);
|
||||||
};
|
|
||||||
|
|
||||||
render(<Editor {...updatedProps} />);
|
|
||||||
|
|
||||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
|
||||||
const editorTextarea = editorTextareas[1];
|
|
||||||
fireEvent.change(editorTextarea, { target: { value: '' } });
|
|
||||||
|
|
||||||
expect(editorTextarea.value).toBe('');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders focus buttons for each question', () => {
|
||||||
|
render(<Editor {...sampleProps} />);
|
||||||
|
const focusButtons = screen.getAllByLabelText('focus question');
|
||||||
|
expect(focusButtons.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
test('does not throw error when onFocusQuestion is not provided', () => {
|
||||||
|
const { onFocusQuestion, ...propsWithoutFocus } = sampleProps;
|
||||||
|
render(<Editor {...propsWithoutFocus} />);
|
||||||
|
const focusButton = screen.getAllByLabelText('focus question')[0];
|
||||||
|
expect(() => fireEvent.click(focusButton)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,46 +1,148 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import ReturnButton from '../../../components/ReturnButton/ReturnButton.tsx'; // Adjust path as needed
|
||||||
|
import ApiService from '../../../services/ApiService';
|
||||||
|
|
||||||
|
// Mock react-router-dom's useNavigate
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useNavigate: jest.fn()
|
useNavigate: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ReturnButton', () => {
|
// Mock ApiService
|
||||||
test('navigates back when askConfirm is false', () => {
|
jest.mock('../../../services/ApiService', () => ({
|
||||||
const navigateMock = jest.fn();
|
createQuiz: jest.fn(),
|
||||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
updateQuiz: jest.fn(),
|
||||||
render(<ReturnButton askConfirm={false} />);
|
}));
|
||||||
fireEvent.click(screen.getByText('Retour'));
|
|
||||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
describe('ReturnButton Component', () => {
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
|
const mockOnReturn = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks before each test
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
|
||||||
|
(ApiService.createQuiz as jest.Mock).mockResolvedValue({});
|
||||||
|
(ApiService.updateQuiz as jest.Mock).mockResolvedValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows confirmation modal when askConfirm is true', () => {
|
test('renders the button with "Retour" text when not saving', () => {
|
||||||
render(<ReturnButton askConfirm={true} />);
|
render(<ReturnButton />);
|
||||||
fireEvent.click(screen.getByText('Retour'));
|
expect(screen.getByText('Retour')).toBeInTheDocument();
|
||||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
|
||||||
expect(confirmButton).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*test('navigates back after confirming in the modal', () => {
|
test('renders "Enregistrement..." text when saving', async () => {
|
||||||
const navigateMock = jest.fn();
|
render(<ReturnButton quizTitle="Test Quiz" quizFolder="folder1" isNewQuiz />);
|
||||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
|
||||||
render(<ReturnButton askConfirm={true} />);
|
|
||||||
fireEvent.click(screen.getByText('Retour'));
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
expect(screen.getByText('Enregistrement...')).toBeInTheDocument();
|
||||||
fireEvent.click(confirmButton);
|
await waitFor(() => expect(screen.queryByText('Enregistrement...')).not.toBeInTheDocument());
|
||||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
});
|
||||||
});*/
|
|
||||||
|
|
||||||
test('cancels navigation when canceling in the modal', () => {
|
test('navigates to /teacher/dashboard by default when clicked', async () => {
|
||||||
const navigateMock = jest.fn();
|
render(<ReturnButton />);
|
||||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
|
||||||
render(<ReturnButton askConfirm={true} />);
|
|
||||||
fireEvent.click(screen.getByText('Retour'));
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
fireEvent.click(screen.getByText('Annuler'));
|
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'));
|
||||||
expect(navigateMock).not.toHaveBeenCalled();
|
});
|
||||||
|
|
||||||
|
test('calls onReturn prop instead of navigating when provided', async () => {
|
||||||
|
render(<ReturnButton onReturn={mockOnReturn} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockOnReturn).toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disables button while saving', async () => {
|
||||||
|
render(<ReturnButton quizTitle="Test Quiz" quizFolder="folder1" isNewQuiz />);
|
||||||
|
const button = screen.getByText('Retour');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
await waitFor(() => expect(button).not.toBeDisabled());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls ApiService.createQuiz for new quiz with valid data', async () => {
|
||||||
|
const props = {
|
||||||
|
quizTitle: 'New Quiz',
|
||||||
|
quizContent: ['Q1', 'Q2'],
|
||||||
|
quizFolder: 'folder1',
|
||||||
|
isNewQuiz: true,
|
||||||
|
};
|
||||||
|
render(<ReturnButton {...props} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.createQuiz).toHaveBeenCalledWith('New Quiz', ['Q1', 'Q2'], 'folder1');
|
||||||
|
expect(ApiService.updateQuiz).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls ApiService.updateQuiz for existing quiz with valid data', async () => {
|
||||||
|
const props = {
|
||||||
|
quizId: 'quiz123',
|
||||||
|
quizTitle: 'Updated Quiz',
|
||||||
|
quizContent: ['Q1', 'Q2'],
|
||||||
|
isNewQuiz: false,
|
||||||
|
};
|
||||||
|
render(<ReturnButton {...props} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.updateQuiz).toHaveBeenCalledWith('quiz123', 'Updated Quiz', ['Q1', 'Q2']);
|
||||||
|
expect(ApiService.createQuiz).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not call ApiService if quizTitle is missing for new quiz', async () => {
|
||||||
|
render(<ReturnButton quizFolder="folder1" isNewQuiz />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.createQuiz).not.toHaveBeenCalled();
|
||||||
|
expect(ApiService.updateQuiz).not.toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not call ApiService if quizId and quizTitle are missing for update', async () => {
|
||||||
|
render(<ReturnButton />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.createQuiz).not.toHaveBeenCalled();
|
||||||
|
expect(ApiService.updateQuiz).not.toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates even if ApiService.createQuiz fails', async () => {
|
||||||
|
(ApiService.createQuiz as jest.Mock).mockRejectedValue(new Error('Save failed'));
|
||||||
|
const props = {
|
||||||
|
quizTitle: 'New Quiz',
|
||||||
|
quizContent: ['Q1'],
|
||||||
|
quizFolder: 'folder1',
|
||||||
|
isNewQuiz: true,
|
||||||
|
};
|
||||||
|
render(<ReturnButton {...props} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.createQuiz).toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates even if ApiService.updateQuiz fails', async () => {
|
||||||
|
(ApiService.updateQuiz as jest.Mock).mockRejectedValue(new Error('Update failed'));
|
||||||
|
const props = {
|
||||||
|
quizId: 'quiz123',
|
||||||
|
quizTitle: 'Updated Quiz',
|
||||||
|
quizContent: ['Q1'],
|
||||||
|
isNewQuiz: false,
|
||||||
|
};
|
||||||
|
render(<ReturnButton {...props} />);
|
||||||
|
fireEvent.click(screen.getByText('Retour'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ApiService.updateQuiz).toHaveBeenCalled();
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,55 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import QuizForm from '../../../../pages/Teacher/EditorQuiz/EditorQuiz';
|
import QuizForm from '../../../../pages/Teacher/EditorQuiz/EditorQuiz';
|
||||||
import { waitFor } from '@testing-library/react';
|
|
||||||
|
|
||||||
|
// Mock localStorage with proper TypeScript types
|
||||||
const localStorageMock = (() => {
|
const localStorageMock = (() => {
|
||||||
let store: Record<string, string> = {};
|
let store: Record<string, string> = {};
|
||||||
return {
|
return {
|
||||||
getItem: (key: string) => store[key] || null,
|
getItem: (key: string): string | null => store[key] || null,
|
||||||
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
setItem: (key: string, value: string): void => {
|
||||||
clear: () => (store = {}),
|
store[key] = value.toString();
|
||||||
|
},
|
||||||
|
clear: (): void => {
|
||||||
|
store = {};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||||
|
|
||||||
|
// Mock react-router-dom
|
||||||
|
const mockNavigate = jest.fn();
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...jest.requireActual('react-router-dom'),
|
...jest.requireActual('react-router-dom'),
|
||||||
useNavigate: jest.fn(),
|
useNavigate: () => mockNavigate,
|
||||||
|
useParams: () => ({ id: 'new' }), // Simulate the "new" route
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock ApiService
|
||||||
|
jest.mock('../../../../services/ApiService', () => ({
|
||||||
|
getUserFolders: jest.fn(() => Promise.resolve([])), // Mock empty folder list
|
||||||
|
getQuiz: jest.fn(),
|
||||||
|
createQuiz: jest.fn(),
|
||||||
|
updateQuiz: jest.fn(),
|
||||||
|
uploadImage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('QuizForm Component', () => {
|
describe('QuizForm Component', () => {
|
||||||
test('renders QuizForm with default state for a new quiz', () => {
|
test('renders QuizForm with default state for a new quiz', async () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/teacher/editor-quiz/new']}>
|
<MemoryRouter initialEntries={['/teacher/editor-quiz/new']}>
|
||||||
<QuizForm />
|
<QuizForm />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument();
|
// Wait for the component to render the title
|
||||||
// expect(screen.queryByText('Éditeur')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Prévisualisation')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.skip('renders QuizForm for a new quiz', async () => {
|
|
||||||
const { container } = render(
|
|
||||||
<MemoryRouter initialEntries={['/teacher/editor-quiz']}>
|
|
||||||
<QuizForm />
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/Éditeur de quiz/i)).toBeInTheDocument();
|
|
||||||
|
|
||||||
// find the 'editor' text area
|
|
||||||
const editorTextArea = container.querySelector('textarea.editor');
|
|
||||||
fireEvent.change(editorTextArea!, { target: { value: 'Sample question?' } });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const sampleQuestionElements = screen.queryAllByText(/Sample question\?/i);
|
expect(screen.getByText('Éditeur de Quiz')).toBeInTheDocument();
|
||||||
expect(sampleQuestionElements.length).toBeGreaterThan(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveButton = screen.getByText(/Enregistrer/i);
|
// Check for other expected elements
|
||||||
fireEvent.click(saveButton);
|
expect(screen.getByText('Prévisualisation')).toBeInTheDocument();
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/Sauvegarder le questionnaire/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
// Editor.tsx
|
import React, { useRef } from 'react';
|
||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import './editor.css';
|
import './editor.css';
|
||||||
import { TextareaAutosize } from '@mui/material';
|
import { TextareaAutosize } from '@mui/material';
|
||||||
|
|
||||||
interface EditorProps {
|
interface EditorProps {
|
||||||
label: string;
|
label: string;
|
||||||
initialValue: string;
|
values: string[];
|
||||||
onEditorChange: (value: string) => void;
|
onEditorChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label }) => {
|
const Editor: React.FC<EditorProps> = ({ label, values, onEditorChange }) => {
|
||||||
const [value, setValue] = useState(initialValue);
|
|
||||||
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
function handleEditorChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
function handleEditorChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||||
const text = event.target.value;
|
const text = event.target.value;
|
||||||
setValue(text);
|
|
||||||
onEditorChange(text || '');
|
onEditorChange(text || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +23,7 @@ const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label })
|
||||||
id="editor-textarea"
|
id="editor-textarea"
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
value={value}
|
value={values}
|
||||||
className="editor"
|
className="editor"
|
||||||
minRows={5}
|
minRows={5}
|
||||||
/>
|
/>
|
||||||
|
|
@ -34,4 +31,4 @@ const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label })
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
|
@ -6,4 +6,4 @@
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
9
client/src/components/Editor/newEditor.css
Normal file
9
client/src/components/Editor/newEditor.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 50vh;
|
||||||
|
background-color: #f8f9ff;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: medium;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
203
client/src/components/Editor/newEditor.tsx
Normal file
203
client/src/components/Editor/newEditor.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TextField, Typography, IconButton, Box, Collapse, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
interface EditorProps {
|
||||||
|
label: string;
|
||||||
|
values: string[];
|
||||||
|
onValuesChange: (values: string[]) => void;
|
||||||
|
onFocusQuestion?: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQuestion }) => {
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean[]>(Array(values.length).fill(false));
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleChange = (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValues = [...values];
|
||||||
|
newValues[index] = event.target.value;
|
||||||
|
onValuesChange(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteQuestion = (index: number) => () => {
|
||||||
|
if (values[index].trim() === '') {
|
||||||
|
const newValues = values.filter((_, i) => i !== index);
|
||||||
|
onValuesChange(newValues);
|
||||||
|
setCollapsed((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
} else {
|
||||||
|
setDeleteIndex(index);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (deleteIndex !== null) {
|
||||||
|
const newValues = values.filter((_, i) => i !== deleteIndex);
|
||||||
|
onValuesChange(newValues);
|
||||||
|
setCollapsed((prev) => prev.filter((_, i) => i !== deleteIndex));
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDeleteIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelDelete = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDeleteIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocusQuestion = (index: number) => () => {
|
||||||
|
if (onFocusQuestion) {
|
||||||
|
onFocusQuestion(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleCollapse = (index: number) => () => {
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const newCollapsed = [...prev];
|
||||||
|
newCollapsed[index] = !newCollapsed[index];
|
||||||
|
return newCollapsed;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (result: any) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
|
||||||
|
const newValues = [...values];
|
||||||
|
const [reorderedItem] = newValues.splice(result.source.index, 1);
|
||||||
|
newValues.splice(result.destination.index, 0, reorderedItem);
|
||||||
|
onValuesChange(newValues);
|
||||||
|
|
||||||
|
const newCollapsed = [...collapsed];
|
||||||
|
const [reorderedCollapsed] = newCollapsed.splice(result.source.index, 1);
|
||||||
|
newCollapsed.splice(result.destination.index, 0, reorderedCollapsed);
|
||||||
|
setCollapsed(newCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (collapsed.length !== values.length) {
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const newCollapsed = [...prev];
|
||||||
|
while (newCollapsed.length < values.length) newCollapsed.push(false);
|
||||||
|
while (newCollapsed.length > values.length) newCollapsed.pop();
|
||||||
|
return newCollapsed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography variant="h6" fontWeight="bold" style={{ marginBottom: '24px' }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="questions">
|
||||||
|
{(provided) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{values.map((value, index) => (
|
||||||
|
<Draggable key={index} draggableId={`question-${index}`} index={index}>
|
||||||
|
{(provided) => (
|
||||||
|
<Box
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
sx={{
|
||||||
|
marginBottom: '8px',
|
||||||
|
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||||
|
padding: '16px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" style={{ marginBottom: '8px' }}>
|
||||||
|
Question {index + 1}
|
||||||
|
</Typography>
|
||||||
|
<Box>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleToggleCollapse(index)}
|
||||||
|
aria-label="toggle collapse"
|
||||||
|
sx={{
|
||||||
|
color: 'gray',
|
||||||
|
'&:hover': { color: 'blue' },
|
||||||
|
mr: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{collapsed[index] ? <ExpandMoreIcon /> : <ExpandLessIcon />}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleFocusQuestion(index)}
|
||||||
|
aria-label="focus question"
|
||||||
|
sx={{
|
||||||
|
color: 'gray',
|
||||||
|
'&:hover': { color: 'blue' },
|
||||||
|
mr: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisibilityIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleDeleteQuestion(index)}
|
||||||
|
aria-label="delete"
|
||||||
|
sx={{
|
||||||
|
color: 'light-gray',
|
||||||
|
'&:hover': { color: 'red' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Collapse in={!collapsed[index]}>
|
||||||
|
<TextField
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange(index)}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
minRows={4}
|
||||||
|
maxRows={Infinity}
|
||||||
|
variant="outlined"
|
||||||
|
style={{ overflow: 'auto' }}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
|
||||||
|
{/* Confirmation Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={handleCancelDelete}
|
||||||
|
aria-labelledby="delete-confirmation-title"
|
||||||
|
aria-describedby="delete-confirmation-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="delete-confirmation-title" sx={{ textAlign: 'center'}}>Suppression</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="delete-confirmation-description">
|
||||||
|
Confirmez vous la suppression de Question {deleteIndex !== null ? deleteIndex + 1 : ''} ?
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ justifyContent: 'center', pb: 2 }}>
|
||||||
|
<Button onClick={handleCancelDelete} color="primary" sx={{ mx: 1 }}>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleConfirmDelete} color="error" sx={{ mx: 1 }} autoFocus>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Editor;
|
||||||
|
|
@ -12,43 +12,47 @@ interface GIFTTemplatePreviewProps {
|
||||||
|
|
||||||
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
questions,
|
questions,
|
||||||
hideAnswers = false
|
hideAnswers = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isPreviewReady, setIsPreviewReady] = useState(false);
|
const [isPreviewReady, setIsPreviewReady] = useState(false);
|
||||||
const [items, setItems] = useState('');
|
const [questionItems, setQuestionItems] = useState<string[]>([]); // Array of HTML strings for each question
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
let previewHTML = '';
|
const previewItems: string[] = [];
|
||||||
questions.forEach((giftQuestion) => {
|
questions.forEach((giftQuestion) => {
|
||||||
try {
|
try {
|
||||||
const question = parse(giftQuestion);
|
const question = parse(giftQuestion);
|
||||||
previewHTML += Template(question[0], {
|
const html = Template(question[0], {
|
||||||
preview: true,
|
preview: true,
|
||||||
theme: 'light'
|
theme: 'light',
|
||||||
});
|
});
|
||||||
|
previewItems.push(html);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMsg: string;
|
let errorMsg: string;
|
||||||
if (error instanceof UnsupportedQuestionTypeError) {
|
if (error instanceof UnsupportedQuestionTypeError) {
|
||||||
errorMsg = ErrorTemplate(giftQuestion, `Erreur: ${error.message}`);
|
errorMsg = ErrorTemplate(giftQuestion, `Erreur: ${error.message}`);
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`);
|
errorMsg = ErrorTemplate(giftQuestion, `Erreur GIFT: ${error.message}`);
|
||||||
} else {
|
} else {
|
||||||
errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
|
errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
|
||||||
}
|
}
|
||||||
previewHTML += `<div label="error-message">${errorMsg}</div>`;
|
previewItems.push(`<div label="error-message">${errorMsg}</div>`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hideAnswers) {
|
if (hideAnswers) {
|
||||||
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
|
previewItems.forEach((item, index) => {
|
||||||
previewHTML = previewHTML.replace(svgRegex, '');
|
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
|
||||||
const placeholderRegex = /(placeholder=")[^"]*(")/gi;
|
const placeholderRegex = /(placeholder=")[^"]*(")/gi;
|
||||||
previewHTML = previewHTML.replace(placeholderRegex, '$1$2');
|
previewItems[index] = item
|
||||||
|
.replace(svgRegex, '')
|
||||||
|
.replace(placeholderRegex, '$1$2');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setItems(previewHTML);
|
setQuestionItems(previewItems);
|
||||||
setIsPreviewReady(true);
|
setIsPreviewReady(true);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
@ -57,7 +61,7 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
setError('Une erreur est survenue durant le chargement de la prévisualisation.');
|
setError('Une erreur est survenue durant le chargement de la prévisualisation.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [questions]);
|
}, [questions, hideAnswers]);
|
||||||
|
|
||||||
const PreviewComponent = () => (
|
const PreviewComponent = () => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
@ -65,8 +69,13 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
<div className="error">{error}</div>
|
<div className="error">{error}</div>
|
||||||
) : isPreviewReady ? (
|
) : isPreviewReady ? (
|
||||||
<div data-testid="preview-container">
|
<div data-testid="preview-container">
|
||||||
|
{questionItems.map((item, index) => (
|
||||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
|
<div
|
||||||
|
key={index}
|
||||||
|
className="question-item"
|
||||||
|
dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: item }) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="loading">Chargement de la prévisualisation...</div>
|
<div className="loading">Chargement de la prévisualisation...</div>
|
||||||
|
|
@ -77,4 +86,4 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||||
return <PreviewComponent />;
|
return <PreviewComponent />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GIFTTemplatePreview;
|
export default GIFTTemplatePreview;
|
||||||
|
|
@ -1,65 +1,69 @@
|
||||||
// GoBackButton.tsx
|
import React, { useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { ChevronLeft } from '@mui/icons-material';
|
import { ChevronLeft } from '@mui/icons-material';
|
||||||
|
import ApiService from '../../services/ApiService'; // Assuming this is where save logic lives
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onReturn?: () => void;
|
onReturn?: () => void;
|
||||||
askConfirm?: boolean;
|
quizTitle?: string; // Quiz title to save
|
||||||
message?: string;
|
quizContent?: string[]; // Quiz content to save
|
||||||
|
quizFolder?: string; // Folder ID to save
|
||||||
|
quizId?: string; // Quiz ID for updates (optional)
|
||||||
|
isNewQuiz?: boolean; // Flag to determine create or update
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReturnButton: React.FC<Props> = ({
|
const ReturnButton: React.FC<Props> = ({
|
||||||
askConfirm = false,
|
onReturn,
|
||||||
message = 'Êtes-vous sûr de vouloir quitter la page ?',
|
quizTitle = '',
|
||||||
onReturn
|
quizContent = [],
|
||||||
|
quizFolder = '',
|
||||||
|
quizId,
|
||||||
|
isNewQuiz = false,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [isSaving, setIsSaving] = useState(false); // Optional: to show saving state
|
||||||
|
|
||||||
const handleOnReturnButtonClick = () => {
|
const handleOnReturnButtonClick = async () => {
|
||||||
if (askConfirm) {
|
setIsSaving(true);
|
||||||
setShowDialog(true);
|
try {
|
||||||
} else {
|
// Automatically save the quiz
|
||||||
|
if (isNewQuiz && quizTitle && quizFolder) {
|
||||||
|
await ApiService.createQuiz(quizTitle, quizContent, quizFolder);
|
||||||
|
} else if (quizId && quizTitle) {
|
||||||
|
await ApiService.updateQuiz(quizId, quizTitle, quizContent);
|
||||||
|
}
|
||||||
|
// If no title/folder, proceed without saving (optional behavior)
|
||||||
handleOnReturn();
|
handleOnReturn();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving quiz on return:', error);
|
||||||
|
// Still navigate even if save fails, to avoid trapping the user
|
||||||
|
handleOnReturn();
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = () => {
|
|
||||||
setShowDialog(false);
|
|
||||||
handleOnReturn();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnReturn = () => {
|
const handleOnReturn = () => {
|
||||||
if (onReturn) {
|
if (onReturn) {
|
||||||
onReturn();
|
onReturn();
|
||||||
} else {
|
} else {
|
||||||
navigate(-1);
|
navigate('/teacher/dashboard'); // Navigate to dashboard instead of -1
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='returnButton'>
|
<div className="returnButton">
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
startIcon={<ChevronLeft />}
|
startIcon={<ChevronLeft />}
|
||||||
onClick={handleOnReturnButtonClick}
|
onClick={handleOnReturnButtonClick}
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ marginLeft: '-0.5rem', fontSize: 16 }}
|
sx={{ marginLeft: '-0.5rem', fontSize: 16 }}
|
||||||
|
disabled={isSaving} // Disable button while saving
|
||||||
>
|
>
|
||||||
Retour
|
{isSaving ? 'Enregistrement...' : 'Retour'}
|
||||||
</Button>
|
</Button>
|
||||||
<ConfirmDialog
|
|
||||||
open={showDialog}
|
|
||||||
title="Confirmer"
|
|
||||||
message={message}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
onCancel={() => setShowDialog(false)}
|
|
||||||
buttonOrderType="warning"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,27 @@
|
||||||
// EditorQuiz.tsx
|
import React, { useState, useEffect, useRef, CSSProperties } from 'react';
|
||||||
import React, { useState, useEffect, CSSProperties } from 'react';
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { FolderType } from '../../../Types/FolderType';
|
import { FolderType } from '../../../Types/FolderType';
|
||||||
|
|
||||||
|
import NewEditor from 'src/components/Editor/newEditor';
|
||||||
import Editor from 'src/components/Editor/Editor';
|
import Editor from 'src/components/Editor/Editor';
|
||||||
|
|
||||||
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
import GiftCheatSheet from 'src/components/GIFTCheatSheet/GiftCheatSheet';
|
||||||
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview';
|
||||||
|
|
||||||
import { QuizType } from '../../../Types/QuizType';
|
import { QuizType } from '../../../Types/QuizType';
|
||||||
|
|
||||||
import SaveIcon from '@mui/icons-material/Save';
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import './editorQuiz.css';
|
import './EditorQuiz.css';
|
||||||
import { Button, TextField, NativeSelect, Divider } from '@mui/material';
|
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent, MenuItem, Select, Snackbar } from '@mui/material';
|
||||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||||
import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal';
|
import ImageGalleryModal from 'src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal';
|
||||||
|
|
||||||
import ApiService from '../../../services/ApiService';
|
import ApiService from '../../../services/ApiService';
|
||||||
import { escapeForGIFT } from '../../../utils/giftUtils';
|
import { escapeForGIFT } from '../../../utils/giftUtils';
|
||||||
|
|
||||||
|
import { Upload } from '@mui/icons-material';
|
||||||
|
import SaveIcon from '@mui/icons-material/Save';
|
||||||
import { ENV_VARIABLES } from 'src/constants';
|
import { ENV_VARIABLES } from 'src/constants';
|
||||||
|
|
||||||
interface EditQuizParams {
|
interface EditQuizParams {
|
||||||
|
|
@ -30,7 +35,7 @@ const QuizForm: React.FC = () => {
|
||||||
const [filteredValue, setFilteredValue] = useState<string[]>([]);
|
const [filteredValue, setFilteredValue] = useState<string[]>([]);
|
||||||
|
|
||||||
const { id } = useParams<EditQuizParams>();
|
const { id } = useParams<EditQuizParams>();
|
||||||
const [value, setValue] = useState('');
|
const [values, setValues] = useState<string[]>([]);
|
||||||
const [isNewQuiz, setNewQuiz] = useState(false);
|
const [isNewQuiz, setNewQuiz] = useState(false);
|
||||||
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -41,6 +46,31 @@ const QuizForm: React.FC = () => {
|
||||||
};
|
};
|
||||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||||
|
|
||||||
|
const [isImagesCollapsed, setIsImagesCollapsed] = useState(true);
|
||||||
|
const [isCheatSheetCollapsed, setIsCheatSheetCollapsed] = useState(true);
|
||||||
|
const [isUploadCollapsed, setIsUploadCollapsed] = useState(true);
|
||||||
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||||
|
const [copySuccess, setCopySuccess] = useState<string | null>(null);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const [useNewEditor, setUserNewEditor] = useState(false);
|
||||||
|
|
||||||
|
const QuestionVraiFaux = "::Exemple de question vrai/faux:: \n 2+2 \\= 4 ? {T} //Utilisez les valeurs {T}, {F}, {TRUE} et {FALSE}.";
|
||||||
|
const QuestionChoixMul = "::Ville capitale du Canada:: \nQuelle ville est la capitale du Canada? {\n~ Toronto\n~ Montréal\n= Ottawa #Rétroaction spécifique.\n} // Commentaire non visible (au besoin)";
|
||||||
|
const QuestionChoixMulMany = "::Villes canadiennes:: \n Quelles villes trouve-t-on au Canada? { \n~ %33.3% Montréal \n ~ %33.3% Ottawa \n ~ %33.3% Vancouver \n ~ %-100% New York \n ~ %-100% Paris \n#### Rétroaction globale de la question. \n} // Utilisez tilde (signe de vague) pour toutes les réponses. // On doit indiquer le pourcentage de chaque réponse.";
|
||||||
|
const QuestionCourte = "::Clé et porte:: \n Avec quoi ouvre-t-on une porte? { \n= clé \n= clef \n} // Permet de fournir plusieurs bonnes réponses. // Note: La casse n'est pas prise en compte.";
|
||||||
|
const QuestionNum = "::Question numérique avec marge:: \nQuel est un nombre de 1 à 5 ? {\n#3:2\n}\n \n// Plage mathématique spécifiée avec des points de fin d'intervalle. \n ::Question numérique avec plage:: \n Quel est un nombre de 1 à 5 ? {\n#1..5\n} \n\n// Réponses numériques multiples avec crédit partiel et commentaires.\n::Question numérique avec plusieurs réponses::\nQuand est né Ulysses S. Grant ? {\n# =1822:0 # Correct ! Crédit complet. \n=%50%1822:2 # Il est né en 1822. Demi-crédit pour être proche.\n}";
|
||||||
|
|
||||||
|
const templates = [
|
||||||
|
{ label: 'Vrai/Faux', value: QuestionVraiFaux },
|
||||||
|
{ label: 'Choix multiples R1', value: QuestionChoixMul },
|
||||||
|
{ label: 'Choix multiples R2+', value: QuestionChoixMulMany },
|
||||||
|
{ label: 'Réponse courte', value: QuestionCourte },
|
||||||
|
{ label: 'Numérique', value: QuestionNum },
|
||||||
|
];
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
@ -100,7 +130,7 @@ const QuizForm: React.FC = () => {
|
||||||
setQuizTitle(title);
|
setQuizTitle(title);
|
||||||
setSelectedFolder(folderId);
|
setSelectedFolder(folderId);
|
||||||
setFilteredValue(content);
|
setFilteredValue(content);
|
||||||
setValue(quiz.content.join('\n\n'));
|
setValues(content);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||||
|
|
@ -112,13 +142,25 @@ const QuizForm: React.FC = () => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
function handleUpdatePreview(value: string) {
|
const handleAddQuestion = () => {
|
||||||
|
console.log("Adding question");
|
||||||
|
console.log("Current values:", values); // Log current state
|
||||||
|
setValues([...values, '']);
|
||||||
|
console.log("Updated values:", [...values, '']); // Log new state
|
||||||
|
};
|
||||||
|
|
||||||
|
const newHandleUpdatePreview = (newValues: string[]) => {
|
||||||
|
setValues(newValues);
|
||||||
|
setFilteredValue(newValues.filter(value => value.trim() !== ''));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdatePreview = (value: string) => {
|
||||||
if (value !== '') {
|
if (value !== '') {
|
||||||
setValue(value);
|
setValues([value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// split value when there is at least one blank line
|
// split value when there is at least one blank line
|
||||||
const linesArray = value.split(/\n{2,}/);
|
const linesArray = value.split(/\n{2,}/);
|
||||||
|
|
||||||
// if the first item in linesArray is blank, remove it
|
// if the first item in linesArray is blank, remove it
|
||||||
if (linesArray[0] === '') linesArray.shift();
|
if (linesArray[0] === '') linesArray.shift();
|
||||||
|
|
@ -134,7 +176,6 @@ const QuizForm: React.FC = () => {
|
||||||
|
|
||||||
const handleQuizSave = async () => {
|
const handleQuizSave = async () => {
|
||||||
try {
|
try {
|
||||||
// check if everything is there
|
|
||||||
if (quizTitle == '') {
|
if (quizTitle == '') {
|
||||||
alert("Veuillez choisir un titre");
|
alert("Veuillez choisir un titre");
|
||||||
return;
|
return;
|
||||||
|
|
@ -153,7 +194,8 @@ const QuizForm: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/teacher/dashboard');
|
setSnackbarMessage('Quiz enregistré avec succès!');
|
||||||
|
setSnackbarOpen(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|
@ -165,124 +207,212 @@ const QuizForm: React.FC = () => {
|
||||||
return <div>Chargement...</div>;
|
return <div>Chargement...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSnackbarClose = () => {
|
||||||
|
setSnackbarOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveImage = async () => {
|
||||||
|
try {
|
||||||
|
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (!inputElement?.files || inputElement.files.length === 0) {
|
||||||
|
setDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputElement.files || inputElement.files.length === 0) {
|
||||||
|
window.alert("Veuillez d'abord choisir une image à téléverser.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = await ApiService.uploadImage(inputElement.files[0]);
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
if (imageUrl.indexOf("ERROR") >= 0) {
|
||||||
|
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageLinks(prevLinks => [...prevLinks, imageUrl]);
|
||||||
|
|
||||||
|
// Reset the file input element
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
window.alert(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`)
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopyToClipboard = async (link: string) => {
|
const handleCopyToClipboard = async (link: string) => {
|
||||||
navigator.clipboard.writeText(link);
|
navigator.clipboard.writeText(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleFocusQuestion = (index: number) => {
|
||||||
|
const previewElement = document.querySelector('.preview-column');
|
||||||
|
if (previewElement) {
|
||||||
|
const questionElements = previewElement.querySelectorAll('.question-item');
|
||||||
|
if (questionElements[index]) {
|
||||||
|
questionElements[index].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string, label: string) => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
setCopySuccess(`Copié dans le presse-papier: ${label}`);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Clipboard error:', error));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (value: string, label: string) => {
|
||||||
|
copyToClipboard(value, label);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditor = () => {
|
||||||
|
setUserNewEditor(!useNewEditor);
|
||||||
|
}
|
||||||
const handleCopyImage = (id: string) => {
|
const handleCopyImage = (id: string) => {
|
||||||
const escLink = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`;
|
const escLink = `${ENV_VARIABLES.BACKEND_URL}/api/image/get/${id}`;
|
||||||
setImageLinks(prevLinks => [...prevLinks, escLink]);
|
setImageLinks(prevLinks => [...prevLinks, escLink]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="quizEditor">
|
<div className='quizEditor'>
|
||||||
<div
|
<div className='editHeader' style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '32px' }}>
|
||||||
className="editHeader"
|
<ReturnButton quizTitle={quizTitle} quizContent={filteredValue} quizFolder={selectedFolder} quizId={quiz?._id} isNewQuiz={isNewQuiz} />
|
||||||
style={{
|
<div className='title'>Éditeur de Quiz</div>
|
||||||
display: 'flex',
|
<Button
|
||||||
justifyContent: 'space-between',
|
variant="outlined"
|
||||||
alignItems: 'center',
|
onClick={toggleEditor}
|
||||||
marginBottom: '32px'
|
size="small"
|
||||||
}}
|
style={{
|
||||||
>
|
fontSize: '10px', // Smaller font
|
||||||
<ReturnButton
|
padding: '2px 6px', // Reduced padding
|
||||||
askConfirm
|
minWidth: 'auto', // Allow button to shrink
|
||||||
message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`}
|
height: 'fit-content', // Match height to content
|
||||||
/>
|
}}
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleQuizSave}
|
|
||||||
sx={{ display: 'flex', alignItems: 'center' }}
|
|
||||||
>
|
>
|
||||||
|
{useNewEditor ? 'Ancien éditeur' : 'Nouvel éditeur'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField onChange={handleQuizTitleChange} value={quizTitle} placeholder="Titre du quiz" label="Titre du quiz" fullWidth />
|
||||||
|
<label>Choisir un dossier:
|
||||||
|
<NativeSelect id="select-folder" color="primary" value={selectedFolder} onChange={handleSelectFolder} disabled={!isNewQuiz} style={{ marginBottom: '16px' }}>
|
||||||
|
<option disabled value=""> Choisir un dossier... </option>
|
||||||
|
{folders.map((folder: FolderType) => (
|
||||||
|
<option value={folder._id} key={folder._id}> {folder.title} </option>
|
||||||
|
))}
|
||||||
|
</NativeSelect>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className='sticky-buttons'>
|
||||||
|
<Select
|
||||||
|
value=""
|
||||||
|
displayEmpty
|
||||||
|
onChange={(e) => handleSelectChange(e.target.value, templates.find(t => t.value === e.target.value)?.label || '')}
|
||||||
|
style={{ width: '210px' }}
|
||||||
|
inputProps={{ 'data-testid': 'template-select' }}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>Modèles de questions</MenuItem>
|
||||||
|
{templates.map((template, index) => (
|
||||||
|
<MenuItem key={index} value={template.value}>{template.label}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button variant="contained" onClick={handleQuizSave} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<SaveIcon sx={{ fontSize: 20 }} />
|
<SaveIcon sx={{ fontSize: 20 }} />
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ textAlign: 'center', marginTop: '30px' }}>
|
<Snackbar open={!!copySuccess} autoHideDuration={3000} onClose={() => setCopySuccess(null)} message={copySuccess} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} key={copySuccess ? 'open' : 'close'} />
|
||||||
<div className="title">Éditeur de quiz</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <h2 className="subtitle">Éditeur</h2> */}
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<TextField
|
|
||||||
onChange={handleQuizTitleChange}
|
|
||||||
value={quizTitle}
|
|
||||||
color="primary"
|
|
||||||
placeholder="Titre du quiz"
|
|
||||||
label="Titre du quiz"
|
|
||||||
sx={{ width: '200px', marginTop: '50px' }}
|
|
||||||
/>
|
|
||||||
<NativeSelect
|
|
||||||
id="select-folder"
|
|
||||||
color="primary"
|
|
||||||
value={selectedFolder}
|
|
||||||
onChange={handleSelectFolder}
|
|
||||||
disabled={!isNewQuiz}
|
|
||||||
style={{ marginBottom: '16px', width: '200px', marginTop: '10px' }}
|
|
||||||
>
|
|
||||||
<option disabled value="">
|
|
||||||
Choisir un dossier...
|
|
||||||
</option>
|
|
||||||
{folders.map((folder: FolderType) => (
|
|
||||||
<option value={folder._id} key={folder._id}>
|
|
||||||
{folder.title}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</NativeSelect>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<Divider style={{ margin: '16px 0' }} />
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
|
|
||||||
<div className='editSection'>
|
<div className='editSection'>
|
||||||
|
|
||||||
<div className='edit'>
|
<div className='edit'>
|
||||||
<Editor
|
{useNewEditor ? (
|
||||||
label="Contenu GIFT du quiz:"
|
<NewEditor label="" values={values} onValuesChange={newHandleUpdatePreview} onFocusQuestion={handleFocusQuestion} />
|
||||||
initialValue={value}
|
) : (
|
||||||
onEditorChange={handleUpdatePreview} />
|
<Editor label="" values={values} onEditorChange={handleUpdatePreview} />
|
||||||
|
)}
|
||||||
<div className='images'>
|
{useNewEditor && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<Button variant="contained" onClick={handleAddQuestion}>
|
||||||
<h4>Mes images :</h4>
|
Ajouter une question
|
||||||
<ImageGalleryModal handleCopy={handleCopyImage} />
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="images">
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
<Button variant="outlined" onClick={() => setIsUploadCollapsed(!isUploadCollapsed)} style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}>
|
||||||
|
{isUploadCollapsed ? 'Afficher Téléverser image' : 'Masquer Téléverser image'}
|
||||||
|
</Button>
|
||||||
|
{!isUploadCollapsed && (
|
||||||
|
<div className="upload">
|
||||||
|
<label className="dropArea">
|
||||||
|
<input type="file" id="file-input" className="file-input" accept="image/jpeg, image/png" multiple ref={fileInputRef} />
|
||||||
|
<Button variant="outlined" aria-label="Téléverser" onClick={handleSaveImage}>
|
||||||
|
Téléverser <Upload />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
||||||
|
<DialogTitle>Erreur</DialogTitle>
|
||||||
|
<DialogContent>Veuillez d'abord choisir une image à téléverser.</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDialogOpen(false)} color="primary">OK</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div style={{ marginTop: '2px' }}>
|
||||||
|
<Button variant="outlined" onClick={() => setIsImagesCollapsed(!isImagesCollapsed)} style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}>
|
||||||
|
{isImagesCollapsed ? 'Afficher Mes images' : 'Masquer Mes images'}
|
||||||
|
</Button>
|
||||||
|
{!isImagesCollapsed && (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "inline" }}>(Voir section </div>
|
<h4>Mes images :</h4>
|
||||||
<a href="#images-section"style={{ textDecoration: "none" }} onClick={scrollToImagesSection}>
|
<div>
|
||||||
<u><em><h4 style={{ display: "inline" }}> 9. Images </h4></em></u>
|
<div>
|
||||||
</a>
|
<div style={{ display: 'inline' }}>(Voir section </div>
|
||||||
<div style={{ display: "inline" }}> ci-dessous</div>
|
<a href="#images-section" style={{ textDecoration: 'none' }} onClick={scrollToImagesSection}>
|
||||||
<div style={{ display: "inline" }}>)</div>
|
<u><em><h4 style={{ display: 'inline' }}> 9. Images </h4></em></u>
|
||||||
<br />
|
</a>
|
||||||
<em> - Cliquez sur un lien pour le copier</em>
|
<div style={{ display: 'inline' }}> ci-dessous</div>
|
||||||
</div>
|
<div style={{ display: 'inline' }}>)</div>
|
||||||
<ul>
|
<br />
|
||||||
{imageLinks.map((link, index) => {
|
<em> - Cliquez sur un lien pour le copier</em>
|
||||||
const imgTag = `[markdown]} "texte de l'infobulle") {T}`;
|
</div>
|
||||||
return (
|
<ul>
|
||||||
<li key={index}>
|
{imageLinks.map((link, index) => {
|
||||||
<code
|
const imgTag = `} "texte de l'infobulle")`;
|
||||||
onClick={() => handleCopyToClipboard(imgTag)}>
|
return (
|
||||||
{imgTag}
|
<li key={index}>
|
||||||
</code>
|
<code onClick={() => handleCopyToClipboard(imgTag)}>{imgTag}</code>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '2px' }}>
|
||||||
|
<Button variant="outlined" onClick={() => setIsCheatSheetCollapsed(!isCheatSheetCollapsed)} style={{ padding: '4px 8px', fontSize: '12px', marginBottom: '4px', width: '40%' }}>
|
||||||
|
{isCheatSheetCollapsed ? 'Afficher CheatSheet' : 'Masquer CheatSheet'}
|
||||||
|
</Button>
|
||||||
|
{!isCheatSheetCollapsed && <GiftCheatSheet />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GiftCheatSheet />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='preview'>
|
<div className="preview">
|
||||||
<div className="preview-column">
|
<div className="preview-column">
|
||||||
<h4>Prévisualisation</h4>
|
<h4>Prévisualisation</h4>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -290,20 +420,15 @@ const QuizForm: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showScrollButton && (
|
{showScrollButton && (
|
||||||
<Button
|
<Button onClick={scrollToTop} variant="contained" color="primary" style={scrollToTopButtonStyle} title="Scroll to top">
|
||||||
onClick={scrollToTop}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
style={scrollToTopButtonStyle}
|
|
||||||
title="Scroll to top"
|
|
||||||
>
|
|
||||||
↑
|
↑
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Snackbar open={snackbarOpen} autoHideDuration={3000} onClose={handleSnackbarClose} message={snackbarMessage} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,24 @@ input[type="file"] {
|
||||||
.quizEditor .editSection .preview {
|
.quizEditor .editSection .preview {
|
||||||
flex: 50%;
|
flex: 50%;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quizEditor {
|
||||||
|
margin: 0 -2rem; /* Counteract the padding */
|
||||||
|
width: calc(100% + 4rem); /* Expand to fill padded area */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue