mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2025-12-13 09:45:50 +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'}
|
format={'YYYY-MM-DD'}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{/* TODO add options
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Min Date'}
|
label={'Min Date'}
|
||||||
name={[field.name, 'min']}
|
name={[field.name, 'min']}
|
||||||
@ -35,6 +36,7 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({field, form}) => {
|
|||||||
>
|
>
|
||||||
<DatePicker />
|
<DatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {Form, Input} from 'antd'
|
import {Form, Rate} from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AdminFieldTypeProps} from './type.props'
|
import {AdminFieldTypeProps} from './type.props'
|
||||||
|
|
||||||
@ -10,8 +10,12 @@ export const RatingType: React.FC<AdminFieldTypeProps> = props => {
|
|||||||
label={'Default Value'}
|
label={'Default Value'}
|
||||||
name={[props.field.name, 'value']}
|
name={[props.field.name, 'value']}
|
||||||
labelCol={{ span: 6 }}
|
labelCol={{ span: 6 }}
|
||||||
|
extra={'Click again to remove default value'}
|
||||||
>
|
>
|
||||||
<Input />
|
<Rate
|
||||||
|
allowHalf
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -51,10 +51,6 @@ export const Field: React.FC<Props> = ({field, save, design, children, next, pre
|
|||||||
padding: 32,
|
padding: 32,
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
}}>
|
}}>
|
||||||
<pre style={{
|
|
||||||
opacity: 0.3
|
|
||||||
}}>{JSON.stringify(field, null, 2)}</pre>
|
|
||||||
|
|
||||||
<StyledH1 design={design} type={'question'}>{field.title}</StyledH1>
|
<StyledH1 design={design} type={'question'}>{field.title}</StyledH1>
|
||||||
{field.description && <StyledP design={design} type={'question'}>{field.description}</StyledP>}
|
{field.description && <StyledP design={design} type={'question'}>{field.description}</StyledP>}
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,10 @@ export const DropdownType: React.FC<FieldTypeProps> = ({field}) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Default Value'}
|
|
||||||
name={[field.id, 'value']}
|
name={[field.id, 'value']}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: 'Please provide Information' },
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
]}
|
]}
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</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 {DateType} from './date.type'
|
||||||
import {DropdownType} from './dropdown.type'
|
import {DropdownType} from './dropdown.type'
|
||||||
import {EmailType} from './email.type'
|
import {EmailType} from './email.type'
|
||||||
import {HiddenType} from './hidden.type'
|
|
||||||
import {LinkType} from './link.type'
|
import {LinkType} from './link.type'
|
||||||
import {NumberType} from './number.type'
|
import {NumberType} from './number.type'
|
||||||
import {RadioType} from './radio.type'
|
import {RadioType} from './radio.type'
|
||||||
@ -23,7 +22,6 @@ export const fieldTypes: {
|
|||||||
'dropdown': DropdownType,
|
'dropdown': DropdownType,
|
||||||
'rating': RatingType,
|
'rating': RatingType,
|
||||||
'radio': RadioType,
|
'radio': RadioType,
|
||||||
'hidden': HiddenType,
|
|
||||||
'yes_no': YesNoType,
|
'yes_no': YesNoType,
|
||||||
'number': NumberType,
|
'number': NumberType,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,12 +8,10 @@ export const RadioType: React.FC<FieldTypeProps> = ({field}) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Default Value'}
|
|
||||||
name={[field.id, 'value']}
|
name={[field.id, 'value']}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: 'Please provide Information' },
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
]}
|
]}
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {Form, Input} from 'antd'
|
import {Form, Rate} from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FieldTypeProps} from './type.props'
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
@ -8,14 +8,12 @@ export const RatingType: React.FC<FieldTypeProps> = ({field}) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Default Value'}
|
|
||||||
name={[field.id, 'value']}
|
name={[field.id, 'value']}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: 'Please provide Information' },
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
]}
|
]}
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Rate allowHalf defaultValue={parseFloat(field.value)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,19 +1,23 @@
|
|||||||
import {Form, Input} from 'antd'
|
import {Form} from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {StyledTextareaInput} from '../../styled/textarea.input'
|
||||||
import {FieldTypeProps} from './type.props'
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
export const TextareaType: React.FC<FieldTypeProps> = ({field}) => {
|
export const TextareaType: React.FC<FieldTypeProps> = ({field, design}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Default Value'}
|
|
||||||
name={[field.id, 'value']}
|
name={[field.id, 'value']}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: 'Please provide Information' },
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
]}
|
]}
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
>
|
||||||
<Input.TextArea autoSize />
|
<StyledTextareaInput
|
||||||
|
design={design}
|
||||||
|
allowClear
|
||||||
|
autoSize
|
||||||
|
defaultValue={field.value}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,20 +1,17 @@
|
|||||||
import {Form, Input} from 'antd'
|
import {Form, Switch} from 'antd'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FieldTypeProps} from './type.props'
|
import {FieldTypeProps} from './type.props'
|
||||||
|
|
||||||
export const YesNoType: React.FC<FieldTypeProps> = ({field}) => {
|
export const YesNoType: React.FC<FieldTypeProps> = ({field}) => {
|
||||||
// TODO add switch
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={'Default Value'}
|
|
||||||
name={[field.id, 'value']}
|
name={[field.id, 'value']}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: field.required, message: 'Please provide Information' },
|
{ required: field.required, message: 'Please provide Information' },
|
||||||
]}
|
]}
|
||||||
labelCol={{ span: 6 }}
|
|
||||||
>
|
>
|
||||||
<Input />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import {HomeOutlined, MessageOutlined, TeamOutlined} from '@ant-design/icons'
|
import {HomeOutlined, MessageOutlined, TeamOutlined} from '@ant-design/icons'
|
||||||
|
import {UserOutlined} from '@ant-design/icons/lib'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
export interface SideMenuElement {
|
export interface SideMenuElement {
|
||||||
@ -18,6 +19,12 @@ export const sideMenu: SideMenuElement[] = [
|
|||||||
href: '/admin',
|
href: '/admin',
|
||||||
icon: <HomeOutlined />,
|
icon: <HomeOutlined />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'profile',
|
||||||
|
name: 'Profile',
|
||||||
|
href: '/admin/profile',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'public',
|
key: 'public',
|
||||||
name: 'Forms',
|
name: 'Forms',
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {useRouter} from 'next/router'
|
|||||||
import React, {FunctionComponent} from 'react'
|
import React, {FunctionComponent} from 'react'
|
||||||
import {sideMenu, SideMenuElement} from './sidemenu'
|
import {sideMenu, SideMenuElement} from './sidemenu'
|
||||||
import {useWindowSize} from './use.window.size'
|
import {useWindowSize} from './use.window.size'
|
||||||
|
import {clearAuth} from './with.auth'
|
||||||
|
|
||||||
const { publicRuntimeConfig } = getConfig()
|
const { publicRuntimeConfig } = getConfig()
|
||||||
|
|
||||||
@ -117,7 +118,8 @@ const Structure: FunctionComponent<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signOut = async (): Promise<void> => {
|
const signOut = async (): Promise<void> => {
|
||||||
// TODO sign out
|
await clearAuth()
|
||||||
|
router.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -145,7 +147,7 @@ const Structure: FunctionComponent<Props> = (props) => {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
overlay={(
|
overlay={(
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item onClick={(): void => console.log('profile??')}>Profile</Menu.Item>
|
<Menu.Item onClick={() => router.push('/admin/profile')}>Profile</Menu.Item>
|
||||||
<Menu.Divider/>
|
<Menu.Divider/>
|
||||||
<Menu.Item onClick={signOut}>Logout</Menu.Item>
|
<Menu.Item onClick={signOut}>Logout</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -10,20 +10,20 @@ interface Props extends ButtonProps {
|
|||||||
color: any
|
color: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledButton: React.FC<Props> = ({background, highlight, color, children, ...props}) => {
|
const Styled = styled(Button)`
|
||||||
const StyledButton = styled(Button)`
|
background: ${props => props.background};
|
||||||
background: ${background};
|
color: ${props => props.color};
|
||||||
color: ${color};
|
border-color: ${props => darken(props.background, 10)};
|
||||||
border-color: ${darken(background, 10)};
|
|
||||||
|
:hover {
|
||||||
:hover {
|
color: ${props => props.highlight};
|
||||||
color: ${highlight};
|
background-color: ${props => lighten(props.background, 10)};
|
||||||
background-color: ${lighten(background, 10)};
|
border-color: ${props => darken(props.highlight, 10)};
|
||||||
border-color: ${darken(highlight, 10)};
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
|
export const StyledButton: React.FC<Props> = ({children, ...props}) => {
|
||||||
return (
|
return (
|
||||||
<StyledButton {...props}>{children}</StyledButton>
|
<Styled {...props}>{children}</Styled>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +1,50 @@
|
|||||||
import {DatePicker} from 'antd'
|
import {DatePicker} from 'antd'
|
||||||
import {PickerProps} from 'antd/lib/date-picker/generatePicker'
|
import {PickerProps} from 'antd/lib/date-picker/generatePicker'
|
||||||
import {Moment} from 'moment'
|
import {Moment} from 'moment'
|
||||||
import React, {useEffect, useState} from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||||
import {transparentize} from './color.change'
|
import {transparentize} from './color.change'
|
||||||
|
|
||||||
type Props = { design: FormDesignFragment } & PickerProps<Moment>
|
type Props = { design: FormDesignFragment } & PickerProps<Moment>
|
||||||
|
|
||||||
export const StyledDateInput: React.FC<Props> = ({design, children, ...props}) => {
|
const Field = styled(DatePicker)`
|
||||||
const [Field, setField] = useState()
|
color: ${props => props.design.colors.answerColor};
|
||||||
|
border-color: ${props => props.design.colors.answerColor};
|
||||||
useEffect(() => {
|
background: none !important;
|
||||||
setField(
|
border-right: none;
|
||||||
styled(DatePicker)`
|
border-top: none;
|
||||||
color: ${design.colors.answerColor};
|
border-left: none;
|
||||||
border-color: ${design.colors.answerColor};
|
border-radius: 0;
|
||||||
background: none !important;
|
width: 100%;
|
||||||
border-right: none;
|
|
||||||
border-top: none;
|
:hover,
|
||||||
border-left: none;
|
:active {
|
||||||
border-radius: 0;
|
border-color: ${props => props.design.colors.answerColor};
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.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 (
|
return (
|
||||||
<Field {...props}>{children}</Field>
|
<Field {...props}>{children}</Field>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,11 +7,11 @@ interface Props {
|
|||||||
design: FormDesignFragment
|
design: FormDesignFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledH1: React.FC<Props> = ({design, type, children, ...props}) => {
|
const Header = styled.h1`
|
||||||
const Header = styled.h1`
|
color: ${props => props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor}
|
||||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
|
export const StyledH1: React.FC<Props> = ({children, ...props}) => {
|
||||||
return (
|
return (
|
||||||
<Header {...props}>{children}</Header>
|
<Header {...props}>{children}</Header>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,12 +6,11 @@ interface Props {
|
|||||||
type: 'question' | 'answer'
|
type: 'question' | 'answer'
|
||||||
design: FormDesignFragment
|
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}) => {
|
export const StyledH2: React.FC<Props> = ({children, ...props}) => {
|
||||||
const Header = styled.h2`
|
|
||||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
|
||||||
`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header {...props}>{children}</Header>
|
<Header {...props}>{children}</Header>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {Input} from 'antd'
|
import {Input} from 'antd'
|
||||||
import {InputProps} from 'antd/lib/input/Input'
|
import {InputProps} from 'antd/lib/input/Input'
|
||||||
import React, {useEffect, useState} from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||||
import {transparentize} from './color.change'
|
import {transparentize} from './color.change'
|
||||||
@ -9,53 +9,43 @@ interface Props extends InputProps{
|
|||||||
design: FormDesignFragment
|
design: FormDesignFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledInput: React.FC<Props> = ({design, children, ...props}) => {
|
const Field = styled(Input)`
|
||||||
const [Field, setField] = useState()
|
color: ${props => props.design.colors.answerColor};
|
||||||
|
border-color: ${props => props.design.colors.answerColor};
|
||||||
useEffect(() => {
|
background: none !important;
|
||||||
setField(
|
border-right: none;
|
||||||
styled(Input)`
|
border-top: none;
|
||||||
color: ${design.colors.answerColor};
|
border-left: none;
|
||||||
border-color: ${design.colors.answerColor};
|
border-radius: 0;
|
||||||
background: none !important;
|
|
||||||
border-right: none;
|
:focus {
|
||||||
border-top: none;
|
outline: ${props => props.design.colors.answerColor} auto 5px
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
: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 (
|
return (
|
||||||
<Field {...props}>{children}</Field>
|
<Field {...props}>{children}</Field>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {InputNumber} from 'antd'
|
import {InputNumber} from 'antd'
|
||||||
import {InputNumberProps} from 'antd/lib/input-number'
|
import {InputNumberProps} from 'antd/lib/input-number'
|
||||||
import React, {useEffect, useState} from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
import {FormDesignFragment} from '../../graphql/fragment/form.fragment'
|
||||||
import {transparentize} from './color.change'
|
import {transparentize} from './color.change'
|
||||||
@ -9,54 +9,44 @@ interface Props extends InputNumberProps {
|
|||||||
design: FormDesignFragment
|
design: FormDesignFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledNumberInput: React.FC<Props> = ({design, children, ...props}) => {
|
const Field = styled(InputNumber)`
|
||||||
const [Field, setField] = useState()
|
color: ${props => props.design.colors.answerColor};
|
||||||
|
border-color: ${props => props.design.colors.answerColor};
|
||||||
useEffect(() => {
|
background: none !important;
|
||||||
setField(
|
border-right: none;
|
||||||
styled(InputNumber)`
|
border-top: none;
|
||||||
color: ${design.colors.answerColor};
|
border-left: none;
|
||||||
border-color: ${design.colors.answerColor};
|
border-radius: 0;
|
||||||
background: none !important;
|
width: 100%;
|
||||||
border-right: none;
|
|
||||||
border-top: none;
|
:focus {
|
||||||
border-left: none;
|
outline: ${props => props.design.colors.answerColor} auto 5px
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
: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 (
|
return (
|
||||||
<Field {...props}>{children}</Field>
|
<Field {...props}>{children}</Field>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,11 +7,11 @@ interface Props {
|
|||||||
design: FormDesignFragment
|
design: FormDesignFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledP: React.FC<Props> = ({design, type, children, ...props}) => {
|
const Paragraph = styled.p`
|
||||||
const Paragraph = styled.p`
|
color: ${props => props.type === 'question' ? props.design.colors.questionColor : props.design.colors.answerColor}
|
||||||
color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
|
export const StyledP: React.FC<Props> = ({children, ...props}) => {
|
||||||
return (
|
return (
|
||||||
<Paragraph {...props}>{children}</Paragraph>
|
<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(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const token = '123' // TODO generate secure token
|
const token = [...Array(40)].map(() => Math.random().toString(36)[2]).join('')
|
||||||
|
|
||||||
const {data} = await start({
|
const {data} = await start({
|
||||||
variables: {
|
variables: {
|
||||||
@ -27,8 +27,8 @@ export const useSubmission = (formId: string) => {
|
|||||||
submission: {
|
submission: {
|
||||||
token,
|
token,
|
||||||
device: {
|
device: {
|
||||||
name: '',
|
name: /Mobi/i.test(window.navigator.userAgent) ? 'mobile' : 'desktop',
|
||||||
type: ''
|
type: window.navigator.userAgent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,8 +59,6 @@ export const useSubmission = (formId: string) => {
|
|||||||
console.log('finish submission!!', formId)
|
console.log('finish submission!!', formId)
|
||||||
}, [submission])
|
}, [submission])
|
||||||
|
|
||||||
console.log('submission saver :D', formId)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setField,
|
setField,
|
||||||
finish,
|
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 {ME_QUERY, MeQueryData} from '../graphql/query/me.query'
|
||||||
import {LoadingPage} from './loading.page'
|
import {LoadingPage} from './loading.page'
|
||||||
|
|
||||||
|
export const clearAuth = async () => {
|
||||||
|
localStorage.removeItem('access')
|
||||||
|
localStorage.removeItem('refresh')
|
||||||
|
|
||||||
|
// TODO logout on server!
|
||||||
|
}
|
||||||
|
|
||||||
export const setAuth = (access, refresh) => {
|
export const setAuth = (access, refresh) => {
|
||||||
localStorage.setItem('access', access)
|
localStorage.setItem('access', access)
|
||||||
localStorage.setItem('refresh', refresh)
|
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'
|
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 {
|
fragment AdminUser on User {
|
||||||
id
|
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'
|
import {gql} from 'apollo-boost'
|
||||||
|
|
||||||
|
export interface AdminPagerSubmissionFormFieldQueryData {
|
||||||
|
title: string
|
||||||
|
required: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminPagerSubmissionFormQueryData {
|
export interface AdminPagerSubmissionFormQueryData {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
isLive: boolean
|
isLive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminPagerSubmissionEntryFieldQueryData {
|
||||||
|
id: string
|
||||||
|
value: string
|
||||||
|
type: string
|
||||||
|
|
||||||
|
field?: AdminPagerSubmissionFormFieldQueryData
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminPagerSubmissionEntryQueryData {
|
export interface AdminPagerSubmissionEntryQueryData {
|
||||||
id: string
|
id: string
|
||||||
created: string
|
created: string
|
||||||
@ -14,7 +27,14 @@ export interface AdminPagerSubmissionEntryQueryData {
|
|||||||
timeElapsed: number
|
timeElapsed: number
|
||||||
geoLocation: {
|
geoLocation: {
|
||||||
country: string
|
country: string
|
||||||
|
city: string
|
||||||
}
|
}
|
||||||
|
device: {
|
||||||
|
type: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
fields: AdminPagerSubmissionEntryFieldQueryData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminPagerSubmissionQueryData {
|
export interface AdminPagerSubmissionQueryData {
|
||||||
@ -52,12 +72,22 @@ export const ADMIN_PAGER_SUBMISSION_QUERY = gql`
|
|||||||
timeElapsed
|
timeElapsed
|
||||||
geoLocation {
|
geoLocation {
|
||||||
country
|
country
|
||||||
|
city
|
||||||
|
}
|
||||||
|
device {
|
||||||
|
type
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
fields {
|
fields {
|
||||||
id
|
id
|
||||||
value
|
value
|
||||||
type
|
type
|
||||||
|
|
||||||
|
field {
|
||||||
|
title
|
||||||
|
required
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
total
|
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;
|
const version = p.version;
|
||||||
|
|
||||||
module.exports = withImages({
|
module.exports = withImages({
|
||||||
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
endpoint: process.env.API_HOST || '/graphql',
|
endpoint: process.env.API_HOST || '/graphql',
|
||||||
version,
|
version,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ohmyform-react",
|
"name": "ohmyform-react",
|
||||||
"version": "0.1.0",
|
"version": "0.9.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:dev": "next dev -p 4000",
|
"start:dev": "next dev -p 4000",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from 'graphql/mutation/admin.form.update.mutation'
|
} from 'graphql/mutation/admin.form.update.mutation'
|
||||||
import {ADMIN_FORM_QUERY, AdminFormQueryData, AdminFormQueryVariables} from 'graphql/query/admin.form.query'
|
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 {useRouter} from 'next/router'
|
import {useRouter} from 'next/router'
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
|
|
||||||
@ -41,7 +42,6 @@ const Index: NextPage = () => {
|
|||||||
|
|
||||||
const save = async (formData: AdminFormQueryData) => {
|
const save = async (formData: AdminFormQueryData) => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
console.log('try to save form!', formData)
|
|
||||||
|
|
||||||
formData.form.fields = formData.form.fields.filter(e => e && e.type)
|
formData.form.fields = formData.form.fields.filter(e => e && e.type)
|
||||||
|
|
||||||
@ -74,6 +74,13 @@ const Index: NextPage = () => {
|
|||||||
{ href: '/admin/forms', name: 'Form' },
|
{ href: '/admin/forms', name: 'Form' },
|
||||||
]}
|
]}
|
||||||
extra={[
|
extra={[
|
||||||
|
<Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${router.query.id}/submissions`}>
|
||||||
|
<Button
|
||||||
|
key={'submissions'}
|
||||||
|
>
|
||||||
|
Submissions
|
||||||
|
</Button>
|
||||||
|
</Link>,
|
||||||
<Button
|
<Button
|
||||||
key={'save'}
|
key={'save'}
|
||||||
onClick={form.submit}
|
onClick={form.submit}
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import {EditOutlined} from '@ant-design/icons/lib'
|
|
||||||
import {useQuery} from '@apollo/react-hooks'
|
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 {PaginationProps} from 'antd/es/pagination'
|
||||||
|
import {ColumnsType} from 'antd/lib/table/interface'
|
||||||
import {DateTime} from 'components/date.time'
|
import {DateTime} from 'components/date.time'
|
||||||
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 dayjs from 'dayjs'
|
||||||
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 {SubmissionValues} from '../../../../components/form/admin/submission.values'
|
||||||
import {
|
import {
|
||||||
ADMIN_PAGER_SUBMISSION_QUERY,
|
ADMIN_PAGER_SUBMISSION_QUERY,
|
||||||
AdminPagerSubmissionEntryQueryData,
|
AdminPagerSubmissionEntryQueryData,
|
||||||
@ -29,7 +31,7 @@ const Submissions: NextPage = () => {
|
|||||||
variables: {
|
variables: {
|
||||||
form: router.query.id as string,
|
form: router.query.id as string,
|
||||||
limit: pagination.pageSize,
|
limit: pagination.pageSize,
|
||||||
start: pagination.current * pagination.pageSize || 0
|
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0
|
||||||
},
|
},
|
||||||
onCompleted: ({pager, form}) => {
|
onCompleted: ({pager, form}) => {
|
||||||
setPagination({
|
setPagination({
|
||||||
@ -41,11 +43,22 @@ const Submissions: NextPage = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = [
|
const columns:ColumnsType<AdminPagerSubmissionEntryQueryData> = [
|
||||||
{
|
{
|
||||||
title: 'Values',
|
title: 'Progress',
|
||||||
dataIndex: 'fields',
|
render: (row: AdminPagerSubmissionEntryQueryData) => {
|
||||||
render: fields => <pre>{JSON.stringify(fields, null, 2)}</pre>
|
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',
|
title: 'Created',
|
||||||
@ -57,21 +70,6 @@ const Submissions: NextPage = () => {
|
|||||||
dataIndex: 'lastModified',
|
dataIndex: 'lastModified',
|
||||||
render: date => <TimeAgo date={date} />
|
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 (
|
return (
|
||||||
@ -97,11 +95,11 @@ const Submissions: NextPage = () => {
|
|||||||
</Link>,
|
</Link>,
|
||||||
<Button
|
<Button
|
||||||
key={'web'}
|
key={'web'}
|
||||||
href={`/forms/${router.query.id}`}
|
href={`/form/${router.query.id}`}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
type={'primary'}
|
type={'primary'}
|
||||||
>
|
>
|
||||||
Open Form
|
Add Submission
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@ -110,8 +108,13 @@ const Submissions: NextPage = () => {
|
|||||||
dataSource={entries}
|
dataSource={entries}
|
||||||
rowKey={'id'}
|
rowKey={'id'}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: record => <SubmissionValues form={form} submission={record} />,
|
||||||
|
rowExpandable: record => record.percentageComplete > 0,
|
||||||
|
}}
|
||||||
onChange={next => {
|
onChange={next => {
|
||||||
setPagination(pagination)
|
setPagination(next)
|
||||||
|
refetch()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Structure>
|
</Structure>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import {DeleteOutlined, EditOutlined, GlobalOutlined, UnorderedListOutlined} from '@ant-design/icons/lib'
|
import {DeleteOutlined, EditOutlined, GlobalOutlined, UnorderedListOutlined} from '@ant-design/icons/lib'
|
||||||
import {useQuery} from '@apollo/react-hooks'
|
import {useMutation, useQuery} from '@apollo/react-hooks'
|
||||||
import {Button, Popconfirm, Space, Table, Tooltip} from 'antd'
|
import {Button, message, Popconfirm, Space, Table, Tooltip} from 'antd'
|
||||||
import {PaginationProps} from 'antd/es/pagination'
|
import {PaginationProps} from 'antd/es/pagination'
|
||||||
|
import {ColumnsType} from 'antd/lib/table/interface'
|
||||||
import {DateTime} from 'components/date.time'
|
import {DateTime} from 'components/date.time'
|
||||||
import {FormIsLive} from 'components/form/admin/is.live'
|
import {FormIsLive} from 'components/form/admin/is.live'
|
||||||
import Structure from 'components/structure'
|
import Structure from 'components/structure'
|
||||||
@ -16,6 +17,11 @@ import {
|
|||||||
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 {
|
||||||
|
ADMIN_FORM_DELETE_MUTATION,
|
||||||
|
AdminFormDeleteMutationData,
|
||||||
|
AdminFormDeleteMutationVariables
|
||||||
|
} from '../../../graphql/mutation/admin.form.delete.mutation'
|
||||||
|
|
||||||
const Index: NextPage = () => {
|
const Index: NextPage = () => {
|
||||||
const [pagination, setPagination] = useState<PaginationProps>({
|
const [pagination, setPagination] = useState<PaginationProps>({
|
||||||
@ -25,7 +31,7 @@ const Index: NextPage = () => {
|
|||||||
const {loading, refetch} = useQuery<AdminPagerFormQueryData, AdminPagerFormQueryVariables>(ADMIN_PAGER_FORM_QUERY, {
|
const {loading, refetch} = useQuery<AdminPagerFormQueryData, AdminPagerFormQueryVariables>(ADMIN_PAGER_FORM_QUERY, {
|
||||||
variables: {
|
variables: {
|
||||||
limit: pagination.pageSize,
|
limit: pagination.pageSize,
|
||||||
start: pagination.current * pagination.pageSize || 0
|
start: Math.max(0, pagination.current - 1) * pagination.pageSize || 0
|
||||||
},
|
},
|
||||||
onCompleted: ({pager}) => {
|
onCompleted: ({pager}) => {
|
||||||
setPagination({
|
setPagination({
|
||||||
@ -35,12 +41,29 @@ const Index: NextPage = () => {
|
|||||||
setEntries(pager.entries)
|
setEntries(pager.entries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const [executeDelete] = useMutation<AdminFormDeleteMutationData, AdminFormDeleteMutationVariables>(ADMIN_FORM_DELETE_MUTATION)
|
||||||
|
|
||||||
const deleteForm = async (form) => {
|
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',
|
title: 'Live',
|
||||||
dataIndex: 'isLive',
|
dataIndex: 'isLive',
|
||||||
@ -76,6 +99,7 @@ const Index: NextPage = () => {
|
|||||||
render: date => <TimeAgo date={date} />
|
render: date => <TimeAgo date={date} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
align: 'right',
|
||||||
render: row => {
|
render: row => {
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
@ -96,9 +120,10 @@ const Index: NextPage = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="Are you sure delete this form?"
|
title="Are you sure delete this form with all submissions?"
|
||||||
onConfirm={deleteForm}
|
onConfirm={() => deleteForm(row)}
|
||||||
okText={'Delete now!'}
|
okText={'Delete now!'}
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
>
|
>
|
||||||
<Button danger><DeleteOutlined /></Button>
|
<Button danger><DeleteOutlined /></Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@ -145,7 +170,8 @@ const Index: NextPage = () => {
|
|||||||
rowKey={'id'}
|
rowKey={'id'}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onChange={next => {
|
onChange={next => {
|
||||||
setPagination(pagination)
|
setPagination(next)
|
||||||
|
refetch()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Structure>
|
</Structure>
|
||||||
|
|||||||
@ -1,14 +1,37 @@
|
|||||||
|
import {useQuery} from '@apollo/react-hooks'
|
||||||
|
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'
|
||||||
import {NextPage} from 'next'
|
import {NextPage} from 'next'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
ADMIN_STATISTIC_QUERY,
|
||||||
|
AdminStatisticQueryData,
|
||||||
|
AdminStatisticQueryVariables
|
||||||
|
} from '../../graphql/query/admin.statistic.query'
|
||||||
|
|
||||||
const Index: NextPage = () => {
|
const Index: NextPage = () => {
|
||||||
|
const {data, loading} = useQuery<AdminStatisticQueryData, AdminStatisticQueryVariables>(ADMIN_STATISTIC_QUERY)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Structure
|
<Structure
|
||||||
title={'Home'}
|
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>
|
</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 Structure from 'components/structure'
|
||||||
import {withAuth} from 'components/with.auth'
|
import {withAuth} from 'components/with.auth'
|
||||||
import {NextPage} from 'next'
|
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 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 (
|
return (
|
||||||
<Structure
|
<Structure
|
||||||
title={'Edit User'}
|
loading={loading || saving}
|
||||||
|
title={loading ? 'Loading User' : `Edit User "${data.user.email}"`}
|
||||||
|
selected={'users'}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ href: '/admin', name: 'Home' },
|
{ href: '/admin', name: 'Home' },
|
||||||
{ href: '/admin/users', name: 'Users' },
|
{ 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>
|
</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 Structure from 'components/structure'
|
||||||
import {withAuth} from 'components/with.auth'
|
import {withAuth} from 'components/with.auth'
|
||||||
import {NextPage} from 'next'
|
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 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 (
|
return (
|
||||||
<Structure
|
<Structure
|
||||||
title={'Users'}
|
title={'Users'}
|
||||||
|
loading={loading}
|
||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ href: '/admin', name: 'Home' },
|
{ href: '/admin', name: 'Home' },
|
||||||
]}
|
]}
|
||||||
|
padded={false}
|
||||||
>
|
>
|
||||||
ok!
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={entries}
|
||||||
|
rowKey={'id'}
|
||||||
|
pagination={pagination}
|
||||||
|
onChange={next => {
|
||||||
|
setPagination(next)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Structure>
|
</Structure>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import {useQuery} from '@apollo/react-hooks'
|
import {useQuery} from '@apollo/react-hooks'
|
||||||
|
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 {useWindowSize} from 'components/use.window.size'
|
|
||||||
import {FORM_QUERY, FormQueryData, FormQueryVariables} from 'graphql/query/form.query'
|
import {FORM_QUERY, FormQueryData, FormQueryVariables} from 'graphql/query/form.query'
|
||||||
import {NextPage} from 'next'
|
import {NextPage} from 'next'
|
||||||
import React, {useState} from 'react'
|
import React, {useState} from 'react'
|
||||||
@ -17,7 +17,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Index: NextPage<Props> = ({id}) => {
|
const Index: NextPage<Props> = ({id}) => {
|
||||||
const windowSize = useWindowSize()
|
|
||||||
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
|
const [swiper, setSwiper] = useState<OriginalSwiper.default>(null)
|
||||||
const submission = useSubmission(id)
|
const submission = useSubmission(id)
|
||||||
|
|
||||||
@ -72,22 +71,47 @@ const Index: NextPage<Props> = ({id}) => {
|
|||||||
next={goNext}
|
next={goNext}
|
||||||
prev={goPrev}
|
prev={goPrev}
|
||||||
/> : undefined,
|
/> : undefined,
|
||||||
...data.form.fields.map((field, i) => (
|
...data.form.fields
|
||||||
<Field
|
.map((field, i) => {
|
||||||
key={field.id}
|
if (field.type === 'hidden') {
|
||||||
field={field}
|
return null
|
||||||
design={design}
|
}
|
||||||
save={values => {
|
|
||||||
submission.setField(field.id, values[field.id])
|
|
||||||
|
|
||||||
if (data.form.fields.length === i - 1) {
|
return (
|
||||||
submission.finish()
|
<Field
|
||||||
}
|
key={field.id}
|
||||||
}}
|
field={field}
|
||||||
next={goNext}
|
design={design}
|
||||||
prev={goPrev}
|
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
|
data.form.endPage.show ? <FormPage
|
||||||
key={'end'}
|
key={'end'}
|
||||||
type={'end'}
|
type={'end'}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user