From 5cc15657517b0d0cf3f9dd4d32bb8a918f8e4c63 Mon Sep 17 00:00:00 2001 From: Michael Schramm Date: Sun, 31 May 2020 23:18:16 +0200 Subject: [PATCH] add submissions, add todo's --- components/form/admin/submission.values.tsx | 58 +++++++ components/form/admin/types/date.type.tsx | 2 + components/form/admin/types/rating.type.tsx | 8 +- components/form/field.tsx | 4 - components/form/types/dropdown.type.tsx | 2 - components/form/types/hidden.type.tsx | 20 --- components/form/types/index.ts | 2 - components/form/types/radio.type.tsx | 2 - components/form/types/rating.type.tsx | 6 +- components/form/types/textarea.type.tsx | 14 +- components/form/types/yes_no.type.tsx | 7 +- components/sidemenu.tsx | 7 + components/structure.tsx | 6 +- components/styled/button.tsx | 26 +-- components/styled/date.input.tsx | 82 +++++----- components/styled/h1.tsx | 8 +- components/styled/h2.tsx | 9 +- components/styled/input.tsx | 82 +++++----- components/styled/number.input.tsx | 84 +++++----- components/styled/p.tsx | 8 +- components/styled/textarea.input.tsx | 50 ++++++ components/use.submission.ts | 8 +- components/user/admin/base.data.tab.tsx | 108 +++++++++++++ components/user/role.tsx | 34 ++++ components/with.auth.tsx | 7 + graphql/fragment/admin.profile.fragment.ts | 26 +++ graphql/fragment/admin.user.fragment.ts | 22 ++- .../mutation/admin.form.delete.mutation.ts | 19 +++ .../mutation/admin.profile.update.mutation.ts | 21 +++ .../mutation/admin.user.delete.mutation.ts | 19 +++ .../mutation/admin.user.update.mutation.ts | 20 +++ graphql/query/admin.pager.submission.query.ts | 30 ++++ graphql/query/admin.pager.user.query.ts | 41 +++++ graphql/query/admin.profile.query.ts | 19 +++ graphql/query/admin.statistic.query.ts | 30 ++++ graphql/query/admin.user.query.ts | 20 +++ next.config.js | 1 - package.json | 2 +- pages/admin/forms/[id]/index.tsx | 9 +- pages/admin/forms/[id]/submissions.tsx | 53 +++--- pages/admin/forms/index.tsx | 42 ++++- pages/admin/index.tsx | 25 ++- pages/admin/profile.tsx | 153 ++++++++++++++++++ pages/admin/users/[id]/index.tsx | 91 ++++++++++- pages/admin/users/index.tsx | 113 ++++++++++++- pages/form/[id]/index.tsx | 58 +++++-- 46 files changed, 1180 insertions(+), 278 deletions(-) create mode 100644 components/form/admin/submission.values.tsx delete mode 100644 components/form/types/hidden.type.tsx create mode 100644 components/styled/textarea.input.tsx create mode 100644 components/user/admin/base.data.tab.tsx create mode 100644 components/user/role.tsx create mode 100644 graphql/fragment/admin.profile.fragment.ts create mode 100644 graphql/mutation/admin.form.delete.mutation.ts create mode 100644 graphql/mutation/admin.profile.update.mutation.ts create mode 100644 graphql/mutation/admin.user.delete.mutation.ts create mode 100644 graphql/mutation/admin.user.update.mutation.ts create mode 100644 graphql/query/admin.pager.user.query.ts create mode 100644 graphql/query/admin.profile.query.ts create mode 100644 graphql/query/admin.statistic.query.ts create mode 100644 graphql/query/admin.user.query.ts create mode 100644 pages/admin/profile.tsx diff --git a/components/form/admin/submission.values.tsx b/components/form/admin/submission.values.tsx new file mode 100644 index 0000000..0da506a --- /dev/null +++ b/components/form/admin/submission.values.tsx @@ -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 => { + const columns: ColumnsType = [ + { + 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 ( +
+ + {props.submission.geoLocation.country} + {props.submission.geoLocation.city} + {props.submission.device.type} + {props.submission.device.name} + + + + + ) +} diff --git a/components/form/admin/types/date.type.tsx b/components/form/admin/types/date.type.tsx index 5a09a0d..4740f96 100644 --- a/components/form/admin/types/date.type.tsx +++ b/components/form/admin/types/date.type.tsx @@ -17,6 +17,7 @@ export const DateType: React.FC = ({field, form}) => { format={'YYYY-MM-DD'} /> + {/* TODO add options = ({field, form}) => { > + */} ) } diff --git a/components/form/admin/types/rating.type.tsx b/components/form/admin/types/rating.type.tsx index 2b76bcd..a7faa91 100644 --- a/components/form/admin/types/rating.type.tsx +++ b/components/form/admin/types/rating.type.tsx @@ -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 = props => { label={'Default Value'} name={[props.field.name, 'value']} labelCol={{ span: 6 }} + extra={'Click again to remove default value'} > - + ) diff --git a/components/form/field.tsx b/components/form/field.tsx index c9c8151..b71e3d7 100644 --- a/components/form/field.tsx +++ b/components/form/field.tsx @@ -51,10 +51,6 @@ export const Field: React.FC = ({field, save, design, children, next, pre padding: 32, justifyContent: 'flex-end', }}> -
{JSON.stringify(field, null, 2)}
- {field.title} {field.description && {field.description}} diff --git a/components/form/types/dropdown.type.tsx b/components/form/types/dropdown.type.tsx index aa47c5a..f7a70c8 100644 --- a/components/form/types/dropdown.type.tsx +++ b/components/form/types/dropdown.type.tsx @@ -7,12 +7,10 @@ export const DropdownType: React.FC = ({field}) => { return (
diff --git a/components/form/types/hidden.type.tsx b/components/form/types/hidden.type.tsx deleted file mode 100644 index 15573ad..0000000 --- a/components/form/types/hidden.type.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import {Form, Input} from 'antd' -import React from 'react' -import {FieldTypeProps} from './type.props' - -export const HiddenType: React.FC = ({field}) => { - return ( -
- - - -
- ) -} diff --git a/components/form/types/index.ts b/components/form/types/index.ts index f861273..f38dfd8 100644 --- a/components/form/types/index.ts +++ b/components/form/types/index.ts @@ -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, } diff --git a/components/form/types/radio.type.tsx b/components/form/types/radio.type.tsx index 97bd030..29fc290 100644 --- a/components/form/types/radio.type.tsx +++ b/components/form/types/radio.type.tsx @@ -8,12 +8,10 @@ export const RadioType: React.FC = ({field}) => { return (
diff --git a/components/form/types/rating.type.tsx b/components/form/types/rating.type.tsx index 4fc578d..8b41f56 100644 --- a/components/form/types/rating.type.tsx +++ b/components/form/types/rating.type.tsx @@ -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 = ({field}) => { return (
- +
) diff --git a/components/form/types/textarea.type.tsx b/components/form/types/textarea.type.tsx index ced4257..267c430 100644 --- a/components/form/types/textarea.type.tsx +++ b/components/form/types/textarea.type.tsx @@ -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 = ({field}) => { +export const TextareaType: React.FC = ({field, design}) => { return (
- +
) diff --git a/components/form/types/yes_no.type.tsx b/components/form/types/yes_no.type.tsx index 27f2afd..07b73c7 100644 --- a/components/form/types/yes_no.type.tsx +++ b/components/form/types/yes_no.type.tsx @@ -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 = ({field}) => { - // TODO add switch return (
- +
) diff --git a/components/sidemenu.tsx b/components/sidemenu.tsx index d362f97..fdeb89a 100644 --- a/components/sidemenu.tsx +++ b/components/sidemenu.tsx @@ -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: , }, + { + key: 'profile', + name: 'Profile', + href: '/admin/profile', + icon: , + }, { key: 'public', name: 'Forms', diff --git a/components/structure.tsx b/components/structure.tsx index f8c13c7..82b757d 100644 --- a/components/structure.tsx +++ b/components/structure.tsx @@ -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) => { } const signOut = async (): Promise => { - // TODO sign out + await clearAuth() + router.reload() } return ( @@ -145,7 +147,7 @@ const Structure: FunctionComponent = (props) => { - console.log('profile??')}>Profile + router.push('/admin/profile')}>Profile Logout diff --git a/components/styled/button.tsx b/components/styled/button.tsx index 8b56428..64befa9 100644 --- a/components/styled/button.tsx +++ b/components/styled/button.tsx @@ -10,20 +10,20 @@ interface Props extends ButtonProps { color: any } -export const StyledButton: React.FC = ({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 = ({children, ...props}) => { return ( - {children} + {children} ) } diff --git a/components/styled/date.input.tsx b/components/styled/date.input.tsx index d33f14f..7b69e39 100644 --- a/components/styled/date.input.tsx +++ b/components/styled/date.input.tsx @@ -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 -export const StyledDateInput: React.FC = ({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 = ({children, ...props}) => { return ( {children} ) diff --git a/components/styled/h1.tsx b/components/styled/h1.tsx index 368b556..1fa5fbb 100644 --- a/components/styled/h1.tsx +++ b/components/styled/h1.tsx @@ -7,11 +7,11 @@ interface Props { design: FormDesignFragment } -export const StyledH1: React.FC = ({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 = ({children, ...props}) => { return (
{children}
) diff --git a/components/styled/h2.tsx b/components/styled/h2.tsx index 3770cf5..658230b 100644 --- a/components/styled/h2.tsx +++ b/components/styled/h2.tsx @@ -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 = ({design, type, children, ...props}) => { - const Header = styled.h2` - color: ${type === 'question' ? design.colors.questionColor : design.colors.answerColor} - ` - +export const StyledH2: React.FC = ({children, ...props}) => { return (
{children}
) diff --git a/components/styled/input.tsx b/components/styled/input.tsx index 3a2c9ee..7faa046 100644 --- a/components/styled/input.tsx +++ b/components/styled/input.tsx @@ -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 = ({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 = ({children, ...props}) => { return ( {children} ) diff --git a/components/styled/number.input.tsx b/components/styled/number.input.tsx index e6e3270..041092d 100644 --- a/components/styled/number.input.tsx +++ b/components/styled/number.input.tsx @@ -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 = ({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 = ({children, ...props}) => { return ( {children} ) diff --git a/components/styled/p.tsx b/components/styled/p.tsx index 4432d83..9af3211 100644 --- a/components/styled/p.tsx +++ b/components/styled/p.tsx @@ -7,11 +7,11 @@ interface Props { design: FormDesignFragment } -export const StyledP: React.FC = ({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 = ({children, ...props}) => { return ( {children} ) diff --git a/components/styled/textarea.input.tsx b/components/styled/textarea.input.tsx new file mode 100644 index 0000000..2181bd7 --- /dev/null +++ b/components/styled/textarea.input.tsx @@ -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 = ({children, ...props}) => { + return ( + {children} + ) +} diff --git a/components/use.submission.ts b/components/use.submission.ts index ab82481..427915d 100644 --- a/components/use.submission.ts +++ b/components/use.submission.ts @@ -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, diff --git a/components/user/admin/base.data.tab.tsx b/components/user/admin/base.data.tab.tsx new file mode 100644 index 0000000..ae1005d --- /dev/null +++ b/components/user/admin/base.data.tab.tsx @@ -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 = props => { + return ( + + + + + + + + + + { + 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 + } + }} + > + + + + + + + + + + + + + + + + + ) +} diff --git a/components/user/role.tsx b/components/user/role.tsx new file mode 100644 index 0000000..7d9b9bd --- /dev/null +++ b/components/user/role.tsx @@ -0,0 +1,34 @@ +import {Tag} from "antd" +import React, {CSSProperties} from 'react' + +interface Props { + roles: string[] +} + +export const UserRole: React.FC = 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 ( + + {level.toUpperCase()} + + ) +} diff --git a/components/with.auth.tsx b/components/with.auth.tsx index 413fb0c..8025b14 100644 --- a/components/with.auth.tsx +++ b/components/with.auth.tsx @@ -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) diff --git a/graphql/fragment/admin.profile.fragment.ts b/graphql/fragment/admin.profile.fragment.ts new file mode 100644 index 0000000..dcd7b77 --- /dev/null +++ b/graphql/fragment/admin.profile.fragment.ts @@ -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 + } +` diff --git a/graphql/fragment/admin.user.fragment.ts b/graphql/fragment/admin.user.fragment.ts index 138064a..31239c9 100644 --- a/graphql/fragment/admin.user.fragment.ts +++ b/graphql/fragment/admin.user.fragment.ts @@ -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 } ` diff --git a/graphql/mutation/admin.form.delete.mutation.ts b/graphql/mutation/admin.form.delete.mutation.ts new file mode 100644 index 0000000..31f0160 --- /dev/null +++ b/graphql/mutation/admin.form.delete.mutation.ts @@ -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 + } + } +` diff --git a/graphql/mutation/admin.profile.update.mutation.ts b/graphql/mutation/admin.profile.update.mutation.ts new file mode 100644 index 0000000..67c4df4 --- /dev/null +++ b/graphql/mutation/admin.profile.update.mutation.ts @@ -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} +` diff --git a/graphql/mutation/admin.user.delete.mutation.ts b/graphql/mutation/admin.user.delete.mutation.ts new file mode 100644 index 0000000..beae284 --- /dev/null +++ b/graphql/mutation/admin.user.delete.mutation.ts @@ -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 + } + } +` diff --git a/graphql/mutation/admin.user.update.mutation.ts b/graphql/mutation/admin.user.update.mutation.ts new file mode 100644 index 0000000..93c428e --- /dev/null +++ b/graphql/mutation/admin.user.update.mutation.ts @@ -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} +` diff --git a/graphql/query/admin.pager.submission.query.ts b/graphql/query/admin.pager.submission.query.ts index ab594de..01ffaaa 100644 --- a/graphql/query/admin.pager.submission.query.ts +++ b/graphql/query/admin.pager.submission.query.ts @@ -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 diff --git a/graphql/query/admin.pager.user.query.ts b/graphql/query/admin.pager.user.query.ts new file mode 100644 index 0000000..17d9ce6 --- /dev/null +++ b/graphql/query/admin.pager.user.query.ts @@ -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 + } + } +` diff --git a/graphql/query/admin.profile.query.ts b/graphql/query/admin.profile.query.ts new file mode 100644 index 0000000..d6b1a2f --- /dev/null +++ b/graphql/query/admin.profile.query.ts @@ -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} +` diff --git a/graphql/query/admin.statistic.query.ts b/graphql/query/admin.statistic.query.ts new file mode 100644 index 0000000..b8a325e --- /dev/null +++ b/graphql/query/admin.statistic.query.ts @@ -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 + } + } +` diff --git a/graphql/query/admin.user.query.ts b/graphql/query/admin.user.query.ts new file mode 100644 index 0000000..40aa3e1 --- /dev/null +++ b/graphql/query/admin.user.query.ts @@ -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} +` diff --git a/next.config.js b/next.config.js index 50816b5..ab07baf 100644 --- a/next.config.js +++ b/next.config.js @@ -4,7 +4,6 @@ const p = require('./package.json') const version = p.version; module.exports = withImages({ - publicRuntimeConfig: { endpoint: process.env.API_HOST || '/graphql', version, diff --git a/package.json b/package.json index bdd49f5..eb25329 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ohmyform-react", - "version": "0.1.0", + "version": "0.9.0", "license": "MIT", "scripts": { "start:dev": "next dev -p 4000", diff --git a/pages/admin/forms/[id]/index.tsx b/pages/admin/forms/[id]/index.tsx index 45a852d..53adf9b 100644 --- a/pages/admin/forms/[id]/index.tsx +++ b/pages/admin/forms/[id]/index.tsx @@ -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={[ + + + , - - - - ) - } - }, ] return ( @@ -97,11 +95,11 @@ const Submissions: NextPage = () => { , , ]} > @@ -110,8 +108,13 @@ const Submissions: NextPage = () => { dataSource={entries} rowKey={'id'} pagination={pagination} + expandable={{ + expandedRowRender: record => , + rowExpandable: record => record.percentageComplete > 0, + }} onChange={next => { - setPagination(pagination) + setPagination(next) + refetch() }} /> diff --git a/pages/admin/forms/index.tsx b/pages/admin/forms/index.tsx index 7070946..dc7aa26 100644 --- a/pages/admin/forms/index.tsx +++ b/pages/admin/forms/index.tsx @@ -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({ @@ -25,7 +31,7 @@ const Index: NextPage = () => { const {loading, refetch} = useQuery(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(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 = [ { title: 'Live', dataIndex: 'isLive', @@ -76,6 +99,7 @@ const Index: NextPage = () => { render: date => }, { + align: 'right', render: row => { return ( @@ -96,9 +120,10 @@ const Index: NextPage = () => { deleteForm(row)} okText={'Delete now!'} + okButtonProps={{ danger: true }} > @@ -145,7 +170,8 @@ const Index: NextPage = () => { rowKey={'id'} pagination={pagination} onChange={next => { - setPagination(pagination) + setPagination(next) + refetch() }} /> diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index 94e6fca..9fa3aed 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -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(ADMIN_STATISTIC_QUERY) + return ( - ok! + +
+ + + + + + + + + + + ) } diff --git a/pages/admin/profile.tsx b/pages/admin/profile.tsx new file mode 100644 index 0000000..f03cfe0 --- /dev/null +++ b/pages/admin/profile.tsx @@ -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(ADMIN_PROFILE_QUERY, { + onCompleted: next => { + form.setFieldsValue(next) + }, + }) + + const [update] = useMutation(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 ( + + Save + , + ]} + > +
{ + message.error('Required fields are missing') + }} + labelCol={{ + xs: { span: 24 }, + sm: { span: 6 }, + }} + wrapperCol={{ + xs: { span: 24 }, + sm: { span: 18 }, + }} + > + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} + +export default Profile diff --git a/pages/admin/users/[id]/index.tsx b/pages/admin/users/[id]/index.tsx index 9be28ea..ad5e76e 100644 --- a/pages/admin/users/[id]/index.tsx +++ b/pages/admin/users/[id]/index.tsx @@ -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(ADMIN_USER_QUERY, { + variables: { + id: router.query.id as string + }, + onCompleted: next => { + form.setFieldsValue(next) + }, + }) + + const [update] = useMutation(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 ( + Save + , + ]} + style={{paddingTop: 0}} > - ok! +
{ + message.error('Required fields are missing') + }} + labelCol={{ + xs: { span: 24 }, + sm: { span: 6 }, + }} + wrapperCol={{ + xs: { span: 24 }, + sm: { span: 18 }, + }} + > + + + + + +
) } diff --git a/pages/admin/users/index.tsx b/pages/admin/users/index.tsx index fa7c0fd..ae54412 100644 --- a/pages/admin/users/index.tsx +++ b/pages/admin/users/index.tsx @@ -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({ + pageSize: 10, + }) + const [entries, setEntries] = useState() + const {loading, refetch} = useQuery(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(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 = [ + { + title: 'Role', + dataIndex: 'roles', + render: roles => + }, + { + title: 'Email', + render: row => {row.email} + }, + { + title: 'Created', + dataIndex: 'created', + render: date => + }, + { + align: 'right', + render: row => { + return ( + + + + + + deleteUser(row)} + okText={'Delete now!'} + okButtonProps={{ danger: true }} + > + + + + ) + } + }, + ] + return ( - ok! +
{ + setPagination(next) + refetch() + }} + /> ) } diff --git a/pages/form/[id]/index.tsx b/pages/form/[id]/index.tsx index 4383513..8a186b7 100644 --- a/pages/form/[id]/index.tsx +++ b/pages/form/[id]/index.tsx @@ -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 = ({id}) => { - const windowSize = useWindowSize() const [swiper, setSwiper] = useState(null) const submission = useSubmission(id) @@ -72,22 +71,47 @@ const Index: NextPage = ({id}) => { next={goNext} prev={goPrev} /> : undefined, - ...data.form.fields.map((field, i) => ( - { - 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 ( + { + 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 ?