feat: add filter field to suggest select field.

This commit is contained in:
a.bouhuolia
2021-08-03 08:26:13 +02:00
parent c09a99a773
commit 4a0bd842e9
12 changed files with 113 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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