9c4c325e5a
* remove submodules * add api and ui files * update github actions * use sparse checkout * update node setup * update checkout * update docker * change permissions * update mariadb health check * update changelog
245 lines
6.8 KiB
TypeScript
245 lines
6.8 KiB
TypeScript
import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'
|
|
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons/lib'
|
|
import { Button, Card, Checkbox, Form, Input, Popconfirm, Popover, Space, Tag, Tooltip } from 'antd'
|
|
import { FormInstance } from 'antd/lib/form'
|
|
import { FieldData } from 'rc-field-form/lib/interface'
|
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { FormFieldFragment, FormFieldLogicFragment } from '../../../graphql/fragment/form.fragment'
|
|
import { fieldTypes } from '../types'
|
|
import { LogicBlock } from './logic.block'
|
|
|
|
interface Props {
|
|
form: FormInstance
|
|
fields: FormFieldFragment[]
|
|
onChangeFields: (fields: FormFieldFragment[]) => void
|
|
field: FieldData
|
|
remove: (index: number) => void
|
|
move: (from: number, to: number) => void
|
|
index: number
|
|
}
|
|
|
|
export const FieldCard: React.FC<Props> = ({
|
|
form,
|
|
field,
|
|
fields,
|
|
onChangeFields,
|
|
remove,
|
|
move,
|
|
index,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
|
|
const type = form.getFieldValue([
|
|
'form', 'fields', field.name as string, 'type',
|
|
]) as string
|
|
const TypeComponent = (fieldTypes[type] || fieldTypes['textfield']).adminFormField()
|
|
|
|
const [shouldUpdate, setShouldUpdate] = useState(false)
|
|
const [nextTitle, setNextTitle] = useState<string>(
|
|
form.getFieldValue([
|
|
'form', 'fields', field.name as string, 'title',
|
|
])
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (!shouldUpdate) {
|
|
return
|
|
}
|
|
|
|
const id = setTimeout(() => {
|
|
setShouldUpdate(false)
|
|
onChangeFields(
|
|
fields.map((field, i) => {
|
|
if (i === index) {
|
|
return {
|
|
...field,
|
|
title: nextTitle,
|
|
}
|
|
} else {
|
|
return field
|
|
}
|
|
})
|
|
)
|
|
}, 500)
|
|
|
|
return () => clearTimeout(id)
|
|
}, [
|
|
nextTitle, shouldUpdate, fields,
|
|
])
|
|
|
|
const addLogic = useCallback((add: (defaults: unknown) => void, index: number) => {
|
|
return (
|
|
<Form.Item wrapperCol={{ span: 24 }}>
|
|
<Space
|
|
style={{
|
|
width: '100%',
|
|
justifyContent: 'flex-end',
|
|
}}
|
|
>
|
|
<Button
|
|
type="dashed"
|
|
onClick={() => {
|
|
const defaults: FormFieldLogicFragment = {
|
|
id: `NEW-${Date.now()}`,
|
|
formula: null,
|
|
action: null,
|
|
jumpTo: null,
|
|
visible: null,
|
|
disable: null,
|
|
require: null,
|
|
enabled: false,
|
|
}
|
|
|
|
add(defaults)
|
|
}}
|
|
>
|
|
<PlusOutlined /> {t('form:logic.add')}
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
)
|
|
}, [])
|
|
|
|
return (
|
|
<Card
|
|
title={nextTitle}
|
|
type={'inner'}
|
|
extra={
|
|
<Space>
|
|
<Tooltip title={t('form:field.move.up')}>
|
|
<Button
|
|
type={'text'}
|
|
disabled={index === 0}
|
|
onClick={() => move(index, index - 1)}
|
|
icon={<VerticalAlignTopOutlined />}
|
|
/>
|
|
</Tooltip>
|
|
<Tooltip title={t('form:field.move.down')}>
|
|
<Button
|
|
type={'text'}
|
|
disabled={index + 1 >= form.getFieldValue(['form', 'fields']).length}
|
|
onClick={() => move(index, index + 1)}
|
|
icon={<VerticalAlignBottomOutlined />}
|
|
/>
|
|
</Tooltip>
|
|
<Form.Item noStyle shouldUpdate>
|
|
{() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
const slug = form.getFieldValue([
|
|
'form', 'fields', field.name as string, 'slug',
|
|
])
|
|
|
|
if (!slug) {
|
|
return null
|
|
}
|
|
|
|
return <Tag color={'warning'}>Slug: {slug}</Tag>
|
|
}}
|
|
</Form.Item>
|
|
<Popover
|
|
placement={'left'}
|
|
content={
|
|
<Form.Item
|
|
name={[field.name as string, 'slug']}
|
|
label={false}
|
|
rules={[
|
|
{
|
|
pattern: /^[a-z0-9_]+$/,
|
|
message: t('validation:invalidSlug'),
|
|
},
|
|
]}
|
|
help={t('type:slugInfo')}
|
|
>
|
|
<Input />
|
|
</Form.Item>
|
|
}
|
|
title={t('type:slug')}
|
|
>
|
|
<Tag color={'blue'}>{t(`type:${type}.name`)}</Tag>
|
|
</Popover>
|
|
<Popconfirm
|
|
placement={'left'}
|
|
title={t('type:confirmDelete')}
|
|
okText={t('type:deleteNow')}
|
|
okButtonProps={{ danger: true }}
|
|
onConfirm={() => {
|
|
remove(index)
|
|
onChangeFields(fields.filter((e, i) => i !== index))
|
|
}}
|
|
>
|
|
<Button danger>
|
|
<DeleteOutlined />
|
|
</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
}
|
|
>
|
|
<Form.Item name={[field.name as string, 'type']} noStyle>
|
|
<Input type={'hidden'} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('type:title')}
|
|
name={[field.name as string, 'title']}
|
|
rules={[{ required: true, message: 'Title is required' }]}
|
|
labelCol={{ span: 6 }}
|
|
>
|
|
<Input
|
|
onChange={(e) => {
|
|
setNextTitle(e.target.value)
|
|
setShouldUpdate(true)
|
|
}}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('type:description')}
|
|
name={[field.name as string, 'description']}
|
|
labelCol={{ span: 6 }}
|
|
extra={t('type:descriptionInfo')}
|
|
>
|
|
<Input.TextArea autoSize />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label={t('type:required')}
|
|
name={[field.name as string, 'required']}
|
|
labelCol={{ span: 6 }}
|
|
valuePropName={'checked'}
|
|
extra={type === 'hidden' && t('type:requiredInfo')}
|
|
>
|
|
<Checkbox />
|
|
</Form.Item>
|
|
|
|
<TypeComponent field={field} form={form} />
|
|
|
|
<Form.List name={[field.name as string, 'logic']}>
|
|
{(logic, { add, remove, move }) => {
|
|
const addAndMove = (index: number) => (defaults) => {
|
|
add(defaults)
|
|
move(fields.length, index)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{addLogic(addAndMove(0), 0)}
|
|
{logic.map((field, index) => (
|
|
<div key={field.key}>
|
|
<Form.Item wrapperCol={{ span: 24 }} noStyle>
|
|
<LogicBlock
|
|
field={field}
|
|
form={form}
|
|
fields={fields}
|
|
index={index}
|
|
remove={remove}
|
|
/>
|
|
</Form.Item>
|
|
{addLogic(addAndMove(index + 1), index + 1)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}}
|
|
</Form.List>
|
|
</Card>
|
|
)
|
|
}
|