mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2025-12-13 01:35:51 +00:00
add submissions, add todo's
This commit is contained in:
parent
4336536840
commit
5cc1565751
58
components/form/admin/submission.values.tsx
Normal file
58
components/form/admin/submission.values.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import {Descriptions, Table} from 'antd'
|
||||
import {ColumnsType} from 'antd/lib/table/interface'
|
||||
import React from 'react'
|
||||
import {
|
||||
AdminPagerSubmissionEntryFieldQueryData,
|
||||
AdminPagerSubmissionEntryQueryData,
|
||||
AdminPagerSubmissionFormQueryData
|
||||
} from '../../../graphql/query/admin.pager.submission.query'
|
||||
|
||||
interface Props {
|
||||
form: AdminPagerSubmissionFormQueryData
|
||||
submission: AdminPagerSubmissionEntryQueryData
|
||||
}
|
||||
|
||||
export const SubmissionValues: React.FC<Props> = props => {
|
||||
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
|
||||
{
|
||||
title: 'Field',
|
||||
render: (row: AdminPagerSubmissionEntryFieldQueryData) => {
|
||||
|
||||
if (row.field) {
|
||||
return `${row.field.title}${row.field.required ? '*' : ''}`
|
||||
}
|
||||
|
||||
return `${row.id}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
render: row => {
|
||||
try {
|
||||
const data = JSON.parse(row.value)
|
||||
|
||||
return data.value
|
||||
} catch (e) {
|
||||
return row.value
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={'Submission'}>
|
||||
<Descriptions.Item label="Country">{props.submission.geoLocation.country}</Descriptions.Item>
|
||||
<Descriptions.Item label="City">{props.submission.geoLocation.city}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device Type">{props.submission.device.type}</Descriptions.Item>
|
||||
<Descriptions.Item label="Device Name">{props.submission.device.name}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={props.submission.fields}
|
||||
rowKey={'id'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -17,6 +17,7 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
|
||||
format={'YYYY-MM-DD'}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* TODO add options
|
||||
<Form.Item
|
||||
label={'Min Date'}
|
||||
name={[field.name, 'min']}
|
||||
@ -35,6 +36,7 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Form, Input} from 'antd'
|
||||
import {Form, Rate} from 'antd'
|
||||
import React from 'react'
|
||||
import {AdminFieldTypeProps} from './type.props'
|
||||
|
||||
@ -10,8 +10,12 @@ export const RatingType: React.FC<AdminFieldTypeProps> = props => {
|
||||
label={'Default Value'}
|
||||
name={[props.field.name, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
extra={'Click again to remove default value'}
|
||||
>
|
||||
<Input />
|
||||
<Rate
|
||||
allowHalf
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -51,10 +51,6 @@ export const Field: React.FC<Props> = ({field, save, design, children, next, pre
|
||||
padding: 32,
|
||||
justifyContent: 'flex-end',
|
||||
}}>
|
||||
<pre style={{
|
||||
opacity: 0.3
|
||||
}}>{JSON.stringify(field, null, 2)}</pre>
|
||||
|
||||
<StyledH1 design={design} type={'question'}>{field.title}</StyledH1>
|
||||
{field.description && <StyledP design={design} type={'question'}>{field.description}</StyledP>}
|
||||
|
||||
|
||||
@ -7,12 +7,10 @@ export const DropdownType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import {Form, Input} from 'antd'
|
||||
import React from 'react'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const HiddenType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,7 +2,6 @@ import React from 'react'
|
||||
import {DateType} from './date.type'
|
||||
import {DropdownType} from './dropdown.type'
|
||||
import {EmailType} from './email.type'
|
||||
import {HiddenType} from './hidden.type'
|
||||
import {LinkType} from './link.type'
|
||||
import {NumberType} from './number.type'
|
||||
import {RadioType} from './radio.type'
|
||||
@ -23,7 +22,6 @@ export const fieldTypes: {
|
||||
'dropdown': DropdownType,
|
||||
'rating': RatingType,
|
||||
'radio': RadioType,
|
||||
'hidden': HiddenType,
|
||||
'yes_no': YesNoType,
|
||||
'number': NumberType,
|
||||
}
|
||||
|
||||
@ -8,12 +8,10 @@ export const RadioType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Form, Input} from 'antd'
|
||||
import {Form, Rate} from 'antd'
|
||||
import React from 'react'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
@ -8,14 +8,12 @@ export const RatingType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
<Rate allowHalf defaultValue={parseFloat(field.value)} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
import {Form, Input} from 'antd'
|
||||
import {Form} from 'antd'
|
||||
import React from 'react'
|
||||
import {StyledTextareaInput} from '../../styled/textarea.input'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const TextareaType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
export const TextareaType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input.TextArea autoSize />
|
||||
<StyledTextareaInput
|
||||
design={design}
|
||||
allowClear
|
||||
autoSize
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,20 +1,17 @@
|
||||
import {Form, Input} from 'antd'
|
||||
import {Form, Switch} from 'antd'
|
||||
import React from 'react'
|
||||
import {FieldTypeProps} from './type.props'
|
||||
|
||||
export const YesNoType: React.FC<FieldTypeProps> = ({field}) => {
|
||||
// TODO add switch
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
label={'Default Value'}
|
||||
name={[field.id, 'value']}
|
||||
rules={[
|
||||
{ required: field.required, message: 'Please provide Information' },
|
||||
]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {HomeOutlined, MessageOutlined, TeamOutlined} from '@ant-design/icons'
|
||||
import {UserOutlined} from '@ant-design/icons/lib'
|
||||
import React from 'react'
|
||||
|
||||
export interface SideMenuElement {
|
||||
@ -18,6 +19,12 @@ export const sideMenu: SideMenuElement[] = [
|
||||
href: '/admin',
|
||||
icon: <HomeOutlined />,
|
||||
},
|
||||
{
|
||||
key: 'profile',
|
||||
name: 'Profile',
|
||||
href: '/admin/profile',
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
{
|
||||
key: 'public',
|
||||
name: 'Forms',
|
||||
|
||||
@ -7,6 +7,7 @@ import {useRouter} from 'next/router'
|
||||
import React, {FunctionComponent} from 'react'
|
||||
import {sideMenu, SideMenuElement} from './sidemenu'
|
||||
import {useWindowSize} from './use.window.size'
|
||||
import {clearAuth} from './with.auth'
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
|
||||
@ -117,7 +118,8 @@ const Structure: FunctionComponent<Props> = (props) => {
|
||||
}
|
||||
|
||||
const signOut = async (): Promise<void> => {
|
||||
// TODO sign out
|
||||
await clearAuth()
|
||||
router.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
@ -145,7 +147,7 @@ const Structure: FunctionComponent<Props> = (props) => {
|
||||
<Dropdown
|
||||
overlay={(
|
||||
<Menu>
|
||||
<Menu.Item onClick={(): void => console.log('profile??')}>Profile</Menu.Item>
|
||||
<Menu.Item onClick={() => router.push('/admin/profile')}>Profile</Menu.Item>
|
||||
<Menu.Divider/>
|
||||
<Menu.Item onClick={signOut}>Logout</Menu.Item>
|
||||
</Menu>
|
||||
|
||||
@ -10,20 +10,20 @@ interface Props extends ButtonProps {
|
||||
color: any
|
||||
}
|
||||
|
||||
export const StyledButton: React.FC<Props> = ({background, highlight, color, children, ...props}) => {
|
||||
const StyledButton = styled(Button)`
|
||||
background: ${background};
|
||||
color: ${color};
|
||||
border-color: ${darken(background, 10)};
|
||||
|
||||
:hover {
|
||||
color: ${highlight};
|
||||
background-color: ${lighten(background, 10)};
|
||||
border-color: ${darken(highlight, 10)};
|
||||
}
|
||||
`
|
||||
const Styled = styled(Button)`
|
||||
background: ${props => props.background};
|
||||
color: ${props => props.color};
|
||||
border-color: ${props => darken(props.background, 10)};
|
||||
|
||||
:hover {
|
||||
color: ${props => props.highlight};
|
||||
background-color: ${props => lighten(props.background, 10)};
|
||||
border-color: ${props => darken(props.highlight, 10)};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledButton: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<StyledButton {...props}>{children}</StyledButton>
|
||||
<Styled {...props}>{children}</Styled>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,60 +1,50 @@
|
||||
import {DatePicker} from 'antd'
|
||||
import {PickerProps} from 'antd/lib/date-picker/generatePicker'
|
||||
import {Moment} from 'moment'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||
import {transparentize} from './color.change'
|
||||
|
||||
type Props = { design: FormDesignFragment } & PickerProps<Moment>
|
||||
|
||||
export const StyledDateInput: React.FC<Props> = ({design, children, ...props}) => {
|
||||
const [Field, setField] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
setField(
|
||||
styled(DatePicker)`
|
||||
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%;
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-picker {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.ant-picker-clear {
|
||||
background: none;
|
||||
}
|
||||
|
||||
input {
|
||||
color: ${design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${transparentize(design.colors.answerColor, 60)}
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
)
|
||||
}, [design])
|
||||
|
||||
if (!Field) {
|
||||
return null
|
||||
const Field = styled(DatePicker)`
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-picker {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.ant-picker-clear {
|
||||
background: none;
|
||||
}
|
||||
|
||||
input {
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${props => transparentize(props.design.colors.answerColor, 60)}
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledDateInput: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Field {...props}>{children}</Field>
|
||||
)
|
||||
|
||||
@ -7,11 +7,11 @@ interface Props {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
export const StyledH1: React.FC<Props> = ({design, type, children, ...props}) => {
|
||||
const Header = styled.h1`
|
||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
||||
`
|
||||
const Header = styled.h1`
|
||||
color: ${props => props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor}
|
||||
`
|
||||
|
||||
export const StyledH1: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Header {...props}>{children}</Header>
|
||||
)
|
||||
|
||||
@ -6,12 +6,11 @@ interface Props {
|
||||
type: 'question' | 'answer'
|
||||
design: FormDesignFragment
|
||||
}
|
||||
const Header = styled.h2`
|
||||
color: ${props => props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor}
|
||||
`
|
||||
|
||||
export const StyledH2: React.FC<Props> = ({design, type, children, ...props}) => {
|
||||
const Header = styled.h2`
|
||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
||||
`
|
||||
|
||||
export const StyledH2: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Header {...props}>{children}</Header>
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {Input} from 'antd'
|
||||
import {InputProps} from 'antd/lib/input/Input'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||
import {transparentize} from './color.change'
|
||||
@ -9,53 +9,43 @@ interface Props extends InputProps{
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
export const StyledInput: React.FC<Props> = ({design, children, ...props}) => {
|
||||
const [Field, setField] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
setField(
|
||||
styled(Input)`
|
||||
color: ${design.colors.answerColor};
|
||||
border-color: ${design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
|
||||
:focus {
|
||||
outline: ${design.colors.answerColor} auto 5px
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
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
|
||||
const Field = styled(Input)`
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
|
||||
:focus {
|
||||
outline: ${props => props.design.colors.answerColor} auto 5px
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${props => transparentize(props.design.colors.answerColor, 60)}
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledInput: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Field {...props}>{children}</Field>
|
||||
)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {InputNumber} from 'antd'
|
||||
import {InputNumberProps} from 'antd/lib/input-number'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||
import {transparentize} from './color.change'
|
||||
@ -9,54 +9,44 @@ 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
|
||||
const Field = styled(InputNumber)`
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
outline: ${props => props.design.colors.answerColor} auto 5px
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-input-number {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${props => transparentize(props.design.colors.answerColor, 60)}
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledNumberInput: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Field {...props}>{children}</Field>
|
||||
)
|
||||
|
||||
@ -7,11 +7,11 @@ interface Props {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
export const StyledP: React.FC<Props> = ({design, type, children, ...props}) => {
|
||||
const Paragraph = styled.p`
|
||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
||||
`
|
||||
const Paragraph = styled.p`
|
||||
color: ${props => props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor}
|
||||
`
|
||||
|
||||
export const StyledP: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Paragraph {...props}>{children}</Paragraph>
|
||||
)
|
||||
|
||||
50
components/styled/textarea.input.tsx
Normal file
50
components/styled/textarea.input.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import {Input} from 'antd'
|
||||
import {TextAreaProps} from 'antd/lib/input/TextArea'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||
import {transparentize} from './color.change'
|
||||
|
||||
interface Props extends TextAreaProps {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
const Field = styled(Input.TextArea)`
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-radius: 0;
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${props => transparentize(props.design.colors.answerColor, 60)}
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${props => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledTextareaInput: React.FC<Props> = ({children, ...props}) => {
|
||||
return (
|
||||
<Field {...props}>{children}</Field>
|
||||
)
|
||||
}
|
||||
@ -19,7 +19,7 @@ export const useSubmission = (formId: string) => {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const token = '123' // TODO generate secure token
|
||||
const token = [...Array(40)].map(() => Math.random().toString(36)[2]).join('')
|
||||
|
||||
const {data} = await start({
|
||||
variables: {
|
||||
@ -27,8 +27,8 @@ export const useSubmission = (formId: string) => {
|
||||
submission: {
|
||||
token,
|
||||
device: {
|
||||
name: '',
|
||||
type: ''
|
||||
name: /Mobi/i.test(window.navigator.userAgent) ? 'mobile' : 'desktop',
|
||||
type: window.navigator.userAgent
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,8 +59,6 @@ export const useSubmission = (formId: string) => {
|
||||
console.log('finish submission!!', formId)
|
||||
}, [submission])
|
||||
|
||||
console.log('submission saver :D', formId)
|
||||
|
||||
return {
|
||||
setField,
|
||||
finish,
|
||||
|
||||
108
components/user/admin/base.data.tab.tsx
Normal file
108
components/user/admin/base.data.tab.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import {Form, Input, Select, Tabs} from 'antd'
|
||||
import {TabPaneProps} from 'antd/lib/tabs'
|
||||
import React from 'react'
|
||||
import {languages} from '../../../i18n'
|
||||
|
||||
export const BaseDataTab: React.FC<TabPaneProps> = props => {
|
||||
return (
|
||||
<Tabs.TabPane {...props}>
|
||||
<Form.Item
|
||||
label="Username"
|
||||
name={['user', 'username']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please provide a Username',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Email"
|
||||
name={['user', 'email']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please provide an email',
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: 'Must be a valid email',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type={'email'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Role"
|
||||
name={['user', 'roles']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a role',
|
||||
},
|
||||
]}
|
||||
getValueFromEvent={e => {
|
||||
switch (e) {
|
||||
case 'superuser':
|
||||
return ['user', 'admin', 'superuser']
|
||||
case 'admin':
|
||||
return ['user', 'admin']
|
||||
default:
|
||||
return ['user']
|
||||
}
|
||||
}}
|
||||
getValueProps={v => {
|
||||
let role = 'user'
|
||||
|
||||
if (v && v.includes('superuser')) {
|
||||
role = 'superuser'
|
||||
} else if (v && v.includes('admin')) {
|
||||
role = 'admin'
|
||||
}
|
||||
|
||||
return {
|
||||
value: role
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Select>
|
||||
{['user', 'admin', 'superuser'].map(role => <Select.Option value={role} key={role}>{role.toUpperCase()}</Select.Option> )}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Language"
|
||||
name={['user', 'language']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a Language',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="First Name"
|
||||
name={['user', 'firstName']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Last Name"
|
||||
name={['user', 'lastName']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
</Tabs.TabPane>
|
||||
)
|
||||
}
|
||||
34
components/user/role.tsx
Normal file
34
components/user/role.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import {Tag} from "antd"
|
||||
import React, {CSSProperties} from 'react'
|
||||
|
||||
interface Props {
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export const UserRole: React.FC<Props> = props => {
|
||||
let color
|
||||
let level = 'unknown'
|
||||
const css: CSSProperties = {}
|
||||
|
||||
|
||||
if (props.roles.includes('superuser')) {
|
||||
color = 'red'
|
||||
level = 'superuser'
|
||||
} else if (props.roles.includes('admin')) {
|
||||
color = 'orange'
|
||||
level = 'admin'
|
||||
} else if (props.roles.includes('user')) {
|
||||
color = '#F0F0F0'
|
||||
css.color = '#AAA'
|
||||
level = 'user'
|
||||
}
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color={color}
|
||||
style={css}
|
||||
>
|
||||
{level.toUpperCase()}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@ -5,6 +5,13 @@ import React, {useEffect, useState} from 'react'
|
||||
import {ME_QUERY, MeQueryData} from '../graphql/query/me.query'
|
||||
import {LoadingPage} from './loading.page'
|
||||
|
||||
export const clearAuth = async () => {
|
||||
localStorage.removeItem('access')
|
||||
localStorage.removeItem('refresh')
|
||||
|
||||
// TODO logout on server!
|
||||
}
|
||||
|
||||
export const setAuth = (access, refresh) => {
|
||||
localStorage.setItem('access', access)
|
||||
localStorage.setItem('refresh', refresh)
|
||||
|
||||
26
graphql/fragment/admin.profile.fragment.ts
Normal file
26
graphql/fragment/admin.profile.fragment.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {gql} from 'apollo-boost'
|
||||
|
||||
export interface AdminProfileFragment {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
language: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
created: string
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export const ADMIN_PROFILE_FRAGMENT = gql`
|
||||
fragment AdminProfile on Profile {
|
||||
id
|
||||
email
|
||||
username
|
||||
language
|
||||
firstName
|
||||
lastName
|
||||
roles
|
||||
created
|
||||
lastModified
|
||||
}
|
||||
`
|
||||
@ -1,7 +1,27 @@
|
||||
import {gql} from 'apollo-boost'
|
||||
|
||||
export const ADMIN_FORM_FRAGMENT = gql`
|
||||
export interface AdminUserFragment {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
language: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
roles: string[]
|
||||
created: string
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export const ADMIN_USER_FRAGMENT = gql`
|
||||
fragment AdminUser on User {
|
||||
id
|
||||
email
|
||||
username
|
||||
language
|
||||
firstName
|
||||
lastName
|
||||
roles
|
||||
created
|
||||
lastModified
|
||||
}
|
||||
`
|
||||
|
||||
19
graphql/mutation/admin.form.delete.mutation.ts
Normal file
19
graphql/mutation/admin.form.delete.mutation.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
`
|
||||
21
graphql/mutation/admin.profile.update.mutation.ts
Normal file
21
graphql/mutation/admin.profile.update.mutation.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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}
|
||||
`
|
||||
19
graphql/mutation/admin.user.delete.mutation.ts
Normal file
19
graphql/mutation/admin.user.delete.mutation.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
`
|
||||
20
graphql/mutation/admin.user.update.mutation.ts
Normal file
20
graphql/mutation/admin.user.update.mutation.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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}
|
||||
`
|
||||
@ -1,11 +1,24 @@
|
||||
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
|
||||
@ -14,7 +27,14 @@ export interface AdminPagerSubmissionEntryQueryData {
|
||||
timeElapsed: number
|
||||
geoLocation: {
|
||||
country: string
|
||||
city: string
|
||||
}
|
||||
device: {
|
||||
type: string
|
||||
name: string
|
||||
}
|
||||
|
||||
fields: AdminPagerSubmissionEntryFieldQueryData[]
|
||||
}
|
||||
|
||||
export interface AdminPagerSubmissionQueryData {
|
||||
@ -52,12 +72,22 @@ export const ADMIN_PAGER_SUBMISSION_QUERY = gql`
|
||||
timeElapsed
|
||||
geoLocation {
|
||||
country
|
||||
city
|
||||
}
|
||||
device {
|
||||
type
|
||||
name
|
||||
}
|
||||
|
||||
fields {
|
||||
id
|
||||
value
|
||||
type
|
||||
|
||||
field {
|
||||
title
|
||||
required
|
||||
}
|
||||
}
|
||||
}
|
||||
total
|
||||
|
||||
41
graphql/query/admin.pager.user.query.ts
Normal file
41
graphql/query/admin.pager.user.query.ts
Normal file
@ -0,0 +1,41 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
`
|
||||
19
graphql/query/admin.profile.query.ts
Normal file
19
graphql/query/admin.profile.query.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {gql} from 'apollo-boost'
|
||||
import {ADMIN_PROFILE_FRAGMENT, AdminProfileFragment} from '../fragment/admin.profile.fragment'
|
||||
|
||||
export interface AdminProfileQueryData {
|
||||
user: AdminProfileFragment
|
||||
}
|
||||
|
||||
export interface AdminProfileQueryVariables {
|
||||
}
|
||||
|
||||
export const ADMIN_PROFILE_QUERY = gql`
|
||||
query profile {
|
||||
user:me {
|
||||
...AdminProfile
|
||||
}
|
||||
}
|
||||
|
||||
${ADMIN_PROFILE_FRAGMENT}
|
||||
`
|
||||
30
graphql/query/admin.statistic.query.ts
Normal file
30
graphql/query/admin.statistic.query.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {gql} from 'apollo-boost'
|
||||
|
||||
export interface AdminStatisticQueryData {
|
||||
forms: {
|
||||
total: number
|
||||
}
|
||||
submissions: {
|
||||
total: number
|
||||
}
|
||||
users: {
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminStatisticQueryVariables {
|
||||
}
|
||||
|
||||
export const ADMIN_STATISTIC_QUERY = gql`
|
||||
query {
|
||||
forms: getFormStatistic {
|
||||
total
|
||||
}
|
||||
submissions: getSubmissionStatistic {
|
||||
total
|
||||
}
|
||||
users: getUserStatistic {
|
||||
total
|
||||
}
|
||||
}
|
||||
`
|
||||
20
graphql/query/admin.user.query.ts
Normal file
20
graphql/query/admin.user.query.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {gql} from 'apollo-boost'
|
||||
import {ADMIN_USER_FRAGMENT, AdminUserFragment} from '../fragment/admin.user.fragment'
|
||||
|
||||
export interface AdminUserQueryData {
|
||||
user: AdminUserFragment
|
||||
}
|
||||
|
||||
export interface AdminUserQueryVariables {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const ADMIN_USER_QUERY = gql`
|
||||
query user($id: ID!){
|
||||
user:getUserById(id: $id) {
|
||||
...AdminUser
|
||||
}
|
||||
}
|
||||
|
||||
${ADMIN_USER_FRAGMENT}
|
||||
`
|
||||
@ -4,7 +4,6 @@ const p = require('./package.json')
|
||||
const version = p.version;
|
||||
|
||||
module.exports = withImages({
|
||||
|
||||
publicRuntimeConfig: {
|
||||
endpoint: process.env.API_HOST || '/graphql',
|
||||
version,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ohmyform-react",
|
||||
"version": "0.1.0",
|
||||
"version": "0.9.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start:dev": "next dev -p 4000",
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
} from 'graphql/mutation/admin.form.update.mutation'
|
||||
import {ADMIN_FORM_QUERY, AdminFormQueryData, AdminFormQueryVariables} from 'graphql/query/admin.form.query'
|
||||
import {NextPage} from 'next'
|
||||
import Link from 'next/link'
|
||||
import {useRouter} from 'next/router'
|
||||
import React, {useState} from 'react'
|
||||
|
||||
@ -41,7 +42,6 @@ const Index: NextPage = () => {
|
||||
|
||||
const save = async (formData: AdminFormQueryData) => {
|
||||
setSaving(true)
|
||||
console.log('try to save form!', formData)
|
||||
|
||||
formData.form.fields = formData.form.fields.filter(e => e && e.type)
|
||||
|
||||
@ -74,6 +74,13 @@ const Index: NextPage = () => {
|
||||
{ href: '/admin/forms', name: 'Form' },
|
||||
]}
|
||||
extra={[
|
||||
<Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${router.query.id}/submissions`}>
|
||||
<Button
|
||||
key={'submissions'}
|
||||
>
|
||||
Submissions
|
||||
</Button>
|
||||
</Link>,
|
||||
<Button
|
||||
key={'save'}
|
||||
onClick={form.submit}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import {EditOutlined} from '@ant-design/icons/lib'
|
||||
import {useQuery} from '@apollo/react-hooks'
|
||||
import {Button, Space, Table} from 'antd'
|
||||
import {Button, Progress, Table} from 'antd'
|
||||
import {PaginationProps} from 'antd/es/pagination'
|
||||
import {ColumnsType} from 'antd/lib/table/interface'
|
||||
import {DateTime} from 'components/date.time'
|
||||
import Structure from 'components/structure'
|
||||
import {TimeAgo} from 'components/time.ago'
|
||||
import {withAuth} from 'components/with.auth'
|
||||
import dayjs from 'dayjs'
|
||||
import {NextPage} from 'next'
|
||||
import Link from 'next/link'
|
||||
import {useRouter} from 'next/router'
|
||||
import React, {useState} from 'react'
|
||||
import {SubmissionValues} from '../../../../components/form/admin/submission.values'
|
||||
import {
|
||||
ADMIN_PAGER_SUBMISSION_QUERY,
|
||||
AdminPagerSubmissionEntryQueryData,
|
||||
@ -29,7 +31,7 @@ const Submissions: NextPage = () => {
|
||||
variables: {
|
||||
form: router.query.id as string,
|
||||
limit: pagination.pageSize,
|
||||
start: pagination.current * pagination.pageSize || 0
|
||||
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0
|
||||
},
|
||||
onCompleted: ({pager, form}) => {
|
||||
setPagination({
|
||||
@ -41,11 +43,22 @@ const Submissions: NextPage = () => {
|
||||
}
|
||||
})
|
||||
|
||||
const columns = [
|
||||
const columns:ColumnsType<AdminPagerSubmissionEntryQueryData> = [
|
||||
{
|
||||
title: 'Values',
|
||||
dataIndex: 'fields',
|
||||
render: fields => <pre>{JSON.stringify(fields, null, 2)}</pre>
|
||||
title: 'Progress',
|
||||
render: (row: AdminPagerSubmissionEntryQueryData) => {
|
||||
let status: any = 'active'
|
||||
|
||||
if (row.percentageComplete >= 1) {
|
||||
status = 'success'
|
||||
} else if (dayjs().diff(dayjs(row.lastModified), 'hour') > 4) {
|
||||
status = 'exception'
|
||||
}
|
||||
|
||||
return (
|
||||
<Progress percent={Math.round(row.percentageComplete * 100)} status={status} />
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
@ -57,21 +70,6 @@ const Submissions: NextPage = () => {
|
||||
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 (
|
||||
@ -97,11 +95,11 @@ const Submissions: NextPage = () => {
|
||||
</Link>,
|
||||
<Button
|
||||
key={'web'}
|
||||
href={`/forms/${router.query.id}`}
|
||||
href={`/form/${router.query.id}`}
|
||||
target={'_blank'}
|
||||
type={'primary'}
|
||||
>
|
||||
Open Form
|
||||
Add Submission
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
@ -110,8 +108,13 @@ const Submissions: NextPage = () => {
|
||||
dataSource={entries}
|
||||
rowKey={'id'}
|
||||
pagination={pagination}
|
||||
expandable={{
|
||||
expandedRowRender: record => <SubmissionValues form={form} submission={record} />,
|
||||
rowExpandable: record => record.percentageComplete > 0,
|
||||
}}
|
||||
onChange={next => {
|
||||
setPagination(pagination)
|
||||
setPagination(next)
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
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 {useMutation, useQuery} from '@apollo/react-hooks'
|
||||
import {Button, message, Popconfirm, Space, Table, Tooltip} from 'antd'
|
||||
import {PaginationProps} from 'antd/es/pagination'
|
||||
import {ColumnsType} from 'antd/lib/table/interface'
|
||||
import {DateTime} from 'components/date.time'
|
||||
import {FormIsLive} from 'components/form/admin/is.live'
|
||||
import Structure from 'components/structure'
|
||||
@ -16,6 +17,11 @@ import {
|
||||
import {NextPage} from 'next'
|
||||
import Link from 'next/link'
|
||||
import React, {useState} from 'react'
|
||||
import {
|
||||
ADMIN_FORM_DELETE_MUTATION,
|
||||
AdminFormDeleteMutationData,
|
||||
AdminFormDeleteMutationVariables
|
||||
} from '../../../graphql/mutation/admin.form.delete.mutation'
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const [pagination, setPagination] = useState<PaginationProps>({
|
||||
@ -25,7 +31,7 @@ const Index: NextPage = () => {
|
||||
const {loading, refetch} = useQuery<AdminPagerFormQueryData, AdminPagerFormQueryVariables>(ADMIN_PAGER_FORM_QUERY, {
|
||||
variables: {
|
||||
limit: pagination.pageSize,
|
||||
start: pagination.current * pagination.pageSize || 0
|
||||
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0
|
||||
},
|
||||
onCompleted: ({pager}) => {
|
||||
setPagination({
|
||||
@ -35,12 +41,29 @@ const Index: NextPage = () => {
|
||||
setEntries(pager.entries)
|
||||
}
|
||||
})
|
||||
const [executeDelete] = useMutation<AdminFormDeleteMutationData, AdminFormDeleteMutationVariables>(ADMIN_FORM_DELETE_MUTATION)
|
||||
|
||||
const deleteForm = async (form) => {
|
||||
// TODO
|
||||
try {
|
||||
await executeDelete({
|
||||
variables: {
|
||||
id: form.id
|
||||
}
|
||||
})
|
||||
const next = entries.filter(entry => entry.id !== form.id)
|
||||
if (next.length === 0) {
|
||||
setPagination({ ...pagination, current: 1 })
|
||||
} else {
|
||||
setEntries(next)
|
||||
}
|
||||
|
||||
message.success('form deleted')
|
||||
} catch (e) {
|
||||
message.error('could not delete form')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
const columns: ColumnsType<AdminPagerFormEntryQueryData> = [
|
||||
{
|
||||
title: 'Live',
|
||||
dataIndex: 'isLive',
|
||||
@ -76,6 +99,7 @@ const Index: NextPage = () => {
|
||||
render: date => <TimeAgo date={date} />
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
render: row => {
|
||||
return (
|
||||
<Space>
|
||||
@ -96,9 +120,10 @@ const Index: NextPage = () => {
|
||||
</Link>
|
||||
|
||||
<Popconfirm
|
||||
title="Are you sure delete this form?"
|
||||
onConfirm={deleteForm}
|
||||
title="Are you sure delete this form with all submissions?"
|
||||
onConfirm={() => deleteForm(row)}
|
||||
okText={'Delete now!'}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button danger><DeleteOutlined /></Button>
|
||||
</Popconfirm>
|
||||
@ -145,7 +170,8 @@ const Index: NextPage = () => {
|
||||
rowKey={'id'}
|
||||
pagination={pagination}
|
||||
onChange={next => {
|
||||
setPagination(pagination)
|
||||
setPagination(next)
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
|
||||
@ -1,14 +1,37 @@
|
||||
import {useQuery} from '@apollo/react-hooks'
|
||||
import {Col, Row, Statistic} from 'antd'
|
||||
import Structure from 'components/structure'
|
||||
import {withAuth} from 'components/with.auth'
|
||||
import {NextPage} from 'next'
|
||||
import React from 'react'
|
||||
import {
|
||||
ADMIN_STATISTIC_QUERY,
|
||||
AdminStatisticQueryData,
|
||||
AdminStatisticQueryVariables
|
||||
} from '../../graphql/query/admin.statistic.query'
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const {data, loading} = useQuery<AdminStatisticQueryData, AdminStatisticQueryVariables>(ADMIN_STATISTIC_QUERY)
|
||||
|
||||
return (
|
||||
<Structure
|
||||
title={'Home'}
|
||||
selected={'home'}
|
||||
loading={loading}
|
||||
>
|
||||
ok!
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Statistic title="Total Forms" value={data && data.forms.total} />
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Statistic title="Total Users" value={data && data.users.total} />
|
||||
</Col>
|
||||
|
||||
<Col span={8}>
|
||||
<Statistic title="Total Submissions" value={data && data.submissions.total} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Structure>
|
||||
)
|
||||
}
|
||||
|
||||
153
pages/admin/profile.tsx
Normal file
153
pages/admin/profile.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import {useMutation, useQuery} from '@apollo/react-hooks'
|
||||
import {Button, Form, Input, message, Select} from 'antd'
|
||||
import {useForm} from 'antd/lib/form/Form'
|
||||
import {NextPage} from 'next'
|
||||
import {useRouter} from 'next/router'
|
||||
import React, {useState} from 'react'
|
||||
import {cleanInput} from '../../components/clean.input'
|
||||
import Structure from '../../components/structure'
|
||||
import {
|
||||
ADMIN_PROFILE_UPDATE_MUTATION,
|
||||
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'
|
||||
|
||||
const Profile: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const [form] = useForm()
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const {data, loading, error} = useQuery<AdminProfileQueryData, AdminProfileQueryVariables>(ADMIN_PROFILE_QUERY, {
|
||||
onCompleted: next => {
|
||||
form.setFieldsValue(next)
|
||||
},
|
||||
})
|
||||
|
||||
const [update] = useMutation<AdminProfileUpdateMutationData, AdminProfileUpdateMutationVariables>(ADMIN_PROFILE_UPDATE_MUTATION)
|
||||
|
||||
const save = async (formData: AdminUserQueryData) => {
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
const next = (await update({
|
||||
variables: cleanInput(formData),
|
||||
})).data
|
||||
|
||||
form.setFieldsValue(next)
|
||||
|
||||
message.success('Profile Updated')
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error('Could not save Profile')
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Structure
|
||||
loading={loading || saving}
|
||||
title={'Profile'}
|
||||
selected={'profile'}
|
||||
breadcrumbs={[
|
||||
{ href: '/admin', name: 'Home' },
|
||||
]}
|
||||
extra={[
|
||||
<Button
|
||||
key={'save'}
|
||||
onClick={form.submit}
|
||||
type={'primary'}
|
||||
>
|
||||
Save
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={errors => {
|
||||
message.error('Required fields are missing')
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
sm: { span: 6 },
|
||||
}}
|
||||
wrapperCol={{
|
||||
xs: { span: 24 },
|
||||
sm: { span: 18 },
|
||||
}}
|
||||
>
|
||||
<Form.Item noStyle name={['user', 'id']}><Input type={'hidden'} /></Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Username"
|
||||
name={['user', 'username']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please provide a Username',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Email"
|
||||
name={['user', 'email']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please provide an email',
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: 'Must be a valid email',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input type={'email'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Language"
|
||||
name={['user', 'language']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select a Language',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{languages.map(language => <Select.Option value={language} key={language}>{language.toUpperCase()}</Select.Option> )}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="First Name"
|
||||
name={['user', 'firstName']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Last Name"
|
||||
name={['user', 'lastName']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Structure>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
||||
@ -1,18 +1,103 @@
|
||||
import {useMutation, useQuery} from '@apollo/react-hooks'
|
||||
import {Button, Form, Input, message, Tabs} from 'antd'
|
||||
import {useForm} from 'antd/lib/form/Form'
|
||||
import Structure from 'components/structure'
|
||||
import {withAuth} from 'components/with.auth'
|
||||
import {NextPage} from 'next'
|
||||
import React from 'react'
|
||||
import {useRouter} from 'next/router'
|
||||
import React, {useState} from 'react'
|
||||
import {cleanInput} from '../../../../components/clean.input'
|
||||
import {BaseDataTab} from '../../../../components/user/admin/base.data.tab'
|
||||
import {
|
||||
ADMIN_USER_UPDATE_MUTATION,
|
||||
AdminUserUpdateMutationData,
|
||||
AdminUserUpdateMutationVariables
|
||||
} from '../../../../graphql/mutation/admin.user.update.mutation'
|
||||
import {ADMIN_USER_QUERY, AdminUserQueryData, AdminUserQueryVariables} from '../../../../graphql/query/admin.user.query'
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const router = useRouter()
|
||||
const [form] = useForm()
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const {data, loading, error} = useQuery<AdminUserQueryData, AdminUserQueryVariables>(ADMIN_USER_QUERY, {
|
||||
variables: {
|
||||
id: router.query.id as string
|
||||
},
|
||||
onCompleted: next => {
|
||||
form.setFieldsValue(next)
|
||||
},
|
||||
})
|
||||
|
||||
const [update] = useMutation<AdminUserUpdateMutationData, AdminUserUpdateMutationVariables>(ADMIN_USER_UPDATE_MUTATION)
|
||||
|
||||
const save = async (formData: AdminUserQueryData) => {
|
||||
setSaving(true)
|
||||
|
||||
console.log('data', formData)
|
||||
|
||||
|
||||
|
||||
try {
|
||||
const next = (await update({
|
||||
variables: cleanInput(formData),
|
||||
})).data
|
||||
|
||||
form.setFieldsValue(next)
|
||||
|
||||
message.success('User Updated')
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error('Could not save User')
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Structure
|
||||
title={'Edit User'}
|
||||
loading={loading || saving}
|
||||
title={loading ? 'Loading User' : `Edit User "${data.user.email}"`}
|
||||
selected={'users'}
|
||||
breadcrumbs={[
|
||||
{ href: '/admin', name: 'Home' },
|
||||
{ href: '/admin/users', name: 'Users' },
|
||||
]}
|
||||
extra={[
|
||||
<Button
|
||||
key={'save'}
|
||||
onClick={form.submit}
|
||||
type={'primary'}
|
||||
>
|
||||
Save
|
||||
</Button>,
|
||||
]}
|
||||
style={{paddingTop: 0}}
|
||||
>
|
||||
ok!
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={errors => {
|
||||
message.error('Required fields are missing')
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
sm: { span: 6 },
|
||||
}}
|
||||
wrapperCol={{
|
||||
xs: { span: 24 },
|
||||
sm: { span: 18 },
|
||||
}}
|
||||
>
|
||||
<Form.Item noStyle name={['user', 'id']}><Input type={'hidden'} /></Form.Item>
|
||||
|
||||
<Tabs>
|
||||
<BaseDataTab
|
||||
key={'base_data'}
|
||||
tab={'Base Data'}
|
||||
/>
|
||||
</Tabs>
|
||||
</Form>
|
||||
</Structure>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,17 +1,126 @@
|
||||
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 {PaginationProps} from 'antd/es/pagination'
|
||||
import {ColumnsType} from 'antd/lib/table/interface'
|
||||
import Structure from 'components/structure'
|
||||
import {withAuth} from 'components/with.auth'
|
||||
import {NextPage} from 'next'
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import React, {useState} from 'react'
|
||||
import {DateTime} from '../../../components/date.time'
|
||||
import {UserRole} from '../../../components/user/role'
|
||||
import {
|
||||
ADMIN_USER_DELETE_MUTATION,
|
||||
AdminUserDeleteMutationData,
|
||||
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 [pagination, setPagination] = useState<PaginationProps>({
|
||||
pageSize: 10,
|
||||
})
|
||||
const [entries, setEntries] = useState<AdminPagerUserEntryQueryData[]>()
|
||||
const {loading, refetch} = useQuery<AdminPagerUserQueryData, AdminPagerUserQueryVariables>(ADMIN_PAGER_USER_QUERY, {
|
||||
variables: {
|
||||
limit: pagination.pageSize,
|
||||
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0
|
||||
},
|
||||
onCompleted: ({pager}) => {
|
||||
setPagination({
|
||||
...pagination,
|
||||
total: pager.total,
|
||||
})
|
||||
setEntries(pager.entries)
|
||||
}
|
||||
})
|
||||
const [executeDelete] = useMutation<AdminUserDeleteMutationData, AdminUserDeleteMutationVariables>(ADMIN_USER_DELETE_MUTATION)
|
||||
|
||||
const deleteUser = async (form) => {
|
||||
try {
|
||||
await executeDelete({
|
||||
variables: {
|
||||
id: form.id
|
||||
}
|
||||
})
|
||||
const next = entries.filter(entry => entry.id !== form.id)
|
||||
if (next.length === 0) {
|
||||
setPagination({ ...pagination, current: 1 })
|
||||
} else {
|
||||
setEntries(next)
|
||||
}
|
||||
message.success('user deleted')
|
||||
} catch (e) {
|
||||
message.error('could not delete user')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: ColumnsType<AdminPagerUserEntryQueryData> = [
|
||||
{
|
||||
title: 'Role',
|
||||
dataIndex: 'roles',
|
||||
render: roles => <UserRole roles={roles} />
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
render: row => <Tag color={row.verifiedEmail ? 'lime' : 'volcano' }>{row.email}</Tag>
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
dataIndex: 'created',
|
||||
render: date => <DateTime date={date} />
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
render: row => {
|
||||
return (
|
||||
<Space>
|
||||
<Link
|
||||
href={'/admin/users/[id]'}
|
||||
as={`/admin/users/${row.id}`}
|
||||
>
|
||||
<Button type={'primary'}><EditOutlined /></Button>
|
||||
</Link>
|
||||
|
||||
<Popconfirm
|
||||
title="Are you sure delete this user?"
|
||||
onConfirm={() => deleteUser(row)}
|
||||
okText={'Delete now!'}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button danger><DeleteOutlined /></Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Structure
|
||||
title={'Users'}
|
||||
loading={loading}
|
||||
breadcrumbs={[
|
||||
{ href: '/admin', name: 'Home' },
|
||||
]}
|
||||
padded={false}
|
||||
>
|
||||
ok!
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={entries}
|
||||
rowKey={'id'}
|
||||
pagination={pagination}
|
||||
onChange={next => {
|
||||
setPagination(next)
|
||||
refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {useQuery} from '@apollo/react-hooks'
|
||||
import {Modal} from 'antd'
|
||||
import {ErrorPage} from 'components/error.page'
|
||||
import {Field} from 'components/form/field'
|
||||
import {FormPage} from 'components/form/page'
|
||||
import {LoadingPage} from 'components/loading.page'
|
||||
import {useWindowSize} from 'components/use.window.size'
|
||||
import {FORM_QUERY, FormQueryData, FormQueryVariables} from 'graphql/query/form.query'
|
||||
import {NextPage} from 'next'
|
||||
import React, {useState} from 'react'
|
||||
@ -17,7 +17,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const Index: NextPage<Props> = ({id}) => {
|
||||
const windowSize = useWindowSize()
|
||||
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
|
||||
const submission = useSubmission(id)
|
||||
|
||||
@ -72,22 +71,47 @@ const Index: NextPage<Props> = ({id}) => {
|
||||
next={goNext}
|
||||
prev={goPrev}
|
||||
/> : undefined,
|
||||
...data.form.fields.map((field, i) => (
|
||||
<Field
|
||||
key={field.id}
|
||||
field={field}
|
||||
design={design}
|
||||
save={values => {
|
||||
submission.setField(field.id, values[field.id])
|
||||
...data.form.fields
|
||||
.map((field, i) => {
|
||||
if (field.type === 'hidden') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (data.form.fields.length === i - 1) {
|
||||
submission.finish()
|
||||
}
|
||||
}}
|
||||
next={goNext}
|
||||
prev={goPrev}
|
||||
/>
|
||||
)),
|
||||
return (
|
||||
<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={() => {
|
||||
if (data.form.fields.length === i + 1) {
|
||||
// prevent going back!
|
||||
swiper.allowSlidePrev = true
|
||||
|
||||
if (!data.form.endPage.show) {
|
||||
Modal.success({
|
||||
content: 'Thank you for your submission!',
|
||||
okText: 'Restart Form',
|
||||
onOk: () => {
|
||||
window.location.reload()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goNext()
|
||||
}}
|
||||
prev={goPrev}
|
||||
/>
|
||||
)
|
||||
})
|
||||
.filter(e => e !== null),
|
||||
data.form.endPage.show ? <FormPage
|
||||
key={'end'}
|
||||
type={'end'}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user