mirror of
https://github.com/IT4Change/ohmyform-ui.git
synced 2025-12-13 01:35:51 +00:00
update linting
This commit is contained in:
parent
a983cb218a
commit
acb26096d6
@ -3,6 +3,8 @@ module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
@ -12,11 +14,13 @@ module.exports = {
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# OhMyForm UI
|
||||
|
||||

|
||||

|
||||
[](https://travis-ci.org/ohmyform/ui)
|
||||

|
||||

|
||||
[](https://app.lokalise.com/public/379418475ede5d5c6937b0.31012044/)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
const omitDeepArrayWalk = (arr, key) => {
|
||||
return arr.map((val) => {
|
||||
if (Array.isArray(val)) return omitDeepArrayWalk(val, key)
|
||||
@ -6,16 +7,11 @@ const omitDeepArrayWalk = (arr, key) => {
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const omitDeep = (obj: any, key: string | number): any => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const keys: Array<any> = Object.keys(obj)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const newObj: any = {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
keys.forEach((i: any) => {
|
||||
if (i !== key) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const val: any = obj[i]
|
||||
if (val instanceof Date) newObj[i] = val
|
||||
else if (Array.isArray(val)) newObj[i] = omitDeepArrayWalk(val, key)
|
||||
|
||||
@ -21,10 +21,10 @@ export const FieldCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { form, field, fields, onChangeFields, remove, index } = props
|
||||
|
||||
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type'])
|
||||
const type = form.getFieldValue(['form', 'fields', field.name as string, 'type']) as string
|
||||
const TypeComponent = adminTypes[type] || TextType
|
||||
|
||||
const [nextTitle, setNextTitle] = useState(
|
||||
const [nextTitle, setNextTitle] = useState<string>(
|
||||
form.getFieldValue(['form', 'fields', field.name as string, 'title'])
|
||||
)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons/lib'
|
||||
import { Button, Form, Select, Space, Tabs } from 'antd'
|
||||
import { FormInstance } from 'antd/lib/form'
|
||||
import { TabPaneProps } from 'antd/lib/tabs'
|
||||
import { FieldData } from 'rc-field-form/lib/interface'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AdminFormFieldFragment } from '../../../graphql/fragment/admin.form.fragment'
|
||||
@ -19,7 +20,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
|
||||
const [nextType, setNextType] = useState('textfield')
|
||||
|
||||
const renderType = useCallback(
|
||||
(field, index, remove) => {
|
||||
(field: FieldData, index: number, remove: (index: number) => void) => {
|
||||
return (
|
||||
<FieldCard
|
||||
form={props.form}
|
||||
@ -35,7 +36,7 @@ export const FieldsTab: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const addField = useCallback(
|
||||
(add, index) => {
|
||||
(add: (defaults: unknown) => void, index: number) => {
|
||||
return (
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
<Space
|
||||
|
||||
@ -16,7 +16,7 @@ export const RespondentNotificationsTab: React.FC<Props> = (props) => {
|
||||
const [enabled, setEnabled] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])
|
||||
const next = props.form.getFieldValue(['form', 'respondentNotifications', 'enabled']) as boolean
|
||||
|
||||
if (next !== enabled) {
|
||||
setEnabled(next)
|
||||
@ -24,14 +24,18 @@ export const RespondentNotificationsTab: React.FC<Props> = (props) => {
|
||||
}, [props.form.getFieldValue(['form', 'respondentNotifications', 'enabled'])])
|
||||
|
||||
useEffect(() => {
|
||||
props.form.validateFields([
|
||||
props.form
|
||||
.validateFields([
|
||||
['form', 'respondentNotifications', 'subject'],
|
||||
['form', 'respondentNotifications', 'htmlTemplate'],
|
||||
['form', 'respondentNotifications', 'toField'],
|
||||
])
|
||||
.catch((e: Error) => console.error('failed to validate fields', e))
|
||||
}, [enabled])
|
||||
|
||||
const groups = {}
|
||||
const groups: {
|
||||
[key: string]: AdminFormFieldFragment[]
|
||||
} = {}
|
||||
|
||||
props.fields.forEach((field) => {
|
||||
if (!groups[field.type]) {
|
||||
|
||||
@ -16,7 +16,7 @@ export const SelfNotificationsTab: React.FC<Props> = (props) => {
|
||||
const [enabled, setEnabled] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])
|
||||
const next = props.form.getFieldValue(['form', 'selfNotifications', 'enabled']) as boolean
|
||||
|
||||
if (next !== enabled) {
|
||||
setEnabled(next)
|
||||
@ -24,13 +24,17 @@ export const SelfNotificationsTab: React.FC<Props> = (props) => {
|
||||
}, [props.form.getFieldValue(['form', 'selfNotifications', 'enabled'])])
|
||||
|
||||
useEffect(() => {
|
||||
props.form.validateFields([
|
||||
props.form
|
||||
.validateFields([
|
||||
['form', 'selfNotifications', 'subject'],
|
||||
['form', 'selfNotifications', 'htmlTemplate'],
|
||||
])
|
||||
.catch((e: Error) => console.error('failed to validate', e))
|
||||
}, [enabled])
|
||||
|
||||
const groups = {}
|
||||
const groups: {
|
||||
[key: string]: AdminFormFieldFragment[]
|
||||
} = {}
|
||||
props.fields.forEach((field) => {
|
||||
if (!groups[field.type]) {
|
||||
groups[field.type] = []
|
||||
|
||||
@ -19,7 +19,7 @@ export const SubmissionValues: React.FC<Props> = (props) => {
|
||||
const columns: ColumnsType<AdminPagerSubmissionEntryFieldQueryData> = [
|
||||
{
|
||||
title: t('submission:field'),
|
||||
render: (row: AdminPagerSubmissionEntryFieldQueryData) => {
|
||||
render(row: AdminPagerSubmissionEntryFieldQueryData) {
|
||||
if (row.field) {
|
||||
return `${row.field.title}${row.field.required ? '*' : ''}`
|
||||
}
|
||||
@ -29,9 +29,9 @@ export const SubmissionValues: React.FC<Props> = (props) => {
|
||||
},
|
||||
{
|
||||
title: t('submission:value'),
|
||||
render: (row) => {
|
||||
render(row: AdminPagerSubmissionEntryFieldQueryData) {
|
||||
try {
|
||||
const data = JSON.parse(row.value)
|
||||
const data = JSON.parse(row.value) as { value: string }
|
||||
|
||||
return data.value
|
||||
} catch (e) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DatePicker, Form } from 'antd'
|
||||
import moment from 'moment'
|
||||
import moment, { Moment } from 'moment'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AdminFieldTypeProps } from './type.props'
|
||||
@ -11,29 +11,29 @@ export const DateType: React.FC<AdminFieldTypeProps> = ({ field }) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:date.default')}
|
||||
name={[field.name, 'value']}
|
||||
name={[field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
getValueFromEvent={(e) => (e ? e.format('YYYY-MM-DD') : undefined)}
|
||||
getValueProps={(e) => ({ value: e ? moment(e) : undefined })}
|
||||
getValueFromEvent={(e: Moment) => (e ? e.format('YYYY-MM-DD') : undefined)}
|
||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
||||
>
|
||||
<DatePicker format={'YYYY-MM-DD'} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('type:date.min')}
|
||||
name={[field.name, 'optionKeys', 'min']}
|
||||
name={[field.name as string, 'optionKeys', 'min']}
|
||||
labelCol={{ span: 6 }}
|
||||
getValueFromEvent={(e) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e) => ({ value: e ? moment(e) : undefined })}
|
||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('type:date.max')}
|
||||
name={[field.name, 'optionKeys', 'max']}
|
||||
name={[field.name as string, 'optionKeys', 'max']}
|
||||
labelCol={{ span: 6 }}
|
||||
getValueFromEvent={(e) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e) => ({ value: e ? moment(e) : undefined })}
|
||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
||||
>
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
|
||||
@ -10,13 +10,13 @@ export const DropdownType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:dropdown.default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name={[props.field.name, 'options']}>
|
||||
<Form.List name={[props.field.name as string, 'options']}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -10,7 +10,7 @@ export const EmailType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:email.default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
rules={[{ type: 'email', message: t('validation:emailRequired') }]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
|
||||
@ -10,7 +10,7 @@ export const HiddenType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:hidden.default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
|
||||
@ -10,7 +10,7 @@ export const LinkType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:link.default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
rules={[{ type: 'url', message: t('validation:invalidUrl') }]}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
|
||||
@ -10,7 +10,7 @@ export const NumberType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:number:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<InputNumber />
|
||||
|
||||
@ -10,13 +10,13 @@ export const RadioType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:radio:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name={[props.field.name, 'options']}>
|
||||
<Form.List name={[props.field.name as string, 'options']}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@ -11,7 +11,7 @@ export const RatingType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:rating:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
extra={t('type:rating.clearNote')}
|
||||
>
|
||||
|
||||
@ -9,7 +9,7 @@ export const TextType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('type:textfield:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
|
||||
@ -10,7 +10,7 @@ export const TextareaType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:textarea:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input.TextArea autoSize />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { FormInstance } from 'antd/lib/form'
|
||||
import { FieldData } from 'rc-field-form/lib/interface'
|
||||
|
||||
export interface AdminFieldTypeProps {
|
||||
form: FormInstance
|
||||
field: { name: string }
|
||||
field: FieldData
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ export const YesNoType: React.FC<AdminFieldTypeProps> = (props) => {
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t('type:yes_no:default')}
|
||||
name={[props.field.name, 'value']}
|
||||
name={[props.field.name as string, 'value']}
|
||||
labelCol={{ span: 6 }}
|
||||
>
|
||||
<Input />
|
||||
|
||||
@ -30,8 +30,8 @@ export const Field: React.FC<Props> = ({ field, save, design, next, prev, ...pro
|
||||
next()
|
||||
}
|
||||
|
||||
const error = () => {
|
||||
message.error('Check inputs!')
|
||||
const error = async () => {
|
||||
await message.error('Check inputs!')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -25,8 +25,8 @@ export const DateType: React.FC<FieldTypeProps> = ({ field, design }) => {
|
||||
<Form.Item
|
||||
name={[field.id, 'value']}
|
||||
rules={[{ required: field.required, message: 'Please provide Information' }]}
|
||||
getValueFromEvent={(e) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e) => ({ value: e ? moment(e) : undefined })}
|
||||
getValueFromEvent={(e: Moment) => e.format('YYYY-MM-DD')}
|
||||
getValueProps={(e: string) => ({ value: e ? moment(e) : undefined })}
|
||||
initialValue={field.value ? moment(field.value) : undefined}
|
||||
>
|
||||
<StyledDateInput
|
||||
|
||||
@ -18,7 +18,7 @@ export const InputColor: React.FC<Props> = (props) => {
|
||||
triangle={'hide'}
|
||||
width={'100%'}
|
||||
color={props.value}
|
||||
onChange={(e) => props.onChange(e.hex)}
|
||||
onChange={(e: { hex: string }) => props.onChange(e.hex)}
|
||||
styles={{
|
||||
default: {
|
||||
card: {
|
||||
|
||||
@ -105,9 +105,9 @@ const Structure: FunctionComponent<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={(): void => {
|
||||
onClick={async () => {
|
||||
if (element.href) {
|
||||
router.push(element.href)
|
||||
await router.push(element.href)
|
||||
}
|
||||
}}
|
||||
key={element.key}
|
||||
@ -120,8 +120,8 @@ const Structure: FunctionComponent<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const signOut = async (): Promise<void> => {
|
||||
await clearAuth()
|
||||
const signOut = (): void => {
|
||||
clearAuth()
|
||||
router.reload()
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ const Structure: FunctionComponent<Props> = (props) => {
|
||||
})}
|
||||
|
||||
<img
|
||||
src={require('assets/images/logo_white_small.png')}
|
||||
src={require('assets/images/logo_white_small.png') as string}
|
||||
height={30}
|
||||
style={{ marginRight: 16 }}
|
||||
alt={'OhMyForm'}
|
||||
|
||||
@ -10,15 +10,16 @@ interface Props extends ButtonProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Styled = styled(Button)`
|
||||
background: ${(props) => props.background};
|
||||
color: ${(props) => props.color};
|
||||
border-color: ${(props) => darken(props.background, 10)};
|
||||
background: ${(props: Props) => props.background};
|
||||
color: ${(props: Props) => props.color};
|
||||
border-color: ${(props: Props) => darken(props.background, 10)};
|
||||
|
||||
:hover {
|
||||
color: ${(props) => props.highlight};
|
||||
background-color: ${(props) => lighten(props.background, 10)};
|
||||
border-color: ${(props) => darken(props.highlight, 10)};
|
||||
color: ${(props: Props) => props.highlight};
|
||||
background-color: ${(props: Props) => lighten(props.background, 10)};
|
||||
border-color: ${(props: Props) => darken(props.highlight, 10)};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* @link https://css-tricks.com/snippets/javascript/lighten-darken-color/
|
||||
*
|
||||
|
||||
@ -8,9 +8,10 @@ import { transparentize } from './color.change'
|
||||
|
||||
type Props = { design: FormDesignFragment } & PickerProps<Moment>
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(DatePicker)`
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
@ -20,7 +21,7 @@ const Field = styled(DatePicker)`
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-picker {
|
||||
@ -32,15 +33,15 @@ const Field = styled(DatePicker)`
|
||||
}
|
||||
|
||||
input {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${(props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const Header = styled.h1`
|
||||
color: ${(props) =>
|
||||
color: ${(props: Props) =>
|
||||
props.type === 'question'
|
||||
? props.design.colors.questionColor
|
||||
: props.design.colors.answerColor};
|
||||
|
||||
@ -7,7 +7,7 @@ interface Props {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
const Header = styled.h2`
|
||||
color: ${(props) =>
|
||||
color: ${(props: Props) =>
|
||||
props.type === 'question'
|
||||
? props.design.colors.questionColor
|
||||
: props.design.colors.answerColor};
|
||||
|
||||
@ -9,9 +9,10 @@ interface Props extends InputProps {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(Input)`
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
@ -19,12 +20,12 @@ const Field = styled(Input)`
|
||||
border-radius: 0;
|
||||
|
||||
:focus {
|
||||
outline: ${(props) => props.design.colors.answerColor} auto 5px;
|
||||
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px;
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-input-affix-wrapper {
|
||||
@ -33,15 +34,15 @@ const Field = styled(Input)`
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${(props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -9,9 +9,10 @@ interface Props extends InputNumberProps {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(InputNumber)`
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
@ -20,12 +21,12 @@ const Field = styled(InputNumber)`
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
outline: ${(props) => props.design.colors.answerColor} auto 5px;
|
||||
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px;
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
&.ant-input-number {
|
||||
@ -34,15 +35,15 @@ const Field = styled(InputNumber)`
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${(props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -7,8 +7,9 @@ interface Props {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
|
||||
const Paragraph = styled.p`
|
||||
color: ${(props) =>
|
||||
color: ${(props: Props) =>
|
||||
props.type === 'question'
|
||||
? props.design.colors.questionColor
|
||||
: props.design.colors.answerColor};
|
||||
|
||||
@ -8,27 +8,28 @@ interface Props extends RadioProps {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(Radio)`
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
background: none;
|
||||
|
||||
.ant-radio {
|
||||
.ant-radio-inner {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
&::after {
|
||||
background: ${(props) => props.design.colors.answerColor};
|
||||
background: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -9,10 +9,11 @@ interface Props extends SelectProps<string> {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(Select)`
|
||||
.ant-select-selector {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor} !important;
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor} !important;
|
||||
background: none !important;
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
@ -22,25 +23,25 @@ const Field = styled(Select)`
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: ${(props) => props.design.colors.answerColor} auto 5px;
|
||||
outline: ${(props: Props) => props.design.colors.answerColor} auto 5px;
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${(props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -9,9 +9,10 @@ interface Props extends TextAreaProps {
|
||||
design: FormDesignFragment
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const Field = styled(Input.TextArea)`
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
background: none !important;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
@ -21,25 +22,25 @@ const Field = styled(Input.TextArea)`
|
||||
:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
:hover,
|
||||
:active {
|
||||
border-color: ${(props) => props.design.colors.answerColor};
|
||||
border-color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
|
||||
input {
|
||||
background: none !important;
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
|
||||
::placeholder {
|
||||
color: ${(props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
color: ${(props: Props) => transparentize(props.design.colors.answerColor, 60)};
|
||||
}
|
||||
}
|
||||
|
||||
.anticon {
|
||||
color: ${(props) => props.design.colors.answerColor};
|
||||
color: ${(props: Props) => props.design.colors.answerColor};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
|
||||
interface Submission {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setField: (fieldId: string, data: any) => Promise<void>
|
||||
setField: (fieldId: string, data: unknown) => Promise<void>
|
||||
finish: () => void
|
||||
}
|
||||
|
||||
@ -28,10 +28,10 @@ export const useSubmission = (formId: string): Submission => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const token = [...Array(40)].map(() => Math.random().toString(36)[2]).join('')
|
||||
|
||||
const { data } = await start({
|
||||
start({
|
||||
variables: {
|
||||
form: formId,
|
||||
submission: {
|
||||
@ -43,12 +43,13 @@ export const useSubmission = (formId: string): Submission => {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
.then(({ data }) => {
|
||||
setSubmission({
|
||||
id: data.submission.id,
|
||||
token,
|
||||
})
|
||||
})()
|
||||
})
|
||||
.catch((e: Error) => console.error('failed to start submission', e))
|
||||
}, [formId])
|
||||
|
||||
const setField = useCallback(
|
||||
|
||||
@ -55,7 +55,7 @@ export const BaseDataTab: React.FC<TabPaneProps> = (props) => {
|
||||
return ['user']
|
||||
}
|
||||
}}
|
||||
getValueProps={(v) => {
|
||||
getValueProps={(v: string[]) => {
|
||||
let role = 'user'
|
||||
|
||||
if (v && v.includes('superuser')) {
|
||||
|
||||
@ -6,7 +6,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UserRole: React.FC<Props> = (props) => {
|
||||
let color
|
||||
let color: string
|
||||
let level = 'unknown'
|
||||
const css: CSSProperties = {}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { ME_QUERY, MeQueryData } from '../graphql/query/me.query'
|
||||
import { LoadingPage } from './loading.page'
|
||||
|
||||
export const clearAuth = async (): Promise<void> => {
|
||||
export const clearAuth = (): void => {
|
||||
localStorage.removeItem('access')
|
||||
localStorage.removeItem('refresh')
|
||||
|
||||
@ -18,7 +18,7 @@ export const setAuth = (access: string, refresh: string): void => {
|
||||
localStorage.setItem('refresh', refresh)
|
||||
}
|
||||
|
||||
export const authConfig = async (config: AxiosRequestConfig = {}): Promise<AxiosRequestConfig> => {
|
||||
export const authConfig = (config: AxiosRequestConfig = {}): AxiosRequestConfig => {
|
||||
if (!config.headers) {
|
||||
config.headers = {}
|
||||
}
|
||||
@ -28,6 +28,7 @@ export const authConfig = async (config: AxiosRequestConfig = {}): Promise<Axios
|
||||
// TODO check for validity / use refresh token
|
||||
|
||||
if (token) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
} catch (e) {
|
||||
@ -60,7 +61,7 @@ export const withAuth = (Component: any, roles: string[] = []): React.FC => {
|
||||
const path = router.asPath || router.pathname
|
||||
localStorage.setItem('redirect', path)
|
||||
|
||||
router.push('/login')
|
||||
router.push('/login').catch((e: Error) => console.error('failed to redirect to login', e))
|
||||
}, [error])
|
||||
|
||||
useEffect(() => {
|
||||
@ -74,7 +75,7 @@ export const withAuth = (Component: any, roles: string[] = []): React.FC => {
|
||||
setAccess(next)
|
||||
|
||||
if (!next) {
|
||||
router.push('/')
|
||||
router.push('/').catch((e: Error) => console.error('failed to redirect to /', e))
|
||||
}
|
||||
}, [data])
|
||||
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import { gql } from 'apollo-boost'
|
||||
|
||||
export interface LoginMutationData {
|
||||
tokens: {
|
||||
access: string
|
||||
refresh: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoginMutationVariables {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const LOGIN_MUTATION = gql`
|
||||
mutation login($username: String!, $password: String!) {
|
||||
tokens: authLogin(username: $username, password: $password) {
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
import { gql } from 'apollo-boost'
|
||||
|
||||
export interface RegisterMutationData {
|
||||
tokens: {
|
||||
access: string
|
||||
refresh: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegisterUserData {
|
||||
username: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterMutationVariables {
|
||||
user: RegisterUserData
|
||||
}
|
||||
|
||||
export const REGISTER_MUTATION = gql`
|
||||
mutation register($user: UserCreateInput!) {
|
||||
tokens: authRegister(user: $user) {
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { gql } from 'apollo-boost'
|
||||
|
||||
export interface AdminPagerFormEntryAdminQueryData {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface AdminPagerFormEntryQueryData {
|
||||
id: string
|
||||
created: string
|
||||
@ -7,11 +13,7 @@ export interface AdminPagerFormEntryQueryData {
|
||||
title: string
|
||||
isLive: boolean
|
||||
language: string
|
||||
admin: {
|
||||
id: string
|
||||
email: string
|
||||
username: string
|
||||
}
|
||||
admin: AdminPagerFormEntryAdminQueryData
|
||||
}
|
||||
|
||||
export interface AdminPagerFormQueryData {
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
"@ant-design/icons": "^4.1.0",
|
||||
"@apollo/react-hooks": "^3.1.5",
|
||||
"@lifeomic/axios-fetch": "^1.4.2",
|
||||
"@types/swiper": "^5.3.1",
|
||||
"antd": "^4.2.2",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"axios": "^0.19.2",
|
||||
@ -42,8 +41,13 @@
|
||||
"swiper": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/lifeomic__axios-fetch": "^1.4.0",
|
||||
"@types/node": "^14.0.1",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/swiper": "^5.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^3.2.0",
|
||||
"@typescript-eslint/parser": "^3.2.0",
|
||||
"commander": "^5.1.0",
|
||||
|
||||
@ -13,13 +13,18 @@ import Head from 'next/head'
|
||||
import React from 'react'
|
||||
import { wrapper } from 'store'
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
const { publicRuntimeConfig } = getConfig() as {
|
||||
publicRuntimeConfig: {
|
||||
endpoint: string
|
||||
}
|
||||
}
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri: publicRuntimeConfig.endpoint,
|
||||
fetch: buildAxiosFetch(axios),
|
||||
request: async (operation): Promise<void> => {
|
||||
operation.setContext(await authConfig())
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-explicit-any
|
||||
fetch: buildAxiosFetch(axios) as any,
|
||||
request: (operation): void => {
|
||||
operation.setContext(authConfig())
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -112,10 +112,10 @@ const Index: NextPage = () => {
|
||||
form.setFieldsValue(next)
|
||||
setFields(next.form.fields)
|
||||
|
||||
message.success(t('form:updated'))
|
||||
await message.success(t('form:updated'))
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error(t('form:updateError'))
|
||||
await message.error(t('form:updateError'))
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
@ -134,7 +134,7 @@ const Index: NextPage = () => {
|
||||
<Link
|
||||
key={'submissions'}
|
||||
href={'/admin/forms/[id]/submissions'}
|
||||
as={`/admin/forms/${router.query.id}/submissions`}
|
||||
as={`/admin/forms/${router.query.id as string}/submissions`}
|
||||
>
|
||||
<Button>{t('admin:submissions')}</Button>
|
||||
</Link>,
|
||||
@ -147,9 +147,9 @@ const Index: NextPage = () => {
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={() => {
|
||||
onFinishFailed={async () => {
|
||||
// TODO process errors
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
|
||||
@ -70,14 +70,14 @@ const Submissions: NextPage = () => {
|
||||
{
|
||||
title: t('submission:created'),
|
||||
dataIndex: 'created',
|
||||
render(date) {
|
||||
render(date: string) {
|
||||
return <DateTime date={date} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('submission:lastModified'),
|
||||
dataIndex: 'lastModified',
|
||||
render(date) {
|
||||
render(date: string) {
|
||||
return <TimeAgo date={date} />
|
||||
},
|
||||
},
|
||||
@ -94,15 +94,24 @@ const Submissions: NextPage = () => {
|
||||
{
|
||||
href: '/admin/forms/[id]',
|
||||
name: loading || !form ? t('form:loading') : t('form:mange', { title: form.title }),
|
||||
as: `/admin/forms/${router.query.id}`,
|
||||
as: `/admin/forms/${router.query.id as string}`,
|
||||
},
|
||||
]}
|
||||
padded={false}
|
||||
extra={[
|
||||
<Link key={'edit'} href={'/admin/forms/[id]'} as={`/admin/forms/${router.query.id}`}>
|
||||
<Link
|
||||
key={'edit'}
|
||||
href={'/admin/forms/[id]'}
|
||||
as={`/admin/forms/${router.query.id as string}`}
|
||||
>
|
||||
<Button>{t('submission:edit')}</Button>
|
||||
</Link>,
|
||||
<Button key={'web'} href={`/form/${router.query.id}`} target={'_blank'} type={'primary'}>
|
||||
<Button
|
||||
key={'web'}
|
||||
href={`/form/${router.query.id as string}`}
|
||||
target={'_blank'}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('submission:add')}
|
||||
</Button>,
|
||||
]}
|
||||
@ -120,9 +129,9 @@ const Submissions: NextPage = () => {
|
||||
return record.percentageComplete > 0
|
||||
},
|
||||
}}
|
||||
onChange={(next) => {
|
||||
onChange={async (next) => {
|
||||
setPagination(next)
|
||||
refetch()
|
||||
await refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
|
||||
@ -35,12 +35,12 @@ const Create: NextPage = () => {
|
||||
})
|
||||
).data
|
||||
|
||||
message.success(t('form:created'))
|
||||
await message.success(t('form:created'))
|
||||
|
||||
router.replace('/admin/forms/[id]', `/admin/forms/${next.form.id}`)
|
||||
await router.replace('/admin/forms/[id]', `/admin/forms/${next.form.id}`)
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error(t('form:creationError'))
|
||||
await message.error(t('form:creationError'))
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
@ -65,9 +65,9 @@ const Create: NextPage = () => {
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={() => {
|
||||
onFinishFailed={async () => {
|
||||
// TODO process errors
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
|
||||
@ -15,6 +15,7 @@ import { TimeAgo } from 'components/time.ago'
|
||||
import { withAuth } from 'components/with.auth'
|
||||
import {
|
||||
ADMIN_PAGER_FORM_QUERY,
|
||||
AdminPagerFormEntryAdminQueryData,
|
||||
AdminPagerFormEntryQueryData,
|
||||
AdminPagerFormQueryData,
|
||||
AdminPagerFormQueryVariables,
|
||||
@ -56,7 +57,7 @@ const Index: NextPage = () => {
|
||||
AdminFormDeleteMutationVariables
|
||||
>(ADMIN_FORM_DELETE_MUTATION)
|
||||
|
||||
const deleteForm = async (form) => {
|
||||
const deleteForm = async (form: AdminFormDeleteMutationVariables) => {
|
||||
try {
|
||||
await executeDelete({
|
||||
variables: {
|
||||
@ -70,9 +71,9 @@ const Index: NextPage = () => {
|
||||
setEntries(next)
|
||||
}
|
||||
|
||||
message.success(t('form:deleted'))
|
||||
await message.success(t('form:deleted'))
|
||||
} catch (e) {
|
||||
message.error(t('form:deleteError'))
|
||||
await message.error(t('form:deleteError'))
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +81,7 @@ const Index: NextPage = () => {
|
||||
{
|
||||
title: t('form:row.isLive'),
|
||||
dataIndex: 'isLive',
|
||||
render(live) {
|
||||
render(live: boolean) {
|
||||
return <FormIsLive isLive={live} />
|
||||
},
|
||||
},
|
||||
@ -91,7 +92,7 @@ const Index: NextPage = () => {
|
||||
{
|
||||
title: t('form:row.admin'),
|
||||
dataIndex: 'admin',
|
||||
render(user) {
|
||||
render(user: AdminPagerFormEntryAdminQueryData) {
|
||||
return (
|
||||
<Link href={'/admin/users/[id]'} as={`/admin/users/${user.id}`}>
|
||||
<Tooltip title={user.email}>
|
||||
@ -104,28 +105,28 @@ const Index: NextPage = () => {
|
||||
{
|
||||
title: t('form:row.language'),
|
||||
dataIndex: 'language',
|
||||
render(lang) {
|
||||
render(lang: string) {
|
||||
return t(`language:${lang}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('form:row.created'),
|
||||
dataIndex: 'created',
|
||||
render(date) {
|
||||
render(date: string) {
|
||||
return <DateTime date={date} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('form:row.lastModified'),
|
||||
dataIndex: 'lastModified',
|
||||
render(date) {
|
||||
render(date: string) {
|
||||
return <TimeAgo date={date} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('form:row.menu'),
|
||||
align: 'right',
|
||||
render(row) {
|
||||
render(row: AdminPagerFormEntryQueryData) {
|
||||
return (
|
||||
<Space>
|
||||
<Link href={'/admin/forms/[id]/submissions'} as={`/admin/forms/${row.id}/submissions`}>
|
||||
@ -182,9 +183,9 @@ const Index: NextPage = () => {
|
||||
dataSource={entries}
|
||||
rowKey={'id'}
|
||||
pagination={pagination}
|
||||
onChange={(next) => {
|
||||
onChange={async (next) => {
|
||||
setPagination(next)
|
||||
refetch()
|
||||
await refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
|
||||
@ -49,10 +49,10 @@ const Profile: NextPage = () => {
|
||||
|
||||
form.setFieldsValue(next)
|
||||
|
||||
message.success(t('profile:updated'))
|
||||
await message.success(t('profile:updated'))
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error(t('profile:updateError'))
|
||||
await message.error(t('profile:updateError'))
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
@ -73,9 +73,9 @@ const Profile: NextPage = () => {
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={() => {
|
||||
onFinishFailed={async () => {
|
||||
// TODO process errors
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
|
||||
@ -53,10 +53,10 @@ const Index: NextPage = () => {
|
||||
|
||||
form.setFieldsValue(next)
|
||||
|
||||
message.success(t('user:updated'))
|
||||
await message.success(t('user:updated'))
|
||||
} catch (e) {
|
||||
console.error('failed to save', e)
|
||||
message.error(t('user:updateError'))
|
||||
await message.error(t('user:updateError'))
|
||||
}
|
||||
|
||||
setSaving(false)
|
||||
@ -81,9 +81,9 @@ const Index: NextPage = () => {
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={save}
|
||||
onFinishFailed={() => {
|
||||
onFinishFailed={async () => {
|
||||
// TODO process errors
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}}
|
||||
labelCol={{
|
||||
xs: { span: 24 },
|
||||
|
||||
@ -50,7 +50,7 @@ const Index: NextPage = () => {
|
||||
AdminUserDeleteMutationVariables
|
||||
>(ADMIN_USER_DELETE_MUTATION)
|
||||
|
||||
const deleteUser = async (form) => {
|
||||
const deleteUser = async (form: AdminPagerUserEntryQueryData) => {
|
||||
try {
|
||||
await executeDelete({
|
||||
variables: {
|
||||
@ -63,9 +63,9 @@ const Index: NextPage = () => {
|
||||
} else {
|
||||
setEntries(next)
|
||||
}
|
||||
message.success(t('user:deleted'))
|
||||
await message.success(t('user:deleted'))
|
||||
} catch (e) {
|
||||
message.error(t('user:deleteError'))
|
||||
await message.error(t('user:deleteError'))
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,27 +73,27 @@ const Index: NextPage = () => {
|
||||
{
|
||||
title: t('user:row.roles'),
|
||||
dataIndex: 'roles',
|
||||
render(roles) {
|
||||
render(roles: string[]) {
|
||||
return <UserRole roles={roles} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('user:row.email'),
|
||||
render(row) {
|
||||
render(row: AdminPagerUserEntryQueryData) {
|
||||
return <Tag color={row.verifiedEmail ? 'lime' : 'volcano'}>{row.email}</Tag>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('user:row.created'),
|
||||
dataIndex: 'created',
|
||||
render(date) {
|
||||
render(date: string) {
|
||||
return <DateTime date={date} />
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('user:row.menu'),
|
||||
align: 'right',
|
||||
render(row) {
|
||||
render(row: AdminPagerUserEntryQueryData) {
|
||||
return (
|
||||
<Space>
|
||||
<Link href={'/admin/users/[id]'} as={`/admin/users/${row.id}`}>
|
||||
@ -130,9 +130,9 @@ const Index: NextPage = () => {
|
||||
dataSource={entries}
|
||||
rowKey={'id'}
|
||||
pagination={pagination}
|
||||
onChange={(next) => {
|
||||
onChange={async (next) => {
|
||||
setPagination(next)
|
||||
refetch()
|
||||
await refetch()
|
||||
}}
|
||||
/>
|
||||
</Structure>
|
||||
|
||||
@ -38,7 +38,9 @@ const Index: NextPage<Props> = () => {
|
||||
}
|
||||
|
||||
if (i18n.language !== data.form.language) {
|
||||
i18n.changeLanguage(data.form.language)
|
||||
i18n
|
||||
.changeLanguage(data.form.language)
|
||||
.catch((e: Error) => console.error('failed to change language', e))
|
||||
}
|
||||
}, [data])
|
||||
|
||||
@ -98,8 +100,8 @@ const Index: NextPage<Props> = () => {
|
||||
key={field.id}
|
||||
field={field}
|
||||
design={design}
|
||||
save={(values) => {
|
||||
submission.setField(field.id, values[field.id])
|
||||
save={async (values: { [key: string]: unknown }) => {
|
||||
await submission.setField(field.id, values[field.id])
|
||||
|
||||
if (data.form.fields.length === i + 1) {
|
||||
submission.finish()
|
||||
@ -144,7 +146,7 @@ const Index: NextPage<Props> = () => {
|
||||
)
|
||||
}
|
||||
|
||||
Index.getInitialProps = async ({ query }) => {
|
||||
Index.getInitialProps = ({ query }) => {
|
||||
return {
|
||||
id: query.id as string,
|
||||
}
|
||||
|
||||
@ -7,7 +7,12 @@ import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPage } from '../components/loading.page'
|
||||
|
||||
const { publicRuntimeConfig } = getConfig()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { publicRuntimeConfig } = getConfig() as {
|
||||
publicRuntimeConfig: {
|
||||
spa: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const Index: NextPage = () => {
|
||||
const router = useRouter()
|
||||
@ -17,14 +22,17 @@ const Index: NextPage = () => {
|
||||
if (router.pathname !== window.location.pathname) {
|
||||
let href = router.asPath
|
||||
const as = router.asPath
|
||||
const possible = [/(\/form\/)[^/]+/i, /(\/admin\/forms\/)[^/]+/i, /(\/admin\/users\/)[^/]+/i]
|
||||
|
||||
;[/(\/form\/)[^/]+/i, /(\/admin\/forms\/)[^/]+/i, /(\/admin\/users\/)[^/]+/i].forEach((r) => {
|
||||
possible.forEach((r) => {
|
||||
if (r.test(as)) {
|
||||
href = href.replace(r, '$1[id]')
|
||||
}
|
||||
})
|
||||
|
||||
router.replace(href, as)
|
||||
router.replace(href, as).catch((e: Error) => {
|
||||
console.error('failed redirect', e)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -50,7 +58,7 @@ const Index: NextPage = () => {
|
||||
width: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
src={require('../assets/images/logo_white.png')}
|
||||
src={require('../assets/images/logo_white.png') as string}
|
||||
/>
|
||||
|
||||
<AuthFooter />
|
||||
|
||||
@ -4,7 +4,11 @@ import { useForm } from 'antd/lib/form/Form'
|
||||
import { AuthFooter } from 'components/auth/footer'
|
||||
import { AuthLayout } from 'components/auth/layout'
|
||||
import { setAuth } from 'components/with.auth'
|
||||
import { LOGIN_MUTATION } from 'graphql/mutation/login.mutation'
|
||||
import {
|
||||
LOGIN_MUTATION,
|
||||
LoginMutationData,
|
||||
LoginMutationVariables,
|
||||
} from 'graphql/mutation/login.mutation'
|
||||
import { NextPage } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
@ -16,29 +20,29 @@ const Index: NextPage = () => {
|
||||
const [form] = useForm()
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [login] = useMutation(LOGIN_MUTATION)
|
||||
const [login] = useMutation<LoginMutationData, LoginMutationVariables>(LOGIN_MUTATION)
|
||||
|
||||
const finish = async (data) => {
|
||||
const finish = async (data: LoginMutationVariables) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await login({
|
||||
variables: data,
|
||||
})
|
||||
|
||||
await setAuth(result.data.tokens.access, result.data.tokens.refresh)
|
||||
setAuth(result.data.tokens.access, result.data.tokens.refresh)
|
||||
|
||||
message.success(t('login:welcomeBack'))
|
||||
await message.success(t('login:welcomeBack'))
|
||||
|
||||
router.push('/admin')
|
||||
await router.push('/admin')
|
||||
} catch (e) {
|
||||
message.error(t('login:invalidLoginCredentials'))
|
||||
await message.error(t('login:invalidLoginCredentials'))
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const failed = () => {
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
const failed = async () => {
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}
|
||||
|
||||
return (
|
||||
@ -55,7 +59,7 @@ const Index: NextPage = () => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={require('../../assets/images/logo_white_small.png')}
|
||||
src={require('../../assets/images/logo_white_small.png') as string}
|
||||
alt={'OhMyForm'}
|
||||
style={{
|
||||
display: 'block',
|
||||
|
||||
@ -4,7 +4,12 @@ import { useForm } from 'antd/lib/form/Form'
|
||||
import { AuthFooter } from 'components/auth/footer'
|
||||
import { AuthLayout } from 'components/auth/layout'
|
||||
import { setAuth } from 'components/with.auth'
|
||||
import { REGISTER_MUTATION } from 'graphql/mutation/register.mutation'
|
||||
import {
|
||||
REGISTER_MUTATION,
|
||||
RegisterMutationData,
|
||||
RegisterMutationVariables,
|
||||
RegisterUserData,
|
||||
} from 'graphql/mutation/register.mutation'
|
||||
import { NextPage } from 'next'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
@ -20,9 +25,9 @@ const Register: NextPage = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { data } = useQuery<SettingsQueryData>(SETTINGS_QUERY)
|
||||
|
||||
const [register] = useMutation(REGISTER_MUTATION)
|
||||
const [register] = useMutation<RegisterMutationData, RegisterMutationVariables>(REGISTER_MUTATION)
|
||||
|
||||
const finish = async (data) => {
|
||||
const finish = async (data: RegisterUserData) => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
@ -32,19 +37,19 @@ const Register: NextPage = () => {
|
||||
},
|
||||
})
|
||||
|
||||
await setAuth(result.data.tokens.access, result.data.tokens.refresh)
|
||||
setAuth(result.data.tokens.access, result.data.tokens.refresh)
|
||||
|
||||
message.success(t('register:welcome'))
|
||||
await message.success(t('register:welcome'))
|
||||
|
||||
router.push('/')
|
||||
await router.push('/')
|
||||
} catch (e) {
|
||||
message.error(t('register:credentialsAlreadyInUse'))
|
||||
await message.error(t('register:credentialsAlreadyInUse'))
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const failed = () => {
|
||||
message.error(t('validation:mandatoryFieldsMissing'))
|
||||
const failed = async () => {
|
||||
await message.error(t('validation:mandatoryFieldsMissing'))
|
||||
}
|
||||
|
||||
if (data && data.disabledSignUp.value) {
|
||||
@ -65,7 +70,7 @@ const Register: NextPage = () => {
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={require('../assets/images/logo_white_small.png')}
|
||||
src={require('../assets/images/logo_white_small.png') as string}
|
||||
alt={'OhMyForm'}
|
||||
style={{
|
||||
display: 'block',
|
||||
|
||||
@ -8,10 +8,10 @@ export interface State {
|
||||
auth: AuthState
|
||||
}
|
||||
|
||||
const root = (state: State, action: AnyAction) => {
|
||||
const root = (state: State, action: AnyAction): State => {
|
||||
switch (action.type) {
|
||||
case HYDRATE:
|
||||
return { ...state, ...action.payload }
|
||||
return { ...state, ...action.payload } as State
|
||||
}
|
||||
|
||||
const combined = combineReducers({
|
||||
|
||||
68
yarn.lock
68
yarn.lock
@ -1169,6 +1169,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@next/react-refresh-utils/-/react-refresh-utils-9.4.0.tgz#6f8732e1909fb12f6dca8a82b7fc4b79c6c8cfcd"
|
||||
integrity sha512-AaEC/diS2two2JLsEItGhuAux8UfPo0o34/7l1SIw0t4SYunUYJsxM/Y55OR2ljiVn9ffKR1n1U9IEQhsK80jw==
|
||||
|
||||
"@types/axios@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
|
||||
integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
|
||||
dependencies:
|
||||
axios "*"
|
||||
|
||||
"@types/color-name@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
@ -1179,11 +1186,40 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
|
||||
|
||||
"@types/hoist-non-react-statics@*":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
|
||||
"@types/json-schema@^7.0.3":
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
||||
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
|
||||
|
||||
"@types/lifeomic__axios-fetch@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/lifeomic__axios-fetch/-/lifeomic__axios-fetch-1.4.0.tgz#c3c6e6fd1af7507cbb992864891a809e1bc1d347"
|
||||
integrity sha512-i6FGbhL840p71LxT4/6qq7LbWfpO1eMRFIfNAYEFqo3WkDI2O+BrJd7ac+FEBy818u+s6gGh5cqw6d1pmwJ3Dw==
|
||||
dependencies:
|
||||
"@types/node-fetch" "*"
|
||||
axios "^0.19.0"
|
||||
|
||||
"@types/node-fetch@*", "@types/node-fetch@^2.5.7":
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
|
||||
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "14.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.12.tgz#9c1d8ffb8084e8936603a6122a7649e40e68e04b"
|
||||
integrity sha512-/sjzehvjkkpvLpYtN6/2dv5kg41otMGuHQUt9T2aiAuIfleCQRQHXXzF1eAw/qkZTj5Kcf4JSTf7EIizHocy6Q==
|
||||
|
||||
"@types/node@>=6", "@types/node@^14.0.1":
|
||||
version "14.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b"
|
||||
@ -1199,7 +1235,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
|
||||
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
|
||||
|
||||
"@types/react@^16.9.35":
|
||||
"@types/react-native@*":
|
||||
version "0.62.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.13.tgz#c688c5ae03e426f927f7e1fa1a59cd067f35d1c2"
|
||||
integrity sha512-hs4/tSABhcJx+J8pZhVoXHrOQD89WFmbs8QiDLNSA9zNrD46pityAuBWuwk1aMjPk9I3vC5ewkJroVRHgRIfdg==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^16.9.35":
|
||||
version "16.9.35"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||
@ -1207,6 +1250,16 @@
|
||||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/styled-components@^5.1.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.0.tgz#24d3412ba5395aa06e14fbc93c52f9454cebd0d6"
|
||||
integrity sha512-ZFlLCuwF5r+4Vb7JUmd+Yr2S0UBdBGmI7ctFTgJMypIp3xOHI4LCFVn2dKMvpk6xDB2hLRykrEWMBwJEpUAUIQ==
|
||||
dependencies:
|
||||
"@types/hoist-non-react-statics" "*"
|
||||
"@types/react" "*"
|
||||
"@types/react-native" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/swiper@^5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/swiper/-/swiper-5.3.1.tgz#cc0c7c6f84e330ff50bec0a3ed0d172751e9d1fc"
|
||||
@ -1836,7 +1889,7 @@ atob@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
axios@^0.19.2:
|
||||
axios@*, axios@^0.19.0, axios@^0.19.2:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
|
||||
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
|
||||
@ -2411,7 +2464,7 @@ color@^3.0.0:
|
||||
color-convert "^1.9.1"
|
||||
color-string "^1.5.2"
|
||||
|
||||
combined-stream@^1.0.6:
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
@ -3545,6 +3598,15 @@ form-data@^2.3.3:
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
|
||||
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fragment-cache@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user