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,
project: ['./tsconfig.json'],
},
plugins: [
'@typescript-eslint/eslint-plugin',
'@typescript-eslint',
'unused-imports'
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
],
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',
'@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: {
react: {

1
.gitignore vendored
View File

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

View File

@ -16,12 +16,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- new slider field type
- new card layout for forms
- field logic
- add enviroment config
### Changed
- combined notificationts to become more versatile
- use exported hooks for graphql
- disable swipe gesture
- upgrade to nextjs 12
### Fixed

View File

@ -30,7 +30,8 @@ WORKDIR /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
USER ohmyform

View File

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

View File

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

View File

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

View File

@ -108,7 +108,9 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
})
}
setLoading(false)
}, [form, getSubmissions, props.form, setLoading, loading])
}, [
form, getSubmissions, props.form, setLoading, 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 {
Button,
Card,
Checkbox,
Form,
Input,
Popconfirm,
Popover,
Space,
Tag,
Tooltip,
} from 'antd'
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface'
import React, { useCallback, useEffect, useState } from 'react'
@ -26,22 +16,40 @@ interface Props {
onChangeFields: (fields: FormFieldFragment[]) => void
field: FieldData
remove: (index: number) => void
move: (from: number, to: number) => void
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 { 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 [shouldUpdate, setShouldUpdate] = useState(false)
const [nextTitle, setNextTitle] = useState<string>(
form.getFieldValue(['form', 'fields', field.name as string, 'title'])
form.getFieldValue([
'form', 'fields', field.name as string, 'title',
])
)
useEffect(() => {
if (!shouldUpdate) {
return
}
const id = setTimeout(() => {
setShouldUpdate(false)
onChangeFields(
fields.map((field, i) => {
if (i === index) {
@ -57,7 +65,9 @@ export const FieldCard: React.FC<Props> = (props) => {
}, 500)
return () => clearTimeout(id)
}, [nextTitle])
}, [
nextTitle, shouldUpdate, fields,
])
const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => {
return (
@ -94,14 +104,32 @@ export const FieldCard: React.FC<Props> = (props) => {
return (
<Card
title={<Tooltip title={`@${field.name as string}`}>nextTitle</Tooltip>}
title={nextTitle}
type={'inner'}
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>
{() => {
// 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) {
return null
@ -145,7 +173,7 @@ export const FieldCard: React.FC<Props> = (props) => {
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
</Space>
}
>
<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' }]}
labelCol={{ span: 6 }}
>
<Input onChange={(e) => setNextTitle(e.target.value)} />
<Input
onChange={(e) => {
setNextTitle(e.target.value)
setShouldUpdate(true)
}}
/>
</Form.Item>
<Form.Item
label={t('type:description')}
@ -181,7 +214,7 @@ export const FieldCard: React.FC<Props> = (props) => {
<Form.List name={[field.name as string, 'logic']}>
{(logic, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => {
const addAndMove = (index: number) => (defaults) => {
add(defaults)
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 { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs'
import debug from 'debug'
import { FieldData } from 'rc-field-form/lib/interface'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -9,6 +10,8 @@ import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { FieldCard } from './field.card'
import { adminTypes } from './types'
const logger = debug('FieldsTab')
interface Props extends TabPaneProps {
form: FormInstance
fields: FormFieldFragment[]
@ -20,13 +23,25 @@ export const FieldsTab: React.FC<Props> = (props) => {
const [nextType, setNextType] = useState('textfield')
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 (
<FieldCard
form={props.form}
field={field}
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}
onChangeFields={props.onChangeFields}
/>
@ -85,7 +100,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
<Tabs.TabPane {...props}>
<Form.List name={['form', 'fields']}>
{(fields, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => {
const addAndMove = (index: number) => (defaults) => {
add(defaults)
move(fields.length, index)
}
@ -96,7 +111,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }}>
{renderType(field, index, remove)}
{renderType(field, index, remove, move)}
</Form.Item>
{addField(addAndMove(index + 1), index + 1)}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,12 @@
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 styled from 'styled-components'
import { FormPublicFieldFragment } from '../../../../graphql/fragment/form.public.fragment'
import { Omf } from '../../../omf'
import { StyledButton } from '../../../styled/button'
import { darken, lighten } from '../../../styled/color.change'
import { useMath } from '../../../use.math'
import { LayoutProps } from '../layout.props'
import { Field } from './field'
import { Page } from './page'
@ -12,7 +14,7 @@ import { Page } from './page'
type Step = 'start' | 'form' | 'end'
const MyCard = styled.div<{ background: string }>`
background: ${(props) => darken(props.background, 10)};
background: ${(props) => darken(0.1, props.background)};
height: 100%;
min-height: 100vh;
@ -20,7 +22,7 @@ const MyCard = styled.div<{ background: string }>`
.ant-card {
background: ${(props) => props.background};
border-color: ${(props) => lighten(props.background, 40)};
border-color: ${(props) => lighten(0.4, props.background)};
width: 800px;
margin: auto;
max-width: 90%;
@ -32,6 +34,8 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
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 { setField } = props.submission
@ -67,6 +71,31 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
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 = () => {
switch (step) {
case 'start':
@ -75,12 +104,32 @@ export const CardLayout: React.FC<LayoutProps> = (props) => {
case 'form':
return (
<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) => {
if (field.type === 'hidden') {
return null
}
if (!isVisible(field)) {
return null
}
return <Field key={field.id} field={field} design={design} focus={i === 0} />
})}
<div

View File

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

View File

@ -62,6 +62,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
}}
>
<div
@ -77,7 +78,7 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
{field.title}
</StyledH1>
{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()} />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Button } from 'antd'
import { ButtonProps } from 'antd/lib/button/button'
import { darken, lighten } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { darken, lighten } from './color.change'
interface Props extends ButtonProps {
background: string
@ -14,12 +14,12 @@ interface Props extends ButtonProps {
const Styled = styled(Button)`
background: ${(props: Props) => props.background};
color: ${(props: Props) => props.color};
border-color: ${(props: Props) => darken(props.background, 10)};
border-color: ${(props: Props) => darken(0.1, props.background)};
:hover {
color: ${(props: Props) => props.highlight};
background-color: ${(props: Props) => lighten(props.background, 10)};
border-color: ${(props: Props) => darken(props.highlight, 10)};
background-color: ${(props: Props) => lighten(0.1, props.background)};
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 { PickerProps } from 'antd/lib/date-picker/generatePicker'
import { Moment } from 'moment'
import { transparentize } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
type Props = { design: FormPublicDesignFragment } & PickerProps<Moment>
@ -36,7 +36,7 @@ const Field = styled(DatePicker)`
color: ${(props: Props) => props.design.colors.answer};
::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 { InputProps } from 'antd/lib/input/Input'
import { transparentize } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends InputProps {
design: FormPublicDesignFragment
@ -37,7 +37,7 @@ const Field = styled(Input)`
color: ${(props: Props) => props.design.colors.answer};
::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 ReactMarkdown, { ReactMarkdownProps } from 'react-markdown'
import ReactMarkdown from 'react-markdown'
import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { lighten } from './color.change'
interface Props extends ReactMarkdownProps {
interface Props extends ReactMarkdownOptions {
type: 'question' | 'answer'
design: FormPublicDesignFragment
}
@ -29,7 +30,7 @@ const Markdown = styled(ReactMarkdown)`
}
blockquote {
color: ${(props: Props) => lighten(getColor(props), 50)};
color: ${(props: Props) => lighten(0.5, getColor(props))};
padding-left: 20px;
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 }) => {
return (
<Markdown escapeHtml={false} {...props}>
<Markdown {...props}>
{children}
</Markdown>
)

View File

@ -1,9 +1,9 @@
import { InputNumber } from 'antd'
import { InputNumberProps } from 'antd/lib/input-number'
import { transparentize } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends InputNumberProps {
design: FormPublicDesignFragment
@ -38,7 +38,7 @@ const Field = styled(InputNumber)`
color: ${(props: Props) => props.design.colors.answer};
::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 { SelectProps } from 'antd/lib/select'
import { transparentize } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends SelectProps<string> {
design: FormPublicDesignFragment
@ -36,7 +36,7 @@ const Field = styled(Select)`
color: ${(props: Props) => props.design.colors.answer};
::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 { TextAreaProps } from 'antd/lib/input/TextArea'
import { transparentize } from 'polished'
import React from 'react'
import styled from 'styled-components'
import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change'
interface Props extends TextAreaProps {
design: FormPublicDesignFragment
@ -35,7 +35,7 @@ const Field = styled(Input.TextArea)`
color: ${(props: Props) => props.design.colors.answer};
::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) => {
const { refetch } = useQuery(query, { skip: true })
const cb = useCallback(
return useCallback(
(variables) => {
return refetch(variables)
},
[refetch]
)
return cb
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,11 @@ export interface SubmissionSetFieldMutationVariables {
field: {
token: string
field: string
data: string
}
}
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) {
id
percentageComplete

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
next-env.d.ts vendored
View File

@ -1,2 +1,5 @@
/// <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 environment = process.env.NODE_ENV ? process.env.NODE_ENV : 'dev';
const version = p.version;
module.exports = withImages({
module.exports = {
poweredByHeader: true,
future: {
webpack5: true,
},
productionBrowserSourceMaps: true,
publicRuntimeConfig: {
environment,
endpoint: process.env.ENDPOINT || '/graphql',
spa: !!process.env.SPA || false,
mainBackground: process.env.MAIN_BACKGROUND || '#8FA2A6'
@ -19,4 +18,4 @@ module.exports = withImages({
env: {
version,
}
})
}

View File

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

View File

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

View File

@ -2,15 +2,26 @@ import { ApolloProvider } from '@apollo/client'
import 'antd/dist/antd.css'
import 'assets/global.scss'
import 'assets/variables.scss'
import debug from 'debug'
import 'i18n'
import App, { AppInitialProps } from 'next/app'
import { AppType } from 'next/dist/next-server/lib/utils'
import getConfig from 'next/config'
import { AppInitialProps, AppType } from 'next/dist/shared/lib/utils'
import Head from 'next/head'
import React from 'react'
import React, { useEffect } from 'react'
import { wrapper } from 'store'
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 (
<ApolloProvider client={getClient()}>
<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 {
form: {
...next.form,
fields: next.form.fields.map((field) => {
const keys: FormFieldOptionKeysFragment = {}
fields: next.form.fields
.map((field) => {
const keys: FormFieldOptionKeysFragment = {}
field.options.forEach((option) => {
if (option.key) {
keys[option.key] = option.value
field.options.forEach((option) => {
if (option.key) {
keys[option.key] = option.value
}
})
return {
...field,
options: field.options.filter((option) => !option.key),
optionKeys: keys,
}
})
return {
...field,
options: field.options.filter((option) => !option.key),
optionKeys: keys,
}
}),
.sort((a, b) => a.idx - b.idx),
},
}
}
@ -69,7 +71,7 @@ const Index: NextPage = () => {
formData.form.fields = formData.form.fields
.filter((e) => e && e.type)
.map(({ optionKeys, ...field }) => {
.map(({ optionKeys, ...field }, index) => {
const options = field.options
if (optionKeys) {
@ -89,6 +91,7 @@ const Index: NextPage = () => {
return {
...field,
options,
idx: index,
}
})

View File

@ -2,9 +2,11 @@ import { Alert, Layout } from 'antd'
import { AuthFooter } from 'components/auth/footer'
import { GetStaticProps, NextPage } from 'next'
import getConfig from 'next/config'
import Image from 'next/image'
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import LogoWhitePng from '../assets/images/logo_white.png'
import { LoadingPage } from '../components/loading.page'
import { Omf } from '../components/omf'
import { useStatusQuery } from '../graphql/query/status.query'
@ -24,7 +26,9 @@ const Index: NextPage = () => {
if (router.pathname !== window.location.pathname) {
let href = 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) => {
if (r.test(as)) {
@ -58,16 +62,22 @@ const Index: NextPage = () => {
}}
>
<Omf />
<img
alt={'OhMyForm'}
<div
style={{
margin: 'auto',
maxWidth: '90%',
width: 500,
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 && (
<Alert

View File

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

View File

@ -5,10 +5,12 @@ import { AuthLayout } from 'components/auth/layout'
import { setAuth } from 'components/with.auth'
import { RegisterUserData, useRegisterMutation } from 'graphql/mutation/register.mutation'
import { NextPage } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import LogoWhitePng from '../assets/images/logo_white.png'
import { ErrorPage } from '../components/error.page'
import { Omf } from '../components/omf'
import { useSettingsQuery } from '../graphql/query/settings.query'
@ -66,9 +68,7 @@ const Register: NextPage = () => {
width: 400,
}}
>
<img
src={require('../assets/images/logo_white_small.png') as string}
alt={'OhMyForm'}
<div
style={{
display: 'block',
width: '70%',
@ -76,7 +76,14 @@ const Register: NextPage = () => {
marginRight: 'auto',
marginBottom: 16,
}}
/>
>
<Image
src={LogoWhitePng.src}
alt={'OhMyForm'}
width={1608 / 4}
height={530 / 4}
/>
</div>
<Form.Item
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 { AnyAction, applyMiddleware, combineReducers, createStore } from 'redux'
import { createWrapper, HYDRATE } from 'next-redux-wrapper'
import { AnyAction, applyMiddleware, combineReducers, createStore, Store } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import { auth, AuthState } from './auth'
@ -21,8 +21,8 @@ const root = (state: State, action: AnyAction): State => {
return combined(state, action)
}
const makeStore: MakeStore<State> = () => {
const makeStore = () => {
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",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
"jsx": "preserve",
"incremental": true
},
"exclude": [
"node_modules",

3272
yarn.lock

File diff suppressed because it is too large Load Diff