mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
Merge 5ff5b2018f into ee7a7a0544
This commit is contained in:
commit
4cfca34a5b
8 changed files with 508 additions and 279 deletions
|
|
@ -1,80 +1,73 @@
|
|||
// Editor.test.tsx
|
||||
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 Editor from 'src/components/Editor/Editor';
|
||||
import Editor from '../../../components/Editor/Editor';
|
||||
|
||||
describe('Editor Component', () => {
|
||||
const mockOnEditorChange = jest.fn();
|
||||
const mockOnValuesChange = jest.fn();
|
||||
const mockOnFocusQuestion = jest.fn();
|
||||
|
||||
const sampleProps = {
|
||||
label: 'Sample Label',
|
||||
initialValue: 'Sample Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
values: ['Question 1', 'Question 2'],
|
||||
onValuesChange: mockOnValuesChange,
|
||||
onFocusQuestion: mockOnFocusQuestion,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnValuesChange.mockClear();
|
||||
mockOnFocusQuestion.mockClear();
|
||||
});
|
||||
|
||||
test('renders the label correctly', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const label = screen.getByText('Sample Label');
|
||||
expect(label).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly with initial value', () => {
|
||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
expect(editorTextarea).toBeInTheDocument();
|
||||
expect(editorTextarea.value).toBe('Sample Initial Value');
|
||||
test('renders the correct number of questions', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const questions = screen.getAllByRole('textbox');
|
||||
expect(questions.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls onEditorChange callback when editor value changes', () => {
|
||||
const editorTextarea = screen.getByRole('textbox') as HTMLTextAreaElement;
|
||||
fireEvent.change(editorTextarea, { target: { value: 'Updated Value' } });
|
||||
expect(mockOnEditorChange).toHaveBeenCalledWith('Updated Value');
|
||||
test('calls onValuesChange with updated values when a question is changed', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const questionInput = screen.getAllByRole('textbox')[0];
|
||||
fireEvent.change(questionInput, { target: { value: 'Updated Question 1' } });
|
||||
expect(mockOnValuesChange).toHaveBeenCalledWith(['Updated Question 1', 'Question 2']);
|
||||
});
|
||||
|
||||
it('updates editor value when initialValue prop changes', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
render(<Editor {...updatedProps} />);
|
||||
|
||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||
const editorTextarea = editorTextareas[1];
|
||||
|
||||
expect(editorTextarea.value).toBe('Updated Initial Value');
|
||||
test('calls onValuesChange with updated values when a question is deleted', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const deleteButton = screen.getAllByLabelText('delete')[0]; // Match original aria-label
|
||||
fireEvent.click(deleteButton);
|
||||
expect(mockOnValuesChange).toHaveBeenCalledWith(['Question 2']);
|
||||
});
|
||||
|
||||
test('should call change text with the correct value on textarea change', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
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('renders delete buttons for each question', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const deleteButtons = screen.getAllByLabelText('delete');
|
||||
expect(deleteButtons.length).toBe(2);
|
||||
});
|
||||
|
||||
test('should call onEditorChange with an empty string if textarea value is falsy', () => {
|
||||
const updatedProps = {
|
||||
label: 'Updated Label',
|
||||
initialValue: 'Updated Initial Value',
|
||||
onEditorChange: mockOnEditorChange
|
||||
};
|
||||
|
||||
render(<Editor {...updatedProps} />);
|
||||
|
||||
const editorTextareas = screen.getAllByRole('textbox') as HTMLTextAreaElement[];
|
||||
const editorTextarea = editorTextareas[1];
|
||||
fireEvent.change(editorTextarea, { target: { value: '' } });
|
||||
|
||||
expect(editorTextarea.value).toBe('');
|
||||
test('calls onFocusQuestion with correct index when focus button is clicked', () => {
|
||||
render(<Editor {...sampleProps} />);
|
||||
const focusButton = screen.getAllByLabelText('focus question')[1];
|
||||
fireEvent.click(focusButton);
|
||||
expect(mockOnFocusQuestion).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
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 { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||
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.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn()
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ReturnButton', () => {
|
||||
test('navigates back when askConfirm is false', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={false} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||
// Mock ApiService
|
||||
jest.mock('../../../services/ApiService', () => ({
|
||||
createQuiz: jest.fn(),
|
||||
updateQuiz: jest.fn(),
|
||||
}));
|
||||
|
||||
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', () => {
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||
expect(confirmButton).toBeInTheDocument();
|
||||
test('renders the button with "Retour" text when not saving', () => {
|
||||
render(<ReturnButton />);
|
||||
expect(screen.getByText('Retour')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
/*test('navigates back after confirming in the modal', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
test('renders "Enregistrement..." text when saving', async () => {
|
||||
render(<ReturnButton quizTitle="Test Quiz" quizFolder="folder1" isNewQuiz />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
const confirmButton = screen.getByTestId('modal-confirm-button');
|
||||
fireEvent.click(confirmButton);
|
||||
expect(navigateMock).toHaveBeenCalledWith(-1);
|
||||
});*/
|
||||
expect(screen.getByText('Enregistrement...')).toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.queryByText('Enregistrement...')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test('cancels navigation when canceling in the modal', () => {
|
||||
const navigateMock = jest.fn();
|
||||
(useNavigate as jest.Mock).mockReturnValue(navigateMock);
|
||||
render(<ReturnButton askConfirm={true} />);
|
||||
test('navigates to /teacher/dashboard by default when clicked', async () => {
|
||||
render(<ReturnButton />);
|
||||
fireEvent.click(screen.getByText('Retour'));
|
||||
fireEvent.click(screen.getByText('Annuler'));
|
||||
expect(navigateMock).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/teacher/dashboard'));
|
||||
});
|
||||
|
||||
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 { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import QuizForm from '../../../../pages/Teacher/EditorQuiz/EditorQuiz';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
// Mock localStorage with proper TypeScript types
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => (store[key] = value.toString()),
|
||||
clear: () => (store = {}),
|
||||
getItem: (key: string): string | null => store[key] || null,
|
||||
setItem: (key: string, value: string): void => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
clear: (): void => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
// Mock react-router-dom
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('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', () => {
|
||||
test('renders QuizForm with default state for a new quiz', () => {
|
||||
test('renders QuizForm with default state for a new quiz', async () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/teacher/editor-quiz/new']}>
|
||||
<QuizForm />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Éditeur de quiz')).toBeInTheDocument();
|
||||
// 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?' } });
|
||||
|
||||
// Wait for the component to render the title
|
||||
await waitFor(() => {
|
||||
const sampleQuestionElements = screen.queryAllByText(/Sample question\?/i);
|
||||
expect(sampleQuestionElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('Éditeur de Quiz')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText(/Enregistrer/i);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Sauvegarder le questionnaire/i)).toBeInTheDocument();
|
||||
// Check for other expected elements
|
||||
expect(screen.getByText('Prévisualisation')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,36 +1,85 @@
|
|||
// Editor.tsx
|
||||
import React, { useState, useRef } from 'react';
|
||||
import './editor.css';
|
||||
import { TextareaAutosize } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { TextField, Typography, IconButton, Box } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
|
||||
interface EditorProps {
|
||||
label: string;
|
||||
initialValue: string;
|
||||
onEditorChange: (value: string) => void;
|
||||
values: string[];
|
||||
onValuesChange: (values: string[]) => void;
|
||||
onFocusQuestion?: (index: number) => void;
|
||||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ initialValue, onEditorChange, label }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const editorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQuestion }) => {
|
||||
const handleChange = (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValues = [...values];
|
||||
newValues[index] = event.target.value;
|
||||
onValuesChange(newValues);
|
||||
};
|
||||
|
||||
function handleEditorChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
const text = event.target.value;
|
||||
setValue(text);
|
||||
onEditorChange(text || '');
|
||||
const handleDeleteQuestion = (index: number) => () => {
|
||||
const newValues = values.filter((_, i) => i !== index); // Remove the question at the specified index
|
||||
onValuesChange(newValues);
|
||||
};
|
||||
|
||||
const handleFocusQuestion = (index: number) => () => {
|
||||
if (onFocusQuestion) {
|
||||
onFocusQuestion(index); // Call the focus function if provided
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<label>
|
||||
<h4>{label}</h4>
|
||||
<TextareaAutosize
|
||||
id="editor-textarea"
|
||||
ref={editorRef}
|
||||
onChange={handleEditorChange}
|
||||
<div>
|
||||
<Typography variant="h6" fontWeight="bold" style={{ marginBottom: '24px' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
|
||||
{values.map((value, index) => (
|
||||
<Box key={index} style={{ marginBottom: '24px' }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="subtitle1" fontWeight="bold" style={{ marginBottom: '8px' }}>
|
||||
Question {index + 1}
|
||||
</Typography>
|
||||
<Box>
|
||||
{/* Focus (Eye) Button */}
|
||||
<IconButton
|
||||
onClick={handleFocusQuestion(index)}
|
||||
aria-label="focus question"
|
||||
sx={{
|
||||
color: 'gray',
|
||||
'&:hover': { color: 'blue' },
|
||||
marginRight: '8px', // Space between eye and delete
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
{/* Delete Button */}
|
||||
<IconButton
|
||||
onClick={handleDeleteQuestion(index)}
|
||||
aria-label="delete"
|
||||
sx={{
|
||||
color: 'light-gray',
|
||||
'&:hover': { color: 'red' },
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<TextField
|
||||
value={value}
|
||||
className="editor"
|
||||
minRows={5}
|
||||
onChange={handleChange(index)}
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={4}
|
||||
maxRows={Infinity}
|
||||
variant="outlined"
|
||||
style={{ overflow: 'auto' }}
|
||||
/>
|
||||
</label>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,22 +12,23 @@ interface GIFTTemplatePreviewProps {
|
|||
|
||||
const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
||||
questions,
|
||||
hideAnswers = false
|
||||
hideAnswers = false,
|
||||
}) => {
|
||||
const [error, setError] = useState('');
|
||||
const [isPreviewReady, setIsPreviewReady] = useState(false);
|
||||
const [items, setItems] = useState('');
|
||||
const [questionItems, setQuestionItems] = useState<string[]>([]); // Array of HTML strings for each question
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
let previewHTML = '';
|
||||
const previewItems: string[] = [];
|
||||
questions.forEach((giftQuestion) => {
|
||||
try {
|
||||
const question = parse(giftQuestion);
|
||||
previewHTML += Template(question[0], {
|
||||
const html = Template(question[0], {
|
||||
preview: true,
|
||||
theme: 'light'
|
||||
theme: 'light',
|
||||
});
|
||||
previewItems.push(html);
|
||||
} catch (error) {
|
||||
let errorMsg: string;
|
||||
if (error instanceof UnsupportedQuestionTypeError) {
|
||||
|
|
@ -37,18 +38,21 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
|||
} else {
|
||||
errorMsg = ErrorTemplate(giftQuestion, 'Erreur inconnue');
|
||||
}
|
||||
previewHTML += `<div label="error-message">${errorMsg}</div>`;
|
||||
previewItems.push(`<div label="error-message">${errorMsg}</div>`);
|
||||
}
|
||||
});
|
||||
|
||||
if (hideAnswers) {
|
||||
previewItems.forEach((item, index) => {
|
||||
const svgRegex = /<svg[^>]*>([\s\S]*?)<\/svg>/gi;
|
||||
previewHTML = previewHTML.replace(svgRegex, '');
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}, [questions]);
|
||||
}, [questions, hideAnswers]);
|
||||
|
||||
const PreviewComponent = () => (
|
||||
<React.Fragment>
|
||||
|
|
@ -65,8 +69,13 @@ const GIFTTemplatePreview: React.FC<GIFTTemplatePreviewProps> = ({
|
|||
<div className="error">{error}</div>
|
||||
) : isPreviewReady ? (
|
||||
<div data-testid="preview-container">
|
||||
|
||||
<div dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: items }) }}></div>
|
||||
{questionItems.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="question-item"
|
||||
dangerouslySetInnerHTML={{ __html: FormattedTextTemplate({ format: 'html', text: item }) }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="loading">Chargement de la prévisualisation...</div>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,69 @@
|
|||
// GoBackButton.tsx
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ConfirmDialog from '../ConfirmDialog/ConfirmDialog';
|
||||
import { Button } from '@mui/material';
|
||||
import { ChevronLeft } from '@mui/icons-material';
|
||||
import ApiService from '../../services/ApiService'; // Assuming this is where save logic lives
|
||||
|
||||
interface Props {
|
||||
onReturn?: () => void;
|
||||
askConfirm?: boolean;
|
||||
message?: string;
|
||||
quizTitle?: string; // Quiz title to save
|
||||
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> = ({
|
||||
askConfirm = false,
|
||||
message = 'Êtes-vous sûr de vouloir quitter la page ?',
|
||||
onReturn
|
||||
onReturn,
|
||||
quizTitle = '',
|
||||
quizContent = [],
|
||||
quizFolder = '',
|
||||
quizId,
|
||||
isNewQuiz = false,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false); // Optional: to show saving state
|
||||
|
||||
const handleOnReturnButtonClick = () => {
|
||||
if (askConfirm) {
|
||||
setShowDialog(true);
|
||||
} else {
|
||||
handleOnReturn();
|
||||
const handleOnReturnButtonClick = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
// Automatically save the quiz
|
||||
if (isNewQuiz && quizTitle && quizFolder) {
|
||||
await ApiService.createQuiz(quizTitle, quizContent, quizFolder);
|
||||
} else if (quizId && quizTitle) {
|
||||
await ApiService.updateQuiz(quizId, quizTitle, quizContent);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
setShowDialog(false);
|
||||
// If no title/folder, proceed without saving (optional behavior)
|
||||
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 handleOnReturn = () => {
|
||||
if (onReturn) {
|
||||
onReturn();
|
||||
} else {
|
||||
navigate(-1);
|
||||
navigate('/teacher/dashboard'); // Navigate to dashboard instead of -1
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='returnButton'>
|
||||
<div className="returnButton">
|
||||
<Button
|
||||
variant="text"
|
||||
startIcon={<ChevronLeft />}
|
||||
onClick={handleOnReturnButtonClick}
|
||||
color="primary"
|
||||
sx={{ marginLeft: '-0.5rem', fontSize: 16 }}
|
||||
disabled={isSaving} // Disable button while saving
|
||||
>
|
||||
Retour
|
||||
{isSaving ? 'Enregistrement...' : 'Retour'}
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
open={showDialog}
|
||||
title="Confirmer"
|
||||
message={message}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
buttonOrderType="warning"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import GIFTTemplatePreview from 'src/components/GiftTemplate/GIFTTemplatePreview
|
|||
import { QuizType } from '../../../Types/QuizType';
|
||||
|
||||
import './editorQuiz.css';
|
||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent } from '@mui/material';
|
||||
import { Button, TextField, NativeSelect, Divider, Dialog, DialogTitle, DialogActions, DialogContent, Snackbar } from '@mui/material';
|
||||
import ReturnButton from 'src/components/ReturnButton/ReturnButton';
|
||||
|
||||
import ApiService from '../../../services/ApiService';
|
||||
|
|
@ -29,7 +29,7 @@ const QuizForm: React.FC = () => {
|
|||
const [filteredValue, setFilteredValue] = useState<string[]>([]);
|
||||
|
||||
const { id } = useParams<EditQuizParams>();
|
||||
const [value, setValue] = useState('');
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
const [isNewQuiz, setNewQuiz] = useState(false);
|
||||
const [quiz, setQuiz] = useState<QuizType | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -42,6 +42,12 @@ const QuizForm: React.FC = () => {
|
|||
const [dialogOpen, setDialogOpen] = 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 scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
|
@ -101,7 +107,7 @@ const QuizForm: React.FC = () => {
|
|||
setQuizTitle(title);
|
||||
setSelectedFolder(folderId);
|
||||
setFilteredValue(content);
|
||||
setValue(quiz.content.join('\n\n'));
|
||||
setValues(content);
|
||||
|
||||
} catch (error) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
|
|
@ -113,21 +119,17 @@ const QuizForm: React.FC = () => {
|
|||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
function handleUpdatePreview(value: string) {
|
||||
if (value !== '') {
|
||||
setValue(value);
|
||||
}
|
||||
const handleAddQuestion = () => {
|
||||
console.log("Adding question");
|
||||
console.log("Current values:", values); // Log current state
|
||||
setValues([...values, '']);
|
||||
console.log("Updated values:", [...values, '']); // Log new state
|
||||
};
|
||||
|
||||
// split value when there is at least one blank line
|
||||
const linesArray = value.split(/\n{2,}/);
|
||||
|
||||
// if the first item in linesArray is blank, remove it
|
||||
if (linesArray[0] === '') linesArray.shift();
|
||||
|
||||
if (linesArray[linesArray.length - 1] === '') linesArray.pop();
|
||||
|
||||
setFilteredValue(linesArray);
|
||||
}
|
||||
const handleUpdatePreview = (newValues: string[]) => {
|
||||
setValues(newValues);
|
||||
setFilteredValue(newValues.filter(value => value.trim() !== ''));
|
||||
};
|
||||
|
||||
const handleQuizTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuizTitle(event.target.value);
|
||||
|
|
@ -135,7 +137,6 @@ const QuizForm: React.FC = () => {
|
|||
|
||||
const handleQuizSave = async () => {
|
||||
try {
|
||||
// check if everything is there
|
||||
if (quizTitle == '') {
|
||||
alert("Veuillez choisir un titre");
|
||||
return;
|
||||
|
|
@ -154,7 +155,8 @@ const QuizForm: React.FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
navigate('/teacher/dashboard');
|
||||
setSnackbarMessage('Quiz enregistré avec succès!');
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
window.alert(`Une erreur est survenue.\n Veuillez réessayer plus tard`)
|
||||
console.log(error)
|
||||
|
|
@ -166,6 +168,10 @@ const QuizForm: React.FC = () => {
|
|||
return <div>Chargement...</div>;
|
||||
}
|
||||
|
||||
const handleSnackbarClose = () => {
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleSaveImage = async () => {
|
||||
try {
|
||||
const inputElement = document.getElementById('file-input') as HTMLInputElement;
|
||||
|
|
@ -204,16 +210,29 @@ const QuizForm: React.FC = () => {
|
|||
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' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='quizEditor'>
|
||||
|
||||
<div className='editHeader'>
|
||||
<ReturnButton
|
||||
askConfirm
|
||||
message={`Êtes-vous sûr de vouloir quitter l'éditeur sans sauvegarder le questionnaire?`}
|
||||
quizTitle={quizTitle}
|
||||
quizContent={filteredValue}
|
||||
quizFolder={selectedFolder}
|
||||
quizId={quiz?._id}
|
||||
isNewQuiz={isNewQuiz}
|
||||
/>
|
||||
|
||||
<div className='title'>Éditeur de quiz</div>
|
||||
<div className='title'>Éditeur de Quiz</div>
|
||||
|
||||
<div className='dumb'></div>
|
||||
</div>
|
||||
|
|
@ -253,32 +272,47 @@ const QuizForm: React.FC = () => {
|
|||
|
||||
<div className='edit'>
|
||||
<Editor
|
||||
label="Contenu GIFT du quiz:"
|
||||
initialValue={value}
|
||||
onEditorChange={handleUpdatePreview} />
|
||||
|
||||
<div className='images'>
|
||||
<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 />
|
||||
label="Contenu GIFT de chaque question:"
|
||||
values={values}
|
||||
onValuesChange={handleUpdatePreview}
|
||||
onFocusQuestion={handleFocusQuestion} />
|
||||
<Button variant="contained" onClick={handleAddQuestion}>
|
||||
Ajouter une question
|
||||
</Button>
|
||||
|
||||
<div className="images">
|
||||
{/* Collapsible Upload Section */}
|
||||
<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)} >
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
||||
<DialogTitle>Erreur</DialogTitle>
|
||||
<DialogContent>
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
Veuillez d'abord choisir une image à téléverser.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDialogOpen(false)} color="primary">
|
||||
|
|
@ -287,16 +321,37 @@ const QuizForm: React.FC = () => {
|
|||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Collapsible Images Section */}
|
||||
<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>
|
||||
<h4>Mes images :</h4>
|
||||
<div>
|
||||
<div>
|
||||
<div style={{ display: "inline" }}>(Voir section </div>
|
||||
<a href="#images-section"style={{ textDecoration: "none" }} onClick={scrollToImagesSection}>
|
||||
<u><em><h4 style={{ display: "inline" }}> 9. Images </h4></em></u>
|
||||
<div style={{ display: 'inline' }}>(Voir section </div>
|
||||
<a
|
||||
href="#images-section"
|
||||
style={{ textDecoration: 'none' }}
|
||||
onClick={scrollToImagesSection}
|
||||
>
|
||||
<u>
|
||||
<em>
|
||||
<h4 style={{ display: 'inline' }}> 9. Images </h4>
|
||||
</em>
|
||||
</u>
|
||||
</a>
|
||||
<div style={{ display: "inline" }}> ci-dessous</div>
|
||||
<div style={{ display: "inline" }}>)</div>
|
||||
<div style={{ display: 'inline' }}> ci-dessous</div>
|
||||
<div style={{ display: 'inline' }}>)</div>
|
||||
<br />
|
||||
<em> - Cliquez sur un lien pour le copier</em>
|
||||
</div>
|
||||
|
|
@ -305,8 +360,7 @@ const QuizForm: React.FC = () => {
|
|||
const imgTag = `} "texte de l'infobulle")`;
|
||||
return (
|
||||
<li key={index}>
|
||||
<code
|
||||
onClick={() => handleCopyToClipboard(imgTag)}>
|
||||
<code onClick={() => handleCopyToClipboard(imgTag)}>
|
||||
{imgTag}
|
||||
</code>
|
||||
</li>
|
||||
|
|
@ -315,12 +369,24 @@ const QuizForm: React.FC = () => {
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiftCheatSheet />
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='preview'>
|
||||
{/* Collapsible CheatSheet Section */}
|
||||
<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 className="preview">
|
||||
<div className="preview-column">
|
||||
<h4>Prévisualisation</h4>
|
||||
<div>
|
||||
|
|
@ -328,7 +394,6 @@ const QuizForm: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{showScrollButton && (
|
||||
|
|
@ -342,6 +407,14 @@ const QuizForm: React.FC = () => {
|
|||
↑
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={3000} // Hide after 3 seconds
|
||||
onClose={handleSnackbarClose}
|
||||
message={snackbarMessage}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} // Lower-right corner
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ input[type="file"] {
|
|||
.quizEditor .editSection .preview {
|
||||
flex: 50%;
|
||||
padding: 5px;
|
||||
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quizEditor {
|
||||
margin: 0 -2rem; /* Counteract the padding */
|
||||
width: calc(100% + 4rem); /* Expand to fill padded area */
|
||||
}
|
||||
Loading…
Reference in a new issue