mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 12:50:38 +00:00
503 lines
14 KiB
JavaScript
503 lines
14 KiB
JavaScript
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
import { useFormik } from 'formik';
|
|
import { FormattedMessage as T } from 'components';
|
|
import intl from 'react-intl-universal';
|
|
|
|
import {useHistory } from 'react-router-dom';
|
|
import {
|
|
InputGroup,
|
|
FormGroup,
|
|
Intent,
|
|
Button,
|
|
MenuItem,
|
|
Classes,
|
|
HTMLSelect,
|
|
Menu,
|
|
H5,
|
|
H6,
|
|
} from '@blueprintjs/core';
|
|
import { Row, Col } from 'react-grid-system';
|
|
import { ReactSortable } from 'react-sortablejs';
|
|
import * as Yup from 'yup';
|
|
import { pick, get } from 'lodash';
|
|
import Icon from 'components/Icon';
|
|
import ErrorMessage from 'components/ErrorMessage';
|
|
import AppToaster from 'components/AppToaster';
|
|
import { If } from 'components';
|
|
import ViewFormContainer from 'containers/Views/ViewForm.container.js';
|
|
|
|
function ViewForm({
|
|
requestSubmitView,
|
|
requestEditView,
|
|
onDelete,
|
|
|
|
viewId,
|
|
viewMeta,
|
|
|
|
resourceName,
|
|
resourceColumns,
|
|
resourceFields,
|
|
resourceMetadata,
|
|
|
|
changePageSubtitle,
|
|
}) {
|
|
const intl = useIntl();
|
|
const history = useHistory();
|
|
|
|
useEffect(() => {
|
|
changePageSubtitle(resourceMetadata.label);
|
|
return () => {
|
|
changePageSubtitle('');
|
|
};
|
|
}, [changePageSubtitle,resourceMetadata.label]);
|
|
|
|
const [draggedColumns, setDraggedColumn] = useState([
|
|
...(viewMeta && viewMeta.columns ? viewMeta.columns : []),
|
|
]);
|
|
|
|
const draggedColumnsIds = useMemo(() => draggedColumns.map((c) => c.id), [
|
|
draggedColumns,
|
|
]);
|
|
|
|
const [availableColumns, setAvailableColumns] = useState([
|
|
...(viewMeta && viewMeta.columns
|
|
? resourceColumns.filter(
|
|
(column) => draggedColumnsIds.indexOf(column.id) === -1
|
|
)
|
|
: resourceColumns),
|
|
]);
|
|
|
|
const defaultViewRole = useMemo(
|
|
() => ({
|
|
field_key: '',
|
|
comparator: '',
|
|
value: '',
|
|
index: 1,
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const validationSchema = Yup.object().shape({
|
|
resource_name: Yup.string().required(),
|
|
name: Yup.string().required().label(intl.formatMessage({id:'name_'})),
|
|
logic_expression: Yup.string().required().label(intl.formatMessage({id:'logic_expression'})),
|
|
roles: Yup.array().of(
|
|
Yup.object().shape({
|
|
comparator: Yup.string().required(),
|
|
value: Yup.string().required(),
|
|
field_key: Yup.string().required(),
|
|
index: Yup.number().required(),
|
|
})
|
|
),
|
|
columns: Yup.array().of(
|
|
Yup.object().shape({
|
|
key: Yup.string().required(),
|
|
index: Yup.string().required(),
|
|
})
|
|
),
|
|
});
|
|
|
|
const initialEmptyForm = useMemo(
|
|
() => ({
|
|
resource_name: resourceName || '',
|
|
name: '',
|
|
logic_expression: '',
|
|
roles: [defaultViewRole],
|
|
columns: [],
|
|
}),
|
|
[defaultViewRole, resourceName]
|
|
);
|
|
|
|
const initialForm = useMemo(
|
|
() => ({
|
|
...initialEmptyForm,
|
|
...(viewMeta
|
|
? {
|
|
...viewMeta,
|
|
resource_name: viewMeta.resource?.name || resourceName,
|
|
}
|
|
: {}),
|
|
}),
|
|
[initialEmptyForm, viewMeta, resourceName]
|
|
);
|
|
|
|
const {
|
|
values,
|
|
errors,
|
|
touched,
|
|
setFieldValue,
|
|
getFieldProps,
|
|
handleSubmit,
|
|
isSubmitting,
|
|
} = useFormik({
|
|
enableReinitialize: true,
|
|
validationSchema: validationSchema,
|
|
initialValues: {
|
|
roles: [],
|
|
...pick(initialForm, Object.keys(initialEmptyForm)),
|
|
logic_expression: initialForm.roles_logic_expression || '',
|
|
roles: [
|
|
...initialForm.roles.map((role) => {
|
|
return {
|
|
...pick(role, Object.keys(defaultViewRole)),
|
|
field_key: role.field ? role.field.key : '',
|
|
};
|
|
}),
|
|
],
|
|
},
|
|
onSubmit: (values, { setSubmitting }) => {
|
|
if (viewMeta && viewMeta.id) {
|
|
requestEditView(viewMeta.id, values).then((response) => {
|
|
AppToaster.show({
|
|
message: 'the_view_has_been_edited',
|
|
intent: Intent.SUCCESS,
|
|
});
|
|
history.push(
|
|
`${resourceMetadata.baseRoute}/${viewMeta.id}/custom_view`
|
|
);
|
|
setSubmitting(false);
|
|
});
|
|
} else {
|
|
requestSubmitView(values).then((response) => {
|
|
AppToaster.show({
|
|
message: 'the_view_has_been_submit',
|
|
intent: Intent.SUCCESS,
|
|
});
|
|
history.push(
|
|
`${resourceMetadata.baseRoute}/${viewMeta.id}/custom_view`
|
|
);
|
|
setSubmitting(false);
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
setFieldValue(
|
|
'columns',
|
|
draggedColumns.map((column, index) => ({
|
|
index,
|
|
key: column.key,
|
|
}))
|
|
);
|
|
}, [setFieldValue, draggedColumns]);
|
|
|
|
const conditionalsItems = useMemo(
|
|
() => [
|
|
{ value: 'and', label: 'AND' },
|
|
{ value: 'or', label: 'OR' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const whenConditionalsItems = useMemo(
|
|
() => [{ value: '', label: 'When' }],
|
|
[]
|
|
);
|
|
|
|
// Compatotors items.
|
|
const compatatorsItems = useMemo(
|
|
() => [
|
|
{ value: '', label: 'Compatator' },
|
|
{ value: 'equals', label: 'Equals' },
|
|
{ value: 'not_equal', label: 'Not Equal' },
|
|
{ value: 'contain', label: 'Contain' },
|
|
{ value: 'not_contain', label: 'Not Contain' },
|
|
],
|
|
[]
|
|
);
|
|
|
|
// Resource fields.
|
|
const resourceFieldsOptions = useMemo(
|
|
() => [
|
|
{ value: '', label: 'Select a field' },
|
|
...resourceFields.map((field) => ({
|
|
value: field.key,
|
|
label: field.label_name,
|
|
})),
|
|
],
|
|
[resourceFields]
|
|
);
|
|
|
|
// Account item of select accounts field.
|
|
const selectItem = (item, { handleClick, modifiers, query }) => {
|
|
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
|
|
};
|
|
// Handle click new condition button.
|
|
const onClickNewRole = useCallback(() => {
|
|
setFieldValue('roles', [
|
|
...values.roles,
|
|
{
|
|
...defaultViewRole,
|
|
index: values.roles.length + 1,
|
|
},
|
|
]);
|
|
}, [defaultViewRole, setFieldValue, values]);
|
|
|
|
// Handle click remove view role button.
|
|
const onClickRemoveRole = useCallback(
|
|
(viewRole, index) => {
|
|
let viewRoles = [...values.roles];
|
|
|
|
// Can't continue if view roles equals or less than 1.
|
|
if (viewRoles.length > 1) {
|
|
viewRoles.splice(index, 1);
|
|
|
|
setFieldValue(
|
|
'roles',
|
|
viewRoles.map((role) => {
|
|
return role;
|
|
})
|
|
);
|
|
}
|
|
},
|
|
[values, setFieldValue]
|
|
);
|
|
|
|
const onClickDeleteView = useCallback(() => {
|
|
onDelete && onDelete(viewMeta);
|
|
}, [onDelete, viewMeta]);
|
|
|
|
const hasError = (path) => get(errors, path) && get(touched, path);
|
|
|
|
const handleClickCancelBtn = () => {
|
|
history.goBack();
|
|
};
|
|
|
|
return (
|
|
<div class='view-form'>
|
|
<form onSubmit={handleSubmit}>
|
|
<div class='view-form--name-section'>
|
|
<Row>
|
|
<Col sm={8}>
|
|
<FormGroup
|
|
label={<T id={'view_name'} />}
|
|
className={'form-group--name'}
|
|
intent={errors.name && touched.name && Intent.DANGER}
|
|
helperText={
|
|
<ErrorMessage {...{ errors, touched }} name={'name'} />
|
|
}
|
|
inline={true}
|
|
fill={true}
|
|
>
|
|
<InputGroup
|
|
intent={errors.name && touched.name && Intent.DANGER}
|
|
fill={true}
|
|
{...getFieldProps('name')}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
<H5 className='mb2'>Define the conditionals</H5>
|
|
|
|
{values.roles.map((role, index) => (
|
|
<Row class='view-form__role-conditional'>
|
|
<Col sm={2} class='flex'>
|
|
<div class='mr2 pt1 condition-number'>{index + 1}</div>
|
|
{index === 0 ? (
|
|
<HTMLSelect
|
|
options={whenConditionalsItems}
|
|
className={Classes.FILL}
|
|
/>
|
|
) : (
|
|
<HTMLSelect
|
|
options={conditionalsItems}
|
|
className={Classes.FILL}
|
|
/>
|
|
)}
|
|
</Col>
|
|
|
|
<Col sm={2}>
|
|
<FormGroup
|
|
intent={hasError(`roles[${index}].field_key`) && Intent.DANGER}
|
|
>
|
|
<HTMLSelect
|
|
options={resourceFieldsOptions}
|
|
value={role.field_key}
|
|
className={Classes.FILL}
|
|
{...getFieldProps(`roles[${index}].field_key`)}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
<Col sm={2}>
|
|
<FormGroup
|
|
intent={hasError(`roles[${index}].comparator`) && Intent.DANGER}
|
|
>
|
|
<HTMLSelect
|
|
options={compatatorsItems}
|
|
value={role.comparator}
|
|
className={Classes.FILL}
|
|
{...getFieldProps(`roles[${index}].comparator`)}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
|
|
<Col sm={5} class='flex'>
|
|
<FormGroup
|
|
intent={hasError(`roles[${index}].value`) && Intent.DANGER}
|
|
>
|
|
<InputGroup
|
|
placeholder={intl.get('value')}
|
|
{...getFieldProps(`roles[${index}].value`)}
|
|
/>
|
|
</FormGroup>
|
|
|
|
<Button
|
|
icon={<Icon icon='times-circle' iconSize={14} />}
|
|
iconSize={14}
|
|
className='ml2'
|
|
minimal={true}
|
|
intent={Intent.DANGER}
|
|
onClick={() => onClickRemoveRole(role, index)}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
))}
|
|
|
|
<div className={'view-form__role-conditions-actions'}>
|
|
<Button
|
|
minimal={true}
|
|
intent={Intent.PRIMARY}
|
|
onClick={onClickNewRole}
|
|
>
|
|
<T id={'new_conditional'} />
|
|
</Button>
|
|
</div>
|
|
|
|
<div class='view-form--logic-expression-section'>
|
|
<Row>
|
|
<Col sm={8}>
|
|
<FormGroup
|
|
label={intl.get('Logic Expression')}
|
|
className={'form-group--logic-expression'}
|
|
intent={
|
|
errors.logic_expression &&
|
|
touched.logic_expression &&
|
|
Intent.DANGER
|
|
}
|
|
helperText={
|
|
<ErrorMessage
|
|
{...{ errors, touched }}
|
|
name='logic_expression'
|
|
/>
|
|
}
|
|
inline={true}
|
|
fill={true}
|
|
>
|
|
<InputGroup
|
|
intent={
|
|
errors.logic_expression &&
|
|
touched.logic_expression &&
|
|
Intent.DANGER
|
|
}
|
|
fill={true}
|
|
{...getFieldProps('logic_expression')}
|
|
/>
|
|
</FormGroup>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
<H5 className={'mb2'}>Columns Preferences</H5>
|
|
|
|
<div class='dragable-columns'>
|
|
<Row gutterWidth={14}>
|
|
<Col sm={4} className='dragable-columns__column'>
|
|
<H6 className='dragable-columns__title'>Available Columns</H6>
|
|
|
|
<InputGroup
|
|
placeholder={intl.get('search')}
|
|
leftIcon='search'
|
|
/>
|
|
|
|
<div class='dragable-columns__items'>
|
|
<Menu>
|
|
<ReactSortable
|
|
list={availableColumns}
|
|
setList={setAvailableColumns}
|
|
group='shared-group-name'
|
|
>
|
|
{availableColumns.map((field) => (
|
|
<MenuItem key={field.id} text={field.label} />
|
|
))}
|
|
</ReactSortable>
|
|
</Menu>
|
|
</div>
|
|
</Col>
|
|
|
|
<Col sm={1}>
|
|
<div class='dragable-columns__arrows'>
|
|
<div>
|
|
<Icon
|
|
icon='arrow-circle-left'
|
|
iconSize={30}
|
|
color='#cecece'
|
|
/>
|
|
</div>
|
|
<div class='mt2'>
|
|
<Icon
|
|
icon='arrow-circle-right'
|
|
iconSize={30}
|
|
color='#cecece'
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Col>
|
|
|
|
<Col sm={4} className='dragable-columns__column'>
|
|
<H6 className='dragable-columns__title'>Selected Columns</H6>
|
|
<InputGroup
|
|
placeholder={intl.get('search')}
|
|
leftIcon='search'
|
|
/>
|
|
|
|
<div class='dragable-columns__items'>
|
|
<Menu>
|
|
<ReactSortable
|
|
list={draggedColumns}
|
|
setList={setDraggedColumn}
|
|
group='shared-group-name'
|
|
>
|
|
{draggedColumns.map((field) => (
|
|
<MenuItem key={field.id} text={field.label} />
|
|
))}
|
|
</ReactSortable>
|
|
</Menu>
|
|
</div>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
|
|
<div class='form__floating-footer'>
|
|
<Button intent={Intent.PRIMARY} type='submit' disabled={isSubmitting}>
|
|
<T id={'submit'} />
|
|
</Button>
|
|
|
|
<Button
|
|
intent={Intent.NONE}
|
|
className='ml1'
|
|
onClick={handleClickCancelBtn}
|
|
>
|
|
<T id={'cancel'} />
|
|
</Button>
|
|
|
|
<If condition={viewMeta && viewMeta.id}>
|
|
<Button
|
|
intent={Intent.DANGER}
|
|
onClick={onClickDeleteView}
|
|
className={'right mr2'}
|
|
>
|
|
<T id={'delete'} />
|
|
</Button>
|
|
</If>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ViewFormContainer(ViewForm);
|