add form submissions in admin

update fields during on next
This commit is contained in:
Michael Schramm 2020-05-30 19:09:26 +02:00
parent 37f9d0b62d
commit 98bb74ea0f
18 changed files with 466 additions and 40 deletions

View File

@ -33,7 +33,6 @@ export const FieldCard: React.FC<Props> = props => {
useEffect(() => {
const id = setTimeout(() => {
console.log('update fields')
onChangeFields(fields.map((field, i) => {
if (i === index) {
return {
@ -102,7 +101,10 @@ export const FieldCard: React.FC<Props> = props => {
<Checkbox />
</Form.Item>
<TypeComponent field={field} />
<TypeComponent
field={field}
form={form}
/>
</Card>
)
}

View File

@ -1,28 +1,37 @@
import {DatePicker, Form} from 'antd'
import moment from 'moment'
import React from 'react'
import {AdminFieldTypeProps} from './type.props'
export const DateType: React.FC<AdminFieldTypeProps> = props => {
export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
return (
<div>
<Form.Item
label={'Default Date'}
name={[props.field.name, 'value']}
name={[field.name, 'value']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e ? e.format('YYYY-MM-DD') : undefined}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker />
<DatePicker
format={'YYYY-MM-DD'}
/>
</Form.Item>
<Form.Item
label={'Min Date'}
name={[props.field.name, 'min']}
name={[field.name, 'min']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker />
</Form.Item>
<Form.Item
label={'Max Date'}
name={[props.field.name, 'max']}
name={[field.name, 'max']}
labelCol={{ span: 6 }}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<DatePicker />
</Form.Item>

View File

@ -4,14 +4,12 @@ import {AdminFieldTypeProps} from './type.props'
export const TextType: React.FC<AdminFieldTypeProps> = props => {
return (
<div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
</div>
<Form.Item
label={'Default Value'}
name={[props.field.name, 'value']}
labelCol={{ span: 6 }}
>
<Input />
</Form.Item>
)
}

View File

@ -1,3 +1,6 @@
import {FormInstance} from 'antd/lib/form'
export interface AdminFieldTypeProps {
form: FormInstance
field: any
}

View File

@ -13,18 +13,19 @@ interface Props {
field: FormFieldFragment
design: FormDesignFragment
save: (data: any) => any
next: () => any
prev: () => any
}
export const Field: React.FC<Props> = ({field, design, children, next, prev, ...props}) => {
export const Field: React.FC<Props> = ({field, save, design, children, next, prev, ...props}) => {
const [form] = useForm()
const FieldInput: React.FC<FieldTypeProps> = fieldTypes[field.type] || TextType
const finish = (data) => {
console.log('received field data', data)
save(data)
next()
}

View File

@ -4,8 +4,9 @@ import React from 'react'
import {StyledDateInput} from '../../styled/date.input'
import {FieldTypeProps} from './type.props'
export const DateType: React.FC<FieldTypeProps> = ({field, design}) => {
export const DateType: React.FC<FieldTypeProps> = ({ field, design}) => {
// TODO check min and max
// TODO if default is passed, then the changing should not be required
return (
<div>
@ -14,6 +15,8 @@ export const DateType: React.FC<FieldTypeProps> = ({field, design}) => {
rules={[
{ required: field.required, message: 'Please provide Information' },
]}
getValueFromEvent={e => e.format('YYYY-MM-DD')}
getValueProps={e => ({value: e ? moment(e) : undefined})}
>
<StyledDateInput
size={'large'}

View File

@ -1,20 +1,23 @@
import {Form, InputNumber} from 'antd'
import {Form} from 'antd'
import React from 'react'
import {StyledNumberInput} from '../../styled/number.input'
import {FieldTypeProps} from './type.props'
export const NumberType: React.FC<FieldTypeProps> = ({field}) => {
export const NumberType: React.FC<FieldTypeProps> = ({field, design}) => {
return (
<div>
<Form.Item
label={'Default Number'}
name={[field.id, 'value']}
rules={[
{ type: 'number', message: 'Must be a valid URL' },
{ required: field.required, message: 'Please provide Information' },
]}
labelCol={{ span: 6 }}
>
<InputNumber />
<StyledNumberInput
design={design}
size={'large'}
defaultValue={parseFloat(field.value)}
/>
</Form.Item>
</div>
)

View File

@ -0,0 +1,63 @@
import {InputNumber} from 'antd'
import {InputNumberProps} from 'antd/lib/input-number'
import React, {useEffect, useState} from 'react'
import styled from 'styled-components'
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
import {transparentize} from './color.change'
interface Props extends InputNumberProps {
design: FormDesignFragment
}
export const StyledNumberInput: React.FC<Props> = ({design, children, ...props}) => {
const [Field, setField] = useState()
useEffect(() => {
setField(
styled(InputNumber)`
color: ${design.colors.answerColor};
border-color: ${design.colors.answerColor};
background: none !important;
border-right: none;
border-top: none;
border-left: none;
border-radius: 0;
width: 100%;
:focus {
outline: ${design.colors.answerColor} auto 5px
}
:hover,
:active {
border-color: ${design.colors.answerColor};
}
&.ant-input-number {
box-shadow: none
}
input {
background: none !important;
color: ${design.colors.answerColor};
::placeholder {
color: ${transparentize(design.colors.answerColor, 60)}
}
}
.anticon {
color: ${design.colors.answerColor};
}
`
)
}, [design])
if (!Field) {
return null
}
return (
<Field {...props}>{children}</Field>
)
}

View File

@ -0,0 +1,68 @@
import {useMutation} from '@apollo/react-hooks'
import {useCallback, useEffect, useState} from 'react'
import {
SUBMISSION_SET_FIELD_MUTATION,
SubmissionSetFieldMutationData,
SubmissionSetFieldMutationVariables
} from '../graphql/mutation/submission.set.field.mutation'
import {
SUBMISSION_START_MUTATION,
SubmissionStartMutationData,
SubmissionStartMutationVariables
} from '../graphql/mutation/submission.start.mutation'
export const useSubmission = (formId: string) => {
const [submission, setSubmission] = useState<{ id: string, token: string }>()
const [start] = useMutation<SubmissionStartMutationData, SubmissionStartMutationVariables>(SUBMISSION_START_MUTATION)
const [save] = useMutation<SubmissionSetFieldMutationData, SubmissionSetFieldMutationVariables>(SUBMISSION_SET_FIELD_MUTATION)
useEffect(() => {
(async () => {
const token = '123' // TODO generate secure token
const {data} = await start({
variables: {
form: formId,
submission: {
token,
device: {
name: '',
type: ''
}
}
}
})
setSubmission({
id: data.submission.id,
token,
})
})()
}, [formId])
const setField = useCallback(async (fieldId: string, data: any) => {
console.log('just save', fieldId, data)
await save({
variables: {
submission: submission.id,
field: {
token: submission.token,
field: fieldId,
data: JSON.stringify(data)
}
}
})
}, [submission])
const finish = useCallback(() => {
console.log('finish submission!!', formId)
}, [submission])
console.log('submission saver :D', formId)
return {
setField,
finish,
}
}

View File

@ -0,0 +1,16 @@
import {gql} from 'apollo-boost'
export interface SubmissionFragment {
id?: string
title: string
created: string
}
export const FORM_FRAGMENT = gql`
fragment Submission on Submission {
id
title
language
showFooter
}
`

View File

@ -0,0 +1,26 @@
import {gql} from 'apollo-boost'
export interface SubmissionSetFieldMutationData {
submission: {
id: string
percentageComplete: string
}
}
export interface SubmissionSetFieldMutationVariables {
submission: string
field: {
token: string
field: string
data: string
}
}
export const SUBMISSION_SET_FIELD_MUTATION = gql`
mutation start($submission: ID!,$field: SubmissionSetFieldInput!) {
submission: submissionSetField(submission: $submission, field: $field) {
id
percentageComplete
}
}
`

View File

@ -0,0 +1,28 @@
import {gql} from 'apollo-boost'
export interface SubmissionStartMutationData {
submission: {
id: string
percentageComplete: string
}
}
export interface SubmissionStartMutationVariables {
form: string
submission: {
token: string
device: {
type: string
name: string
}
}
}
export const SUBMISSION_START_MUTATION = gql`
mutation start($form: ID!,$submission: SubmissionStartInput!) {
submission: submissionStart(form: $form, submission: $submission) {
id
percentageComplete
}
}
`

View File

@ -1,6 +1,6 @@
import {gql} from 'apollo-boost'
export interface PagerFormEntryQueryData {
export interface AdminPagerFormEntryQueryData {
id: string
created: string
lastModified?: string
@ -14,9 +14,9 @@ export interface PagerFormEntryQueryData {
}
}
export interface PagerFormQueryData {
export interface AdminPagerFormQueryData {
pager: {
entries: PagerFormEntryQueryData[]
entries: AdminPagerFormEntryQueryData[]
total: number
limit: number
@ -24,12 +24,12 @@ export interface PagerFormQueryData {
}
}
export interface PagerFormQueryVariables {
export interface AdminPagerFormQueryVariables {
start?: number
limit?: number
}
export const PAGER_FORM_QUERY = gql`
export const ADMIN_PAGER_FORM_QUERY = gql`
query pager($start: Int, $limit: Int){
pager: listForms(start: $start, limit: $limit) {
entries {

View File

@ -0,0 +1,68 @@
import {gql} from 'apollo-boost'
export interface AdminPagerSubmissionFormQueryData {
id: string
title: string
isLive: boolean
}
export interface AdminPagerSubmissionEntryQueryData {
id: string
created: string
lastModified?: string
percentageComplete: number
timeElapsed: number
geoLocation: {
country: string
}
}
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
}
fields {
id
value
type
}
}
total
limit
start
}
}
`

View File

@ -0,0 +1,121 @@
import {EditOutlined} from '@ant-design/icons/lib'
import {useQuery} from '@apollo/react-hooks'
import {Button, Space, Table} from 'antd'
import {PaginationProps} from 'antd/es/pagination'
import {DateTime} from 'components/date.time'
import Structure from 'components/structure'
import {TimeAgo} from 'components/time.ago'
import {withAuth} from 'components/with.auth'
import {NextPage} from 'next'
import Link from 'next/link'
import {useRouter} from 'next/router'
import React, {useState} from 'react'
import {
ADMIN_PAGER_SUBMISSION_QUERY,
AdminPagerSubmissionEntryQueryData,
AdminPagerSubmissionFormQueryData,
AdminPagerSubmissionQueryData,
AdminPagerSubmissionQueryVariables
} from '../../../../graphql/query/admin.pager.submission.query'
const Submissions: NextPage = () => {
const router = useRouter()
const [pagination, setPagination] = useState<PaginationProps>({
pageSize: 25,
})
const [form, setForm] = useState<AdminPagerSubmissionFormQueryData>()
const [entries, setEntries] = useState<AdminPagerSubmissionEntryQueryData[]>()
const {loading, refetch} = useQuery<AdminPagerSubmissionQueryData, AdminPagerSubmissionQueryVariables>(ADMIN_PAGER_SUBMISSION_QUERY, {
variables: {
form: router.query.id as string,
limit: pagination.pageSize,
start: pagination.current * pagination.pageSize || 0
},
onCompleted: ({pager, form}) => {
setPagination({
...pagination,
total: pager.total,
})
setForm(form)
setEntries(pager.entries)
}
})
const columns = [
{
title: 'Values',
dataIndex: 'fields',
render: fields => <pre>{JSON.stringify(fields, null, 2)}</pre>
},
{
title: 'Created',
dataIndex: 'created',
render: date => <DateTime date={date} />
},
{
title: 'Last Change',
dataIndex: 'lastModified',
render: date => <TimeAgo date={date} />
},
{
render: row => {
return (
<Space>
<Link
href={'/admin/forms/[id]'}
as={`/admin/forms/${row.id}`}
>
<Button type={'primary'}><EditOutlined /></Button>
</Link>
</Space>
)
}
},
]
return (
<Structure
title={loading ? 'Loading Submissions' : 'Submissions'}
selected={'forms'}
loading={loading}
breadcrumbs={[
{ href: '/admin', name: 'Home' },
{ href: '/admin/forms', name: 'Form' },
{ href: '/admin/forms/[id]', name: loading || !form ? 'Loading Form' : `Edit Form "${form.title}"`, as: `/admin/forms/${router.query.id}` },
]}
padded={false}
extra={[
<Link
key={'edit'}
href={'/admin/forms/[id]'}
as={`/admin/forms/${router.query.id}`}
>
<Button>
Edit
</Button>
</Link>,
<Button
key={'web'}
href={`/forms/${router.query.id}`}
target={'_blank'}
type={'primary'}
>
Open Form
</Button>,
]}
>
<Table
columns={columns}
dataSource={entries}
rowKey={'id'}
pagination={pagination}
onChange={next => {
setPagination(pagination)
}}
/>
</Structure>
)
}
export default withAuth(Submissions, ['admin'])

View File

@ -1,4 +1,4 @@
import {DeleteOutlined, EditOutlined, GlobalOutlined} from '@ant-design/icons/lib'
import {DeleteOutlined, EditOutlined, GlobalOutlined, UnorderedListOutlined} from '@ant-design/icons/lib'
import {useQuery} from '@apollo/react-hooks'
import {Button, Popconfirm, Space, Table, Tooltip} from 'antd'
import {PaginationProps} from 'antd/es/pagination'
@ -8,11 +8,11 @@ import Structure from 'components/structure'
import {TimeAgo} from 'components/time.ago'
import {withAuth} from 'components/with.auth'
import {
PAGER_FORM_QUERY,
PagerFormEntryQueryData,
PagerFormQueryData,
PagerFormQueryVariables
} from 'graphql/query/pager.form.query'
ADMIN_PAGER_FORM_QUERY,
AdminPagerFormEntryQueryData,
AdminPagerFormQueryData,
AdminPagerFormQueryVariables
} from 'graphql/query/admin.pager.form.query'
import {NextPage} from 'next'
import Link from 'next/link'
import React, {useState} from 'react'
@ -21,9 +21,8 @@ const Index: NextPage = () => {
const [pagination, setPagination] = useState<PaginationProps>({
pageSize: 25,
})
const [entries, setEntries] = useState<PagerFormEntryQueryData[]>()
// TODO limit forms if user is only admin!
const {loading, refetch} = useQuery<PagerFormQueryData, PagerFormQueryVariables>(PAGER_FORM_QUERY, {
const [entries, setEntries] = useState<AdminPagerFormEntryQueryData[]>()
const {loading, refetch} = useQuery<AdminPagerFormQueryData, AdminPagerFormQueryVariables>(ADMIN_PAGER_FORM_QUERY, {
variables: {
limit: pagination.pageSize,
start: pagination.current * pagination.pageSize || 0
@ -80,6 +79,15 @@ const Index: NextPage = () => {
render: row => {
return (
<Space>
<Link
href={'/admin/forms/[id]/submissions'}
as={`/admin/forms/${row.id}/submissions`}
>
<Tooltip title={'Show Submissions'}>
<Button><UnorderedListOutlined /></Button>
</Tooltip>
</Link>
<Link
href={'/admin/forms/[id]'}
as={`/admin/forms/${row.id}`}

View File

@ -10,6 +10,7 @@ import React, {useState} from 'react'
import Swiper from 'react-id-swiper'
import {ReactIdSwiperProps} from 'react-id-swiper/lib/types'
import * as OriginalSwiper from 'swiper'
import {useSubmission} from '../../../components/use.submission'
interface Props {
id: string
@ -18,6 +19,7 @@ interface Props {
const Index: NextPage<Props> = ({id}) => {
const windowSize = useWindowSize()
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
const submission = useSubmission(id)
const {loading, data, error} = useQuery<FormQueryData, FormQueryVariables>(FORM_QUERY, {
variables: {
@ -70,11 +72,18 @@ const Index: NextPage<Props> = ({id}) => {
next={goNext}
prev={goPrev}
/> : undefined,
...data.form.fields.map(field => (
...data.form.fields.map((field, i) => (
<Field
key={field.id}
field={field}
design={design}
save={values => {
submission.setField(field.id, values[field.id])
if (data.form.fields.length === i - 1) {
submission.finish()
}
}}
next={goNext}
prev={goPrev}
/>
@ -84,7 +93,7 @@ const Index: NextPage<Props> = ({id}) => {
type={'end'}
page={data.form.endPage}
design={design}
next={goNext}
next={submission.finish}
prev={goPrev}
/> : undefined
].filter(e => !!e)}