- ability to change user passwords

- add default page background
- add environment list in [doc](doc/environment.md)
- combined notificationts to become more versatile
- use exported hooks for graphql
- links at the bottom for new users
- fixes for hide contrib setting
- upgrad all packages
This commit is contained in:
Michael Schramm 2021-05-02 12:38:19 +02:00
parent dfbad77d7e
commit 8713c0a8c6
94 changed files with 3384 additions and 5662 deletions

1
.gitignore vendored
View File

@ -26,6 +26,7 @@ yarn-error.log*
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.env
# development environments # development environments
/.idea /.idea

View File

@ -9,12 +9,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added ### Added
- ability to change user passwords
- add default page background
- add environment list in [doc](doc/environment.md)
### Changed ### Changed
- combined notificationts to become more versatile
- use exported hooks for graphql
### Fixed ### Fixed
- links at the bottom for new users
- fixes for hide contrib setting
### Security ### Security
- upgrad all packages
## [0.9.9] - 2021-02-14 ## [0.9.9] - 2021-02-14
### Added ### Added

View File

@ -1,5 +1,7 @@
.footer { .footer {
position: absolute; position: absolute;
padding-left: 16px;
margin-bottom: 4px;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;

View File

@ -1,26 +1,30 @@
import { useQuery } from '@apollo/react-hooks'
import { Button, Select } from 'antd' import { Button, Select } from 'antd'
import getConfig from 'next/config'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React from 'react' import React 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 { SETTINGS_QUERY, SettingsQueryData } from '../../graphql/query/settings.query' import { useSettingsQuery } from '../../graphql/query/settings.query'
import { languages } from '../../i18n' import { languages } from '../../i18n'
import { NextConfigType } from '../../next.config.type'
import { clearAuth, withAuth } from '../with.auth' import { clearAuth, withAuth } from '../with.auth'
import scss from './footer.module.scss' import scss from './footer.module.scss'
const { publicRuntimeConfig } = getConfig() as NextConfigType
interface Props { interface Props {
me?: { me?: {
id: string id: string
username: string username: string
roles: string[]
} }
} }
const AuthFooterInner: React.FC<Props> = (props) => { const AuthFooterInner: React.FC<Props> = (props) => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const router = useRouter() const router = useRouter()
const { data } = useQuery<SettingsQueryData>(SETTINGS_QUERY) const { data, loading } = useSettingsQuery()
const logout = () => { const logout = () => {
clearAuth() clearAuth()
@ -29,7 +33,13 @@ const AuthFooterInner: React.FC<Props> = (props) => {
return ( return (
<footer className={scss.footer}> <footer className={scss.footer}>
<Link href={'/admin'}> {props.me
? [
<span style={{ color: '#FFF' }} key={'user'}>
Hi, {props.me.username}
</span>,
props.me.roles.includes('admin') && (
<Link key={'admin'} href={'/admin'}>
<Button <Button
type={'link'} type={'link'}
ghost ghost
@ -40,11 +50,18 @@ const AuthFooterInner: React.FC<Props> = (props) => {
{t('admin')} {t('admin')}
</Button> </Button>
</Link> </Link>
{props.me ),
? [ <Link key={'profile'} href={'/admin/profile'}>
<span style={{ color: '#FFF' }} key={'user'}> <Button
Hi, {props.me.username} type={'link'}
</span>, ghost
style={{
color: '#FFF',
}}
>
{t('profile')}
</Button>
</Link>,
<Button <Button
key={'logout'} key={'logout'}
type={'link'} type={'link'}
@ -69,18 +86,19 @@ const AuthFooterInner: React.FC<Props> = (props) => {
{t('login')} {t('login')}
</Button> </Button>
</Link>, </Link>,
!loading && !data?.disabledSignUp.value && (
<Link href={'/register'} key={'register'}> <Link href={'/register'} key={'register'}>
<Button <Button
type={'link'} type={'link'}
ghost ghost
disabled={data ? data.disabledSignUp.value : false}
style={{ style={{
color: '#FFF', color: '#FFF',
}} }}
> >
{t('register')} {t('register')}
</Button> </Button>
</Link>, </Link>
),
]} ]}
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<Select <Select
@ -99,6 +117,8 @@ const AuthFooterInner: React.FC<Props> = (props) => {
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
{!loading && !data?.hideContrib.value && (
<>
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" /> <GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
<Button <Button
type={'link'} type={'link'}
@ -124,6 +144,8 @@ const AuthFooterInner: React.FC<Props> = (props) => {
> >
translated with Lokalize translated with Lokalize
</Button> </Button>
</>
)}
</footer> </footer>
) )
} }

View File

@ -1,5 +1,9 @@
import { Layout, Spin } from 'antd' import { Layout, Spin } from 'antd'
import getConfig from 'next/config'
import React from 'react' import React from 'react'
import { NextConfigType } from '../../next.config.type'
const { publicRuntimeConfig } = getConfig() as NextConfigType
interface Props { interface Props {
loading?: boolean loading?: boolean
@ -11,7 +15,7 @@ export const AuthLayout: React.FC<Props> = (props) => {
<Layout <Layout
style={{ style={{
height: '100vh', height: '100vh',
background: '#437fdc', background: publicRuntimeConfig.mainBackground,
}} }}
> >
{props.children} {props.children}

View File

@ -13,17 +13,10 @@ export const DesignTab: React.FC<TabPaneProps> = (props) => {
<Input /> <Input />
</Form.Item> </Form.Item>
{[ {['background', 'question', 'answer', 'button', 'buttonActive', 'buttonText'].map((name) => (
'backgroundColor',
'questionColor',
'answerColor',
'buttonColor',
'buttonActiveColor',
'buttonTextColor',
].map((name) => (
<Form.Item <Form.Item
key={name} key={name}
label={t(`form:design.${name}`)} label={t(`form:design.color.${name}`)}
name={['form', 'design', 'colors', name]} name={['form', 'design', 'colors', name]}
> >
<InputColor /> <InputColor />

View File

@ -1,19 +1,9 @@
import { useQuery } from '@apollo/react-hooks'
import { message } from 'antd' import { message } from 'antd'
import { useCallback, useState } from 'react'
import ExcelJS from 'exceljs' import ExcelJS from 'exceljs'
import { import { useCallback, useState } from 'react'
ADMIN_FORM_QUERY, import { SubmissionFragment } from '../../../graphql/fragment/submission.fragment'
AdminFormQueryData, import { useFormQuery } from '../../../graphql/query/form.query'
AdminFormQueryVariables, import { useSubmissionPagerImperativeQuery } from '../../../graphql/query/submission.pager.query'
} from '../../../graphql/query/admin.form.query'
import {
ADMIN_PAGER_SUBMISSION_QUERY,
AdminPagerSubmissionEntryQueryData,
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables,
} from '../../../graphql/query/admin.pager.submission.query'
import { useImperativeQuery } from '../../use.imerative.query'
interface Props { interface Props {
form: string form: string
@ -23,16 +13,13 @@ interface Props {
export const ExportSubmissionAction: React.FC<Props> = (props) => { export const ExportSubmissionAction: React.FC<Props> = (props) => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const form = useQuery<AdminFormQueryData, AdminFormQueryVariables>(ADMIN_FORM_QUERY, { const form = useFormQuery({
variables: { variables: {
id: props.form, id: props.form,
}, },
}) })
const getSubmissions = useImperativeQuery< const getSubmissions = useSubmissionPagerImperativeQuery()
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables
>(ADMIN_PAGER_SUBMISSION_QUERY)
const exportSubmissions = useCallback(async () => { const exportSubmissions = useCallback(async () => {
if (loading) { if (loading) {
@ -66,7 +53,7 @@ export const ExportSubmissionAction: React.FC<Props> = (props) => {
start: 0, start: 0,
}) })
const buildRow = (data: AdminPagerSubmissionEntryQueryData): any[] => { const buildRow = (data: SubmissionFragment): any[] => {
const row = [ const row = [
data.id, data.id,
data.created, data.created,

View File

@ -4,14 +4,14 @@ import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface' import { FieldData } from 'rc-field-form/lib/interface'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment' import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
import { adminTypes } from './types' import { adminTypes } from './types'
import { TextType } from './types/text.type' import { TextType } from './types/text.type'
interface Props { interface Props {
form: FormInstance form: FormInstance
fields: AdminFormFieldFragment[] fields: FormFieldFragment[]
onChangeFields: (fields: AdminFormFieldFragment[]) => void onChangeFields: (fields: FormFieldFragment[]) => void
field: FieldData field: FieldData
remove: (index: number) => void remove: (index: number) => void
index: number index: number

View File

@ -5,14 +5,14 @@ import { TabPaneProps } from 'antd/lib/tabs'
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'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment' 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'
interface Props extends TabPaneProps { interface Props extends TabPaneProps {
form: FormInstance form: FormInstance
fields: AdminFormFieldFragment[] fields: FormFieldFragment[]
onChangeFields: (fields: AdminFormFieldFragment[]) => void onChangeFields: (fields: FormFieldFragment[]) => void
} }
export const FieldsTab: React.FC<Props> = (props) => { export const FieldsTab: React.FC<Props> = (props) => {
@ -55,10 +55,8 @@ export const FieldsTab: React.FC<Props> = (props) => {
<Button <Button
type="dashed" type="dashed"
onClick={() => { onClick={() => {
const defaults: AdminFormFieldFragment = { const defaults: FormFieldFragment = {
logicJump: { logic: [],
enabled: false,
},
options: [], options: [],
id: `NEW-${Date.now()}`, id: `NEW-${Date.now()}`,
type: nextType, type: nextType,

View File

@ -0,0 +1,231 @@
import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons/lib'
import { Button, Card, Form, Input, Popconfirm, Select, Switch } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { FieldData } from 'rc-field-form/lib/interface'
import React, { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { FormFieldFragment } from '../../../graphql/fragment/form.fragment'
interface Props {
form: FormInstance
field: FieldData
groups: {
[key: string]: FormFieldFragment[]
}
remove: (index: number) => void
index: number
}
export const NotificationCard: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { form, field, remove, index, groups } = props
const [enabled, setEnabled] = useState<boolean>()
return (
<Card
title={'Notification'}
type={'inner'}
extra={
<div>
<Popconfirm
placement={'left'}
title={t('type:confirmDelete')}
okText={t('type:deleteNow')}
okButtonProps={{ danger: true }}
onConfirm={() => {
remove(index)
}}
>
<Button danger>
<DeleteOutlined />
</Button>
</Popconfirm>
</div>
}
actions={[<DeleteOutlined key={'delete'} onClick={() => remove(index)} />]}
>
<Form.Item
label={t('form:notifications.enabled')}
name={[field.name as string, 'enabled']}
valuePropName={'checked'}
labelCol={{ span: 6 }}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.subject')}
name={[field.name as string, 'subject']}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled'])
),
message: t('validation:subjectRequired'),
},
]}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.htmlTemplate')}
name={[field.name as string, 'htmlTemplate']}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled'])
),
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:notifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
labelCol={{ span: 6 }}
>
<Input.TextArea autoSize />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.fromField')}
name={[field.name as string, 'fromField']}
extra={t('form:notifications.fromFieldInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) &&
!form.getFieldValue([
'form',
'notifications',
field.name as string,
'fromEmail',
])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((element) => (
<Select.Option value={element.id} key={element.id}>
{element.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.fromEmail')}
name={[field.name as string, 'fromEmail']}
extra={t('form:notifications.fromEmailInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) &&
!form.getFieldValue([
'form',
'notifications',
field.name as string,
'fromField',
])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Input />
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.toField')}
name={[field.name as string, 'toField']}
extra={t('form:notifications.toFieldInfo')}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) &&
!form.getFieldValue(['form', 'notifications', field.name as string, 'toEmail'])
),
message: t('validation:emailFieldRequired'),
},
]}
labelCol={{ span: 6 }}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((field) => (
<Select.Option value={field.id} key={field.id}>
{field.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
)}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => (
<Form.Item
label={t('form:notifications.toEmail')}
name={[field.name as string, 'toEmail']}
extra={t('form:notifications.toEmailInfo')}
labelCol={{ span: 6 }}
rules={[
{
required: Boolean(
form.getFieldValue(['form', 'notifications', field.name as string, 'enabled']) &&
!form.getFieldValue(['form', 'notifications', field.name as string, 'toField'])
),
message: t('validation:emailFieldRequired'),
},
]}
>
<Input />
</Form.Item>
)}
</Form.Item>
</Card>
)
}

View File

@ -0,0 +1,93 @@
import { PlusOutlined } from '@ant-design/icons/lib'
import { Button, Form, Space, Tabs } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
FormFieldFragment,
FormNotificationFragment,
} from '../../../graphql/fragment/form.fragment'
import { NotificationCard } from './notification.card'
interface Props extends TabPaneProps {
form: FormInstance
fields: FormFieldFragment[]
}
export const NotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const groups: {
[key: string]: FormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
const addField = useCallback(
(add: (defaults: unknown) => void, index: number) => {
return (
<Form.Item wrapperCol={{ span: 24 }}>
<Space
style={{
width: '100%',
justifyContent: 'flex-end',
}}
>
<Button
type="dashed"
onClick={() => {
const defaults: FormNotificationFragment = {
id: Math.random().toString(),
enabled: false,
}
add(defaults)
}}
>
<PlusOutlined /> {t('form:notifications.add')}
</Button>
</Space>
</Form.Item>
)
},
[props.fields]
)
return (
<Tabs.TabPane {...props}>
<Form.List name={['form', 'notifications']}>
{(fields, { add, remove, move }) => {
const addAndMove = (index) => (defaults) => {
add(defaults)
move(fields.length, index)
}
return (
<div>
{addField(addAndMove(0), 0)}
{fields.map((field, index) => (
<div key={field.key}>
<Form.Item wrapperCol={{ span: 24 }}>
<NotificationCard
form={props.form}
field={field}
index={index}
remove={remove}
groups={groups}
/>
</Form.Item>
{addField(addAndMove(index + 1), index + 1)}
</div>
))}
</div>
)
}}
</Form.List>
</Tabs.TabPane>
)
}

View File

@ -1,131 +0,0 @@
import { InfoCircleOutlined } from '@ant-design/icons/lib'
import { Form, Input, Select, Switch, Tabs } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const RespondentNotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled']) as boolean
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
useEffect(() => {
props.form
.validateFields([
['form', 'respondentNotifications', 'subject'],
['form', 'respondentNotifications', 'htmlTemplate'],
['form', 'respondentNotifications', 'toField'],
])
.catch((e: Error) => console.error('failed to validate fields', e))
}, [enabled])
const groups: {
[key: string]: AdminFormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:respondentNotifications.enabled')}
name={['form', 'respondentNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.subject')}
name={['form', 'respondentNotifications', 'subject']}
rules={[
{
required: enabled,
message: t('validation:subjectRequired'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.htmlTemplate')}
name={['form', 'respondentNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:respondentNotifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.toField')}
name={['form', 'respondentNotifications', 'toField']}
extra={t('form:respondentNotifications.toFieldInfo')}
rules={[
{
required: enabled,
message: t('validation:emailFieldRequired'),
},
]}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((field) => (
<Select.Option value={field.id} key={field.id}>
{field.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
<Form.Item
label={t('form:respondentNotifications.fromEmail')}
name={['form', 'respondentNotifications', 'fromEmail']}
extra={t('form:respondentNotifications.fromEmailInfo')}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -1,123 +0,0 @@
import { InfoCircleOutlined } from '@ant-design/icons/lib'
import { Form, Input, Select, Switch, Tabs } from 'antd'
import { FormInstance } from 'antd/lib/form'
import { TabPaneProps } from 'antd/lib/tabs'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
interface Props extends TabPaneProps {
form: FormInstance
fields: AdminFormFieldFragment[]
}
export const SelfNotificationsTab: React.FC<Props> = (props) => {
const { t } = useTranslation()
const [enabled, setEnabled] = useState<boolean>()
useEffect(() => {
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled']) as boolean
if (next !== enabled) {
setEnabled(next)
}
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
useEffect(() => {
props.form
.validateFields([
['form', 'selfNotifications', 'subject'],
['form', 'selfNotifications', 'htmlTemplate'],
])
.catch((e: Error) => console.error('failed to validate', e))
}, [enabled])
const groups: {
[key: string]: AdminFormFieldFragment[]
} = {}
props.fields.forEach((field) => {
if (!groups[field.type]) {
groups[field.type] = []
}
groups[field.type].push(field)
})
return (
<Tabs.TabPane {...props}>
<Form.Item
label={t('form:selfNotifications.enabled')}
name={['form', 'selfNotifications', 'enabled']}
valuePropName={'checked'}
>
<Switch onChange={(e) => setEnabled(e.valueOf())} />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.subject')}
name={['form', 'selfNotifications', 'subject']}
rules={[
{
required: enabled,
message: t('validation:subjectRequired'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.htmlTemplate')}
name={['form', 'selfNotifications', 'htmlTemplate']}
rules={[
{
required: enabled,
message: t('validation:templateRequired'),
},
]}
extra={
<div>
<Trans>form:selfNotifications.htmlTemplateInfo</Trans>
<a
href={'https://mjml.io/try-it-live'}
target={'_blank'}
rel={'noreferrer'}
style={{
marginLeft: 16,
}}
>
<InfoCircleOutlined />
</a>
</div>
}
>
<Input.TextArea autoSize />
</Form.Item>
<Form.Item
label={t('form:selfNotifications.fromField')}
name={['form', 'selfNotifications', 'fromField']}
extra={t('form:selfNotifications.fromFieldInfo')}
>
<Select>
{Object.keys(groups).map((key) => (
<Select.OptGroup label={key.toUpperCase()} key={key}>
{groups[key].map((field) => (
<Select.Option value={field.id} key={field.id}>
{field.title}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</Form.Item>
<Form.Item
label={t('form:selfNotifications.toEmail')}
name={['form', 'selfNotifications', 'toEmail']}
extra={t('form:selfNotifications.toEmailInfo')}
>
<Input />
</Form.Item>
</Tabs.TabPane>
)
}

View File

@ -2,24 +2,24 @@ import { Descriptions, Table } from 'antd'
import { ColumnsType } from 'antd/lib/table/interface' import { ColumnsType } from 'antd/lib/table/interface'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FormPagerFragment } from '../../../graphql/fragment/form.pager.fragment'
import { import {
AdminPagerSubmissionEntryFieldQueryData, SubmissionFragment,
AdminPagerSubmissionEntryQueryData, SubmissionFragmentField,
AdminPagerSubmissionFormQueryData, } from '../../../graphql/fragment/submission.fragment'
} from '../../../graphql/query/admin.pager.submission.query'
interface Props { interface Props {
form: AdminPagerSubmissionFormQueryData form: FormPagerFragment
submission: AdminPagerSubmissionEntryQueryData submission: SubmissionFragment
} }
export const SubmissionValues: React.FC<Props> = (props) => { export const SubmissionValues: React.FC<Props> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [ const columns: ColumnsType<SubmissionFragmentField> = [
{ {
title: t('submission:field'), title: t('submission:field'),
render(row: AdminPagerSubmissionEntryFieldQueryData) { render(_, row) {
if (row.field) { if (row.field) {
return `${row.field.title}${row.field.required ? '*' : ''}` return `${row.field.title}${row.field.required ? '*' : ''}`
} }
@ -29,10 +29,12 @@ export const SubmissionValues: React.FC<Props> = (props) => {
}, },
{ {
title: t('submission:value'), title: t('submission:value'),
render(row: AdminPagerSubmissionEntryFieldQueryData) { render(_, row) {
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

@ -6,7 +6,6 @@ import { AdminFieldTypeProps } from './type.props'
export const RatingType: React.FC<AdminFieldTypeProps> = (props) => { export const RatingType: React.FC<AdminFieldTypeProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
// TODO add ratings
return ( return (
<div> <div>
<Form.Item <Form.Item

View File

@ -6,7 +6,6 @@ import { AdminFieldTypeProps } from './type.props'
export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => { export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
// TODO add switch
return ( return (
<div> <div>
<Form.Item <Form.Item

View File

@ -1,7 +1,10 @@
import { Form, message } from 'antd' import { Form, message } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import React from 'react' import React from 'react'
import { FormDesignFragment, FormFieldFragment } from '../../graphql/fragment/form.fragment' import {
FormPublicDesignFragment,
FormPublicFieldFragment,
} from '../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../styled/button' import { StyledButton } from '../styled/button'
import { StyledH1 } from '../styled/h1' import { StyledH1 } from '../styled/h1'
import { StyledMarkdown } from '../styled/markdown' import { StyledMarkdown } from '../styled/markdown'
@ -11,8 +14,8 @@ import { TextType } from './types/text.type'
import { FieldTypeProps } from './types/type.props' import { FieldTypeProps } from './types/type.props'
interface Props { interface Props {
field: FormFieldFragment field: FormPublicFieldFragment
design: FormDesignFragment design: FormPublicDesignFragment
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
save: (data: any) => void save: (data: any) => void
@ -84,9 +87,9 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
}} }}
> >
<StyledButton <StyledButton
background={design.colors.buttonColor} background={design.colors.button}
color={design.colors.buttonTextColor} color={design.colors.buttonText}
highlight={design.colors.buttonActiveColor} highlight={design.colors.buttonActive}
onClick={prev} onClick={prev}
> >
{'Previous'} {'Previous'}
@ -95,9 +98,9 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<StyledButton <StyledButton
background={design.colors.buttonColor} background={design.colors.button}
color={design.colors.buttonTextColor} color={design.colors.buttonText}
highlight={design.colors.buttonActiveColor} highlight={design.colors.buttonActive}
size={'large'} size={'large'}
onClick={form.submit} onClick={form.submit}
> >

View File

@ -1,6 +1,9 @@
import { Space } from 'antd' import { Space } from 'antd'
import React from 'react' import React from 'react'
import { FormDesignFragment, FormPageFragment } from '../../graphql/fragment/form.fragment' import {
FormPublicDesignFragment,
FormPublicPageFragment,
} from '../../graphql/fragment/form.public.fragment'
import { StyledButton } from '../styled/button' import { StyledButton } from '../styled/button'
import { StyledH1 } from '../styled/h1' import { StyledH1 } from '../styled/h1'
import { StyledMarkdown } from '../styled/markdown' import { StyledMarkdown } from '../styled/markdown'
@ -8,8 +11,8 @@ import scss from './page.module.scss'
interface Props { interface Props {
type: 'start' | 'end' type: 'start' | 'end'
page: FormPageFragment page: FormPublicPageFragment
design: FormDesignFragment design: FormPublicDesignFragment
className?: string className?: string
next: () => void next: () => void
@ -59,9 +62,9 @@ export const FormPage: React.FC<Props> = ({ page, design, next, prev, className,
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<StyledButton <StyledButton
background={design.colors.buttonColor} background={design.colors.button}
color={design.colors.buttonTextColor} color={design.colors.buttonText}
highlight={design.colors.buttonActiveColor} highlight={design.colors.buttonActive}
size={'large'} size={'large'}
onClick={next} onClick={next}
> >

View File

@ -5,7 +5,6 @@ import { FieldTypeProps } from './type.props'
export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => { export const RatingType: React.FC<FieldTypeProps> = ({ field, urlValue }) => {
const { t } = useTranslation() const { t } = useTranslation()
// TODO add ratings
let initialValue: number = undefined let initialValue: number = undefined

View File

@ -1,7 +1,10 @@
import { FormDesignFragment, FormFieldFragment } from '../../../graphql/fragment/form.fragment' import {
FormPublicDesignFragment,
FormPublicFieldFragment,
} from '../../../graphql/fragment/form.public.fragment'
export interface FieldTypeProps { export interface FieldTypeProps {
field: FormFieldFragment field: FormPublicFieldFragment
design: FormDesignFragment design: FormPublicDesignFragment
urlValue?: string urlValue?: string
} }

View File

@ -1,10 +1,9 @@
import { useQuery } from '@apollo/react-hooks'
import React from 'react' import React from 'react'
import { SETTINGS_QUERY, SettingsQueryData } from '../graphql/query/settings.query' import { useSettingsQuery } from '../graphql/query/settings.query'
import scss from './omf.module.scss' import scss from './omf.module.scss'
export const Omf: React.FC = () => { export const Omf: React.FC = () => {
const { data, loading } = useQuery<SettingsQueryData>(SETTINGS_QUERY) const { data, loading } = useSettingsQuery()
if (loading || (data && data.hideContrib.value)) { if (loading || (data && data.hideContrib.value)) {
return null return null

View File

@ -1,15 +1,14 @@
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 { useQuery } from '@apollo/react-hooks' import { Alert, Dropdown, Layout, Menu, PageHeader, Select, Space, Spin, Tag } from 'antd'
import { Dropdown, Layout, Menu, PageHeader, Select, Space, Spin, Tag } from 'antd'
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 { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ME_QUERY, MeQueryData } 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'
import GitHubButton from 'react-github-button'
import { useWindowSize } from './use.window.size' import { useWindowSize } from './use.window.size'
import { clearAuth } from './with.auth' import { clearAuth } from './with.auth'
@ -33,9 +32,10 @@ interface Props {
title?: string title?: string
subTitle?: string subTitle?: string
extra?: JSX.Element[] extra?: JSX.Element[]
error?: string
} }
const Structure: FunctionComponent<Props> = (props) => { export const Structure: FunctionComponent<Props> = (props) => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const size = useWindowSize() const size = useWindowSize()
const [userMenu, setUserMenu] = React.useState(false) const [userMenu, setUserMenu] = React.useState(false)
@ -43,7 +43,7 @@ const Structure: FunctionComponent<Props> = (props) => {
const [selected, setSelected] = React.useState<string[]>() const [selected, setSelected] = React.useState<string[]>()
const [sidebar, setSidebar] = React.useState(size.width < 700) const [sidebar, setSidebar] = React.useState(size.width < 700)
const router = useRouter() const router = useRouter()
const user = useQuery<MeQueryData>(ME_QUERY) const user = useMeQuery()
React.useEffect(() => { React.useEffect(() => {
if (sidebar !== size.width < 700) { if (sidebar !== size.width < 700) {
@ -76,7 +76,7 @@ const Structure: FunctionComponent<Props> = (props) => {
return false return false
} }
return user.data.me.roles.includes(element.role) return user.data?.me.roles.includes(element.role)
}) })
.map( .map(
(element): JSX.Element => { (element): JSX.Element => {
@ -249,7 +249,9 @@ const Structure: FunctionComponent<Props> = (props) => {
</Menu.Item> </Menu.Item>
</Menu> </Menu>
</Sider> </Sider>
<Layout style={{ padding: '0 24px 24px' }}> <Layout
style={{ padding: '0 24px 24px', minHeight: 500, height: '100%', overflow: 'auto' }}
>
{props.title && ( {props.title && (
<PageHeader <PageHeader
title={props.title} title={props.title}
@ -285,6 +287,10 @@ const Structure: FunctionComponent<Props> = (props) => {
/> />
)} )}
{props.error && (
<Alert message={props.error} type={'error'} style={{ marginBottom: 24 }} />
)}
<Spin spinning={!!props.loading}> <Spin spinning={!!props.loading}>
<Content <Content
style={{ style={{

View File

@ -3,15 +3,15 @@ import { PickerProps } from 'antd/lib/date-picker/generatePicker'
import { Moment } from 'moment' import { Moment } from 'moment'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change' import { transparentize } from './color.change'
type Props = { design: FormDesignFragment } & PickerProps<Moment> type Props = { design: FormPublicDesignFragment } & PickerProps<Moment>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(DatePicker)` const Field = styled(DatePicker)`
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
background: none !important; background: none !important;
border-right: none; border-right: none;
border-top: none; border-top: none;
@ -21,7 +21,7 @@ const Field = styled(DatePicker)`
:hover, :hover,
:active { :active {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
&.ant-picker { &.ant-picker {
@ -33,15 +33,15 @@ const Field = styled(DatePicker)`
} }
input { input {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)}; color: ${(props: Props) => transparentize(props.design.colors.answer, 60)};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -1,17 +1,15 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
interface Props { interface Props {
type: 'question' | 'answer' type: 'question' | 'answer'
design: FormDesignFragment design: FormPublicDesignFragment
} }
const Header = styled.h1` const Header = styled.h1`
color: ${(props: Props) => color: ${(props: Props) =>
props.type === 'question' props.type === 'question' ? props.design.colors.question : props.design.colors.answer};
? props.design.colors.questionColor
: props.design.colors.answerColor};
` `
export const StyledH1: React.FC<Props> = ({ children, ...props }) => { export const StyledH1: React.FC<Props> = ({ children, ...props }) => {

View File

@ -1,16 +1,14 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
interface Props { interface Props {
type: 'question' | 'answer' type: 'question' | 'answer'
design: FormDesignFragment design: FormPublicDesignFragment
} }
const Header = styled.h2` const Header = styled.h2`
color: ${(props: Props) => color: ${(props: Props) =>
props.type === 'question' props.type === 'question' ? props.design.colors.question : props.design.colors.answer};
? props.design.colors.questionColor
: props.design.colors.answerColor};
` `
export const StyledH2: React.FC<Props> = ({ children, ...props }) => { export const StyledH2: React.FC<Props> = ({ children, ...props }) => {

View File

@ -2,17 +2,17 @@ import { Input } from 'antd'
import { InputProps } from 'antd/lib/input/Input' import { InputProps } from 'antd/lib/input/Input'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change' import { transparentize } from './color.change'
interface Props extends InputProps { interface Props extends InputProps {
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(Input)` const Field = styled(Input)`
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
background: none !important; background: none !important;
border-right: none; border-right: none;
border-top: none; border-top: none;
@ -20,12 +20,12 @@ const Field = styled(Input)`
border-radius: 0; border-radius: 0;
:focus { :focus {
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px; outline: ${(props: Props) => props.design.colors.answer} auto 5px;
} }
:hover, :hover,
:active { :active {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
&.ant-input-affix-wrapper { &.ant-input-affix-wrapper {
@ -34,15 +34,15 @@ const Field = styled(Input)`
input { input {
background: none !important; background: none !important;
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)}; color: ${(props: Props) => transparentize(props.design.colors.answer, 60)};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -1,16 +1,16 @@
import React from 'react' import React from 'react'
import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown' import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { lighten } from './color.change' import { lighten } from './color.change'
interface Props extends ReactMarkdownProps { interface Props extends ReactMarkdownProps {
type: 'question' | 'answer' type: 'question' | 'answer'
design: FormDesignFragment design: FormPublicDesignFragment
} }
const getColor = (props: Props) => const getColor = (props: Props) =>
props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor props.type === 'question' ? props.design.colors.question : props.design.colors.answer
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
const Markdown = styled(ReactMarkdown)` const Markdown = styled(ReactMarkdown)`

View File

@ -2,17 +2,17 @@ import { InputNumber } from 'antd'
import { InputNumberProps } from 'antd/lib/input-number' import { InputNumberProps } from 'antd/lib/input-number'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change' import { transparentize } from './color.change'
interface Props extends InputNumberProps { interface Props extends InputNumberProps {
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(InputNumber)` const Field = styled(InputNumber)`
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
background: none !important; background: none !important;
border-right: none; border-right: none;
border-top: none; border-top: none;
@ -21,12 +21,12 @@ const Field = styled(InputNumber)`
width: 100%; width: 100%;
:focus { :focus {
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px; outline: ${(props: Props) => props.design.colors.answer} auto 5px;
} }
:hover, :hover,
:active { :active {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
&.ant-input-number { &.ant-input-number {
@ -35,15 +35,15 @@ const Field = styled(InputNumber)`
input { input {
background: none !important; background: none !important;
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)}; color: ${(props: Props) => transparentize(props.design.colors.answer, 60)};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
interface Props { interface Props {
type: 'question' | 'answer' type: 'question' | 'answer'
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
const Paragraph = styled.p` const Paragraph = styled.p`
color: ${(props: Props) => color: ${(props: Props) =>
props.type === 'question' props.type === 'question' ? props.design.colors.question : props.design.colors.answer};
? props.design.colors.questionColor
: props.design.colors.answerColor};
` `
export const StyledP: React.FC<Props> = ({ children, ...props }) => { export const StyledP: React.FC<Props> = ({ children, ...props }) => {

View File

@ -2,34 +2,34 @@ import { Radio } from 'antd'
import { RadioProps } from 'antd/lib/radio/interface' import { RadioProps } from 'antd/lib/radio/interface'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
interface Props extends RadioProps { interface Props extends RadioProps {
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(Radio)` const Field = styled(Radio)`
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
background: none; background: none;
.ant-radio { .ant-radio {
.ant-radio-inner { .ant-radio-inner {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
&::after { &::after {
background: ${(props: Props) => props.design.colors.answerColor}; background: ${(props: Props) => props.design.colors.answer};
} }
} }
&::after { &::after {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -2,18 +2,18 @@ import { Select } from 'antd'
import { SelectProps } from 'antd/lib/select' import { SelectProps } from 'antd/lib/select'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change' import { transparentize } from './color.change'
interface Props extends SelectProps<string> { interface Props extends SelectProps<string> {
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(Select)` const Field = styled(Select)`
.ant-select-selector { .ant-select-selector {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor} !important; border-color: ${(props: Props) => props.design.colors.answer} !important;
background: none !important; background: none !important;
border-right: none !important; border-right: none !important;
border-top: none !important; border-top: none !important;
@ -23,25 +23,25 @@ const Field = styled(Select)`
} }
:focus { :focus {
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px; outline: ${(props: Props) => props.design.colors.answer} auto 5px;
} }
:hover, :hover,
:active { :active {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
input { input {
background: none !important; background: none !important;
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)}; color: ${(props: Props) => transparentize(props.design.colors.answer, 60)};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -2,17 +2,17 @@ import { Input } from 'antd'
import { TextAreaProps } from 'antd/lib/input/TextArea' import { TextAreaProps } from 'antd/lib/input/TextArea'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { FormDesignFragment } from '../../graphql/fragment/form.fragment' import { FormPublicDesignFragment } from '../../graphql/fragment/form.public.fragment'
import { transparentize } from './color.change' import { transparentize } from './color.change'
interface Props extends TextAreaProps { interface Props extends TextAreaProps {
design: FormDesignFragment design: FormPublicDesignFragment
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
const Field = styled(Input.TextArea)` const Field = styled(Input.TextArea)`
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
background: none !important; background: none !important;
border-right: none; border-right: none;
border-top: none; border-top: none;
@ -22,25 +22,25 @@ const Field = styled(Input.TextArea)`
:focus { :focus {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
:hover, :hover,
:active { :active {
border-color: ${(props: Props) => props.design.colors.answerColor}; border-color: ${(props: Props) => props.design.colors.answer};
} }
input { input {
background: none !important; background: none !important;
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
::placeholder { ::placeholder {
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)}; color: ${(props: Props) => transparentize(props.design.colors.answer, 60)};
} }
} }
.anticon { .anticon {
color: ${(props: Props) => props.design.colors.answerColor}; color: ${(props: Props) => props.design.colors.answer};
} }
` `

View File

@ -1,4 +1,4 @@
import { useMutation } from '@apollo/react-hooks' import { useMutation } from '@apollo/client'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { import {
SUBMISSION_SET_FIELD_MUTATION, SUBMISSION_SET_FIELD_MUTATION,

View File

@ -1,16 +1,12 @@
import { useQuery } from '@apollo/react-hooks'
import { AxiosRequestConfig } from 'axios'
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 { ME_QUERY, MeQueryData } from '../graphql/query/me.query' import { useMeQuery } from '../graphql/query/me.query'
import { LoadingPage } from './loading.page' import { LoadingPage } from './loading.page'
export const clearAuth = (): void => { export const clearAuth = (): void => {
localStorage.removeItem('access') localStorage.removeItem('access')
localStorage.removeItem('refresh') localStorage.removeItem('refresh')
// TODO logout on server!
} }
export const setAuth = (access: string, refresh: string): void => { export const setAuth = (access: string, refresh: string): void => {
@ -18,26 +14,6 @@ export const setAuth = (access: string, refresh: string): void => {
localStorage.setItem('refresh', refresh) localStorage.setItem('refresh', refresh)
} }
export const authConfig = (config: AxiosRequestConfig = {}): AxiosRequestConfig => {
if (!config.headers) {
config.headers = {}
}
try {
const token = localStorage.getItem('access')
// TODO check for validity / use refresh token
if (token) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
config.headers.Authorization = `Bearer ${token}`
}
} catch (e) {
return config
}
return config
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/no-explicit-any
export const withAuth = (Component: any, roles: string[] = [], optional?: boolean): React.FC => { export const withAuth = (Component: any, roles: string[] = [], optional?: boolean): React.FC => {
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
@ -45,7 +21,7 @@ export const withAuth = (Component: any, roles: string[] = [], optional?: boolea
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [access, setAccess] = useState(false) const [access, setAccess] = useState(false)
const { loading, data, error } = useQuery<MeQueryData>(ME_QUERY) const { loading, data, error } = useMeQuery()
useEffect(() => { useEffect(() => {
if (roles.length === 0) { if (roles.length === 0) {

8
doc/environment.md Normal file
View File

@ -0,0 +1,8 @@
# Environment Variables
| Name | Default Value | Description |
| ---- | ------------- | ----------- |
| ENDPOINT | `/graphql` | Path to graphql api endpoint |
| SERVER_ENDPOINT | *ENDPOINT* | if endpoint is relative it should point to a resolvable url of graphql api endpoint |
| MAIN_BACKGROUND | *not set* | background color of home page |
| SPA | `false` | dependent on the build mode this must be set to true to have working routing

45
graphql/client.ts Normal file
View File

@ -0,0 +1,45 @@
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import 'isomorphic-fetch'
import getConfig from 'next/config'
import { NextConfigType } from '../next.config.type'
let client: ApolloClient<any>
const getClient = (): ApolloClient<any> => {
if (!client) {
const config = getConfig() as NextConfigType
if (!config) return client
const { publicRuntimeConfig, serverRuntimeConfig } = config
client = new ApolloClient({
cache: new InMemoryCache(),
link: from([
setContext((request, context) => {
const headers: { [key: string]: string } = {}
if (process.browser) {
const access = localStorage.getItem('access')
if (access) {
headers.authorization = `Bearer ${access}`
}
}
return {
headers,
}
}),
new HttpLink({
uri: serverRuntimeConfig.endpoint || publicRuntimeConfig.endpoint,
}),
]),
})
}
return client
}
export default getClient

View File

@ -1,209 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminFormPageFragment {
show: boolean
title?: string
paragraph?: string
buttonText?: string
buttons: {
url?: string
action?: string
text?: string
bgColor?: string
color?: string
}[]
}
export interface AdminFormFieldOptionFragment {
key?: string
title?: string
value: string
}
export interface AdminFormFieldOptionKeysFragment {
[key: string]: string
}
export interface AdminFormFieldLogicJumpFragment {
fieldA?: string
valueB?: string
expressionString?: string
jumpTo?: string
enabled: boolean
}
export interface AdminFormFieldFragment {
id: string
title: string
slug?: string
type: string
description: string
required: boolean
value: string
options: AdminFormFieldOptionFragment[]
optionKeys?: AdminFormFieldOptionKeysFragment
logicJump: AdminFormFieldLogicJumpFragment
rating?: {
steps?: number
shape?: string
}
}
export interface AdminFormHookFragment {
id: string
enabled: boolean
url?: string
format?: string
}
export interface AdminFormFragment {
id?: string
title: string
created: string
lastModified?: string
language: string
showFooter: boolean
isLive: boolean
fields: AdminFormFieldFragment[]
hooks: AdminFormHookFragment[]
selfNotifications: {
enabled: boolean
subject?: string
htmlTemplate?: string
fromField?: string
toEmail?: string
}
respondentNotifications: {
enabled: boolean
subject?: string
htmlTemplate?: string
toField?: string
fromEmail?: string
}
design: {
colors: {
backgroundColor: string
questionColor: string
answerColor: string
buttonColor: string
buttonTextColor: string
}
font?: string
}
startPage: AdminFormPageFragment
endPage: AdminFormPageFragment
admin: {
id: string
username: string
email: string
}
}
export const ADMIN_FORM_FRAGMENT = gql`
fragment AdminForm on Form {
id
title
created
lastModified
language
showFooter
isLive
hooks {
id
enabled
format
url
}
fields {
id
title
slug
type
description
required
value
options {
key
title
value
}
logicJump {
fieldA
valueB
expressionString
jumpTo
enabled
}
rating {
steps
shape
}
}
selfNotifications {
enabled
subject
htmlTemplate
fromField
toEmail
}
respondentNotifications {
enabled
subject
htmlTemplate
toField
fromEmail
}
design {
colors {
backgroundColor
questionColor
answerColor
buttonColor
buttonActiveColor
buttonTextColor
}
font
}
startPage {
show
title
paragraph
buttonText
buttons {
url
action
text
bgColor
activeColor
color
}
}
endPage {
show
title
paragraph
buttonText
buttons {
url
action
text
bgColor
activeColor
color
}
}
admin {
id
username
email
}
}
`

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface AdminProfileFragment { export interface AdminProfileFragment {
id: string id: string

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface AdminUserFragment { export interface AdminUserFragment {
id: string id: string

View File

@ -1,32 +1,41 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface FormPageFragment { export interface FormPageFragment {
id: string
show: boolean show: boolean
title?: string title?: string
paragraph?: string paragraph?: string
buttonText?: string buttonText?: string
buttons: { buttons: {
id: string
url?: string url?: string
action?: string action?: string
text?: string text?: string
bgColor?: string bgColor?: string
activeColor?: string
color?: string color?: string
}[] }[]
} }
export interface FormFieldOptionFragment { export interface FormFieldOptionFragment {
id: string
key?: string key?: string
title?: string title?: string
value: string value: string
} }
export interface FormFieldLogicJumpFragment { export interface FormFieldOptionKeysFragment {
fieldA?: string [key: string]: string
valueB?: string }
expressionString?: string
jumpTo?: string export interface FormFieldLogicFragment {
id: string
action: string
formula: string
enabled: boolean enabled: boolean
jumpTo?: string
require?: boolean
visible?: boolean
disable?: boolean
} }
export interface FormFieldFragment { export interface FormFieldFragment {
@ -39,8 +48,9 @@ export interface FormFieldFragment {
value: string value: string
options: FormFieldOptionFragment[] options: FormFieldOptionFragment[]
optionKeys?: FormFieldOptionKeysFragment
logicJump: FormFieldLogicJumpFragment logic: FormFieldLogicFragment[]
rating?: { rating?: {
steps?: number steps?: number
@ -48,36 +58,70 @@ export interface FormFieldFragment {
} }
} }
export interface FormDesignFragment { export interface FormHookFragment {
colors: { id: string
backgroundColor: string enabled: boolean
questionColor: string url?: string
answerColor: string format?: string
buttonColor: string
buttonActiveColor: string
buttonTextColor: string
} }
font?: string
export interface FormNotificationFragment {
id: string
enabled: boolean
subject?: string
htmlTemplate?: string
toField?: string
toEmail?: string
fromField?: string
fromEmail?: string
} }
export interface FormFragment { export interface FormFragment {
id?: string id?: string
title: string title: string
created: string created: string
lastModified?: string
language: string language: string
showFooter: boolean showFooter: boolean
isLive: boolean
fields: FormFieldFragment[] fields: FormFieldFragment[]
design: FormDesignFragment hooks: FormHookFragment[]
notifications: FormNotificationFragment[]
design: {
colors: {
background: string
question: string
answer: string
button: string
buttonText: string
}
font?: string
}
startPage: FormPageFragment startPage: FormPageFragment
endPage: FormPageFragment endPage: FormPageFragment
admin: {
id: string
username: string
email: string
}
} }
export const FORM_FRAGMENT = gql` export const FORM_FRAGMENT = gql`
fragment Form on Form { fragment Form on Form {
id id
title title
created
lastModified
language language
showFooter showFooter
isLive
hooks {
id
enabled
format
url
}
fields { fields {
id id
@ -89,17 +133,21 @@ export const FORM_FRAGMENT = gql`
value value
options { options {
id
key key
title title
value value
} }
logicJump { logic {
fieldA id
valueB action
expressionString formula
jumpTo
enabled enabled
jumpTo
require
visible
disable
} }
rating { rating {
steps steps
@ -107,24 +155,36 @@ export const FORM_FRAGMENT = gql`
} }
} }
notifications {
id
enabled
subject
htmlTemplate
fromField
fromEmail
toField
toEmail
}
design { design {
colors { colors {
backgroundColor background
questionColor question
answerColor answer
buttonColor button
buttonActiveColor buttonActive
buttonTextColor buttonText
} }
font font
} }
startPage { startPage {
id
show show
title title
paragraph paragraph
buttonText buttonText
buttons { buttons {
id
url url
action action
text text
@ -133,13 +193,14 @@ export const FORM_FRAGMENT = gql`
color color
} }
} }
endPage { endPage {
id
show show
title title
paragraph paragraph
buttonText buttonText
buttons { buttons {
id
url url
action action
text text
@ -148,5 +209,10 @@ export const FORM_FRAGMENT = gql`
color color
} }
} }
admin {
id
username
email
}
} }
` `

View File

@ -0,0 +1,31 @@
import { gql } from '@apollo/client/core'
export interface FormPagerFragment {
id: string
created: string
lastModified?: string
title: string
isLive: boolean
language: string
admin: {
id: string
email: string
username: string
}
}
export const FORM_PAGER_FRAGMENT = gql`
fragment Form on Form {
id
created
lastModified
title
isLive
language
admin {
id
email
username
}
}
`

View File

@ -0,0 +1,173 @@
import { gql } from '@apollo/client/core'
export interface FormPublicPageFragment {
id: string
show: boolean
title?: string
paragraph?: string
buttonText?: string
buttons: {
id: string
url?: string
action?: string
text?: string
bgColor?: string
activeColor?: string
color?: string
}[]
}
export interface FormPublicFieldOptionFragment {
key?: string
title?: string
value: string
}
export interface FormPublicFieldLogicFragment {
id: string
action: string
formula: string
jumpTo?: string
require?: boolean
visible?: boolean
disable?: boolean
}
export interface FormPublicFieldFragment {
id: string
title: string
slug?: string
type: string
description: string
required: boolean
value: string
options: FormPublicFieldOptionFragment[]
logic: FormPublicFieldLogicFragment[]
rating?: {
steps?: number
shape?: string
}
}
export interface FormPublicDesignFragment {
colors: {
background: string
question: string
answer: string
button: string
buttonActive: string
buttonText: string
}
font?: string
}
export interface FormPublicFragment {
id?: string
title: string
created: string
language: string
showFooter: boolean
fields: FormPublicFieldFragment[]
design: FormPublicDesignFragment
startPage: FormPublicPageFragment
endPage: FormPublicPageFragment
}
export const FORM_PUBLIC_FRAGMENT = gql`
fragment Form on Form {
id
title
language
showFooter
fields {
id
title
slug
type
description
required
value
logic {
id
formula
action
disable
jumpTo
require
visible
}
options {
id
key
title
value
}
logic {
id
action
formula
jumpTo
require
visible
disable
}
rating {
steps
shape
}
}
design {
colors {
background
question
answer
button
buttonActive
buttonText
}
font
}
startPage {
id
show
title
paragraph
buttonText
buttons {
id
url
action
text
bgColor
activeColor
color
}
}
endPage {
id
show
title
paragraph
buttonText
buttons {
id
url
action
text
bgColor
activeColor
color
}
}
}
`

View File

@ -1,16 +1,61 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
interface SubmissionFragmentFormField {
title: string
required: boolean
}
export interface SubmissionFragmentField {
id: string
value: string
type: string
field?: SubmissionFragmentFormField
}
export interface SubmissionFragment { export interface SubmissionFragment {
id?: string id: string
title: string
created: string created: string
lastModified?: string
percentageComplete: number
timeElapsed: number
geoLocation: {
country: string
city: string
}
device: {
type: string
name: string
} }
export const FORM_FRAGMENT = gql` fields: SubmissionFragmentField[]
}
export const SUBMISSION_FRAGMENT = gql`
fragment Submission on Submission { fragment Submission on Submission {
id id
created
lastModified
percentageComplete
timeElapsed
geoLocation {
country
city
}
device {
type
name
}
fields {
id
value
type
field {
title title
language required
showFooter }
}
} }
` `

View File

@ -0,0 +1,19 @@
import { gql } from '@apollo/client/core'
export interface UserPagerFragment {
id: string
roles: string[]
verifiedEmail: boolean
email: string
created: string
}
export const USER_PAGER_FRAGMENT = gql`
fragment User on User {
id
roles
verifiedEmail
email
created
}
`

View File

@ -1,20 +0,0 @@
import { gql } from 'apollo-boost'
import { ADMIN_FORM_FRAGMENT, AdminFormFragment } from '../fragment/admin.form.fragment'
export interface AdminFormCreateMutationData {
form: AdminFormFragment
}
export interface AdminFormCreateMutationVariables {
form: AdminFormFragment
}
export const ADMIN_FORM_CREATE_MUTATION = gql`
mutation update($form: FormCreateInput!) {
form: createForm(form: $form) {
...AdminForm
}
}
${ADMIN_FORM_FRAGMENT}
`

View File

@ -1,19 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminFormDeleteMutationData {
form: {
id
}
}
export interface AdminFormDeleteMutationVariables {
id: string
}
export const ADMIN_FORM_DELETE_MUTATION = gql`
mutation delete($id: ID!) {
form: deleteForm(id: $id) {
id
}
}
`

View File

@ -1,20 +0,0 @@
import { gql } from 'apollo-boost'
import { ADMIN_FORM_FRAGMENT, AdminFormFragment } from '../fragment/admin.form.fragment'
export interface AdminFormUpdateMutationData {
form: AdminFormFragment
}
export interface AdminFormUpdateMutationVariables {
form: AdminFormFragment
}
export const ADMIN_FORM_UPDATE_MUTATION = gql`
mutation update($form: FormUpdateInput!) {
form: updateForm(form: $form) {
...AdminForm
}
}
${ADMIN_FORM_FRAGMENT}
`

View File

@ -1,21 +0,0 @@
import { gql } from 'apollo-boost'
import { ADMIN_PROFILE_FRAGMENT } from '../fragment/admin.profile.fragment'
import { AdminUserFragment } from '../fragment/admin.user.fragment'
export interface AdminProfileUpdateMutationData {
user: AdminUserFragment
}
export interface AdminProfileUpdateMutationVariables {
user: AdminUserFragment
}
export const ADMIN_PROFILE_UPDATE_MUTATION = gql`
mutation update($user: ProfileUpdateInput!) {
form: updateProfile(user: $user) {
...AdminProfile
}
}
${ADMIN_PROFILE_FRAGMENT}
`

View File

@ -1,19 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminUserDeleteMutationData {
form: {
id
}
}
export interface AdminUserDeleteMutationVariables {
id: string
}
export const ADMIN_USER_DELETE_MUTATION = gql`
mutation delete($id: ID!) {
form: deleteUser(id: $id) {
id
}
}
`

View File

@ -1,20 +0,0 @@
import { gql } from 'apollo-boost'
import { ADMIN_USER_FRAGMENT, AdminUserFragment } from '../fragment/admin.user.fragment'
export interface AdminUserUpdateMutationData {
user: AdminUserFragment
}
export interface AdminUserUpdateMutationVariables {
user: AdminUserFragment
}
export const ADMIN_USER_UPDATE_MUTATION = gql`
mutation update($user: UserUpdateInput!) {
form: updateUser(user: $user) {
...AdminUser
}
}
${ADMIN_USER_FRAGMENT}
`

View File

@ -0,0 +1,30 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { FORM_FRAGMENT, FormFragment } from '../fragment/form.fragment'
interface Data {
form: FormFragment
}
interface Variables {
form: {
isLive: boolean
language: string
showFooter?: boolean
title: string
}
}
const MUTATION = gql`
mutation update($form: FormCreateInput!) {
form: createForm(form: $form) {
...Form
}
}
${FORM_FRAGMENT}
`
export const useFormCreateMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -0,0 +1,24 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
interface Data {
form: {
id
}
}
interface Variables {
id: string
}
const MUTATION = gql`
mutation delete($id: ID!) {
form: deleteForm(id: $id) {
id
}
}
`
export const useFormDeleteMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -0,0 +1,25 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { FORM_FRAGMENT, FormFragment } from '../fragment/form.fragment'
interface Data {
form: FormFragment
}
interface Variables {
form: FormFragment
}
const MUTATION = gql`
mutation update($form: FormUpdateInput!) {
form: updateForm(form: $form) {
...Form
}
}
${FORM_FRAGMENT}
`
export const useFormUpdateMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface LoginMutationData { export interface LoginMutationData {
tokens: { tokens: {

View File

@ -0,0 +1,34 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { ADMIN_PROFILE_FRAGMENT } from '../fragment/admin.profile.fragment'
import { AdminUserFragment } from '../fragment/admin.user.fragment'
export interface Data {
user: AdminUserFragment
}
export interface Variables {
user: {
email?: string
firstName?: string
id: string
language?: string
lastName?: string
password?: string
username?: string
}
}
export const MUTATION = gql`
mutation update($user: ProfileUpdateInput!) {
form: updateProfile(user: $user) {
...AdminProfile
}
}
${ADMIN_PROFILE_FRAGMENT}
`
export const useProfileUpdateMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -1,6 +1,7 @@
import { gql } from 'apollo-boost' import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
export interface RegisterMutationData { interface Data {
tokens: { tokens: {
access: string access: string
refresh: string refresh: string
@ -13,11 +14,11 @@ export interface RegisterUserData {
password: string password: string
} }
export interface RegisterMutationVariables { interface Variables {
user: RegisterUserData user: RegisterUserData
} }
export const REGISTER_MUTATION = gql` const MUTATION = gql`
mutation register($user: UserCreateInput!) { mutation register($user: UserCreateInput!) {
tokens: authRegister(user: $user) { tokens: authRegister(user: $user) {
access: accessToken access: accessToken
@ -25,3 +26,7 @@ export const REGISTER_MUTATION = gql`
} }
} }
` `
export const useRegisterMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' 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 start($submission: ID!, $field: SubmissionSetFieldInput!) {

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface SubmissionSetFieldMutationData { export interface SubmissionSetFieldMutationData {
submission: { submission: {

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface SubmissionStartMutationData { export interface SubmissionStartMutationData {
submission: { submission: {

View File

@ -0,0 +1,24 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
interface Data {
form: {
id
}
}
interface Variables {
id: string
}
const MUTATION = gql`
mutation delete($id: ID!) {
form: deleteUser(id: $id) {
id
}
}
`
export const useUserDeleteMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -0,0 +1,25 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { ADMIN_USER_FRAGMENT, AdminUserFragment } from '../fragment/admin.user.fragment'
interface Data {
user: AdminUserFragment
}
interface Variables {
user: AdminUserFragment
}
const MUTATION = gql`
mutation update($user: UserUpdateInput!) {
form: updateUser(user: $user) {
...AdminUser
}
}
${ADMIN_USER_FRAGMENT}
`
export const useUserUpdateMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)

View File

@ -1,20 +0,0 @@
import { gql } from 'apollo-boost'
import { ADMIN_FORM_FRAGMENT, AdminFormFragment } from '../fragment/admin.form.fragment'
export interface AdminFormQueryData {
form: AdminFormFragment
}
export interface AdminFormQueryVariables {
id: string
}
export const ADMIN_FORM_QUERY = gql`
query form($id: ID!) {
form: getFormById(id: $id) {
...AdminForm
}
}
${ADMIN_FORM_FRAGMENT}
`

View File

@ -1,55 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminPagerFormEntryAdminQueryData {
id: string
email: string
username: string
}
export interface AdminPagerFormEntryQueryData {
id: string
created: string
lastModified?: string
title: string
isLive: boolean
language: string
admin: AdminPagerFormEntryAdminQueryData
}
export interface AdminPagerFormQueryData {
pager: {
entries: AdminPagerFormEntryQueryData[]
total: number
limit: number
start: number
}
}
export interface AdminPagerFormQueryVariables {
start?: number
limit?: number
}
export const ADMIN_PAGER_FORM_QUERY = gql`
query pager($start: Int, $limit: Int) {
pager: listForms(start: $start, limit: $limit) {
entries {
id
created
lastModified
title
isLive
language
admin {
id
email
username
}
}
total
limit
start
}
}
`

View File

@ -1,98 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminPagerSubmissionFormFieldQueryData {
title: string
required: boolean
}
export interface AdminPagerSubmissionFormQueryData {
id: string
title: string
isLive: boolean
}
export interface AdminPagerSubmissionEntryFieldQueryData {
id: string
value: string
type: string
field?: AdminPagerSubmissionFormFieldQueryData
}
export interface AdminPagerSubmissionEntryQueryData {
id: string
created: string
lastModified?: string
percentageComplete: number
timeElapsed: number
geoLocation: {
country: string
city: string
}
device: {
type: string
name: string
}
fields: AdminPagerSubmissionEntryFieldQueryData[]
}
export interface AdminPagerSubmissionQueryData {
pager: {
entries: AdminPagerSubmissionEntryQueryData[]
total: number
limit: number
start: number
}
form: AdminPagerSubmissionFormQueryData
}
export interface AdminPagerSubmissionQueryVariables {
form: string
start?: number
limit?: number
}
export const ADMIN_PAGER_SUBMISSION_QUERY = gql`
query pager($form: ID!, $start: Int, $limit: Int) {
form: getFormById(id: $form) {
id
title
isLive
}
pager: listSubmissions(form: $form, start: $start, limit: $limit) {
entries {
id
created
lastModified
percentageComplete
timeElapsed
geoLocation {
country
city
}
device {
type
name
}
fields {
id
value
type
field {
title
required
}
}
}
total
limit
start
}
}
`

View File

@ -1,41 +0,0 @@
import { gql } from 'apollo-boost'
export interface AdminPagerUserEntryQueryData {
id: string
roles: string[]
verifiedEmail: boolean
email: string
created: string
}
export interface AdminPagerUserQueryData {
pager: {
entries: AdminPagerUserEntryQueryData[]
total: number
limit: number
start: number
}
}
export interface AdminPagerUserQueryVariables {
start?: number
limit?: number
}
export const ADMIN_PAGER_USER_QUERY = gql`
query pager($start: Int, $limit: Int) {
pager: listUsers(start: $start, limit: $limit) {
entries {
id
roles
verifiedEmail
email
created
}
total
limit
start
}
}
`

View File

@ -1,13 +1,12 @@
import { gql } from 'apollo-boost' import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { ADMIN_PROFILE_FRAGMENT, AdminProfileFragment } from '../fragment/admin.profile.fragment' import { ADMIN_PROFILE_FRAGMENT, AdminProfileFragment } from '../fragment/admin.profile.fragment'
export interface AdminProfileQueryData { interface Data {
user: AdminProfileFragment user: AdminProfileFragment
} }
export interface AdminProfileQueryVariables {} export const QUERY = gql`
export const ADMIN_PROFILE_QUERY = gql`
query profile { query profile {
user: me { user: me {
...AdminProfile ...AdminProfile
@ -16,3 +15,7 @@ export const ADMIN_PROFILE_QUERY = gql`
${ADMIN_PROFILE_FRAGMENT} ${ADMIN_PROFILE_FRAGMENT}
` `
export const useProfileQuery = (
options?: QueryHookOptions<Data, unknown>
): QueryResult<Data, unknown> => useQuery<Data, unknown>(QUERY, options)

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
export interface AdminStatisticQueryData { export interface AdminStatisticQueryData {
forms: { forms: {

View File

@ -1,4 +1,4 @@
import { gql } from 'apollo-boost' import { gql } from '@apollo/client/core'
import { ADMIN_USER_FRAGMENT, AdminUserFragment } from '../fragment/admin.user.fragment' import { ADMIN_USER_FRAGMENT, AdminUserFragment } from '../fragment/admin.user.fragment'
export interface AdminUserQueryData { export interface AdminUserQueryData {

View File

@ -0,0 +1,37 @@
import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { FORM_PAGER_FRAGMENT, FormPagerFragment } from '../fragment/form.pager.fragment'
interface Data {
pager: {
entries: FormPagerFragment[]
total: number
limit: number
start: number
}
}
interface Variables {
start?: number
limit?: number
}
const QUERY = gql`
query pager($start: Int, $limit: Int) {
pager: listForms(start: $start, limit: $limit) {
entries {
...Form
}
total
limit
start
}
}
${FORM_PAGER_FRAGMENT}
`
export const useFormPagerQuery = (
options?: QueryHookOptions<Data, Variables>
): QueryResult<Data, Variables> => useQuery<Data, Variables>(QUERY, options)

View File

@ -0,0 +1,25 @@
import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { FORM_PUBLIC_FRAGMENT, FormPublicFragment } from '../fragment/form.public.fragment'
interface Data {
form: FormPublicFragment
}
interface Variables {
id: string
}
const QUERY = gql`
query form($id: ID!) {
form: getFormById(id: $id) {
...Form
}
}
${FORM_PUBLIC_FRAGMENT}
`
export const useFormPublicQuery = (
options?: QueryHookOptions<Data, Variables>
): QueryResult<Data, Variables> => useQuery<Data, Variables>(QUERY, options)

View File

@ -1,16 +1,17 @@
import { gql } from 'apollo-boost' import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { FORM_FRAGMENT, FormFragment } from '../fragment/form.fragment' import { FORM_FRAGMENT, FormFragment } from '../fragment/form.fragment'
export interface FormQueryData { export interface Data {
form: FormFragment form: FormFragment
} }
export interface FormQueryVariables { interface Variables {
id: string id: string
} }
export const FORM_QUERY = gql` const QUERY = gql`
query form($id: ID!) { query getFormById($id: ID!) {
form: getFormById(id: $id) { form: getFormById(id: $id) {
...Form ...Form
} }
@ -18,3 +19,7 @@ export const FORM_QUERY = gql`
${FORM_FRAGMENT} ${FORM_FRAGMENT}
` `
export const useFormQuery = (
options?: QueryHookOptions<Data, Variables>
): QueryResult<Data, Variables> => useQuery<Data, Variables>(QUERY, options)

View File

@ -1,6 +1,7 @@
import { gql } from 'apollo-boost' import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
export interface MeQueryData { interface Data {
me: { me: {
id: string id: string
username: string username: string
@ -8,8 +9,8 @@ export interface MeQueryData {
} }
} }
export const ME_QUERY = gql` const QUERY = gql`
query { query me {
me { me {
id id
roles roles
@ -17,3 +18,6 @@ export const ME_QUERY = gql`
} }
} }
` `
export const useMeQuery = (options?: QueryHookOptions<Data, unknown>): QueryResult<Data, unknown> =>
useQuery<Data, unknown>(QUERY, options)

View File

@ -1,6 +1,7 @@
import { gql } from 'apollo-boost' import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
export interface SettingsQueryData { interface Data {
disabledSignUp: { disabledSignUp: {
value: boolean value: boolean
} }
@ -12,8 +13,8 @@ export interface SettingsQueryData {
} }
} }
export const SETTINGS_QUERY = gql` const QUERY = gql`
query { query settings {
disabledSignUp: getSetting(key: "SIGNUP_DISABLED") { disabledSignUp: getSetting(key: "SIGNUP_DISABLED") {
value: isTrue value: isTrue
} }
@ -25,3 +26,7 @@ export const SETTINGS_QUERY = gql`
} }
} }
` `
export const useSettingsQuery = (
options?: QueryHookOptions<Data, unknown>
): QueryResult<Data, unknown> => useQuery<Data, unknown>(QUERY, options)

View File

@ -0,0 +1,49 @@
import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { useImperativeQuery } from '../../components/use.imerative.query'
import { FORM_PAGER_FRAGMENT, FormPagerFragment } from '../fragment/form.pager.fragment'
import { SUBMISSION_FRAGMENT, SubmissionFragment } from '../fragment/submission.fragment'
interface Data {
pager: {
entries: SubmissionFragment[]
total: number
limit: number
start: number
}
form: FormPagerFragment
}
interface Variables {
form: string
start?: number
limit?: number
}
const QUERY = gql`
query pager($form: ID!, $start: Int, $limit: Int) {
form: getFormById(id: $form) {
...Form
}
pager: listSubmissions(form: $form, start: $start, limit: $limit) {
entries {
...Submission
}
total
limit
start
}
}
${SUBMISSION_FRAGMENT}
${FORM_PAGER_FRAGMENT}
`
export const useSubmissionPagerQuery = (
options?: QueryHookOptions<Data, Variables>
): QueryResult<Data, Variables> => useQuery<Data, Variables>(QUERY, options)
export const useSubmissionPagerImperativeQuery = () => useImperativeQuery<Data, Variables>(QUERY)

View File

@ -0,0 +1,37 @@
import { QueryHookOptions, QueryResult, useQuery } from '@apollo/client'
import { gql } from '@apollo/client/core'
import { USER_PAGER_FRAGMENT, UserPagerFragment } from '../fragment/user.pager.fragment'
interface Data {
pager: {
entries: UserPagerFragment[]
total: number
limit: number
start: number
}
}
interface Variables {
start?: number
limit?: number
}
const QUERY = gql`
query pager($start: Int, $limit: Int) {
pager: listUsers(start: $start, limit: $limit) {
entries {
...User
}
total
limit
start
}
}
${USER_PAGER_FRAGMENT}
`
export const useUserPagerQuery = (
options?: QueryHookOptions<Data, Variables>
): QueryResult<Data, Variables> => useQuery<Data, Variables>(QUERY, options)

View File

@ -1,5 +1,6 @@
{ {
"admin": "Admin", "admin": "Admin",
"profile": "Profile",
"checkingCredentials": "Checking Credentials", "checkingCredentials": "Checking Credentials",
"loadingCredentials": "Loading Credentials", "loadingCredentials": "Loading Credentials",
"login": "Login", "login": "Login",

View File

@ -6,8 +6,12 @@ const version = p.version;
module.exports = withImages({ module.exports = withImages({
poweredByHeader: false, poweredByHeader: false,
publicRuntimeConfig: { publicRuntimeConfig: {
endpoint: process.env.API_HOST || '/graphql', endpoint: process.env.ENDPOINT || '/graphql',
spa: !!process.env.SPA || false, spa: !!process.env.SPA || false,
mainBackground: process.env.MAIN_BACKGROUND || '#8FA2A6'
},
serverRuntimeConfig: {
endpoint: process.env.SERVER_ENDPOINT || process.env.ENDPOINT || '/graphql',
}, },
env: { env: {
version, version,

10
next.config.type.ts Normal file
View File

@ -0,0 +1,10 @@
export interface NextConfigType {
publicRuntimeConfig: {
endpoint: string
spa?: boolean
mainBackground?: string
}
serverRuntimeConfig: {
endpoint: string
}
}

View File

@ -1,71 +1,70 @@
{ {
"name": "ohmyform-react", "name": "ohmyform-react",
"version": "0.9.8", "version": "1.0.0-alpha",
"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",
"build": "next build", "build": "next build",
"lint": "eslint pages/ store/ components/ graphql/", "lint": "eslint pages/ store/ components/ graphql/",
"type-check": "tsc --pretty",
"export": "cross-env SPA=1 next build && next export", "export": "cross-env SPA=1 next build && next export",
"start": "next start -p $PORT", "start": "next start -p $PORT",
"translation:sort": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node-script locales/sort.ts", "translation:sort": "cross-env TS_NODE_TRANSPILE_ONLY=true ts-node-script locales/sort.ts",
"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.2.2", "@ant-design/icons": "^4.6.2",
"@apollo/client": "^3.2.0", "@apollo/client": "^3.3.15",
"@apollo/react-common": "^3.1.4", "@lifeomic/axios-fetch": "^2.0.0",
"@apollo/react-hooks": "^4.0.0", "antd": "^4.15.3",
"@lifeomic/axios-fetch": "^1.4.2", "axios": "^0.21.1",
"antd": "^4.6.2", "cross-env": "^7.0.3",
"apollo-boost": "^0.4.9", "dayjs": "^1.10.4",
"axios": "^0.20.0", "exceljs": "^4.2.1",
"cross-env": "^7.0.2", "graphql": "^15.5.0",
"dayjs": "^1.8.34", "i18next": "^19.9.2",
"exceljs": "^4.2.0", "i18next-browser-languagedetector": "^6.1.0",
"graphql": "^15.3.0", "isomorphic-fetch": "^3.0.0",
"i18next": "^19.7.0", "next": "^10.2.0",
"i18next-browser-languagedetector": "^6.0.1", "next-images": "^1.7.0",
"next": "9.5.3",
"next-images": "^1.4.1",
"next-redux-wrapper": "^6.0.2", "next-redux-wrapper": "^6.0.2",
"react": "16.13.1", "react": "^17.0.2",
"react-color": "^2.18.1", "react-color": "^2.19.3",
"react-dom": "16.13.1", "react-dom": "^17.0.2",
"react-github-button": "^0.1.11", "react-github-button": "^0.1.11",
"react-i18next": "^11.7.2", "react-i18next": "^11.8.15",
"react-icons": "^3.11.0", "react-icons": "^3.11.0",
"react-id-swiper": "^4.0.0", "react-id-swiper": "^4.0.0",
"react-markdown": "^4.3.1", "react-markdown": "^4.3.1",
"react-redux": "^7.2.1", "react-redux": "^7.2.4",
"redux": "^4.0.5", "redux": "^4.1.0",
"redux-devtools-extension": "^2.13.8", "redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"sass": "^1.26.10", "sass": "^1.32.12",
"styled-components": "^5.1.1", "styled-components": "^5.2.3",
"swiper": "^6.1.2" "swiper": "^6.5.8"
}, },
"devDependencies": { "devDependencies": {
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
"@types/lifeomic__axios-fetch": "^1.4.0", "@types/lifeomic__axios-fetch": "^1.5.0",
"@types/node": "^14.6.2", "@types/node": "^14.14.43",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.10",
"@types/react": "^16.9.49", "@types/react": "^17.0.4",
"@types/styled-components": "^5.1.2", "@types/styled-components": "^5.1.9",
"@types/swiper": "^5.4.0", "@types/swiper": "^5.4.2",
"@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.0.1", "@typescript-eslint/parser": "^4.22.0",
"commander": "^6.1.0", "commander": "^6.2.1",
"eslint": "^7.8.1", "eslint": "^7.25.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.15.0",
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.20.6", "eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.1.0", "eslint-plugin-react-hooks": "^4.2.0",
"glob": "^7.1.6", "glob": "^7.1.6",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"prettier": "^2.1.1", "prettier": "^2.1.1",
"ts-node": "^9.0.0", "ts-node": "^9.1.1",
"typescript": "^4.0.2" "typescript": "^4.2.4"
} }
} }

View File

@ -1,19 +1,15 @@
import { ApolloProvider } from '@apollo/react-common' import { ApolloProvider } from '@apollo/client'
import { buildAxiosFetch } from '@lifeomic/axios-fetch'
import 'antd/dist/antd.css' import 'antd/dist/antd.css'
import { ApolloProvider as ApolloHooksProvider } from '@apollo/react-hooks'
import ApolloClient from 'apollo-boost'
import 'assets/global.scss' import 'assets/global.scss'
import 'assets/variables.scss' import 'assets/variables.scss'
import axios from 'axios'
import { authConfig } from 'components/with.auth'
import 'i18n' import 'i18n'
import { AppInitialProps, AppProps } from 'next/app' import { AppInitialProps } from 'next/app'
import getConfig from 'next/config' import getConfig from 'next/config'
import { AppType } from 'next/dist/next-server/lib/utils' import { AppType } from 'next/dist/next-server/lib/utils'
import Head from 'next/head' import Head from 'next/head'
import React from 'react' import React from 'react'
import { wrapper } from 'store' import { wrapper } from 'store'
import getClient from '../graphql/client'
const { publicRuntimeConfig } = getConfig() as { const { publicRuntimeConfig } = getConfig() as {
publicRuntimeConfig: { publicRuntimeConfig: {
@ -21,26 +17,14 @@ const { publicRuntimeConfig } = getConfig() as {
} }
} }
const client = new ApolloClient({
uri: publicRuntimeConfig.endpoint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-explicit-any
fetch: buildAxiosFetch(axios as any) as any,
request: (operation): void => {
operation.setContext(authConfig())
},
})
const App: AppType = ({ Component, pageProps }) => { const App: AppType = ({ Component, pageProps }) => {
return ( return (
<ApolloProvider client={client}> <ApolloProvider client={getClient()}>
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */}
<ApolloHooksProvider client={client as any}>
<Head> <Head>
<title>OhMyForm</title> <title>OhMyForm</title>
<meta name="theme-color" content={'#4182e4'} /> <meta name="theme-color" content={'#4182e4'} />
</Head> </Head>
<Component {...pageProps} /> <Component {...pageProps} />
</ApolloHooksProvider>
</ApolloProvider> </ApolloProvider>
) )
} }

View File

@ -1,4 +1,3 @@
import { useMutation, useQuery } from '@apollo/react-hooks'
import { Button, Form, Input, message, Tabs } from 'antd' import { Button, Form, Input, message, Tabs } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import { cleanInput } from 'components/clean.input' import { cleanInput } from 'components/clean.input'
@ -6,45 +5,37 @@ import { BaseDataTab } from 'components/form/admin/base.data.tab'
import { DesignTab } from 'components/form/admin/design.tab' import { DesignTab } from 'components/form/admin/design.tab'
import { EndPageTab } from 'components/form/admin/end.page.tab' import { EndPageTab } from 'components/form/admin/end.page.tab'
import { FieldsTab } from 'components/form/admin/fields.tab' import { FieldsTab } from 'components/form/admin/fields.tab'
import { RespondentNotificationsTab } from 'components/form/admin/respondent.notifications.tab' import { NotificationsTab } from 'components/form/admin/notifications.tab'
import { SelfNotificationsTab } from 'components/form/admin/self.notifications.tab'
import { StartPageTab } from 'components/form/admin/start.page.tab' import { StartPageTab } from 'components/form/admin/start.page.tab'
import Structure from 'components/structure' import { Structure } from 'components/structure'
import { withAuth } from 'components/with.auth' import { withAuth } from 'components/with.auth'
import { import { useFormUpdateMutation } from 'graphql/mutation/form.update.mutation'
AdminFormFieldFragment,
AdminFormFieldOptionKeysFragment,
} from 'graphql/fragment/admin.form.fragment'
import {
ADMIN_FORM_UPDATE_MUTATION,
AdminFormUpdateMutationData,
AdminFormUpdateMutationVariables,
} from 'graphql/mutation/admin.form.update.mutation'
import {
ADMIN_FORM_QUERY,
AdminFormQueryData,
AdminFormQueryVariables,
} from 'graphql/query/admin.form.query'
import { NextPage } from 'next' import { NextPage } from 'next'
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 { HooksTab } from '../../../../components/form/admin/hooks.tab' import { HooksTab } from '../../../../components/form/admin/hooks.tab'
import {
FormFieldFragment,
FormFieldOptionKeysFragment,
} from '../../../../graphql/fragment/form.fragment'
import { Data, useFormQuery } from '../../../../graphql/query/form.query'
const Index: NextPage = () => { const Index: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [form] = useForm() const [form] = useForm()
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [fields, setFields] = useState<AdminFormFieldFragment[]>([]) const [fields, setFields] = useState<FormFieldFragment[]>([])
const [update] = useMutation<AdminFormUpdateMutationData, AdminFormUpdateMutationVariables>( const [update] = useFormUpdateMutation()
ADMIN_FORM_UPDATE_MUTATION
)
const processNext = (next: AdminFormQueryData): AdminFormQueryData => { const processNext = (next: Data) => {
next.form.fields = next.form.fields.map((field) => { return {
const keys: AdminFormFieldOptionKeysFragment = {} form: {
...next.form,
fields: next.form.fields.map((field) => {
const keys: FormFieldOptionKeysFragment = {}
field.options.forEach((option) => { field.options.forEach((option) => {
if (option.key) { if (option.key) {
@ -52,29 +43,28 @@ const Index: NextPage = () => {
} }
}) })
field.options = field.options.filter((option) => !option.key) return {
field.optionKeys = keys ...field,
return field options: field.options.filter((option) => !option.key),
}) optionKeys: keys,
}
return next }),
},
}
} }
const { data, loading } = useQuery<AdminFormQueryData, AdminFormQueryVariables>( const { data, loading } = useFormQuery({
ADMIN_FORM_QUERY,
{
variables: { variables: {
id: router.query.id as string, id: router.query.id as string,
}, },
onCompleted: (next) => { onCompleted: (next) => {
next = processNext(next) const processed = processNext(next)
form.setFieldsValue(next) form.setFieldsValue(processed)
setFields(next.form.fields) setFields(processed.form.fields)
}, },
} })
)
const save = async (formData: AdminFormQueryData) => { const save = async (formData: Data) => {
setSaving(true) setSaving(true)
formData.form.fields = formData.form.fields formData.form.fields = formData.form.fields
@ -89,6 +79,7 @@ const Index: NextPage = () => {
} }
options.push({ options.push({
id: null, // TODO improve this
value: optionKeys[key], value: optionKeys[key],
key, key,
}) })
@ -175,15 +166,9 @@ const Index: NextPage = () => {
/> />
<BaseDataTab key={'base_data'} tab={t('form:baseDataTab')} /> <BaseDataTab key={'base_data'} tab={t('form:baseDataTab')} />
<DesignTab key={'design'} tab={t('form:designTab')} /> <DesignTab key={'design'} tab={t('form:designTab')} />
<SelfNotificationsTab <NotificationsTab
key={'self_notifications'} key={'notifications'}
tab={t('form:selfNotificationsTab')} tab={t('form:notificationsTab')}
fields={fields}
form={form}
/>
<RespondentNotificationsTab
key={'respondent_notifications'}
tab={t('form:respondentNotificationsTab')}
fields={fields} fields={fields}
form={form} form={form}
/> />

View File

@ -1,4 +1,3 @@
import { useQuery } from '@apollo/react-hooks'
import { Button, Progress, Table } from 'antd' import { Button, Progress, Table } from 'antd'
import { PaginationProps } from 'antd/es/pagination' import { PaginationProps } from 'antd/es/pagination'
import { ProgressProps } from 'antd/lib/progress' import { ProgressProps } from 'antd/lib/progress'
@ -15,13 +14,9 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ExportSubmissionAction } from '../../../../components/form/admin/export.submission.action' import { ExportSubmissionAction } from '../../../../components/form/admin/export.submission.action'
import { SubmissionValues } from '../../../../components/form/admin/submission.values' import { SubmissionValues } from '../../../../components/form/admin/submission.values'
import { import { FormPagerFragment } from '../../../../graphql/fragment/form.pager.fragment'
ADMIN_PAGER_SUBMISSION_QUERY, import { SubmissionFragment } from '../../../../graphql/fragment/submission.fragment'
AdminPagerSubmissionEntryQueryData, import { useSubmissionPagerQuery } from '../../../../graphql/query/submission.pager.query'
AdminPagerSubmissionFormQueryData,
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables,
} from '../../../../graphql/query/admin.pager.submission.query'
const Submissions: NextPage = () => { const Submissions: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -29,12 +24,9 @@ const Submissions: NextPage = () => {
const [pagination, setPagination] = useState<PaginationProps>({ const [pagination, setPagination] = useState<PaginationProps>({
pageSize: 25, pageSize: 25,
}) })
const [form, setForm] = useState<AdminPagerSubmissionFormQueryData>() const [form, setForm] = useState<FormPagerFragment>()
const [entries, setEntries] = useState<AdminPagerSubmissionEntryQueryData[]>() const [entries, setEntries] = useState<SubmissionFragment[]>()
const { loading, refetch } = useQuery< const { loading, refetch } = useSubmissionPagerQuery({
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables
>(ADMIN_PAGER_SUBMISSION_QUERY, {
variables: { variables: {
form: router.query.id as string, form: router.query.id as string,
limit: pagination.pageSize, limit: pagination.pageSize,
@ -50,10 +42,10 @@ const Submissions: NextPage = () => {
}, },
}) })
const columns: ColumnsType<AdminPagerSubmissionEntryQueryData> = [ const columns: ColumnsType<SubmissionFragment> = [
{ {
title: t('submission:progress'), title: t('submission:progress'),
render(row: AdminPagerSubmissionEntryQueryData) { render(_, row) {
const props: ProgressProps = { const props: ProgressProps = {
status: 'active', status: 'active',
percent: Math.round(row.percentageComplete * 100), percent: Math.round(row.percentageComplete * 100),

View File

@ -1,31 +1,27 @@
import { useMutation } from '@apollo/react-hooks'
import { Button, Form, Input, message, Tabs } from 'antd' import { Button, Form, Input, message, Tabs } from 'antd'
import { useForm } from 'antd/lib/form/Form'
import { cleanInput } from 'components/clean.input' import { cleanInput } from 'components/clean.input'
import { BaseDataTab } from 'components/form/admin/base.data.tab' import { BaseDataTab } from 'components/form/admin/base.data.tab'
import Structure from 'components/structure' import Structure from 'components/structure'
import { withAuth } from 'components/with.auth' import { withAuth } from 'components/with.auth'
import { AdminFormQueryData } from 'graphql/query/admin.form.query' import { FormFragment } from 'graphql/fragment/form.fragment'
import { useFormCreateMutation } from 'graphql/mutation/form.create.mutation'
import { NextPage } from 'next' import { NextPage } from 'next'
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 {
ADMIN_FORM_CREATE_MUTATION, interface FormData {
AdminFormCreateMutationData, form: FormFragment
AdminFormCreateMutationVariables, }
} from '../../../graphql/mutation/admin.form.create.mutation'
const Create: NextPage = () => { const Create: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [form] = useForm() const [form] = Form.useForm<FormData>()
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [create] = useMutation<AdminFormCreateMutationData, AdminFormCreateMutationVariables>( const [create] = useFormCreateMutation()
ADMIN_FORM_CREATE_MUTATION
)
const save = async (formData: AdminFormQueryData) => { const save = async (formData: FormData) => {
setSaving(true) setSaving(true)
try { try {

View File

@ -4,7 +4,6 @@ import {
GlobalOutlined, GlobalOutlined,
UnorderedListOutlined, UnorderedListOutlined,
} from '@ant-design/icons/lib' } from '@ant-design/icons/lib'
import { useMutation, useQuery } from '@apollo/react-hooks'
import { Button, message, Popconfirm, Space, Table, Tag, Tooltip } from 'antd' import { Button, message, Popconfirm, Space, Table, Tag, Tooltip } from 'antd'
import { PaginationProps } from 'antd/es/pagination' import { PaginationProps } from 'antd/es/pagination'
import { ColumnsType } from 'antd/lib/table/interface' import { ColumnsType } from 'antd/lib/table/interface'
@ -13,23 +12,14 @@ import { FormIsLive } from 'components/form/admin/is.live'
import Structure from 'components/structure' import Structure from 'components/structure'
import { TimeAgo } from 'components/time.ago' import { TimeAgo } from 'components/time.ago'
import { withAuth } from 'components/with.auth' import { withAuth } from 'components/with.auth'
import {
ADMIN_PAGER_FORM_QUERY,
AdminPagerFormEntryAdminQueryData,
AdminPagerFormEntryQueryData,
AdminPagerFormQueryData,
AdminPagerFormQueryVariables,
} from 'graphql/query/admin.pager.form.query'
import { NextPage } from 'next' import { NextPage } from 'next'
import Link from 'next/link' import Link from 'next/link'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useWindowSize } from '../../../components/use.window.size' import { useWindowSize } from '../../../components/use.window.size'
import { import { FormPagerFragment } from '../../../graphql/fragment/form.pager.fragment'
ADMIN_FORM_DELETE_MUTATION, import { useFormDeleteMutation } from '../../../graphql/mutation/form.delete.mutation'
AdminFormDeleteMutationData, import { useFormPagerQuery } from '../../../graphql/query/form.pager.query'
AdminFormDeleteMutationVariables,
} from '../../../graphql/mutation/admin.form.delete.mutation'
const Index: NextPage = () => { const Index: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -37,10 +27,8 @@ const Index: NextPage = () => {
const [pagination, setPagination] = useState<PaginationProps>({ const [pagination, setPagination] = useState<PaginationProps>({
pageSize: 25, pageSize: 25,
}) })
const [entries, setEntries] = useState<AdminPagerFormEntryQueryData[]>() const [entries, setEntries] = useState<FormPagerFragment[]>()
const { loading, refetch } = useQuery<AdminPagerFormQueryData, AdminPagerFormQueryVariables>( const { loading, refetch, error } = useFormPagerQuery({
ADMIN_PAGER_FORM_QUERY,
{
variables: { variables: {
limit: pagination.pageSize, limit: pagination.pageSize,
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0, start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0,
@ -52,21 +40,17 @@ const Index: NextPage = () => {
}) })
setEntries(pager.entries) setEntries(pager.entries)
}, },
} })
) const [executeDelete] = useFormDeleteMutation()
const [executeDelete] = useMutation<
AdminFormDeleteMutationData,
AdminFormDeleteMutationVariables
>(ADMIN_FORM_DELETE_MUTATION)
const deleteForm = async (form: AdminFormDeleteMutationVariables) => { const deleteForm = async (id: string) => {
try { try {
await executeDelete({ await executeDelete({
variables: { variables: {
id: form.id, id,
}, },
}) })
const next = entries.filter((entry) => entry.id !== form.id) const next = entries.filter((entry) => entry.id !== id)
if (next.length === 0) { if (next.length === 0) {
setPagination({ ...pagination, current: 1 }) setPagination({ ...pagination, current: 1 })
} else { } else {
@ -79,7 +63,7 @@ const Index: NextPage = () => {
} }
} }
const columns: ColumnsType<AdminPagerFormEntryQueryData> = [ const columns: ColumnsType<FormPagerFragment> = [
{ {
title: t('form:row.isLive'), title: t('form:row.isLive'),
dataIndex: 'isLive', dataIndex: 'isLive',
@ -95,7 +79,7 @@ const Index: NextPage = () => {
{ {
title: t('form:row.admin'), title: t('form:row.admin'),
dataIndex: 'admin', dataIndex: 'admin',
render(user: AdminPagerFormEntryAdminQueryData) { render(_, { admin: user }) {
if (!user) { if (!user) {
return <Tag color={'red'} title={t('form:row.adminMissing')} /> return <Tag color={'red'} title={t('form:row.adminMissing')} />
} }
@ -137,7 +121,7 @@ const Index: NextPage = () => {
{ {
title: t('form:row.menu'), title: t('form:row.menu'),
align: 'right', align: 'right',
render(row: AdminPagerFormEntryQueryData) { render(_, row) {
return ( return (
<Space direction={width < 600 ? 'vertical' : 'horizontal'}> <Space direction={width < 600 ? 'vertical' : 'horizontal'}>
<Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${row.id}/submissions`}> <Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${row.id}/submissions`}>
@ -156,7 +140,7 @@ const Index: NextPage = () => {
<Popconfirm <Popconfirm
title={t('form:confirmDelete')} title={t('form:confirmDelete')}
onConfirm={() => deleteForm(row)} onConfirm={() => deleteForm(row.id)}
okText={t('form:deleteNow')} okText={t('form:deleteNow')}
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
> >
@ -188,6 +172,7 @@ const Index: NextPage = () => {
<Button type={'primary'}>{t('form:new')}</Button> <Button type={'primary'}>{t('form:new')}</Button>
</Link>, </Link>,
]} ]}
error={error?.message}
> >
<Table <Table
columns={columns} columns={columns}

View File

@ -1,4 +1,4 @@
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/client'
import { Col, Row, Statistic } from 'antd' import { Col, Row, Statistic } from 'antd'
import Structure from 'components/structure' import Structure from 'components/structure'
import { withAuth } from 'components/with.auth' import { withAuth } from 'components/with.auth'

View File

@ -1,50 +1,57 @@
import { useMutation, useQuery } from '@apollo/react-hooks' import { Button, Divider, Form, Input, message, Select } from 'antd'
import { Button, Form, Input, message, Select } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import { NextPage } from 'next' import { NextPage } from 'next'
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 { cleanInput } from '../../components/clean.input'
import Structure from '../../components/structure' import Structure from '../../components/structure'
import { withAuth } from '../../components/with.auth' import { withAuth } from '../../components/with.auth'
import { import { useProfileUpdateMutation } from '../../graphql/mutation/profile.update.mutation'
ADMIN_PROFILE_UPDATE_MUTATION, import { useProfileQuery } from '../../graphql/query/admin.profile.query'
AdminProfileUpdateMutationData,
AdminProfileUpdateMutationVariables,
} from '../../graphql/mutation/admin.profile.update.mutation'
import {
ADMIN_PROFILE_QUERY,
AdminProfileQueryData,
AdminProfileQueryVariables,
} from '../../graphql/query/admin.profile.query'
import { AdminUserQueryData } from '../../graphql/query/admin.user.query'
import { languages } from '../../i18n' import { languages } from '../../i18n'
interface FormData {
user: {
id: string
username: string
email: string
language: string
firstName: string
lastName: string
}
password: string
confirm: string
}
const Profile: NextPage = () => { const Profile: NextPage = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [form] = useForm() const [form] = useForm<FormData>()
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const router = useRouter()
const { loading } = useQuery<AdminProfileQueryData, AdminProfileQueryVariables>( const { loading } = useProfileQuery({
ADMIN_PROFILE_QUERY,
{
onCompleted: (next) => { onCompleted: (next) => {
form.setFieldsValue(next) form.setFieldsValue(next)
}, },
} onError(e) {
) void router.push('/')
},
})
const [update] = useMutation<AdminProfileUpdateMutationData, AdminProfileUpdateMutationVariables>( const [update] = useProfileUpdateMutation()
ADMIN_PROFILE_UPDATE_MUTATION
)
const save = async (formData: AdminUserQueryData) => { const save = async (data: FormData) => {
setSaving(true) setSaving(true)
try { try {
const next = ( const next = (
await update({ await update({
variables: cleanInput(formData), variables: {
user: {
...data.user,
password: data.password && data.password === data.confirm ? data.password : undefined,
},
},
}) })
).data ).data
@ -147,6 +154,46 @@ const Profile: NextPage = () => {
<Form.Item label={t('profile:lastName')} name={['user', 'lastName']}> <Form.Item label={t('profile:lastName')} name={['user', 'lastName']}>
<Input /> <Input />
</Form.Item> </Form.Item>
<Divider />
<Form.Item
name="password"
label={t('profile:password')}
rules={[
{
min: 5,
message: t('validation:passwordMinLength'),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{() => (
<Form.Item
name="confirm"
label={t('profile:confirmPassword')}
rules={[
{
required: Boolean(form.getFieldValue('password')),
message: t('validation:passwordConfirmRequired'),
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve()
}
return Promise.reject(new Error(t('validation:passwordConfirmMismatch')))
},
}),
]}
>
<Input.Password />
</Form.Item>
)}
</Form.Item>
</Form> </Form>
</Structure> </Structure>
) )

View File

@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/client'
import { Button, Form, Input, message, Tabs } from 'antd' import { Button, Form, Input, message, Tabs } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import Structure from 'components/structure' import Structure from 'components/structure'
@ -9,11 +9,7 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cleanInput } from '../../../../components/clean.input' import { cleanInput } from '../../../../components/clean.input'
import { BaseDataTab } from '../../../../components/user/admin/base.data.tab' import { BaseDataTab } from '../../../../components/user/admin/base.data.tab'
import { import { useUserUpdateMutation } from '../../../../graphql/mutation/user.update.mutation'
ADMIN_USER_UPDATE_MUTATION,
AdminUserUpdateMutationData,
AdminUserUpdateMutationVariables,
} from '../../../../graphql/mutation/admin.user.update.mutation'
import { import {
ADMIN_USER_QUERY, ADMIN_USER_QUERY,
AdminUserQueryData, AdminUserQueryData,
@ -38,9 +34,7 @@ const Index: NextPage = () => {
} }
) )
const [update] = useMutation<AdminUserUpdateMutationData, AdminUserUpdateMutationVariables>( const [update] = useUserUpdateMutation()
ADMIN_USER_UPDATE_MUTATION
)
const save = async (formData: AdminUserQueryData) => { const save = async (formData: AdminUserQueryData) => {
setSaving(true) setSaving(true)

View File

@ -1,5 +1,4 @@
import { DeleteOutlined, EditOutlined } from '@ant-design/icons/lib' import { DeleteOutlined, EditOutlined } from '@ant-design/icons/lib'
import { useMutation, useQuery } from '@apollo/react-hooks'
import { Button, message, Popconfirm, Space, Table, Tag } from 'antd' import { Button, message, Popconfirm, Space, Table, Tag } from 'antd'
import { PaginationProps } from 'antd/es/pagination' import { PaginationProps } from 'antd/es/pagination'
import { ColumnsType } from 'antd/lib/table/interface' import { ColumnsType } from 'antd/lib/table/interface'
@ -12,17 +11,9 @@ import { useTranslation } from 'react-i18next'
import { DateTime } from '../../../components/date.time' import { DateTime } from '../../../components/date.time'
import { useWindowSize } from '../../../components/use.window.size' import { useWindowSize } from '../../../components/use.window.size'
import { UserRole } from '../../../components/user/role' import { UserRole } from '../../../components/user/role'
import { import { UserPagerFragment } from '../../../graphql/fragment/user.pager.fragment'
ADMIN_USER_DELETE_MUTATION, import { useUserDeleteMutation } from '../../../graphql/mutation/user.delete.mutation'
AdminUserDeleteMutationData, import { useUserPagerQuery } from '../../../graphql/query/user.pager.query'
AdminUserDeleteMutationVariables,
} from '../../../graphql/mutation/admin.user.delete.mutation'
import {
ADMIN_PAGER_USER_QUERY,
AdminPagerUserEntryQueryData,
AdminPagerUserQueryData,
AdminPagerUserQueryVariables,
} from '../../../graphql/query/admin.pager.user.query'
const Index: NextPage = () => { const Index: NextPage = () => {
const { width } = useWindowSize() const { width } = useWindowSize()
@ -30,10 +21,8 @@ const Index: NextPage = () => {
const [pagination, setPagination] = useState<PaginationProps>({ const [pagination, setPagination] = useState<PaginationProps>({
pageSize: 10, pageSize: 10,
}) })
const [entries, setEntries] = useState<AdminPagerUserEntryQueryData[]>() const [entries, setEntries] = useState<UserPagerFragment[]>()
const { loading, refetch } = useQuery<AdminPagerUserQueryData, AdminPagerUserQueryVariables>( const { loading, refetch, error } = useUserPagerQuery({
ADMIN_PAGER_USER_QUERY,
{
variables: { variables: {
limit: pagination.pageSize, limit: pagination.pageSize,
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0, start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0,
@ -45,21 +34,17 @@ const Index: NextPage = () => {
}) })
setEntries(pager.entries) setEntries(pager.entries)
}, },
} })
) const [executeDelete] = useUserDeleteMutation()
const [executeDelete] = useMutation<
AdminUserDeleteMutationData,
AdminUserDeleteMutationVariables
>(ADMIN_USER_DELETE_MUTATION)
const deleteUser = async (form: AdminPagerUserEntryQueryData) => { const deleteUser = async (id: string) => {
try { try {
await executeDelete({ await executeDelete({
variables: { variables: {
id: form.id, id,
}, },
}) })
const next = entries.filter((entry) => entry.id !== form.id) const next = entries.filter((entry) => entry.id !== id)
if (next.length === 0) { if (next.length === 0) {
setPagination({ ...pagination, current: 1 }) setPagination({ ...pagination, current: 1 })
} else { } else {
@ -71,7 +56,7 @@ const Index: NextPage = () => {
} }
} }
const columns: ColumnsType<AdminPagerUserEntryQueryData> = [ const columns: ColumnsType<UserPagerFragment> = [
{ {
title: t('user:row.roles'), title: t('user:row.roles'),
dataIndex: 'roles', dataIndex: 'roles',
@ -82,7 +67,7 @@ const Index: NextPage = () => {
}, },
{ {
title: t('user:row.email'), title: t('user:row.email'),
render(row: AdminPagerUserEntryQueryData) { render(_, row) {
return <Tag color={row.verifiedEmail ? 'lime' : 'volcano'}>{row.email}</Tag> return <Tag color={row.verifiedEmail ? 'lime' : 'volcano'}>{row.email}</Tag>
}, },
}, },
@ -97,7 +82,7 @@ const Index: NextPage = () => {
{ {
title: t('user:row.menu'), title: t('user:row.menu'),
align: 'right', align: 'right',
render(row: AdminPagerUserEntryQueryData) { render(_, row) {
return ( return (
<Space direction={width < 600 ? 'vertical' : 'horizontal'}> <Space direction={width < 600 ? 'vertical' : 'horizontal'}>
<Link href={'/admin/users/[id]'} as={`/admin/users/${row.id}`}> <Link href={'/admin/users/[id]'} as={`/admin/users/${row.id}`}>
@ -108,7 +93,7 @@ const Index: NextPage = () => {
<Popconfirm <Popconfirm
title={t('user:confirmDelete')} title={t('user:confirmDelete')}
onConfirm={() => deleteUser(row)} onConfirm={() => deleteUser(row.id)}
okText={t('user:deleteNow')} okText={t('user:deleteNow')}
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}
> >
@ -128,6 +113,7 @@ const Index: NextPage = () => {
loading={loading} loading={loading}
breadcrumbs={[{ href: '/admin', name: t('admin:home') }]} breadcrumbs={[{ href: '/admin', name: t('admin:home') }]}
padded={false} padded={false}
error={error?.message}
> >
<Table <Table
columns={columns} columns={columns}

View File

@ -1,10 +1,8 @@
import { useQuery } from '@apollo/react-hooks'
import { Modal } from 'antd' import { Modal } from 'antd'
import { ErrorPage } from 'components/error.page' import { ErrorPage } from 'components/error.page'
import { Field } from 'components/form/field' import { Field } from 'components/form/field'
import { FormPage } from 'components/form/page' import { FormPage } from 'components/form/page'
import { LoadingPage } from 'components/loading.page' import { LoadingPage } from 'components/loading.page'
import { FORM_QUERY, FormQueryData, FormQueryVariables } from 'graphql/query/form.query'
import { NextPage } from 'next' import { NextPage } from 'next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
@ -14,6 +12,7 @@ import { ReactIdSwiperProps } from 'react-id-swiper/lib/types'
import * as OriginalSwiper from 'swiper' import * as OriginalSwiper from 'swiper'
import { Omf } from '../../../components/omf' import { Omf } from '../../../components/omf'
import { useSubmission } from '../../../components/use.submission' import { useSubmission } from '../../../components/use.submission'
import { useFormPublicQuery } from '../../../graphql/query/form.public.query'
interface Props { interface Props {
id: string id: string
@ -26,7 +25,7 @@ const Index: NextPage<Props> = () => {
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null) const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
const submission = useSubmission(id) const submission = useSubmission(id)
const { loading, data, error } = useQuery<FormQueryData, FormQueryVariables>(FORM_QUERY, { const { loading, data, error } = useFormPublicQuery({
variables: { variables: {
id, id,
}, },
@ -74,7 +73,7 @@ const Index: NextPage<Props> = () => {
return ( return (
<div <div
style={{ style={{
background: design.colors.backgroundColor, background: design.colors.background,
}} }}
> >
<Omf /> <Omf />

View File

@ -1,19 +1,15 @@
import { Layout } from 'antd' import { Layout } from 'antd'
import { AuthFooter } from 'components/auth/footer' import { AuthFooter } from 'components/auth/footer'
import { NextPage } from 'next' import { GetStaticProps, NextPage } from 'next'
import getConfig from 'next/config' import getConfig from 'next/config'
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 { LoadingPage } from '../components/loading.page' import { LoadingPage } from '../components/loading.page'
import { Omf } from '../components/omf' import { Omf } from '../components/omf'
import { NextConfigType } from '../next.config.type'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { publicRuntimeConfig } = getConfig() as NextConfigType
const { publicRuntimeConfig } = getConfig() as {
publicRuntimeConfig: {
spa: boolean
}
}
const Index: NextPage = () => { const Index: NextPage = () => {
const router = useRouter() const router = useRouter()
@ -56,7 +52,7 @@ const Index: NextPage = () => {
<Layout <Layout
style={{ style={{
height: '100vh', height: '100vh',
background: '#437fdc', background: publicRuntimeConfig.mainBackground,
}} }}
> >
<Omf /> <Omf />
@ -76,4 +72,11 @@ const Index: NextPage = () => {
) )
} }
export const getStaticProps: GetStaticProps = async () => {
return {
revalidate: 10,
props: {},
}
}
export default Index export default Index

View File

@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@apollo/react-hooks' import { useMutation } from '@apollo/client'
import { Alert, Button, Form, Input, message } from 'antd' import { Alert, Button, Form, Input, message } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import { AuthFooter } from 'components/auth/footer' import { AuthFooter } from 'components/auth/footer'
@ -16,7 +16,7 @@ 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 { Omf } from '../../components/omf' import { Omf } from '../../components/omf'
import { SETTINGS_QUERY, SettingsQueryData } from '../../graphql/query/settings.query' import { useSettingsQuery } from '../../graphql/query/settings.query'
import scss from './index.module.scss' import scss from './index.module.scss'
const Index: NextPage = () => { const Index: NextPage = () => {
@ -25,7 +25,7 @@ const Index: NextPage = () => {
const router = useRouter() const router = useRouter()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [login] = useMutation<LoginMutationData, LoginMutationVariables>(LOGIN_MUTATION) const [login] = useMutation<LoginMutationData, LoginMutationVariables>(LOGIN_MUTATION)
const { data } = useQuery<SettingsQueryData>(SETTINGS_QUERY) const { data } = useSettingsQuery()
const finish = async (data: LoginMutationVariables) => { const finish = async (data: LoginMutationVariables) => {
setLoading(true) setLoading(true)

View File

@ -1,15 +1,9 @@
import { useMutation, useQuery } from '@apollo/react-hooks'
import { Button, Form, Input, message } from 'antd' import { Button, Form, Input, message } from 'antd'
import { useForm } from 'antd/lib/form/Form' import { useForm } from 'antd/lib/form/Form'
import { AuthFooter } from 'components/auth/footer' import { AuthFooter } from 'components/auth/footer'
import { AuthLayout } from 'components/auth/layout' import { AuthLayout } from 'components/auth/layout'
import { setAuth } from 'components/with.auth' import { setAuth } from 'components/with.auth'
import { import { RegisterUserData, useRegisterMutation } from 'graphql/mutation/register.mutation'
REGISTER_MUTATION,
RegisterMutationData,
RegisterMutationVariables,
RegisterUserData,
} from 'graphql/mutation/register.mutation'
import { NextPage } from 'next' import { NextPage } from 'next'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -17,7 +11,7 @@ import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ErrorPage } from '../components/error.page' import { ErrorPage } from '../components/error.page'
import { Omf } from '../components/omf' import { Omf } from '../components/omf'
import { SETTINGS_QUERY, SettingsQueryData } from '../graphql/query/settings.query' import { useSettingsQuery } from '../graphql/query/settings.query'
import scss from './register.module.scss' import scss from './register.module.scss'
const Register: NextPage = () => { const Register: NextPage = () => {
@ -25,9 +19,9 @@ const Register: NextPage = () => {
const [form] = useForm() const [form] = useForm()
const router = useRouter() const router = useRouter()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { data } = useQuery<SettingsQueryData>(SETTINGS_QUERY) const { data } = useSettingsQuery()
const [register] = useMutation<RegisterMutationData, RegisterMutationVariables>(REGISTER_MUTATION) const [register] = useRegisterMutation()
const finish = async (data: RegisterUserData) => { const finish = async (data: RegisterUserData) => {
setLoading(true) setLoading(true)

View File

@ -5,12 +5,6 @@ schema {
mutation: Mutation mutation: Mutation
} }
interface Notification {
enabled: Boolean!
htmlTemplate: String
subject: String
}
type AuthToken { type AuthToken {
accessToken: String! accessToken: String!
refreshToken: String! refreshToken: String!
@ -21,17 +15,18 @@ type Button {
activeColor: String activeColor: String
bgColor: String bgColor: String
color: String color: String
id: ID!
text: String text: String
url: String url: String
} }
type Colors { type Colors {
answerColor: String! answer: String!
backgroundColor: String! background: String!
buttonActiveColor: String! button: String!
buttonColor: String! buttonActive: String!
buttonTextColor: String! buttonText: String!
questionColor: String! question: String!
} }
type Deleted { type Deleted {
@ -44,6 +39,7 @@ type Design {
} }
type Device { type Device {
language: String
name: String! name: String!
type: String! type: String!
} }
@ -59,8 +55,7 @@ type Form {
isLive: Boolean! isLive: Boolean!
language: String! language: String!
lastModified: DateTime lastModified: DateTime
respondentNotifications: RespondentNotificationsModel! notifications: [FormNotification!]!
selfNotifications: SelfNotificationsModel!
showFooter: Boolean! showFooter: Boolean!
startPage: Page! startPage: Page!
title: String! title: String!
@ -69,7 +64,7 @@ type Form {
type FormField { type FormField {
description: String! description: String!
id: ID! id: ID!
logicJump: LogicJump! logic: [FormFieldLogic!]!
options: [FormFieldOption!]! options: [FormFieldOption!]!
rating: FormFieldRating rating: FormFieldRating
required: Boolean! required: Boolean!
@ -79,7 +74,19 @@ type FormField {
value: String! value: String!
} }
type FormFieldLogic {
action: String!
disable: Boolean
enabled: Boolean!
formula: String
id: ID!
jumpTo: ID
require: Boolean
visible: Boolean
}
type FormFieldOption { type FormFieldOption {
id: ID!
key: String key: String
title: String title: String
value: String! value: String!
@ -97,6 +104,24 @@ type FormHook {
url: String 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 { type FormStatistic {
total: Int! total: Int!
} }
@ -106,14 +131,6 @@ type GeoLocation {
country: String country: String
} }
type LogicJump {
enabled: Boolean!
expressionString: String
fieldA: ID
jumpTo: ID
valueB: String
}
type Mutation { type Mutation {
authLogin(password: String!, username: String!): AuthToken! authLogin(password: String!, username: String!): AuthToken!
authRegister(user: UserCreateInput!): AuthToken! authRegister(user: UserCreateInput!): AuthToken!
@ -130,39 +147,12 @@ type Mutation {
type Page { type Page {
buttonText: String buttonText: String
buttons: [Button!]! buttons: [Button!]!
id: ID!
paragraph: String paragraph: String
show: Boolean! show: Boolean!
title: String title: String
} }
type PagerForm {
entries: [Form!]!
limit: Int!
start: Int!
total: Int!
}
type PagerSetting {
entries: [Setting!]!
limit: Int!
start: Int!
total: Int!
}
type PagerSubmission {
entries: [Submission!]!
limit: Int!
start: Int!
total: Int!
}
type PagerUser {
entries: [User!]!
limit: Int!
start: Int!
total: Int!
}
type Profile { type Profile {
created: DateTime! created: DateTime!
email: String! email: String!
@ -180,33 +170,17 @@ type Query {
getFormById(id: ID!): Form! getFormById(id: ID!): Form!
getFormStatistic: FormStatistic! getFormStatistic: FormStatistic!
getSetting(key: ID!): Setting! getSetting(key: ID!): Setting!
getSettings: PagerSetting! getSettings: SettingPager!
getSubmissionStatistic: SubmissionStatistic! getSubmissionStatistic: SubmissionStatistic!
getUserById(id: ID!): User! getUserById(id: ID!): User!
getUserStatistic: UserStatistic! getUserStatistic: UserStatistic!
listForms(limit: Int = 50, start: Int = 0): PagerForm! listForms(limit: Int = 50, start: Int = 0): FormPager!
listSubmissions(form: ID!, limit: Int = 50, start: Int = 0): PagerSubmission! listSubmissions(form: ID!, limit: Int = 50, start: Int = 0): SubmissionPager!
listUsers(limit: Int = 50, start: Int = 0): PagerUser! listUsers(limit: Int = 50, start: Int = 0): UserPager!
me: Profile! me: Profile!
status: Version! status: Version!
} }
type RespondentNotificationsModel implements Notification {
enabled: Boolean!
fromEmail: String
htmlTemplate: String
subject: String
toField: String
}
type SelfNotificationsModel implements Notification {
enabled: Boolean!
fromField: String
htmlTemplate: String
subject: String
toEmail: String
}
type Setting { type Setting {
isFalse: Boolean! isFalse: Boolean!
isTrue: Boolean! isTrue: Boolean!
@ -214,6 +188,13 @@ type Setting {
value: String value: String
} }
type SettingPager {
entries: [Setting!]!
limit: Int!
start: Int!
total: Int!
}
type Submission { type Submission {
created: DateTime! created: DateTime!
device: Device! device: Device!
@ -233,6 +214,13 @@ type SubmissionField {
value: String! value: String!
} }
type SubmissionPager {
entries: [Submission!]!
limit: Int!
start: Int!
total: Int!
}
type SubmissionProgress { type SubmissionProgress {
created: DateTime! created: DateTime!
id: ID! id: ID!
@ -258,6 +246,13 @@ type User {
verifiedEmail: Boolean! verifiedEmail: Boolean!
} }
type UserPager {
entries: [User!]!
limit: Int!
start: Int!
total: Int!
}
type UserStatistic { type UserStatistic {
total: Int! total: Int!
} }
@ -271,17 +266,18 @@ input ButtonInput {
activeColor: String activeColor: String
bgColor: String bgColor: String
color: String color: String
id: ID
text: String text: String
url: String url: String
} }
input ColorsInput { input ColorsInput {
answerColor: String! answer: String!
backgroundColor: String! background: String!
buttonActiveColor: String! button: String!
buttonColor: String! buttonActive: String!
buttonTextColor: String! buttonText: String!
questionColor: String! question: String!
} }
input DesignInput { input DesignInput {
@ -290,6 +286,7 @@ input DesignInput {
} }
input DeviceInput { input DeviceInput {
language: String
name: String! name: String!
type: String! type: String!
} }
@ -303,8 +300,9 @@ input FormCreateInput {
input FormFieldInput { input FormFieldInput {
description: String! description: String!
disabled: Boolean
id: ID! id: ID!
logicJump: LogicJumpInput logic: [FormFieldLogicInput!]
options: [FormFieldOptionInput!] options: [FormFieldOptionInput!]
rating: FormFieldRatingInput rating: FormFieldRatingInput
required: Boolean! required: Boolean!
@ -314,7 +312,19 @@ input FormFieldInput {
value: String! value: String!
} }
input FormFieldLogicInput {
action: String
disable: Boolean
enabled: Boolean
formula: String
id: ID
jumpTo: ID
require: Boolean
visible: Boolean
}
input FormFieldOptionInput { input FormFieldOptionInput {
id: ID
key: String key: String
title: String title: String
value: String! value: String!
@ -332,6 +342,17 @@ input FormHookInput {
url: String url: String
} }
input FormNotificationInput {
enabled: Boolean!
fromEmail: String
fromField: String
htmlTemplate: String
id: ID
subject: String
toEmail: String
toField: String
}
input FormUpdateInput { input FormUpdateInput {
design: DesignInput design: DesignInput
endPage: PageInput endPage: PageInput
@ -340,24 +361,16 @@ input FormUpdateInput {
id: ID! id: ID!
isLive: Boolean isLive: Boolean
language: String language: String
respondentNotifications: RespondentNotificationsInput notifications: [FormNotificationInput!]
selfNotifications: SelfNotificationsInput
showFooter: Boolean showFooter: Boolean
startPage: PageInput startPage: PageInput
title: String title: String
} }
input LogicJumpInput {
enabled: Boolean
expressionString: String
fieldA: ID
jumpTo: ID
valueB: String
}
input PageInput { input PageInput {
buttonText: String buttonText: String
buttons: [ButtonInput!]! buttons: [ButtonInput!]!
id: ID
paragraph: String paragraph: String
show: Boolean! show: Boolean!
title: String title: String
@ -373,22 +386,6 @@ input ProfileUpdateInput {
username: String username: String
} }
input RespondentNotificationsInput {
enabled: Boolean!
fromEmail: String
htmlTemplate: String
subject: String
toField: String
}
input SelfNotificationsInput {
enabled: Boolean!
fromField: String
htmlTemplate: String
subject: String
toEmail: String
}
input SubmissionSetFieldInput { input SubmissionSetFieldInput {
data: String! data: String!
field: ID! field: ID!

5434
yarn.lock

File diff suppressed because it is too large Load Diff