diff --git a/client/src/components/Accounts/AccountsDataTable.js b/client/src/components/Accounts/AccountsDataTable.js index 6afe6fdba..4cd5097fd 100644 --- a/client/src/components/Accounts/AccountsDataTable.js +++ b/client/src/components/Accounts/AccountsDataTable.js @@ -121,18 +121,21 @@ function AccountsDataTable({ ) : 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 (); }, className: 'normal', + width: 75, }, { id: 'balance', @@ -159,6 +163,7 @@ function AccountsDataTable({ ) : (--); }, + width: 150, }, { id: 'actions', diff --git a/client/src/components/Dashboard/DashboardInsider.js b/client/src/components/Dashboard/DashboardInsider.js index 88c508852..cb1e7a193 100644 --- a/client/src/components/Dashboard/DashboardInsider.js +++ b/client/src/components/Dashboard/DashboardInsider.js @@ -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 (
- + { children }
diff --git a/client/src/components/Views/ViewForm.js b/client/src/components/Views/ViewForm.js index 0caca61e4..d44ea3ccc 100644 --- a/client/src/components/Views/ViewForm.js +++ b/client/src/components/Views/ViewForm.js @@ -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 () }; // 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 (
-
+
} inline={true} fill={true}> + {...getFieldProps('name')} /> @@ -183,7 +234,7 @@ function ViewForm({
Define the conditionals
- {formik.values.roles.map((role, index) => ( + {values.roles.map((role, index) => (
{ index + 1 }
@@ -196,38 +247,36 @@ function ViewForm({ - + intent={hasError(`roles[${index}].field_key`) && Intent.DANGER}> + {...getFieldProps(`roles[${index}].field_key`)} /> - + intent={hasError(`roles[${index}].comparator`) && Intent.DANGER}> + {...getFieldProps(`roles[${index}].comparator`)} /> - + + {...getFieldProps(`roles[${index}].value`)} />
@@ -252,22 +301,24 @@ function ViewForm({ } inline={true} fill={true}> - + -
+
Columns Preferences
- +
Available Columns
@@ -317,11 +368,22 @@ function ViewForm({
@@ -331,4 +393,6 @@ function ViewForm({ export default compose( ViewFormConnect, + DashboardConnect, + ResourceConnect, )(ViewForm); \ No newline at end of file diff --git a/client/src/connectors/Resource.connector.js b/client/src/connectors/Resource.connector.js index eee66fa20..51575cbd0 100644 --- a/client/src/connectors/Resource.connector.js +++ b/client/src/connectors/Resource.connector.js @@ -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) => ({ diff --git a/client/src/connectors/ViewFormPage.connector.js b/client/src/connectors/ViewFormPage.connector.js index ea0c1c535..c8d44d3af 100644 --- a/client/src/connectors/ViewFormPage.connector.js +++ b/client/src/connectors/ViewFormPage.connector.js @@ -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 })), diff --git a/client/src/containers/Dashboard/Views/ViewFormPage.js b/client/src/containers/Dashboard/Views/ViewFormPage.js index fb1469e6e..fdfb1993e 100644 --- a/client/src/containers/Dashboard/Views/ViewFormPage.js +++ b/client/src/containers/Dashboard/Views/ViewFormPage.js @@ -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 ( - + { }; export const editView = ({ id, form }) => { - return (dispatch) => ApiService.post(`views/${id}`); + return (dispatch) => ApiService.post(`views/${id}`, form); }; export const deleteView = ({ id }) => { diff --git a/client/src/store/resources/resources.reducer.js b/client/src/store/resources/resources.reducer.js index 752650450..4d43fee43 100644 --- a/client/src/store/resources/resources.reducer.js +++ b/client/src/store/resources/resources.reducer.js @@ -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) : []; -} \ No newline at end of file + 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]; +}; \ No newline at end of file diff --git a/client/src/style/components/data-table.scss b/client/src/style/components/data-table.scss index 372a17252..4d9310a4f 100644 --- a/client/src/style/components/data-table.scss +++ b/client/src/style/components/data-table.scss @@ -134,6 +134,14 @@ background-color: #CFDCEE; } } + .tr.no-results{ + .td{ + flex-direction: column; + padding: 20px; + color: #666; + align-items: center; + } + } } .tr .th.expander, diff --git a/client/src/style/objects/form.scss b/client/src/style/objects/form.scss index 5efc41023..11928ac19 100644 --- a/client/src/style/objects/form.scss +++ b/client/src/style/objects/form.scss @@ -80,12 +80,12 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,