From bb7887cb9367b45b7513d7c5de87cf8f03cf4cd0 Mon Sep 17 00:00:00 2001 From: Kacper Donat <kadet1090@gmail.com> Date: Sat, 2 Jan 2021 23:11:33 +0100 Subject: [PATCH] Forms for editiing reporting form --- src/components/async.tsx | 1 - src/data/report.ts | 36 ++--- src/forms/report.tsx | 40 +++--- src/management/edition/list.tsx | 41 +++--- src/management/edition/manage.tsx | 82 +++++++++++ src/management/edition/report/fields/edit.tsx | 45 ++++++ src/management/edition/report/fields/form.tsx | 104 ++++++++++++++ src/management/edition/report/fields/list.tsx | 84 +++++++++++ src/management/main.tsx | 21 +-- src/management/routing.tsx | 5 + src/management/type/list.tsx | 2 - src/provider/dummy/report.ts | 130 ++++++++++-------- translations/management.pl.yaml | 19 +++ 13 files changed, 489 insertions(+), 121 deletions(-) create mode 100644 src/management/edition/manage.tsx create mode 100644 src/management/edition/report/fields/edit.tsx create mode 100644 src/management/edition/report/fields/form.tsx create mode 100644 src/management/edition/report/fields/list.tsx 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<string>[]; } -export interface BaseField { +export interface BaseFieldDefinition { name: string; description: Multilingual<string>; label: Multilingual<string>; } -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<string>[] | null; +export type TextFieldValue = string; + +export interface MultiChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices { + type: "checkbox"; } -export interface SingleChoiceField extends BaseField, PredefinedChoices { - type: "single-choice"; - value: Multilingual<string> | null +export type MultiChoiceValue = Multilingual<string>[]; + +export interface SingleChoiceFieldDefinition extends BaseFieldDefinition, PredefinedChoices { + type: "radio" | "select"; } -export interface SelectField extends BaseField, PredefinedChoices { - type: "select"; - value: Multilingual<string> | null -} +export type SingleChoiceValue = Multilingual<string>; -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<TField = ReportField> = { +export type ReportFieldProps<TField = ReportFieldDefinition> = { 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 <CustomField.Text {...props} field={ field } /> - case "single-choice": - case "multi-choice": + case "checkbox": + case "radio": return <CustomField.Choice {...props} field={ field }/> case "select": return <CustomField.Select {...props} field={ field }/> @@ -42,12 +43,16 @@ const CustomField = ({ field, ...props }: ReportFieldProps) => { CustomField.Text = ({ field }: ReportFieldProps) => { return <> - <Field label={ field.label.pl } name={ field.name } fullWidth component={ TextFieldFormik }/> + <Field label={ field.label.pl } name={ field.name } + fullWidth + rows={ field.type == "long-text" ? 4 : 1 } multiline={ field.type == "long-text" } + component={ TextFieldFormik } + /> <Typography variant="caption" color="textSecondary" dangerouslySetInnerHTML={{ __html: field.description.pl }}/> </> } -CustomField.Select = ({ field }: ReportFieldProps<SelectField>) => { +CustomField.Select = ({ field }: ReportFieldProps<SingleChoiceFieldDefinition>) => { const { t } = useTranslation(); const id = `custom-field-${field.name}`; const { values, setFieldValue } = useFormikContext<any>(); @@ -63,24 +68,24 @@ CustomField.Select = ({ field }: ReportFieldProps<SelectField>) => { </FormControl> } -CustomField.Choice = ({ field }: ReportFieldProps<SingleChoiceField | MultiChoiceField>) => { +CustomField.Choice = ({ field }: ReportFieldProps<SingleChoiceFieldDefinition | MultiChoiceFieldDefinition>) => { const { t } = useTranslation(); const { values, setFieldValue } = useFormikContext<any>(); const value = values[field.name]; - const isSelected = field.type == 'single-choice' + const isSelected = field.type == 'radio' ? (checked: Multilingual<string>) => value == checked : (checked: Multilingual<string>) => (value || []).includes(checked) - const handleChange = field.type == 'single-choice' + const handleChange = field.type == 'radio' ? (choice: Multilingual<string>) => () => setFieldValue(field.name, choice, false) : (choice: Multilingual<string>) => () => { const current = value || []; setFieldValue(field.name, !current.includes(choice) ? [ ...current, choice ] : current.filter((c: Multilingual<string>) => c != choice), false); } - const Component = field.type == 'single-choice' ? Radio : Checkbox; + const Component = field.type == 'radio' ? Radio : Checkbox; return <FormControl component="fieldset"> <FormLabel component="legend">{ field.label.pl }</FormLabel> @@ -94,19 +99,20 @@ CustomField.Choice = ({ field }: ReportFieldProps<SingleChoiceField | MultiChoic </FormControl> } -export type ReportFormValues = { [field: string]: any }; +export type ReportFormValues = ReportFieldValues; const reportToFormValuesTransformer: Transformer<Report, ReportFormValues, { report: Report }> = { 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() { <Grid item xs={12}> <Typography variant="body1" component="p">{ t('forms.report.instructions') }</Typography> </Grid> - { report.fields.map(field => <Grid item xs={12}><CustomField field={ field }/></Grid>) } + { schema.map(field => <Grid item xs={12}><CustomField field={ field }/></Grid>) } <Grid item xs={12}> <Actions> <Button variant="contained" color="primary" onClick={ submitForm }> diff --git a/src/management/edition/list.tsx b/src/management/edition/list.tsx index d718651..14b1248 100644 --- a/src/management/edition/list.tsx +++ b/src/management/edition/list.tsx @@ -1,23 +1,24 @@ -import React, { useCallback, useEffect } from "react"; +import React, { useCallback } from "react"; import { Page } from "@/pages/base"; import { useTranslation } from "react-i18next"; -import { useAsync, useAsyncState } from "@/hooks"; +import { useAsync } from "@/hooks"; import api from "@/management/api"; import { Async } from "@/components/async"; -import { Container, Typography } from "@material-ui/core"; -import MaterialTable, { Action, Column } from "material-table"; +import { Container, IconButton, Tooltip, Typography } from "@material-ui/core"; +import MaterialTable, { Column } from "material-table"; import { Edition } from "@/data/edition"; -import { Pencil } from "mdi-material-ui"; +import { FileFind } from "mdi-material-ui"; import { Management } from "../main"; -import { createPortal } from "react-dom"; -import { EditStaticPageDialog } from "@/management/page/create"; +import { useHistory } from "react-router-dom"; +import { route } from "@/routing"; +import { actionsColumn } from "@/management/common/helpers"; export type EditionDetailsProps = { edition: string; } export function EditionDetails({ edition, ...props }: EditionDetailsProps) { - const result = useAsync(useCallback(() => api.edition.details(edition), [ edition ])); + const result = useAsync(useCallback(() => api.edition.details(edition), [edition])); return <Async async={ result }>{ edition => <pre>{ JSON.stringify(edition, null, 2) }</pre> }</Async> } @@ -26,6 +27,15 @@ export function EditionsManagement() { const { t } = useTranslation("management"); const editions = useAsync(useCallback(api.edition.all, [])); + const ManageEditionAction = ({ edition }: { edition: Edition }) => { + const history = useHistory(); + const handlePagePreview = async () => history.push(route('management:edition_manage', { edition: edition.id || "" })); + + return <Tooltip title={ t("actions.manage") as string }> + <IconButton onClick={ handlePagePreview }><FileFind/></IconButton> + </Tooltip>; + } + const columns: Column<Edition>[] = [ { title: t("edition.field.id"), @@ -47,13 +57,9 @@ export function EditionsManagement() { customSort: (a, b) => a.course.name.localeCompare(b.course.name), render: edition => edition.course.name, }, - ] - - const actions: Action<Edition>[] = [ - { - icon: () => <Pencil />, - onClick: () => {}, - } + actionsColumn(edition => <> + <ManageEditionAction edition={ edition }/> + </>), ] return <Page> @@ -69,10 +75,9 @@ export function EditionsManagement() { <MaterialTable columns={ columns } data={ editions } - actions={ actions } - detailPanel={ edition => <EditionDetails edition={ edition.id as string } /> } + detailPanel={ edition => <EditionDetails edition={ edition.id as string }/> } title={ t("edition.index.title") } - options={{ search: false, actionsColumnIndex: -1 }} + options={ { search: false } } /> } </Async> diff --git a/src/management/edition/manage.tsx b/src/management/edition/manage.tsx new file mode 100644 index 0000000..7ba0a5c --- /dev/null +++ b/src/management/edition/manage.tsx @@ -0,0 +1,82 @@ +import React, { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouteMatch } from "react-router-dom"; +import { useAsync } from "@/hooks"; +import api from "@/management/api"; +import { Page } from "@/pages/base"; +import { Container, Paper, Typography } from "@material-ui/core"; +import { Management, ManagementLink } from "@/management/main"; +import { Edition } from "@/data/edition"; +import { Async } from "@/components/async"; +import { AccountMultiple, BriefcaseAccount, CertificateOutline, CogOutline, FileChartOutline, FormatPageBreak } from "mdi-material-ui"; +import { route } from "@/routing"; +import { useSpacing } from "@/styles"; +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; + +const useSectionStyles = makeStyles((theme: Theme) => createStyles({ + header: { + padding: theme.spacing(2), + paddingBottom: 0, + fontWeight: 'bold', + color: theme.palette.grey["800"], + }, +})) + +export function title(edition: Edition) { + return `${edition.course.name} - ${edition.startDate.year()}` +} + +export const ManageEditionPage = () => { + const { t } = useTranslation("management"); + const { params } = useRouteMatch(); + + const edition = useAsync<Edition>(useCallback(() => api.edition.details(params.edition), [params.edition])) + const spacing = useSpacing(2); + const classes = useSectionStyles(); + + return <Page> + <Async async={ edition }>{ edition => + <> + <Page.Header> + <Management.Breadcrumbs> + <Typography color="textPrimary">{ title(edition) }</Typography> + </Management.Breadcrumbs> + <Page.Title>{ title(edition) }</Page.Title> + </Page.Header> + <Container className={ spacing.vertical }> + <Paper elevation={ 2 }> + <Typography className={ classes.header }>{ t("edition.manage.internships") }</Typography> + <Management.Menu> + <ManagementLink icon={ <AccountMultiple/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.students.title") } + </ManagementLink> + <ManagementLink icon={ <BriefcaseAccount/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.internships.title") } + </ManagementLink> + <ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.ipp.title") } + </ManagementLink> + <ManagementLink icon={ <FileChartOutline/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.reports.title") } + </ManagementLink> + <ManagementLink icon={ <CertificateOutline/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.dean-approvals.title") } + </ManagementLink> + </Management.Menu> + </Paper> + <Paper elevation={ 2 }> + <Typography className={ classes.header }>{ t("edition.manage.management") }</Typography> + <Management.Menu> + <ManagementLink icon={ <FormatPageBreak/> } route={ route("management:edition_report_form", { edition: edition.id || "" }) }> + { t("management:edition.report-form.title") } + </ManagementLink> + <ManagementLink icon={ <CogOutline/> } route={ route("management:edition_settings", { edition: edition.id || "" }) }> + { t("management:edition.settings.title") } + </ManagementLink> + </Management.Menu> + </Paper> + </Container> + </> + }</Async> + </Page> +} diff --git a/src/management/edition/report/fields/edit.tsx b/src/management/edition/report/fields/edit.tsx new file mode 100644 index 0000000..0ced08d --- /dev/null +++ b/src/management/edition/report/fields/edit.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Button, Dialog, DialogActions, DialogContent, DialogProps, DialogTitle } from "@material-ui/core"; +import { ReportFieldDefinition } from "@/data/report"; +import { useTranslation } from "react-i18next"; +import { useSpacing } from "@/styles"; +import { Form, Formik } from "formik"; +import { Actions } from "@/components"; +import { Save } from "@material-ui/icons"; +import { Cancel } from "mdi-material-ui"; +import { FieldDefinitionForm, FieldDefinitionFormValues, fieldFormValuesTransformer, initialFieldFormValues } from "@/management/edition/report/fields/form"; + +export type EditFieldDialogProps = { + onSave?: (field: ReportFieldDefinition) => void; + field?: ReportFieldDefinition; +} & DialogProps; + +export function EditFieldDefinitionDialog({ onSave, field, ...props }: EditFieldDialogProps) { + const { t } = useTranslation("management"); + const spacing = useSpacing(3); + + const handleSubmit = (values: FieldDefinitionFormValues) => { + onSave?.(fieldFormValuesTransformer.reverseTransform(values)); + }; + + const initialValues = field + ? fieldFormValuesTransformer.transform(field) + : initialFieldFormValues; + + return <Dialog { ...props } maxWidth="md"> + <Formik initialValues={ initialValues } onSubmit={ handleSubmit }> + <Form className={ spacing.vertical }> + <DialogTitle>{ t(field ? "report-field.edit.title" : "report-field.create.title") }</DialogTitle> + <DialogContent> + <FieldDefinitionForm /> + </DialogContent> + <DialogActions> + <Actions> + <Button variant="contained" color="primary" startIcon={ <Save /> } type="submit">{ t("save") }</Button> + <Button startIcon={ <Cancel /> } onClick={ ev => props.onClose?.(ev, "escapeKeyDown") }>{ t("cancel") }</Button> + </Actions> + </DialogActions> + </Form> + </Formik> + </Dialog> +} 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<ReportFieldDefinition, FieldDefinitionFormValues> = identityTransformer; +export type ChoiceFieldProps = { name: string }; + +const ChoiceField = ({ field, form, meta }: FieldProps) => { + const { name } = field; + const { t } = useTranslation("management"); + const spacing = useSpacing(2); + + return <div className={ spacing.vertical }> + <Field label={ t("translation:language.pl") } name={`${name}.pl`} fullWidth component={ TextFieldFormik }/> + <Field label={ t("translation:language.en") } name={`${name}.en`} fullWidth component={ TextFieldFormik }/> + </div> +} + +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<any>(); + const classes = useStyles(); + + return <div className={ spacing.vertical }> + <Field label={ t("report-field.field.name") } name="name" fullWidth component={ TextFieldFormik }/> + <FormControl variant="outlined"> + <InputLabel htmlFor="report-field-type">{ t("report-field.field.type") }</InputLabel> + <Field + component={Select} + name="type" + label={ t("report-field.field.name") } + inputProps={{ id: 'report-field-type', }} + > + { reportFieldTypes.map(type => <MenuItem value={ type }>{ t(`report-field.type.${type}`) }</MenuItem>)} + </Field> + </FormControl> + <Typography variant="subtitle2">{ t("report-field.field.label") }</Typography> + <Field label={ t("translation:language.pl") } name="label.pl" fullWidth component={ TextFieldFormik }/> + <Field label={ t("translation:language.en") } name="label.en" fullWidth component={ TextFieldFormik }/> + <Typography variant="subtitle2">{ t("report-field.field.description") }</Typography> + <Field label={ t("translation:language.pl") } name="description.pl" fullWidth component={ CKEditorField }/> + <Field label={ t("translation:language.en") } name="description.en" fullWidth component={ CKEditorField }/> + + { ["radio", "select", "checkbox"].includes(values.type) && <> + <Typography variant="subtitle2">{ t("report-field.field.choices") }</Typography> + <FieldArray name="choices" render={ helper => <> + { values.choices.map((value: Multilingual<string>, index: number) => <Card> + <CardHeader subheader={ t("report-field.field.choice", { index }) } action={ <> + <IconButton onClick={ () => helper.remove(index) }> + <TrashCan /> + </IconButton> + </> }/> + <CardContent> + <Field name={`choices[${index}]`} component={ ChoiceField } /> + </CardContent> + </Card>) } + <Actions> + <Button variant="contained" startIcon={ <Add /> } color="primary" onClick={() => helper.push({ pl: "", en: "" })}>{ t("actions.add") }</Button> + </Actions> + </> } /> + </> } + + <div className={ classes.preview }> + <Typography variant="subtitle2">{ t("report-field.preview") }</Typography> + <FieldPreview field={ fieldFormValuesTransformer.reverseTransform(values) }/> + </div> + </div> +} 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 <Formik initialValues={{}} onSubmit={() => {}}> + <CustomField field={ field }/> + </Formik> +} + +export const EditionReportFields = () => { + const { t } = useTranslation("management"); + const schema = sampleReportSchema; + + const handleFieldDeletion = () => {} + + const DeleteFieldAction = createDeleteAction<ReportFieldDefinition>({ label: field => field.label.pl, onDelete: handleFieldDeletion }) + const EditFieldAction = ({ field }: { field: ReportFieldDefinition }) => { + const [ open, setOpen ] = useState<boolean>(false); + + const handleFieldSave = async (field: ReportFieldDefinition) => { + } + + return <> + <Tooltip title={ t("actions.edit") as any }> + <IconButton onClick={ () => setOpen(true) }><Edit /></IconButton> + </Tooltip> + { open && createPortal( + <EditFieldDefinitionDialog open={ open } onSave={ handleFieldSave } field={ field } onClose={ () => setOpen(false) }/>, + document.getElementById("modals") as Element + ) } + </> + } + + const columns: Column<ReportFieldDefinition>[] = [ + { + title: t("report-field.field.label"), + customSort: fieldComparator('label', multilingualStringComparator), + cellStyle: { whiteSpace: "nowrap" }, + render: field => <MultilingualCell value={ field.label }/>, + }, + { + title: t("report-field.field.type"), + cellStyle: { whiteSpace: "nowrap" }, + render: field => t(`report-field.type.${field.type}`), + }, + actionsColumn(field => <> + <EditFieldAction field={ field }/> + <DeleteFieldAction resource={ field }/> + </>), + ] + + return <Page> + <Page.Header maxWidth="lg"> + <Management.Breadcrumbs> + <Typography color="textPrimary">{ t(title) }</Typography> + </Management.Breadcrumbs> + <Page.Title>{ t(title) }</Page.Title> + </Page.Header> + <Container maxWidth="lg"> + <MaterialTable + columns={ columns } + data={ schema } + title={ t(title) } + detailPanel={ field => <Box p={3}><FieldPreview field={ field } /></Box> } + /> + </Container> + </Page> +} 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) => + <ListItem button component={ RouterLink } to={ route }> + <ListItemIcon>{ icon }</ListItemIcon> + <ListItemText>{ children }</ListItemText> + </ListItem> + export const Management = { Breadcrumbs: ({ children, ...props }: BreadcrumbsProps) => { const { t } = useTranslation(); @@ -14,7 +21,9 @@ export const Management = { <Link component={ RouterLink } to={ route("management:index") }>{ t("management:title") }</Link> { children } </Page.Breadcrumbs>; - } + }, + Menu: List, + MenuItem: ManagementLink, } type ManagementLinkProps = React.PropsWithChildren<{ @@ -22,12 +31,6 @@ type ManagementLinkProps = React.PropsWithChildren<{ route: string, }>; -const ManagementLink = ({ icon, route, children }: ManagementLinkProps) => - <ListItem button component={ RouterLink } to={ route }> - <ListItemIcon>{ icon }</ListItemIcon> - <ListItemText>{ children }</ListItemText> - </ListItem> - export const ManagementIndex = () => { const { t } = useTranslation(); @@ -37,7 +40,7 @@ export const ManagementIndex = () => { </Page.Header> <Container> <Paper elevation={ 2 }> - <List> + <Management.Menu> <ManagementLink icon={ <CalendarClock /> } route={ route("management:editions") }> { t("management:edition.index.title") } </ManagementLink> @@ -47,7 +50,7 @@ export const ManagementIndex = () => { <ManagementLink icon={ <FileDocumentMultipleOutline /> } route={ route("management:static_pages") }> { t("management:page.index.title") } </ManagementLink> - </List> + </Management.Menu> </Paper> </Container> </Page> 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 <strong>HTML</strong> description", + pl: "Pole tekstowe, z opisem w formacie <strong>HTML</strong>" + }, + label: { + en: "Text Field", + pl: "Pole tekstowe", + }, + }, + { + type: "long-text", + name: "long", + description: { + en: "Long text field, with <strong>HTML</strong> description", + pl: "Długie pole tekstowe, z opisem w formacie <strong>HTML</strong>" + }, + label: { + en: "Long Text Field", + pl: "Długie Pole tekstowe", + }, + }, + { + type: "radio", + name: "radio", + description: { + en: "single choice field, with <strong>HTML</strong> description", + pl: "Pole jednokrotnego wyboru, z opisem w formacie <strong>HTML</strong>" + }, + choices, + label: { + en: "Single choice field", + pl: "Pole jednokrotnego wyboru", + }, + }, + { + type: "select", + name: "select", + description: { + en: "select field, with <strong>HTML</strong> description", + pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie <strong>HTML</strong>" + }, + choices, + label: { + en: "Select field", + pl: "Pole jednokrotnego wyboru (selectbox)", + }, + }, + { + name: "multi", + type: "checkbox", + description: { + en: "Multiple choice field, with <strong>HTML</strong> description", + pl: "Pole wielokrotnego wyboru, z opisem w formacie <strong>HTML</strong>" + }, + choices, + label: { + en: "Multi choice field", + pl: "Pole wielokrotnego wyboru", + }, + }, +] + export const emptyReport: Report = { - fields: [ - { - type: "text", - name: "text", - description: { - en: "Text field, with <strong>HTML</strong> description", - pl: "Pole tekstowe, z opisem w formacie <strong>HTML</strong>" - }, - value: null, - label: { - en: "Text Field", - pl: "Pole tekstowe", - }, - }, - { - type: "single-choice", - name: "single", - description: { - en: "single choice field, with <strong>HTML</strong> description", - pl: "Pole jednokrotnego wyboru, z opisem w formacie <strong>HTML</strong>" - }, - value: null, - choices, - label: { - en: "Single choice field", - pl: "Pole jednokrotnego wyboru", - }, - }, - { - type: "select", - name: "select", - description: { - en: "select field, with <strong>HTML</strong> description", - pl: "Pole jednokrotnego wyboru z selectboxem, z opisem w formacie <strong>HTML</strong>" - }, - value: choices[2], - choices, - label: { - en: "Select field", - pl: "Pole jednokrotnego wyboru (selectbox)", - }, - }, - { - name: "multi", - type: "multi-choice", - description: { - en: "Multiple choice field, with <strong>HTML</strong> description", - pl: "Pole wielokrotnego wyboru, z opisem w formacie <strong>HTML</strong>" - }, - 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: