upgrade to nextjs 12, add visible logic check

This commit is contained in:
Michael Schramm 2022-01-03 00:38:44 +01:00
parent 26c2f9e095
commit e54da2b111
70 changed files with 2361 additions and 2366 deletions

View File

@ -6,22 +6,64 @@ module.exports = {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
}, },
plugins: [
'@typescript-eslint/eslint-plugin',
'@typescript-eslint',
'unused-imports'
],
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:jsx-a11y/recommended', 'plugin:jsx-a11y/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
], ],
rules: { rules: {
'prettier/prettier': ['error', {}, { usePrettierrc: true }], '@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'react/prop-types': 'off', 'react/prop-types': 'off',
'@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-var-requires': 'off', 'jsx-a11y/no-autofocus': 'off',
'jsx-a11y/no-autofocus': 'off' 'array-element-newline': ['error', {
'ArrayExpression': 'consistent',
'ArrayPattern': {
'minItems': 3,
'multiline': true,
}
}],
'array-bracket-newline': ['error', {
'minItems': 3,
'multiline': true,
}],
'indent': [
'error',
2,
{
'SwitchCase': 1
}
],
'no-tabs': ['error'],
'max-len': ['error', {
'code': 100,
'ignoreComments': true,
'ignoreUrls': true,
'ignoreTemplateLiterals': true,
'ignoreTrailingComments': true,
'ignoreStrings': true,
}],
'quotes': ['error', 'single', { 'avoidEscape': true }],
'comma-dangle': ['error', 'always-multiline'],
'linebreak-style': [
'error',
'unix'
],
'no-trailing-spaces': 'error',
'eol-last': 'error',
'unused-imports/no-unused-imports': 'error',
}, },
settings: { settings: {
react: { react: {

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ yarn-error.log*
# development environments # development environments
/.idea /.idea
schema.graphql

View File

@ -16,12 +16,14 @@ 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
### Changed ### Changed
- combined notificationts to become more versatile - combined notificationts to become more versatile
- use exported hooks for graphql - use exported hooks for graphql
- disable swipe gesture - disable swipe gesture
- upgrade to nextjs 12
### Fixed ### Fixed

View File

@ -30,7 +30,8 @@ WORKDIR /usr/src/app
COPY --from=builder /usr/src/app /usr/src/app COPY --from=builder /usr/src/app /usr/src/app
ENV PORT=4000 ENV PORT=4000 \
NODE_ENV=production
# Change to non-root privilege # Change to non-root privilege
USER ohmyform USER ohmyform

View File

@ -31,66 +31,66 @@ const AuthFooterInner: React.FC<Props> = (props) => {
<footer className={scss.footer}> <footer className={scss.footer}>
{props.me {props.me
? [ ? [
<span style={{ color: '#FFF' }} key={'user'}> <span style={{ color: '#FFF' }} key={'user'}>
Hi, {props.me.username} Hi, {props.me.username}
</span>, </span>,
props.me.roles.includes('admin') && ( props.me.roles.includes('admin') && (
<Link key={'admin'} href={'/admin'}> <Link key={'admin'} href={'/admin'}>
<Button
type={'link'}
style={{
color: '#FFF',
}}
>
{t('admin')}
</Button>
</Link>
),
<Link key={'profile'} href={'/admin/profile'}>
<Button <Button
type={'link'} type={'link'}
style={{ style={{
color: '#FFF', color: '#FFF',
}} }}
> >
{t('profile')} {t('admin')}
</Button> </Button>
</Link>, </Link>
),
<Link key={'profile'} href={'/admin/profile'}>
<Button <Button
key={'logout'}
type={'link'} type={'link'}
onClick={logout}
style={{ style={{
color: '#FFF', color: '#FFF',
}} }}
> >
{t('logout')} {t('profile')}
</Button>, </Button>
] </Link>,
<Button
key={'logout'}
type={'link'}
onClick={logout}
style={{
color: '#FFF',
}}
>
{t('logout')}
</Button>,
]
: [ : [
<Link href={'/login'} key={'login'}> <Link href={'/login'} key={'login'}>
<Button
type={'link'}
style={{
color: '#FFF',
}}
>
{t('login')}
</Button>
</Link>,
!loading && !data?.disabledSignUp.value && (
<Link href={'/register'} key={'register'}>
<Button <Button
type={'link'} type={'link'}
style={{ style={{
color: '#FFF', color: '#FFF',
}} }}
> >
{t('login')} {t('register')}
</Button> </Button>
</Link>, </Link>
!loading && !data?.disabledSignUp.value && ( ),
<Link href={'/register'} key={'register'}> ]}
<Button
type={'link'}
style={{
color: '#FFF',
}}
>
{t('register')}
</Button>
</Link>
),
]}
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<Select <Select
bordered={false} bordered={false}

View File

@ -9,10 +9,14 @@ export const DesignTab: React.FC<TabPaneProps> = (props) => {
return ( return (
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item label={t('form:design.font')} name={['form', 'design', 'font']}> <Form.Item label={t('form:design.font')} name={[
'form', 'design', 'font',
]}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t('form:design.layouts')} name={['form', 'design', 'layout']}> <Form.Item label={t('form:design.layouts')} name={[
'form', 'design', 'layout',
]}>
<Select <Select
options={[ options={[
{ {
@ -27,11 +31,15 @@ export const DesignTab: React.FC<TabPaneProps> = (props) => {
/> />
</Form.Item> </Form.Item>
{['background', 'question', 'answer', 'button', 'buttonActive', 'buttonText'].map((name) => ( {[
'background', 'question', 'answer', 'button', 'buttonActive', 'buttonText',
].map((name) => (
<Form.Item <Form.Item
key={name} key={name}
label={t(`form:design.color.${name}`)} label={t(`form:design.color.${name}`)}
name={['form', 'design', 'colors', name]} name={[
'form', 'design', 'colors', name,
]}
> >
<InputColor /> <InputColor />
</Form.Item> </Form.Item>

View File

@ -12,19 +12,25 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item <Form.Item
label={t('form:endPage.show')} label={t('form:endPage.show')}
name={['form', 'endPage', 'show']} name={[
'form', 'endPage', 'show',
]}
valuePropName={'checked'} valuePropName={'checked'}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t('form:endPage.title')} name={['form', 'endPage', 'title']}> <Form.Item label={t('form:endPage.title')} name={[
'form', 'endPage', 'title',
]}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:endPage.paragraph')} label={t('form:endPage.paragraph')}
name={['form', 'endPage', 'paragraph']} name={[
'form', 'endPage', 'paragraph',
]}
extra={t('type:descriptionInfo')} extra={t('type:descriptionInfo')}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />
@ -32,12 +38,16 @@ export const EndPageTab: React.FC<TabPaneProps> = (props) => {
<Form.Item <Form.Item
label={t('form:endPage.continueButtonText')} label={t('form:endPage.continueButtonText')}
name={['form', 'endPage', 'buttonText']} name={[
'form', 'endPage', 'buttonText',
]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={['form', 'endPage', 'buttons']}> <Form.List name={[
'form', 'endPage', 'buttons',
]}>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>

View File

@ -108,7 +108,9 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
}) })
} }
setLoading(false) setLoading(false)
}, [form, getSubmissions, props.form, setLoading, loading]) }, [
form, getSubmissions, props.form, setLoading, loading,
])
return props.trigger(() => exportSubmissions(), loading) return props.trigger(() => exportSubmissions(), loading)
} }

View File

@ -1,16 +1,6 @@
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib' import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
import { import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd'
Button,
Card,
Checkbox,
Form,
Input,
Popconfirm,
Popover,
Space,
Tag,
Tooltip,
} from 'antd'
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'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
@ -26,22 +16,40 @@ interface Props {
onChangeFields: (fields: FormFieldFragment[]) => void onChangeFields: (fields: FormFieldFragment[]) => void
field: FieldData field: FieldData
remove: (index: number) => void remove: (index: number) => void
move: (from: number, to: number) => void
index: number index: number
} }
export const FieldCard: React.FC<Props> = (props) => { export const FieldCard: React.FC<Props> = ({
form,
field,
fields,
onChangeFields,
remove,
move,
index,
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { form, field, fields, onChangeFields, remove, index } = props
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type']) as string const type = form.getFieldValue([
'form', 'fields', field.name as string, 'type',
]) as string
const TypeComponent = adminTypes[type] || TextType const TypeComponent = adminTypes[type] || TextType
const [shouldUpdate, setShouldUpdate] = useState(false)
const [nextTitle, setNextTitle] = useState<string>( const [nextTitle, setNextTitle] = useState<string>(
form.getFieldValue(['form', 'fields', field.name as string, 'title']) form.getFieldValue([
'form', 'fields', field.name as string, 'title',
])
) )
useEffect(() => { useEffect(() => {
if (!shouldUpdate) {
return
}
const id = setTimeout(() => { const id = setTimeout(() => {
setShouldUpdate(false)
onChangeFields( onChangeFields(
fields.map((field, i) => { fields.map((field, i) => {
if (i === index) { if (i === index) {
@ -57,7 +65,9 @@ export const FieldCard: React.FC<Props> = (props) => {
}, 500) }, 500)
return () => clearTimeout(id) return () => clearTimeout(id)
}, [nextTitle]) }, [
nextTitle, shouldUpdate, fields,
])
const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => { const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => {
return ( return (
@ -94,14 +104,32 @@ export const FieldCard: React.FC<Props> = (props) => {
return ( return (
<Card <Card
title={<Tooltip title={`@${field.name as string}`}>nextTitle</Tooltip>} title={nextTitle}
type={'inner'} type={'inner'}
extra={ extra={
<div> <Space>
<Tooltip title={t('form:field.move.up')}>
<Button
type={'text'}
disabled={index === 0}
onClick={() => move(index, index - 1)}
icon={<VerticalAlignTopOutlined />}
/>
</Tooltip>
<Tooltip title={t('form:field.move.down')}>
<Button
type={'text'}
disabled={index + 1 >= form.getFieldValue(['form', 'fields']).length}
onClick={() => move(index, index + 1)}
icon={<VerticalAlignBottomOutlined />}
/>
</Tooltip>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{() => { {() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const slug = form.getFieldValue(['form', 'fields', field.name as string, 'slug']) const slug = form.getFieldValue([
'form', 'fields', field.name as string, 'slug',
])
if (!slug) { if (!slug) {
return null return null
@ -145,7 +173,7 @@ export const FieldCard: React.FC<Props> = (props) => {
<DeleteOutlined /> <DeleteOutlined />
</Button> </Button>
</Popconfirm> </Popconfirm>
</div> </Space>
} }
> >
<Form.Item name={[field.name as string, 'type']} noStyle> <Form.Item name={[field.name as string, 'type']} noStyle>
@ -157,7 +185,12 @@ export const FieldCard: React.FC<Props> = (props) => {
rules={[{ required: true, message: 'Title is required' }]} rules={[{ required: true, message: 'Title is required' }]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
> >
<Input onChange={(e) => setNextTitle(e.target.value)} /> <Input
onChange={(e) => {
setNextTitle(e.target.value)
setShouldUpdate(true)
}}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('type:description')} label={t('type:description')}
@ -181,7 +214,7 @@ export const FieldCard: React.FC<Props> = (props) => {
<Form.List name={[field.name as string, 'logic']}> <Form.List name={[field.name as string, 'logic']}>
{(logic, { add, remove, move }) => { {(logic, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => { const addAndMove = (index: number) => (defaults) => {
add(defaults) add(defaults)
move(fields.length, index) move(fields.length, index)
} }

View File

@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons/lib'
import { Button, Form, Select, Space, Tabs } from 'antd' import { Button, Form, Select, Space, Tabs } from 'antd'
import { FormInstance } from 'antd/lib/form' import { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs' import { TabPaneProps } from 'antd/lib/tabs'
import debug from 'debug'
import { FieldData } from 'rc-field-form/lib/interface' 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'
@ -9,6 +10,8 @@ import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { FieldCard } from './field.card' import { FieldCard } from './field.card'
import { adminTypes } from './types' import { adminTypes } from './types'
const logger = debug('FieldsTab')
interface Props extends TabPaneProps { interface Props extends TabPaneProps {
form: FormInstance form: FormInstance
fields: FormFieldFragment[] fields: FormFieldFragment[]
@ -20,13 +23,25 @@ export const FieldsTab: React.FC<Props> = (props) => {
const [nextType, setNextType] = useState('textfield') const [nextType, setNextType] = useState('textfield')
const renderType = useCallback( const renderType = useCallback(
(field: FieldData, index: number, remove: (index: number) => void) => { (
field: FieldData,
index: number,
remove: (index: number) => void,
move: (from: number, to: number) => void
) => {
return ( return (
<FieldCard <FieldCard
form={props.form} form={props.form}
field={field} field={field}
index={index} index={index}
remove={remove} remove={(index: number) => {
logger('remove %d', index)
remove(index)
}}
move={(from: number, to: number) => {
logger('move %d TO %d', from, to)
move(from, to)
}}
fields={props.fields} fields={props.fields}
onChangeFields={props.onChangeFields} onChangeFields={props.onChangeFields}
/> />
@ -85,7 +100,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.List name={['form', 'fields']}> <Form.List name={['form', 'fields']}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => { const addAndMove = (index: number) => (defaults) => {
add(defaults) add(defaults)
move(fields.length, index) move(fields.length, index)
} }
@ -96,7 +111,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.key}> <div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }}> <Form.Item wrapperCol={{ span: 24 }}>
{renderType(field, index, remove)} {renderType(field, index, remove, move)}
</Form.Item> </Form.Item>
{addField(addAndMove(index + 1), index + 1)} {addField(addAndMove(index + 1), index + 1)}
</div> </div>

View File

@ -1,7 +1,6 @@
import { DeleteOutlined } from '@ant-design/icons' import { DeleteOutlined } from '@ant-design/icons'
import { Alert, Button, Checkbox, Form, Mentions, Popconfirm, Select } from 'antd' import { Alert, Button, Checkbox, Form, Mentions, Popconfirm, Select } from 'antd'
import { FormInstance } from 'antd/lib/form' import { FormInstance } from 'antd/lib/form'
import FormItemContext from 'rc-field-form/lib/FieldContext'
import { FieldData } from 'rc-field-form/lib/interface' import { FieldData } from 'rc-field-form/lib/interface'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -16,9 +15,14 @@ interface Props {
index: number index: number
} }
export const LogicBlock: React.FC<Props> = (props) => { export const LogicBlock: React.FC<Props> = ({
form,
field,
fields,
remove,
index,
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const { form, field, fields, remove, index } = props
const evaluator = useMath() const evaluator = useMath()
return ( return (
@ -45,9 +49,7 @@ export const LogicBlock: React.FC<Props> = (props) => {
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{(form) => { {(form: FormInstance & { prefixName: string[] }) => {
const context = React.useContext(FormItemContext)
try { try {
const defaults = {} const defaults = {}
@ -60,7 +62,11 @@ export const LogicBlock: React.FC<Props> = (props) => {
}) })
const result = evaluator( const result = evaluator(
form.getFieldValue([...context.prefixName, field.name as string, 'formula']), form.getFieldValue([
...form.prefixName,
field.name as string,
'formula',
]),
defaults defaults
) )
@ -109,13 +115,13 @@ export const LogicBlock: React.FC<Props> = (props) => {
/> />
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{(form) => { {(form: FormInstance & { prefixName: string[] }) => {
const context = React.useContext(FormItemContext)
return ( return (
<Form.Item <Form.Item
hidden={ hidden={
form.getFieldValue([...context.prefixName, field.name as string, 'action']) !== form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'jumpTo' 'jumpTo'
} }
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
@ -137,13 +143,13 @@ export const LogicBlock: React.FC<Props> = (props) => {
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{(form) => { {(form: FormInstance & { prefixName: string[] }) => {
const context = React.useContext(FormItemContext)
return ( return (
<Form.Item <Form.Item
hidden={ hidden={
form.getFieldValue([...context.prefixName, field.name as string, 'action']) !== form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'visible' 'visible'
} }
initialValue={true} initialValue={true}
@ -160,13 +166,13 @@ export const LogicBlock: React.FC<Props> = (props) => {
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{(form) => { {(form: FormInstance & { prefixName: string[] }) => {
const context = React.useContext(FormItemContext)
return ( return (
<Form.Item <Form.Item
hidden={ hidden={
form.getFieldValue([...context.prefixName, field.name as string, 'action']) !== form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'disable' 'disable'
} }
initialValue={false} initialValue={false}
@ -183,13 +189,13 @@ export const LogicBlock: React.FC<Props> = (props) => {
</Form.Item> </Form.Item>
<Form.Item noStyle shouldUpdate> <Form.Item noStyle shouldUpdate>
{(form) => { {(form: FormInstance & { prefixName: string[] }) => {
const context = React.useContext(FormItemContext)
return ( return (
<Form.Item <Form.Item
hidden={ hidden={
form.getFieldValue([...context.prefixName, field.name as string, 'action']) !== form.getFieldValue([
...form.prefixName, field.name as string, 'action',
]) !==
'require' 'require'
} }
initialValue={true} initialValue={true}

View File

@ -62,7 +62,9 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
])
), ),
message: t('validation:subjectRequired'), message: t('validation:subjectRequired'),
}, },
@ -82,7 +84,9 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
])
), ),
message: t('validation:templateRequired'), message: t('validation:templateRequired'),
}, },
@ -119,7 +123,9 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) && form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([ !form.getFieldValue([
'form', 'form',
'notifications', 'notifications',
@ -156,7 +162,9 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) && form.getFieldValue([
'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([ !form.getFieldValue([
'form', 'form',
'notifications', 'notifications',
@ -182,8 +190,12 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) && form.getFieldValue([
!form.getFieldValue(['form', 'notifications', field.name as string, 'toEmail']) 'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form', 'notifications', field.name as string, 'toEmail',
])
), ),
message: t('validation:emailFieldRequired'), message: t('validation:emailFieldRequired'),
}, },
@ -215,8 +227,12 @@ export const NotificationCard: React.FC<Props> = (props) => {
rules={[ rules={[
{ {
required: Boolean( required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) && form.getFieldValue([
!form.getFieldValue(['form', 'notifications', field.name as string, 'toField']) 'form', 'notifications', field.name as string, 'enabled',
]) &&
!form.getFieldValue([
'form', 'notifications', field.name as string, 'toField',
])
), ),
message: t('validation:emailFieldRequired'), message: t('validation:emailFieldRequired'),
}, },

View File

@ -62,7 +62,7 @@ export const NotificationsTab: React.FC<Props> = (props) => {
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.List name={['form', 'notifications']}> <Form.List name={['form', 'notifications']}>
{(fields, { add, remove, move }) => { {(fields, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => { const addAndMove = (index: number) => (defaults) => {
add(defaults) add(defaults)
move(fields.length, index) move(fields.length, index)
} }

View File

@ -12,19 +12,25 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
<Tabs.TabPane {...props}> <Tabs.TabPane {...props}>
<Form.Item <Form.Item
label={t('form:startPage.show')} label={t('form:startPage.show')}
name={['form', 'startPage', 'show']} name={[
'form', 'startPage', 'show',
]}
valuePropName={'checked'} valuePropName={'checked'}
> >
<Switch /> <Switch />
</Form.Item> </Form.Item>
<Form.Item label={t('form:startPage.title')} name={['form', 'startPage', 'title']}> <Form.Item label={t('form:startPage.title')} name={[
'form', 'startPage', 'title',
]}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('form:startPage.paragraph')} label={t('form:startPage.paragraph')}
name={['form', 'startPage', 'paragraph']} name={[
'form', 'startPage', 'paragraph',
]}
extra={t('form:startPage.paragraphInfo')} extra={t('form:startPage.paragraphInfo')}
> >
<Input.TextArea autoSize /> <Input.TextArea autoSize />
@ -32,12 +38,16 @@ export const StartPageTab: React.FC<TabPaneProps> = (props) => {
<Form.Item <Form.Item
label={t('form:startPage.continueButtonText')} label={t('form:startPage.continueButtonText')}
name={['form', 'startPage', 'buttonText']} name={[
'form', 'startPage', 'buttonText',
]}
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.List name={['form', 'startPage', 'buttons']}> <Form.List name={[
'form', 'startPage', 'buttons',
]}>
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
return ( return (
<div> <div>

View File

@ -33,8 +33,6 @@ export const SubmissionValues: React.FC<Props> = (props) => {
try { try {
const data = JSON.parse(row.value) as { value: string } const data = JSON.parse(row.value) as { value: string }
console.log('DATA', data)
return data.value return data.value
} catch (e) { } catch (e) {
return row.value return row.value

View File

@ -20,7 +20,9 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('type:date.min')} label={t('type:date.min')}
name={[field.name as string, 'optionKeys', 'min']} name={[
field.name as string, 'optionKeys', 'min',
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
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 })}
@ -30,7 +32,9 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
<Form.Item <Form.Item
label={t('type:date.max')} label={t('type:date.max')}
name={[field.name as string, 'optionKeys', 'max']} name={[
field.name as string, 'optionKeys', 'max',
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
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 })}

View File

@ -50,7 +50,9 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
<Form.Item <Form.Item
label={t('type:slider.min')} label={t('type:slider.min')}
name={[props.field.name as string, 'optionKeys', 'min']} name={[
props.field.name as string, 'optionKeys', 'min',
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={0} initialValue={0}
getValueFromEvent={(value: number) => getValueFromEvent={(value: number) =>
@ -63,7 +65,9 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
<Form.Item <Form.Item
label={t('type:slider.max')} label={t('type:slider.max')}
name={[props.field.name as string, 'optionKeys', 'max']} name={[
props.field.name as string, 'optionKeys', 'max',
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={100} initialValue={100}
getValueFromEvent={(value: number) => getValueFromEvent={(value: number) =>
@ -76,7 +80,9 @@ export const SliderType: React.FC<AdminFieldTypeProps> = (props) => {
<Form.Item <Form.Item
label={t('type:slider.step')} label={t('type:slider.step')}
name={[props.field.name as string, 'optionKeys', 'step']} name={[
props.field.name as string, 'optionKeys', 'step',
]}
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
initialValue={1} initialValue={1}
getValueFromEvent={(value: number) => getValueFromEvent={(value: number) =>

View File

@ -54,7 +54,7 @@ export const Field: React.FC<Props> = ({ field, design, focus, ...props }) => {
{field.title} {field.title}
</StyledH1> </StyledH1>
{field.description && ( {field.description && (
<StyledMarkdown design={design} type={'question'} source={field.description} /> <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={focus} />

View File

@ -1,10 +1,12 @@
import { Card, Form, message, Modal, Spin } from 'antd' import { Card, Form, message, Modal, Spin } from 'antd'
import React, { useState } from 'react' import { darken, lighten } from 'polished'
import React, { useCallback, 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 { Omf } from '../../../omf' import { Omf } from '../../../omf'
import { StyledButton } from '../../../styled/button' import { StyledButton } from '../../../styled/button'
import { darken, lighten } from '../../../styled/color.change' import { useMath } from '../../../use.math'
import { LayoutProps } from '../layout.props' import { LayoutProps } from '../layout.props'
import { Field } from './field' import { Field } from './field'
import { Page } from './page' import { Page } from './page'
@ -12,7 +14,7 @@ import { Page } from './page'
type Step = 'start' | 'form' | 'end' type Step = 'start' | 'form' | 'end'
const MyCard = styled.div<{ background: string }>` const MyCard = styled.div<{ background: string }>`
background: ${(props) => darken(props.background, 10)}; background: ${(props) => darken(0.1, props.background)};
height: 100%; height: 100%;
min-height: 100vh; min-height: 100vh;
@ -20,7 +22,7 @@ const MyCard = styled.div<{ background: string }>`
.ant-card { .ant-card {
background: ${(props) => props.background}; background: ${(props) => props.background};
border-color: ${(props) => lighten(props.background, 40)}; border-color: ${(props) => lighten(0.4, props.background)};
width: 800px; width: 800px;
margin: auto; margin: auto;
max-width: 90%; max-width: 90%;
@ -32,6 +34,8 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
const [form] = Form.useForm() const [form] = Form.useForm()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [step, setStep] = useState<Step>(props.form.startPage.show ? 'start' : 'form') const [step, setStep] = useState<Step>(props.form.startPage.show ? 'start' : 'form')
const evaluator = useMath()
const [values, setValues] = useState({})
const { design, startPage, endPage, fields } = props.form const { design, startPage, endPage, fields } = props.form
const { setField } = props.submission const { setField } = props.submission
@ -67,6 +71,31 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
setLoading(false) setLoading(false)
} }
const isVisible = useCallback((field: FormPublicFieldFragment): boolean => {
if (!field.logic) return true
console.log('DEFAULTS', values)
return field.logic
.filter(logic => logic.action === 'visible')
.map(logic => {
try {
const r = evaluator(
logic.formula,
values
)
console.log('result', r)
return Boolean(r)
} catch {
return true
}
})
.reduce<boolean>((previous, current) => previous && current, true)
}, [
fields, form, values,
])
const render = () => { const render = () => {
switch (step) { switch (step) {
case 'start': case 'start':
@ -75,12 +104,32 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
case 'form': case 'form':
return ( return (
<Card> <Card>
<Form form={form} onFinish={finish}> <Form
form={form}
onFinish={finish}
onValuesChange={() => {
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') {
return null return null
} }
if (!isVisible(field)) {
return null
}
return <Field key={field.id} field={field} design={design} focus={i === 0} /> return <Field key={field.id} field={field} design={design} focus={i === 0} />
})} })}
<div <div

View File

@ -26,7 +26,7 @@ export const Page: React.FC<Props> = ({ design, page, next, prev }) => {
<StyledH1 design={design} type={'question'}> <StyledH1 design={design} type={'question'}>
{page.title} {page.title}
</StyledH1> </StyledH1>
<StyledMarkdown design={design} type={'question'} source={page.paragraph} /> <StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
<div <div
style={{ style={{

View File

@ -62,6 +62,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%',
}} }}
> >
<div <div
@ -77,7 +78,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
{field.title} {field.title}
</StyledH1> </StyledH1>
{field.description && ( {field.description && (
<StyledMarkdown design={design} type={'question'} source={field.description} /> <StyledMarkdown design={design} type={'question'}>{field.description}</StyledMarkdown>
)} )}
<FieldInput design={design} field={field} urlValue={getUrlDefault()} /> <FieldInput design={design} field={field} urlValue={getUrlDefault()} />

View File

@ -1,31 +1,42 @@
import { Modal } from 'antd' import { Modal } 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 Swiper from 'react-id-swiper' import SwiperClass from 'swiper'
import { ReactIdSwiperProps } from 'react-id-swiper/lib/types' import { Swiper, SwiperProps, SwiperSlide } from 'swiper/react'
import * as OriginalSwiper from 'swiper'
import { Omf } from '../../../omf' import { Omf } from '../../../omf'
import { useWindowSize } from '../../../use.window.size'
import { LayoutProps } from '../layout.props' import { LayoutProps } from '../layout.props'
import { Field } from './field' import { Field } from './field'
import { FormPage } from './page' import { FormPage } from './page'
const logger = debug('layout/slider')
export const SliderLayout: React.FC<LayoutProps> = (props) => { export const SliderLayout: React.FC<LayoutProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null) const [swiper, setSwiper] = useState<SwiperClass>(null)
const { height } = useWindowSize()
const { design, startPage, endPage, fields } = props.form const { design, startPage, endPage, fields } = props.form
const { finish, setField } = props.submission const { finish, setField } = props.submission
const goNext = () => { const goNext = () => {
if (!swiper) return if (!swiper) return
logger('goNext')
swiper.allowSlideNext = true swiper.allowSlideNext = true
swiper.slideNext() swiper.slideNext()
swiper.allowSlideNext = false swiper.allowSlideNext = false
} }
const goPrev = () => swiper && swiper.slidePrev() const goPrev = () => {
if (!swiper) {
return
}
const swiperConfig: ReactIdSwiperProps = { logger('goPrevious')
swiper.slidePrev()
}
const swiperConfig: SwiperProps = {
direction: 'vertical', direction: 'vertical',
allowSlideNext: false, allowSlideNext: false,
allowSlidePrev: true, allowSlidePrev: true,
@ -37,14 +48,25 @@ export const SliderLayout: React.FC<LayoutProps> = (props) => {
<div <div
style={{ style={{
background: design.colors.background, background: design.colors.background,
overflow: 'hidden',
height: '100vh',
}} }}
> >
<Omf /> <Omf />
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */} {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
<Swiper {...swiperConfig} ref={(element) => element && setSwiper((element as any).swiper)}> <Swiper
height={height}
{...swiperConfig}
onSwiper={next => {
logger('setSwiper')
setSwiper(next)
}}
>
{[ {[
startPage.show ? ( startPage.show ? (
<FormPage key={'start'} page={startPage} design={design} next={goNext} prev={goPrev} /> <SwiperSlide key={'start'}>
<FormPage page={startPage} design={design} next={goNext} prev={goPrev} />
</SwiperSlide>
) : undefined, ) : undefined,
...fields ...fields
.map((field, i) => { .map((field, i) => {
@ -53,42 +75,45 @@ export const SliderLayout: React.FC<LayoutProps> = (props) => {
} }
return ( return (
<Field <SwiperSlide key={field.id}>
key={field.id} <Field
field={field} field={field}
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])
if (fields.length === i + 1) { if (fields.length === i + 1) {
finish() await finish()
}
}}
next={() => {
if (fields.length === i + 1) {
// prevent going back!
swiper.allowSlidePrev = true
if (!endPage.show) {
Modal.success({
content: t('form:submitted'),
okText: t('from:restart'),
onOk: () => {
window.location.reload()
},
})
} }
} }}
next={() => {
if (fields.length === i + 1) {
// prevent going back!
swiper.allowSlidePrev = true
goNext() if (!endPage.show) {
}} Modal.success({
prev={goPrev} content: t('form:submitted'),
/> okText: t('from:restart'),
onOk: () => {
window.location.reload()
},
})
}
}
goNext()
}}
prev={goPrev}
/>
</SwiperSlide>
) )
}) })
.filter((e) => e !== null), .filter((e) => e !== null),
endPage.show ? ( endPage.show ? (
<FormPage key={'end'} page={endPage} design={design} next={finish} prev={goPrev} /> <SwiperSlide key={'end'}>
<FormPage page={endPage} design={design} next={finish} prev={goPrev} />
</SwiperSlide>
) : undefined, ) : undefined,
].filter((e) => !!e)} ].filter((e) => !!e)}
</Swiper> </Swiper>

View File

@ -1,6 +1,7 @@
.main { .main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
.content { .content {
flex: 1; flex: 1;

View File

@ -32,7 +32,7 @@ export const FormPage: React.FC<Props> = ({ page, design, next, prev, className,
<StyledH1 design={design} type={'question'}> <StyledH1 design={design} type={'question'}>
{page.title} {page.title}
</StyledH1> </StyledH1>
<StyledMarkdown design={design} type={'question'} source={page.paragraph} /> <StyledMarkdown design={design} type={'question'}>{page.paragraph}</StyledMarkdown>
</div> </div>
<div <div
style={{ style={{

View File

@ -1,11 +1,13 @@
import { CaretDownOutlined, UserOutlined } from '@ant-design/icons' import { CaretDownOutlined, UserOutlined } from '@ant-design/icons'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons/lib' import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons/lib'
import { Alert, Dropdown, Layout, Menu, PageHeader, Select, Space, Spin, Tag } from 'antd' import { Alert, Dropdown, Layout, Menu, PageHeader, Select, Space, Spin, Tag } from 'antd'
import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { CSSProperties, FunctionComponent } from 'react' import React, { CSSProperties, FunctionComponent } from 'react'
import GitHubButton from 'react-github-button' import GitHubButton from 'react-github-button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LogoWhitePng from '../assets/images/logo_white.png'
import { useMeQuery } from '../graphql/query/me.query' import { useMeQuery } from '../graphql/query/me.query'
import { languages } from '../i18n' import { languages } from '../i18n'
import { sideMenu, SideMenuElement } from './sidemenu' import { sideMenu, SideMenuElement } from './sidemenu'
@ -86,7 +88,7 @@ export const Structure: FunctionComponent<Props> = (props) => {
<ItemGroup <ItemGroup
key={element.key} key={element.key}
title={ title={
<div <Space
style={{ style={{
textTransform: 'uppercase', textTransform: 'uppercase',
paddingTop: 16, paddingTop: 16,
@ -95,8 +97,10 @@ export const Structure: FunctionComponent<Props> = (props) => {
}} }}
> >
{element.icon} {element.icon}
{t(element.name)} <div>
</div> {t(element.name)}
</div>
</Space>
} }
> >
{buildMenu(element.items)} {buildMenu(element.items)}
@ -108,10 +112,12 @@ export const Structure: FunctionComponent<Props> = (props) => {
<SubMenu <SubMenu
key={element.key} key={element.key}
title={ title={
<span> <Space>
{element.icon} {element.icon}
{t(element.name)} <div>
</span> {t(element.name)}
</div>
</Space>
} }
> >
{buildMenu(element.items)} {buildMenu(element.items)}
@ -128,8 +134,12 @@ export const Structure: FunctionComponent<Props> = (props) => {
}} }}
key={element.key} key={element.key}
> >
{element.icon} <Space>
{t(element.name)} {element.icon}
<div>
{t(element.name)}
</div>
</Space>
</Menu.Item> </Menu.Item>
) )
} }
@ -148,7 +158,7 @@ export const Structure: FunctionComponent<Props> = (props) => {
paddingLeft: 0, paddingLeft: 0,
}} }}
> >
<div <Space
style={{ style={{
float: 'left', float: 'left',
color: '#FFF', color: '#FFF',
@ -162,13 +172,18 @@ export const Structure: FunctionComponent<Props> = (props) => {
onClick: () => setSidebar(!sidebar), onClick: () => setSidebar(!sidebar),
})} })}
<img <div style={{
src={require('assets/images/logo_white_small.png') as string} display: 'flex',
height={30} alignItems: 'center',
style={{ marginRight: 16 }} }}>
alt={'OhMyForm'} <Image
/> src={LogoWhitePng.src}
</div> width={1608 / 12}
height={530 / 12}
alt={'OhMyForm'}
/>
</div>
</Space>
<div style={{ float: 'right', display: 'flex', height: '100%' }}> <div style={{ float: 'right', display: 'flex', height: '100%' }}>
<Dropdown <Dropdown
overlay={ overlay={
@ -218,9 +233,9 @@ export const Structure: FunctionComponent<Props> = (props) => {
style={{ flex: 1 }} style={{ flex: 1 }}
defaultSelectedKeys={['1']} defaultSelectedKeys={['1']}
selectedKeys={selected} selectedKeys={selected}
onSelect={(s): void => setSelected(s.keyPath as string[])} onSelect={(s): void => setSelected(s.keyPath )}
openKeys={open} openKeys={open}
onOpenChange={(open): void => setOpen(open as string[])} onOpenChange={(open): void => setOpen(open )}
> >
{buildMenu(sideMenu)} {buildMenu(sideMenu)}
</Menu> </Menu>

View File

@ -1,8 +1,8 @@
import { Button } from 'antd' import { Button } from 'antd'
import { ButtonProps } from 'antd/lib/button/button' import { ButtonProps } from 'antd/lib/button/button'
import { darken, lighten } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { darken, lighten } from './color.change'
interface Props extends ButtonProps { interface Props extends ButtonProps {
background: string background: string
@ -14,12 +14,12 @@ interface Props extends ButtonProps {
const Styled = styled(Button)` const Styled = styled(Button)`
background: ${(props: Props) => props.background}; background: ${(props: Props) => props.background};
color: ${(props: Props) => props.color}; color: ${(props: Props) => props.color};
border-color: ${(props: Props) => darken(props.background, 10)}; border-color: ${(props: Props) => darken(0.1, props.background)};
:hover { :hover {
color: ${(props: Props) => props.highlight}; color: ${(props: Props) => props.highlight};
background-color: ${(props: Props) => lighten(props.background, 10)}; background-color: ${(props: Props) => lighten(0.1, props.background)};
border-color: ${(props: Props) => darken(props.highlight, 10)}; border-color: ${(props: Props) => darken(0.1, props.highlight)};
} }
` `

View File

@ -1,61 +0,0 @@
/* eslint-disable */
/**
* @link https://css-tricks.com/snippets/javascript/lighten-darken-color/
*
* @author Chris Coyier
*/
function LightenDarkenColor(col, amt): string {
let usePound = false
if (col[0] == '#') {
col = col.slice(1)
usePound = true
}
const num = parseInt(col, 16)
let r = (num >> 16) + amt
if (r > 255) r = 255
else if (r < 0) r = 0
let b = ((num >> 8) & 0x00ff) + amt
if (b > 255) b = 255
else if (b < 0) b = 0
let g = (num & 0x0000ff) + amt
if (g > 255) g = 255
else if (g < 0) g = 0
return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16)
}
export const transparentize = (col: string, amt: number): string => {
if (col[0] == '#') {
col = col.slice(1)
}
const num = parseInt(col, 16)
let r = (num >> 16) + amt
if (r > 255) r = 255
else if (r < 0) r = 0
let b = ((num >> 8) & 0x00ff) + amt
if (b > 255) b = 255
else if (b < 0) b = 0
let g = (num & 0x0000ff) + amt
if (g > 255) g = 255
else if (g < 0) g = 0
return `rgba(${r}, ${b}, ${g}, ${1 - amt / 100})`
}
export const lighten = (color: string, amount: number): string => LightenDarkenColor(color, amount)
export const darken = (color: string, amount: number): string => LightenDarkenColor(color, -amount)

View File

@ -1,10 +1,10 @@
import { DatePicker } from 'antd' import { DatePicker } from 'antd'
import { PickerProps } from 'antd/lib/date-picker/generatePicker' import { PickerProps } from 'antd/lib/date-picker/generatePicker'
import { Moment } from 'moment' import { Moment } from 'moment'
import { transparentize } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
type Props = { design: FormPublicDesignFragment } & PickerProps<Moment> type Props = { design: FormPublicDesignFragment } & PickerProps<Moment>
@ -36,7 +36,7 @@ const Field = styled(DatePicker)`
color: ${(props: Props) => props.design.colors.answer}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answer, 60)}; color: ${(props: Props) => transparentize(0.6, props.design.colors.answer)};
} }
} }

View File

@ -1,9 +1,9 @@
import { Input } from 'antd' import { Input } from 'antd'
import { InputProps } from 'antd/lib/input/Input' import { InputProps } from 'antd/lib/input/Input'
import { transparentize } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends InputProps { interface Props extends InputProps {
design: FormPublicDesignFragment design: FormPublicDesignFragment
@ -37,7 +37,7 @@ const Field = styled(Input)`
color: ${(props: Props) => props.design.colors.answer}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answer, 60)}; color: ${(props: Props) => transparentize(0.6, props.design.colors.answer)};
} }
} }

View File

@ -1,10 +1,11 @@
import { lighten } from 'polished'
import React from 'react' import React from 'react'
import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown' import ReactMarkdown from 'react-markdown'
import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { lighten } from './color.change'
interface Props extends ReactMarkdownProps { interface Props extends ReactMarkdownOptions {
type: 'question' | 'answer' type: 'question' | 'answer'
design: FormPublicDesignFragment design: FormPublicDesignFragment
} }
@ -29,7 +30,7 @@ const Markdown = styled(ReactMarkdown)`
} }
blockquote { blockquote {
color: ${(props: Props) => lighten(getColor(props), 50)}; color: ${(props: Props) => lighten(0.5, getColor(props))};
padding-left: 20px; padding-left: 20px;
border-left: 10px rgba(0, 0, 0, 0.05) solid; border-left: 10px rgba(0, 0, 0, 0.05) solid;
} }
@ -56,7 +57,7 @@ const Markdown = styled(ReactMarkdown)`
export const StyledMarkdown: React.FC<Props> = ({ children, ...props }) => { export const StyledMarkdown: React.FC<Props> = ({ children, ...props }) => {
return ( return (
<Markdown escapeHtml={false} {...props}> <Markdown {...props}>
{children} {children}
</Markdown> </Markdown>
) )

View File

@ -1,9 +1,9 @@
import { InputNumber } from 'antd' import { InputNumber } from 'antd'
import { InputNumberProps } from 'antd/lib/input-number' import { InputNumberProps } from 'antd/lib/input-number'
import { transparentize } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends InputNumberProps { interface Props extends InputNumberProps {
design: FormPublicDesignFragment design: FormPublicDesignFragment
@ -38,7 +38,7 @@ const Field = styled(InputNumber)`
color: ${(props: Props) => props.design.colors.answer}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answer, 60)}; color: ${(props: Props) => transparentize(0.6, props.design.colors.answer)};
} }
} }

View File

@ -1,9 +1,9 @@
import { Select } from 'antd' import { Select } from 'antd'
import { SelectProps } from 'antd/lib/select' import { SelectProps } from 'antd/lib/select'
import { transparentize } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends SelectProps<string> { interface Props extends SelectProps<string> {
design: FormPublicDesignFragment design: FormPublicDesignFragment
@ -36,7 +36,7 @@ const Field = styled(Select)`
color: ${(props: Props) => props.design.colors.answer}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answer, 60)}; color: ${(props: Props) => transparentize(0.6, props.design.colors.answer)};
} }
} }

View File

@ -1,9 +1,9 @@
import { Input } from 'antd' import { Input } from 'antd'
import { TextAreaProps } from 'antd/lib/input/TextArea' import { TextAreaProps } from 'antd/lib/input/TextArea'
import { transparentize } from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends TextAreaProps { interface Props extends TextAreaProps {
design: FormPublicDesignFragment design: FormPublicDesignFragment
@ -35,7 +35,7 @@ const Field = styled(Input.TextArea)`
color: ${(props: Props) => props.design.colors.answer}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answer, 60)}; color: ${(props: Props) => transparentize(0.6, props.design.colors.answer)};
} }
} }

View File

@ -7,12 +7,10 @@ export const useImperativeQuery: <TData, TVariables>(
) => (variables: TVariables) => Promise<ApolloQueryResult<TData>> = (query) => { ) => (variables: TVariables) => Promise<ApolloQueryResult<TData>> = (query) => {
const { refetch } = useQuery(query, { skip: true }) const { refetch } = useQuery(query, { skip: true })
const cb = useCallback( return useCallback(
(variables) => { (variables) => {
return refetch(variables) return refetch(variables)
}, },
[refetch] [refetch]
) )
return cb
} }

View File

@ -1,6 +1,9 @@
import debug from 'debug'
import { all, create } from 'mathjs' import { all, create } from 'mathjs'
import { useState } from 'react' import { useState } from 'react'
const logger = debug('useMath')
export const useMath = (): (( export const useMath = (): ((
expression: string, expression: string,
values?: { [id: string]: string | number } values?: { [id: string]: string | number }
@ -14,15 +17,24 @@ export const useMath = (): ((
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) => {
const r = new RegExp(key.replace('$', '\\$'), 'ig') const r = new RegExp(key.replace('$', '\\$'), 'ig')
if (r.test(processed)) { const test = r.test(processed)
if (test) {
processed = processed.replace(r, String(values[key])) processed = processed.replace(r, String(values[key]))
} }
}) })
const result = math.evaluate(processed) return Boolean(math.evaluate(processed))
return Boolean(result)
} catch (e) { } catch (e) {
logger(
'failed to calculate %O: %s',
{
expression,
values,
},
e.message
)
throw e throw e
} }
} }

View File

@ -1,6 +1,8 @@
import { NextRouter, useRouter as useNextRouter } from 'next/router' import { NextRouter, useRouter as useNextRouter } from 'next/router'
const parseQuery = (path) => { type parseQueryResponse = { [key: string]: string }
const parseQuery = (path: string): parseQueryResponse => {
const query = {} const query = {}
const regex = /[?&]([^&$=]+)(=([^&$]+))?/g const regex = /[?&]([^&$=]+)(=([^&$]+))?/g
let param: RegExpExecArray let param: RegExpExecArray

View File

@ -1,4 +1,5 @@
import { useMutation } from '@apollo/client' import { useMutation } from '@apollo/client'
import debug from 'debug'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { import {
SUBMISSION_SET_FIELD_MUTATION, SUBMISSION_SET_FIELD_MUTATION,
@ -11,6 +12,8 @@ import {
SubmissionStartMutationVariables, SubmissionStartMutationVariables,
} from '../graphql/mutation/submission.start.mutation' } from '../graphql/mutation/submission.start.mutation'
const logger = debug('useSubmission')
export interface Submission { export interface Submission {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
setField: (fieldId: string, data: unknown) => Promise<void> setField: (fieldId: string, data: unknown) => Promise<void>
@ -44,18 +47,19 @@ export const useSubmission = (formId: string): Submission => {
}, },
}) })
.then(({ data }) => { .then(({ data }) => {
logger('submission id = %O', data.submission.id)
setSubmission({ setSubmission({
id: data.submission.id, id: data.submission.id,
token, token,
}) })
}) })
.catch((e: Error) => console.error('failed to start submission', e)) .catch((e: Error) => logger('failed to start submission %J', e))
}, [formId]) }, [formId])
const setField = useCallback( const setField = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async (fieldId: string, data: any) => { async (fieldId: string, data: any) => {
console.log('just save', fieldId, data) logger('save field id=%O %O', fieldId, data)
await save({ await save({
variables: { variables: {
submission: submission.id, submission: submission.id,
@ -71,7 +75,7 @@ export const useSubmission = (formId: string): Submission => {
) )
const finish = useCallback(async () => { const finish = useCallback(async () => {
console.log('finish submission!!', formId) logger('finish submission!!', formId)
await Promise.resolve() await Promise.resolve()
}, [submission]) }, [submission])

View File

@ -48,7 +48,9 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
getValueFromEvent={(e) => { getValueFromEvent={(e) => {
switch (e) { switch (e) {
case 'superuser': case 'superuser':
return ['user', 'admin', 'superuser'] return [
'user', 'admin', 'superuser',
]
case 'admin': case 'admin':
return ['user', 'admin'] return ['user', 'admin']
default: default:
@ -70,7 +72,9 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
}} }}
> >
<Select> <Select>
{['user', 'admin', 'superuser'].map((role) => ( {[
'user', 'admin', 'superuser',
].map((role) => (
<Select.Option value={role} key={role}> <Select.Option value={role} key={role}>
{role.toUpperCase()} {role.toUpperCase()}
</Select.Option> </Select.Option>

View File

@ -40,6 +40,7 @@ export interface FormFieldLogicFragment {
export interface FormFieldFragment { export interface FormFieldFragment {
id: string id: string
idx?: number
title: string title: string
slug?: string slug?: string
type: string type: string
@ -126,6 +127,7 @@ export const FORM_FRAGMENT = gql`
fields { fields {
id id
idx
title title
slug slug
type type

View File

@ -16,7 +16,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation update($form: FormCreateInput!) { mutation createForm($form: FormCreateInput!) {
form: createForm(form: $form) { form: createForm(form: $form) {
...Form ...Form
} }

View File

@ -12,7 +12,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation delete($id: ID!) { mutation deleteForm($id: ID!) {
form: deleteForm(id: $id) { form: deleteForm(id: $id) {
id id
} }

View File

@ -11,7 +11,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation update($form: FormUpdateInput!) { mutation updateForm($form: FormUpdateInput!) {
form: updateForm(form: $form) { form: updateForm(form: $form) {
...Form ...Form
} }

View File

@ -13,7 +13,7 @@ export interface LoginMutationVariables {
} }
export const LOGIN_MUTATION = gql` export const LOGIN_MUTATION = gql`
mutation login($username: String!, $password: String!) { mutation authLogin($username: String!, $password: String!) {
tokens: authLogin(username: $username, password: $password) { tokens: authLogin(username: $username, password: $password) {
access: accessToken access: accessToken
refresh: refreshToken refresh: refreshToken

View File

@ -20,7 +20,7 @@ export interface Variables {
} }
export const MUTATION = gql` export const MUTATION = gql`
mutation update($user: ProfileUpdateInput!) { mutation updateProfile($user: ProfileUpdateInput!) {
form: updateProfile(user: $user) { form: updateProfile(user: $user) {
...AdminProfile ...AdminProfile
} }

View File

@ -19,7 +19,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation register($user: UserCreateInput!) { mutation authRegister($user: UserCreateInput!) {
tokens: authRegister(user: $user) { tokens: authRegister(user: $user) {
access: accessToken access: accessToken
refresh: refreshToken refresh: refreshToken

View File

@ -1,7 +1,7 @@
import { gql } from '@apollo/client/core' import { gql } from '@apollo/client/core'
export const SUBMISSION_FINISH_MUTATION = gql` export const SUBMISSION_FINISH_MUTATION = gql`
mutation start($submission: ID!, $field: SubmissionSetFieldInput!) { mutation submissionSetField($submission: ID!, $field: SubmissionSetFieldInput!) {
submission: submissionSetField(submission: $submission, field: $field) { submission: submissionSetField(submission: $submission, field: $field) {
id id
percentageComplete percentageComplete

View File

@ -12,12 +12,11 @@ export interface SubmissionSetFieldMutationVariables {
field: { field: {
token: string token: string
field: string field: string
data: string
} }
} }
export const SUBMISSION_SET_FIELD_MUTATION = gql` export const SUBMISSION_SET_FIELD_MUTATION = gql`
mutation start($submission: ID!, $field: SubmissionSetFieldInput!) { mutation submissionSetField($submission: ID!, $field: SubmissionSetFieldInput!) {
submission: submissionSetField(submission: $submission, field: $field) { submission: submissionSetField(submission: $submission, field: $field) {
id id
percentageComplete percentageComplete

View File

@ -19,7 +19,7 @@ export interface SubmissionStartMutationVariables {
} }
export const SUBMISSION_START_MUTATION = gql` export const SUBMISSION_START_MUTATION = gql`
mutation start($form: ID!, $submission: SubmissionStartInput!) { mutation submissionStart($form: ID!, $submission: SubmissionStartInput!) {
submission: submissionStart(form: $form, submission: $submission) { submission: submissionStart(form: $form, submission: $submission) {
id id
percentageComplete percentageComplete

View File

@ -12,7 +12,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation delete($id: ID!) { mutation deleteUser($id: ID!) {
form: deleteUser(id: $id) { form: deleteUser(id: $id) {
id id
} }

View File

@ -11,7 +11,7 @@ interface Variables {
} }
const MUTATION = gql` const MUTATION = gql`
mutation update($user: UserUpdateInput!) { mutation updateUser($user: UserUpdateInput!) {
form: updateUser(user: $user) { form: updateUser(user: $user) {
...AdminUser ...AdminUser
} }

View File

@ -7,7 +7,7 @@ interface Data {
} }
export const QUERY = gql` export const QUERY = gql`
query profile { query adminMe {
user: me { user: me {
...AdminProfile ...AdminProfile
} }

View File

@ -15,7 +15,7 @@ export interface AdminStatisticQueryData {
export interface AdminStatisticQueryVariables {} export interface AdminStatisticQueryVariables {}
export const ADMIN_STATISTIC_QUERY = gql` export const ADMIN_STATISTIC_QUERY = gql`
query { query statistics {
forms: getFormStatistic { forms: getFormStatistic {
total total
} }

View File

@ -10,7 +10,7 @@ export interface AdminUserQueryVariables {
} }
export const ADMIN_USER_QUERY = gql` export const ADMIN_USER_QUERY = gql`
query user($id: ID!) { query getUserById($id: ID!) {
user: getUserById(id: $id) { user: getUserById(id: $id) {
...AdminUser ...AdminUser
} }

View File

@ -18,7 +18,7 @@ interface Variables {
} }
const QUERY = gql` const QUERY = gql`
query pager($start: Int, $limit: Int) { query listForms($start: Int, $limit: Int) {
pager: listForms(start: $start, limit: $limit) { pager: listForms(start: $start, limit: $limit) {
entries { entries {
...Form ...Form

View File

@ -11,7 +11,7 @@ interface Variables {
} }
const QUERY = gql` const QUERY = gql`
query form($id: ID!) { query getFormById($id: ID!) {
form: getFormById(id: $id) { form: getFormById(id: $id) {
...Form ...Form
} }

View File

@ -23,7 +23,7 @@ interface Variables {
} }
const QUERY = gql` const QUERY = gql`
query pager($form: ID!, $start: Int, $limit: Int) { query listSubmissions($form: ID!, $start: Int, $limit: Int) {
form: getFormById(id: $form) { form: getFormById(id: $form) {
...Form ...Form
} }

View File

@ -18,7 +18,7 @@ interface Variables {
} }
const QUERY = gql` const QUERY = gql`
query pager($start: Int, $limit: Int) { query listUsers($start: Int, $limit: Int) {
pager: listUsers(start: $start, limit: $limit) { pager: listUsers(start: $start, limit: $limit) {
entries { entries {
...User ...User

5
next-env.d.ts vendored
View File

@ -1,2 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,14 +1,13 @@
const withImages = require('next-images')
const p = require('./package.json') const p = require('./package.json')
const environment = process.env.NODE_ENV ? process.env.NODE_ENV : 'dev';
const version = p.version; const version = p.version;
module.exports = withImages({ module.exports = {
poweredByHeader: true, poweredByHeader: true,
future: { productionBrowserSourceMaps: true,
webpack5: true,
},
publicRuntimeConfig: { publicRuntimeConfig: {
environment,
endpoint: process.env.ENDPOINT || '/graphql', endpoint: process.env.ENDPOINT || '/graphql',
spa: !!process.env.SPA || false, spa: !!process.env.SPA || false,
mainBackground: process.env.MAIN_BACKGROUND || '#8FA2A6' mainBackground: process.env.MAIN_BACKGROUND || '#8FA2A6'
@ -19,4 +18,4 @@ module.exports = withImages({
env: { env: {
version, version,
} }
}) }

View File

@ -1,5 +1,6 @@
export interface NextConfigType { export interface NextConfigType {
publicRuntimeConfig: { publicRuntimeConfig: {
environment: string,
endpoint: string endpoint: string
spa?: boolean spa?: boolean
mainBackground?: string mainBackground?: string

View File

@ -1,6 +1,6 @@
{ {
"name": "ohmyform-react", "name": "ohmyform-react",
"version": "1.0.0-alpha", "version": "1.0.0",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
"start:dev": "next dev -p 4000", "start:dev": "next dev -p 4000",
@ -13,60 +13,60 @@
"translation:missing": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node locales/missing.ts" "translation:missing": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node locales/missing.ts"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.7.0",
"@apollo/client": "^3.3.15", "@apollo/client": "^3.5.6",
"@lifeomic/axios-fetch": "^2.0.0", "antd": "^4.18.2",
"antd": "^4.15.3", "axios": "^0.24.0",
"axios": "^0.21.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dayjs": "^1.10.4", "dayjs": "^1.10.7",
"exceljs": "^4.2.1", "debug": "^4.3.3",
"graphql": "^15.5.0", "exceljs": "^4.3.0",
"i18next": "^19.9.2", "graphql": "^15.8.0",
"i18next-browser-languagedetector": "^6.1.0", "i18next": "^21.6.4",
"i18next-browser-languagedetector": "^6.1.2",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"mathjs": "^9.3.2", "mathjs": "^10.0.2",
"next": "^10.2.0", "next": "^12.0.7",
"next-images": "^1.7.0", "next-redux-wrapper": "^7.0.5",
"next-redux-wrapper": "^6.0.2", "polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-github-button": "^0.1.11", "react-github-button": "^0.1.11",
"react-i18next": "^11.8.15", "react-i18next": "^11.15.3",
"react-icons": "^3.11.0", "react-icons": "^4.3.1",
"react-id-swiper": "^4.0.0", "react-id-swiper": "^4.0.0",
"react-markdown": "^4.3.1", "react-markdown": "^7.1.2",
"react-redux": "^7.2.4", "react-redux": "^7.2.6",
"redux": "^4.1.0", "redux": "^4.1.2",
"redux-devtools-extension": "^2.13.9", "redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.4.1",
"sass": "^1.32.12", "sass": "^1.45.2",
"styled-components": "^5.2.3", "styled-components": "^5.3.3",
"swiper": "^6.5.8" "swiper": "^7.4.1"
}, },
"devDependencies": { "devDependencies": {
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/lifeomic__axios-fetch": "^1.5.0", "@types/mathjs": "^9.4.2",
"@types/mathjs": "^6.0.12", "@types/node": "^16.11.17",
"@types/node": "^14.14.43", "@types/node-fetch": "^3.0.3",
"@types/node-fetch": "^2.5.10",
"@types/react": "^17.0.4", "@types/react": "^17.0.4",
"@types/styled-components": "^5.1.9", "@types/styled-components": "^5.1.19",
"@types/swiper": "^5.4.2", "@types/swiper": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^5.8.1",
"commander": "^6.2.1", "commander": "^8.3.0",
"eslint": "^7.25.0", "eslint": "^8.6.0",
"eslint-config-prettier": "^6.15.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.3.0",
"glob": "^7.1.6", "eslint-plugin-unused-imports": "^2.0.0",
"glob": "^7.2.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"prettier": "^2.1.1", "prettier": "^2.5.1",
"ts-node": "^9.1.1", "ts-node": "^10.4.0",
"typescript": "^4.2.4" "typescript": "^4.5.4"
} }
} }

View File

@ -2,15 +2,26 @@ import { ApolloProvider } from '@apollo/client'
import 'antd/dist/antd.css' import 'antd/dist/antd.css'
import 'assets/global.scss' import 'assets/global.scss'
import 'assets/variables.scss' import 'assets/variables.scss'
import debug from 'debug'
import 'i18n' import 'i18n'
import App, { AppInitialProps } from 'next/app' import getConfig from 'next/config'
import { AppType } from 'next/dist/next-server/lib/utils' import { AppInitialProps, AppType } from 'next/dist/shared/lib/utils'
import Head from 'next/head' import Head from 'next/head'
import React from 'react' import React, { useEffect } from 'react'
import { wrapper } from 'store' import { wrapper } from 'store'
import getClient from '../graphql/client' import getClient from '../graphql/client'
import { NextConfigType } from '../next.config.type'
const { publicRuntimeConfig } = getConfig() as NextConfigType
const App: AppType = ({ Component, pageProps }) => {
useEffect(() => {
if (publicRuntimeConfig.environment !== 'production') {
debug.enable('*,-micromark')
}
})
const MyApp: AppType = ({ Component, pageProps }) => {
return ( return (
<ApolloProvider client={getClient()}> <ApolloProvider client={getClient()}>
<Head> <Head>
@ -22,6 +33,8 @@ const MyApp: AppType = ({ Component, pageProps }) => {
) )
} }
MyApp.getInitialProps = (context): Promise<AppInitialProps> => App.getInitialProps(context as any) App.getInitialProps = (): AppInitialProps => ({
pageProps: {},
})
export default wrapper.withRedux(MyApp) export default wrapper.withRedux(App)

View File

@ -34,21 +34,23 @@ const Index: NextPage = () => {
return { return {
form: { form: {
...next.form, ...next.form,
fields: next.form.fields.map((field) => { fields: next.form.fields
const keys: FormFieldOptionKeysFragment = {} .map((field) => {
const keys: FormFieldOptionKeysFragment = {}
field.options.forEach((option) => { field.options.forEach((option) => {
if (option.key) { if (option.key) {
keys[option.key] = option.value keys[option.key] = option.value
}
})
return {
...field,
options: field.options.filter((option) => !option.key),
optionKeys: keys,
} }
}) })
.sort((a, b) => a.idx - b.idx),
return {
...field,
options: field.options.filter((option) => !option.key),
optionKeys: keys,
}
}),
}, },
} }
} }
@ -69,7 +71,7 @@ const Index: NextPage = () => {
formData.form.fields = formData.form.fields formData.form.fields = formData.form.fields
.filter((e) => e && e.type) .filter((e) => e && e.type)
.map(({ optionKeys, ...field }) => { .map(({ optionKeys, ...field }, index) => {
const options = field.options const options = field.options
if (optionKeys) { if (optionKeys) {
@ -89,6 +91,7 @@ const Index: NextPage = () => {
return { return {
...field, ...field,
options, options,
idx: index,
} }
}) })

View File

@ -2,9 +2,11 @@ import { Alert, Layout } from 'antd'
import { AuthFooter } from 'components/auth/footer' import { AuthFooter } from 'components/auth/footer'
import { GetStaticProps, NextPage } from 'next' import { GetStaticProps, NextPage } from 'next'
import getConfig from 'next/config' import getConfig from 'next/config'
import Image from 'next/image'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LogoWhitePng from '../assets/images/logo_white.png'
import { LoadingPage } from '../components/loading.page' import { LoadingPage } from '../components/loading.page'
import { Omf } from '../components/omf' import { Omf } from '../components/omf'
import { useStatusQuery } from '../graphql/query/status.query' import { useStatusQuery } from '../graphql/query/status.query'
@ -24,7 +26,9 @@ const Index: NextPage = () => {
if (router.pathname !== window.location.pathname) { if (router.pathname !== window.location.pathname) {
let href = router.asPath let href = router.asPath
const as = router.asPath const as = router.asPath
const possible = [/(\/form\/)[^/]+/i, /(\/admin\/forms\/)[^/]+/i, /(\/admin\/users\/)[^/]+/i] const possible = [
/(\/form\/)[^/]+/i, /(\/admin\/forms\/)[^/]+/i, /(\/admin\/users\/)[^/]+/i,
]
possible.forEach((r) => { possible.forEach((r) => {
if (r.test(as)) { if (r.test(as)) {
@ -58,16 +62,22 @@ const Index: NextPage = () => {
}} }}
> >
<Omf /> <Omf />
<img <div
alt={'OhMyForm'}
style={{ style={{
margin: 'auto', margin: 'auto',
maxWidth: '90%', maxWidth: '90%',
width: 500, width: 500,
textAlign: 'center', textAlign: 'center',
}} }}
src={require('../assets/images/logo_white.png') as string} >
/> <Image
alt={'OhMyForm'}
layout={'responsive'}
width={1608 / 4}
height={530 / 4}
src={LogoWhitePng.src}
/>
</div>
{status.error && ( {status.error && (
<Alert <Alert

View File

@ -10,11 +10,13 @@ import {
LoginMutationVariables, LoginMutationVariables,
} from 'graphql/mutation/login.mutation' } from 'graphql/mutation/login.mutation'
import { NextPage } from 'next' import { NextPage } from 'next'
import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import LogoWhitePng from '../../assets/images/logo_white.png'
import { Omf } from '../../components/omf' import { Omf } from '../../components/omf'
import { useSettingsQuery } from '../../graphql/query/settings.query' import { useSettingsQuery } from '../../graphql/query/settings.query'
import scss from './index.module.scss' import scss from './index.module.scss'
@ -64,9 +66,7 @@ const Index: NextPage = () => {
width: 400, width: 400,
}} }}
> >
<img <div
src={require('../../assets/images/logo_white_small.png') as string}
alt={'OhMyForm'}
style={{ style={{
display: 'block', display: 'block',
width: '70%', width: '70%',
@ -74,14 +74,21 @@ const Index: NextPage = () => {
marginRight: 'auto', marginRight: 'auto',
marginBottom: 16, marginBottom: 16,
}} }}
/> >
<Image
src={LogoWhitePng.src}
alt={'OhMyForm'}
width={1608 / 4}
height={530 / 4}
/>
</div>
{data && data.loginNote.value && ( {data && data.loginNote.value && (
<Alert <Alert
type="warning" type="warning"
showIcon showIcon
message={t('login:note')} message={t('login:note')}
description={<ReactMarkdown escapeHtml={false} source={data.loginNote.value} />} description={<ReactMarkdown>{data.loginNote.value}</ReactMarkdown>}
style={{ style={{
marginBottom: 24, marginBottom: 24,
}} }}

View File

@ -5,10 +5,12 @@ import { AuthLayout } from 'components/auth/layout'
import { setAuth } from 'components/with.auth' import { setAuth } from 'components/with.auth'
import { RegisterUserData, useRegisterMutation } from 'graphql/mutation/register.mutation' import { RegisterUserData, useRegisterMutation } from 'graphql/mutation/register.mutation'
import { NextPage } from 'next' import { NextPage } from 'next'
import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import LogoWhitePng from '../assets/images/logo_white.png'
import { ErrorPage } from '../components/error.page' import { ErrorPage } from '../components/error.page'
import { Omf } from '../components/omf' import { Omf } from '../components/omf'
import { useSettingsQuery } from '../graphql/query/settings.query' import { useSettingsQuery } from '../graphql/query/settings.query'
@ -66,9 +68,7 @@ const Register: NextPage = () => {
width: 400, width: 400,
}} }}
> >
<img <div
src={require('../assets/images/logo_white_small.png') as string}
alt={'OhMyForm'}
style={{ style={{
display: 'block', display: 'block',
width: '70%', width: '70%',
@ -76,7 +76,14 @@ const Register: NextPage = () => {
marginRight: 'auto', marginRight: 'auto',
marginBottom: 16, marginBottom: 16,
}} }}
/> >
<Image
src={LogoWhitePng.src}
alt={'OhMyForm'}
width={1608 / 4}
height={530 / 4}
/>
</div>
<Form.Item <Form.Item
name="username" name="username"

View File

@ -1,425 +0,0 @@
# This file was generated based on ".graphqlconfig". Do not edit manually.
schema {
query: Query
mutation: Mutation
}
type AuthToken {
accessToken: String!
refreshToken: String!
}
type Button {
action: String
activeColor: String
bgColor: String
color: String
id: ID!
text: String
url: String
}
type Colors {
answer: String!
background: String!
button: String!
buttonActive: String!
buttonText: String!
question: String!
}
type Deleted {
id: String!
}
type Design {
colors: Colors!
font: String
layout: String
}
type Device {
language: String
name: String!
type: String!
}
type Form {
admin: User
created: DateTime!
design: Design!
endPage: Page!
fields: [FormField!]!
hooks: [FormHook!]!
id: ID!
isLive: Boolean!
language: String!
lastModified: DateTime
notifications: [FormNotification!]!
showFooter: Boolean!
startPage: Page!
title: String!
}
type FormField {
description: String!
id: ID!
logic: [FormFieldLogic!]!
options: [FormFieldOption!]!
rating: FormFieldRating
required: Boolean!
slug: String
title: String!
type: String!
value: String!
}
type FormFieldLogic {
action: String!
disable: Boolean
enabled: Boolean!
formula: String
id: ID!
jumpTo: ID
require: Boolean
visible: Boolean
}
type FormFieldOption {
id: ID!
key: String
title: String
value: String!
}
type FormFieldRating {
shape: String
steps: Int
}
type FormHook {
enabled: Boolean!
format: String
id: ID!
url: String
}
type FormNotification {
enabled: Boolean!
fromEmail: String
fromField: String
htmlTemplate: String
id: ID!
subject: String
toEmail: String
toField: String
}
type FormPager {
entries: [Form!]!
limit: Int!
start: Int!
total: Int!
}
type FormStatistic {
total: Int!
}
type GeoLocation {
city: String
country: String
}
type Mutation {
authLogin(password: String!, username: String!): AuthToken!
authRegister(user: UserCreateInput!): AuthToken!
createForm(form: FormCreateInput!): Form!
deleteForm(id: ID!): Deleted!
deleteUser(id: ID!): Deleted!
submissionSetField(field: SubmissionSetFieldInput!, submission: ID!): SubmissionProgress!
submissionStart(form: ID!, submission: SubmissionStartInput!): SubmissionProgress!
updateForm(form: FormUpdateInput!): Form!
updateProfile(user: ProfileUpdateInput!): Profile!
updateUser(user: UserUpdateInput!): User!
}
type Page {
buttonText: String
buttons: [Button!]!
id: ID!
paragraph: String
show: Boolean!
title: String
}
type Profile {
created: DateTime!
email: String!
firstName: String
id: ID!
language: String!
lastModified: DateTime
lastName: String
roles: [String!]!
username: String!
verifiedEmail: Boolean!
}
type Query {
getFormById(id: ID!): Form!
getFormStatistic: FormStatistic!
getSetting(key: ID!): Setting!
getSettings: SettingPager!
getSubmissionStatistic: SubmissionStatistic!
getUserById(id: ID!): User!
getUserStatistic: UserStatistic!
listForms(limit: Int = 50, start: Int = 0): FormPager!
listSubmissions(form: ID!, limit: Int = 50, start: Int = 0): SubmissionPager!
listUsers(limit: Int = 50, start: Int = 0): UserPager!
me: Profile!
status: Version!
}
type Setting {
isFalse: Boolean!
isTrue: Boolean!
key: ID!
value: String
}
type SettingPager {
entries: [Setting!]!
limit: Int!
start: Int!
total: Int!
}
type Submission {
created: DateTime!
device: Device!
fields: [SubmissionField!]!
geoLocation: GeoLocation!
id: ID!
ipAddr: String!
lastModified: DateTime
percentageComplete: Float!
timeElapsed: Float!
}
type SubmissionField {
field: FormField
id: ID!
type: String!
value: String!
}
type SubmissionPager {
entries: [Submission!]!
limit: Int!
start: Int!
total: Int!
}
type SubmissionProgress {
created: DateTime!
id: ID!
lastModified: DateTime
percentageComplete: Float!
timeElapsed: Float!
}
type SubmissionStatistic {
total: Int!
}
type User {
created: DateTime!
email: String!
firstName: String
id: ID!
language: String!
lastModified: DateTime
lastName: String
roles: [String!]!
username: String!
verifiedEmail: Boolean!
}
type UserPager {
entries: [User!]!
limit: Int!
start: Int!
total: Int!
}
type UserStatistic {
total: Int!
}
type Version {
version: String!
}
input ButtonInput {
action: String
activeColor: String
bgColor: String
color: String
id: ID
text: String
url: String
}
input ColorsInput {
answer: String!
background: String!
button: String!
buttonActive: String!
buttonText: String!
question: String!
}
input DesignInput {
colors: ColorsInput!
font: String
layout: String
}
input DeviceInput {
language: String
name: String!
type: String!
}
input FormCreateInput {
isLive: Boolean
language: String!
layout: String
showFooter: Boolean
title: String!
}
input FormFieldInput {
description: String!
disabled: Boolean
id: ID!
logic: [FormFieldLogicInput!]
options: [FormFieldOptionInput!]
rating: FormFieldRatingInput
required: Boolean!
slug: String
title: String!
type: String!
value: String!
}
input FormFieldLogicInput {
action: String
disable: Boolean
enabled: Boolean
formula: String
id: ID
jumpTo: ID
require: Boolean
visible: Boolean
}
input FormFieldOptionInput {
id: ID
key: String
title: String
value: String!
}
input FormFieldRatingInput {
shape: String
steps: Int
}
input FormHookInput {
enabled: Boolean!
format: String
id: ID!
url: String
}
input FormNotificationInput {
enabled: Boolean!
fromEmail: String
fromField: String
htmlTemplate: String
id: ID
subject: String
toEmail: String
toField: String
}
input FormUpdateInput {
design: DesignInput
endPage: PageInput
fields: [FormFieldInput!]
hooks: [FormHookInput!]
id: ID!
isLive: Boolean
language: String
notifications: [FormNotificationInput!]
showFooter: Boolean
startPage: PageInput
title: String
}
input PageInput {
buttonText: String
buttons: [ButtonInput!]!
id: ID
paragraph: String
show: Boolean!
title: String
}
input ProfileUpdateInput {
email: String
firstName: String
id: ID!
language: String
lastName: String
password: String
username: String
}
input SubmissionSetFieldInput {
data: String!
field: ID!
token: String!
}
input SubmissionStartInput {
device: DeviceInput!
token: String!
}
input UserCreateInput {
email: String!
firstName: String
language: String
lastName: String
password: String!
username: String!
}
input UserUpdateInput {
email: String
firstName: String
id: ID!
language: String
lastName: String
password: String
roles: [String!]
username: String
}
"A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format."
scalar DateTime

View File

@ -1,5 +1,5 @@
import { createWrapper, HYDRATE, MakeStore } from 'next-redux-wrapper' import { createWrapper, HYDRATE } from 'next-redux-wrapper'
import { AnyAction, applyMiddleware, combineReducers, createStore } from 'redux' import { AnyAction, applyMiddleware, combineReducers, createStore, Store } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension' import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk' import thunkMiddleware from 'redux-thunk'
import { auth, AuthState } from './auth' import { auth, AuthState } from './auth'
@ -21,8 +21,8 @@ const root = (state: State, action: AnyAction): State => {
return combined(state, action) return combined(state, action)
} }
const makeStore: MakeStore<State> = () => { const makeStore = () => {
return createStore(root, undefined, composeWithDevTools(applyMiddleware(thunkMiddleware))) return createStore(root, undefined, composeWithDevTools(applyMiddleware(thunkMiddleware)))
} }
export const wrapper = createWrapper<State>(makeStore, { debug: false }) export const wrapper = createWrapper<Store<State>>(makeStore, { debug: false })

View File

@ -17,7 +17,8 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve" "jsx": "preserve",
"incremental": true
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",

3272
yarn.lock

File diff suppressed because it is too large Load Diff