add submissions, add todo's

This commit is contained in:
Michael Schramm 2020-05-31 23:18:16 +02:00
parent 4336536840
commit 5cc1565751
46 changed files with 1180 additions and 278 deletions

View 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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)

View File

@ -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>}

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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,
}

View File

@ -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>

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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',

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View File

@ -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>
)

View 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>
)
}

View File

@ -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,

View 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
View 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>
)
}

View File

@ -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)

View 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
}
`

View File

@ -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
}
`

View 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
}
}
`

View 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}
`

View 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
}
}
`

View 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}
`

View File

@ -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

View 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
}
}
`

View 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}
`

View 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
}
}
`

View 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}
`

View File

@ -4,7 +4,6 @@ const p = require('./package.json')
const version = p.version;
module.exports = withImages({
publicRuntimeConfig: {
endpoint: process.env.API_HOST || '/graphql',
version,

View File

@ -1,6 +1,6 @@
{
"name": "ohmyform-react",
"version": "0.1.0",
"version": "0.9.0",
"license": "MIT",
"scripts": {
"start:dev": "next dev -p 4000",

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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'}