change default form layout to card

This commit is contained in:
Michael Schramm 2022-03-01 15:21:51 +01:00
parent 951dd2e5b4
commit fe51c528d2
63 changed files with 786 additions and 203 deletions

View File

@ -25,6 +25,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'react/prop-types': 'off', 'react/prop-types': 'off',
'@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-interface': 'off',
'jsx-a11y/no-autofocus': 'off', 'jsx-a11y/no-autofocus': 'off',

View File

@ -16,11 +16,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- update translations (https://github.com/ohmyform/ui/pull/70) - update translations (https://github.com/ohmyform/ui/pull/70)
- show warning icon in form list if not public - show warning icon in form list if not public
- default form layout is now "card" - default form layout is now "card"
- creating of new fields combined in new field types
### Fixed ### Fixed
- locale scripts were missing dependency - locale scripts were missing dependency
- edit user shows now email in title - edit user shows now email in title
- focus is now passed also do slide layout fields
- empty fields are no longer submitted
### Security ### Security

View File

@ -4,6 +4,7 @@ import { useCallback, useState } from 'react'
import { SubmissionFragment } from '../../../graphql/fragment/submission.fragment' import { SubmissionFragment } from '../../../graphql/fragment/submission.fragment'
import { useFormQuery } from '../../../graphql/query/form.query' import { useFormQuery } from '../../../graphql/query/form.query'
import { useSubmissionPagerImperativeQuery } from '../../../graphql/query/submission.pager.query' import { useSubmissionPagerImperativeQuery } from '../../../graphql/query/submission.pager.query'
import { fieldTypes } from '../types'
interface Props { interface Props {
form: string form: string
@ -66,9 +67,9 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
data.fields.forEach((field) => { data.fields.forEach((field) => {
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment fieldTypes[field.type]?.stringifyValue(field.value)
const decoded: { value: CellValue } = JSON.parse(field.value)
row.push(decoded.value || '') row.push(fieldTypes[field.type]?.stringifyValue(field.value))
} catch (e) { } catch (e) {
row.push('') row.push('')
} }

View File

@ -6,9 +6,8 @@ import { FieldData } from 'rc-field-form/lib/interface'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment' import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment'
import { fieldTypes } from '../types'
import { LogicBlock } from './logic.block' import { LogicBlock } from './logic.block'
import { adminTypes } from './types'
import { TextType } from './types/text.type'
interface Props { interface Props {
form: FormInstance form: FormInstance
@ -34,7 +33,7 @@ export const FieldCard: React.FC<Props> = ({
const type = form.getFieldValue([ const type = form.getFieldValue([
'form', 'fields', field.name as string, 'type', 'form', 'fields', field.name as string, 'type',
]) as string ]) as string
const TypeComponent = adminTypes[type] || TextType const TypeComponent = (fieldTypes[type] || fieldTypes['textfield']).adminFormField()
const [shouldUpdate, setShouldUpdate] = useState(false) const [shouldUpdate, setShouldUpdate] = useState(false)
const [nextTitle, setNextTitle] = useState<string>( const [nextTitle, setNextTitle] = useState<string>(

View File

@ -7,8 +7,8 @@ import { FieldData } from 'rc-field-form/lib/interface'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment' import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { fieldTypes } from '../types'
import { FieldCard } from './field.card' import { FieldCard } from './field.card'
import { adminTypes } from './types'
const logger = debug('FieldsTab') const logger = debug('FieldsTab')
@ -61,7 +61,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
}} }}
> >
<Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}> <Select value={nextType} onChange={(e) => setNextType(e)} style={{ minWidth: 200 }}>
{Object.keys(adminTypes).map((type) => ( {Object.keys(fieldTypes).map((type) => (
<Select.Option value={type} key={type}> <Select.Option value={type} key={type}>
{t(`type:${type}.name`)} {t(`type:${type}.name`)}
</Select.Option> </Select.Option>

View File

@ -7,6 +7,7 @@ import {
SubmissionFragment, SubmissionFragment,
SubmissionFragmentField, SubmissionFragmentField,
} from '../../../graphql/fragment/submission.fragment' } from '../../../graphql/fragment/submission.fragment'
import { fieldTypes } from '../types'
interface Props { interface Props {
form: FormPagerFragment form: FormPagerFragment
@ -30,21 +31,8 @@ export const SubmissionValues: React.FC<Props> = (props) => {
{ {
title: t('submission:value'), title: t('submission:value'),
render(_, row) { render(_, row) {
console.log('row.value', row.value)
try { try {
const data = JSON.parse(row.value) as { value: string } return fieldTypes[row.type]?.displayValue(row.value)
if (Array.isArray(data.value)) {
return (
<ul>
{data.value.map(r => (
<li key={r}>{JSON.stringify(r)}</li>
))}
</ul>
)
}
return data.value
} catch (e) { } catch (e) {
return row.value return row.value
} }

View File

@ -1,22 +0,0 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AdminFieldTypeProps } from './type.props'
export const adminTypes: {
[key: string]: ComponentType<AdminFieldTypeProps>
} = {
checkbox: dynamic<AdminFieldTypeProps>(() => import('./checkbox.type').then(c => c.CheckboxType )),
date: dynamic<AdminFieldTypeProps>(() => import('./date.type').then(c => c.DateType )),
dropdown: dynamic<AdminFieldTypeProps>(() => import('./dropdown.type').then(c => c.DropdownType )),
email: dynamic<AdminFieldTypeProps>(() => import('./email.type').then(c => c.EmailType )),
hidden: dynamic<AdminFieldTypeProps>(() => import('./hidden.type').then(c => c.HiddenType )),
link: dynamic<AdminFieldTypeProps>(() => import('./link.type').then(c => c.LinkType )),
location: dynamic<AdminFieldTypeProps>(() => import('./location.type').then(c => c.LocationType ), { ssr: false }),
number: dynamic<AdminFieldTypeProps>(() => import('./number.type').then(c => c.NumberType )),
radio: dynamic<AdminFieldTypeProps>(() => import('./radio.type').then(c => c.RadioType )),
rating: dynamic<AdminFieldTypeProps>(() => import('./rating.type').then(c => c.RatingType )),
slider: dynamic<AdminFieldTypeProps>(() => import('./slider.type').then(c => c.SliderType )),
textarea: dynamic<AdminFieldTypeProps>(() => import('./textarea.type').then(c => c.TextareaType )),
textfield: dynamic<AdminFieldTypeProps>(() => import('./text.type').then(c => c.TextType )),
yes_no: dynamic<AdminFieldTypeProps>(() => import('./yes_no.type').then(c => c.YesNoType )),
}

View File

@ -7,11 +7,9 @@ import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown' import { StyledMarkdown } from '../../../styled/markdown'
import { useRouter } from '../../../use.router' import { useRouter } from '../../../use.router'
import { fieldTypes } from '../../types' import { fieldTypes } from '../../types'
import { TextType } from '../../types/text.type'
import { FieldTypeProps } from '../../types/type.props'
interface Props { interface Props {
focus: boolean focus?: boolean
field: FormPublicFieldFragment field: FormPublicFieldFragment
design: FormPublicDesignFragment design: FormPublicDesignFragment
} }
@ -19,7 +17,7 @@ interface Props {
export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => { export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => {
const router = useRouter() const router = useRouter()
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType const FieldInput = (fieldTypes[field.type] || fieldTypes['text']).inputFormField()
const getUrlDefault = (): string => { const getUrlDefault = (): string => {
if (router.query[field.id]) { if (router.query[field.id]) {
@ -57,7 +55,12 @@ export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => {
<StyledMarkdown design={design} type={'question'} >{field.description}</StyledMarkdown> <StyledMarkdown design={design} type={'question'} >{field.description}</StyledMarkdown>
)} )}
<FieldInput design={design} field={field} urlValue={getUrlDefault()} focus={focus} /> <FieldInput
design={design}
field={field}
urlValue={getUrlDefault()}
focus={false}/* cannot use this in card layout! try with 2 input fields */
/>
</div> </div>
</div> </div>
) )

View File

@ -139,7 +139,14 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
return null return null
} }
return <Field key={field.id} field={field} design={design} focus={i === 0} /> return (
<Field
key={field.id}
field={field}
design={design}
focus={false}
/>
)
})} })}
<div <div
style={{ style={{

View File

@ -11,10 +11,9 @@ import { StyledH1 } from '../../../styled/h1'
import { StyledMarkdown } from '../../../styled/markdown' import { StyledMarkdown } from '../../../styled/markdown'
import { useRouter } from '../../../use.router' import { useRouter } from '../../../use.router'
import { fieldTypes } from '../../types' import { fieldTypes } from '../../types'
import { TextType } from '../../types/text.type'
import { FieldTypeProps } from '../../types/type.props'
interface Props { interface Props {
focus: boolean
field: FormPublicFieldFragment field: FormPublicFieldFragment
design: FormPublicDesignFragment design: FormPublicDesignFragment
@ -29,7 +28,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
const router = useRouter() const router = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType const FieldInput = (fieldTypes[field.type] || fieldTypes[field.type]).inputFormField()
const finish = (data) => { const finish = (data) => {
console.log('received field data', data) console.log('received field data', data)
@ -81,7 +80,12 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
<StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown> <StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown>
)} )}
<FieldInput design={design} field={field} urlValue={getUrlDefault()} /> <FieldInput
design={design}
field={field}
focus={props.focus}
urlValue={getUrlDefault()}
/>
</div> </div>
<div <div
style={{ style={{

View File

@ -77,6 +77,7 @@ export const SliderLayout: React.FC<LayoutProps> = (props) => {
<SwiperSlide key={field.id}> <SwiperSlide key={field.id}>
<Field <Field
field={field} field={field}
focus={swiper?.activeIndex === (startPage.show ? 1 : 0) + i}
design={design} design={design}
save={async (values: { [key: string]: unknown }) => { save={async (values: { [key: string]: unknown }) => {
await setField(field.id, values[field.id]) await setField(field.id, values[field.id])

View File

@ -0,0 +1,38 @@
import React, { ComponentType } from 'react'
import { FieldAdminProps } from './field.admin.props'
import { FieldInputProps } from './field.input.props'
export abstract class AbstractType<A = any> {
public parseValue(raw: string): A {
return JSON.parse(raw) as A
}
public parseUrlValue(raw: string): A {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return raw as any
}
public stringifyValue(raw: string): string {
return raw
}
public displayValue(raw: string): JSX.Element {
const data = this.parseValue(raw)
if (Array.isArray(data)) {
return (
<ul>
{data.map(r => (
<li key={r}>{JSON.stringify(r)}</li>
))}
</ul>
)
}
return <div>{this.stringifyValue(raw)}</div>
}
public abstract adminFormField(): ComponentType<FieldAdminProps>
public abstract inputFormField(): ComponentType<FieldInputProps>
}

View File

@ -1,9 +1,9 @@
import { Button, Col, Form, Input, Row } from 'antd' import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const CheckboxType: React.FC<AdminFieldTypeProps> = (props) => { export const CheckboxAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Checkbox, Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledCheckbox } from '../../styled/checkbox' import { StyledCheckbox } from '../../../styled/checkbox'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/checkbox') const logger = debug('checkbox.input')
export const CheckboxType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function CheckboxInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: string = undefined let initialValue: string = undefined
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={field.options initialValue={field.options
.map((option) => option.value) .map((option) => option.value)
@ -36,8 +44,13 @@ export const CheckboxType: React.FC<FieldTypeProps> = ({ field, design, urlValue
<Checkbox.Group> <Checkbox.Group>
{field.options {field.options
.filter((option) => option.key === null) .filter((option) => option.key === null)
.map((option) => ( .map((option, i) => (
<StyledCheckbox design={design} value={option.value} key={option.value}> <StyledCheckbox
design={design}
value={option.value}
key={option.value}
autoFocus={i === 0 && focus}
>
{option.title || option.value} {option.title || option.value}
</StyledCheckbox> </StyledCheckbox>
))} ))}

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class CheckboxType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./checkbox.admin').then(c => c.CheckboxAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./checkbox.input').then(c => c.builder(this)));
}
}

View File

@ -2,9 +2,9 @@ import { DatePicker, Form } from 'antd'
import moment, { Moment } from 'moment' import moment, { Moment } from 'moment'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => { export const DateAdmin: React.FC<FieldAdminProps> = ({ field }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -4,12 +4,20 @@ import debug from 'debug'
import moment, { Moment } from 'moment' import moment, { Moment } from 'moment'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledDateInput } from '../../styled/date.input' import { StyledDateInput } from '../../../styled/date.input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/date') const logger = debug('date.input')
export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function DateInput ({
field,
design,
urlValue,
focus,
}) {
const [min, setMin] = useState<Dayjs>() const [min, setMin] = useState<Dayjs>()
const [max, setMax] = useState<Dayjs>() const [max, setMax] = useState<Dayjs>()
const { t } = useTranslation() const { t } = useTranslation()
@ -29,20 +37,20 @@ export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, fo
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = moment(JSON.parse(field.defaultValue)) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = moment(urlValue) initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')} getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })} getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
@ -56,9 +64,11 @@ export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, fo
if (min && min.isAfter(d.toDate())) { if (min && min.isAfter(d.toDate())) {
return true return true
} }
if (max && max.isBefore(d.toDate())) { if (max && max.isBefore(d.toDate())) {
return true return true
} }
return false return false
}} }}
/> />

View File

@ -0,0 +1,24 @@
import moment, { Moment } from 'moment'
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class DateType extends AbstractType<Moment> {
parseValue(raw: string): Moment {
return moment(JSON.parse(raw))
}
parseUrlValue(raw: string): Moment {
return moment(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./date.admin').then(c => c.DateAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./date.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Button, Col, Form, Input, Row } from 'antd' import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const DropdownType: React.FC<AdminFieldTypeProps> = (props) => { export const DropdownAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,12 +2,20 @@ import { Form, Select } from 'antd'
import debug from 'debug' import debug from 'debug'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledSelect } from '../../styled/select' import { StyledSelect } from '../../../styled/select'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/dropdown') const logger = debug('field/dropdown')
export const DropdownType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function DateInput ({
field,
design,
urlValue,
focus,
}) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
@ -15,20 +23,20 @@ export const DropdownType: React.FC<FieldTypeProps> = ({ field, design, urlValue
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
> >

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class DropdownType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const EmailType: React.FC<AdminFieldTypeProps> = (props) => { export const EmailAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input' import { StyledInput } from '../../../styled/input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/email') const logger = debug('email.input')
export const EmailType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function EmailInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = null let initialValue = null
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[ rules={[
{ required: field.required, message: t('validation:valueRequired') }, { required: field.required, message: t('validation:valueRequired') },
{ type: 'email', message: t('validation:invalidEmail') }, { type: 'email', message: t('validation:invalidEmail') },

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class EmailType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./email.admin').then(c => c.EmailAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./email.input').then(c => c.builder(this)));
}
}

View File

@ -1,7 +1,7 @@
import { FormInstance } from 'antd/lib/form' import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface' import { FieldData } from 'rc-field-form/lib/interface'
export interface AdminFieldTypeProps { export interface FieldAdminProps {
form: FormInstance form: FormInstance
field: FieldData field: FieldData
} }

View File

@ -0,0 +1,6 @@
import { ComponentType } from 'react'
import { AbstractType } from './abstract.type'
import { FieldInputProps } from './field.input.props'
export type FieldInputBuilderType<A = AbstractType> = (type: A) => ComponentType<FieldInputProps>

View File

@ -3,7 +3,7 @@ import {
FormPublicFieldFragment, FormPublicFieldFragment,
} from '../../../graphql/fragment/form.public.fragment' } from '../../../graphql/fragment/form.public.fragment'
export interface FieldTypeProps { export interface FieldInputProps {
field: FormPublicFieldFragment field: FormPublicFieldFragment
design: FormPublicDesignFragment design: FormPublicDesignFragment
focus?: boolean focus?: boolean

View File

@ -1,9 +1,9 @@
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const HiddenType: React.FC<AdminFieldTypeProps> = (props) => { export const HiddenAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class HiddenType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./hidden.admin').then(c => c.HiddenAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return null;
}
}

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class ImageType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./dropdown.admin').then(c => c.DropdownAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./dropdown.input').then(c => c.builder(this)));
}
}

View File

@ -1,31 +1,34 @@
import React from 'react' import { AbstractType } from './abstract.type'
import { CheckboxType } from './checkbox.type' import { CheckboxType } from './checkbox'
import { DateType } from './date.type' import { DateType } from './date'
import { DropdownType } from './dropdown.type' import { DropdownType } from './dropdown'
import { EmailType } from './email.type' import { EmailType } from './email'
import { LinkType } from './link.type' import { HiddenType } from './hidden'
import { NumberType } from './number.type' import { LinkType } from './link'
import { RadioType } from './radio.type' import { LocationType } from './location'
import { RatingType } from './rating.type' import { NumberType } from './number'
import { SliderType } from './slider.type' import { RadioType } from './radio'
import { TextType } from './text.type' import { RatingType } from './rating'
import { TextareaType } from './textarea.type' import { SliderType } from './slider'
import { FieldTypeProps } from './type.props' import { TextareaType } from './textarea'
import { YesNoType } from './yes_no.type' import { TextfieldType } from './textfield'
import { YesNoType } from './yes_no'
export const fieldTypes: { export const fieldTypes: {
[key: string]: React.FC<FieldTypeProps> [key: string]: AbstractType
} = { } = {
date: DateType, checkbox: new CheckboxType(),
dropdown: DropdownType, date: new DateType(),
checkbox: CheckboxType, dropdown: new DropdownType(),
email: EmailType, email: new EmailType(),
link: LinkType, hidden: new HiddenType(),
number: NumberType, link: new LinkType(),
radio: RadioType, location: new LocationType(),
rating: RatingType, number: new NumberType(),
slider: SliderType, radio: new RadioType(),
textarea: TextareaType, rating: new RatingType(),
textfield: TextType, slider: new SliderType(),
yes_no: YesNoType, textarea: new TextareaType(),
textfield: new TextfieldType(),
yes_no: new YesNoType(),
} }

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class LinkType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./link.admin').then(c => c.LinkAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./link.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const LinkType: React.FC<AdminFieldTypeProps> = (props) => { export const LinkAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input' import { StyledInput } from '../../../styled/input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/link') const logger = debug('link.input')
export const LinkType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function LinkInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = null let initialValue = null
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[ rules={[
{ required: field.required, message: t('validation:valueRequired') }, { required: field.required, message: t('validation:valueRequired') },
{ type: 'url', message: t('validation:invalidUrl') }, { type: 'url', message: t('validation:invalidUrl') },

View File

@ -0,0 +1,34 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class LocationType extends AbstractType<{ lat: number, lng: number }> {
parseUrlValue(raw: string): { lat: number; lng: number } {
if (raw.includes(',')) {
const [lat, lng] = raw.split(',')
return {
lat: parseFloat(lat),
lng: parseFloat(lng),
}
}
throw new Error('no separator found')
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./location.admin').then(c => c.LocationAdmin), { ssr: false });
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./location.input').then(c => c.builder(this)), { ssr: false });
}
stringifyValue(raw: string): string {
const data = this.parseValue(raw)
return `${data.lat}, ${data.lng}`
}
}

View File

@ -3,9 +3,9 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MapContainer, TileLayer } from 'react-leaflet' import { MapContainer, TileLayer } from 'react-leaflet'
import { DraggableMarker } from '../../../map/draggable.marker' import { DraggableMarker } from '../../../map/draggable.marker'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const LocationType: React.FC<AdminFieldTypeProps> = (props) => { export const LocationAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
@ -105,7 +105,7 @@ export const LocationType: React.FC<AdminFieldTypeProps> = (props) => {
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url={tiles} url={tiles}
/> />
{center.lat && center.lng && ( {center?.lat && center?.lng && (
<DraggableMarker <DraggableMarker
value={center} value={center}
onChange={next => { onChange={next => {

View File

@ -0,0 +1,153 @@
import { Alert, Form, InputNumber, Space, Spin } from 'antd'
import debug from 'debug'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MapContainer, TileLayer } from 'react-leaflet'
import { DraggableMarker } from '../../../map/draggable.marker'
import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('location.number')
export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function LocationInput ({
field,
design,
urlValue,
focus,
}) {
const [initialZoom, setInitialZoom] = useState<number>(13)
const [tiles, setTiles] = useState<string>()
const [loading, setLoading] = useState(true)
const { t } = useTranslation()
useEffect(() => {
field.options.forEach((option) => {
if (option.key === 'initialZoom') {
try {
setInitialZoom(JSON.parse(option.value))
} catch (e) {
logger('invalid initialZoom value %O', e)
}
}
if (option.key === 'tiles') {
try {
setTiles(JSON.parse(option.value))
} catch (e) {
logger('invalid tiles value %O', e)
}
}
})
setLoading(false)
}, [field])
let initialValue: { lat: number, lng: number } = undefined
if (field.defaultValue) {
try {
initialValue = parseValue(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
try {
initialValue = parseUrlValue(urlValue)
} catch (e) {
logger('invalid url value %O', e)
}
}
if (loading) {
return (
<div>
<Spin />
</div>
)
}
if (!tiles) {
return <Alert message={'Tiles missing!'} />
}
return (
<div>
<Form.Item>
<Space>
<Form.Item
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
name={[
field.id,
'lat',
]}
initialValue={initialValue?.lat}
noStyle
>
<InputNumber addonAfter={'LAT'} precision={7} step={0.00001} max={90} min={-90} />
</Form.Item>
<Form.Item
rules={[{ required: field.required, message: t('validation:valueRequired') }]}
name={[
field.id,
'lng',
]}
initialValue={initialValue?.lng}
noStyle
>
<InputNumber addonAfter={'LNG'} precision={7} step={0.00001} max={180} min={-180} />
</Form.Item>
</Space>
</Form.Item>
<Form.Item dependencies={[[field.id, 'lat'], [field.id, 'lng']]}>
{(form) => {
const center = form.getFieldValue([field.id])
return (
<div>
<MapContainer
center={initialValue}
zoom={initialZoom}
style={{ height: 300, width: '100%' }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url={tiles}
/>
{center.lat && center.lng && (
<DraggableMarker
value={center}
onChange={next => {
form.setFields([
{
name: [
field.id,
'lng',
],
value: next.lng,
},
{
name: [
field.id,
'lat',
],
value: next.lat,
},
])
}}
/>
)}
</MapContainer>
</div>
)
}}
</Form.Item>
</div>
)
}

View File

@ -0,0 +1,19 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class NumberType extends AbstractType<number> {
parseUrlValue(raw: string): number {
return parseFloat(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./number.admin').then(c => c.NumberAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./number.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, InputNumber } from 'antd' import { Form, InputNumber } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const NumberType: React.FC<AdminFieldTypeProps> = (props) => { export const NumberAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledNumberInput } from '../../styled/number.input' import { StyledNumberInput } from '../../../styled/number.input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/number') const logger = debug('number.input')
export const NumberType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function NumberInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: number = undefined let initialValue: number = undefined
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = parseFloat(urlValue) initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[ rules={[
{ type: 'number', message: t('validation:invalidNumber') }, { type: 'number', message: t('validation:invalidNumber') },
{ required: field.required, message: t('validation:valueRequired') }, { required: field.required, message: t('validation:valueRequired') },

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class RadioType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./radio.admin').then(c => c.RadioAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./radio.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Button, Col, Form, Input, Row } from 'antd' import { Button, Col, Form, Input, Row } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const RadioType: React.FC<AdminFieldTypeProps> = (props) => { export const RadioAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Form, Radio } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledRadio } from '../../styled/radio' import { StyledRadio } from '../../../styled/radio'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/radio') const logger = debug('radio.input')
export const RadioType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function RadioInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: string = undefined let initialValue: string = undefined
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={field.options initialValue={field.options
.map((option) => option.value) .map((option) => option.value)

View File

@ -0,0 +1,19 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class RatingType extends AbstractType<number> {
parseUrlValue(raw: string): number {
return parseFloat(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./rating.admin').then(c => c.RatingAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./rating.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, Rate } from 'antd' import { Form, Rate } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const RatingType: React.FC<AdminFieldTypeProps> = (props) => { export const RatingAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,31 +2,39 @@ import { Form, Rate } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/rating') const logger = debug('rating.input')
export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function RatingInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: number = undefined let initialValue: number = undefined
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = parseFloat(urlValue) initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
> >

View File

@ -0,0 +1,19 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class SliderType extends AbstractType<number> {
parseUrlValue(raw: string): number {
return parseFloat(raw)
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./slider.admin').then(c => c.SliderAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./slider.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, InputNumber, Slider } from 'antd' import { Form, InputNumber, Slider } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const SliderType: React.FC<AdminFieldTypeProps> = (props) => { export const SliderAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,11 +2,19 @@ import { Form, Slider, Spin } from 'antd'
import debug from 'debug' import debug from 'debug'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/slider') const logger = debug('slider.input')
export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function SliderInput ({
field,
design,
urlValue,
focus,
}) {
const [min, setMin] = useState<number>() const [min, setMin] = useState<number>()
const [max, setMax] = useState<number>() const [max, setMax] = useState<number>()
const [step, setStep] = useState<number>() const [step, setStep] = useState<number>()
@ -46,14 +54,14 @@ export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = parseFloat(urlValue) initialValue = parseUrlValue(urlValue)
} }
if (loading) { if (loading) {
@ -67,11 +75,12 @@ export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
> >
<Slider <Slider
autoFocus={focus}
min={min} min={min}
max={max} max={max}
step={step} step={step}

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class TextareaType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./textarea.admin').then(c => c.TextareaAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./textarea.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const TextareaType: React.FC<AdminFieldTypeProps> = (props) => { export const TextareaAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,32 +2,40 @@ import { Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledTextareaInput } from '../../styled/textarea.input' import { StyledTextareaInput } from '../../../styled/textarea.input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/textarea') const logger = debug('textarea.input')
export const TextareaType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function TextareaInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = undefined let initialValue = undefined
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
> >

View File

@ -0,0 +1,15 @@
import dynamic from 'next/dynamic'
import { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class TextfieldType extends AbstractType<string> {
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./textfield.admin').then(c => c.TextfieldAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./textfield.input').then(c => c.builder(this)));
}
}

View File

@ -1,9 +1,9 @@
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const TextType: React.FC<AdminFieldTypeProps> = (props) => { export const TextfieldAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,12 +2,20 @@ import { Form } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyledInput } from '../../styled/input' import { StyledInput } from '../../../styled/input'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/text') const logger = debug('textfield.input')
export const TextType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function TextfieldInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
// TODO focus when becomes visible // TODO focus when becomes visible
@ -15,24 +23,29 @@ export const TextType: React.FC<FieldTypeProps> = ({ field, design, urlValue, fo
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue) { if (urlValue) {
initialValue = urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
> >
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} /> <StyledInput
autoFocus={focus}
design={design}
allowClear
size={'large'}
/>
</Form.Item> </Form.Item>
</div> </div>
) )

View File

@ -0,0 +1,36 @@
import { Tag } from 'antd'
import dynamic from 'next/dynamic'
import React, { ComponentType } from 'react'
import { AbstractType } from '../abstract.type'
import { FieldAdminProps } from '../field.admin.props'
import { FieldInputProps } from '../field.input.props'
export class YesNoType extends AbstractType<boolean> {
parseUrlValue(raw: string): boolean {
return !!raw
}
adminFormField(): ComponentType<FieldAdminProps> {
return dynamic(() => import('./yes_no.admin').then(c => c.YesNoAdmin));
}
inputFormField(): ComponentType<FieldInputProps> {
return dynamic(() => import('./yes_no.input').then(c => c.builder(this)));
}
stringifyValue(raw: string): string {
if (this.parseValue(raw)) {
return 'YES'
} else {
return 'NO'
}
}
displayValue(raw: string): JSX.Element {
if (this.parseValue(raw)) {
return <Tag color={'green'}>YES</Tag>
} else {
return <Tag color={'red'}>NO</Tag>
}
}
}

View File

@ -1,9 +1,9 @@
import { Form, Switch } from 'antd' import { Form, Switch } from 'antd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFieldTypeProps } from './type.props' import { FieldAdminProps } from '../field.admin.props'
export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => { export const YesNoAdmin: React.FC<FieldAdminProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (

View File

@ -2,11 +2,19 @@ import { Form, Switch } from 'antd'
import debug from 'debug' import debug from 'debug'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FieldTypeProps } from './type.props' import { FieldInputBuilderType } from '../field.input.builder.type'
const logger = debug('field/link') const logger = debug('yes_no.input')
export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const builder: FieldInputBuilderType = ({
parseUrlValue,
parseValue,
}) => function YesNoInput ({
field,
design,
urlValue,
focus,
}) {
const { t } = useTranslation() const { t } = useTranslation()
@ -14,20 +22,20 @@ export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
if (field.defaultValue) { if (field.defaultValue) {
try { try {
initialValue = JSON.parse(field.defaultValue) initialValue = parseValue(field.defaultValue)
} catch (e) { } catch (e) {
logger('invalid default value %O', e) logger('invalid default value %O', e)
} }
} }
if (urlValue !== undefined) { if (urlValue !== undefined) {
initialValue = !!urlValue initialValue = parseUrlValue(urlValue)
} }
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id]}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
valuePropName={'checked'} valuePropName={'checked'}

View File

@ -1,6 +1,11 @@
import { useMutation } from '@apollo/client' import { useMutation } from '@apollo/client'
import debug from 'debug' import debug from 'debug'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import {
SUBMISSION_FINISH_MUTATION,
SubmissionFinishMutationData,
SubmissionFinishMutationVariables,
} from '../graphql/mutation/submission.finish.mutation'
import { import {
SUBMISSION_SET_FIELD_MUTATION, SUBMISSION_SET_FIELD_MUTATION,
SubmissionSetFieldMutationData, SubmissionSetFieldMutationData,
@ -11,11 +16,6 @@ import {
SubmissionStartMutationData, SubmissionStartMutationData,
SubmissionStartMutationVariables, SubmissionStartMutationVariables,
} from '../graphql/mutation/submission.start.mutation' } from '../graphql/mutation/submission.start.mutation'
import {
SUBMISSION_FINISH_MUTATION,
SubmissionFinishMutationData,
SubmissionFinishMutationVariables,
} from '../graphql/mutation/submission.finish.mutation'
const logger = debug('useSubmission') const logger = debug('useSubmission')
@ -65,8 +65,12 @@ export const useSubmission = (formId: string): Submission => {
}, [formId]) }, [formId])
const setField = useCallback( const setField = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any async (fieldId: string, data: unknown) => {
async (fieldId: string, data: any) => { if (data === undefined || data === null) {
logger('skip save field id=%O %O', fieldId, data)
return
}
logger('save field id=%O %O', fieldId, data) logger('save field id=%O %O', fieldId, data)
await save({ await save({
variables: { variables: {

View File

@ -82,7 +82,6 @@ const Create: NextPage = () => {
noStyle noStyle
name={[ name={[
'form', 'form',
'design',
'layout', 'layout',
]} ]}
initialValue={'card'} initialValue={'card'}