From 9eebaf5a6ee1e7d4f2072b0f9179aa96862ff99e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 21 Nov 2020 19:58:02 +0200 Subject: [PATCH] feat: style vendor form and list. feat: items state selectors. --- client/src/common/classes.js | 1 + .../CustomerFormAfterPrimarySection.js | 1 + client/src/containers/Items/ItemForm.js | 11 +- .../src/containers/Items/ItemForm.schema.js | 9 + client/src/containers/Items/ItemsDataTable.js | 37 ++-- client/src/containers/Items/ItemsList.js | 5 +- client/src/containers/Items/withItems.js | 25 +-- client/src/containers/Vendors/Vendor.js | 13 +- client/src/containers/Vendors/VendorForm.js | 15 +- .../Vendors/VendorFormAfterPrimarySection.js | 4 +- .../Vendors/VendorFormPrimarySection.js | 185 +++++++++--------- .../containers/Vendors/VendorsEmptyStatus.js | 37 ++++ client/src/containers/Vendors/VendorsTable.js | 65 +++--- client/src/store/items/items.reducer.js | 26 +-- client/src/store/items/items.selectors.js | 70 ++++++- client/src/store/vendors/vendors.selectors.js | 23 +-- client/src/style/pages/dashboard.scss | 2 +- client/src/style/pages/items.scss | 2 +- .../20190822214303_create_accounts_table.js | 3 +- 19 files changed, 332 insertions(+), 202 deletions(-) create mode 100644 client/src/containers/Vendors/VendorsEmptyStatus.js diff --git a/client/src/common/classes.js b/client/src/common/classes.js index 6b8f3f395..ba9837ce7 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -27,6 +27,7 @@ const CLASSES = { PAGE_FORM_PAYMENT_MADE: 'page-form--payment-made', PAGE_FORM_PAYMENT_RECEIVE: 'page-form--payment-receive', PAGE_FORM_CUSTOMER: 'page-form--customer', + PAGE_FORM_VENDOR: 'page-form--customer', PAGE_FORM_ITEM: 'page-form--item', FORM_GROUP_LIST_SELECT: 'form-group--select-list', diff --git a/client/src/containers/Customers/CustomerFormAfterPrimarySection.js b/client/src/containers/Customers/CustomerFormAfterPrimarySection.js index 87ca54a4d..b79012ac7 100644 --- a/client/src/containers/Customers/CustomerFormAfterPrimarySection.js +++ b/client/src/containers/Customers/CustomerFormAfterPrimarySection.js @@ -38,6 +38,7 @@ export default function CustomerFormAfterPrimarySection({}) { /> )} + {({ field, meta: { error, touched } }) => ( { + return { + ...item, + sellable: item?.sellable ? Boolean(item.sellable) : defaultValue.sellable, + purchasable: item?.purchasable ? Boolean(item.purchasable) : defaultValue.purchasable, + }; +} + export const CreateItemFormSchema = Schema; export const EditItemFormSchema = Schema; \ No newline at end of file diff --git a/client/src/containers/Items/ItemsDataTable.js b/client/src/containers/Items/ItemsDataTable.js index 738e3e401..03eba0f8d 100644 --- a/client/src/containers/Items/ItemsDataTable.js +++ b/client/src/containers/Items/ItemsDataTable.js @@ -27,6 +27,8 @@ function ItemsDataTable({ itemsTableLoading, itemsCurrentPage, itemsTableQuery, + itemsCurrentViewId, + itemsPagination, // #withItemsActions addItemsTableQueries, @@ -120,9 +122,7 @@ function ItemsDataTable({ {formatMessage({ id: row.type })} - ) : ( - '' - ), + ) : (''), className: 'item_type', width: 120, }, @@ -184,11 +184,16 @@ function ItemsDataTable({ [onSelectedRowsChange], ); + const showEmptyStatus = [ + itemsCurrentPage.length === 0, + itemsCurrentViewId === -1, + ].every((condition) => condition === true); + return (
- + @@ -198,14 +203,14 @@ function ItemsDataTable({ data={itemsCurrentPage} onFetchData={handleFetchData} noInitialFetch={true} - expandable={true} selectionColumn={true} spinnerProps={{ size: 30 }} onSelectedRowsChange={handleSelectedRowsChange} rowContextMenu={handleRowContextMenu} + expandable={false} sticky={true} pagination={true} - pagesCount={2} + pagesCount={itemsPagination.pagesCount} autoResetSortBy={false} autoResetPage={false} initialPageSize={itemsTableQuery.page_size} @@ -219,10 +224,20 @@ function ItemsDataTable({ } export default compose( - withItems(({ itemsCurrentPage, itemsTableLoading, itemsTableQuery }) => ({ - itemsCurrentPage, - itemsTableLoading, - itemsTableQuery, - })), + withItems( + ({ + itemsCurrentPage, + itemsTableLoading, + itemsTableQuery, + itemsCurrentViewId, + itemsPagination + }) => ({ + itemsCurrentPage, + itemsTableLoading, + itemsTableQuery, + itemsCurrentViewId, + itemsPagination + }), + ), withItemsActions, )(ItemsDataTable); diff --git a/client/src/containers/Items/ItemsList.js b/client/src/containers/Items/ItemsList.js index 4c7d89f9a..606501507 100644 --- a/client/src/containers/Items/ItemsList.js +++ b/client/src/containers/Items/ItemsList.js @@ -65,8 +65,9 @@ function ItemsList({ ); // Handle fetching the items table based on the given query. - const fetchItems = useQuery(['items-table', itemsTableQuery], () => - requestFetchItems({}), + const fetchItems = useQuery( + ['items-table', itemsTableQuery], + (key, _query) => requestFetchItems({ ..._query }), ); // Handle click delete item. diff --git a/client/src/containers/Items/withItems.js b/client/src/containers/Items/withItems.js index ead8096ba..297696917 100644 --- a/client/src/containers/Items/withItems.js +++ b/client/src/containers/Items/withItems.js @@ -1,26 +1,29 @@ import {connect} from 'react-redux'; import { getResourceViews, - getViewPages, } from 'store/customViews/customViews.selectors' import { - getCurrentPageResults -} from 'store/selectors'; + getItemsCurrentPageFactory, + getItemsPaginationMetaFactory, + getItemsTableQueryFactory, + getItemsCurrentViewIdFactory +} from 'store/items/items.selectors'; export default (mapState) => { - const mapStateToProps = (state, props) => { - const viewPages = getViewPages(state.items.views, state.items.currentViewId); + const getItemsCurrentPage = getItemsCurrentPageFactory(); + const getItemsPaginationMeta = getItemsPaginationMetaFactory(); + const getItemsTableQuery = getItemsTableQueryFactory(); + const getItemsCurrentViewId = getItemsCurrentViewIdFactory(); + const mapStateToProps = (state, props) => { const mapped = { itemsViews: getResourceViews(state, props, 'items'), - itemsCurrentPage: getCurrentPageResults( - state.items.items, - viewPages, - state.items.currentPage, - ), + itemsCurrentPage: getItemsCurrentPage(state, props), itemsBulkSelected: state.items.bulkActions, itemsTableLoading: state.items.loading, - itemsTableQuery: state.items.tableQuery, + itemsTableQuery: getItemsTableQuery(state, props), + itemsPagination: getItemsPaginationMeta(state, props), + itemsCurrentViewId: getItemsCurrentViewId(state, props), }; return mapState ? mapState(mapped, state, props) : mapped; }; diff --git a/client/src/containers/Vendors/Vendor.js b/client/src/containers/Vendors/Vendor.js index e7e4ed825..032da663e 100644 --- a/client/src/containers/Vendors/Vendor.js +++ b/client/src/containers/Vendors/Vendor.js @@ -2,6 +2,7 @@ import React, { useCallback } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useQuery } from 'react-query'; +import { DashboardCard } from 'components'; import VendorFrom from './VendorForm'; import DashboardInsider from 'components/Dashboard/DashboardInsider'; @@ -53,11 +54,13 @@ function Vendor({ } name={'vendor-form'} > - + + + ); } diff --git a/client/src/containers/Vendors/VendorForm.js b/client/src/containers/Vendors/VendorForm.js index 3a529d4fd..923c28a89 100644 --- a/client/src/containers/Vendors/VendorForm.js +++ b/client/src/containers/Vendors/VendorForm.js @@ -153,9 +153,18 @@ function VendorForm({ > {({ isSubmitting }) => (
- - - +
+ +
+ +
+ +
+ +
+ +
+ +
{/*------------ Vendor email -----------*/} {({ field, meta: { error, touched } }) => ( @@ -27,6 +27,7 @@ function VendorFormAfterPrimarySection() { )} + {/*------------ Phone number -----------*/} + {/*------------ Vendor website -----------*/} {({ field, meta: { error, touched } }) => ( diff --git a/client/src/containers/Vendors/VendorFormPrimarySection.js b/client/src/containers/Vendors/VendorFormPrimarySection.js index f9621d861..e68cd3e34 100644 --- a/client/src/containers/Vendors/VendorFormPrimarySection.js +++ b/client/src/containers/Vendors/VendorFormPrimarySection.js @@ -18,105 +18,100 @@ import { inputIntent } from 'utils'; */ function VendorFormPrimarySection() { return ( -
-
- {/**----------- Vendor name -----------*/} - } - inline={true} - > - - - {({ form, field: { value }, meta: { error, touched } }) => ( - { - form.setFieldValue('salutation', salutation.label); - }} - selectedItem={value} - popoverProps={{ minimal: true }} - className={classNames( - CLASSES.FORM_GROUP_LIST_SELECT, - CLASSES.FILL, - 'input-group--salutation-list', - 'select-list--fill-button', - )} - /> - )} - - - - {({ field, meta: { error, touched } }) => ( - - )} - - - - {({ field, meta: { error, touched } }) => ( - - )} - - - - - {/*----------- Company Name -----------*/} - - {({ field, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - helperText={} - inline={true} - > - - - )} - - {/*----------- Display Name -----------*/} - - {({ form, field: { value }, meta: { error, touched } }) => ( - } - intent={inputIntent({ error, touched })} - label={ - <> - - - - - } - className={classNames( - CLASSES.FORM_GROUP_LIST_SELECT, - CLASSES.FILL, - )} - inline={true} - > - { - form.setFieldValue('display_name', displayName.label); +
+ {/**----------- Vendor name -----------*/} + } + inline={true} + > + + + {({ form, field: { value }, meta: { error, touched } }) => ( + { + form.setFieldValue('salutation', salutation.label); }} selectedItem={value} popoverProps={{ minimal: true }} + className={classNames( + CLASSES.FORM_GROUP_LIST_SELECT, + CLASSES.FILL, + 'input-group--salutation-list', + 'select-list--fill-button', + )} /> - - )} - -
+ )} + + + + {({ field, meta: { error, touched } }) => ( + + )} + + + + {({ field, meta: { error, touched } }) => ( + + )} + + +
+ + {/*----------- Company Name -----------*/} + + {({ field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + inline={true} + > + + + )} + + {/*----------- Display Name -----------*/} + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + label={ + <> + + + + + } + className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, CLASSES.FILL)} + inline={true} + > + { + form.setFieldValue('display_name', displayName.label); + }} + selectedItem={value} + popoverProps={{ minimal: true }} + /> + + )} +
); } diff --git a/client/src/containers/Vendors/VendorsEmptyStatus.js b/client/src/containers/Vendors/VendorsEmptyStatus.js new file mode 100644 index 000000000..ff2d11247 --- /dev/null +++ b/client/src/containers/Vendors/VendorsEmptyStatus.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Button, Intent } from '@blueprintjs/core'; +import { useHistory } from 'react-router-dom'; +import { EmptyStatus } from 'components'; + +export default function VendorsEmptyStatus() { + const history = useHistory(); + + return ( + + Here a list of your organization products and services, to be used + when you create invoices or bills to your customers or vendors. +

+ } + action={ + <> + + + + + } + /> + ); +} diff --git a/client/src/containers/Vendors/VendorsTable.js b/client/src/containers/Vendors/VendorsTable.js index ac85d9213..8f9936df1 100644 --- a/client/src/containers/Vendors/VendorsTable.js +++ b/client/src/containers/Vendors/VendorsTable.js @@ -9,10 +9,12 @@ import { Intent, } from '@blueprintjs/core'; import { FormattedMessage as T, useIntl } from 'react-intl'; +import classNames from 'classnames'; import { useIsValuePassed } from 'hooks'; -import LoadingIndicator from 'components/LoadingIndicator'; -import { DataTable, Icon, Money } from 'components'; +import VendorsEmptyStatus from './VendorsEmptyStatus'; +import { DataTable, LoadingIndicator, Icon, Money, Choose } from 'components'; +import { CLASSES } from 'common/classes'; import withVendors from './withVendors'; import withVendorsActions from './withVendorActions'; @@ -34,8 +36,7 @@ function VendorsTable({ // #withVendorsActions addVendorsTableQueries, - // #OwnProps - loading, + // #ownProps onEditVendor, onDeleteVendor, onSelectedRowsChange, @@ -182,29 +183,41 @@ function VendorsTable({ onDeleteVendor, }); - console.log(vendorsCurrentPage, 'vendorsCurrentPage'); return ( - - - +
+ + + + + + + + + + + +
); } diff --git a/client/src/store/items/items.reducer.js b/client/src/store/items/items.reducer.js index bc44fc477..51fb2579c 100644 --- a/client/src/store/items/items.reducer.js +++ b/client/src/store/items/items.reducer.js @@ -43,35 +43,13 @@ export default createReducer(initialState, { const viewId = customViewId || -1; const view = state.views[viewId] || {}; - const viewPages = getItemsViewPages(state.views, viewId); - - items.forEach((item) => { - const stateItem = state.items[item.id]; - const itemRelation = state.itemsRelation[stateItem.id]; - - if (typeof itemRelation === 'undefined') { - state.itemsRelation[item.id] = []; - } - const filteredRelation = state.itemsRelation[item.id].filter( - (relation) => - relation.viewId === viewId && - relation.pageNumber === paginationMeta.page, - ); - - filteredRelation.push({ - viewId, - pageNumber: paginationMeta.page, - }); - state.itemsRelation[item.id] = filteredRelation; - }); state.views[viewId] = { ...view, pages: { - ...viewPages, + ...(state.views?.[viewId]?.pages || {}), [paginationMeta.page]: { - ids: items.map((i) => i.id), - meta: paginationMeta, + ids: items.map((item) => item.id), }, }, }; diff --git a/client/src/store/items/items.selectors.js b/client/src/store/items/items.selectors.js index 607f95bd0..fbfde0364 100644 --- a/client/src/store/items/items.selectors.js +++ b/client/src/store/items/items.selectors.js @@ -1,7 +1,69 @@ +import { paginationLocationQuery } from "store/selectors"; +import { createSelector } from 'reselect'; +import { + pickItemsFromIds, + defaultPaginationMeta, +} from 'store/selectors'; +const itemsTableQuerySelector = (state) => state.items.tableQuery; +const itemsCurrentPageSelector = (state, props) => { + const currentViewId = state.items.currentViewId; + const currentView = state.items.views?.[currentViewId]; + const currentPageId = currentView?.paginationMeta?.page; -export const getItemsViewPages = (itemsViews, viewId) => { - return itemsViews[viewId] ? - itemsViews[viewId].pages : {}; -}; \ No newline at end of file + return currentView?.pages?.[currentPageId]; +}; +const itemsDataSelector = (state) => state.items.items; + +const itemsPaginationSelector = (state, props) => { + const viewId = state.items.currentViewId; + return state.items.views?.[viewId]?.paginationMeta; +}; +const customersCurrentViewIdSelector = (state) => state.customers.currentViewId; + +// Get items table query marged with location query. +export const getItemsTableQueryFactory = () => + createSelector( + paginationLocationQuery, + itemsTableQuerySelector, + (locationQuery, tableQuery) => { + return { + ...locationQuery, + ...tableQuery, + } + }, + ); + +// Retrieve items current page and view. +export const getItemsCurrentPageFactory = () => + createSelector( + itemsDataSelector, + itemsCurrentPageSelector, + (items, itemsIdsCurrentPage) => { + return typeof itemsIdsCurrentPage === 'object' + ? pickItemsFromIds(items, itemsIdsCurrentPage.ids) || [] + : []; + }, + ); + +// Retrieve items pagination meta. +export const getItemsPaginationMetaFactory = () => + createSelector( + itemsPaginationSelector, + (itemsPagination) => { + return { + ...defaultPaginationMeta(), + ...itemsPagination, + }; + } + ); + +// Retrieve items current view id. +export const getItemsCurrentViewIdFactory = () => + createSelector( + customersCurrentViewIdSelector, + (currentViewId) => { + return currentViewId; + } + ); \ No newline at end of file diff --git a/client/src/store/vendors/vendors.selectors.js b/client/src/store/vendors/vendors.selectors.js index 202dc3c36..0bc45539b 100644 --- a/client/src/store/vendors/vendors.selectors.js +++ b/client/src/store/vendors/vendors.selectors.js @@ -5,12 +5,14 @@ import { defaultPaginationMeta, } from 'store/selectors'; -const vendorsTableQuery = (state) => { - return state.vendors.tableQuery; -}; +const vendorsTableQuery = (state) => state.vendors.tableQuery; +const vendorByIdSelector = (state, props) => + state.vendors.items[props.vendorId]; +const vendorsItemsSelector = (state) => state.vendors.items; -const vendorByIdSelector = (state, props) => { - return state.vendors.items[props.vendorId]; +const vendorsPaginationSelector = (state, props) => { + const viewId = state.vendors.currentViewId; + return state.vendors.views?.[viewId]; }; export const getVendorsTableQuery = createSelector( @@ -28,11 +30,9 @@ const vendorsPageSelector = (state, props, query) => { const viewId = state.vendors.currentViewId; const currentView = state.vendors.views?.[viewId]; const currentPageId = currentView?.pages; - return currentView?.pages?.[currentPageId]; - // return state.vendors.views?.[viewId]?.pages?.[query.page]; -}; -const vendorsItemsSelector = (state) => state.vendors.items; + return currentView?.pages?.[currentPageId]; +}; export const getVendorCurrentPageFactory = () => createSelector( @@ -45,11 +45,6 @@ export const getVendorCurrentPageFactory = () => }, ); -const vendorsPaginationSelector = (state, props) => { - const viewId = state.vendors.currentViewId; - return state.vendors.views?.[viewId]; -}; - export const getVendorsPaginationMetaFactory = () => createSelector(vendorsPaginationSelector, (vendorPage) => { return { diff --git a/client/src/style/pages/dashboard.scss b/client/src/style/pages/dashboard.scss index ced3a42fd..c7be8bf47 100644 --- a/client/src/style/pages/dashboard.scss +++ b/client/src/style/pages/dashboard.scss @@ -341,7 +341,7 @@ .datatable-empty-status{ margin-top: auto; margin-bottom: auto; - padding-bottom: 40px; + padding-bottom: 20px; } } diff --git a/client/src/style/pages/items.scss b/client/src/style/pages/items.scss index ebd72f422..1a5480885 100644 --- a/client/src/style/pages/items.scss +++ b/client/src/style/pages/items.scss @@ -8,7 +8,7 @@ } #{$self}__primary-section{ overflow: hidden; - padding-top: 10px; + padding-top: 5px; margin-bottom: 20px; border-bottom: 1px solid #eaeaea; padding-bottom: 5px; diff --git a/server/src/database/migrations/20190822214303_create_accounts_table.js b/server/src/database/migrations/20190822214303_create_accounts_table.js index d2dd03580..3116f8f1b 100644 --- a/server/src/database/migrations/20190822214303_create_accounts_table.js +++ b/server/src/database/migrations/20190822214303_create_accounts_table.js @@ -1,7 +1,6 @@ - exports.up = function (knex) { return knex.schema.createTable('accounts', (table) => { - table.increments('id').comment('Auto-generated id');; + table.increments('id').comment('Auto-generated id'); table.string('name').index(); table.string('slug'); table.integer('account_type_id').unsigned().references('id').inTable('account_types');