Merge branch 'feature/landed-cost'

This commit is contained in:
a.bouhuolia
2021-07-27 05:49:20 +02:00
237 changed files with 5482 additions and 926 deletions

View File

@@ -4,6 +4,7 @@ import { setLocale } from 'yup';
import intl from 'react-intl-universal';
import { find } from 'lodash';
import rtlDetect from 'rtl-detect';
import { AppIntlProvider } from './AppIntlProvider';
import DashboardLoadingIndicator from 'components/Dashboard/DashboardLoadingIndicator';
const SUPPORTED_LOCALES = [
@@ -40,16 +41,14 @@ function loadYupLocales(currentLocale) {
/**
* Modifies the html document direction to RTl if it was rtl-language.
*/
function useDocumentDirectionModifier(locale) {
function useDocumentDirectionModifier(locale, isRTL) {
React.useEffect(() => {
const isRTL = rtlDetect.isRtlLang(locale);
if (isRTL) {
const htmlDocument = document.querySelector('html');
htmlDocument.setAttribute('dir', 'rtl');
htmlDocument.setAttribute('lang', locale);
}
}, []);
}, [isRTL, locale]);
}
/**
@@ -59,8 +58,10 @@ export default function AppIntlLoader({ children }) {
const [isLoading, setIsLoading] = React.useState(true);
const currentLocale = getCurrentLocal();
const isRTL = rtlDetect.isRtlLang(currentLocale);
// Modifies the html document direction
useDocumentDirectionModifier(currentLocale);
useDocumentDirectionModifier(currentLocale, isRTL);
React.useEffect(() => {
// Lodas the locales data file.
@@ -86,10 +87,12 @@ export default function AppIntlLoader({ children }) {
})
.then(() => {});
}, [currentLocale]);
return (
<DashboardLoadingIndicator isLoading={isLoading}>
{children}
</DashboardLoadingIndicator>
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
<DashboardLoadingIndicator isLoading={isLoading}>
{children}
</DashboardLoadingIndicator>
</AppIntlProvider>
);
}

View File

@@ -0,0 +1,24 @@
import React, { createContext } from 'react';
const AppIntlContext = createContext();
/**
* Application intl provider.
*/
function AppIntlProvider({ currentLocale, isRTL, children }) {
const provider = {
currentLocale,
isRTL,
isLTR: !isRTL,
};
return (
<AppIntlContext.Provider value={provider}>
{children}
</AppIntlContext.Provider>
);
}
const useAppIntlContext = () => React.useContext(AppIntlContext);
export { AppIntlProvider, useAppIntlContext };

View File

@@ -0,0 +1,5 @@
import React from 'react';
export default function Card({ children }) {
return <div class="card">{children}</div>;
}

View File

@@ -1,58 +1,57 @@
import React, { useMemo, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { omit } from 'lodash';
import intl from 'react-intl-universal';
import MultiSelect from 'components/MultiSelect';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { safeInvoke } from 'utils';
/**
* Contacts multi-select component.
*/
export default function ContactsMultiSelect({
contacts,
defaultText = <T id={'all_customers'} />,
buttonProps,
onCustomerSelected: onContactSelected,
...selectProps
onContactSelect,
contactsSelected = [],
...multiSelectProps
}) {
const [selectedContacts, setSelectedContacts] = useState({});
const [localSelected, setLocalSelected] = useState([ ...contactsSelected]);
const isContactSelect = useCallback(
(id) => typeof selectedContacts[id] !== 'undefined',
[selectedContacts],
// Detarmines the given id is selected.
const isItemSelected = useCallback(
(id) => localSelected.some(s => s === id),
[localSelected],
);
// Contact item renderer.
const contactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
icon={isContactSelect(contact.id) ? 'tick' : 'blank'}
icon={isItemSelected(contact.id) ? 'tick' : 'blank'}
text={contact.display_name}
key={contact.id}
onClick={handleClick}
/>
),
[isContactSelect],
[isItemSelected],
);
const countSelected = useMemo(
() => Object.values(selectedContacts).length,
[selectedContacts],
);
// Count selected items.
const countSelected = localSelected.length;
const onContactSelect = useCallback(
// Handle item selected.
const handleItemSelect = useCallback(
({ id }) => {
const selected = {
...(isContactSelect(id)
? {
...omit(selectedContacts, [id]),
}
: {
...selectedContacts,
[id]: true,
}),
};
setSelectedContacts({ ...selected });
onContactSelected && onContactSelected(selected);
const selected = isItemSelected(id)
? localSelected.filter(s => s !== id)
: [...localSelected, id];
setLocalSelected([ ...selected ]);
safeInvoke(onContactSelect, selected);
},
[setSelectedContacts, selectedContacts, isContactSelect, onContactSelected],
[setLocalSelected, localSelected, isItemSelected, onContactSelect],
);
return (
@@ -62,7 +61,8 @@ export default function ContactsMultiSelect({
itemRenderer={contactRenderer}
popoverProps={{ minimal: true }}
filterable={true}
onItemSelect={onContactSelect}
onItemSelect={handleItemSelect}
{...multiSelectProps}
>
<Button
text={

View File

@@ -0,0 +1,41 @@
import React from 'react';
import classNames from 'classnames';
import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core';
const CheckboxEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
// intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Checkbox
value={value}
onChange={onChange}
onBlur={onBlur}
minimal={true}
className="ml2"
/>
</FormGroup>
);
};
export default CheckboxEditableCell;

View File

@@ -6,6 +6,7 @@ import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
import NumericInputCell from './NumericInputCell';
import CheckBoxFieldCell from './CheckBoxFieldCell'
export {
AccountsListFieldCell,
@@ -16,5 +17,6 @@ export {
PercentFieldCell,
DivFieldCell,
EmptyDiv,
NumericInputCell
NumericInputCell,
CheckBoxFieldCell
};

View File

@@ -4,14 +4,26 @@ import { CLASSES } from 'common/classes';
import { DataTable, If } from 'components';
import 'style/components/DataTable/DataTableEditable.scss';
/**
* Editable datatable.
*/
export default function DatatableEditable({
totalRow = false,
actions,
name,
className,
...tableProps
}) {
return (
<div className={classNames(CLASSES.DATATABLE_EDITOR, className)}>
<div
className={classNames(
CLASSES.DATATABLE_EDITOR,
{
[`${CLASSES.DATATABLE_EDITOR}--${name}`]: name,
},
className,
)}
>
<DataTable {...tableProps} />
<If condition={actions}>

View File

@@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import classNames from 'classnames';
import { If } from 'components';
import { Skeleton } from 'components';
import { useAppIntlContext } from 'components/AppIntlProvider';
import TableContext from './TableContext';
import { isCellLoading } from './utils';
@@ -26,6 +27,9 @@ export default function TableCell({
const isExpandColumn = expandToggleColumn === index;
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = {};
// Application intl context.
const { isRTL } = useAppIntlContext();
// Detarmines whether the current cell is loading.
const cellLoading = isCellLoading(
cellsLoading,
@@ -46,8 +50,6 @@ export default function TableCell({
);
}
const isRTL = true;
return (
<div
{...cell.getCellProps({

View File

@@ -0,0 +1,29 @@
import React from 'react';
import className from 'classname';
/**
* Details menu.
*/
export function DetailsMenu({ children, vertical = false }) {
return (
<div
className={className('details-menu', {
'is-vertical': vertical,
})}
>
{children}
</div>
);
}
/**
* Detail item.
*/
export function DetailItem({ label, children }) {
return (
<div class="detail-item">
<div class="detail-item__label">{label}</div>
<div class="detail-item__content">{children}</div>
</div>
);
}

View File

@@ -13,6 +13,7 @@ import KeyboardShortcutsDialog from 'containers/Dialogs/keyboardShortcutsDialog'
import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog';
import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog';
import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog';
import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog';
/**
* Dialogs container.
@@ -32,6 +33,7 @@ export default function DialogsContainer() {
<ContactDuplicateDialog dialogName={'contact-duplicate'} />
<QuickPaymentReceiveFormDialog dialogName={'quick-payment-receive'} />
<QuickPaymentMadeFormDialog dialogName={'quick-payment-made'} />
<AllocateLandedCostDialog dialogName={'allocate-landed-cost'} />
</div>
);
}

View File

@@ -1,9 +1,14 @@
import React from 'react';
import { Position, Drawer } from '@blueprintjs/core';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import 'style/components/Drawer.scss';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { compose } from 'utils';
/**
* Drawer component.
*/
function DrawerComponent(props) {
const { name, children, onClose, closeDrawer } = props;

View File

@@ -6,6 +6,7 @@ import PaymentReceiveDrawer from 'containers/Sales/PaymentReceives/PaymentDetail
import AccountDrawer from 'containers/Drawers/AccountDrawer';
import ManualJournalDrawer from 'containers/Drawers/ManualJournalDrawer';
import ExpenseDrawer from 'containers/Drawers/ExpenseDrawer';
import BillDrawer from 'containers/Drawers/BillDrawer';
export default function DrawersContainer() {
return (
@@ -17,6 +18,7 @@ export default function DrawersContainer() {
<AccountDrawer name={'account-drawer'} />
<ManualJournalDrawer name={'journal-drawer'} />
<ExpenseDrawer name={'expense-drawer'} />
<BillDrawer name={'bill-drawer'} />
</div>
);
}

View File

@@ -1,6 +1,15 @@
import React from 'react';
import className from 'classnames';
import 'style/containers/FinancialStatements/FinancialSheet.scss';
export default function FinancialStatements({ children }) {
return <div class="financial-statement">{children}</div>;
export default function FinancialStatements({ name, children }) {
return (
<div
className={className('financial-statement', {
[`financial-statement--${name}`]: name,
})}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,77 @@
import React, { useCallback, useState } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import MultiSelect from 'components/MultiSelect';
import { FormattedMessage as T } from 'components';
import { safeInvoke } from 'utils';
/**
* Items multi-select.
*/
export function ItemsMultiSelect({
items,
defaultText = <T id={'All items'} />,
buttonProps,
selectedItems = [],
onItemSelect,
...multiSelectProps
}) {
const [localSelected, setLocalSelected] = useState([...selectedItems]);
// Detarmines the given id is selected.
const isItemSelected = useCallback(
(id) => localSelected.some((s) => s === id),
[localSelected],
);
// Contact item renderer.
const itemRenderer = useCallback(
(item, { handleClick }) => (
<MenuItem
icon={isItemSelected(item.id) ? 'tick' : 'blank'}
text={item.name}
key={item.id}
onClick={handleClick}
/>
),
[isItemSelected],
);
// Count selected items.
const countSelected = localSelected.length;
// Handle item selected.
const handleItemSelect = useCallback(
({ id }) => {
const selected = isItemSelected(id)
? localSelected.filter((s) => s !== id)
: [...localSelected, id];
setLocalSelected([...selected]);
safeInvoke(onItemSelect, selected);
},
[setLocalSelected, localSelected, isItemSelected, onItemSelect],
);
return (
<MultiSelect
items={items}
noResults={<MenuItem disabled={true} text={<T id={'No items'} />} />}
itemRenderer={itemRenderer}
popoverProps={{ minimal: true }}
filterable={true}
onItemSelect={handleItemSelect}
{...multiSelectProps}
>
<Button
text={
countSelected === 0
? defaultText
: intl.get('Selected items ({count})', { count: countSelected })
}
{...buttonProps}
/>
</MultiSelect>
);
}

View File

@@ -0,0 +1 @@
export * from './ItemsMultiSelect';

View File

@@ -13,18 +13,16 @@
* limitations under the License.
*/
import classNames from "classnames";
import * as React from "react";
import classNames from 'classnames';
import * as React from 'react';
import {
Classes,
DISPLAYNAME_PREFIX,
Popover,
Position,
Utils,
} from "@blueprintjs/core";
import {
QueryList,
} from '@blueprintjs/select';
} from '@blueprintjs/core';
import { QueryList } from '@blueprintjs/select';
// export interface IMultiSelectProps<T> extends IListItemsProps<T> {
// /**
@@ -63,114 +61,124 @@ import {
// }
export default class MultiSelect extends React.Component {
static get displayName() {
return `${DISPLAYNAME_PREFIX}.MultiSelect`;
}
static get displayName() {
return `${DISPLAYNAME_PREFIX}.MultiSelect`;
}
static get defaultProps() {
return {
fill: false,
placeholder: "Search...",
};
static get defaultProps() {
return {
fill: false,
placeholder: 'Search...',
};
}
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = { isOpen: (this.props.popoverProps && this.props.popoverProps.isOpen) || false };
this.input = null;
this.queryList = null;
this.listRef = React.createRef();
this.state = {
isOpen:
(this.props.popoverProps && this.props.popoverProps.isOpen) || false,
};
this.input = null;
this.queryList = null;
this.listRef = React.createRef();
this.refHandlers = {
queryList: (ref) => {
this.queryList = ref;
},
};
this.refHandlers = {
queryList: (ref) => {
this.queryList = ref;
},
};
}
render() {
// omit props specific to this component, spread the rest.
const { openOnKeyDown, popoverProps, tagInputProps, ...restProps } =
this.props;
return (
<QueryList
{...restProps}
onItemSelect={this.handleItemSelect.bind(this)}
onQueryChange={this.handleQueryChange.bind(this)}
ref={this.refHandlers.queryList}
renderer={this.renderQueryList.bind(this)}
/>
);
}
renderQueryList(listProps) {
const { fill, tagInputProps = {}, popoverProps = {} } = this.props;
const { handleKeyDown, handleKeyUp } = listProps;
if (fill) {
popoverProps.fill = true;
tagInputProps.fill = true;
}
render() {
// omit props specific to this component, spread the rest.
const { openOnKeyDown, popoverProps, tagInputProps, ...restProps } = this.props;
return (
<QueryList
{...restProps}
onItemSelect={this.handleItemSelect.bind(this)}
onQueryChange={this.handleQueryChange.bind(this)}
ref={this.refHandlers.queryList}
renderer={this.renderQueryList.bind(this)}
/>
);
}
// add our own inputProps.className so that we can reference it in event handlers
const { inputProps = {} } = tagInputProps;
inputProps.className = classNames(
inputProps.className,
Classes.MULTISELECT_TAG_INPUT_INPUT,
);
renderQueryList(listProps) {
const { fill, tagInputProps = {}, popoverProps = {} } = this.props;
const { handleKeyDown, handleKeyUp } = listProps;
if (fill) {
popoverProps.fill = true;
tagInputProps.fill = true;
}
// add our own inputProps.className so that we can reference it in event handlers
const { inputProps = {} } = tagInputProps;
inputProps.className = classNames(inputProps.className, Classes.MULTISELECT_TAG_INPUT_INPUT);
return (
<Popover
autoFocus={false}
canEscapeKeyClose={true}
enforceFocus={false}
isOpen={this.state.isOpen}
position={Position.BOTTOM_LEFT}
{...popoverProps}
className={classNames(listProps.className, popoverProps.className)}
onInteraction={this.handlePopoverInteraction.bind(this)}
popoverClassName={classNames(Classes.MULTISELECT_POPOVER, popoverProps.popoverClassName)}
onOpened={this.handlePopoverOpened.bind(this)}
return (
<Popover
autoFocus={false}
canEscapeKeyClose={true}
enforceFocus={false}
isOpen={this.state.isOpen}
position={Position.BOTTOM_LEFT}
{...popoverProps}
className={classNames(listProps.className, popoverProps.className)}
onInteraction={this.handlePopoverInteraction.bind(this)}
popoverClassName={classNames(
Classes.MULTISELECT_POPOVER,
popoverProps.popoverClassName,
)}
onOpened={this.handlePopoverOpened.bind(this)}
>
<div
onKeyDown={
this.state.isOpen ? handleKeyDown : this.handleTargetKeyDown
}
onKeyUp={this.state.isOpen ? handleKeyUp : undefined}
>
<div
onKeyDown={this.state.isOpen ? handleKeyDown : this.handleTargetKeyDown}
onKeyUp={this.state.isOpen ? handleKeyUp : undefined}
>
{this.props.children}
</div>
<div onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} ref={this.listRef}>
{listProps.itemList}
</div>
</Popover>
);
};
{this.props.children}
</div>
<div onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} ref={this.listRef}>
{listProps.itemList}
</div>
</Popover>
);
}
handleItemSelect(item, evt) {
if (this.input != null) {
this.input.focus();
}
Utils.safeInvoke(this.props.onItemSelect, item, evt);
};
handleQueryChange(query, evt) {
this.setState({ isOpen: query.length > 0 || !this.props.openOnKeyDown });
Utils.safeInvoke(this.props.onQueryChange, query, evt);
};
handlePopoverInteraction = (isOpen, e) => {
if (e && this.listRef.current && this.listRef.current.contains(e.target)) {
this.setState({ isOpen: true })
} else {
this.setState({ isOpen });
}
Utils.safeInvokeMember(this.props.popoverProps, "onInteraction", isOpen);
handleItemSelect(item, evt) {
if (this.input != null) {
this.input.focus();
}
Utils.safeInvoke(this.props.onItemSelect, item, evt);
}
handlePopoverOpened(node) {
if (this.queryList != null) {
// scroll active item into view after popover transition completes and all dimensions are stable.
this.queryList.scrollActiveItemIntoView();
}
Utils.safeInvokeMember(this.props.popoverProps, "onOpened", node);
};
handleQueryChange(query, evt) {
this.setState({ isOpen: query.length > 0 || !this.props.openOnKeyDown });
Utils.safeInvoke(this.props.onQueryChange, query, evt);
}
handlePopoverInteraction = (isOpen, e) => {
if (e && this.listRef.current && this.listRef.current.contains(e.target)) {
this.setState({ isOpen: true });
} else {
this.setState({ isOpen });
}
Utils.safeInvokeMember(this.props.popoverProps, 'onInteraction', isOpen);
};
handlePopoverOpened(node) {
if (this.queryList != null) {
// scroll active item into view after popover transition completes and all dimensions are stable.
this.queryList.scrollActiveItemIntoView();
}
Utils.safeInvokeMember(this.props.popoverProps, 'onOpened', node);
}
}

View File

@@ -56,6 +56,10 @@ import DrawerHeaderContent from './Drawer/DrawerHeaderContent';
import Postbox from './Postbox';
import AccountsSuggestField from './AccountsSuggestField';
import MaterialProgressBar from './MaterialProgressBar';
import { MoneyFieldCell } from './DataTableCells';
import Card from './Card';
import { ItemsMultiSelect } from './Items';
const Hint = FieldHint;
@@ -123,4 +127,7 @@ export {
Postbox,
AccountsSuggestField,
MaterialProgressBar,
MoneyFieldCell,
ItemsMultiSelect,
Card
};