feat: style vendor form and list.

feat: items state selectors.
This commit is contained in:
Ahmed Bouhuolia
2020-11-21 19:58:02 +02:00
parent b9e61461ae
commit 9eebaf5a6e
19 changed files with 332 additions and 202 deletions

View File

@@ -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',

View File

@@ -38,6 +38,7 @@ export default function CustomerFormAfterPrimarySection({}) {
/>
)}
</FastField>
<FastField name={'personal_phone'}>
{({ field, meta: { error, touched } }) => (
<InputGroup

View File

@@ -23,7 +23,11 @@ import withSettings from 'containers/Settings/withSettings';
import { compose, transformToForm } from 'utils';
import { transitionItemTypeKeyToLabel } from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import {
EditItemFormSchema,
CreateItemFormSchema,
transformItemFormData,
} from './ItemForm.schema';
const defaultInitialValues = {
active: true,
@@ -99,7 +103,10 @@ function ItemForm({
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(itemDetail, defaultInitialValues),
...transformToForm(
transformItemFormData(itemDetail, defaultInitialValues),
defaultInitialValues,
),
}),
[
itemDetail,

View File

@@ -52,5 +52,14 @@ const Schema = Yup.object().shape({
purchasable: Yup.boolean().required(),
});
export const transformItemFormData = (item, defaultValue) => {
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;

View File

@@ -27,6 +27,8 @@ function ItemsDataTable({
itemsTableLoading,
itemsCurrentPage,
itemsTableQuery,
itemsCurrentViewId,
itemsPagination,
// #withItemsActions
addItemsTableQueries,
@@ -120,9 +122,7 @@ function ItemsDataTable({
<Tag minimal={true} round={true} intent={Intent.NONE}>
{formatMessage({ id: row.type })}
</Tag>
) : (
''
),
) : (''),
className: 'item_type',
width: 120,
},
@@ -184,11 +184,16 @@ function ItemsDataTable({
[onSelectedRowsChange],
);
const showEmptyStatus = [
itemsCurrentPage.length === 0,
itemsCurrentViewId === -1,
].every((condition) => condition === true);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={itemsTableLoading && !isLoadedBefore}>
<Choose>
<Choose.When condition={true}>
<Choose.When condition={showEmptyStatus}>
<ItemsEmptyStatus />
</Choose.When>
@@ -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);

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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'}
>
<VendorFrom
onFormSubmit={handleFormSubmit}
vendorId={id}
onCancelForm={handleCancel}
/>
<DashboardCard page>
<VendorFrom
onFormSubmit={handleFormSubmit}
vendorId={id}
onCancelForm={handleCancel}
/>
</DashboardCard>
</DashboardInsider>
);
}

View File

@@ -153,9 +153,18 @@ function VendorForm({
>
{({ isSubmitting }) => (
<Form>
<VendorFormPrimarySection />
<VendorFormAfterPrimarySection />
<VendorTabs vendor={vendorId} />
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<VendorFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<VendorFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<VendorTabs vendor={vendorId} />
</div>
<VendorFloatingActions
isSubmitting={isSubmitting}
vendor={vendorId}

View File

@@ -12,7 +12,7 @@ import { inputIntent } from 'utils';
*/
function VendorFormAfterPrimarySection() {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div class="customer-form__after-primary-section-content">
{/*------------ Vendor email -----------*/}
<FastField name={'email'}>
{({ field, meta: { error, touched } }) => (
@@ -27,6 +27,7 @@ function VendorFormAfterPrimarySection() {
</FormGroup>
)}
</FastField>
{/*------------ Phone number -----------*/}
<FormGroup
className={'form-group--phone-number'}
@@ -54,6 +55,7 @@ function VendorFormAfterPrimarySection() {
</FastField>
</ControlGroup>
</FormGroup>
{/*------------ Vendor website -----------*/}
<FastField name={'website'}>
{({ field, meta: { error, touched } }) => (

View File

@@ -18,105 +18,100 @@ import { inputIntent } from 'utils';
*/
function VendorFormPrimarySection() {
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
{/**----------- Vendor name -----------*/}
<FormGroup
className={classNames('form-group--contact_name')}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<FastField name={'salutation'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<SalutationList
onItemSelect={(salutation) => {
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',
)}
/>
)}
</FastField>
<FastField name={'first_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'First Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--first-name')}
{...field}
/>
)}
</FastField>
<FastField name={'last_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'Last Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--last-name')}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*----------- Company Name -----------*/}
<FastField name={'company_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
className={classNames('form-group--company_name')}
label={<T id={'company_name'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'company_name'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*----------- Display Name -----------*/}
<Field name={'display_name'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
helperText={<ErrorMessage name={'display_name'} />}
intent={inputIntent({ error, touched })}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
className={classNames(
CLASSES.FORM_GROUP_LIST_SELECT,
CLASSES.FILL,
)}
inline={true}
>
<DisplayNameList
firstName={form.values.first_name}
lastName={form.values.last_name}
company={form.values.company_name}
salutation={form.values.salutation}
onItemSelect={(displayName) => {
form.setFieldValue('display_name', displayName.label);
<div className={'customer-form__primary-section-content'}>
{/**----------- Vendor name -----------*/}
<FormGroup
className={classNames('form-group--contact_name')}
label={<T id={'contact_name'} />}
inline={true}
>
<ControlGroup>
<FastField name={'salutation'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<SalutationList
onItemSelect={(salutation) => {
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',
)}
/>
</FormGroup>
)}
</Field>
</div>
)}
</FastField>
<FastField name={'first_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'First Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--first-name')}
{...field}
/>
)}
</FastField>
<FastField name={'last_name'}>
{({ field, meta: { error, touched } }) => (
<InputGroup
placeholder={'Last Name'}
intent={inputIntent({ error, touched })}
className={classNames('input-group--last-name')}
{...field}
/>
)}
</FastField>
</ControlGroup>
</FormGroup>
{/*----------- Company Name -----------*/}
<FastField name={'company_name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
className={classNames('form-group--company_name')}
label={<T id={'company_name'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'company_name'} />}
inline={true}
>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/*----------- Display Name -----------*/}
<Field name={'display_name'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
helperText={<ErrorMessage name={'display_name'} />}
intent={inputIntent({ error, touched })}
label={
<>
<T id={'display_name'} />
<FieldRequiredHint />
<Hint />
</>
}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, CLASSES.FILL)}
inline={true}
>
<DisplayNameList
firstName={form.values.first_name}
lastName={form.values.last_name}
company={form.values.company_name}
salutation={form.values.salutation}
onItemSelect={(displayName) => {
form.setFieldValue('display_name', displayName.label);
}}
selectedItem={value}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</Field>
</div>
);
}

View File

@@ -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 (
<EmptyStatus
title={"Create and manage your organization's vendors."}
description={
<p>
Here a list of your organization products and services, to be used
when you create invoices or bills to your customers or vendors.
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/vendors/new');
}}
>
New vendor
</Button>
<Button intent={Intent.NONE} large={true}>
Learn more
</Button>
</>
}
/>
);
}

View File

@@ -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 (
<LoadingIndicator loading={vendorsLoading && !isLoadedBefore} mount={false}>
<DataTable
noInitialFetch={true}
columns={columns}
data={vendorItems}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
pagination={true}
manualSortBy={true}
pagesCount={vendorsPageination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={vendorTableQuery.page_size}
initialPageIndex={vendorTableQuery.page - 1}
/>
</LoadingIndicator>
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator
loading={vendorsLoading && !isLoadedBefore}
mount={false}
>
<Choose>
<Choose.When condition={true}>
<VendorsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={vendorItems}
onFetchData={handleFetchData}
selectionColumn={true}
expandable={false}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
pagination={true}
manualSortBy={true}
pagesCount={vendorsPageination.pagesCount}
autoResetSortBy={false}
autoResetPage={false}
initialPageSize={vendorTableQuery.page_size}
initialPageIndex={vendorTableQuery.page - 1}
/>
</Choose.Otherwise>
</Choose>
</LoadingIndicator>
</div>
);
}

View File

@@ -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),
},
},
};

View File

@@ -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 : {};
};
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;
}
);

View File

@@ -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 {

View File

@@ -341,7 +341,7 @@
.datatable-empty-status{
margin-top: auto;
margin-bottom: auto;
padding-bottom: 40px;
padding-bottom: 20px;
}
}

View File

@@ -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;

View File

@@ -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');