mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: add filter field to suggest select field.
This commit is contained in:
@@ -17,11 +17,11 @@ export default function ContactsMultiSelect({
|
|||||||
contactsSelected = [],
|
contactsSelected = [],
|
||||||
...multiSelectProps
|
...multiSelectProps
|
||||||
}) {
|
}) {
|
||||||
const [localSelected, setLocalSelected] = useState([ ...contactsSelected]);
|
const [localSelected, setLocalSelected] = useState([...contactsSelected]);
|
||||||
|
|
||||||
// Detarmines the given id is selected.
|
// Detarmines the given id is selected.
|
||||||
const isItemSelected = useCallback(
|
const isItemSelected = useCallback(
|
||||||
(id) => localSelected.some(s => s === id),
|
(id) => localSelected.some((s) => s === id),
|
||||||
[localSelected],
|
[localSelected],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -45,15 +45,30 @@ export default function ContactsMultiSelect({
|
|||||||
const handleItemSelect = useCallback(
|
const handleItemSelect = useCallback(
|
||||||
({ id }) => {
|
({ id }) => {
|
||||||
const selected = isItemSelected(id)
|
const selected = isItemSelected(id)
|
||||||
? localSelected.filter(s => s !== id)
|
? localSelected.filter((s) => s !== id)
|
||||||
: [...localSelected, id];
|
: [...localSelected, id];
|
||||||
|
|
||||||
setLocalSelected([ ...selected ]);
|
setLocalSelected([...selected]);
|
||||||
safeInvoke(onContactSelect, selected);
|
safeInvoke(onContactSelect, selected);
|
||||||
},
|
},
|
||||||
[setLocalSelected, localSelected, isItemSelected, onContactSelect],
|
[setLocalSelected, localSelected, isItemSelected, onContactSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filters accounts items.
|
||||||
|
const filterContactsPredicater = useCallback(
|
||||||
|
(query, contact, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = contact.display_name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return normalizedTitle.indexOf(normalizedQuery) >= 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
items={contacts}
|
items={contacts}
|
||||||
@@ -62,6 +77,7 @@ export default function ContactsMultiSelect({
|
|||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
filterable={true}
|
filterable={true}
|
||||||
onItemSelect={handleItemSelect}
|
onItemSelect={handleItemSelect}
|
||||||
|
itemPredicate={filterContactsPredicater}
|
||||||
{...multiSelectProps}
|
{...multiSelectProps}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function ItemsMultiSelect({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
icon={isItemSelected(item.id) ? 'tick' : 'blank'}
|
icon={isItemSelected(item.id) ? 'tick' : 'blank'}
|
||||||
text={item.name}
|
text={item.name}
|
||||||
|
label={item.code}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
/>
|
/>
|
||||||
@@ -54,6 +55,21 @@ export function ItemsMultiSelect({
|
|||||||
[setLocalSelected, localSelected, isItemSelected, onItemSelect],
|
[setLocalSelected, localSelected, isItemSelected, onItemSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filters accounts items.
|
||||||
|
const filterItemsPredicater = useCallback(
|
||||||
|
(query, item, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = item.name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
items={items}
|
items={items}
|
||||||
@@ -62,6 +78,7 @@ export function ItemsMultiSelect({
|
|||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
filterable={true}
|
filterable={true}
|
||||||
onItemSelect={handleItemSelect}
|
onItemSelect={handleItemSelect}
|
||||||
|
itemPredicate={filterItemsPredicater}
|
||||||
{...multiSelectProps}
|
{...multiSelectProps}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function ItemsSuggestField({
|
|||||||
if (exactMatch) {
|
if (exactMatch) {
|
||||||
return normalizedTitle === normalizedQuery;
|
return normalizedTitle === normalizedQuery;
|
||||||
} else {
|
} else {
|
||||||
return normalizedTitle.indexOf(normalizedQuery) >= 0;
|
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Position,
|
Position,
|
||||||
Utils,
|
Utils,
|
||||||
|
InputGroup,
|
||||||
|
Button,
|
||||||
|
refHandler,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { QueryList } from '@blueprintjs/select';
|
import { QueryList } from '@blueprintjs/select';
|
||||||
|
|
||||||
@@ -61,6 +64,13 @@ import { QueryList } from '@blueprintjs/select';
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
export default class MultiSelect extends React.Component {
|
export default class MultiSelect extends React.Component {
|
||||||
|
inputElement = null;
|
||||||
|
handleInputRef = refHandler(
|
||||||
|
this,
|
||||||
|
'inputElement',
|
||||||
|
this.props.inputProps?.inputRef,
|
||||||
|
);
|
||||||
|
|
||||||
static get displayName() {
|
static get displayName() {
|
||||||
return `${DISPLAYNAME_PREFIX}.MultiSelect`;
|
return `${DISPLAYNAME_PREFIX}.MultiSelect`;
|
||||||
}
|
}
|
||||||
@@ -92,8 +102,7 @@ export default class MultiSelect extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
// omit props specific to this component, spread the rest.
|
// omit props specific to this component, spread the rest.
|
||||||
const { openOnKeyDown, popoverProps, tagInputProps, ...restProps } =
|
const { openOnKeyDown, popoverProps, ...restProps } = this.props;
|
||||||
this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryList
|
<QueryList
|
||||||
@@ -106,20 +115,36 @@ export default class MultiSelect extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maybeRenderClearButton(query) {
|
||||||
|
return query.length > 0 ? (
|
||||||
|
<Button icon="cross" minimal={true} onClick={this.resetQuery} />
|
||||||
|
) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
renderQueryList(listProps) {
|
renderQueryList(listProps) {
|
||||||
const { fill, tagInputProps = {}, popoverProps = {} } = this.props;
|
const { filterable, fill, inputProps = {}, popoverProps = {} } = this.props;
|
||||||
const { handleKeyDown, handleKeyUp } = listProps;
|
const { handleKeyDown, handleKeyUp } = listProps;
|
||||||
|
|
||||||
if (fill) {
|
if (fill) {
|
||||||
popoverProps.fill = true;
|
popoverProps.fill = true;
|
||||||
tagInputProps.fill = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add our own inputProps.className so that we can reference it in event handlers
|
// const handleInputRef = refHandler(
|
||||||
const { inputProps = {} } = tagInputProps;
|
// this,
|
||||||
inputProps.className = classNames(
|
// 'inputElement',
|
||||||
inputProps.className,
|
// this.props.inputProps?.inputRef,
|
||||||
Classes.MULTISELECT_TAG_INPUT_INPUT,
|
// );
|
||||||
|
|
||||||
|
const input = (
|
||||||
|
<InputGroup
|
||||||
|
leftIcon="search"
|
||||||
|
placeholder="Filter..."
|
||||||
|
rightElement={this.maybeRenderClearButton(listProps.query)}
|
||||||
|
{...inputProps}
|
||||||
|
inputRef={this.handleInputRef}
|
||||||
|
onChange={listProps.handleQueryChange}
|
||||||
|
value={listProps.query}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -147,6 +172,7 @@ export default class MultiSelect extends React.Component {
|
|||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
<div onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} ref={this.listRef}>
|
<div onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} ref={this.listRef}>
|
||||||
|
{filterable ? input : undefined}
|
||||||
{listProps.itemList}
|
{listProps.itemList}
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -179,6 +205,15 @@ export default class MultiSelect extends React.Component {
|
|||||||
// scroll active item into view after popover transition completes and all dimensions are stable.
|
// scroll active item into view after popover transition completes and all dimensions are stable.
|
||||||
this.queryList.scrollActiveItemIntoView();
|
this.queryList.scrollActiveItemIntoView();
|
||||||
}
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const { inputProps = {} } = this.props;
|
||||||
|
// autofocus is enabled by default
|
||||||
|
if (inputProps.autoFocus !== false) {
|
||||||
|
this.inputElement.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
Utils.safeInvokeMember(this.props.popoverProps, 'onOpened', node);
|
Utils.safeInvokeMember(this.props.popoverProps, 'onOpened', node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetQuery = () => this.queryList && this.queryList.setQuery("", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ function InventoryItemDetailsProvider({ filter, ...props }) {
|
|||||||
data: { items },
|
data: { items },
|
||||||
isLoading: isItemsLoading,
|
isLoading: isItemsLoading,
|
||||||
isFetching: isItemsFetching,
|
isFetching: isItemsFetching,
|
||||||
} = useItems({ page_size: 10000 });
|
} = useItems({
|
||||||
|
stringified_filter_roles: JSON.stringify([
|
||||||
|
{ fieldKey: 'type', comparator: 'is', value: 'inventory', index: 1 },
|
||||||
|
]),
|
||||||
|
page_size: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
inventoryItemDetails,
|
inventoryItemDetails,
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ function InventoryValuationProvider({ query, ...props }) {
|
|||||||
data: { items },
|
data: { items },
|
||||||
isLoading: isItemsLoading,
|
isLoading: isItemsLoading,
|
||||||
isFetching: isItemsFetching,
|
isFetching: isItemsFetching,
|
||||||
} = useItems({ page_size: 10000 });
|
} = useItems({
|
||||||
|
stringified_filter_roles: JSON.stringify([
|
||||||
|
{ fieldKey: 'type', comparator: 'is', value: 'inventory', index: 1 },
|
||||||
|
]),
|
||||||
|
page_size: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
// Provider data.
|
// Provider data.
|
||||||
const provider = {
|
const provider = {
|
||||||
@@ -36,7 +41,7 @@ function InventoryValuationProvider({ query, ...props }) {
|
|||||||
|
|
||||||
items,
|
items,
|
||||||
isItemsFetching,
|
isItemsFetching,
|
||||||
isItemsLoading
|
isItemsLoading,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import FinancialReportPage from '../FinancialReportPage';
|
|||||||
import { usePurchasesByItems, useItems } from 'hooks/query';
|
import { usePurchasesByItems, useItems } from 'hooks/query';
|
||||||
import { transformFilterFormToQuery } from '../common';
|
import { transformFilterFormToQuery } from '../common';
|
||||||
|
|
||||||
|
|
||||||
const PurchasesByItemsContext = createContext();
|
const PurchasesByItemsContext = createContext();
|
||||||
|
|
||||||
function PurchasesByItemsProvider({ query, ...props }) {
|
function PurchasesByItemsProvider({ query, ...props }) {
|
||||||
@@ -27,7 +26,12 @@ function PurchasesByItemsProvider({ query, ...props }) {
|
|||||||
data: { items },
|
data: { items },
|
||||||
isLoading: isItemsLoading,
|
isLoading: isItemsLoading,
|
||||||
isFetching: isItemsFetching,
|
isFetching: isItemsFetching,
|
||||||
} = useItems({ page_size: 10000 });
|
} = useItems({
|
||||||
|
page_size: 10000,
|
||||||
|
stringified_filter_roles: JSON.stringify([
|
||||||
|
{ fieldKey: 'type', comparator: 'is', value: 'inventory', index: 1 },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
purchaseByItems,
|
purchaseByItems,
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ function SalesByItemProvider({ query, ...props }) {
|
|||||||
data: { items },
|
data: { items },
|
||||||
isLoading: isItemsLoading,
|
isLoading: isItemsLoading,
|
||||||
isFetching: isItemsFetching,
|
isFetching: isItemsFetching,
|
||||||
} = useItems({ page_size: 10000 });
|
} = useItems({
|
||||||
|
page_size: 10000,
|
||||||
|
stringified_filter_roles: JSON.stringify([
|
||||||
|
{ fieldKey: 'type', comparator: 'is', value: 'inventory', index: 1 },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
salesByItems,
|
salesByItems,
|
||||||
@@ -35,7 +40,7 @@ function SalesByItemProvider({ query, ...props }) {
|
|||||||
items,
|
items,
|
||||||
isItemsLoading,
|
isItemsLoading,
|
||||||
isItemsFetching,
|
isItemsFetching,
|
||||||
|
|
||||||
refetchSheet: refetch,
|
refetchSheet: refetch,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { FormattedMessage as T } from 'components';
|
import { FormattedMessage as T } from 'components';
|
||||||
import { Icon, If, Choose, Money } from 'components';
|
import { Icon, If, Choose, Money } from 'components';
|
||||||
import { safeCallback, isBlank, calculateStatus } from 'utils';
|
import { formattedAmount, safeCallback, isBlank, calculateStatus } from 'utils';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,12 +115,7 @@ export function StatusAccessor(bill) {
|
|||||||
<If condition={bill.is_partially_paid}>
|
<If condition={bill.is_partially_paid}>
|
||||||
<span className="partial-paid">
|
<span className="partial-paid">
|
||||||
{intl.get('day_partially_paid', {
|
{intl.get('day_partially_paid', {
|
||||||
due: (
|
due: formattedAmount(bill.due_amount, bill.currency_code),
|
||||||
<Money
|
|
||||||
amount={bill.due_amount}
|
|
||||||
currency={bill.currency_code}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { FormattedMessage as T } from 'components';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Choose, If, Icon } from 'components';
|
import { Choose, If, Icon } from 'components';
|
||||||
import { Money, AppToaster } from 'components';
|
import { Money, AppToaster } from 'components';
|
||||||
import { safeCallback, calculateStatus } from 'utils';
|
import { formattedAmount, safeCallback, calculateStatus } from 'utils';
|
||||||
|
|
||||||
export const statusAccessor = (row) => {
|
export const statusAccessor = (row) => {
|
||||||
return (
|
return (
|
||||||
@@ -48,9 +48,7 @@ export const statusAccessor = (row) => {
|
|||||||
<If condition={row.is_partially_paid}>
|
<If condition={row.is_partially_paid}>
|
||||||
<span class="partial-paid">
|
<span class="partial-paid">
|
||||||
{intl.get('day_partially_paid', {
|
{intl.get('day_partially_paid', {
|
||||||
due: (
|
due: formattedAmount(row.due_amount, row.currency_code),
|
||||||
<Money amount={row.due_amount} currency={row.currency_code} />
|
|
||||||
),
|
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -12,9 +12,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.status.td {
|
.status.td {
|
||||||
.status-accessor {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
.overdue-status,
|
.overdue-status,
|
||||||
.due-status {
|
.due-status {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
.dashboard__insider--sales-invoices-list{
|
.dashboard__insider--sales-invoices-list{
|
||||||
|
|
||||||
|
|
||||||
.bigcapital-datatable{
|
.bigcapital-datatable{
|
||||||
|
|
||||||
|
|
||||||
.tbody{
|
.tbody{
|
||||||
.tr{
|
.tr{
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
@@ -20,9 +17,6 @@
|
|||||||
}
|
}
|
||||||
.status.td{
|
.status.td{
|
||||||
|
|
||||||
.status-accessor{
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
.overdue-status,
|
.overdue-status,
|
||||||
.due-status{
|
.due-status{
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
Reference in New Issue
Block a user