re-structure to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 01:02:31 +02:00
parent 8242ec64ba
commit 7a0a13f9d5
10400 changed files with 46966 additions and 17223 deletions

View File

@@ -0,0 +1,74 @@
// @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

@@ -0,0 +1,31 @@
// @ts-nocheck
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable';
export function AccountsMultiSelect({ ...multiSelectProps }) {
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}
/>
);
}}
popoverProps={{ minimal: true }}
fill={true}
tagRenderer={(item) => item.name}
resetOnSelect={true}
{...multiSelectProps}
/>
);
}

View File

@@ -0,0 +1,177 @@
// @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

@@ -0,0 +1,176 @@
// @ts-nocheck
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import * as R from 'ramda';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes';
import { DialogsName } from '@/constants/dialogs';
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
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,
};
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
// 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 suggest field.
*/
function AccountsSuggestFieldRoot({
// #withDialogActions
openDialog,
// #ownProps
accounts,
initialAccountId,
selectedAccountId,
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
popoverFill = false,
onAccountSelected,
filterByParentTypes = [],
filterByTypes = [],
filterByNormal,
filterByRootTypes = [],
allowCreate,
...suggestProps
}) {
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],
);
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}
/>
);
}, []);
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 (
<Suggest
items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
onItemSelect={handleAccountSelect}
selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={handleInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
{...suggestProps}
/>
);
}
export const AccountsSuggestField = R.compose(withDialogActions)(
AccountsSuggestFieldRoot,
);

View File

@@ -0,0 +1,49 @@
// @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);
};
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}
/>
);
}

View File

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

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
import * as Yup from 'yup';
export const getFilterDropdownSchema = () =>
Yup.object().shape({
conditions: Yup.array().of(
Yup.object().shape({
fieldKey: Yup.string(),
value: Yup.string().nullable(),
condition: Yup.string().nullable(),
comparator: Yup.string().nullable(),
}),
),
});

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import React from 'react';
import { Classes } from '@blueprintjs/core';
import { ListSelect } from '../Select';
import { getConditionTypeCompatators } from './utils';
export default function DynamicFilterCompatatorField({
dataType,
...restProps
}) {
const options = getConditionTypeCompatators(dataType);
return (
<ListSelect
textProp={'label'}
selectedItemProp={'value'}
items={options}
className={Classes.FILL}
filterable={false}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
{...restProps}
/>
);
}

View File

@@ -0,0 +1,398 @@
// @ts-nocheck
import React from 'react';
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
import {
Button,
FormGroup,
Classes,
InputGroup,
MenuItem,
} from '@blueprintjs/core';
import { get, first, defaultTo, isEqual, isEmpty } from 'lodash';
import intl from 'react-intl-universal';
import { Choose, Icon, FormattedMessage as T, ListSelect } from '@/components';
import { useUpdateEffect } from '@/hooks';
import {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useFilterCondition,
useAdvancedFilterContext,
} from './AdvancedFilterDropdownContext';
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
import AdvancedFilterValueField from './AdvancedFilterValueField';
import {
filterConditionRoles,
getConditionalsOptions,
transformFieldsToOptions,
shouldFilterValueFieldUpdate,
getConditionTypeCompatators,
} from './utils';
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
import { useAdvancedFilterAutoSubmit } from './components';
/**
* Condition item list renderer.
*/
function ConditionItemRenderer(condition, { handleClick, modifiers, query }) {
return (
<MenuItem
text={
<>
<div>{condition.label}</div>
<span className="text-hint">{condition.text}</span>
</>
}
key={condition.value}
onClick={handleClick}
/>
);
}
/**
* Filter condition field.
*/
function FilterConditionField() {
const conditionalsOptions = getConditionalsOptions();
const { conditionIndex, getConditionFieldPath } = useFilterCondition();
const conditionFieldPath = getConditionFieldPath('condition');
return (
<FastField name={conditionFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--condition'}>
<Choose>
<Choose.When condition={conditionIndex === 0}>
<InputGroup disabled value={intl.get('filter.when')} />
</Choose.When>
<Choose.Otherwise>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
labelProp={'text'}
items={conditionalsOptions}
className={Classes.FILL}
filterable={false}
onItemSelect={(option) => {
form.setFieldValue(conditionFieldPath, option.value);
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
itemRenderer={ConditionItemRenderer}
/>
</Choose.Otherwise>
</Choose>
</FormGroup>
)}
</FastField>
);
}
/**
* Compatator field.
*/
function FilterCompatatorFilter() {
const { getConditionFieldPath, fieldMeta } = useFilterCondition();
const comparatorFieldPath = getConditionFieldPath('comparator');
const fieldType = get(fieldMeta, 'fieldType');
return (
<FastField name={comparatorFieldPath}>
{({ form, field }) => (
<FormGroup className={'form-group--comparator'}>
<AdvancedFilterCompatatorField
dataType={fieldType}
className={Classes.FILL}
selectedItem={field.value}
onItemSelect={(option) => {
form.setFieldValue(comparatorFieldPath, option.value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Changes default value of comparator field in the condition row once the
* field option changing.
*/
function useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
}) {
const fieldKeyValue = getConditionValue('fieldKey');
const comparatorsOptions = React.useMemo(
() => getConditionTypeCompatators(fieldMeta.fieldType),
[fieldMeta.fieldType],
);
useUpdateEffect(() => {
if (fieldKeyValue) {
const defaultValue = get(first(comparatorsOptions), 'value');
setConditionValue('comparator', defaultValue);
}
}, [fieldKeyValue, setConditionValue, comparatorsOptions]);
}
/**
* Resource fields field.
*/
function FilterFieldsField() {
const {
getConditionFieldPath,
getConditionValue,
setConditionValue,
fieldMeta,
} = useFilterCondition();
const { fields } = useAdvancedFilterContext();
const fieldPath = getConditionFieldPath('fieldKey');
const valueFieldPath = getConditionFieldPath('value');
useDefaultComparatorFieldValue({
getConditionValue,
setConditionValue,
fieldMeta,
});
return (
<FastField name={fieldPath}>
{({ field, form }) => (
<FormGroup className={'form-group--fieldKey'}>
<ListSelect
selectedItem={field.value}
textProp={'label'}
selectedItemProp={'value'}
items={transformFieldsToOptions(fields)}
className={Classes.FILL}
onItemSelect={(option) => {
form.setFieldValue(fieldPath, option.value);
// Resets the value field to empty once the field option changing.
form.setFieldValue(valueFieldPath, '');
}}
popoverProps={{
inline: true,
minimal: true,
captureDismiss: true,
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter value field.
*/
function FilterValueField() {
const { conditionIndex, fieldMeta, getConditionFieldPath } =
useFilterCondition();
// Can't continue if the given field key is not selected yet.
if (!fieldMeta) {
return null;
}
// Field meta type, name and options.
const fieldType = get(fieldMeta, 'fieldType');
const fieldName = get(fieldMeta, 'name');
const options = get(fieldMeta, 'options');
const valueFieldPath = getConditionFieldPath('value');
return (
<FastField
name={valueFieldPath}
fieldKey={fieldType} // Pass to shouldUpdate function.
shouldUpdate={shouldFilterValueFieldUpdate}
>
{({ form: { setFieldValue }, field }) => (
<FormGroup className={'form-group--value'}>
<AdvancedFilterValueField
isFocus={conditionIndex === 0}
value={field.value}
key={'name'}
label={fieldName}
fieldType={fieldType}
options={options}
onChange={(value) => {
setFieldValue(valueFieldPath, value);
}}
/>
</FormGroup>
)}
</FastField>
);
}
/**
* Advanced filter condition line.
*/
function AdvancedFilterDropdownCondition({ conditionIndex, onRemoveClick }) {
// Handle click remove condition.
const handleClickRemoveCondition = () => {
onRemoveClick && onRemoveClick(conditionIndex);
};
return (
<div className="filter-dropdown__condition">
<FilterConditionProvider conditionIndex={conditionIndex}>
<FilterConditionField />
<FilterFieldsField />
<FilterCompatatorFilter />
<FilterValueField />
<Button
icon={<Icon icon="times" iconSize={14} />}
minimal={true}
onClick={handleClickRemoveCondition}
className={'button--remove'}
/>
</FilterConditionProvider>
</div>
);
}
/**
* Advanced filter dropdown condition.
*/
function AdvancedFilterDropdownConditions({ push, remove, replace, form }) {
const { initialCondition } = useAdvancedFilterContext();
// Handle remove condition.
const handleClickRemoveCondition = (conditionIndex) => {
if (form.values.conditions.length > 1) {
remove(conditionIndex);
} else {
replace(0, { ...initialCondition });
}
};
// Handle new condition button click.
const handleNewConditionBtnClick = (index) => {
push({ ...initialCondition });
};
return (
<div className="filter-dropdonw__conditions-wrap">
<div className={'filter-dropdown__conditions'}>
{form.values.conditions.map((condition, index) => (
<AdvancedFilterDropdownCondition
conditionIndex={index}
onRemoveClick={handleClickRemoveCondition}
/>
))}
</div>
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
</div>
);
}
/**
* Advanced filter dropdown form.
*/
function AdvancedFilterDropdownForm() {
// Advanced filter auto-save.
useAdvancedFilterAutoSubmit();
return (
<div className="filter-dropdown__form">
<FieldArray
name={'conditions'}
render={({ ...fieldArrayProps }) => (
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
)}
/>
</div>
);
}
/**
* Advanced filter dropdown footer.
*/
function AdvancedFilterDropdownFooter({ onClick }) {
// Handle new filter condition button click.
const onClickNewFilter = (event) => {
onClick && onClick(event);
};
return (
<div className="filter-dropdown__footer">
<Button minimal={true} onClick={onClickNewFilter}>
<T id={'new_conditional'} />
</Button>
</div>
);
}
/**
* Advanced filter dropdown.
*/
export function AdvancedFilterDropdown({
fields,
conditions,
defaultFieldKey,
defaultComparator,
defaultValue,
defaultCondition,
onFilterChange,
}) {
// Initial condition.
const initialCondition = {
fieldKey: defaultFieldKey,
comparator: defaultTo(defaultComparator, 'contain'),
condition: defaultTo(defaultCondition, 'or'),
value: defaultTo(defaultValue, ''),
};
// Initial conditions.
const initialConditions = !isEmpty(conditions)
? conditions
: [initialCondition, initialCondition];
const [prevConditions, setPrevConditions] = React.useState(initialConditions);
// Handle the filter dropdown form submit.
const handleFitlerDropdownSubmit = (values) => {
const conditions = filterConditionRoles(values.conditions);
// Campare the current conditions with previous conditions, if they were equal
// there is no need to execute `onFilterChange` function.
if (!isEqual(prevConditions, conditions)) {
onFilterChange && onFilterChange(conditions);
setPrevConditions(conditions);
}
};
// Filter dropdown validation schema.
const validationSchema = getFilterDropdownSchema();
// Initial values.
const initialValues = {
conditions: initialConditions,
};
return (
<div className="filter-dropdown">
<AdvancedFilterDropdownProvider
initialCondition={initialCondition}
fields={fields}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
component={AdvancedFilterDropdownForm}
onSubmit={handleFitlerDropdownSubmit}
/>
</AdvancedFilterDropdownProvider>
</div>
);
}

View File

@@ -0,0 +1,85 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import { get, keyBy } from 'lodash';
import { useFormikContext } from 'formik';
const AdvancedFilterContext = createContext({});
const FilterConditionContext = createContext({});
/**
* Advanced filter dropdown context provider.
*/
function AdvancedFilterDropdownProvider({
initialCondition,
fields,
...props
}) {
const fieldsByKey = keyBy(fields, 'key');
// Retrieve field meta by the given field key.
const getFieldMetaByKey = React.useCallback(
(key) => get(fieldsByKey, key),
[fieldsByKey],
);
// Provider payload.
const provider = { initialCondition, fields, fieldsByKey, getFieldMetaByKey };
return <AdvancedFilterContext.Provider value={provider} {...props} />;
}
/**
* Filter condition row context provider.
*/
function FilterConditionProvider({ conditionIndex, ...props }) {
const { setFieldValue, values } = useFormikContext();
const { getFieldMetaByKey } = useAdvancedFilterContext();
// Condition value path.
const conditionPath = `conditions[${conditionIndex}]`;
// Sets conditions value.
const setConditionValue = React.useCallback(
(field, value) => {
return setFieldValue(`${conditionPath}.${field}`, value);
},
[conditionPath, setFieldValue],
);
// Retrieve condition field value.
const getConditionValue = React.useCallback(
(field) => get(values, `${conditionPath}.${field}`),
[conditionPath, values],
);
// The current condition field meta.
const fieldMeta = React.useMemo(
() => getFieldMetaByKey(getConditionValue('fieldKey')),
[getFieldMetaByKey, getConditionValue],
);
// Retrieve the condition field path.
const getConditionFieldPath = React.useCallback(
(field) => `${conditionPath}.${field}`,
[conditionPath],
);
// Provider payload.
const provider = {
fieldMeta,
conditionIndex,
getConditionValue,
getConditionFieldPath,
setConditionValue,
};
return <FilterConditionContext.Provider value={provider} {...props} />;
}
const useFilterCondition = () => useContext(FilterConditionContext);
const useAdvancedFilterContext = () => useContext(AdvancedFilterContext);
export {
AdvancedFilterDropdownProvider,
FilterConditionProvider,
useAdvancedFilterContext,
useFilterCondition,
};

View File

@@ -0,0 +1,33 @@
// @ts-nocheck
import React from 'react';
import { Popover, PopoverInteractionKind, Position } from '@blueprintjs/core';
import { AdvancedFilterDropdown } from './AdvancedFilterDropdown';
/**
* Advanced filter popover.
*/
export function AdvancedFilterPopover({
popoverProps,
advancedFilterProps,
children,
}) {
return (
<Popover
minimal={true}
content={
<AdvancedFilterDropdown
{...advancedFilterProps}
/>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
canOutsideClickClose={true}
modifiers={{
offset: { offset: '0, 4' },
}}
{...popoverProps}
>
{children}
</Popover>
);
}

View File

@@ -0,0 +1,132 @@
// @ts-nocheck
import React from 'react';
import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment';
import intl from 'react-intl-universal';
import { isUndefined } from 'lodash';
import { useAutofocus } from '@/hooks';
import { T, Choose, ListSelect } from '@/components';
import { momentFormatter } from '@/utils';
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
return (
<ListSelect
items={options}
selectedItem={value}
popoverProps={{
fill: true,
inline: true,
minimal: true,
captureDismiss: true,
}}
defaultText={<T id={'filter.select_option'} />}
textProp={'label'}
selectedItemProp={'key'}
{...rest}
/>
);
}
const IFieldType = {
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
NUMBER: 'number',
DATE: 'date',
};
function tansformDateValue(date, defaultValue = null) {
return date ? moment(date).toDate() : defaultValue;
}
/**
* Advanced filter value field detarminer.
*/
export default function AdvancedFilterValueField2({
value,
fieldType,
options,
onChange,
isFocus,
}) {
const [localValue, setLocalValue] = React.useState(value);
React.useEffect(() => {
if (localValue !== value && !isUndefined(value)) {
setLocalValue(value);
}
}, [localValue, value]);
// Input field reference.
const valueRef = useAutofocus(isFocus);
const triggerOnChange = (value) => onChange && onChange(value);
// Handle input change.
const handleInputChange = (e) => {
if (e.currentTarget.type === 'checkbox') {
setLocalValue(e.currentTarget.checked);
triggerOnChange(e.currentTarget.checked);
} else {
setLocalValue(e.currentTarget.value);
triggerOnChange(e.currentTarget.value);
}
};
// Handle enumeration field type change.
const handleEnumerationChange = (option) => {
setLocalValue(option.key);
triggerOnChange(option.key);
};
// Handle date field change.
const handleDateChange = (date) => {
const formattedDate = moment(date).format('YYYY/MM/DD');
setLocalValue(formattedDate);
triggerOnChange(formattedDate);
};
return (
<Choose>
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
<AdvancedFilterEnumerationField
options={options}
value={localValue}
onItemSelect={handleEnumerationChange}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.DATE}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(localValue)}
onChange={handleDateChange}
popoverProps={{
minimal: true,
position: Position.BOTTOM,
}}
shortcuts={true}
placeholder={intl.get('filter.enter_date')}
fill={true}
inputProps={{
fill: true,
}}
/>
</Choose.When>
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
<Checkbox value={localValue} onChange={handleInputChange} />
</Choose.When>
<Choose.Otherwise>
<InputGroup
placeholder={intl.get('filter.value')}
onChange={handleInputChange}
value={localValue}
inputRef={valueRef}
/>
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -0,0 +1,23 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { debounce } from 'lodash';
const DEBOUNCE_MS = 100;
/**
* Advanced filter auto-save.
*/
export function useAdvancedFilterAutoSubmit() {
const { submitForm, values } = useFormikContext();
const [isSubmit, setIsSubmit] = React.useState(false);
const debouncedSubmit = React.useCallback(
debounce(() => {
return submitForm().then(() => setIsSubmit(true));
}, DEBOUNCE_MS),
[submitForm],
);
React.useEffect(() => debouncedSubmit, [debouncedSubmit, values]);
}

View File

@@ -0,0 +1,112 @@
// @ts-nocheck
import { ArrayHelpers } from 'formik';
import { IPopoverProps } from '@blueprintjs/core';
export type IResourceFieldType = 'text' | 'number' | 'enumeration' | 'boolean';
export interface IResourceField {
name: string;
key: string;
fieldType: IResourceFieldType;
}
export interface IAdvancedFilterDropdown {
fields: IResourceField[];
conditions?: IFilterRole[];
defaultFieldKey: string;
defaultComparator?: string;
defaultValue?: string;
defaultCondition?: string;
onFilterChange?: (filterRoles: IFilterRole[]) => void;
}
export interface IAdvancedFilterDropdownFooter {
onClick?: Function;
}
export interface IFilterFieldsField {
fields: IResourceField[];
}
export interface IFilterRole {
fieldKey: string;
comparator: string;
condition: string;
value: string;
}
export interface IAdvancedFilterContextProps {
initialCondition: IFilterRole;
fields: IResourceField[];
fieldsByKey: { [fieldKey: string]: IResourceField };
}
export interface IFilterConditionContextProps {
conditionIndex: number;
}
export interface IAdvancedFilterProviderProps {
initialCondition: IFilterRole;
fields: IResourceField[];
children: JSX.Element | JSX.Element[];
}
export interface IFilterConditionProviderProps {
conditionIndex: number;
children: JSX.Element | JSX.Element[];
}
export interface IFilterDropdownFormikValues {
conditions: IFilterRole[];
}
export type IAdvancedFilterDropdownConditionsProps = ArrayHelpers;
export interface IAdvancedFilterDropdownCondition {
conditionIndex: number;
onRemoveClick: Function;
}
export interface IFilterOption {
key: string;
label: string;
}
export interface IAdvancedFilterValueField {
fieldType: string;
value?: string;
key: string;
label: string;
options?: IFilterOption[];
onChange: Function;
}
export enum IFieldType {
TEXT = 'text',
NUMBER = 'number',
DATE = 'date',
ENUMERATION = 'enumeration',
BOOLEAN = 'boolean',
}
export interface IConditionTypeOption {
value: string;
label: string;
}
export interface IConditionOption {
label: string;
value: string;
text?: string;
}
export interface IAdvancedFilterPopover {
popoverProps?: IPopoverProps;
advancedFilterProps: IAdvancedFilterDropdown;
children: JSX.Element | JSX.Element[];
}
export interface IDynamicFilterCompatatorFieldProps {
dataType: string;
}

View File

@@ -0,0 +1,113 @@
// @ts-nocheck
import intl from 'react-intl-universal';
import {
defaultFastFieldShouldUpdate,
uniqueMultiProps,
checkRequiredProperties,
} from '@/utils';
// Conditions options.
export const getConditionalsOptions = () => [
{
value: 'and',
label: intl.get('and'),
text: intl.get('filter.all_filters_must_match'),
},
{
value: 'or',
label: intl.get('or'),
text: intl.get('filter.atleast_one_filter_must_match'),
},
];
export const getBooleanCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getTextCompatators = () => [
{ value: 'contain', label: intl.get('contain') },
{ value: 'not_contain', label: intl.get('not_contain') },
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equals') },
{ value: 'starts_with', label: intl.get('starts_with') },
{ value: 'ends_with', label: intl.get('ends_with') },
];
export const getDateCompatators = () => [
{ value: 'in', label: intl.get('in') },
{ value: 'after', label: intl.get('after') },
{ value: 'before', label: intl.get('before') },
];
export const getOptionsCompatators = () => [
{ value: 'is', label: intl.get('is') },
{ value: 'is_not', label: intl.get('is_not') },
];
export const getNumberCampatators = () => [
{ value: 'equal', label: intl.get('equals') },
{ value: 'not_equal', label: intl.get('not_equal') },
{ value: 'bigger_than', label: intl.get('bigger_than') },
{ value: 'bigger_or_equal', label: intl.get('bigger_or_equals') },
{ value: 'smaller_than', label: intl.get('smaller_than') },
{ value: 'smaller_or_equal', label: intl.get('smaller_or_equals') },
];
export const getConditionTypeCompatators = (
dataType,
) => {
return [
...(dataType === 'enumeration'
? [...getOptionsCompatators()]
: dataType === 'date'
? [...getDateCompatators()]
: dataType === 'boolean'
? [...getBooleanCompatators()]
: dataType === 'number'
? [...getNumberCampatators()]
: [...getTextCompatators()]),
];
};
export const getConditionDefaultCompatator = (
dataType,
) => {
const compatators = getConditionTypeCompatators(dataType);
return compatators[0];
};
export const transformFieldsToOptions = (fields) =>
fields.map((field) => ({
value: field.key,
label: field.name,
}));
/**
* Filtered conditions that don't contain atleast on required fields or
* fileds keys that not exists.
* @param {IFilterRole[]} conditions
* @returns
*/
export const filterConditionRoles = (
conditions,
) => {
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
const filteredConditions = conditions.filter(
(condition) =>
!checkRequiredProperties(condition, requiredProps),
);
return uniqueMultiProps(filteredConditions, requiredProps);
};
/**
* Detarmines the value field when should update.
* @returns {boolean}
*/
export const shouldFilterValueFieldUpdate = (newProps, oldProps) => {
return (
newProps.fieldKey !== oldProps.fieldKey ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};

View File

@@ -0,0 +1,61 @@
// @ts-nocheck
import React from 'react';
import clsx from 'classnames';
import styled from 'styled-components';
export function Alert({ title, description, children, intent, className }) {
return (
<AlertRoot className={clsx(className)} intent={intent}>
{title && <AlertTitle>{title}</AlertTitle>}
{description && <AlertDesc>{description}</AlertDesc>}
{children && <AlertDesc>{children}</AlertDesc>}
</AlertRoot>
);
}
const AlertRoot = styled.div`
border: 1px solid rgb(223, 227, 230);
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
${(props) =>
props.intent === 'danger' &&
`
border-color: rgb(249, 198, 198);
background: rgb(255, 248, 248);
${AlertDesc} {
color: #d95759;
}
${AlertTitle} {
color: rgb(205, 43, 49);
}
`}
${(props) =>
props.intent === 'primary' &&
`
background: #fff;
border-color: #98a8ee;
${AlertTitle} {
color: #1a3bd4;
}
${AlertDesc} {
color: #455883;
}
`}
`;
export const AlertTitle = styled.h3`
color: rgb(17, 24, 28);
margin-bottom: 4px;
font-size: 14px;
font-weight: 600;
`;
export const AlertDesc = styled.p`
color: rgb(104, 112, 118);
margin: 0;
`;

View File

@@ -0,0 +1,64 @@
// @ts-nocheck
import React from 'react';
import { Router, Switch, Route } from 'react-router';
import { createBrowserHistory } from 'history';
import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import '@/style/App.scss';
import 'moment/locale/ar-ly';
import 'moment/locale/es-us';
import AppIntlLoader from './AppIntlLoader';
import PrivateRoute from '@/components/Guards/PrivateRoute';
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
import Authentication from '@/components/Authentication';
import { SplashScreen, DashboardThemeProvider } from '../components';
import { queryConfig } from '../hooks/query/base';
/**
* App inner.
*/
function AppInsider({ history }) {
return (
<div className="App">
<DashboardThemeProvider>
<Router history={history}>
<Switch>
<Route path={'/auth'} component={Authentication} />
<Route path={'/'}>
<PrivateRoute component={DashboardPrivatePages} />
</Route>
</Switch>
</Router>
<GlobalErrors />
</DashboardThemeProvider>
</div>
);
}
/**
* Core application.
*/
export default function App() {
// Browser history.
const history = createBrowserHistory();
// Query client.
const queryClient = new QueryClient(queryConfig);
return (
<QueryClientProvider client={queryClient}>
<SplashScreen />
<AppIntlLoader>
<AppInsider history={history} />
</AppIntlLoader>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,157 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import { setLocale } from 'yup';
import intl from 'react-intl-universal';
import { find } from 'lodash';
import rtlDetect from 'rtl-detect';
import * as R from 'ramda';
import { AppIntlProvider } from './AppIntlProvider';
import { useSplashLoading } from '@/hooks/state';
import { useWatchImmediate } from '../hooks';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
const SUPPORTED_LOCALES = [
{ name: 'English', value: 'en' },
{ name: 'العربية', value: 'ar' },
];
/**
* Retrieve the current local.
*/
function getCurrentLocal() {
let currentLocale = intl.determineLocale({
urlLocaleKey: 'lang',
cookieLocaleKey: 'locale',
localStorageLocaleKey: 'lang',
});
if (!find(SUPPORTED_LOCALES, { value: currentLocale })) {
currentLocale = 'en';
}
return currentLocale;
}
/**
* Loads the localization data of the given locale.
*/
function loadLocales(currentLocale) {
return import(`../lang/${currentLocale}/index.json`);
}
/**
* Loads the localization data of yup validation library.
*/
function loadYupLocales(currentLocale) {
return import(`../lang/${currentLocale}/locale`);
}
/**
* Modifies the html document direction to RTl if it was rtl-language.
*/
function useDocumentDirectionModifier(locale, isRTL) {
React.useEffect(() => {
if (isRTL) {
const htmlDocument = document.querySelector('html');
htmlDocument.setAttribute('dir', 'rtl');
htmlDocument.setAttribute('lang', locale);
}
}, [isRTL, locale]);
}
function transformMomentLocale(currentLocale) {
return currentLocale === 'ar' ? 'ar-ly' : currentLocale;
}
/**
* Loads application locales of the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Lodas the locales data file.
loadLocales(currentLocale)
.then((results) => {
return intl.init({
currentLocale,
locales: {
[currentLocale]: results,
},
});
})
.then(() => {
moment.locale(transformMomentLocale(currentLocale));
setIsLoading(false);
});
}, [currentLocale, stopLoading]);
// Watches the value to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Loads application yup locales based on the given current locale.
* @param {string} currentLocale
* @returns {{ isLoading: boolean }}
*/
function useAppYupLoadLocales(currentLocale) {
const [startLoading, stopLoading] = useSplashLoading();
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
loadYupLocales(currentLocale)
.then(({ locale }) => {
setLocale(locale);
setIsLoading(false);
})
.then(() => {});
}, [currentLocale, stopLoading]);
// Watches the valiue to start/stop splash screen.
useWatchImmediate(
(value) => (value ? startLoading() : stopLoading()),
isLoading,
);
return { isLoading };
}
/**
* Application Intl loader.
*/
function AppIntlLoader({ children }) {
// Retrieve the current locale.
const currentLocale = getCurrentLocal();
// Detarmines the document direction based on the given locale.
const isRTL = rtlDetect.isRtlLang(currentLocale);
// Modifies the html document direction
useDocumentDirectionModifier(currentLocale, isRTL);
// Loads yup localization of the given locale.
const { isLoading: isAppYupLocalesLoading } =
useAppYupLoadLocales(currentLocale);
// Loads application locales of the given locale.
const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale);
// Detarmines whether the app locales loading.
const isLoading = isAppYupLocalesLoading || isAppLocalesLoading;
return (
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
{isLoading ? null : children}
</AppIntlProvider>
);
}
export default R.compose(withDashboardActions)(AppIntlLoader);

View File

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

View File

@@ -0,0 +1,7 @@
// @ts-nocheck
import { Position, Toaster, Intent } from '@blueprintjs/core';
export const AppToaster = Toaster.create({
position: Position.RIGHT_BOTTOM,
intent: Intent.WARNING,
});

View File

@@ -0,0 +1,62 @@
// @ts-nocheck
import React from 'react';
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
import BodyClassName from 'react-body-classname';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state';
import '@/style/pages/Authentication/Auth.scss';
function PageFade(props) {
return <CSSTransition {...props} classNames="authTransition" timeout={500} />;
}
export default function AuthenticationWrapper({ ...rest }) {
const to = { pathname: '/' };
const location = useLocation();
const isAuthenticated = useIsAuthenticated();
const locationKey = location.pathname;
return (
<>
{isAuthenticated ? (
<Redirect to={to} />
) : (
<BodyClassName className={'authentication'}>
<div class="authentication-page">
<a
href={'http://bigcapital.ly'}
className={'authentication-page__goto-bigcapital'}
>
<T id={'go_to_bigcapital_com'} />
</a>
<div class="authentication-page__form-wrapper">
<div class="authentication-insider">
<div className={'authentication-insider__logo-section'}>
<Icon icon="bigcapital" height={37} width={214} />
</div>
<TransitionGroup>
<PageFade key={locationKey}>
<Switch>
{authenticationRoutes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</PageFade>
</TransitionGroup>
</div>
</div>
</div>
</BodyClassName>
)}
</>
);
}

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
import { firstLettersArgs } from '@/utils';
export default function AvatarCell({ row: { original }, size }) {
return (
<span className="avatar" data-size={size}>
{firstLettersArgs(original?.display_name)}
</span>
);
}

View File

@@ -0,0 +1,207 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import clsx from 'classnames';
import { Classes } from '@blueprintjs/core';
import { Icon } from '@/components/Icon';
const ACCOUNT_TYPE = {
CASH: 'cash',
BANK: 'bank',
CREDIT_CARD: 'credit-card',
};
const ACCOUNT_TYPE_PAIR_ICON = {
[ACCOUNT_TYPE.CASH]: 'payments',
[ACCOUNT_TYPE.CREDIT_CARD]: 'credit-card',
[ACCOUNT_TYPE.BANK]: 'account-balance',
};
function BankAccountMetaLine({ title, value, className }) {
return (
<MetaLineWrap className={className}>
<MetaLineTitle>{title}</MetaLineTitle>
{value && <MetaLineValue>{value}</MetaLineValue>}
</MetaLineWrap>
);
}
function BankAccountBalance({ amount, loading }) {
return (
<BankAccountBalanceWrap>
<BankAccountBalanceAmount
className={clsx({
[Classes.SKELETON]: loading,
})}
>
{amount}
</BankAccountBalanceAmount>
<BankAccountBalanceLabel>{intl.get('balance')}</BankAccountBalanceLabel>
</BankAccountBalanceWrap>
);
}
function BankAccountTypeIcon({ type }) {
const icon = ACCOUNT_TYPE_PAIR_ICON[type];
if (!icon) {
return;
}
return (
<AccountIconWrap>
<Icon icon={icon} iconSize={18} />
</AccountIconWrap>
);
}
export function BankAccount({
title,
code,
type,
balance,
loading = false,
updatedBeforeText,
...restProps
}) {
return (
<BankAccountWrap {...restProps}>
<BankAccountHeader>
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
{title}
</BankAccountTitle>
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
{code}
</BnakAccountCode>
{!loading && <BankAccountTypeIcon type={type} />}
</BankAccountHeader>
<BankAccountMeta>
<BankAccountMetaLine
title={intl.get('cash_flow.label_account_transcations')}
value={2}
className={clsx({ [Classes.SKELETON]: loading })}
/>
<BankAccountMetaLine
title={updatedBeforeText}
className={clsx({ [Classes.SKELETON]: loading })}
/>
</BankAccountMeta>
<BankAccountBalance amount={balance} loading={loading} />
</BankAccountWrap>
);
}
const BankAccountWrap = styled.div`
width: 225px;
height: 180px;
display: flex;
flex-direction: column;
border-radius: 3px;
background: #fff;
margin: 8px;
border: 1px solid #c8cad0;
transition: all 0.1s ease-in-out;
&:hover {
border-color: #0153cc;
}
`;
const BankAccountHeader = styled.div`
padding: 10px 12px;
padding-top: 16px;
position: relative;
`;
const BankAccountTitle = styled.div`
font-size: 15px;
font-style: inherit;
letter-spacing: -0.003em;
color: rgb(23, 43, 77);
white-space: nowrap;
font-weight: 600;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
margin: 0px;
padding-right: 24px;
`;
const BnakAccountCode = styled.div`
font-size: 11px;
margin-top: 4px;
color: rgb(23, 43, 77);
display: inline-block;
`;
const BankAccountBalanceWrap = styled.div`
display: flex;
flex-direction: column;
margin-top: auto;
border-top: 1px solid #dfdfdf;
padding: 10px 12px;
`;
const BankAccountBalanceAmount = styled.div`
font-size: 16px;
font-weight: 600;
line-height: 1;
color: #57657e;
`;
const BankAccountBalanceLabel = styled.div`
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
margin-top: 3px;
opacity: 0.6;
`;
const MetaLineWrap = styled.div`
font-size: 11px;
display: flex;
color: #2f3c58;
&:not(:first-of-type) {
margin-top: 6px;
}
`;
const MetaLineTitle = styled.div``;
const MetaLineValue = styled.div`
box-sizing: border-box;
font-style: inherit;
background: rgb(223, 225, 230);
line-height: initial;
align-content: center;
padding: 0px 2px;
border-radius: 9.6px;
font-weight: normal;
text-transform: none;
width: 30px;
min-width: 30px;
height: 16px;
text-align: center;
color: rgb(23, 43, 77);
font-size: 11px;
margin-left: auto;
`;
const BankAccountMeta = styled.div`
padding: 0 12px 10px;
`;
export const BankAccountsList = styled.div`
display: flex;
margin: -8px;
flex-wrap: wrap;
`;
const AccountIconWrap = styled.div`
position: absolute;
top: 14px;
color: #abb3bb;
right: 12px;
`;

View File

@@ -0,0 +1,73 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { FMultiSelect } from '../Forms';
/**
*
* @param {*} query
* @param {*} branch
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const branchItemPredicate = (query, branch, _index, exactMatch) => {
const normalizedTitle = branch.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} branch
* @param {*} param1
* @returns
*/
const branchItemRenderer = (
branch,
{ handleClick, modifiers, query },
{ isSelected },
) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
icon={isSelected ? 'tick' : 'blank'}
text={branch.name}
label={branch.code}
key={branch.id}
onClick={handleClick}
/>
);
};
const branchSelectProps = {
itemPredicate: branchItemPredicate,
itemRenderer: branchItemRenderer,
valueAccessor: (item) => item.id,
labelAccessor: (item) => item.code,
tagRenderer: (item) => item.name,
};
/**
* branches mulit select.
* @param {*} param0
* @returns {JSX.Element}
*/
export function BranchMultiSelect({ branches, ...rest }) {
return (
<FMultiSelect
items={branches}
placeholder={intl.get('branches_multi_select.placeholder')}
popoverProps={{ minimal: true }}
{...branchSelectProps}
{...rest}
/>
);
}

View File

@@ -0,0 +1,70 @@
// @ts-nocheck
import React from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from '../Forms';
/**
*
* @param {*} query
* @param {*} branch
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const branchItemPredicate = (query, branch, _index, exactMatch) => {
const normalizedTitle = branch.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} film
* @param {*} param1
* @returns
*/
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
const text = `${branch.name}`;
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
label={branch.code}
key={branch.id}
onClick={handleClick}
text={text}
/>
);
};
const branchSelectProps = {
itemPredicate: branchItemPredicate,
itemRenderer: branchItemRenderer,
valueAccessor: 'id',
labelAccessor: 'name',
};
/**
*
* @param {*} param0
* @returns
*/
export function BranchSelect({ branches, ...rest }) {
return <FSelect {...branchSelectProps} {...rest} items={branches} />;
}
/**
*
* @param {*} param0
* @returns
*/
export function BranchSelectButton({ label, ...rest }) {
return <Button text={label} {...rest} />;
}

View File

@@ -0,0 +1,125 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
/**
* branch suggest field.
* @returns
*/
export function BranchSuggestField({
branches,
initialBranchId,
selectedBranchId,
defaultSelectText = intl.get('select_branch'),
popoverFill = false,
onBranchSelected,
...suggestProps
}) {
const initialBranch = React.useMemo(
() => branches.find((b) => b.id === initialBranchId),
[initialBranchId, branches],
);
const [selectedBranch, setSelectedBranch] = React.useState(
initialBranch || null,
);
React.useEffect(() => {
if (typeof selectedBranchId !== 'undefined') {
const branch = selectedBranchId
? branches.find((a) => a.id === selectedBranchId)
: null;
setSelectedBranch(branch);
}
}, [selectedBranchId, branches, setSelectedBranch]);
/**
*
* @param {*} branch
* @returns
*/
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
return (
<MenuItem
// active={modifiers.active}
disabled={modifiers.disabled}
label={branch.code}
key={branch.id}
onClick={handleClick}
text={branch.name}
/>
);
};
/**
*
* @param {*} query
* @param {*} branch
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const branchItemPredicate = (query, branch, _index, exactMatch) => {
const normalizedTitle = branch.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
/**
*
* @param {*} branch
* @returns
*/
const brnachItemSelect = React.useCallback(
(branch) => {
if (branch.id) {
setSelectedBranch({ ...branch });
onBranchSelected && onBranchSelected(branch);
}
},
[setSelectedBranch, onBranchSelected],
);
/**
*
* @param {*} inputVaue
* @returns
*/
const branchInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
return (
<Suggest
items={branches}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={branchItemRenderer}
itemPredicate={branchItemPredicate}
onItemSelect={brnachItemSelect}
selectedItem={selectedBranch}
inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true}
fill={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={branchInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -0,0 +1,4 @@
// @ts-nocheck
export * from './BranchSelect';
export * from './BranchMultiSelect';
export * from './BranchSuggestField';

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import styled from 'styled-components';
export const ButtonLink = styled.button`
color: #0052cc;
border: 0;
background: transparent;
cursor: pointer;
text-align: inherit;
&:hover,
&:active {
text-decoration: underline;
}
`;

View File

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

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
export function Card({ className, children }) {
return <CardRoot className={className}>{children}</CardRoot>;
}
const CardRoot = styled.div`
padding: 15px;
margin: 15px;
background: #fff;
border: 1px solid #d2dce2;
`;
export const CardFooterActions = styled.div`
padding-top: 16px;
border-top: 1px solid #e0e7ea;
margin-top: 30px;
.bp3-button {
min-width: 70px;
+ .bp3-button {
margin-left: 10px;
}
}
`;

View File

@@ -0,0 +1,25 @@
import styled from 'styled-components';
import { Card } from '../Card';
import { DataTable } from '../Datatable';
export const CommercialDocBox = styled(Card)`
padding: 22px 20px;
`;
export const CommercialDocHeader = styled.div`
margin-bottom: 25px;
`;
export const CommercialDocTopHeader = styled.div`
margin-bottom: 30px;
`;
export const CommercialDocEntriesTable = styled(DataTable)`
.tbody .tr:last-child .td {
border-bottom: 1px solid #d2dce2;
}
`;
export const CommercialDocFooter = styled.div`
margin-top: 28px;
`;

View File

@@ -0,0 +1,111 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
export function ContactSelecetList({
contactsList,
initialContactId,
selectedContactId,
createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
// Filter Contact List
const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Select
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={onContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,87 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,43 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { MultiSelect } from '../MultiSelectTaggable';
/**
* Contacts multi-select component.
*/
export function ContactsMultiSelect({ ...multiSelectProps }) {
// 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 (
<MultiSelect
itemRenderer={(contact, { selected, active, handleClick }) => (
<MenuItem
active={active}
icon={selected ? 'tick' : 'blank'}
text={contact.display_name}
key={contact.id}
onClick={handleClick}
/>
)}
popoverProps={{ minimal: true }}
fill={true}
itemPredicate={filterContactsPredicater}
tagRenderer={(item) => item.display_name}
{...multiSelectProps}
/>
);
}

View File

@@ -0,0 +1,108 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import intl from 'react-intl-universal';
export function ContactsSuggestField({
contactsList,
initialContactId,
selectedContactId,
defaultTextSelect = intl.get('select_contact'),
onContactSelected,
selectedContactType = [],
popoverFill = false,
...suggestProps
}) {
// filteredContacts
const contacts = useMemo(
() =>
contactsList.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contactsList],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const contact = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(contact);
}
}, [selectedContactId, contacts, setSelectedContact]);
const contactRenderer = useCallback(
(contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
label={contact.formatted_contact_service}
onClick={handleClick}
/>
),
[],
);
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.display_name.toString();
}
};
const filterContacts = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return (
<Suggest
items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={contactRenderer}
itemPredicate={filterContacts}
onItemSelect={onContactSelect}
selectedItem={selecetedContact}
inputProps={{ placeholder: defaultTextSelect }}
resetOnClose={true}
popoverProps={{ minimal: true, boundary: 'window' }}
inputValueRenderer={handleInputValueRenderer}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
{...suggestProps}
/>
);
}

View File

@@ -0,0 +1,5 @@
// @ts-nocheck
export * from './ContactSelectField';
export * from './ContactsSuggestField';
export * from './ContactSelecetList';
export * from './ContactsMultiSelect';

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
// Filter Contact List
export const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
};
export const handleContactRenderer = (contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
);
// Creates a new item from query.
export const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
export const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};

View File

@@ -0,0 +1,53 @@
// @ts-nocheck
import React, { memo } from 'react';
import { Popover, Position, Classes } from '@blueprintjs/core';
import { saveInvoke } from '@/utils';
const POPPER_MODIFIERS = {
preventOverflow: { boundariesElement: 'viewport' },
};
function ContextMenu(props) {
const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
const handleClosed = () => {
requestAnimationFrame(() => saveInvoke(onClosed));
};
const handleInteraction = (nextOpenState) => {
if (!nextOpenState) {
// Delay the actual hiding till the event queue clears
// to avoid flicker of opening twice
requestAnimationFrame(() => saveInvoke(onClosed));
}
};
return (
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} {...bindMenu}>
<Popover
onClosed={handleClosed}
modifiers={POPPER_MODIFIERS}
content={children}
enforceFocus={true}
isOpen={isOpen}
minimal={true}
position={Position.RIGHT_TOP}
target={<div />}
usePortal={false}
onInteraction={handleInteraction}
{...popoverProps}
/>
</div>
);
}
export default memo(ContextMenu, (prevProps, nextProps) => {
if (
prevProps.isOpen === nextProps.isOpen &&
prevProps.bindMenu.style === nextProps.bindMenu.style
) {
return true;
} else {
return false;
}
});

View File

@@ -0,0 +1,11 @@
// @ts-nocheck
import React from 'react';
import { CurrencyTag } from '@/components';
/**
* base currecncy.
* @returns
*/
export function BaseCurrency({ currency }) {
return <CurrencyTag>{currency}</CurrencyTag>;
}

View File

@@ -0,0 +1,59 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
export function CurrenciesSelectList({ selectProps, onItemSelect, className }) {
const currencies = [
{
name: 'USD US dollars',
key: 'USD',
name: 'CAD Canadian dollars',
key: 'CAD',
},
];
// Handle currency item select.
const onCurrencySelect = useCallback(
(currency) => {
onItemSelect && onItemSelect(currency);
},
[onItemSelect],
);
// Filters currencies list.
const filterCurrenciesPredicator = useCallback(
(query, currency, _index, exactMatch) => {
const normalizedTitle = currency.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
return `${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
},
[],
);
// Currency item of select currencies field.
const currencyItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
};
return (
<Select
items={currencies}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={currencyItem}
itemPredicate={filterCurrenciesPredicator}
popoverProps={{ minimal: true }}
onItemSelect={onCurrencySelect}
{...selectProps}
>
<Button text={'USD US dollars'} />
</Select>
);
}

View File

@@ -0,0 +1,77 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { FSelect } from '../Forms';
/**
*
* @param {*} query
* @param {*} currency
* @param {*} _index
* @param {*} exactMatch
* @returns
*/
const currencyItemPredicate = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${currency.currency_code}. ${normalizedTitle}`.indexOf(
normalizedQuery,
) >= 0
);
}
};
/**
* @param {*} currency
* @returns
*/
const currencyItemRenderer = (currency, { handleClick, modifiers, query }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
text={currency.currency_name}
label={currency.currency_code.toString()}
key={currency.id}
onClick={handleClick}
/>
);
};
const currencySelectProps = {
itemPredicate: currencyItemPredicate,
itemRenderer: currencyItemRenderer,
valueAccessor: 'currency_code',
labelAccessor: 'currency_code',
};
/**
*
* @param {*} currencies
* @returns
*/
export function CurrencySelect({ currencies, ...rest }) {
return (
<FSelect
{...currencySelectProps}
{...rest}
items={currencies}
input={CurrnecySelectButton}
/>
);
}
/**
* @param {*} label
* @returns
*/
function CurrnecySelectButton({ label }) {
return <Button text={label ? label : intl.get('select_currency_code')} />;
}

View File

@@ -0,0 +1,83 @@
// @ts-nocheck
import React, { useCallback, useEffect, useState } from 'react';
import { FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes';
import classNames from 'classnames';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
export function CurrencySelectList({
currenciesList,
selectedCurrencyCode,
defaultSelectText = <T id={'select_currency_code'} />,
onCurrencySelected,
popoverFill = false,
disabled = false,
}) {
const [selectedCurrency, setSelectedCurrency] = useState(null);
// Filters currencies list.
const filterCurrencies = (query, currency, _index, exactMatch) => {
const normalizedTitle = currency.currency_code.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${currency.currency_code} ${normalizedTitle}`.indexOf(
normalizedQuery,
) >= 0
);
}
};
const onCurrencySelect = useCallback((currency) => {
setSelectedCurrency({ ...currency });
onCurrencySelected && onCurrencySelected(currency);
});
const currencyCodeRenderer = useCallback((CurrencyCode, { handleClick }) => {
return (
<MenuItem
key={CurrencyCode.id}
text={CurrencyCode.currency_code}
onClick={handleClick}
/>
);
}, []);
useEffect(() => {
if (typeof selectedCurrencyCode !== 'undefined') {
const currency = selectedCurrencyCode
? currenciesList.find((a) => a.currency_code === selectedCurrencyCode)
: null;
setSelectedCurrency(currency);
}
}, [selectedCurrencyCode, currenciesList, setSelectedCurrency]);
return (
<Select
items={currenciesList}
itemRenderer={currencyCodeRenderer}
itemPredicate={filterCurrencies}
onItemSelect={onCurrencySelect}
filterable={true}
popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
>
<Button
disabled={disabled}
text={
selectedCurrency ? selectedCurrency.currency_code : defaultSelectText
}
/>
</Select>
);
}

View File

@@ -0,0 +1,5 @@
// @ts-nocheck
export * from './CurrencySelect';
export * from './BaseCurrency';
export * from './CurrenciesSelectList';
export * from './CurrencySelectList';

View File

@@ -0,0 +1,32 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { ButtonLink } from '../Button';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
function CustomerDrawerLinkComponent({
// #ownProps
children,
customerId,
className,
// #withDrawerActions
openDrawer,
}) {
// Handle view customer drawer.
const handleCustomerDrawer = (event) => {
openDrawer('customer-detail-drawer', { customerId });
event.preventDefault();
};
return (
<ButtonLink className={className} onClick={handleCustomerDrawer}>
{children}
</ButtonLink>
);
}
export const CustomerDrawerLink = R.compose(withDrawerActions)(
CustomerDrawerLinkComponent,
);

View File

@@ -0,0 +1,119 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemRenderer,
createNewItemFromQuery,
} from './utils';
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers';
function CustomerSelectFieldRoot({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export const CustomerSelectField = R.compose(withDrawerActions)(
CustomerSelectFieldRoot,
);

View File

@@ -0,0 +1,3 @@
// @ts-nocheck
export * from './CustomerSelectField';
export * from './CustomerDrawerLink';

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem } from '@blueprintjs/core';
// Filter Contact List
export const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
};
export const handleContactRenderer = (contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
);
// Creates a new item from query.
export const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
export const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={intl.get('list.create', { value: `"${query}"` })}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};

View File

@@ -0,0 +1,19 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { Icon } from '@/components';
import '@/style/components/BigcapitalLoading.scss';
/**
* Bigcapital logo loading.
*/
export default function BigcapitalLoading({ className }) {
return (
<div className={classNames('bigcapital-loading', className)}>
<div class="center">
<Icon icon="bigcapital" height={37} width={228} />
</div>
</div>
);
}

View File

@@ -0,0 +1,63 @@
// @ts-nocheck
import React from 'react';
import { Switch, Route } from 'react-router';
import '@/style/pages/Dashboard/Dashboard.scss';
import { Sidebar } from '@/containers/Dashboard/Sidebar/Sidebar';
import DashboardContent from '@/components/Dashboard/DashboardContent';
import DialogsContainer from '@/components/DialogsContainer';
import PreferencesPage from '@/components/Preferences/PreferencesPage';
import DashboardUniversalSearch from '@/containers/UniversalSearch/DashboardUniversalSearch';
import DashboardSplitPane from '@/components/Dashboard/DashboardSplitePane';
import GlobalHotkeys from './GlobalHotkeys';
import DashboardProvider from './DashboardProvider';
import DrawersContainer from '@/components/DrawersContainer';
import AlertsContainer from '@/containers/AlertsContainer';
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
/**
* Dashboard preferences.
*/
function DashboardPreferences() {
return (
<EnsureSubscriptionIsActive>
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</EnsureSubscriptionIsActive>
);
}
/**
* Dashboard other routes.
*/
function DashboardAnyPage() {
return (
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
);
}
/**
* Dashboard page.
*/
export default function Dashboard() {
return (
<DashboardProvider>
<Switch>
<Route path="/preferences" component={DashboardPreferences} />
<Route path="/" component={DashboardAnyPage} />
</Switch>
<DashboardUniversalSearch />
<DialogsContainer />
<GlobalHotkeys />
<DrawersContainer />
<AlertsContainer />
</DashboardProvider>
);
}

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import React from 'react';
import { Ability } from '@casl/ability';
import { createContextualCan } from '@casl/react';
import { useDashboardMetaBoot } from './DashboardBoot';
export const AbilityContext = React.createContext();
export const Can = createContextualCan(AbilityContext.Consumer);
/**
* Dashboard ability provider.
*/
export function DashboardAbilityProvider({ children }) {
const {
meta: { abilities },
} = useDashboardMetaBoot();
// Ability instance.
const ability = new Ability(abilities);
return (
<AbilityContext.Provider value={ability}>
{children}
</AbilityContext.Provider>
);
}

View File

@@ -0,0 +1,69 @@
// @ts-nocheck
import React, { useMemo, useState } from 'react';
import classNames from 'classnames';
import {
Button,
Classes,
MenuItem,
Menu,
Popover,
PopoverInteractionKind,
Position,
Divider,
} from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components';
import { Icon } from '@/components';
/**
* Dashboard action views list.
*/
export function DashboardActionViewsList({
resourceName,
allMenuItem,
allMenuItemText,
views,
onChange,
}) {
const handleClickViewItem = (view) => {
onChange && onChange(view);
};
const viewsMenuItems = views.map((view) => (
<MenuItem onClick={() => handleClickViewItem(view)} text={view.name} />
));
const handleAllTabClick = () => {
handleClickViewItem(null);
};
const content = (
<Menu>
{allMenuItem && (
<>
<MenuItem
onClick={handleAllTabClick}
text={allMenuItemText || 'All'}
/>
<Divider />
</>
)}
{viewsMenuItems}
</Menu>
);
return (
<Popover
content={content}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="table-16" iconSize={16} />}
text={<T id={'table_views'} />}
rightIcon={'caret-down'}
/>
</Popover>
);
}

View File

@@ -0,0 +1,20 @@
// @ts-nocheck
import React from 'react';
import clsx from 'classnames';
import { Navbar } from '@blueprintjs/core';
export function DashboardActionsBar({ className, children, name }) {
return (
<div
className={clsx(
{
'dashboard__actions-bar': true,
[`dashboard__actions-bar--${name}`]: !!name,
},
className,
)}
>
<Navbar className="navbar--dashboard-actions-bar">{children}</Navbar>
</div>
);
}

View File

@@ -0,0 +1,40 @@
// @ts-nocheck
import React from 'react';
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
import { useHistory } from 'react-router-dom';
import { getDashboardRoutes } from '@/routes/dashboard';
import { If, Icon } from '@/components';
import { FormattedMessage as T } from '@/components';
import withDashboard from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils';
function DashboardBackLink({ dashboardBackLink, breadcrumbs }) {
const history = useHistory();
const crumb = breadcrumbs[breadcrumbs.length - 2];
const handleClick = (event) => {
const url =
typeof dashboardBackLink === 'string'
? dashboardBackLink
: crumb.match.url;
history.push(url);
event.preventDefault();
};
return (
<If condition={dashboardBackLink && crumb}>
<div class="dashboard__back-link">
<a href="#no-link" onClick={handleClick}>
<Icon icon={'arrow-left'} iconSize={18} /> <T id={'back_to_list'} />
</a>
</div>
</If>
);
}
export default compose(
withBreadcrumbs([]),
withDashboard(({ dashboardBackLink }) => ({
dashboardBackLink,
})),
)(DashboardBackLink);

View File

@@ -0,0 +1,129 @@
// @ts-nocheck
import React from 'react';
import {
useAuthenticatedAccount,
useCurrentOrganization,
useDashboardMeta,
} from '@/hooks/query';
import { useSplashLoading } from '@/hooks/state';
import { useWatch, useWatchImmediate, useWhen } from '@/hooks';
import { useSubscription } from '@/hooks/state';
import { setCookie, getCookie } from '@/utils';
/**
* Dashboard meta async booting.
* - Fetches the dashboard meta only if the organization subscribe is active.
* - Once the dashboard meta query is loading display dashboard splash screen.
*/
export function useDashboardMetaBoot() {
const { isSubscriptionActive } = useSubscription();
const {
data: dashboardMeta,
isLoading: isDashboardMetaLoading,
isSuccess: isDashboardMetaSuccess,
} = useDashboardMeta({
keepPreviousData: true,
// Avoid run the query if the organization subscription is not active.
enabled: isSubscriptionActive,
});
const [startLoading, stopLoading] = useSplashLoading();
useWatchImmediate((value) => {
value && startLoading();
}, isDashboardMetaLoading);
useWatchImmediate(() => {
isDashboardMetaSuccess && stopLoading();
}, isDashboardMetaSuccess);
return {
meta: dashboardMeta,
isLoading: isDashboardMetaLoading,
isSuccess: isDashboardMetaSuccess,
};
}
/**
* Application async booting.
*/
export function useApplicationBoot() {
// Fetches the current user's organization.
const {
isSuccess: isCurrentOrganizationSuccess,
isLoading: isOrgLoading,
data: organization,
} = useCurrentOrganization();
// Authenticated user.
const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } =
useAuthenticatedAccount();
// Initial locale cookie value.
const localeCookie = getCookie('locale');
// Is the dashboard booted.
const isBooted = React.useRef(false);
// Syns the organization language with locale cookie.
React.useEffect(() => {
if (organization?.metadata?.language) {
setCookie('locale', organization.metadata.language);
}
}, [organization]);
React.useEffect(() => {
// Can't continue if the organization metadata is not loaded yet.
if (!organization?.metadata?.language) {
return;
}
// Can't continue if the organization is already booted.
if (isBooted.current) {
return;
}
// Reboot the application in case the initial locale not equal
// the current organization language.
if (localeCookie !== organization.metadata.language) {
window.location.reload();
}
}, [localeCookie, organization]);
const [startLoading, stopLoading] = useSplashLoading();
// Splash loading when organization request loading and
// applicaiton still not booted.
useWatchImmediate((value) => {
value && !isBooted.current && startLoading();
}, isOrgLoading);
// Splash loading when request authenticated user loading and
// application still not booted yet.
useWatchImmediate((value) => {
value && !isBooted.current && startLoading();
}, isAuthUserLoading);
// Stop splash loading once organization request success.
useWatch((value) => {
value && stopLoading();
}, isCurrentOrganizationSuccess);
// Stop splash loading once authenticated user request success.
useWatch((value) => {
value && stopLoading();
}, isAuthUserSuccess);
// Once the all requests complete change the app loading state.
useWhen(
isAuthUserSuccess &&
isCurrentOrganizationSuccess &&
localeCookie === organization?.metadata?.language,
() => {
isBooted.current = true;
},
);
return {
isLoading: isOrgLoading || isAuthUserLoading,
};
}

View File

@@ -0,0 +1,35 @@
// @ts-nocheck
import React from 'react';
import {
CollapsibleList,
MenuItem,
Classes,
Boundary,
} from '@blueprintjs/core';
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
import { getDashboardRoutes } from '@/routes/dashboard';
import { useHistory } from 'react-router-dom';
function DashboardBreadcrumbs({ breadcrumbs }){
const history = useHistory();
return(
<CollapsibleList
className={Classes.BREADCRUMBS}
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
collapseFrom={Boundary.START}
visibleItemCount={0}>
{
breadcrumbs.map(({ breadcrumb,match })=>{
return (<MenuItem
key={match.url}
icon={'folder-close'}
text={breadcrumb}
onClick={() => history.push(match.url) } />)
})
}
</CollapsibleList>
)
}
export default withBreadcrumbs([])(DashboardBreadcrumbs)

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
// Dashboard card.
export function DashboardCard({ children, page }) {
return (
<div
className={classNames(CLASSES.DASHBOARD_CARD, {
[CLASSES.DASHBOARD_CARD_PAGE]: page,
})}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from '@/components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from '@/components/Dashboard/DashboardContentRoute';
import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => {
return (
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
<div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar />
<DashboardContentRoutes />
</div>
</ErrorBoundary>
);
});

View File

@@ -0,0 +1,60 @@
// @ts-nocheck
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { getDashboardRoutes } from '@/routes/dashboard';
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
import DashboardPage from './DashboardPage';
/**
* Dashboard inner route content.
*/
function DashboardContentRouteContent({ route }) {
const content = (
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
);
return route.subscriptionActive ? (
<EnsureSubscriptionsIsInactive
subscriptionTypes={route.subscriptionActive}
children={content}
redirectTo={'/billing'}
/>
) : route.subscriptionInactive ? (
<EnsureSubscriptionsIsActive
subscriptionTypes={route.subscriptionInactive}
children={content}
redirectTo={'/'}
/>
) : (
content
);
}
/**
* Dashboard content route.
*/
export default function DashboardContentRoute() {
const routes = getDashboardRoutes();
return (
<Route pathname="/">
<Switch>
{routes.map((route, index) => (
<Route exact={route.exact} key={index} path={`${route.path}`}>
<DashboardContentRouteContent route={route} />
</Route>
))}
</Switch>
</Route>
);
}

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
/**
* Dashboard content table.
*/
export function DashboardContentTable({ children }) {
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>{children}</div>
);
}

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import React from 'react';
import { FormattedMessage as T, Icon } from '@/components';
export default function DashboardErrorBoundary({}) {
return (
<div class="dashboard__error-boundary">
<h1><T id={'sorry_about_that_something_went_wrong'} /></h1>
<p><T id={'if_the_problem_stuck_please_contact_us_as_soon_as_possible'} /></p>
<Icon icon="bigcapital" height={30} width={160} />
</div>
)
}

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import intl from "react-intl-universal";
import { Classes, Button } from '@blueprintjs/core';
import { T, Icon } from '@/components';
/**
* Dashboard advanced filter button.
*/
export function DashboardFilterButton({ conditionsCount }) {
return (
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': conditionsCount > 0,
})}
text={
conditionsCount > 0 ? (
intl.get('count_filters_applied', { count: conditionsCount })
) : (
<T id={'filter'} />
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
);
}

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import React from 'react';
import { getFooterLinks } from '@/constants/footerLinks';
import { For } from '@/components';
function FooterLinkItem({ title, link }) {
return (
<div class="">
<a href={link} target="_blank" rel="noopener noreferrer">
{title}
</a>
</div>
);
}
export default function DashboardFooter() {
const footerLinks = getFooterLinks();
return (
<div class="dashboard__footer">
<div class="footer-links">
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
// @ts-nocheck
import React from 'react';
import classnames from 'classnames';
import { LoadingIndicator } from '../Indicator';
export function DashboardInsider({
loading,
children,
name,
mount = false,
className,
}) {
return (
<div
className={classnames(
{
dashboard__insider: true,
'dashboard__insider--loading': loading,
[`dashboard__insider--${name}`]: !!name,
},
className,
)}
>
<LoadingIndicator loading={loading} mount={mount}>
{children}
</LoadingIndicator>
</div>
);
}

View File

@@ -0,0 +1,25 @@
// @ts-nocheck
import React from 'react';
import { Choose } from '@/components';
import BigcapitalLoading from './BigcapitalLoading';
/**
* Dashboard loading indicator.
*/
export default function DashboardLoadingIndicator({
isLoading = false,
className,
children,
}) {
return (
<Choose>
<Choose.When condition={isLoading}>
<BigcapitalLoading />
</Choose.When>
<Choose.Otherwise>
{ children }
</Choose.Otherwise>
</Choose>
);
}

View File

@@ -0,0 +1,105 @@
// @ts-nocheck
import React, { useEffect, Suspense } from 'react';
import { CLASSES } from '@/constants/classes';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose } from '@/utils';
import { Spinner } from '@blueprintjs/core';
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
/**
* Dashboard pages wrapper.
*/
function DashboardPage({
// #ownProps
pageTitle,
backLink,
sidebarExpand = true,
Component,
name,
hint,
defaultSearchResource,
// #withDashboardActions
changePageTitle,
setDashboardBackLink,
changePageHint,
toggleSidebarExpand,
// #withUniversalSearch
setResourceTypeUniversalSearch,
resetResourceTypeUniversalSearch,
}) {
// Hydrate the given page title.
useEffect(() => {
pageTitle && changePageTitle(pageTitle);
return () => {
pageTitle && changePageTitle('');
};
});
// Hydrate the given page hint.
useEffect(() => {
hint && changePageHint(hint);
return () => {
hint && changePageHint('');
};
}, [hint, changePageHint]);
// Hydrate the dashboard back link status.
useEffect(() => {
backLink && setDashboardBackLink(backLink);
return () => {
backLink && setDashboardBackLink(false);
};
}, [backLink, setDashboardBackLink]);
useEffect(() => {
const className = `page-${name}`;
name && document.body.classList.add(className);
return () => {
name && document.body.classList.remove(className);
};
}, [name]);
useEffect(() => {
toggleSidebarExpand(sidebarExpand);
}, [toggleSidebarExpand, sidebarExpand]);
useEffect(() => {
if (defaultSearchResource) {
setResourceTypeUniversalSearch(defaultSearchResource);
}
return () => {
resetResourceTypeUniversalSearch();
};
}, [
defaultSearchResource,
resetResourceTypeUniversalSearch,
setResourceTypeUniversalSearch,
]);
return (
<div className={CLASSES.DASHBOARD_PAGE}>
<Suspense
fallback={
<div class="dashboard__fallback-loading">
<Spinner size={40} value={null} />
</div>
}
>
<Component />
</Suspense>
</div>
);
}
export default compose(
withDashboardActions,
// withUniversalSearch,
withUniversalSearchActions,
)(DashboardPage);

View File

@@ -0,0 +1,9 @@
// @ts-nocheck
import React from 'react';
/**
* Dashboard page content.
*/
export function DashboardPageContent({ children }) {
return <div class="dashboard__page-content">{children}</div>;
}

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import React from 'react';
import { DashboardAbilityProvider } from '../../components';
import { useDashboardMetaBoot } from './DashboardBoot';
/**
* Dashboard provider.
*/
export default function DashboardProvider({ children }) {
const { isLoading } = useDashboardMetaBoot();
// Avoid display any dashboard component before complete booting.
if (isLoading) {
return null;
}
return <DashboardAbilityProvider>{children}</DashboardAbilityProvider>;
}

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
import React from 'react';
import {
Button,
PopoverInteractionKind,
Popover,
Menu,
MenuItem,
MenuDivider,
Classes,
Tooltip,
Position,
} from '@blueprintjs/core';
import clsx from 'classnames';
import { Icon, T } from '@/components';
import Style from './style.module.scss';
/**
* Dashboard rows height button control.
*/
export function DashboardRowsHeightButton({ initialValue, value, onChange }) {
const [localSize, setLocalSize] = React.useState(initialValue);
// Handle menu item click.
const handleItemClick = (size) => (event) => {
setLocalSize(size);
onChange && onChange(size, event);
};
// Button icon name.
const btnIcon = `table-row-${localSize}`;
return (
<Popover
minimal={true}
content={
<Menu className={Style.menu}>
<MenuDivider title={<T id={'dashboard.rows_height'} />} />
<MenuItem
onClick={handleItemClick('small')}
text={<T id={'dashboard.row_small'} />}
/>
<MenuItem
onClick={handleItemClick('medium')}
text={<T id={'dashboard.row_medium'} />}
/>
</Menu>
}
placement="bottom-start"
modifiers={{
offset: { offset: '0, 4' },
}}
interactionKind={PopoverInteractionKind.CLICK}
>
<Tooltip
content={<T id={'dashboard.rows_height'} />}
minimal={true}
position={Position.BOTTOM}
>
<Button
className={clsx(Classes.MINIMAL, Style.button)}
icon={<Icon icon={btnIcon} iconSize={16} />}
/>
</Tooltip>
</Popover>
);
}
DashboardRowsHeightButton.defaultProps = {
initialValue: 'medium',
};

View File

@@ -0,0 +1,12 @@
.menu{
:global .bp3-heading{
font-weight: 400;
opacity: 0.5;
font-size: 12px;
}
}
.button{
min-width: 34px;
}

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React, { useState, useRef } from 'react';
import SplitPane from 'react-split-pane';
import { debounce } from 'lodash';
import withDashboard from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils';
function DashboardSplitPane({
sidebarExpended,
children
}) {
const initialSize = 220;
const [defaultSize, setDefaultSize] = useState(
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
);
const debounceSaveSize = useRef(
debounce((size) => {
localStorage.setItem('dashboard-size', size);
}, 500),
);
const handleChange = (size) => {
debounceSaveSize.current(size);
setDefaultSize(size);
}
return (
<SplitPane
allowResize={sidebarExpended}
split="vertical"
minSize={180}
maxSize={300}
defaultSize={sidebarExpended ? defaultSize : 50}
size={sidebarExpended ? defaultSize : 50}
onChange={handleChange}
className="primary"
>
{children}
</SplitPane>
);
}
export default compose(
withDashboard(({ sidebarExpended }) => ({ sidebarExpended }))
)(DashboardSplitPane);

View File

@@ -0,0 +1,23 @@
// @ts-nocheck
import React from 'react';
import { ThemeProvider, StyleSheetManager } from 'styled-components';
import rtlcss from 'stylis-rtlcss';
import { useAppIntlContext } from '../AppIntlProvider';
interface DashboardThemeProviderProps {
children: React.ReactNode;
}
export function DashboardThemeProvider({
children,
}: DashboardThemeProviderProps) {
const { direction } = useAppIntlContext();
return (
<StyleSheetManager
{...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})}
>
<ThemeProvider theme={{ dir: direction }}>{children}</ThemeProvider>
</StyleSheetManager>
);
}

View File

@@ -0,0 +1,216 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router';
import {
Navbar,
NavbarGroup,
NavbarDivider,
Button,
Classes,
Tooltip,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components';
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
import { Icon, Hint, If } from '@/components';
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import withDashboard from '@/containers/Dashboard/withDashboard';
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from '@/utils';
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
import { useGetUniversalSearchTypeOptions } from '@/containers/UniversalSearch/utils';
function DashboardTopbarSubscriptionMessage() {
return (
<div class="dashboard__topbar-subscription-msg">
<span>
<T id={'dashboard.subscription_msg.period_over'} />
</span>
</div>
);
}
function DashboardHamburgerButton({ ...props }) {
return (
<Button minimal={true} {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
);
}
/**
* Dashboard topbar.
*/
function DashboardTopbar({
// #withDashboard
pageTitle,
editViewId,
pageHint,
// #withDashboardActions
toggleSidebarExpand,
// #withDashboard
sidebarExpended,
// #withGlobalSearch
openGlobalSearch,
// #withSubscriptions
isSubscriptionActive,
isSubscriptionInactive,
}) {
const history = useHistory();
const handlerClickEditView = () => {
history.push(`/custom_views/${editViewId}/edit`);
};
const handleSidebarToggleBtn = () => {
toggleSidebarExpand();
};
return (
<div class="dashboard__topbar">
<div class="dashboard__topbar-left">
<div class="dashboard__topbar-sidebar-toggle">
<Tooltip
content={
!sidebarExpended ? (
<T id={'open_sidebar'} />
) : (
<T id={'close_sidebar'} />
)
}
position={Position.RIGHT}
>
<DashboardHamburgerButton onClick={handleSidebarToggleBtn} />
</Tooltip>
</div>
<div class="dashboard__title">
<h1>{pageTitle}</h1>
<If condition={pageHint}>
<div class="dashboard__hint">
<Hint content={pageHint} />
</div>
</If>
<If condition={editViewId}>
<Button
className={Classes.MINIMAL + ' button--view-edit'}
icon={<Icon icon="pen" iconSize={13} />}
onClick={handlerClickEditView}
/>
</If>
</div>
<div class="dashboard__breadcrumbs">
<DashboardBreadcrumbs />
</div>
<DashboardBackLink />
</div>
<div class="dashboard__topbar-right">
<If condition={isSubscriptionInactive}>
<DashboardTopbarSubscriptionMessage />
</If>
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<If condition={isSubscriptionActive}>
<DashboardQuickSearchButton
onClick={() => openGlobalSearch(true)}
/>
<QuickNewDropdown />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'notification-24'} iconSize={20} />}
/>
</Tooltip>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />}
text={<T id={'help'} />}
/>
<NavbarDivider />
</NavbarGroup>
</Navbar>
<div class="dashboard__topbar-user">
<DashboardTopbarUser />
</div>
</div>
</div>
);
}
export default compose(
withUniversalSearchActions,
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
pageTitle,
editViewId,
sidebarExpended,
pageHint,
})),
withDashboardActions,
withSubscriptions(
({ isSubscriptionActive, isSubscriptionInactive }) => ({
isSubscriptionActive,
isSubscriptionInactive,
}),
'main',
),
)(DashboardTopbar);
/**
* Dashboard quick search button.
*/
function DashboardQuickSearchButton({ ...rest }) {
const searchTypeOptions = useGetUniversalSearchTypeOptions();
// Can't continue if there is no any search type option.
if (searchTypeOptions.length <= 0) {
return null;
}
return (
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'} />}
{...rest}
/>
);
}

View File

@@ -0,0 +1,104 @@
// @ts-nocheck
import React, { useRef, useState, useEffect } from 'react';
import { FormattedMessage as T } from '@/components';
import PropTypes from 'prop-types';
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
import { useHistory } from 'react-router';
import { debounce } from 'lodash';
import { If, Icon } from '@/components';
import { saveInvoke } from '@/utils';
/**
* Dashboard views tabs.
*/
export function DashboardViewsTabs({
initialViewSlug = 0,
currentViewSlug,
tabs,
defaultTabText = <T id={'all'} />,
allTab = true,
newViewTab = true,
resourceName,
onNewViewTabClick,
onChange,
OnThrottledChange,
throttleTime = 250,
}) {
const history = useHistory();
const [currentView, setCurrentView] = useState(initialViewSlug || 0);
useEffect(() => {
if (
typeof currentViewSlug !== 'undefined' &&
currentViewSlug !== currentView
) {
setCurrentView(currentViewSlug || 0);
}
}, [currentView, setCurrentView, currentViewSlug]);
const throttledOnChange = useRef(
debounce((viewId) => saveInvoke(OnThrottledChange, viewId), throttleTime),
);
// Trigger `onChange` and `onThrottledChange` events.
const triggerOnChange = (viewSlug) => {
const value = viewSlug === 0 ? null : viewSlug;
saveInvoke(onChange, value);
throttledOnChange.current(value);
};
// Handles click a new view.
const handleClickNewView = () => {
history.push(`/custom_views/${resourceName}/new`);
onNewViewTabClick && onNewViewTabClick();
};
// Handle tabs change.
const handleTabsChange = (viewSlug) => {
setCurrentView(viewSlug);
triggerOnChange(viewSlug);
};
return (
<div class="dashboard__views-tabs">
<Tabs
id="navbar"
large={true}
selectedTabId={currentView}
className="tabs--dashboard-views"
onChange={handleTabsChange}
animate={false}
>
{allTab && <Tab id={0} title={defaultTabText} />}
{tabs.map((tab) => (
<Tab id={tab.slug} title={tab.name} />
))}
<If condition={newViewTab}>
<Tooltip
content={<T id={'create_a_new_view'} />}
position={Position.RIGHT}
>
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView}
minimal={true}
/>
</Tooltip>
</If>
</Tabs>
</div>
);
}
DashboardViewsTabs.propTypes = {
tabs: PropTypes.array.isRequired,
allTab: PropTypes.bool,
newViewTab: PropTypes.bool,
onNewViewTabClick: PropTypes.func,
onChange: PropTypes.func,
OnThrottledChange: PropTypes.func,
throttleTime: PropTypes.number,
};

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { useHistory } from 'react-router-dom';
import { getDashboardRoutes } from '@/routes/dashboard';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function GlobalHotkeys({
// #withDashboardActions
toggleSidebarExpend,
// #withDialogActions
openDialog,
}) {
const history = useHistory();
const routes = getDashboardRoutes();
const globalHotkeys = routes
.filter(({ hotkey }) => hotkey)
.map(({ hotkey }) => hotkey)
.toString();
const handleSidebarToggleBtn = () => {
toggleSidebarExpend();
};
useHotkeys(
globalHotkeys,
(event, handle) => {
routes.map(({ path, hotkey }) => {
if (handle.key === hotkey) {
history.push(path);
}
});
},
[history],
);
useHotkeys('ctrl+/', (event, handle) => handleSidebarToggleBtn());
useHotkeys('shift+d', (event, handle) => openDialog('money-in', {}));
useHotkeys('shift+q', (event, handle) => openDialog('money-out', {}));
return <div></div>;
}
export default compose(withDashboardActions, withDialogActions)(GlobalHotkeys);

View File

@@ -0,0 +1,35 @@
// @ts-nocheck
import React from 'react';
import { Switch, Route } from 'react-router';
import Dashboard from '@/components/Dashboard/Dashboard';
import SetupWizardPage from '@/containers/Setup/WizardSetupPage';
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
import EnsureOrganizationIsNotReady from '../Guards/EnsureOrganizationIsNotReady';
import { PrivatePagesProvider } from './PrivatePagesProvider';
import '@/style/pages/Dashboard/Dashboard.scss';
/**
* Dashboard inner private pages.
*/
export default function DashboardPrivatePages() {
return (
<PrivatePagesProvider>
<Switch>
<Route path={'/setup'}>
<EnsureOrganizationIsNotReady>
<SetupWizardPage />
</EnsureOrganizationIsNotReady>
</Route>
<Route path="/">
<EnsureOrganizationIsReady>
<Dashboard />
</EnsureOrganizationIsReady>
</Route>
</Switch>
</PrivatePagesProvider>
);
}

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import { useApplicationBoot } from '@/components';
/**
* Private pages provider.
*/
export function PrivatePagesProvider({
// #ownProps
children,
}) {
const { isLoading } = useApplicationBoot();
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
}

View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import BigcapitalLoading from './BigcapitalLoading';
import withDashboard from '@/containers/Dashboard/withDashboard';
function SplashScreenComponent({ splashScreenLoading }) {
return splashScreenLoading ? <BigcapitalLoading /> : null;
}
export const SplashScreen = R.compose(
withDashboard(({ splashScreenLoading }) => ({
splashScreenLoading,
})),
)(SplashScreenComponent);

View File

@@ -0,0 +1,94 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Menu,
MenuItem,
MenuDivider,
Button,
Popover,
Position,
} from '@blueprintjs/core';
import { If, FormattedMessage as T } from '@/components';
import { firstLettersArgs } from '@/utils';
import { useAuthActions } from '@/hooks/state';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
import { useAuthenticatedAccount } from '@/hooks/query';
import { compose } from '@/utils';
/**
* Dashboard topbar user.
*/
function DashboardTopbarUser({
openDialog,
// #withSubscriptions
isSubscriptionActive,
}) {
const history = useHistory();
const { setLogout } = useAuthActions();
// Retrieve authenticated user information.
const { data: user } = useAuthenticatedAccount();
const onClickLogout = () => {
setLogout();
};
const onKeyboardShortcut = () => {
openDialog('keyboard-shortcuts');
};
return (
<Popover
content={
<Menu className={'menu--logged-user-dropdown'}>
<MenuItem
multiline={true}
className={'menu-item--profile'}
text={
<div>
<div class="person">
{user.first_name} {user.last_name}
</div>
<div class="org">
<T id="organization_id" />: {user.tenant_id}
</div>
</div>
}
/>
<MenuDivider />
<If condition={isSubscriptionActive}>
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
/>
<MenuItem
text={<T id={'preferences'} />}
onClick={() => history.push('/preferences')}
/>
</If>
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
</Menu>
}
position={Position.BOTTOM}
>
<Button>
<div className="user-text">
{firstLettersArgs(user.first_name, user.last_name)}
</div>
</Button>
</Popover>
);
}
export default compose(
withDialogActions,
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(DashboardTopbarUser);

View File

@@ -0,0 +1,14 @@
// @ts-nocheck
export * from './SplashScreen';
export * from './DashboardBoot';
export * from './DashboardThemeProvider';
export * from './DashboardAbilityProvider';
export * from './DashboardCard'
export * from './DashboardActionsBar'
export * from './DashboardFilterButton'
export * from './DashboardRowsHeightButton'
export * from './DashboardViewsTabs'
export * from './DashboardActionViewsList'
export * from './DashboardContentTable'
export * from './DashboardPageContent'
export * from './DashboardInsider'

View File

@@ -0,0 +1,77 @@
// @ts-nocheck
import React, { useRef, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { CellType } from '@/constants';
import { useCellAutoFocus } from '@/hooks';
import { AccountsSuggestField } from '@/components';
/**
* Account cell renderer.
*/
export default function AccountCellRenderer({
column: {
id,
accountsDataProp,
filterAccountsByRootTypes,
filterAccountsByTypes,
fieldProps,
formGroupProps,
},
row: { index, original },
cell: { value: initialValue },
payload: {
accounts: defaultAccounts,
updateData,
errors,
autoFocus,
...restPayloadProps
},
}) {
const accountRef = useRef();
useCellAutoFocus(accountRef, autoFocus, id, index);
const handleAccountSelected = useCallback(
(account) => {
updateData(index, id, account.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
const accounts = useMemo(
() => restPayloadProps[accountsDataProp] || defaultAccounts,
[restPayloadProps, defaultAccounts, accountsDataProp],
);
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--account',
Classes.FILL,
)}
{...formGroupProps}
>
<AccountsSuggestField
accounts={accounts}
onAccountSelected={handleAccountSelected}
selectedAccountId={initialValue}
filterByRootTypes={filterAccountsByRootTypes}
filterByTypes={filterAccountsByTypes}
inputProps={{
inputRef: (ref) => (accountRef.current = ref),
placeholder: intl.get('search'),
}}
openOnKeyDown={true}
blurOnSelectClose={false}
{...fieldProps}
/>
</FormGroup>
);
}
AccountCellRenderer.cellType = CellType.Field;

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import React from 'react';
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
import { CellType } from '@/constants';
import { BranchSuggestField } from '../Branches';
/**
* Branches list field cell.
* @returns
*/
export default function BranchesListFieldCell({
column: { id },
row: { index, original },
payload: { branches, updateData, errors },
}) {
const handleBranchSelected = React.useCallback(
(branch) => {
updateData(index, 'branch_id', branch.id);
},
[updateData, index],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--contacts-list',
Classes.FILL,
)}
>
<BranchSuggestField
branches={branches}
onBranchSelected={handleBranchSelected}
selectedBranchId={original?.branch_id}
/>
</FormGroup>
);
}
BranchesListFieldCell.cellType = CellType.Field;

View File

@@ -0,0 +1,52 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { get } from 'lodash';
import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core';
import { CellType } from '@/constants';
const CheckboxEditableCell = ({
row: { index, original },
column: { id, disabledAccessor, checkboxProps },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
const newValue = e.target.checked;
setValue(newValue);
payload.updateData(index, id, newValue);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
// Detarmines whether the checkbox is disabled.
const disabled = disabledAccessor ? get(original, disabledAccessor) : false;
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Checkbox
value={value}
onChange={onChange}
checked={initialValue}
disabled={disabled}
minimal={true}
className="ml2"
{...checkboxProps}
/>
</FormGroup>
);
};
CheckboxEditableCell.cellType = CellType.Field;
export default CheckboxEditableCell;

View File

@@ -0,0 +1,42 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
import { CellType } from '@/constants';
import { ContactsSuggestField } from '@/components';
export default function ContactsListCellRenderer({
column: { id },
row: { index, original },
cell: { value },
payload: { contacts, updateData, errors },
}) {
const handleContactSelected = useCallback(
(contact) => {
updateData(index, 'contact_id', contact.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--contacts-list',
Classes.FILL,
)}
>
<ContactsSuggestField
contactsList={contacts}
onContactSelected={handleContactSelected}
selectedContactId={original?.contact_id}
selectedContactType={original?.contact_type}
/>
</FormGroup>
);
}
ContactsListCellRenderer.cellType = CellType.Field;

View File

@@ -0,0 +1,21 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react';
export const DivFieldCell = ({ cell: { value: initialValue } }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <div>${value}</div>;
};
export const EmptyDiv = ({ cell: { value: initialValue } }) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <div>{value}</div>;
};

View File

@@ -0,0 +1,44 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Classes, InputGroup, FormGroup, Intent } from '@blueprintjs/core';
import { CellType } from '@/constants';
const InputEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<InputGroup
value={value}
onChange={onChange}
onBlur={onBlur}
fill={true}
/>
</FormGroup>
);
};
InputEditableCell.cellType = CellType.Field;
export default InputEditableCell;

View File

@@ -0,0 +1,60 @@
// @ts-nocheck
import React, { useCallback, useRef } from 'react';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { CellType } from '@/constants';
import { ItemsSuggestField } from '@/components';
import { useCellAutoFocus } from '@/hooks';
/**
* Items list cell.
*/
export default function ItemsListCell({
column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
row: { index },
cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus },
}) {
const fieldRef = useRef();
// Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index);
// Handle the item selected.
const handleItemSelected = useCallback(
(item) => {
updateData(index, id, item.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames('form-group--select-list', Classes.FILL)}
{...formGroupProps}
>
<ItemsSuggestField
items={items}
onItemSelected={handleItemSelected}
selectedItemId={initialValue}
sellable={filterSellable}
purchasable={filterPurchasable}
inputProps={{
inputRef: (ref) => (fieldRef.current = ref),
placeholder: intl.get('enter_an_item'),
}}
openOnKeyDown={true}
blurOnSelectClose={false}
{...fieldProps}
/>
</FormGroup>
);
}
ItemsListCell.cellType = CellType.Field;

View File

@@ -0,0 +1,56 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import { MoneyInputGroup } from '@/components';
import { CLASSES } from '@/constants/classes';
import { CellType } from '@/constants';
// Input form cell renderer.
const MoneyFieldCellRenderer = ({
row: { index, moneyInputGroupProps = {} },
column: { id },
cell: { value: initialValue },
payload: { errors, updateData },
}) => {
const [value, setValue] = useState(initialValue);
const handleFieldChange = useCallback((value) => {
setValue(value);
}, [setValue]);
function isNumeric(data) {
return (
!isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array
);
}
const handleFieldBlur = () => {
const updateValue = isNumeric(value) ? parseFloat(value) : value;
updateData(index, id, updateValue);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={CLASSES.FILL}>
<MoneyInputGroup
value={value}
// prefix={'$'}
onChange={handleFieldChange}
onBlur={handleFieldBlur}
{...moneyInputGroupProps}
/>
</FormGroup>
);
};
MoneyFieldCellRenderer.cellType = CellType.Field;
export default MoneyFieldCellRenderer;

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import { FormGroup, NumericInput, Intent } from '@blueprintjs/core';
import classNames from 'classnames';
import { CellType } from '@/constants';
import { CLASSES } from '@/constants/classes';
/**
* Numeric input table cell.
*/
export default function NumericInputCell({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) {
const [value, setValue] = useState(initialValue);
const handleValueChange = (newValue) => {
setValue(newValue);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(CLASSES.FILL)}
>
<NumericInput
value={value}
onValueChange={handleValueChange}
onBlur={onBlur}
fill={true}
buttonPosition={'none'}
/>
</FormGroup>
);
}
NumericInputCell.cellType = CellType.Field;

View File

@@ -0,0 +1,39 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import { PaymentReceiveListField } from '@/components';
import { CellType } from '@/constants';
function PaymentReceiveListFieldCell({
column: { id },
row: { index },
cell: { value: initialValue },
payload: { invoices, updateData, errors },
}) {
const handleInvoicesSelected = useCallback(
(_item) => {
updateData(index, id, _item.id);
},
[updateData, index, id],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames('form-group--selcet-list', Classes.FILL)}
>
<PaymentReceiveListField
invoices={invoices}
onInvoiceSelected={handleInvoicesSelected}
selectedInvoiceId={initialValue}
/>
</FormGroup>
);
}
PaymentReceiveListFieldCell.cellType = CellType.Field;
export default PaymentReceiveListFieldCell;

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import { MoneyInputGroup } from '@/components';
import { CellType } from '@/constants';
const PercentFieldCell = ({
cell: { value: initialValue },
row: { index },
column: { id },
payload: { errors, updateData },
}) => {
const [value, setValue] = useState(initialValue);
const handleBlurChange = (newValue) => {
const parsedValue = newValue === '' || newValue === undefined
? '' : parseInt(newValue, 10);
updateData(index, id, parsedValue);
};
const handleChange = useCallback((value) => {
setValue(value);
}, [setValue]);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = errors?.[index]?.[id];
return (
<FormGroup intent={error ? Intent.DANGER : null}>
<MoneyInputGroup
prefix={'%'}
value={value}
onChange={handleChange}
onBlurValue={handleBlurChange}
/>
</FormGroup>
);
};
PercentFieldCell.cellType = CellType.Field;
export default PercentFieldCell;

View File

@@ -0,0 +1,27 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { Popover2 } from '@blueprintjs/popover2';
import { Button } from '@blueprintjs/core';
import { CellType } from '@/constants';
import { Icon, FormattedMessage as T } from '@/components';
import ProjectBillableEntries from '@/containers/Projects/containers/ProjectBillableEntries';
/**
*
* @return
*/
export function ProjectBillableEntriesCell() {
const content = <ProjectBillableEntries />;
return (
<Popover2 content={content}>
<Button
icon={<Icon icon={'info'} iconSize={14} />}
className="m12"
minimal={true}
/>
</Popover2>
);
}
ProjectBillableEntriesCell.cellType = CellType.Button;

View File

@@ -0,0 +1,44 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
import classNames from 'classnames';
import { CellType } from '@/constants';
import { ProjectSuggestField } from '@/containers/Projects/components';
/**
* projects list field cell.
* @returns
*/
export function ProjectsListFieldCell({
column: { id },
row: { index, original },
payload: { projects, updateData, errors },
}) {
const handleProjectSelected = useCallback(
(project) => {
updateData(index, 'project_id', project.id);
},
[updateData, index],
);
const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(
'form-group--select-list',
'form-group--contacts-list',
Classes.FILL,
)}
>
<ProjectSuggestField
projects={projects}
onProjectSelected={handleProjectSelected}
selectedProjectId={original?.project_id}
/>
</FormGroup>
);
}
ProjectsListFieldCell.cellType = CellType.Field;

View File

@@ -0,0 +1,55 @@
// @ts-nocheck
import React from 'react';
import classNames from 'classnames';
import { Classes, Switch, FormGroup, Intent } from '@blueprintjs/core';
import { CellType } from '@/constants';
import { safeInvoke } from '@/utils';
/**
* Switch editable cell.
*/
const SwitchEditableCell = ({
row: { index, original },
column: { id, switchProps, onSwitchChange },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
// Handle the switch change.
const onChange = (e) => {
const newValue = e.target.checked;
setValue(newValue);
safeInvoke(payload.updateData, index, id, newValue);
safeInvoke(onSwitchChange, e, newValue, original);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Switch
value={value}
onChange={onChange}
checked={initialValue}
minimal={true}
className="ml2"
{...switchProps}
/>
</FormGroup>
);
};
SwitchEditableCell.cellType = CellType.Field;
export default SwitchEditableCell;

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Classes, TextArea, FormGroup, Intent } from '@blueprintjs/core';
import { CellType } from '@/constants';
const TextAreaEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<TextArea
growVertically={true}
large={true}
value={value}
onChange={onChange}
onBlur={onBlur}
fill={true}
/>
</FormGroup>
);
};
TextAreaEditableCell.cellType = CellType.Field;
export default TextAreaEditableCell;

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import React from 'react';
import { Tooltip, Position } from '@blueprintjs/core';
/**
* Text overview tooltip cell.
* @returns {JSX.Element}
*/
export function TextOverviewTooltipCell({ cell: { value } }) {
const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 },
offset: { offset: '0, 10' },
preventOverflow: { boundariesElement: 'viewport', padding: 40 },
};
return (
<Tooltip
content={value}
position={Position.BOTTOM_LEFT}
boundary={'viewport'}
minimal={true}
modifiers={SUBMENU_POPOVER_MODIFIERS}
targetClassName={'table-tooltip-overview-target'}
>
{value}
</Tooltip>
);
}

View File

@@ -0,0 +1,35 @@
// @ts-nocheck
import AccountsListFieldCell from './AccountsListFieldCell';
import MoneyFieldCell from './MoneyFieldCell';
import InputGroupCell from './InputGroupCell';
import ContactsListFieldCell from './ContactsListFieldCell';
import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
import NumericInputCell from './NumericInputCell';
import CheckBoxFieldCell from './CheckBoxFieldCell';
import SwitchFieldCell from './SwitchFieldCell';
import TextAreaCell from './TextAreaCell';
import BranchesListFieldCell from './BranchesListFieldCell';
import { ProjectsListFieldCell } from './ProjectsListFieldCell';
import { ProjectBillableEntriesCell } from './ProjectBillableEntriesCell';
import { TextOverviewTooltipCell } from './TextOverviewTooltipCell';
export {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
ContactsListFieldCell,
ItemsListCell,
PercentFieldCell,
DivFieldCell,
EmptyDiv,
NumericInputCell,
CheckBoxFieldCell,
SwitchFieldCell,
TextAreaCell,
BranchesListFieldCell,
ProjectsListFieldCell,
ProjectBillableEntriesCell,
TextOverviewTooltipCell,
};

View File

@@ -0,0 +1,30 @@
// @ts-nocheck
import React from 'react';
import { get } from 'lodash';
import { getForceWidth } from '@/utils';
export function CellForceWidth({
value,
column: { forceWidthAccess },
row: { original },
}) {
const forceWidthValue = forceWidthAccess
? get(original, forceWidthAccess)
: value;
return <ForceWidth forceValue={forceWidthValue}>{value}</ForceWidth>;
}
export function ForceWidth({ children, forceValue }) {
const forceWidthValue = forceValue || children;
return (
<span
className={'force-width'}
style={{ minWidth: getForceWidth(forceWidthValue) }}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,6 @@
// @ts-nocheck
import React from 'react';
export function CellTextSpan({ cell: { value } }) {
return (<span class="cell-text">{ value }</span>)
}

View File

@@ -0,0 +1,231 @@
// @ts-nocheck
import React, { useEffect, useRef } from 'react';
import {
useTable,
useExpanded,
useRowSelect,
usePagination,
useResizeColumns,
useSortBy,
useFlexLayout,
useAsyncDebounce,
} from 'react-table';
import { useSticky } from 'react-table-sticky';
import { useUpdateEffect } from '@/hooks';
import { saveInvoke } from '@/utils';
import '@/style/components/DataTable/DataTable.scss';
import TableNoResultsRow from './TableNoResultsRow';
import TableLoadingRow from './TableLoading';
import TableHeader from './TableHeader';
import TablePage from './TablePage';
import TableFooter from './TableFooter';
import TableRow from './TableRow';
import TableRows from './TableRows';
import TableCell from './TableCell';
import TableTBody from './TableTBody';
import TableContext from './TableContext';
import TablePagination from './TablePagination';
import TableWrapper from './TableWrapper';
import TableIndeterminateCheckboxRow from './TableIndeterminateCheckboxRow';
import TableIndeterminateCheckboxHeader from './TableIndeterminateCheckboxHeader';
import { useResizeObserver } from './utils';
/**
* Datatable component.
*/
export function DataTable(props) {
const {
columns,
data,
onFetchData,
onSelectedRowsChange,
manualSortBy = false,
manualPagination = true,
selectionColumn = false,
expandSubRows = true,
expanded = {},
rowClassNames,
payload,
expandable = false,
noInitialFetch = false,
pagesCount: controlledPageCount,
// Pagination props.
initialPageIndex = 0,
initialPageSize = 10,
updateDebounceTime = 200,
selectionColumnWidth = 42,
autoResetPage,
autoResetExpanded,
autoResetGroupBy,
autoResetSelectedRows,
autoResetSortBy,
autoResetFilters,
autoResetRowState,
// Components
TableHeaderRenderer,
TablePageRenderer,
TableWrapperRenderer,
TableTBodyRenderer,
TablePaginationRenderer,
TableFooterRenderer,
onColumnResizing,
initialColumnsWidths,
...restProps
} = props;
const selectionColumnObj = {
id: 'selection',
disableResizing: true,
minWidth: selectionColumnWidth,
width: selectionColumnWidth,
maxWidth: selectionColumnWidth,
skeletonWidthMin: 100,
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: TableIndeterminateCheckboxHeader,
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: TableIndeterminateCheckboxRow,
className: 'selection',
...(typeof selectionColumn === 'object' ? selectionColumn : {}),
};
const table = useTable(
{
columns,
data,
initialState: {
pageIndex: initialPageIndex,
pageSize: initialPageSize,
expanded,
columnResizing: {
columnWidths: initialColumnsWidths || {},
},
},
manualPagination,
pageCount: controlledPageCount,
getSubRows: (row) => row.children,
manualSortBy,
expandSubRows,
payload,
autoResetPage,
autoResetExpanded,
autoResetGroupBy,
autoResetSelectedRows,
autoResetSortBy,
autoResetFilters,
autoResetRowState,
...restProps,
},
useSortBy,
useExpanded,
useResizeColumns,
useFlexLayout,
useSticky,
usePagination,
useRowSelect,
(hooks) => {
hooks.visibleColumns.push((columns) => [
// Let's make a column for selection
...(selectionColumn ? [selectionColumnObj] : []),
...columns,
]);
},
);
const {
selectedFlatRows,
state: { pageIndex, pageSize, sortBy, selectedRowIds },
} = table;
const isInitialMount = useRef(noInitialFetch);
const onFetchDataDebounced = useAsyncDebounce((...args) => {
saveInvoke(onFetchData, ...args);
}, updateDebounceTime);
// When these table states change, fetch new data!
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
onFetchDataDebounced({ pageIndex, pageSize, sortBy });
}
}, [pageIndex, pageSize, sortBy, onFetchDataDebounced]);
useUpdateEffect(() => {
saveInvoke(onSelectedRowsChange, selectedFlatRows);
}, [selectedRowIds, onSelectedRowsChange]);
// Column resizing observer.
useResizeObserver(table.state, (current, columnWidth, columnsResizing) => {
onColumnResizing && onColumnResizing(current, columnWidth, columnsResizing);
});
return (
<TableContext.Provider value={{ table, props }}>
<TableWrapperRenderer>
<TableHeaderRenderer />
<TableTBodyRenderer>
<TablePageRenderer />
</TableTBodyRenderer>
<TableFooterRenderer />
</TableWrapperRenderer>
<TablePaginationRenderer />
</TableContext.Provider>
);
}
DataTable.defaultProps = {
pagination: false,
hidePaginationNoPages: true,
hideTableHeader: false,
size: null,
spinnerProps: { size: 30 },
expandToggleColumn: 1,
expandColumnSpace: 0.8,
autoResetPage: true,
autoResetExpanded: true,
autoResetGroupBy: true,
autoResetSelectedRows: true,
autoResetSortBy: true,
autoResetFilters: true,
autoResetRowState: true,
TableHeaderRenderer: TableHeader,
TableFooterRenderer: TableFooter,
TableLoadingRenderer: TableLoadingRow,
TablePageRenderer: TablePage,
TableRowsRenderer: TableRows,
TableRowRenderer: TableRow,
TableCellRenderer: TableCell,
TableWrapperRenderer: TableWrapper,
TableTBodyRenderer: TableTBody,
TablePaginationRenderer: TablePagination,
TableNoResultsRowRenderer: TableNoResultsRow,
noResults: '',
payload: {},
};

View File

@@ -0,0 +1,116 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { DataTable } from './DataTable';
/**
* Editable datatable.
*/
export function DataTableEditable({
totalRow = false,
actions,
name,
...tableProps
}) {
return (
<DatatableEditableRoot>
<DataTable {...tableProps} />
</DatatableEditableRoot>
);
}
const DatatableEditableRoot = styled.div`
.bp3-form-group {
margin-bottom: 0;
}
.table {
border: 1px solid #d2dce2;
border-radius: 5px;
background-color: #fff;
.th,
.td {
border-left: 1px solid #e2e2e2;
&:first-of-type {
border-left: 0;
}
}
.thead {
.tr .th {
padding: 9px 14px;
background-color: #f2f3fb;
font-size: 13px;
color: #415060;
border-bottom: 1px solid #d2dce2;
&,
.inner-resizer {
border-left-color: transparent;
}
}
}
.tbody {
.tr .td {
border-bottom: 0;
border-bottom: 1px solid #d8d8d8;
min-height: 38px;
padding: 4px 14px;
&.td-field-type,
&.td-button-type {
padding: 2px;
}
}
.tr:last-of-type .td {
border-bottom: 0;
}
.tr {
&:hover .td,
.bp3-input {
background-color: transparent;
}
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
.form-group--select-list .bp3-button {
border-color: #ffffff;
color: #222;
border-radius: 3px;
text-align: inherit;
}
.bp3-form-group:not(.bp3-intent-danger) .bp3-input {
border-radius: 2px;
padding-left: 14px;
padding-right: 14px;
&:focus {
box-shadow: 0 0 0 2px #116cd0;
}
}
.form-group--select-list .bp3-button {
padding-left: 6px;
padding-right: 6px;
&:after {
display: none;
}
}
.form-group--select-list,
.bp3-form-group {
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal),
.bp3-input {
border-color: #f7b6b6;
}
}
}
.td.actions {
.bp3-button {
color: #80858f;
}
}
}
}
}
`;

Some files were not shown because too many files have changed in this diff Show More