refactor(webapp): Accounts Select and MultiSelect components

This commit is contained in:
a.bouhuolia
2023-04-30 17:33:15 +02:00
parent 83510cfa70
commit 6f0f47f38a
13 changed files with 341 additions and 481 deletions

View File

@@ -1,74 +0,0 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { MenuItem } from '@blueprintjs/core';
import { FMultiSelect } from '../Forms';
import classNames from 'classnames';
import { Classes } from '@blueprintjs/popover2';
/**
*
* @param {*} query
* @param {*} account
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const accountItemPredicate = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} account
* @param {*} param1
* @returns
*/
const accountItemRenderer = (
account,
{ handleClick, modifiers, query },
{ isSelected },
) => {
return (
<MenuItem
icon={isSelected ? 'tick' : 'blank'}
text={account.name}
label={account.code}
key={account.id}
onClick={handleClick}
/>
);
};
const accountSelectProps = {
itemPredicate: accountItemPredicate,
itemRenderer: accountItemRenderer,
valueAccessor: (item) => item.id,
labelAccessor: (item) => item.code,
tagRenderer: (item) => item.name,
};
/**
* branches mulit select.
* @param {*} param0
* @returns {JSX.Element}
*/
export function AccountMultiSelect({ accounts, ...rest }) {
return (
<FMultiSelect
items={accounts}
popoverProps={{
minimal: true,
}}
{...accountSelectProps}
{...rest}
/>
);
}

View File

@@ -1,31 +1,79 @@
// @ts-nocheck
import React from 'react';
import React, { useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable';
import { FMultiSelect } from '../Forms';
import { accountPredicate } from './_components';
import { filterAccountsByQuery, nestedArrayToflatten } from '@/utils';
import { MenuItemNestedText } from '../Menu';
export function AccountsMultiSelect({ ...multiSelectProps }) {
/**
* Default account item renderer of the list.
* @returns {JSX.Element}
*/
const accountRenderer = (
item,
{ handleClick, modifiers, query },
{ isSelected },
) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MultiSelect
itemRenderer={(
item,
{ active, selected, handleClick, modifiers, query },
) => {
return (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}}
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={<MenuItemNestedText level={item.account_level} text={item.name} />}
key={item.id}
onClick={handleClick}
icon={isSelected ? 'tick' : 'blank'}
/>
);
};
/**
* Accounts multi-select field binded with Formik form.
* @param {*} param0
* @returns {JSX.Element}
*/
export function AccountsMultiSelect({
items,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
...rest
}) {
// Filters accounts based on the given filter props.
const filteredAccounts = useMemo(() => {
const flattenAccounts = nestedArrayToflatten(items);
return filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
}, [
items,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
return (
<FMultiSelect
items={filteredAccounts}
valueAccessor={'id'}
textAccessor={'name'}
labelAccessor={'code'}
tagAccessor={'name'}
popoverProps={{ minimal: true }}
fill={true}
tagRenderer={(item) => item.name}
resetOnSelect={true}
{...multiSelectProps}
itemPredicate={accountPredicate}
itemRenderer={accountRenderer}
{...rest}
/>
);
}

View File

@@ -0,0 +1,105 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import * as R from 'ramda';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import {
MenuItemNestedText,
FormattedMessage as T,
FSelect,
} from '@/components';
import { filterAccountsByQuery, nestedArrayToflatten } from '@/utils';
import { accountPredicate } from './_components';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
/**
* Default account item renderer.
* @returns {JSX.Element}
*/
const accountRenderer = (item, { handleClick, modifiers, query }) => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
label={item.code}
key={item.id}
text={<MenuItemNestedText level={item.account_level} text={item.name} />}
onClick={handleClick}
/>
);
};
/**
* Accounts select field binded with Formik form.
* @returns {JSX.Element}
*/
function AccountsSelectRoot({
// #withDialogActions
openDialog,
// #ownProps
items,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
...restProps
}) {
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
const flattenAccounts = nestedArrayToflatten(items);
const filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
return filteredAccounts;
}, [
items,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
return (
<FSelect
items={filteredAccounts}
textAccessor={'name'}
labelAccessor={'code'}
valueAccessor={'id'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
itemPredicate={accountPredicate}
itemRenderer={accountRenderer}
{...restProps}
/>
);
}
export const AccountsSelect = R.compose(withDialogActions)(AccountsSelectRoot);

View File

@@ -1,177 +0,0 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import * as R from 'ramda';
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
import { CLASSES } from '@/constants/classes';
import { DialogsName } from '@/constants/dialogs';
import withDialogActions from '@/containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
* Accounts select list.
*/
function AccountsSelectListRoot({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = 'Select account',
onAccountSelected,
disabled = false,
popoverFill = false,
filterByParentTypes,
filterByTypes,
filterByNormal,
filterByRootTypes,
allowCreate,
buttonProps = {},
}) {
const flattenAccounts = useMemo(
() => nestedArrayToflatten(accounts),
[accounts],
);
// Filters accounts based on filter props.
const filteredAccounts = useMemo(() => {
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
});
return filteredAccounts;
}, [
flattenAccounts,
filterByRootTypes,
filterByParentTypes,
filterByTypes,
filterByNormal,
]);
// Find initial account object to set it as default account in initial render.
const initialAccount = useMemo(
() => filteredAccounts.find((a) => a.id === initialAccountId),
[initialAccountId, filteredAccounts],
);
// Select account item.
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
useEffect(() => {
if (typeof selectedAccountId !== 'undefined') {
const account = selectedAccountId
? filteredAccounts.find((a) => a.id === selectedAccountId)
: null;
setSelectedAccount(account);
}
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={<MenuItemNestedText level={item.level} text={item.name} />}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
}, []);
// Handle the account item select.
const handleAccountSelect = useCallback(
(account) => {
if (account.id) {
setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog(DialogsName.AccountForm);
}
},
[setSelectedAccount, onAccountSelected, openDialog],
);
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
filterable={true}
onItemSelect={handleAccountSelect}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
>
<Button
disabled={disabled}
text={selectedAccount ? selectedAccount.name : defaultSelectText}
{...buttonProps}
/>
</Select>
);
}
export const AccountsSelectList = R.compose(withDialogActions)(
AccountsSelectListRoot,
);

View File

@@ -1,49 +1,15 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { ListSelect } from '@/components';
import { CLASSES } from '@/constants/classes';
export function AccountsTypesSelect({
accountsTypes,
selectedTypeId,
defaultSelectText = 'Select account type',
onTypeSelected,
disabled = false,
popoverFill = false,
...restProps
}) {
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.label.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle item selected.
const handleItemSelected = (accountType) => {
onTypeSelected && onTypeSelected(accountType);
};
import React from 'react';
import { FSelect } from '@/components/Forms';
export function AccountsTypesSelect({ ...props }) {
return (
<ListSelect
items={accountsTypes}
selectedItemProp={'key'}
selectedItem={selectedTypeId}
textProp={'label'}
defaultText={defaultSelectText}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
disabled={disabled}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...restProps}
<FSelect
valueAccessor={'key'}
labelAccessor={'label'}
textAccessor={'label'}
placeholder={'Select an account...'}
{...props}
/>
);
}

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MenuItemNestedText } from '../Menu';
// Filters accounts items.
export const accountPredicate = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};

View File

@@ -1,5 +1,4 @@
export * from './AccountMultiSelect';
export * from './AccountsSelect';
export * from './AccountsMultiSelect';
export * from './AccountsSelectList';
export * from './AccountsSuggestField';
export * from './AccountsTypesSelect';

View File

@@ -45,13 +45,6 @@ const currencyItemRenderer = (currency, { handleClick, modifiers, query }) => {
);
};
const currencySelectProps = {
itemPredicate: currencyItemPredicate,
itemRenderer: currencyItemRenderer,
valueAccessor: 'currency_code',
labelAccessor: 'currency_code',
};
/**
*
* @param {*} currencies
@@ -60,7 +53,10 @@ const currencySelectProps = {
export function CurrencySelect({ currencies, ...rest }) {
return (
<FSelect
{...currencySelectProps}
itemPredicate={currencyItemPredicate}
itemRenderer={currencyItemRenderer}
valueAccessor={'currency_code'}
labelAccessor={'currency_code'}
{...rest}
items={currencies}
input={CurrnecySelectButton}

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import React from 'react';
import {
FormGroup,
InputGroup,
@@ -9,9 +10,23 @@ import {
EditableText,
TextArea,
} from '@blueprintjs-formik/core';
import { Button } from '@blueprintjs/core';
import { Select, MultiSelect } from '@blueprintjs-formik/select';
import { DateInput } from '@blueprintjs-formik/datetime';
function FSelect({ ...props }) {
const input = ({ activeItem, text, label, value }) => {
return (
<Button
text={text || props.placeholder || 'Select an item ...'}
rightIcon="double-caret-vertical"
{...props.buttonProps}
/>
);
};
return <Select input={input} {...props} />;
}
export {
FormGroup as FFormGroup,
InputGroup as FInputGroup,
@@ -19,7 +34,7 @@ export {
Checkbox as FCheckbox,
RadioGroup as FRadioGroup,
Switch as FSwitch,
Select as FSelect,
FSelect,
MultiSelect as FMultiSelect,
EditableText as FEditableText,
TextArea as FTextArea,

View File

@@ -1,28 +1,23 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { Form, FastField, Field, ErrorMessage, useFormikContext } from 'formik';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
Checkbox,
} from '@blueprintjs/core';
import { Form, ErrorMessage, useFormikContext } from 'formik';
import { Button, Classes, FormGroup, Intent } from '@blueprintjs/core';
import {
If,
FieldRequiredHint,
Hint,
AccountsSelectList,
AccountsSelect,
AccountsTypesSelect,
CurrencySelect,
FormattedMessage as T,
FFormGroup,
FInputGroup,
FCheckbox,
FTextArea,
} from '@/components';
import withAccounts from '@/containers/Accounts/withAccounts';
import { inputIntent, compose } from '@/utils';
import { compose } from '@/utils';
import { useAutofocus } from '@/hooks';
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
import { useAccountDialogContext } from './AccountDialogProvider';
@@ -36,7 +31,7 @@ function AccountFormDialogFields({
onClose,
action,
}) {
const { values, isSubmitting } = useFormikContext();
const { values, isSubmitting, setFieldValue } = useFormikContext();
const accountNameFieldRef = useAutofocus();
// Account form context.
@@ -46,146 +41,117 @@ function AccountFormDialogFields({
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<Field name={'account_type'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--account-type', Classes.FILL)}
inline={true}
helperText={<ErrorMessage name="account_type" />}
intent={inputIntent({ error, touched })}
>
<AccountsTypesSelect
accountsTypes={accountsTypes}
selectedTypeId={value}
defaultSelectText={<T id={'select_account_type'} />}
onTypeSelected={(accountType) => {
form.setFieldValue('account_type', accountType.key);
form.setFieldValue('currency_code', '');
}}
disabled={fieldsDisabled.accountType}
popoverProps={{ minimal: true }}
popoverFill={true}
/>
</FormGroup>
)}
</Field>
<FastField name={'name'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />}
inline={true}
>
<InputGroup
medium={true}
inputRef={(ref) => (accountNameFieldRef.current = ref)}
{...field}
/>
</FormGroup>
)}
</FastField>
<FastField name={'code'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'account_code'} />}
className={'form-group--account-code'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="code" />}
inline={true}
labelInfo={<Hint content={<T id="account_code_hint" />} />}
>
<InputGroup medium={true} {...field} />
</FormGroup>
)}
</FastField>
<Field name={'subaccount'} type={'checkbox'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={' '}
className={classNames('form-group--subaccount')}
intent={inputIntent({ error, touched })}
inline={true}
>
<Checkbox
inline={true}
label={<T id={'sub_account'} />}
name={'subaccount'}
{...field}
/>
</FormGroup>
)}
</Field>
<FastField
name={'parent_account_id'}
shouldUpdate={parentAccountShouldUpdate}
<FFormGroup
inline={true}
label={<T id={'account_type'} />}
labelInfo={<FieldRequiredHint />}
name={'account_type'}
fastField={true}
>
{({
form: { values, setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={<T id={'parent_account'} />}
className={classNames('form-group--parent-account', Classes.FILL)}
inline={true}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="parent_account_id" />}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
setFieldValue('parent_account_id', account.id);
}}
defaultSelectText={<T id={'select_parent_account'} />}
selectedAccountId={value}
popoverFill={true}
filterByTypes={values.account_type}
disabled={!values.subaccount}
/>
</FormGroup>
)}
</FastField>
<AccountsTypesSelect
name={'account_type'}
items={accountsTypes}
onItemSelect={(accountType) => {
setFieldValue('account_type', accountType.key);
setFieldValue('currency_code', '');
}}
disabled={fieldsDisabled.accountType}
popoverProps={{ minimal: true }}
fastField={true}
/>
</FFormGroup>
<FFormGroup
name={'name'}
label={<T id={'account_name'} />}
labelInfo={<FieldRequiredHint />}
helperText={<ErrorMessage name="name" />}
inline={true}
fastField={true}
>
<FInputGroup
medium={true}
inputRef={(ref) => (accountNameFieldRef.current = ref)}
name={'name'}
fastField={true}
/>
</FFormGroup>
<FFormGroup
label={<T id={'account_code'} />}
name={'code'}
helperText={<ErrorMessage name="code" />}
labelInfo={<Hint content={<T id="account_code_hint" />} />}
inline={true}
fastField={true}
>
<FInputGroup medium={true} name={'code'} fastField={true} />
</FFormGroup>
<FFormGroup
label={' '}
name={'subaccount'}
inline={true}
fastField={true}
>
<FCheckbox
inline={true}
label={<T id={'sub_account'} />}
name={'subaccount'}
fastField={true}
/>
</FFormGroup>
{values.subaccount && (
<FFormGroup
name={'parent_account_id'}
shouldUpdate={parentAccountShouldUpdate}
label={<T id={'parent_account'} />}
inline={true}
fastField={true}
>
<AccountsSelect
name={'parent_account_id'}
items={accounts}
shouldUpdate={parentAccountShouldUpdate}
placeholder={<T id={'select_parent_account'} />}
filterByTypes={values.account_type}
buttonProps={{ disabled: !values.subaccount }}
fastField={true}
/>
</FFormGroup>
)}
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
{/*------------ Currency -----------*/}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency'} />}
className={classNames('form-group--select-list', Classes.FILL)}
inline={true}
>
<CurrencySelect
name={'currency_code'}
currencies={currencies}
popoverProps={{ minimal: true }}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
label={<T id={'currency'} />}
name={'currency_code'}
inline={true}
fastField={true}
>
<CurrencySelect
name={'currency_code'}
currencies={currencies}
popoverProps={{ minimal: true }}
fastField={true}
/>
</FFormGroup>
</If>
<FastField name={'description'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'description'} />}
className={'form-group--description'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'description'} />}
inline={true}
>
<TextArea growVertically={true} height={280} {...field} />
</FormGroup>
)}
</FastField>
<FFormGroup
label={<T id={'description'} />}
name={'description'}
inline={true}
fastField={true}
>
<FTextArea
name={'description'}
growVertically={true}
height={280}
fastField={true}
/>
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { Classes } from '@blueprintjs/core';
import {
AccountMultiSelect,
AccountsMultiSelect,
Row,
Col,
FormattedMessage as T,
@@ -54,7 +54,7 @@ function GLHeaderGeneralPaneContent() {
name={'accountsIds'}
className={Classes.FILL}
>
<AccountMultiSelect name="accountsIds" accounts={accounts} />
<AccountsMultiSelect name="accountsIds" items={accounts} />
</FFormGroup>
</Col>
</Row>

View File

@@ -242,7 +242,7 @@
"organization": "Organization.",
"check_your_email_for_a_link_to_reset": "Check your email for a link to reset your password.If it doesnt appear within a few minutes, check your spam folder.",
"we_couldn_t_find_your_account_with_that_email": "We couldn't find your account with that email.",
"select_parent_account": "Select Parent Account",
"select_parent_account": "Select Parent Account...",
"the_exchange_rate_has_been_edited_successfully": "The exchange rate has been edited successfully",
"the_exchange_rate_has_been_created_successfully": "The exchange rate has been created successfully",
"the_user_details_has_been_updated": "The user details has been updated",