upgrade packages, improve field logic, fix slider, hide empty submissions, fix urls for buttons, improve data handling for fields, improve sqlite migration handling

This commit is contained in:
Michael Schramm 2022-02-27 12:58:52 +01:00
parent e33b3ff392
commit ca5edbbb3b
40 changed files with 282 additions and 137 deletions

View File

@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- new slider field type - new slider field type
- new card layout for forms - new card layout for forms
- field logic - field logic
- add enviroment config - add environment config
- anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108) - anonymous form submissions (fixes https://github.com/ohmyform/ohmyform/issues/108)
- checkbox field type (fixed https://github.com/ohmyform/ohmyform/issues/138) - checkbox field type (fixed https://github.com/ohmyform/ohmyform/issues/138)
@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- use exported hooks for graphql - use exported hooks for graphql
- disable swipe gesture - disable swipe gesture
- upgrade to nextjs 12 - upgrade to nextjs 12
- change default value from value to defaultValue
- handle options and values as json correctly
- exclude empty submissions per default (https://github.com/ohmyform/ohmyform/issues/153)
### Fixed ### Fixed
@ -153,11 +156,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed ### Changed
- `export` uses now spa mode for initial loading screen - `export` uses now spa mode for initial loading screen
- change value to defaultValue for initial form
### Fixed ### Fixed
- [OMF#93](https://github.com/ohmyform/ohmyform/issues/93) dropdown options are not saved - dropdown options are not saved (https://github.com/ohmyform/ohmyform/issues/93)
- redirect attempts on static export - redirect attempts on static export
- date can now be prefilled by url
## [0.9.2] - 2020-06-04 ## [0.9.2] - 2020-06-04

View File

@ -78,7 +78,6 @@ export const FieldsTab: React.FC<Props> = (props) => {
title: '', title: '',
description: '', description: '',
required: false, required: false,
value: '',
} }
add(defaults) add(defaults)

View File

@ -37,7 +37,7 @@ export const LogicBlock: React.FC<Props> = ({
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
label={'Formula'} label={'Formula'}
rules={[{ required: true, message: 'combine other fields' }]} rules={[{ required: true, message: 'combine other fields' }]}
extra={'Save form to get new IDs and slugs'} extra={'Save form to get new @IDs and $slugs. (example: $slug < 21 or @id = 42)'}
> >
<Mentions rows={1}> <Mentions rows={1}>
{fields.map((field) => ( {fields.map((field) => (
@ -54,10 +54,10 @@ export const LogicBlock: React.FC<Props> = ({
const defaults = {} const defaults = {}
fields.forEach((field) => { fields.forEach((field) => {
defaults[`@${field.id}`] = field.value defaults[`@${field.id}`] = field.defaultValue
if (field.slug) { if (field.slug) {
defaults[`$${field.slug}`] = field.value defaults[`$${field.slug}`] = field.defaultValue
} }
}) })

View File

@ -10,7 +10,7 @@ export const CheckboxType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:checkbox:default')} label={t('type:checkbox:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />

View File

@ -11,7 +11,7 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:date.default')} label={t('type:date.default')}
name={[field.name as string, 'value']} name={[field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
getValueFromEvent={(e: Moment) => (e ? e.format('YYYY-MM-DD') : undefined)} getValueFromEvent={(e: Moment) => (e ? e.format('YYYY-MM-DD') : undefined)}
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })} getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}

View File

@ -10,7 +10,7 @@ export const DropdownType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:dropdown.default')} label={t('type:dropdown.default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />

View File

@ -10,7 +10,7 @@ export const EmailType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:email.default')} label={t('type:email.default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'email', message: t('validation:emailRequired') }]} rules={[{ type: 'email', message: t('validation:emailRequired') }]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >

View File

@ -10,7 +10,7 @@ export const HiddenType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:hidden.default')} label={t('type:hidden.default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />

View File

@ -10,7 +10,7 @@ export const LinkType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:link.default')} label={t('type:link.default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
rules={[{ type: 'url', message: t('validation:invalidUrl') }]} rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >

View File

@ -10,12 +10,8 @@ export const NumberType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:number:default')} label={t('type:number:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
> >
<InputNumber precision={2} /> <InputNumber precision={2} />
</Form.Item> </Form.Item>

View File

@ -10,7 +10,7 @@ export const RadioType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:radio:default')} label={t('type:radio:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />

View File

@ -10,13 +10,9 @@ export const RatingType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:rating:default')} label={t('type:rating:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
extra={t('type:rating.clearNote')} extra={t('type:rating.clearNote')}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
> >
<Rate allowHalf allowClear /> <Rate allowHalf allowClear />
</Form.Item> </Form.Item>

View File

@ -35,11 +35,8 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
return ( return (
<Form.Item <Form.Item
label={t('type:slider.default')} label={t('type:slider.default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })} getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
> >
<Slider min={min} max={max} step={step} dots={(max - min) / step <= 10} /> <Slider min={min} max={max} step={step} dots={(max - min) / step <= 10} />
@ -57,10 +54,6 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
]} ]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={0} initialValue={0}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(e: string) => ({ value: e ? parseFloat(e) : undefined })}
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
@ -74,10 +67,6 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
]} ]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={100} initialValue={100}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(e: string) => ({ value: e ? parseFloat(e) : undefined })}
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>
@ -91,10 +80,6 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
]} ]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={1} initialValue={1}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(e: string) => ({ value: e ? parseFloat(e) : undefined })}
> >
<InputNumber /> <InputNumber />
</Form.Item> </Form.Item>

View File

@ -9,7 +9,7 @@ export const TextType: React.FC<AdminFieldTypeProps> = (props) => {
return ( return (
<Form.Item <Form.Item
label={t('type:textfield:default')} label={t('type:textfield:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input /> <Input />

View File

@ -10,7 +10,7 @@ export const TextareaType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:textarea:default')} label={t('type:textarea:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />

View File

@ -10,11 +10,9 @@ export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => {
<div> <div>
<Form.Item <Form.Item
label={t('type:yes_no:default')} label={t('type:yes_no:default')}
name={[props.field.name as string, 'value']} name={[props.field.name as string, 'defaultValue']}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
valuePropName={'checked'} valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>

View File

@ -1,6 +1,7 @@
import { Card, Form, message, Modal, Spin } from 'antd' import { Card, Form, message, Modal, Spin } from 'antd'
import debug from 'debug'
import { darken, lighten } from 'polished' import { darken, lighten } from 'polished'
import React, { useCallback, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicFieldFragment } from '../../../../graphql/fragment/form.public.fragment' import { FormPublicFieldFragment } from '../../../../graphql/fragment/form.public.fragment'
@ -13,6 +14,8 @@ import { Page } from './page'
type Step = 'start' | 'form' | 'end' type Step = 'start' | 'form' | 'end'
const logger = debug('layout/card')
const MyCard = styled.div<{ background: string }>` const MyCard = styled.div<{ background: string }>`
background: ${(props) => darken(0.1, props.background)}; background: ${(props) => darken(0.1, props.background)};
height: 100%; height: 100%;
@ -41,8 +44,28 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
const { design, startPage, endPage, fields } = props.form const { design, startPage, endPage, fields } = props.form
const { setField } = props.submission const { setField } = props.submission
const updateValues = useCallback(() => {
const defaults = {}
fields.forEach(field => {
const defaultValue = field.defaultValue ? JSON.parse(field.defaultValue) : null
defaults[`@${field.id}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
if (field.slug) {
defaults[`$${field.slug}`] = form.getFieldValue([field.id, 'value']) ?? defaultValue
}
})
setValues(defaults)
}, [fields, form])
useEffect(() => {
updateValues()
}, [updateValues])
const finish = async (data: { [key: number]: unknown }) => { const finish = async (data: { [key: number]: unknown }) => {
console.log('data', data) logger('finish form %O', data)
setLoading(true) setLoading(true)
try { try {
@ -63,7 +86,7 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
}) })
} }
} catch (e) { } catch (e) {
console.error(e) logger('failed to finish form %O', e)
void message.error({ void message.error({
content: 'Error saving Input', content: 'Error saving Input',
}) })
@ -75,8 +98,6 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
const isVisible = useCallback((field: FormPublicFieldFragment): boolean => { const isVisible = useCallback((field: FormPublicFieldFragment): boolean => {
if (!field.logic) return true if (!field.logic) return true
console.log('DEFAULTS', values)
return field.logic return field.logic
.filter(logic => logic.action === 'visible') .filter(logic => logic.action === 'visible')
.map(logic => { .map(logic => {
@ -86,7 +107,6 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
values values
) )
console.log('result', r)
return Boolean(r) return Boolean(r)
} catch { } catch {
return true return true
@ -108,19 +128,7 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
<Form <Form
form={form} form={form}
onFinish={finish} onFinish={finish}
onValuesChange={() => { onValuesChange={updateValues}
const defaults = {}
fields.forEach(field => {
defaults[`@${field.id}`] = form.getFieldValue([field.id, 'value'])
if (field.slug) {
defaults[`$${field.slug}`] = form.getFieldValue([field.id, 'value'])
}
})
setValues(defaults)
}}
> >
{fields.map((field, i) => { {fields.map((field, i) => {
if (field.type === 'hidden') { if (field.type === 'hidden') {

View File

@ -1,16 +1,23 @@
import { Checkbox, Form } from 'antd' import { Checkbox, Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/checkbox')
export const CheckboxType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => { export const CheckboxType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: string = undefined let initialValue: string = undefined
if (field.value) { if (field.defaultValue) {
initialValue = field.value try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {

View File

@ -1,11 +1,14 @@
import { Form } from 'antd' import { Form } from 'antd'
import dayjs, { Dayjs } from 'dayjs' import dayjs, { Dayjs } from 'dayjs'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/date')
export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const [min, setMin] = useState<Dayjs>() const [min, setMin] = useState<Dayjs>()
const [max, setMax] = useState<Dayjs>() const [max, setMax] = useState<Dayjs>()
@ -24,12 +27,16 @@ export const DateType: React.FC<FieldTypeProps> = ({ field, design, urlValue, fo
let initialValue: Moment = undefined let initialValue: Moment = undefined
if (field.value) { if (field.defaultValue) {
initialValue = moment(field.value) try {
initialValue = moment(JSON.parse(field.defaultValue))
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {
initialValue = moment(field.value) initialValue = moment(urlValue)
} }
return ( return (

View File

@ -1,19 +1,36 @@
import { Form, Select } from 'antd' import { Form, Select } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/dropdown')
export const DropdownType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const DropdownType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
}
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value || null} initialValue={initialValue}
> >
<StyledSelect <StyledSelect
autoFocus={focus} autoFocus={focus}

View File

@ -1,12 +1,29 @@
import { Form } from 'antd' import { Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/email')
export const EmailType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const EmailType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
}
return ( return (
<div> <div>
<Form.Item <Form.Item
@ -15,7 +32,7 @@ export const EmailType: React.FC<FieldTypeProps> = ({ field, design, urlValue, f
{ 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') },
]} ]}
initialValue={urlValue || field.value} initialValue={initialValue}
> >
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} /> <StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
</Form.Item> </Form.Item>

View File

@ -1,12 +1,29 @@
import { Form } from 'antd' import { Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/link')
export const LinkType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const LinkType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = null
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
}
return ( return (
<div> <div>
<Form.Item <Form.Item
@ -15,7 +32,7 @@ export const LinkType: React.FC<FieldTypeProps> = ({ field, design, urlValue, fo
{ 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') },
]} ]}
initialValue={urlValue || field.value} initialValue={initialValue}
> >
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} /> <StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
</Form.Item> </Form.Item>

View File

@ -1,16 +1,23 @@
import { Form } from 'antd' import { Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/number')
export const NumberType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const NumberType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: number = undefined let initialValue: number = undefined
if (field.value) { if (field.defaultValue) {
initialValue = parseFloat(field.value) try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {

View File

@ -1,16 +1,23 @@
import { Form, Radio } from 'antd' import { Form, Radio } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/radio')
export const RadioType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => { export const RadioType: React.FC<FieldTypeProps> = ({ field, design, urlValue }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: string = undefined let initialValue: string = undefined
if (field.value) { if (field.defaultValue) {
initialValue = field.value try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {

View File

@ -1,15 +1,22 @@
import { Form, Rate } from 'antd' import { Form, Rate } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/rating')
export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue: number = undefined let initialValue: number = undefined
if (field.value) { if (field.defaultValue) {
initialValue = parseFloat(field.value) try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {

View File

@ -1,8 +1,11 @@
import { Form, Slider, Spin } from 'antd' import { Form, Slider, Spin } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/slider')
export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const [min, setMin] = useState<number>() const [min, setMin] = useState<number>()
const [max, setMax] = useState<number>() const [max, setMax] = useState<number>()
@ -14,13 +17,25 @@ export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
useEffect(() => { useEffect(() => {
field.options.forEach((option) => { field.options.forEach((option) => {
if (option.key === 'min') { if (option.key === 'min') {
setMin(parseFloat(option.value)) try {
setMin(JSON.parse(option.value))
} catch (e) {
logger('invalid min value %O', e)
}
} }
if (option.key === 'max') { if (option.key === 'max') {
setMax(parseFloat(option.value)) try {
setMax(JSON.parse(option.value))
} catch (e) {
logger('invalid max value %O', e)
}
} }
if (option.key === 'step') { if (option.key === 'step') {
setStep(parseFloat(option.value)) try {
setStep(JSON.parse(option.value))
} catch (e) {
logger('invalid step value %O', e)
}
} }
}) })
@ -29,8 +44,12 @@ export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
let initialValue: number = undefined let initialValue: number = undefined
if (field.value) { if (field.defaultValue) {
initialValue = parseFloat(field.value) try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
} }
if (urlValue) { if (urlValue) {
@ -51,10 +70,6 @@ export const SliderType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
name={[field.id, 'value']} name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
getValueFromEvent={(value: number) =>
typeof value === 'number' ? value.toFixed(2) : value
}
getValueProps={(value: string) => ({ value: value ? parseFloat(value) : undefined })}
> >
<Slider <Slider
min={min} min={min}

View File

@ -1,19 +1,36 @@
import { Form } from 'antd' import { Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/text')
export const TextType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const TextType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const { t } = useTranslation() const { t } = useTranslation()
// TODO focus when becomes visible // TODO focus when becomes visible
let initialValue = undefined
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
}
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value} initialValue={initialValue}
> >
<StyledInput autoFocus={focus} design={design} allowClear size={'large'} /> <StyledInput autoFocus={focus} design={design} allowClear size={'large'} />
</Form.Item> </Form.Item>

View File

@ -1,18 +1,35 @@
import { Form } from 'antd' import { Form } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/textarea')
export const TextareaType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => { export const TextareaType: React.FC<FieldTypeProps> = ({ field, design, urlValue, focus }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = undefined
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue) {
initialValue = urlValue
}
return ( return (
<div> <div>
<Form.Item <Form.Item
name={[field.id, 'value']} name={[field.id, 'value']}
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={urlValue || field.value} initialValue={initialValue}
> >
<StyledTextareaInput autoFocus={focus} design={design} allowClear autoSize /> <StyledTextareaInput autoFocus={focus} design={design} allowClear autoSize />
</Form.Item> </Form.Item>

View File

@ -1,12 +1,24 @@
import { Form, Switch } from 'antd' import { Form, Switch } from 'antd'
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 { FieldTypeProps } from './type.props'
const logger = debug('field/link')
export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const { t } = useTranslation() const { t } = useTranslation()
let initialValue = !!field.value
let initialValue: boolean = undefined
if (field.defaultValue) {
try {
initialValue = JSON.parse(field.defaultValue)
} catch (e) {
logger('invalid default value %O', e)
}
}
if (urlValue !== undefined) { if (urlValue !== undefined) {
initialValue = !!urlValue initialValue = !!urlValue
@ -19,8 +31,6 @@ export const YesNoType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
rules={[{ required: field.required, message: t('validation:valueRequired') }]} rules={[{ required: field.required, message: t('validation:valueRequired') }]}
initialValue={initialValue} initialValue={initialValue}
valuePropName={'checked'} valuePropName={'checked'}
getValueFromEvent={(checked: boolean) => (checked ? '1' : '')}
getValueProps={(e: string) => ({ checked: !!e })}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>

View File

@ -185,9 +185,9 @@ export const Structure: FunctionComponent<Props> = (props) => {
<Dropdown <Dropdown
overlay={ overlay={
<Menu> <Menu>
<Menu.Item onClick={() => router.push('/admin/profile')}>Profile</Menu.Item> <Menu.Item key={'profile'} onClick={() => router.push('/admin/profile')}>Profile</Menu.Item>
<Menu.Divider /> <Menu.Divider key={'d1'} />
<Menu.Item onClick={signOut}>Logout</Menu.Item> <Menu.Item key={'logout'} onClick={signOut}>Logout</Menu.Item>
</Menu> </Menu>
} }
onVisibleChange={setUserMenu} onVisibleChange={setUserMenu}
@ -237,7 +237,7 @@ export const Structure: FunctionComponent<Props> = (props) => {
{buildMenu(sideMenu)} {buildMenu(sideMenu)}
</Menu> </Menu>
<Menu mode="inline" selectable={false}> <Menu mode="inline" selectable={false}>
<Menu.Item className={'language-selector'}> <Menu.Item className={'language-selector'} key={'language-selector'}>
<Select <Select
bordered={false} bordered={false}
value={i18n.language.replace(/-.*/, '')} value={i18n.language.replace(/-.*/, '')}
@ -253,10 +253,10 @@ export const Structure: FunctionComponent<Props> = (props) => {
))} ))}
</Select> </Select>
</Menu.Item> </Menu.Item>
<Menu.Item style={{ display: 'flex', alignItems: 'center' }}> <Menu.Item style={{ display: 'flex', alignItems: 'center' }} key={'github'}>
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" /> <GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item key={'version'}>
Version: <Tag color="gold">{process.env.version}</Tag> Version: <Tag color="gold">{process.env.version}</Tag>
</Menu.Item> </Menu.Item>
</Menu> </Menu>

View File

@ -1,6 +1,5 @@
import debug from 'debug' import debug from 'debug'
import { all, create } from 'mathjs' import { formula, init } from 'expressionparser'
import { useState } from 'react'
const logger = debug('useMath') const logger = debug('useMath')
@ -8,9 +7,15 @@ export const useMath = (): ((
expression: string, expression: string,
values?: { [id: string]: string | number } values?: { [id: string]: string | number }
) => boolean) => { ) => boolean) => {
const [math] = useState(create(all, {}))
return (expression, values) => { return (expression, values) => {
const parser = init(formula, (term: string) => {
if (values[term]) {
return values[term]
}
throw new Error(`Invalid term: ${term}`);
})
try { try {
let processed = expression let processed = expression
@ -24,7 +29,9 @@ export const useMath = (): ((
} }
}) })
return Boolean(math.evaluate(processed)) parser.expressionToValue(expression)
return Boolean(parser.expressionToValue(expression))
} catch (e) { } catch (e) {
logger( logger(
'failed to calculate %O: %s', 'failed to calculate %O: %s',

View File

@ -46,7 +46,7 @@ export interface FormFieldFragment {
type: string type: string
description: string description: string
required: boolean required: boolean
value: string defaultValue?: string
options: FormFieldOptionFragment[] options: FormFieldOptionFragment[]
optionKeys?: FormFieldOptionKeysFragment optionKeys?: FormFieldOptionKeysFragment
@ -135,7 +135,7 @@ export const FORM_FRAGMENT = gql`
type type
description description
required required
value defaultValue
options { options {
id id

View File

@ -42,7 +42,7 @@ export interface FormPublicFieldFragment {
type: string type: string
description: string description: string
required: boolean required: boolean
value: string defaultValue: string
options: FormPublicFieldOptionFragment[] options: FormPublicFieldOptionFragment[]
@ -93,7 +93,7 @@ export const FORM_PUBLIC_FRAGMENT = gql`
type type
description description
required required
value defaultValue
logic { logic {
id id

View File

@ -20,15 +20,19 @@ interface Variables {
form: string form: string
start?: number start?: number
limit?: number limit?: number
filter?: {
finished?: boolean
excludeEmpty?: boolean
}
} }
const QUERY = gql` const QUERY = gql`
query listSubmissions($form: ID!, $start: Int, $limit: Int) { query listSubmissions($form: ID!, $start: Int, $limit: Int, $filter: SubmissionPagerFilterInput) {
form: getFormById(id: $form) { form: getFormById(id: $form) {
...PagerForm ...PagerForm
} }
pager: listSubmissions(form: $form, start: $start, limit: $limit) { pager: listSubmissions(form: $form, start: $start, limit: $limit, filter: $filter) {
entries { entries {
...Submission ...Submission
} }

View File

@ -20,13 +20,13 @@
"dayjs": "^1.10.7", "dayjs": "^1.10.7",
"debug": "^4.3.3", "debug": "^4.3.3",
"exceljs": "^4.3.0", "exceljs": "^4.3.0",
"expressionparser": "^1.1.5",
"graphql": "^16.3.0", "graphql": "^16.3.0",
"i18next": "^21.6.12", "i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3", "i18next-browser-languagedetector": "^6.1.3",
"imagemin-optipng": "^8.0.0", "imagemin-optipng": "^8.0.0",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jimp": "^0.16.1", "jimp": "^0.16.1",
"mathjs": "^10.1.1",
"next": "^12.1.0", "next": "^12.1.0",
"next-compose-plugins": "^2.2.1", "next-compose-plugins": "^2.2.1",
"next-optimized-images": "^2.6.2", "next-optimized-images": "^2.6.2",

View File

@ -9,6 +9,7 @@ import { NotificationsTab } from 'components/form/admin/notifications.tab'
import { StartPageTab } from 'components/form/admin/start.page.tab' import { StartPageTab } from 'components/form/admin/start.page.tab'
import { Structure } from 'components/structure' import { Structure } from 'components/structure'
import { withAuth } from 'components/with.auth' import { withAuth } from 'components/with.auth'
import debug from 'debug'
import { useFormUpdateMutation } from 'graphql/mutation/form.update.mutation' import { useFormUpdateMutation } from 'graphql/mutation/form.update.mutation'
import { NextPage } from 'next' import { NextPage } from 'next'
import Link from 'next/link' import Link from 'next/link'
@ -22,6 +23,8 @@ import {
} from '../../../../graphql/fragment/form.fragment' } from '../../../../graphql/fragment/form.fragment'
import { Data, useFormQuery } from '../../../../graphql/query/form.query' import { Data, useFormQuery } from '../../../../graphql/query/form.query'
const logger = debug('page/admin/form/[id]')
const Index: NextPage = () => { const Index: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
@ -40,12 +43,17 @@ const Index: NextPage = () => {
field.options.forEach((option) => { field.options.forEach((option) => {
if (option.key) { if (option.key) {
keys[option.key] = option.value try {
keys[option.key] = JSON.parse(option.value)
} catch (e) {
logger('invalid option value %O', e)
}
} }
}) })
return { return {
...field, ...field,
defaultValue: field.defaultValue ? JSON.parse(field.defaultValue) : null,
options: field.options.filter((option) => !option.key), options: field.options.filter((option) => !option.key),
optionKeys: keys, optionKeys: keys,
} }
@ -76,13 +84,13 @@ const Index: NextPage = () => {
if (optionKeys) { if (optionKeys) {
Object.keys(optionKeys).forEach((key) => { Object.keys(optionKeys).forEach((key) => {
if (!optionKeys[key]) { if (optionKeys[key] === undefined) {
return return
} }
options.push({ options.push({
id: null, // TODO improve this id: null, // TODO improve this
value: optionKeys[key], value: JSON.stringify(optionKeys[key]),
key, key,
}) })
}) })
@ -90,6 +98,7 @@ const Index: NextPage = () => {
return { return {
...field, ...field,
defaultValue: field.defaultValue !== null ? JSON.stringify(field.defaultValue) : null,
options, options,
idx: index, idx: index,
} }

View File

@ -31,6 +31,9 @@ const Submissions: NextPage = () => {
form: router.query.id as string, form: router.query.id as string,
limit: pagination.pageSize, limit: pagination.pageSize,
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0, start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0,
filter: {
excludeEmpty: true,
},
}, },
onCompleted: ({ pager, form }) => { onCompleted: ({ pager, form }) => {
setPagination({ setPagination({

View File

@ -85,11 +85,11 @@ const Index: NextPage = () => {
} }
return ( return (
<Link href={'/admin/users/[id]'} as={`/admin/users/${user.id}`}> <Tooltip title={user.email}>
<Tooltip title={user.email}> <Link href={`/admin/users/${user.id}`} passHref>
<Button type={'dashed'}>{user.username}</Button> <Button type={'dashed'}>{user.username}</Button>
</Tooltip> </Link>
</Link> </Tooltip>
) )
}, },
responsive: ['lg'], responsive: ['lg'],
@ -124,15 +124,15 @@ const Index: NextPage = () => {
render(_, row) { render(_, row) {
return ( return (
<Space direction={width < 600 ? 'vertical' : 'horizontal'}> <Space direction={width < 600 ? 'vertical' : 'horizontal'}>
<Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${row.id}/submissions`}> <Tooltip title={'Show Submissions'}>
<Tooltip title={'Show Submissions'}> <Link href={`/admin/forms/${row.id}/submissions`} passHref>
<Button> <Button>
<UnorderedListOutlined /> <UnorderedListOutlined />
</Button> </Button>
</Tooltip> </Link>
</Link> </Tooltip>
<Link href={'/admin/forms/[id]'} as={`/admin/forms/${row.id}`}> <Link href={`/admin/forms/${row.id}`} passHref>
<Button type={'primary'}> <Button type={'primary'}>
<EditOutlined /> <EditOutlined />
</Button> </Button>

View File

@ -85,7 +85,7 @@ const Index: NextPage = () => {
render(_, row) { render(_, row) {
return ( return (
<Space direction={width < 600 ? 'vertical' : 'horizontal'}> <Space direction={width < 600 ? 'vertical' : 'horizontal'}>
<Link href={'/admin/users/[id]'} as={`/admin/users/${row.id}`}> <Link href={`/admin/users/${row.id}`} passHref>
<Button type={'primary'}> <Button type={'primary'}>
<EditOutlined /> <EditOutlined />
</Button> </Button>

View File

@ -2339,6 +2339,11 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1" snapdragon "^0.8.1"
to-regex "^3.0.1" to-regex "^3.0.1"
expressionparser@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/expressionparser/-/expressionparser-1.1.5.tgz#b16bc45e7557be437c2259e2ab0abd8b857e979b"
integrity sha512-9GldsRvXhcJ+ZPiK7fel5KBpgU+LjjQjnyWQVugJcvRc99lXTBICwXKaQV2laZ1KUlTVUBnMmFpgtkMVwwMiHA==
ext-list@^2.0.0: ext-list@^2.0.0:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
@ -3713,21 +3718,6 @@ mathjs@*:
tiny-emitter "^2.1.0" tiny-emitter "^2.1.0"
typed-function "^2.0.0" typed-function "^2.0.0"
mathjs@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-10.1.1.tgz#99b647387b65c4b5c47b71e11d59481473ccfa0d"
integrity sha512-4QJP8a0Vy90ajFYESnITSluCrQBZnI+2XQhKJIRdo/6t95oupffS5qA4MTWnLGm5GsEZF179JSMjST7wCdZQkA==
dependencies:
"@babel/runtime" "^7.16.7"
complex.js "^2.0.15"
decimal.js "^10.3.1"
escape-latex "^1.2.0"
fraction.js "^4.1.2"
javascript-natural-sort "^0.7.1"
seedrandom "^3.0.5"
tiny-emitter "^2.1.0"
typed-function "^2.0.0"
mdast-util-definitions@^5.0.0: mdast-util-definitions@^5.0.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"