diff --git a/package.json b/package.json index 4cde8223..3b93e6df 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "mathjax-react": "^2.0.1", "next": "^13.4.12", "react": "^18.2.0", - "react-admin": "^4.10.2", + "react-admin": "^4.16.15", "react-cookie": "^4.1.1", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/src/components/Admin/Admin.tsx b/src/components/Admin/Admin.tsx index 1f1daac1..7294c58c 100644 --- a/src/components/Admin/Admin.tsx +++ b/src/components/Admin/Admin.tsx @@ -18,6 +18,10 @@ import {EventCreate} from './resources/competition/event/EventCreate' import {EventEdit} from './resources/competition/event/EventEdit' import {EventList} from './resources/competition/event/EventList' import {EventShow} from './resources/competition/event/EventShow' +import {ProblemCreate} from './resources/competition/problems/ProblemCreate' +import {ProblemEdit} from './resources/competition/problems/ProblemEdit' +import {ProblemList} from './resources/competition/problems/ProblemList' +import {ProblemShow} from './resources/competition/problems/ProblemShow' import {SemesterCreate} from './resources/competition/semester/SemesterCreate' import {SemesterEdit} from './resources/competition/semester/SemesterEdit' import {SemesterList} from './resources/competition/semester/SemesterList' @@ -63,6 +67,13 @@ export const Admin: FC = () => { create={SemesterCreate} /> + ) } diff --git a/src/components/Admin/custom/LatexPreview.tsx b/src/components/Admin/custom/LatexPreview.tsx new file mode 100644 index 00000000..5a77b7ee --- /dev/null +++ b/src/components/Admin/custom/LatexPreview.tsx @@ -0,0 +1,21 @@ +import {FC} from 'react' +import {FieldProps, FormDataConsumer, Labeled} from 'react-admin' + +import {Latex} from '@/components/Latex/Latex' + +export const LatexPreview: FC = ({source}) => { + if (!source) return null + + return ( + + + {({formData}) => { + const data = formData[source] + if (!data) return null + + return {data} + }} + + + ) +} diff --git a/src/components/Admin/custom/MyEditActions.tsx b/src/components/Admin/custom/MyEditActions.tsx index cc372204..761207ff 100644 --- a/src/components/Admin/custom/MyEditActions.tsx +++ b/src/components/Admin/custom/MyEditActions.tsx @@ -18,6 +18,10 @@ export const MyEditActions: FC = () => { return ( + {/* the `to` prop was omitted from ShowButton in recent RA version, but it's still working + and RA doesn't provide better way to do this + eslint-disable-next-line @typescript-eslint/ban-ts-comment + @ts-ignore */} diff --git a/src/components/Admin/custom/MyFileField.tsx b/src/components/Admin/custom/MyFileField.tsx new file mode 100644 index 00000000..845ee1fa --- /dev/null +++ b/src/components/Admin/custom/MyFileField.tsx @@ -0,0 +1,14 @@ +import {FC} from 'react' +import {FileField, RecordContextProvider, useRecordContext} from 'react-admin' + +export const MyFileField: FC = () => { + const record = useRecordContext() + + const myRecord = typeof record === 'string' ? {src: record, title: 'Vzorák'} : record + + return ( + + + + ) +} diff --git a/src/components/Admin/custom/MyImageField.tsx b/src/components/Admin/custom/MyImageField.tsx new file mode 100644 index 00000000..aee9a716 --- /dev/null +++ b/src/components/Admin/custom/MyImageField.tsx @@ -0,0 +1,14 @@ +import {FC} from 'react' +import {ImageField, RecordContextProvider, useRecordContext} from 'react-admin' + +export const MyImageField: FC = () => { + const record = useRecordContext() + + const myRecord = typeof record === 'string' ? {src: record} : record + + return ( + + + + ) +} diff --git a/src/components/Admin/dataProvider.ts b/src/components/Admin/dataProvider.ts index c236a484..a4a17782 100644 --- a/src/components/Admin/dataProvider.ts +++ b/src/components/Admin/dataProvider.ts @@ -79,8 +79,13 @@ export const dataProvider: DataProvider = { } }, update: async (resource, params) => { - const {id, ...input} = params.data - const {data} = await axios.patch(`${apiUrl}/${resource}/${id}`, input) + const {id, formData, ...input} = params.data + + // create/update problemu moze obsahovat obrazok a tym padom to musime poslat ako form data. + // ked existuju formData, ktore sme do recordu pridali v `transform` v `MyEdit`, pouzijeme tie + const body = formData ?? input + + const {data} = await axios.patch(`${apiUrl}/${resource}/${id}`, body) return {data} }, @@ -90,7 +95,11 @@ export const dataProvider: DataProvider = { return {data: data.map(({data}) => data)} }, create: async (resource, params) => { - const {data} = await axios.post(`${apiUrl}/${resource}`, params.data) + const {formData, ...input} = params.data + + const body = formData ?? input + + const {data} = await axios.post(`${apiUrl}/${resource}`, body) return {data} }, diff --git a/src/components/Admin/resources/competition/problems/ProblemCreate.tsx b/src/components/Admin/resources/competition/problems/ProblemCreate.tsx new file mode 100644 index 00000000..a690df4c --- /dev/null +++ b/src/components/Admin/resources/competition/problems/ProblemCreate.tsx @@ -0,0 +1,35 @@ +import {FC} from 'react' +import {FileInput, FormTab, ImageInput, ReferenceInput, required, SelectInput, TabbedForm, TextInput} from 'react-admin' + +import {LatexPreview} from '@/components/Admin/custom/LatexPreview' +import {MyCreate} from '@/components/Admin/custom/MyCreate' +import {MyFileField} from '@/components/Admin/custom/MyFileField' +import {MyImageField} from '@/components/Admin/custom/MyImageField' + +import {createProblemFormData} from './createProblemFormData' + +export const ProblemCreate: FC = () => ( + { + record.formData = createProblemFormData(record) + return record + }} + > + + + + + + + + + + + + + + + + + +) diff --git a/src/components/Admin/resources/competition/problems/ProblemEdit.tsx b/src/components/Admin/resources/competition/problems/ProblemEdit.tsx new file mode 100644 index 00000000..bc703e90 --- /dev/null +++ b/src/components/Admin/resources/competition/problems/ProblemEdit.tsx @@ -0,0 +1,35 @@ +import {FC} from 'react' +import {FileInput, FormTab, ImageInput, ReferenceInput, required, SelectInput, TabbedForm, TextInput} from 'react-admin' + +import {LatexPreview} from '@/components/Admin/custom/LatexPreview' +import {MyEdit} from '@/components/Admin/custom/MyEdit' +import {MyFileField} from '@/components/Admin/custom/MyFileField' +import {MyImageField} from '@/components/Admin/custom/MyImageField' + +import {createProblemFormData} from './createProblemFormData' + +export const ProblemEdit: FC = () => ( + { + record.formData = createProblemFormData(record) + return record + }} + > + + + + + + + + + + + + + + + + + +) diff --git a/src/components/Admin/resources/competition/problems/ProblemList.tsx b/src/components/Admin/resources/competition/problems/ProblemList.tsx new file mode 100644 index 00000000..03254833 --- /dev/null +++ b/src/components/Admin/resources/competition/problems/ProblemList.tsx @@ -0,0 +1,29 @@ +import {FC} from 'react' +import { + BooleanField, + Datagrid, + FunctionField, + ImageField, + List, + NumberField, + RaRecord, + ReferenceField, +} from 'react-admin' + +import {TruncatedTextField} from '@/components/Admin/custom/TruncatedTextField' + +export const ProblemList: FC = () => ( + + + + + + + + label="Má vzorák" + render={(record) => record && } + /> + + + +) diff --git a/src/components/Admin/resources/competition/problems/ProblemShow.tsx b/src/components/Admin/resources/competition/problems/ProblemShow.tsx new file mode 100644 index 00000000..38787fb1 --- /dev/null +++ b/src/components/Admin/resources/competition/problems/ProblemShow.tsx @@ -0,0 +1,26 @@ +import {FC} from 'react' +import { + FileField, + ImageField, + NumberField, + RecordRepresentation, + ReferenceField, + Show, + SimpleShowLayout, +} from 'react-admin' + +import {MyShowActions} from '@/components/Admin/custom/MyShowActions' +import {TruncatedTextField} from '@/components/Admin/custom/TruncatedTextField' + +export const ProblemShow: FC = () => ( + } title={}> + + + + + + + + + +) diff --git a/src/components/Admin/resources/competition/problems/createProblemFormData.ts b/src/components/Admin/resources/competition/problems/createProblemFormData.ts new file mode 100644 index 00000000..ea45d1e2 --- /dev/null +++ b/src/components/Admin/resources/competition/problems/createProblemFormData.ts @@ -0,0 +1,23 @@ +// approach based on: https://marmelab.com/react-admin/DataProviders.html#handling-file-uploads + +import {Problem} from '@/types/api/competition' + +// takisto urobene zmeny v RA dataProvideri +export const createProblemFormData = ({ + image, + solution_pdf, + ...data +}: Omit & { + image?: {rawFile: File} + solution_pdf?: {rawFile: File} +}) => { + const formData = new FormData() + // vzdy appendneme kazdy kluc, aj tieto fily, len null sa tu neda pouzit. null znamena, ze file odstranujeme + formData.append('image', image?.rawFile ?? '') + formData.append('solution_pdf', solution_pdf?.rawFile ?? '') + Object.entries(data).forEach(([key, value]) => { + if (value) formData.append(key, value.toString()) + }) + + return formData +} diff --git a/yarn.lock b/yarn.lock index 5ad9f273..0bbb1de1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6363,6 +6363,13 @@ __metadata: languageName: node linkType: hard +"hotscript@npm:^1.0.12": + version: 1.0.13 + resolution: "hotscript@npm:1.0.13" + checksum: 09141bde1dfea1fd28e21b3c8c6e849593998dc42fc93980404bd1ad8e66ae96c0bacf03b53ee645fa736e56bcc135176ba08c1f20e93566c26856f7f3024d9c + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -8864,9 +8871,9 @@ __metadata: languageName: node linkType: hard -"ra-core@npm:^4.10.2": - version: 4.10.2 - resolution: "ra-core@npm:4.10.2" +"ra-core@npm:^4.16.15": + version: 4.16.15 + resolution: "ra-core@npm:4.16.15" dependencies: clsx: ^1.1.1 date-fns: ^2.19.0 @@ -8882,40 +8889,41 @@ __metadata: history: ^5.1.0 react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 - react-hook-form: ^7.40.0 + react-hook-form: ^7.43.9 react-router: ^6.1.0 react-router-dom: ^6.1.0 - checksum: fa067b9da1de1a6981db18e40ad84e507d8015422461d600665a754ab9424ce0a6b6687174ce3683979ac4c79d7550f21097300e80eaeb46e9a5dd8bc7b7ba9f + checksum: 6b3cf850080f5e34daf8c5ed8000dcda1a1a95ef06201389ac5514e1547353003d75c121e5dac7b20a16628fd9bafeb2afbb76e2c004cfd0d2f51a8a5b0246ff languageName: node linkType: hard -"ra-i18n-polyglot@npm:^4.10.2": - version: 4.10.2 - resolution: "ra-i18n-polyglot@npm:4.10.2" +"ra-i18n-polyglot@npm:^4.16.15": + version: 4.16.15 + resolution: "ra-i18n-polyglot@npm:4.16.15" dependencies: node-polyglot: ^2.2.2 - ra-core: ^4.10.2 - checksum: 5ad63d7fe1331b200e986d5c0acf4f0e5ec55b5e4b65298a9eaed40b36a9a2b51212b8def955a2937136cc767beeb26051acc47070d881c9c49b4552b9fbd07f + ra-core: ^4.16.15 + checksum: 16eaa35072b1b2ba97b1b41c68bf0357a4c3f384cff273376e3b9a56c6f4838149021580ff58248f5e63e746a09dc554df75e2a50f6a39ed45f87674e898a332 languageName: node linkType: hard -"ra-language-english@npm:^4.10.2": - version: 4.10.2 - resolution: "ra-language-english@npm:4.10.2" +"ra-language-english@npm:^4.16.15": + version: 4.16.15 + resolution: "ra-language-english@npm:4.16.15" dependencies: - ra-core: ^4.10.2 - checksum: ed58ec52b821b502e73e1c39136eecefd9f33d18cdec9059c033f4d9dda3d0b074dff81575897aec2f847140027a3e65b237b6a505f872e176fe83ba1473467b + ra-core: ^4.16.15 + checksum: f145f0047b26509e7b9146a5a0f69be8f6adce11658778584b26ab85f6394768d16ccf6d8d7a4a68b57ee8f5b29c6b773c5fd4e204625b7a35153bedb8585568 languageName: node linkType: hard -"ra-ui-materialui@npm:^4.10.2": - version: 4.10.2 - resolution: "ra-ui-materialui@npm:4.10.2" +"ra-ui-materialui@npm:^4.16.15": + version: 4.16.15 + resolution: "ra-ui-materialui@npm:4.16.15" dependencies: autosuggest-highlight: ^3.1.1 clsx: ^1.1.1 css-mediaquery: ^0.1.2 dompurify: ^2.4.3 + hotscript: ^1.0.12 inflection: ~1.12.0 jsonexport: ^3.2.0 lodash: ~4.17.5 @@ -8932,32 +8940,33 @@ __metadata: react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 react-hook-form: "*" + react-is: ^16.9.0 || ^17.0.0 || ^18.0.0 react-router: ^6.1.0 react-router-dom: ^6.1.0 - checksum: 5202a22a646f8b32a7e7f4950f3911bef868791d26a83491b137ed652f8d7409264bcc748e86dc31f7f3e94b965af63294f7088f6ce13b1ca7c7c549e81e680a + checksum: 0edf3cc6ddb3989bc33b9d56fbae60cb88bb571a821dcf1afe1d8ad60e51358dbe83a38695b6c5db06a361af2ed7151dc5ec66e124a5fa29f7c009d9b41f8047 languageName: node linkType: hard -"react-admin@npm:^4.10.2": - version: 4.10.2 - resolution: "react-admin@npm:4.10.2" +"react-admin@npm:^4.16.15": + version: 4.16.15 + resolution: "react-admin@npm:4.16.15" dependencies: "@emotion/react": ^11.4.1 "@emotion/styled": ^11.3.0 "@mui/icons-material": ^5.0.1 "@mui/material": ^5.0.2 history: ^5.1.0 - ra-core: ^4.10.2 - ra-i18n-polyglot: ^4.10.2 - ra-language-english: ^4.10.2 - ra-ui-materialui: ^4.10.2 - react-hook-form: ^7.40.0 + ra-core: ^4.16.15 + ra-i18n-polyglot: ^4.16.15 + ra-language-english: ^4.16.15 + ra-ui-materialui: ^4.16.15 + react-hook-form: ^7.43.9 react-router: ^6.1.0 react-router-dom: ^6.1.0 peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 - checksum: 78810c865c02292e09125f6df6888aa75dfaef50db492b5fa6e6929610142188032490b1ba80b4d47b70489d4467e576f34d3b0669c4ef197d02cb2846701141 + checksum: 5c02c81e87e2d8d1da083422f5dac87bbf325d203e0244fa359dc140768a859acc1f6e87cf3532a92c00f759cf645add7da8b81a6a9f57d0001928e4dfe086d6 languageName: node linkType: hard @@ -9023,7 +9032,7 @@ __metadata: languageName: node linkType: hard -"react-hook-form@npm:^7.40.0, react-hook-form@npm:^7.43.9": +"react-hook-form@npm:^7.43.9": version: 7.43.9 resolution: "react-hook-form@npm:7.43.9" peerDependencies: @@ -10850,7 +10859,7 @@ __metadata: next: ^13.4.12 prettier: ^2.8.8 react: ^18.2.0 - react-admin: ^4.10.2 + react-admin: ^4.16.15 react-cookie: ^4.1.1 react-dom: ^18.2.0 react-dropzone: ^14.2.3