feat: optimize view form.

This commit is contained in:
Ahmed Bouhuolia
2020-04-13 22:17:53 +02:00
parent 2e7e18bb97
commit fb6b31d922
14 changed files with 243 additions and 105 deletions

View File

@@ -121,18 +121,21 @@ function AccountsDataTable({
</Tooltip>) : row.name;
},
className: 'account_name',
width: 300,
},
{
id: 'code',
Header: 'Code',
accessor: 'code',
className: 'code',
width: 100,
},
{
id: 'type',
Header: 'Type',
accessor: 'type.name',
className: 'type',
width: 120,
},
{
id: 'normal',
@@ -145,6 +148,7 @@ function AccountsDataTable({
return (<Icon icon={`arrow-${arrowDirection}`} />);
},
className: 'normal',
width: 75,
},
{
id: 'balance',
@@ -159,6 +163,7 @@ function AccountsDataTable({
</span>) :
(<span class="placeholder">--</span>);
},
width: 150,
},
{
id: 'actions',

View File

@@ -2,14 +2,19 @@ import React from 'react';
import classnames from 'classnames';
import LoadingIndicator from 'components/LoadingIndicator';
export default function DashboardInsider({ loading, children, name }) {
export default function DashboardInsider({
loading,
children,
name,
mount = true,
}) {
return (
<div className={classnames({
'dashboard__insider': true,
'dashboard__insider--loading': loading,
[`dashboard__insider--${name}`]: !!name,
})}>
<LoadingIndicator loading={loading}>
<LoadingIndicator loading={loading} mount={mount}>
{ children }
</LoadingIndicator>
</div>

View File

@@ -1,5 +1,5 @@
import React, {useState, useEffect, useCallback, useMemo} from 'react';
import {Formik, useFormik, ErrorMessage} from "formik";
import { useFormik } from "formik";
import {useIntl} from 'react-intl';
import {
InputGroup,
@@ -16,29 +16,47 @@ import {
import {Row, Col} from 'react-grid-system';
import { ReactSortable } from 'react-sortablejs';
import * as Yup from 'yup';
import {pick} from 'lodash';
import {pick, get} from 'lodash';
import Icon from 'components/Icon';
import ViewFormConnect from 'connectors/ViewFormPage.connector';
import {compose} from 'utils';
import ErrorMessage from 'components/ErrorMessage';
import DashboardConnect from 'connectors/Dashboard.connector';
import ResourceConnect from 'connectors/Resource.connector';
import AppToaster from 'components/AppToaster';
function ViewForm({
resourceName,
columns,
fields,
viewColumns,
viewForm,
viewFormColumns,
submitView,
editView,
onDelete,
getResourceField,
getResourceColumn,
}) {
const intl = useIntl();
const [draggedColumns, setDraggedColumn] = useState([]);
const [availableColumns, setAvailableColumns] = useState(columns);
const [draggedColumns, setDraggedColumn] = useState([
...(viewForm && viewForm.columns) ? viewForm.columns.map((column) => {
return getResourceColumn(column.field_id);
}) : []
]);
const draggedColumnsIds = useMemo(() =>
draggedColumns.map(c => c.id), [draggedColumns]);
const [availableColumns, setAvailableColumns] = useState([
...(viewForm && viewForm.columns) ? columns.filter((column) =>
draggedColumnsIds.indexOf(column.id) === -1
) : columns,
]);
const defaultViewRole = useMemo(() => ({
field_key: '',
comparator: 'AND',
value: '',
index: 1,
field_key: '', comparator: '', value: '', index: 1,
}), []);
const validationSchema = Yup.object().shape({
@@ -58,20 +76,37 @@ function ViewForm({
key: Yup.string().required(),
index: Yup.string().required(),
}),
)
),
});
const initialEmptyForm = {
resource_name: '',
const initialEmptyForm = useMemo(() => ({
resource_name: resourceName || '',
name: '',
logic_expression: '',
roles: [
defaultViewRole,
],
columns: [],
};
const initialForm = { ...initialEmptyForm, ...viewForm };
}), [defaultViewRole, resourceName]);
const formik = useFormik({
const initialForm = useMemo(() =>
({
...initialEmptyForm,
...viewForm ? {
...viewForm,
resource_name: viewForm.resource.name,
} : {},
}),
[initialEmptyForm, viewForm]);
const {
values,
errors,
touched,
setFieldValue,
getFieldProps,
handleSubmit,
isSubmitting,
} = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
@@ -86,96 +121,112 @@ function ViewForm({
}),
],
},
onSubmit: (values) => {
onSubmit: (values, { setSubmitting }) => {
if (viewForm && viewForm.id) {
editView(viewForm.id, values).then((response) => {
AppToaster.show({
message: 'the_view_has_been_edited'
});
setSubmitting(false);
});
} else {
submitView(values).then((response) => {
AppToaster.show({
message: 'the_view_has_been_submit'
});
setSubmitting(false);
});
}
},
});
useEffect(() => {
formik.setFieldValue('columns',
setFieldValue('columns',
draggedColumns.map((column, index) => ({
index, key: column.key,
})));
}, [draggedColumns, formik]);
}, [setFieldValue, draggedColumns]);
const conditionalsItems = [
const conditionalsItems = useMemo(() => ([
{ value: 'and', label: 'AND' },
{ value: 'or', label: 'OR' },
];
const whenConditionalsItems = [
]), []);
const whenConditionalsItems = useMemo(() => ([
{ value: '', label: 'When' },
];
]), []);
// Compatotors items.
const compatatorsItems = [
{value: '', label: 'Select a compatator'},
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 resourceFields = useMemo(() => ([
{value: '', label: 'Select a field'},
...fields.map((field) => ({ value: field.key, label: field.labelName, })),
]), []);
...fields.map((field) => ({ value: field.key, label: field.label_name, })),
]), [fields]);
// 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(() => {
formik.setFieldValue('roles', [
...formik.values.roles,
setFieldValue('roles', [
...values.roles,
{
...defaultViewRole,
index: formik.values.roles.length + 1,
index: values.roles.length + 1,
}
]);
}, [formik, defaultViewRole]);
}, [defaultViewRole, setFieldValue, values]);
// Handle click remove view role button.
const onClickRemoveRole = useCallback((viewRole, index) => () => {
const viewRoles = [...formik.values.roles];
const viewRoles = [...values.roles];
// Can't continue if view roles equals or less than 1.
if (viewRoles.length <= 1) { return; }
viewRoles.splice(index, 1);
viewRoles.map((role, i) => {
role.index = i + 1;
return role;
});
formik.setFieldValue('roles', viewRoles);
}, [formik]);
setFieldValue('roles', viewRoles);
}, [values, setFieldValue]);
const onClickDeleteView = useCallback(() => {
onDelete && onDelete(viewForm);
}, [onDelete, viewForm]);
const hasError = (path) => get(errors, path) && get(touched, path);
console.log(errors, touched);
return (
<div class="view-form">
<form onSubmit={formik.handleSubmit}>
<form onSubmit={handleSubmit}>
<div class="view-form--name-section">
<Row>
<Col sm={8}>
<FormGroup
label={intl.formatMessage({'id': 'View Name'})}
className={'form-group--name'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.label}
intent={(errors.name && touched.name) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name={'name'} />}
inline={true}
fill={true}>
<InputGroup
intent={formik.errors.name && Intent.DANGER}
intent={(errors.name && touched.name) && Intent.DANGER}
fill={true}
{...formik.getFieldProps('name')} />
{...getFieldProps('name')} />
</FormGroup>
</Col>
</Row>
@@ -183,7 +234,7 @@ function ViewForm({
<H5 className="mb2">Define the conditionals</H5>
{formik.values.roles.map((role, index) => (
{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>
@@ -196,38 +247,36 @@ function ViewForm({
<Col sm={2}>
<FormGroup
intent={formik.getFieldMeta(`roles[${index}].field_key`).error && Intent.DANGER}>
intent={hasError(`roles[${index}].field_key`) && Intent.DANGER}>
<HTMLSelect
options={resourceFields}
value={role.field}
value={role.field_key}
className={Classes.FILL}
{...formik.getFieldProps(`roles[${index}].field_key`)} />
{...getFieldProps(`roles[${index}].field_key`)} />
</FormGroup>
</Col>
<Col sm={2}>
<FormGroup
intent={formik.getFieldMeta(`roles[${index}].comparator`).error && Intent.DANGER}>
intent={hasError(`roles[${index}].comparator`) && Intent.DANGER}>
<HTMLSelect
options={compatatorsItems}
value={role.comparator}
className={Classes.FILL}
{...formik.getFieldProps(`roles[${index}].comparator`)} />
{...getFieldProps(`roles[${index}].comparator`)} />
</FormGroup>
</Col>
<Col sm={5} class="flex">
<FormGroup>
<FormGroup
intent={hasError(`roles[${index}].value`) && Intent.DANGER}>
<InputGroup
placeholder={intl.formatMessage({'id': 'value'})}
intent={formik.getFieldMeta(`roles[${index}].value`).error && Intent.DANGER}
{...formik.getFieldProps(`roles[${index}].value`)} />
{...getFieldProps(`roles[${index}].value`)} />
</FormGroup>
<Button
icon={<Icon icon="mines" />}
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
@@ -237,12 +286,12 @@ function ViewForm({
</Row>
))}
<div class="mt1">
<div className={'view-form__role-conditions-actions'}>
<Button
minimal={true}
intent={Intent.PRIMARY}
onClick={onClickNewRole}>
+ New Conditional
New Conditional
</Button>
</div>
@@ -252,22 +301,24 @@ function ViewForm({
<FormGroup
label={intl.formatMessage({'id': 'Logic Expression'})}
className={'form-group--logic-expression'}
intent={formik.errors.logic_expression && Intent.DANGER}
helperText={formik.errors.logic_expression && formik.errors.logic_expression}
intent={(errors.logic_expression && touched.logic_expression) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name='logic_expression' />}
inline={true}
fill={true}>
<InputGroup intent={formik.errors.logic_expression && Intent.DANGER} fill={true}
{...formik.getFieldProps('logic_expression')} />
<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>
<Row gutterWidth={14}>
<Col sm={4} className="dragable-columns__column">
<H6 className="dragable-columns__title">Available Columns</H6>
@@ -317,11 +368,22 @@ function ViewForm({
</div>
<div class="form__floating-footer">
<Button intent={Intent.PRIMARY} type="submit">Submit</Button>
<Button intent={Intent.NONE} type="submit" className="ml2">Cancel</Button>
<Button
intent={Intent.PRIMARY}
type="submit"
disabled={isSubmitting}>
Submit
</Button>
<Button intent={Intent.NONE} type="submit" className="ml1">Cancel</Button>
{ (viewForm && viewForm.id) && (
<Button intent={Intent.DANGER} onClick={onClickDeleteView}>Delete</Button>
<Button
intent={Intent.DANGER}
onClick={onClickDeleteView}
className={"right mr2"}>
Delete
</Button>
) }
</div>
</form>
@@ -331,4 +393,6 @@ function ViewForm({
export default compose(
ViewFormConnect,
DashboardConnect,
ResourceConnect,
)(ViewForm);

View File

@@ -6,11 +6,16 @@ import {
import {
getResourceColumns,
getResourceFields,
getResourceColumn,
getResourceField,
} from 'store/resources/resources.reducer';
export const mapStateToProps = (state, props) => ({
getResourceColumns: (resourceSlug) => getResourceColumns(state, resourceSlug),
getResourceFields: (resourceSlug) => getResourceFields(state, resourceSlug),
getResourceColumn: (columnId) => getResourceColumn(state, columnId),
getResourceField: (fieldId) => getResourceField(state, fieldId),
});
export const mapDispatchToProps = (dispatch) => ({

View File

@@ -16,18 +16,11 @@ import t from 'store/types';
export const mapStateToProps = (state, props) => {
return {
getResourceColumns: (resourceSlug) => getResourceColumns(state, resourceSlug),
getResourceFields: (resourceSlug) => getResourceFields(state, resourceSlug),
};
};
export const mapDispatchToProps = (dispatch) => ({
changePageTitle: pageTitle => dispatch({
type: t.CHANGE_DASHBOARD_PAGE_TITLE,
pageTitle,
}),
fetchResourceFields: (resourceSlug) => dispatch(fetchResourceFields({ resourceSlug })),
fetchResourceColumns: (resourceSlug) => dispatch(fetchResourceColumns({ resourceSlug })),
fetchView: (id) => dispatch(fetchView({ id })),
submitView: (form) => dispatch(submitView({ form })),
editView: (id, form) => dispatch(editView({ id, form })),

View File

@@ -1,6 +1,6 @@
import React, {useEffect, useState} from 'react';
import React, {useEffect, useState, useCallback} from 'react';
import { useAsync } from 'react-use';
import { useParams } from 'react-router-dom';
import { useParams, useHistory } from 'react-router-dom';
import { Intent, Alert } from '@blueprintjs/core';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
@@ -23,8 +23,10 @@ function ViewFormPage({
deleteView,
}) {
const { resource_slug: resourceSlug, view_id: viewId } = useParams();
const columns = getResourceColumns('accounts');
const fields = getResourceFields('accounts');
const viewForm = (viewId) ? getViewMeta(viewId) : null;
const [stateDeleteView, setStateDeleteView] = useState(null);
@@ -35,7 +37,7 @@ function ViewFormPage({
} else {
changePageTitle('New Custom View');
}
}, [viewId]);
}, [viewId, changePageTitle]);
const fetchHook = useAsync(async () => {
await Promise.all([
@@ -47,21 +49,28 @@ function ViewFormPage({
]);
}, []);
const handleDeleteView = (view) => { setStateDeleteView(view); };
const handleCancelDeleteView = () => { setStateDeleteView(null); };
const handleDeleteView = useCallback((view) => {
setStateDeleteView(view);
}, []);
const handleConfirmDeleteView = () => {
const handleCancelDeleteView = useCallback(() => {
setStateDeleteView(null);
}, []);
const handleConfirmDeleteView = useCallback(() => {
deleteView(stateDeleteView.id).then((response) => {
setStateDeleteView(null);
AppToaster.show({
message: 'the_custom_view_has_been_deleted',
});
})
};
}, [deleteView, stateDeleteView]);
return (
<DashboardInsider name={'view-form'} loading={fetchHook.loading}>
<DashboardInsider name={'view-form'} loading={fetchHook.loading} mount={false}>
<DashboardPageContent>
<ViewForm
resourceName={resourceSlug}
columns={columns}
fields={fields}
viewForm={viewForm}

View File

@@ -6,7 +6,7 @@ export const submitView = ({ form }) => {
};
export const editView = ({ id, form }) => {
return (dispatch) => ApiService.post(`views/${id}`);
return (dispatch) => ApiService.post(`views/${id}`, form);
};
export const deleteView = ({ id }) => {

View File

@@ -1,22 +1,41 @@
import { createReducer } from "@reduxjs/toolkit";
import t from 'store/types';
import { pickItemsFromIds } from 'store/selectors'
const initialState = {
resourceFields: {
// resource name => { field_id }
},
fields: {},
columns: {},
resourceFields: {},
resourceColumns: {},
};
export default createReducer(initialState, {
[t.RESOURCE_COLUMNS_SET]: (state, action) => {
state.resourceColumns[action.resource_slug] = action.columns;
const _columns = {};
action.columns.forEach((column) => {
_columns[column.id] = column;
});
state.columns = {
...state.columns,
..._columns,
};
state.resourceColumns[action.resource_slug] = action.columns.map(c => c.id);
},
[t.RESOURCE_FIELDS_SET]: (state, action) => {
state.resourceFields[action.resource_slug] = action.fields;
const _fields = {};
action.fields.forEach((field) => {
_fields[field.id] = field;
});
state.fields = {
...state.fields,
..._fields,
};
state.resourceFields[action.resource_slug] = action.fields.map(f => f.id);
},
})
});
/**
* Retrieve resource fields of the given resource slug.
@@ -24,9 +43,10 @@ export default createReducer(initialState, {
* @param {String} resourceSlug
*/
export const getResourceFields = (state, resourceSlug) => {
const resourceFields = state.resources.resourceFields[resourceSlug];
return resourceFields ? Object.values(resourceFields) : [];
}
const resourceIds = state.resources.resourceFields[resourceSlug];
const items = state.resources.fields;
return pickItemsFromIds(items, resourceIds);
};
/**
* Retrieve resource columns of the given resource slug.
@@ -34,6 +54,25 @@ export const getResourceFields = (state, resourceSlug) => {
* @param {String} resourceSlug -
*/
export const getResourceColumns = (state, resourceSlug) => {
const resourceColumns = state.resources.resourceColumns[resourceSlug];
return resourceColumns ? Object.values(resourceColumns) : [];
}
const resourceIds = state.resources.resourceColumns[resourceSlug];
const items = state.resources.columns;
return pickItemsFromIds(items, resourceIds);
};
/**
*
* @param {State} state
* @param {Number} fieldId
*/
export const getResourceField = (state, fieldId) => {
return state.resources.fields[fieldId];
};
/**
*
* @param {State} state
* @param {Number} columnId
*/
export const getResourceColumn = (state, columnId) => {
return state.resources.columns[columnId];
};

View File

@@ -134,6 +134,14 @@
background-color: #CFDCEE;
}
}
.tr.no-results{
.td{
flex-direction: column;
padding: 20px;
color: #666;
align-items: center;
}
}
}
.tr .th.expander,

View File

@@ -80,12 +80,12 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
.#{$ns}-html-select select,
.#{$ns}-select select{
background-image: none;
border-radius: 0;
border-radius: 2px;
&,
&:hover{
background: #fff;
box-shadow: 0 0 0;
box-shadow: none;
border: 1px solid #ced4da;
}
&:focus{
@@ -149,16 +149,16 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
.#{$ns}-control {
input:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
box-shadow: none;
background-color: transparent;
background-image: none;
}
&:hover input:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
box-shadow: none;
background-color: transparent;
}
input:not(:disabled):active:checked ~ .#{$ns}-control-indicator {
box-shadow: 0 0 0 transparent;
box-shadow: none;
background: transparent;
}
input:disabled:checked ~ .#{$ns}-control-indicator {

View File

@@ -20,7 +20,7 @@
}
.tbody{
.tr .td{
.tr:not(.no-results) .td{
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}

View File

@@ -205,7 +205,6 @@
}
&__insider{
height: 100%;
&--loading{
display: flex;

View File

@@ -4,6 +4,7 @@
.dashboard__insider--view-form{
padding-left: 25px;
padding-right: 25px;
padding-bottom: 90px;
.view-form--name-section{
margin-left: -25px;
@@ -23,7 +24,7 @@
}
&--logic-expression-section{
padding: 20px 25px;
padding: 30px 25px;
margin: 1rem -25px 1.5rem;
background: #fbfafa;
@@ -33,7 +34,7 @@
}
.condition-number{
color: #666;
color: #888;
}
.#{$ns}-form-group.#{$ns}-inline{
@@ -61,7 +62,9 @@
}
&__title{
color: #666;
color: #888;
font-weight: 400;
font-size: 15px;
}
}
@@ -87,5 +90,13 @@
&__role-conditional{
margin-top: 1rem;
.bp3-form-group{
margin-bottom: 0;
}
}
&__role-conditions-actions{
margin-top: 14px;
}
}

View File

@@ -2,7 +2,7 @@
$sidebar-background: #01194E;
$sidebar-text-color: #fff;
$sidebar-width: 220px;
$sidebar-menu-item-color: #a8b1c7;
$sidebar-menu-item-color: #b8c0d5;
$sidebar-popover-submenu-bg: rgb(1, 20, 62);