mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
fix: Filter financial reports by items, customers or vendors.
This commit is contained in:
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
77
client/src/components/Items/ItemsMultiSelect.js
Normal file
77
client/src/components/Items/ItemsMultiSelect.js
Normal 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>
|
||||
);
|
||||
}
|
||||
1
client/src/components/Items/index.js
Normal file
1
client/src/components/Items/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ItemsMultiSelect';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ import AccountsSuggestField from './AccountsSuggestField';
|
||||
import MaterialProgressBar from './MaterialProgressBar';
|
||||
import { MoneyFieldCell } from './DataTableCells';
|
||||
|
||||
import { ItemsMultiSelect } from './Items';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
const T = FormattedMessage;
|
||||
@@ -125,4 +127,5 @@ export {
|
||||
AccountsSuggestField,
|
||||
MaterialProgressBar,
|
||||
MoneyFieldCell,
|
||||
ItemsMultiSelect
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user