diff --git a/src/components/async.tsx b/src/components/async.tsx index f99fbca..82b7e8e 100644 --- a/src/components/async.tsx +++ b/src/components/async.tsx @@ -1,6 +1,5 @@ import { AsyncResult } from "@/hooks"; import React from "react"; -import { CircularProgress } from "@material-ui/core"; import { Alert } from "@material-ui/lab"; import { Loading } from "@/components/loading"; diff --git a/src/data/report.ts b/src/data/report.ts index d399ce8..a5eeafc 100644 --- a/src/data/report.ts +++ b/src/data/report.ts @@ -4,34 +4,38 @@ interface PredefinedChoices { choices: Multilingual[]; } -export interface BaseField { +export interface BaseFieldDefinition { name: string; description: Multilingual; label: Multilingual; } -export interface TextField extends BaseField { - type: "text"; - value: string | null; +export interface TextFieldDefinition extends BaseFieldDefinition { + type: "short-text" | "long-text"; } -export interface MultiChoiceField extends BaseField, PredefinedChoices { - type: "multi-choice"; - value: Multilingual[] | null; +export type TextFieldValue = string; + +export interface MultiChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices { + type: "checkbox"; } -export interface SingleChoiceField extends BaseField, PredefinedChoices { - type: "single-choice"; - value: Multilingual | null +export type MultiChoiceValue = Multilingual[]; + +export interface SingleChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices { + type: "radio" | "select"; } -export interface SelectField extends BaseField, PredefinedChoices { - type: "select"; - value: Multilingual | null -} +export type SingleChoiceValue = Multilingual; -export type ReportField = TextField | MultiChoiceField | SingleChoiceField | SelectField; +export type ReportFieldDefinition = TextFieldDefinition | MultiChoiceFieldDefinition | SingleChoiceFieldDefinition; +export type ReportFieldValue = TextFieldValue | MultiChoiceValue | SingleChoiceValue; +export type ReportFieldValues = { [field: string]: ReportFieldValue }; +export type ReportSchema = ReportFieldDefinition[]; export interface Report { - fields: ReportField[]; + fields: ReportFieldValues; } + +export const reportFieldTypes = ["short-text", "long-text", "checkbox", "radio", "select"]; + diff --git a/src/forms/report.tsx b/src/forms/report.tsx index a4936bf..dfb08f7 100644 --- a/src/forms/report.tsx +++ b/src/forms/report.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { emptyReport } from "@/provider/dummy/report"; +import { emptyReport, sampleReportSchema } from "@/provider/dummy/report"; import { Button, FormControl, @@ -18,22 +18,23 @@ import { Actions } from "@/components"; import { Link as RouterLink } from "react-router-dom"; import { route } from "@/routing"; import { useTranslation } from "react-i18next"; -import { MultiChoiceField, Report, ReportField, SelectField, SingleChoiceField } from "@/data/report"; +import { MultiChoiceFieldDefinition, Report, ReportFieldDefinition, ReportFieldValues, SingleChoiceFieldDefinition } from "@/data/report"; import { TextField as TextFieldFormik } from "formik-material-ui"; import { Field, Form, Formik, useFormik, useFormikContext } from "formik"; import { Multilingual } from "@/data"; import { Transformer } from "@/serialization"; -export type ReportFieldProps = { +export type ReportFieldProps = { field: TField; } -const CustomField = ({ field, ...props }: ReportFieldProps) => { +export const CustomField = ({ field, ...props }: ReportFieldProps) => { switch (field.type) { - case "text": + case "short-text": + case "long-text": return - case "single-choice": - case "multi-choice": + case "checkbox": + case "radio": return case "select": return @@ -42,12 +43,16 @@ const CustomField = ({ field, ...props }: ReportFieldProps) => { CustomField.Text = ({ field }: ReportFieldProps) => { return <> - + } -CustomField.Select = ({ field }: ReportFieldProps) => { +CustomField.Select = ({ field }: ReportFieldProps) => { const { t } = useTranslation(); const id = `custom-field-${field.name}`; const { values, setFieldValue } = useFormikContext(); @@ -63,24 +68,24 @@ CustomField.Select = ({ field }: ReportFieldProps) => { } -CustomField.Choice = ({ field }: ReportFieldProps) => { +CustomField.Choice = ({ field }: ReportFieldProps) => { const { t } = useTranslation(); const { values, setFieldValue } = useFormikContext(); const value = values[field.name]; - const isSelected = field.type == 'single-choice' + const isSelected = field.type == 'radio' ? (checked: Multilingual) => value == checked : (checked: Multilingual) => (value || []).includes(checked) - const handleChange = field.type == 'single-choice' + const handleChange = field.type == 'radio' ? (choice: Multilingual) => () => setFieldValue(field.name, choice, false) : (choice: Multilingual) => () => { const current = value || []; setFieldValue(field.name, !current.includes(choice) ? [ ...current, choice ] : current.filter((c: Multilingual) => c != choice), false); } - const Component = field.type == 'single-choice' ? Radio : Checkbox; + const Component = field.type == 'radio' ? Radio : Checkbox; return { field.label.pl } @@ -94,19 +99,20 @@ CustomField.Choice = ({ field }: ReportFieldProps } -export type ReportFormValues = { [field: string]: any }; +export type ReportFormValues = ReportFieldValues; const reportToFormValuesTransformer: Transformer = { reverseTransform(subject: ReportFormValues, context: { report: Report }): Report { - return { ...context.report }; + return { ...context.report, fields: subject }; }, transform(subject: Report, context: undefined): ReportFormValues { - return Object.fromEntries(subject.fields.map(field => [ field.name, field.value ])); + return subject.fields; } } export default function ReportForm() { const report = emptyReport; + const schema = sampleReportSchema; const { t } = useTranslation(); const handleSubmit = async () => {}; @@ -117,7 +123,7 @@ export default function ReportForm() { { t('forms.report.instructions') } - { report.fields.map(field => ) } + { schema.map(field => ) } + + + + + + +} diff --git a/src/management/edition/report/fields/form.tsx b/src/management/edition/report/fields/form.tsx new file mode 100644 index 0000000..2c720df --- /dev/null +++ b/src/management/edition/report/fields/form.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { ReportFieldDefinition, reportFieldTypes } from "@/data/report"; +import { identityTransformer, Transformer } from "@/serialization"; +import { useTranslation } from "react-i18next"; +import { useSpacing } from "@/styles"; +import { Field, FieldArray, FieldProps, useFormikContext } from "formik"; +import { TextField as TextFieldFormik, Select } from "formik-material-ui"; +import { FormControl, InputLabel, Typography, MenuItem, Card, Box, Button, CardContent, CardHeader, IconButton } from "@material-ui/core"; +import { CKEditorField } from "@/field/ckeditor"; +import { Multilingual } from "@/data"; +import { Actions } from "@/components"; +import { Add } from "@material-ui/icons"; +import { TrashCan } from "mdi-material-ui"; +import { FieldPreview } from "@/management/edition/report/fields/list"; +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; + +export type FieldDefinitionFormValues = ReportFieldDefinition | { type: string }; + +export const initialFieldFormValues: FieldDefinitionFormValues = { + type: "short-text", + name: "", + description: { + pl: "", + en: "", + }, + label: { + pl: "", + en: "", + }, + choices: [], +} + +export const fieldFormValuesTransformer: Transformer = identityTransformer; +export type ChoiceFieldProps = { name: string }; + +const ChoiceField = ({ field, form, meta }: FieldProps) => { + const { name } = field; + const { t } = useTranslation("management"); + const spacing = useSpacing(2); + + return
+ + +
+} + +const useStyles = makeStyles((theme: Theme) => createStyles({ + preview: { + padding: theme.spacing(2), + backgroundColor: "#e9f0f5", + }, +})) + +export function FieldDefinitionForm() { + const { t } = useTranslation("management"); + const spacing = useSpacing(2); + const { values } = useFormikContext(); + const classes = useStyles(); + + return
+ + + { t("report-field.field.type") } + + { reportFieldTypes.map(type => { t(`report-field.type.${type}`) })} + + + { t("report-field.field.label") } + + + { t("report-field.field.description") } + + + + { ["radio", "select", "checkbox"].includes(values.type) && <> + { t("report-field.field.choices") } + <> + { values.choices.map((value: Multilingual, index: number) => + + helper.remove(index) }> + + + }/> + + + + ) } + + + + } /> + } + +
+ { t("report-field.preview") } + +
+
+} diff --git a/src/management/edition/report/fields/list.tsx b/src/management/edition/report/fields/list.tsx new file mode 100644 index 0000000..2e62e15 --- /dev/null +++ b/src/management/edition/report/fields/list.tsx @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import { Page } from "@/pages/base"; +import { Management } from "@/management/main"; +import { Box, Container, IconButton, Tooltip, Typography } from "@material-ui/core"; +import { useTranslation } from "react-i18next"; +import { sampleReportSchema } from "@/provider/dummy/report"; +import MaterialTable, { Column } from "material-table"; +import { actionsColumn, fieldComparator, multilingualStringComparator } from "@/management/common/helpers"; +import { MultilingualCell } from "@/management/common/MultilangualCell"; +import { ReportFieldDefinition } from "@/data/report"; +import { Formik } from "formik"; +import { CustomField } from "@/forms/report"; +import { Edit } from "@material-ui/icons"; +import { createPortal } from "react-dom"; +import { createDeleteAction } from "@/management/common/DeleteResourceAction"; +import { EditFieldDefinitionDialog } from "@/management/edition/report/fields/edit"; + +const title = "edition.report-fields.title"; + +export const FieldPreview = ({ field }: { field: ReportFieldDefinition }) => { + return {}}> + + +} + +export const EditionReportFields = () => { + const { t } = useTranslation("management"); + const schema = sampleReportSchema; + + const handleFieldDeletion = () => {} + + const DeleteFieldAction = createDeleteAction({ label: field => field.label.pl, onDelete: handleFieldDeletion }) + const EditFieldAction = ({ field }: { field: ReportFieldDefinition }) => { + const [ open, setOpen ] = useState(false); + + const handleFieldSave = async (field: ReportFieldDefinition) => { + } + + return <> + + setOpen(true) }> + + { open && createPortal( + setOpen(false) }/>, + document.getElementById("modals") as Element + ) } + + } + + const columns: Column[] = [ + { + title: t("report-field.field.label"), + customSort: fieldComparator('label', multilingualStringComparator), + cellStyle: { whiteSpace: "nowrap" }, + render: field => , + }, + { + title: t("report-field.field.type"), + cellStyle: { whiteSpace: "nowrap" }, + render: field => t(`report-field.type.${field.type}`), + }, + actionsColumn(field => <> + + + ), + ] + + return + + + { t(title) } + + { t(title) } + + + } + /> + + +} diff --git a/src/management/main.tsx b/src/management/main.tsx index 4a990ce..73b8dcb 100644 --- a/src/management/main.tsx +++ b/src/management/main.tsx @@ -6,6 +6,13 @@ import { route } from "@/routing"; import { useTranslation } from "react-i18next"; import { CalendarClock, FileCertificateOutline, FileDocumentMultipleOutline } from "mdi-material-ui"; + +export const ManagementLink = ({ icon, route, children }: ManagementLinkProps) => + + { icon } + { children } + + export const Management = { Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => { const { t } = useTranslation(); @@ -14,7 +21,9 @@ export const Management = { { t("management:title") } { children } ; - } + }, + Menu: List, + MenuItem: ManagementLink, } type ManagementLinkProps = React.PropsWithChildren<{ @@ -22,12 +31,6 @@ type ManagementLinkProps = React.PropsWithChildren<{ route: string, }>; -const ManagementLink = ({ icon, route, children }: ManagementLinkProps) => - - { icon } - { children } - - export const ManagementIndex = () => { const { t } = useTranslation(); @@ -37,7 +40,7 @@ export const ManagementIndex = () => { - + } route={ route("management:editions") }> { t("management:edition.index.title") } @@ -47,7 +50,7 @@ export const ManagementIndex = () => { } route={ route("management:static_pages") }> { t("management:page.index.title") } - + diff --git a/src/management/routing.tsx b/src/management/routing.tsx index a994e67..8e35409 100644 --- a/src/management/routing.tsx +++ b/src/management/routing.tsx @@ -5,11 +5,16 @@ import React from "react"; import { ManagementIndex } from "@/management/main"; import StaticPageManagement from "@/management/page/list"; import { InternshipTypeManagement } from "@/management/type/list"; +import { ManageEditionPage } from "@/management/edition/manage"; +import { EditionReportFields } from "@/management/edition/report/fields/list"; export const managementRoutes: Route[] = ([ { name: "index", path: "/", content: ManagementIndex, exact: true }, + { name: "edition_report_form", path: "/editions/:edition/report", content: EditionReportFields }, + { name: "edition_manage", path: "/editions/:edition", content: ManageEditionPage }, { name: "editions", path: "/editions", content: EditionsManagement }, + { name: "types", path: "/types", content: InternshipTypeManagement }, { name: "static_pages", path: "/static-pages", content: StaticPageManagement } ] as Route[]).map( diff --git a/src/management/type/list.tsx b/src/management/type/list.tsx index 7d34c66..ebad003 100644 --- a/src/management/type/list.tsx +++ b/src/management/type/list.tsx @@ -17,10 +17,8 @@ import { BulkActions } from "@/management/common/BulkActions"; import { useSpacing } from "@/styles"; import { Actions } from "@/components"; import { MultilingualCell } from "@/management/common/MultilangualCell"; -import { default as StaticPage } from "@/data/page"; import { Add, Edit } from "@material-ui/icons"; import { createPortal } from "react-dom"; -import { EditStaticPageDialog } from "@/management/page/edit"; import { EditInternshipTypeDialog } from "@/management/type/edit"; const title = "type.index.title"; diff --git a/src/provider/dummy/report.ts b/src/provider/dummy/report.ts index ad77aad..8973586 100644 --- a/src/provider/dummy/report.ts +++ b/src/provider/dummy/report.ts @@ -1,66 +1,80 @@ -import { Report } from "@/data/report"; +import { Report, ReportSchema } from "@/data/report"; const choices = [1, 2, 3, 4, 5].map(n => ({ pl: `Wybór ${n}`, en: `Choice ${n}` })) +export const sampleReportSchema: ReportSchema = [ + { + type: "short-text", + name: "short", + description: { + en: "Text field, with HTML description", + pl: "Pole tekstowe, z opisem w formacie HTML" + }, + label: { + en: "Text Field", + pl: "Pole tekstowe", + }, + }, + { + type: "long-text", + name: "long", + description: { + en: "Long text field, with HTML description", + pl: "Długie pole tekstowe, z opisem w formacie HTML" + }, + label: { + en: "Long Text Field", + pl: "Długie Pole tekstowe", + }, + }, + { + type: "radio", + name: "radio", + description: { + en: "single choice field, with HTML description", + pl: "Pole jednokrotnego wyboru, z opisem w formacie HTML" + }, + choices, + label: { + en: "Single choice field", + pl: "Pole jednokrotnego wyboru", + }, + }, + { + type: "select", + name: "select", + description: { + en: "select field, with HTML description", + pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie HTML" + }, + choices, + label: { + en: "Select field", + pl: "Pole jednokrotnego wyboru (selectbox)", + }, + }, + { + name: "multi", + type: "checkbox", + description: { + en: "Multiple choice field, with HTML description", + pl: "Pole wielokrotnego wyboru, z opisem w formacie HTML" + }, + choices, + label: { + en: "Multi choice field", + pl: "Pole wielokrotnego wyboru", + }, + }, +] + export const emptyReport: Report = { - fields: [ - { - type: "text", - name: "text", - description: { - en: "Text field, with HTML description", - pl: "Pole tekstowe, z opisem w formacie HTML" - }, - value: null, - label: { - en: "Text Field", - pl: "Pole tekstowe", - }, - }, - { - type: "single-choice", - name: "single", - description: { - en: "single choice field, with HTML description", - pl: "Pole jednokrotnego wyboru, z opisem w formacie HTML" - }, - value: null, - choices, - label: { - en: "Single choice field", - pl: "Pole jednokrotnego wyboru", - }, - }, - { - type: "select", - name: "select", - description: { - en: "select field, with HTML description", - pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie HTML" - }, - value: choices[2], - choices, - label: { - en: "Select field", - pl: "Pole jednokrotnego wyboru (selectbox)", - }, - }, - { - name: "multi", - type: "multi-choice", - description: { - en: "Multiple choice field, with HTML description", - pl: "Pole wielokrotnego wyboru, z opisem w formacie HTML" - }, - value: [ choices[0], choices[3] ], - choices, - label: { - en: "Multi choice field", - pl: "Pole wielokrotnego wyboru", - }, - }, - ] + fields: { + "short": "Testowa wartość", + "select": choices[0], + "multi": [ choices[1], choices[2] ], + } } diff --git a/translations/management.pl.yaml b/translations/management.pl.yaml index 1367f4e..993c35b 100644 --- a/translations/management.pl.yaml +++ b/translations/management.pl.yaml @@ -11,6 +11,7 @@ actions: preview: Podgląd delete: Usuń edit: Edytuj + add: Dodaj edition: index: @@ -20,6 +21,24 @@ edition: start: Początek end: Koniec course: Kierunek + report-fields: + title: "Pola formularza raportu praktyki" + +report-field: + preview: Podgląd + field: + type: "Rodzaj" + name: "Unikalny identyfikator" + label: "Etykieta" + description: "Opis" + choices: "Możliwe wybory" + choice: "Wybór #{{ index }}" + type: + select: "Pole wyboru" + radio: "Jednokrotny wybór (radio)" + checkbox: "Wielokrotny wybór (checkboxy)" + short-text: "Pole krótkiej odpowiedzi" + long-text: "Pole długiej odpowiedzi" type: index: