mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
chrone: sperate client and server to different repos.
This commit is contained in:
30
src/components/AccountsMultiSelect.js
Normal file
30
src/components/AccountsMultiSelect.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MultiSelect } from './MultiSelectTaggable';
|
||||
|
||||
export default 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
129
src/components/AccountsSelectList.js
Normal file
129
src/components/AccountsSelectList.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import { MenuItemNestedText, FormattedMessage as T } from 'components';
|
||||
import classNames from 'classnames';
|
||||
import { filterAccountsByQuery } from './utils';
|
||||
import { nestedArrayToflatten } from 'utils';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
export default function AccountsSelectList({
|
||||
accounts,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
defaultSelectText = 'Select account',
|
||||
onAccountSelected,
|
||||
disabled = false,
|
||||
popoverFill = false,
|
||||
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
filterByRootTypes,
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
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 onAccountSelect = useCallback(
|
||||
(account) => {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
},
|
||||
[setSelectedAccount, onAccountSelected],
|
||||
);
|
||||
|
||||
// Filters accounts items.
|
||||
const filterAccountsPredicater = useCallback(
|
||||
(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
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
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={onAccountSelect}
|
||||
disabled={disabled}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={selectedAccount ? selectedAccount.name : defaultSelectText}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
135
src/components/AccountsSuggestField.js
Normal file
135
src/components/AccountsSuggestField.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
import { MenuItemNestedText, FormattedMessage as T } from 'components';
|
||||
import { filterAccountsByQuery } from './utils';
|
||||
import { nestedArrayToflatten } from 'utils';
|
||||
|
||||
/**
|
||||
* Accounts suggest field.
|
||||
*/
|
||||
export default function AccountsSuggestField({
|
||||
accounts,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
|
||||
popoverFill = false,
|
||||
onAccountSelected,
|
||||
|
||||
filterByParentTypes = [],
|
||||
filterByTypes = [],
|
||||
filterByNormal,
|
||||
filterByRootTypes = [],
|
||||
|
||||
...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]);
|
||||
|
||||
// Filters accounts items.
|
||||
const filterAccountsPredicater = useCallback(
|
||||
(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
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 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 handleInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.name.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const onAccountSelect = useCallback(
|
||||
(account) => {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
},
|
||||
[setSelectedAccount, onAccountSelected],
|
||||
);
|
||||
|
||||
return (
|
||||
<Suggest
|
||||
items={filteredAccounts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccountsPredicater}
|
||||
onItemSelect={onAccountSelect}
|
||||
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,
|
||||
})}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
src/components/AccountsTypesSelect.js
Normal file
48
src/components/AccountsTypesSelect.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ListSelect } from 'components';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
export default 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/components/AdvancedFilter/AdvancedFilter.schema.js
Normal file
13
src/components/AdvancedFilter/AdvancedFilter.schema.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import ListSelect from '../ListSelect';
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
397
src/components/AdvancedFilter/AdvancedFilterDropdown.js
Normal file
397
src/components/AdvancedFilter/AdvancedFilterDropdown.js
Normal file
@@ -0,0 +1,397 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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,
|
||||
};
|
||||
32
src/components/AdvancedFilter/AdvancedFilterPopover.js
Normal file
32
src/components/AdvancedFilter/AdvancedFilterPopover.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
131
src/components/AdvancedFilter/AdvancedFilterValueField.js
Normal file
131
src/components/AdvancedFilter/AdvancedFilterValueField.js
Normal file
@@ -0,0 +1,131 @@
|
||||
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 { 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={`Select an 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={'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('value')}
|
||||
onChange={handleInputChange}
|
||||
value={localValue}
|
||||
inputRef={valueRef}
|
||||
/>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
22
src/components/AdvancedFilter/components.js
Normal file
22
src/components/AdvancedFilter/components.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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]);
|
||||
}
|
||||
111
src/components/AdvancedFilter/interfaces.ts
Normal file
111
src/components/AdvancedFilter/interfaces.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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;
|
||||
}
|
||||
112
src/components/AdvancedFilter/utils.js
Normal file
112
src/components/AdvancedFilter/utils.js
Normal file
@@ -0,0 +1,112 @@
|
||||
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)
|
||||
);
|
||||
};
|
||||
17
src/components/Alert/index.js
Normal file
17
src/components/Alert/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import clsx from 'classnames';
|
||||
|
||||
import Style from './style.module.scss';
|
||||
|
||||
export function Alert({ title, description, intent }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(Style.root, {
|
||||
[`${Style['root_' + intent]}`]: intent,
|
||||
})}
|
||||
>
|
||||
{title && <h3 className={clsx(Style.title)}>{title}</h3>}
|
||||
{description && <p class={clsx(Style.description)}>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/Alert/style.module.scss
Normal file
32
src/components/Alert/style.module.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
.root {
|
||||
border: 1px solid rgb(223, 227, 230);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&_danger {
|
||||
border-color: rgb(249, 198, 198);
|
||||
background: rgb(255, 248, 248);
|
||||
|
||||
.description {
|
||||
color: #d95759;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: rgb(205, 43, 49);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
color: rgb(17, 24, 28);
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: rgb(104, 112, 118);
|
||||
margin: 0;
|
||||
}
|
||||
51
src/components/App.js
Normal file
51
src/components/App.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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 } from '../components';
|
||||
import { queryConfig } from '../hooks/query/base'
|
||||
|
||||
/**
|
||||
* Core application.
|
||||
*/
|
||||
export default function App() {
|
||||
// Browser history.
|
||||
const history = createBrowserHistory();
|
||||
|
||||
// Query client.
|
||||
const queryClient = new QueryClient(queryConfig);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SplashScreen />
|
||||
|
||||
<AppIntlLoader>
|
||||
<div className="App">
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'} component={Authentication} />
|
||||
<Route path={'/'}>
|
||||
<PrivateRoute component={DashboardPrivatePages} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
<GlobalErrors />
|
||||
</div>
|
||||
</AppIntlLoader>
|
||||
|
||||
<ReactQueryDevtools initialIsOpen />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
123
src/components/AppIntlLoader.js
Normal file
123
src/components/AppIntlLoader.js
Normal file
@@ -0,0 +1,123 @@
|
||||
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 withDashboardActions from '../containers/Dashboard/withDashboardActions';
|
||||
import withDashboard from '../containers/Dashboard/withDashboard';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application Intl loader.
|
||||
*/
|
||||
function AppIntlLoader({ appIntlIsLoading, setAppIntlIsLoading, children }) {
|
||||
const [isLocalsLoading, setIsLocalsLoading] = React.useState(true);
|
||||
const [isYupLoading, setIsYupLoading] = React.useState(true);
|
||||
|
||||
// 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);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Lodas the locales data file.
|
||||
loadLocales(currentLocale)
|
||||
.then((results) => {
|
||||
return intl.init({
|
||||
currentLocale,
|
||||
locales: {
|
||||
[currentLocale]: results,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
moment.locale(transformMomentLocale(currentLocale));
|
||||
setIsLocalsLoading(false);
|
||||
});
|
||||
}, [currentLocale, setIsLocalsLoading]);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadYupLocales(currentLocale)
|
||||
.then(({ locale }) => {
|
||||
setLocale(locale);
|
||||
setIsYupLoading(false);
|
||||
})
|
||||
.then(() => {});
|
||||
}, [currentLocale]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLocalsLoading && !isYupLoading) {
|
||||
setAppIntlIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
|
||||
{appIntlIsLoading ? null : children}
|
||||
</AppIntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withDashboardActions,
|
||||
withDashboard(({ appIntlIsLoading }) => ({ appIntlIsLoading })),
|
||||
)(AppIntlLoader);
|
||||
24
src/components/AppIntlProvider.js
Normal file
24
src/components/AppIntlProvider.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const AppIntlContext = createContext();
|
||||
|
||||
/**
|
||||
* Application intl provider.
|
||||
*/
|
||||
function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
const provider = {
|
||||
currentLocale,
|
||||
isRTL,
|
||||
isLTR: !isRTL,
|
||||
};
|
||||
|
||||
return (
|
||||
<AppIntlContext.Provider value={provider}>
|
||||
{children}
|
||||
</AppIntlContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useAppIntlContext = () => React.useContext(AppIntlContext);
|
||||
|
||||
export { AppIntlProvider, useAppIntlContext };
|
||||
8
src/components/AppToaster.js
Normal file
8
src/components/AppToaster.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Position, Toaster, Intent } from "@blueprintjs/core";
|
||||
|
||||
const AppToaster = Toaster.create({
|
||||
position: Position.RIGHT_BOTTOM,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
|
||||
export default AppToaster;
|
||||
64
src/components/Authentication.js
Normal file
64
src/components/Authentication.js
Normal file
@@ -0,0 +1,64 @@
|
||||
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 { FormattedMessage as T } from 'components';
|
||||
import Icon from 'components/Icon';
|
||||
import { useIsAuthenticated } from 'hooks/state';
|
||||
import {AuthenticationBoot} from '../containers/Authentication/AuthenticationBoot';
|
||||
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">
|
||||
<AuthenticationBoot />
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
6
src/components/Card.js
Normal file
6
src/components/Card.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function Card({ className, children }) {
|
||||
return <div className={classNames('card', className)}>{children}</div>;
|
||||
}
|
||||
59
src/components/CategoriesSelectList.js
Normal file
59
src/components/CategoriesSelectList.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { ListSelect } from 'components';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { saveInvoke } from 'utils';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
export default function CategoriesSelectList({
|
||||
categories,
|
||||
selecetedCategoryId,
|
||||
defaultSelectText = <T id={'select_category'} />,
|
||||
onCategorySelected,
|
||||
popoverFill = false,
|
||||
className,
|
||||
...restProps
|
||||
}) {
|
||||
|
||||
// Filter Items Category
|
||||
const filterItemCategory = (query, item, _index, exactMatch) => {
|
||||
const normalizedTitle = item.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${item.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemCategorySelected = useCallback(
|
||||
(ItemCategory) => saveInvoke(onCategorySelected, ItemCategory),
|
||||
[onCategorySelected],
|
||||
);
|
||||
|
||||
const categoryItem = useCallback(
|
||||
(item, { handleClick }) => (
|
||||
<MenuItem key={item.id} text={item.name} onClick={handleClick} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<ListSelect
|
||||
items={categories}
|
||||
selectedItemProp={'id'}
|
||||
selectedItem={selecetedCategoryId}
|
||||
textProp={'name'}
|
||||
defaultText={defaultSelectText}
|
||||
onItemSelect={handleItemCategorySelected}
|
||||
itemPredicate={filterItemCategory}
|
||||
itemRenderer={categoryItem}
|
||||
popoverProps={{ minimal: true, usePortal: !popoverFill }}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
}, className)}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
23
src/components/CloudLoadingIndicator.js
Normal file
23
src/components/CloudLoadingIndicator.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import If from './Utils/If';
|
||||
|
||||
export default function CloudLoadingIndicator({
|
||||
isLoading,
|
||||
children,
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
CLASSES.CLOUD_SPINNER,
|
||||
{ [CLASSES.IS_LOADING]: isLoading },
|
||||
)}>
|
||||
<If condition={isLoading}>
|
||||
<Spinner size={30} value={null} />
|
||||
</If>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
107
src/components/ContactSelecetList.js
Normal file
107
src/components/ContactSelecetList.js
Normal file
@@ -0,0 +1,107 @@
|
||||
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 'common/classes';
|
||||
|
||||
export default function ContactSelecetList({
|
||||
contactsList,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
selectedContactType,
|
||||
defaultSelectText = <T id={'select_contact'} />,
|
||||
onContactSelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
buttonProps,
|
||||
}) {
|
||||
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 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 (
|
||||
<Select
|
||||
items={contacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={handleContactRenderer}
|
||||
itemPredicate={filterContacts}
|
||||
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_')
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={
|
||||
selecetedContact ? selecetedContact.display_name : defaultSelectText
|
||||
}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
42
src/components/ContactsMultiSelect.js
Normal file
42
src/components/ContactsMultiSelect.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MultiSelect } from '../components/MultiSelectTaggable';
|
||||
|
||||
/**
|
||||
* Contacts multi-select component.
|
||||
*/
|
||||
export default 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
107
src/components/ContactsSuggestField.js
Normal file
107
src/components/ContactsSuggestField.js
Normal file
@@ -0,0 +1,107 @@
|
||||
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 'common/classes';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export default 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
src/components/ContextMenu.js
Normal file
52
src/components/ContextMenu.js
Normal file
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
56
src/components/CurrenciesSelectList.js
Normal file
56
src/components/CurrenciesSelectList.js
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {
|
||||
MenuItem,
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import {
|
||||
Select
|
||||
} from '@blueprintjs/select';
|
||||
|
||||
export default 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>
|
||||
);
|
||||
}
|
||||
82
src/components/CurrencySelectList.js
Normal file
82
src/components/CurrencySelectList.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import classNames from 'classnames';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
|
||||
export default 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>
|
||||
);
|
||||
}
|
||||
0
src/components/CustomFields.js
Normal file
0
src/components/CustomFields.js
Normal file
28
src/components/Dashboard/AuthenticatedUser.js
Normal file
28
src/components/Dashboard/AuthenticatedUser.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useUser } from 'hooks/query';
|
||||
import withAuthentication from '../../containers/Authentication/withAuthentication';
|
||||
|
||||
const AuthenticatedUserContext = React.createContext();
|
||||
|
||||
function AuthenticatedUserComponent({ authenticatedUserId, children }) {
|
||||
const { data: user, ...restProps } = useUser(authenticatedUserId);
|
||||
|
||||
return (
|
||||
<AuthenticatedUserContext.Provider
|
||||
value={{
|
||||
user,
|
||||
...restProps,
|
||||
}}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const AuthenticatedUser = withAuthentication(
|
||||
({ authenticatedUserId }) => ({
|
||||
authenticatedUserId,
|
||||
}),
|
||||
)(AuthenticatedUserComponent);
|
||||
|
||||
export const useAuthenticatedUser = () =>
|
||||
React.useContext(AuthenticatedUserContext);
|
||||
18
src/components/Dashboard/BigcapitalLoading.js
Normal file
18
src/components/Dashboard/BigcapitalLoading.js
Normal file
@@ -0,0 +1,18 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
60
src/components/Dashboard/Dashboard.js
Normal file
60
src/components/Dashboard/Dashboard.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
|
||||
import 'style/pages/Dashboard/Dashboard.scss';
|
||||
|
||||
import Sidebar from 'components/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 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 />
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
68
src/components/Dashboard/DashboardActionViewsList.js
Normal file
68
src/components/Dashboard/DashboardActionViewsList.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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 default 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>
|
||||
);
|
||||
}
|
||||
16
src/components/Dashboard/DashboardActionsBar.js
Normal file
16
src/components/Dashboard/DashboardActionsBar.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Navbar } from '@blueprintjs/core';
|
||||
|
||||
export default function DashboardActionsBar({ children, name }) {
|
||||
return (
|
||||
<div
|
||||
className={classnames({
|
||||
'dashboard__actions-bar': true,
|
||||
[`dashboard__actions-bar--${name}`]: !!name
|
||||
})}
|
||||
>
|
||||
<Navbar className='navbar--dashboard-actions-bar'>{children}</Navbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/components/Dashboard/DashboardBackLink.js
Normal file
39
src/components/Dashboard/DashboardBackLink.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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);
|
||||
76
src/components/Dashboard/DashboardBoot.js
Normal file
76
src/components/Dashboard/DashboardBoot.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { useUser, useCurrentOrganization } from 'hooks/query';
|
||||
import withAuthentication from '../../containers/Authentication/withAuthentication';
|
||||
import withDashboardActions from '../../containers/Dashboard/withDashboardActions';
|
||||
|
||||
import { setCookie, getCookie } from '../../utils';
|
||||
|
||||
/**
|
||||
* Dashboard async booting.
|
||||
*/
|
||||
function DashboardBootJSX({ setAppIsLoading, authenticatedUserId }) {
|
||||
// Fetches the current user's organization.
|
||||
const { isSuccess: isCurrentOrganizationSuccess, data: organization } =
|
||||
useCurrentOrganization();
|
||||
|
||||
// Authenticated user.
|
||||
const { isSuccess: isAuthUserSuccess, data: authUser } =
|
||||
useUser(authenticatedUserId);
|
||||
|
||||
// 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]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Once the all requests complete change the app loading state.
|
||||
if (
|
||||
isAuthUserSuccess &&
|
||||
isCurrentOrganizationSuccess &&
|
||||
localeCookie === organization?.metadata?.language
|
||||
) {
|
||||
setAppIsLoading(false);
|
||||
isBooted.current = true;
|
||||
}
|
||||
}, [
|
||||
isAuthUserSuccess,
|
||||
isCurrentOrganizationSuccess,
|
||||
organization,
|
||||
setAppIsLoading,
|
||||
localeCookie,
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
export const DashboardBoot = R.compose(
|
||||
withAuthentication(({ authenticatedUserId }) => ({
|
||||
authenticatedUserId,
|
||||
})),
|
||||
withDashboardActions,
|
||||
)(DashboardBootJSX);
|
||||
34
src/components/Dashboard/DashboardBreadcrumbs.js
Normal file
34
src/components/Dashboard/DashboardBreadcrumbs.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
17
src/components/Dashboard/DashboardCard.js
Normal file
17
src/components/Dashboard/DashboardCard.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
// Dashboard card.
|
||||
export default function DashboardCard({ children, page }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(CLASSES.DASHBOARD_CARD, {
|
||||
[CLASSES.DASHBOARD_CARD_PAGE]: page,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/Dashboard/DashboardContent.js
Normal file
18
src/components/Dashboard/DashboardContent.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
|
||||
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
|
||||
import DashboardFooter from 'components/Dashboard/DashboardFooter';
|
||||
import DashboardErrorBoundary from './DashboardErrorBoundary';
|
||||
|
||||
export default React.forwardRef(({}, ref) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
|
||||
<div className="dashboard-content" id="dashboard" ref={ref}>
|
||||
<DashboardTopbar />
|
||||
<DashboardContentRoutes />
|
||||
<DashboardFooter />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
59
src/components/Dashboard/DashboardContentRoute.js
Normal file
59
src/components/Dashboard/DashboardContentRoute.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
10
src/components/Dashboard/DashboardContentTable.js
Normal file
10
src/components/Dashboard/DashboardContentTable.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
/**
|
||||
* Dashboard content table.
|
||||
*/
|
||||
export default function DashboardContentTable({ children }) {
|
||||
return (<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>{ children }</div>)
|
||||
}
|
||||
12
src/components/Dashboard/DashboardErrorBoundary.js
Normal file
12
src/components/Dashboard/DashboardErrorBoundary.js
Normal file
@@ -0,0 +1,12 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
26
src/components/Dashboard/DashboardFilterButton.js
Normal file
26
src/components/Dashboard/DashboardFilterButton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
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} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
25
src/components/Dashboard/DashboardFooter.js
Normal file
25
src/components/Dashboard/DashboardFooter.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { getFooterLinks } from 'config/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>
|
||||
);
|
||||
}
|
||||
23
src/components/Dashboard/DashboardInsider.js
Normal file
23
src/components/Dashboard/DashboardInsider.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
export default 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>
|
||||
);
|
||||
}
|
||||
24
src/components/Dashboard/DashboardLoadingIndicator.js
Normal file
24
src/components/Dashboard/DashboardLoadingIndicator.js
Normal file
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
106
src/components/Dashboard/DashboardPage.js
Normal file
106
src/components/Dashboard/DashboardPage.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, Suspense } from 'react';
|
||||
// import { isUndefined } from 'lodash';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import { compose } from 'utils';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
// import withUniversalSearch from '../../containers/UniversalSearch/withUniversalSearch';
|
||||
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);
|
||||
12
src/components/Dashboard/DashboardPageContent.js
Normal file
12
src/components/Dashboard/DashboardPageContent.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Dashboard page content.
|
||||
*/
|
||||
export default function DashboardPageContent({ children }) {
|
||||
return (
|
||||
<div class="dashboard__page-content">
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
8
src/components/Dashboard/DashboardProvider.js
Normal file
8
src/components/Dashboard/DashboardProvider.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Dashboard provider.
|
||||
*/
|
||||
export default function DashboardProvider({ children }) {
|
||||
return children;
|
||||
}
|
||||
36
src/components/Dashboard/DashboardRowsHeightButton.js
Normal file
36
src/components/Dashboard/DashboardRowsHeightButton.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
PopoverInteractionKind,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Classes
|
||||
} from '@blueprintjs/core';
|
||||
import { Icon } from 'components';
|
||||
|
||||
export function DashboardRowsHeightButton() {
|
||||
return (
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={
|
||||
<Menu>
|
||||
<MenuDivider title={'Rows height'} />
|
||||
<MenuItem text="Compact" />
|
||||
<MenuItem text="Medium" />
|
||||
</Menu>
|
||||
}
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="rows-height" iconSize={16} />}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
44
src/components/Dashboard/DashboardSplitePane.js
Normal file
44
src/components/Dashboard/DashboardSplitePane.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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 = 180;
|
||||
|
||||
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);
|
||||
197
src/components/Dashboard/DashboardTopbar.js
Normal file
197
src/components/Dashboard/DashboardTopbar.js
Normal file
@@ -0,0 +1,197 @@
|
||||
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';
|
||||
|
||||
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}>
|
||||
<Button
|
||||
onClick={() => openGlobalSearch(true)}
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'search-24'} iconSize={20} />}
|
||||
text={<T id={'quick_find'} />}
|
||||
/>
|
||||
<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);
|
||||
103
src/components/Dashboard/DashboardViewsTabs.js
Normal file
103
src/components/Dashboard/DashboardViewsTabs.js
Normal file
@@ -0,0 +1,103 @@
|
||||
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 default 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,
|
||||
};
|
||||
38
src/components/Dashboard/GlobalHotkeys.js
Normal file
38
src/components/Dashboard/GlobalHotkeys.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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 { compose } from 'utils';
|
||||
|
||||
function GlobalHotkeys({
|
||||
// #withDashboardActions
|
||||
toggleSidebarExpend,
|
||||
}) {
|
||||
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());
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default compose(withDashboardActions)(GlobalHotkeys);
|
||||
37
src/components/Dashboard/PrivatePages.js
Normal file
37
src/components/Dashboard/PrivatePages.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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 'components/Guards/EnsureOrganizationIsReady';
|
||||
import EnsureOrganizationIsNotReady from 'components/Guards/EnsureOrganizationIsNotReady';
|
||||
import { PrivatePagesProvider } from './PrivatePagesProvider';
|
||||
import { DashboardBoot } from '../../components';
|
||||
|
||||
import 'style/pages/Dashboard/Dashboard.scss';
|
||||
|
||||
/**
|
||||
* Dashboard inner private pages.
|
||||
*/
|
||||
export default function DashboardPrivatePages() {
|
||||
return (
|
||||
<PrivatePagesProvider>
|
||||
<DashboardBoot />
|
||||
|
||||
<Switch>
|
||||
<Route path={'/setup'}>
|
||||
<EnsureOrganizationIsNotReady>
|
||||
<SetupWizardPage />
|
||||
</EnsureOrganizationIsNotReady>
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<EnsureOrganizationIsReady>
|
||||
<Dashboard />
|
||||
</EnsureOrganizationIsReady>
|
||||
</Route>
|
||||
</Switch>
|
||||
</PrivatePagesProvider>
|
||||
);
|
||||
}
|
||||
9
src/components/Dashboard/PrivatePagesProvider.js
Normal file
9
src/components/Dashboard/PrivatePagesProvider.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { AuthenticatedUser } from './AuthenticatedUser';
|
||||
|
||||
/**
|
||||
* Private pages provider.
|
||||
*/
|
||||
export function PrivatePagesProvider({ children }) {
|
||||
return <AuthenticatedUser>{children}</AuthenticatedUser>;
|
||||
}
|
||||
15
src/components/Dashboard/SplashScreen.js
Normal file
15
src/components/Dashboard/SplashScreen.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import BigcapitalLoading from './BigcapitalLoading';
|
||||
import withDashboard from '../../containers/Dashboard/withDashboard';
|
||||
|
||||
function SplashScreenComponent({ appIsLoading, appIntlIsLoading }) {
|
||||
return appIsLoading || appIntlIsLoading ? <BigcapitalLoading /> : null;
|
||||
}
|
||||
|
||||
export const SplashScreen = R.compose(
|
||||
withDashboard(({ appIsLoading, appIntlIsLoading }) => ({
|
||||
appIsLoading,
|
||||
appIntlIsLoading,
|
||||
})),
|
||||
)(SplashScreenComponent);
|
||||
89
src/components/Dashboard/TopbarUser.js
Normal file
89
src/components/Dashboard/TopbarUser.js
Normal file
@@ -0,0 +1,89 @@
|
||||
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 { compose } from 'utils';
|
||||
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
|
||||
import { useAuthenticatedUser } from './AuthenticatedUser';
|
||||
|
||||
function DashboardTopbarUser({
|
||||
openDialog,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { setLogout } = useAuthActions();
|
||||
|
||||
// Retrieve authenticated user information.
|
||||
const { user } = useAuthenticatedUser();
|
||||
|
||||
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);
|
||||
4
src/components/Dashboard/index.js
Normal file
4
src/components/Dashboard/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
|
||||
export * from './SplashScreen';
|
||||
export * from './DashboardBoot';
|
||||
226
src/components/DataTable.js
Normal file
226
src/components/DataTable.js
Normal file
@@ -0,0 +1,226 @@
|
||||
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 './Datatable/TableNoResultsRow';
|
||||
import TableLoadingRow from './Datatable/TableLoading';
|
||||
import TableHeader from './Datatable/TableHeader';
|
||||
import TablePage from './Datatable/TablePage';
|
||||
import TableFooter from './Datatable/TableFooter';
|
||||
import TableRow from './Datatable/TableRow';
|
||||
import TableRows from './Datatable/TableRows';
|
||||
import TableCell from './Datatable/TableCell';
|
||||
import TableTBody from './Datatable/TableTBody';
|
||||
import TableContext from './Datatable/TableContext';
|
||||
import TablePagination from './Datatable/TablePagination';
|
||||
import TableWrapper from './Datatable/TableWrapper';
|
||||
|
||||
import TableIndeterminateCheckboxRow from './Datatable/TableIndeterminateCheckboxRow';
|
||||
import TableIndeterminateCheckboxHeader from './Datatable/TableIndeterminateCheckboxHeader';
|
||||
|
||||
import { useResizeObserver } from './Datatable/utils';
|
||||
|
||||
/**
|
||||
* Datatable component.
|
||||
*/
|
||||
export default 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,
|
||||
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: {},
|
||||
};
|
||||
72
src/components/DataTableCells/AccountsListFieldCell.js
Normal file
72
src/components/DataTableCells/AccountsListFieldCell.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { useRef, useCallback, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useCellAutoFocus } from 'hooks';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import AccountsSuggestField from 'components/AccountsSuggestField';
|
||||
|
||||
// import AccountsSelectList from 'components/AccountsSelectList';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Account cell renderer.
|
||||
*/
|
||||
export default function AccountCellRenderer({
|
||||
column: {
|
||||
id,
|
||||
accountsDataProp,
|
||||
filterAccountsByRootTypes,
|
||||
filterAccountsByTypes,
|
||||
},
|
||||
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,
|
||||
)}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
48
src/components/DataTableCells/CheckBoxFieldCell.js
Normal file
48
src/components/DataTableCells/CheckBoxFieldCell.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxEditableCell;
|
||||
39
src/components/DataTableCells/ContactsListFieldCell.js
Normal file
39
src/components/DataTableCells/ContactsListFieldCell.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { ContactSelecetList } from 'components';
|
||||
import ContactsSuggestField from 'components/ContactsSuggestField';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
20
src/components/DataTableCells/DivFieldCell.js
Normal file
20
src/components/DataTableCells/DivFieldCell.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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>;
|
||||
};
|
||||
40
src/components/DataTableCells/InputGroupCell.js
Normal file
40
src/components/DataTableCells/InputGroupCell.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Classes, InputGroup, FormGroup, Intent } from '@blueprintjs/core';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputEditableCell;
|
||||
51
src/components/DataTableCells/ItemsListCell.js
Normal file
51
src/components/DataTableCells/ItemsListCell.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
// import ItemsListField from 'components/ItemsListField';
|
||||
import ItemsSuggestField from 'components/ItemsSuggestField';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { useCellAutoFocus } from 'hooks';
|
||||
|
||||
export default function ItemsListCell({
|
||||
column: { id, filterSellable, filterPurchasable },
|
||||
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);
|
||||
|
||||
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)}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
51
src/components/DataTableCells/MoneyFieldCell.js
Normal file
51
src/components/DataTableCells/MoneyFieldCell.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { MoneyInputGroup } from 'components';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoneyFieldCellRenderer;
|
||||
43
src/components/DataTableCells/NumericInputCell.js
Normal file
43
src/components/DataTableCells/NumericInputCell.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FormGroup, NumericInput, Intent } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/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>
|
||||
);
|
||||
}
|
||||
35
src/components/DataTableCells/PaymentReceiveListFieldCell.js
Normal file
35
src/components/DataTableCells/PaymentReceiveListFieldCell.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PaymentReceiveListField from 'components/PaymentReceiveListField';
|
||||
import classNames from 'classnames';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default PaymentReceiveListFieldCell;
|
||||
41
src/components/DataTableCells/PercentFieldCell.js
Normal file
41
src/components/DataTableCells/PercentFieldCell.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { MoneyInputGroup } from 'components';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default PercentFieldCell;
|
||||
22
src/components/DataTableCells/index.js
Normal file
22
src/components/DataTableCells/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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'
|
||||
|
||||
export {
|
||||
AccountsListFieldCell,
|
||||
MoneyFieldCell,
|
||||
InputGroupCell,
|
||||
ContactsListFieldCell,
|
||||
ItemsListCell,
|
||||
PercentFieldCell,
|
||||
DivFieldCell,
|
||||
EmptyDiv,
|
||||
NumericInputCell,
|
||||
CheckBoxFieldCell
|
||||
};
|
||||
5
src/components/Datatable/Cells.js
Normal file
5
src/components/Datatable/Cells.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export function CellTextSpan({ cell: { value } }) {
|
||||
return (<span class="cell-text">{ value }</span>)
|
||||
}
|
||||
36
src/components/Datatable/DatatableEditable.js
Normal file
36
src/components/Datatable/DatatableEditable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { DataTable, If } from 'components';
|
||||
import 'style/components/DataTable/DataTableEditable.scss';
|
||||
|
||||
/**
|
||||
* Editable datatable.
|
||||
*/
|
||||
export default function DatatableEditable({
|
||||
totalRow = false,
|
||||
actions,
|
||||
name,
|
||||
className,
|
||||
...tableProps
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.DATATABLE_EDITOR,
|
||||
{
|
||||
[`${CLASSES.DATATABLE_EDITOR}--${name}`]: name,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DataTable {...tableProps} />
|
||||
|
||||
<If condition={actions}>
|
||||
<div className={classNames(CLASSES.DATATABLE_EDITOR_ACTIONS)}>
|
||||
{actions}
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/components/Datatable/TableBody.js
Normal file
11
src/components/Datatable/TableBody.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
export default function TableBody({}) {
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
<div class="tbody-inner" style={{ minWidth: totalColumnsWidth }}></div>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
100
src/components/Datatable/TableCell.js
Normal file
100
src/components/Datatable/TableCell.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { If } from 'components';
|
||||
import { Skeleton } from 'components';
|
||||
import { useAppIntlContext } from 'components/AppIntlProvider';
|
||||
import TableContext from './TableContext';
|
||||
import { saveInvoke } from 'utils';
|
||||
import { isCellLoading } from './utils';
|
||||
|
||||
/**
|
||||
* Table cell.
|
||||
*/
|
||||
export default function TableCell({ cell, row, index }) {
|
||||
const { index: rowIndex, depth, getToggleRowExpandedProps, isExpanded } = row;
|
||||
const {
|
||||
props: {
|
||||
expandToggleColumn,
|
||||
expandColumnSpace,
|
||||
expandable,
|
||||
cellsLoading,
|
||||
cellsLoadingCoords,
|
||||
onCellClick,
|
||||
},
|
||||
} = useContext(TableContext);
|
||||
|
||||
const isExpandColumn = expandToggleColumn === index;
|
||||
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = {};
|
||||
|
||||
// Application intl context.
|
||||
const { isRTL } = useAppIntlContext();
|
||||
|
||||
// Detarmines whether the current cell is loading.
|
||||
const cellLoading = isCellLoading(
|
||||
cellsLoading,
|
||||
cellsLoadingCoords,
|
||||
rowIndex,
|
||||
cell.column.id,
|
||||
);
|
||||
|
||||
if (cellLoading) {
|
||||
return (
|
||||
<div
|
||||
{...cell.getCellProps({
|
||||
className: classNames(cell.column.className, 'td'),
|
||||
})}
|
||||
>
|
||||
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Handle cell click action.
|
||||
const handleCellClick = (event) => {
|
||||
saveInvoke(onCellClick, cell, event);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
{...cell.getCellProps({
|
||||
className: classNames(cell.column.className, 'td', {
|
||||
'is-text-overview': cell.column.textOverview,
|
||||
'clickable': cell.column.clickable,
|
||||
'align-right': cell.column.align === 'right',
|
||||
}),
|
||||
onClick: handleCellClick,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
{
|
||||
'text-overview': cell.column.textOverview,
|
||||
},
|
||||
'cell-inner',
|
||||
)}
|
||||
style={{
|
||||
[isRTL ? 'paddingRight' : 'paddingLeft']:
|
||||
isExpandColumn && expandable
|
||||
? `${depth * expandColumnSpace}rem`
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
{
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
}
|
||||
<If condition={cell.row.canExpand && expandable && isExpandColumn}>
|
||||
<span {...getToggleRowExpandedProps({ className: 'expand-toggle' })}>
|
||||
<span
|
||||
className={classNames({
|
||||
'arrow-down': isExpanded,
|
||||
'arrow-right': !isExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
|
||||
{cell.render('Cell')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/components/Datatable/TableContext.js
Normal file
3
src/components/Datatable/TableContext.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export default createContext();
|
||||
20
src/components/Datatable/TableFastCell.js
Normal file
20
src/components/Datatable/TableFastCell.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { memo } from 'react';
|
||||
import TableCell from './TableCell';
|
||||
|
||||
function TableFastCell({ cell, row, index }) {
|
||||
return <TableCell cell={cell} row={row} index={index} />;
|
||||
}
|
||||
|
||||
export default memo(TableFastCell, (prevProps, nextProps) => {
|
||||
if (
|
||||
prevProps.row.canExpand === nextProps.row.canExpand &&
|
||||
prevProps.row.isExpanded === nextProps.row.isExpanded &&
|
||||
prevProps.cell.value === nextProps.cell.value &&
|
||||
prevProps.cell.maxWidth === nextProps.cell.maxWidth &&
|
||||
prevProps.cell.width === nextProps.cell.width
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
36
src/components/Datatable/TableFooter.js
Normal file
36
src/components/Datatable/TableFooter.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table footer.
|
||||
*/
|
||||
export default function TableFooter() {
|
||||
const {
|
||||
props: { footer },
|
||||
table: { footerGroups },
|
||||
} = useContext(TableContext);
|
||||
|
||||
// Can't contiunue if the footer is disabled.
|
||||
if (!footer) { return null; }
|
||||
|
||||
return (
|
||||
<div class="tfooter">
|
||||
{footerGroups.map((group) => (
|
||||
<div {...group.getFooterGroupProps({ className: 'tr' })}>
|
||||
{group.headers.map((column) => (
|
||||
<div
|
||||
{...column.getFooterProps({
|
||||
className: classNames(column.className || '', 'td'),
|
||||
})}
|
||||
>
|
||||
<div className={'cell-inner'}>
|
||||
{column.render('Footer')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
src/components/Datatable/TableHeader.js
Normal file
101
src/components/Datatable/TableHeader.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
import { If } from 'components';
|
||||
import TableContext from './TableContext';
|
||||
import MaterialProgressBar from 'components/MaterialProgressBar';
|
||||
|
||||
function TableHeaderCell({ column, index }) {
|
||||
const {
|
||||
table: { getToggleAllRowsExpandedProps, isAllRowsExpanded },
|
||||
props: { expandable, expandToggleColumn },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: classNames(column.className || '', 'th', {
|
||||
'align-right': column.align === 'right',
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<If condition={expandable && index + 1 === expandToggleColumn}>
|
||||
<span {...getToggleAllRowsExpandedProps()} className="expand-toggle">
|
||||
<span
|
||||
className={classNames({
|
||||
'arrow-down': isAllRowsExpanded,
|
||||
'arrow-right': !isAllRowsExpanded,
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<div
|
||||
{...column.getSortByToggleProps({
|
||||
className: classNames('cell-inner', {
|
||||
'text-overview': column.textOverview,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{column.render('Header')}
|
||||
|
||||
<If condition={column.isSorted}>
|
||||
<span
|
||||
className={classNames(
|
||||
{
|
||||
'sort-icon--desc': column.isSortedDesc,
|
||||
'sort-icon--asc': !column.isSortedDesc,
|
||||
},
|
||||
'sort-icon',
|
||||
)}
|
||||
></span>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{column.canResize && (
|
||||
<div
|
||||
{...column.getResizerProps()}
|
||||
className={`resizer ${column.isResizing ? 'isResizing' : ''}`}
|
||||
>
|
||||
<div class="inner-resizer" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeaderGroup({ headerGroup }) {
|
||||
return (
|
||||
<div {...headerGroup.getHeaderGroupProps()} className="tr">
|
||||
{headerGroup.headers.map((column, index) => (
|
||||
<TableHeaderCell key={index} column={column} index={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table header.
|
||||
*/
|
||||
export default function TableHeader() {
|
||||
const {
|
||||
table: { headerGroups, page },
|
||||
props: { TableHeaderSkeletonRenderer, headerLoading, progressBarLoading },
|
||||
} = useContext(TableContext);
|
||||
|
||||
if (headerLoading && TableHeaderSkeletonRenderer) {
|
||||
return <TableHeaderSkeletonRenderer />;
|
||||
}
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
{headerGroups.map((headerGroup, index) => (
|
||||
<TableHeaderGroup key={index} headerGroup={headerGroup} />
|
||||
))}
|
||||
<If condition={progressBarLoading}>
|
||||
<MaterialProgressBar />
|
||||
</If>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
42
src/components/Datatable/TableHeaderSkeleton.js
Normal file
42
src/components/Datatable/TableHeaderSkeleton.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
import { Skeleton } from 'components';
|
||||
|
||||
function TableHeaderCell({ column }) {
|
||||
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: 'th',
|
||||
})}
|
||||
>
|
||||
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table skeleton rows.
|
||||
*/
|
||||
export default function TableSkeletonHeader({}) {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div class="thead">
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<div
|
||||
{...headerGroup.getHeaderGroupProps({
|
||||
className: 'tr',
|
||||
})}
|
||||
>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<TableHeaderCell column={column} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/components/Datatable/TableIndeterminateCheckboxHeader.js
Normal file
12
src/components/Datatable/TableIndeterminateCheckboxHeader.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '@blueprintjs/core';
|
||||
|
||||
export default function TableIndeterminateCheckboxHeader({
|
||||
getToggleAllRowsSelectedProps,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/Datatable/TableIndeterminateCheckboxRow.js
Normal file
10
src/components/Datatable/TableIndeterminateCheckboxRow.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Checkbox } from '@blueprintjs/core';
|
||||
|
||||
export default function TableIndeterminateCheckboxRow({ row }) {
|
||||
return (
|
||||
<div>
|
||||
<Checkbox {...row.getToggleRowSelectedProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/components/Datatable/TableLoading.js
Normal file
15
src/components/Datatable/TableLoading.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Table loading component.
|
||||
*/
|
||||
export default function TableLoading({
|
||||
spinnerProps
|
||||
}) {
|
||||
return (
|
||||
<div class="loading">
|
||||
<Spinner {...spinnerProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/components/Datatable/TableNoResultsRow.js
Normal file
21
src/components/Datatable/TableNoResultsRow.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useContext } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table no-results row text.
|
||||
*/
|
||||
export default function TableNoResultsRow() {
|
||||
const {
|
||||
props: { noResults },
|
||||
} = useContext(TableContext);
|
||||
|
||||
const noResultText =
|
||||
noResults || intl.get('there_is_no_results_in_the_table');
|
||||
|
||||
return (
|
||||
<div className={'tr no-results'}>
|
||||
<div class="td">{noResultText}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
src/components/Datatable/TablePage.js
Normal file
26
src/components/Datatable/TablePage.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table page.
|
||||
*/
|
||||
export default function TablePage() {
|
||||
const {
|
||||
table: { page },
|
||||
props: {
|
||||
spinnerProps,
|
||||
loading,
|
||||
TableRowsRenderer,
|
||||
TableLoadingRenderer,
|
||||
TableNoResultsRowRenderer,
|
||||
},
|
||||
} = useContext(TableContext);
|
||||
|
||||
if (loading) {
|
||||
return <TableLoadingRenderer spinnerProps={spinnerProps} />;
|
||||
}
|
||||
if (page.length === 0) {
|
||||
return <TableNoResultsRowRenderer />;
|
||||
}
|
||||
return (<TableRowsRenderer />);
|
||||
}
|
||||
59
src/components/Datatable/TablePagination.js
Normal file
59
src/components/Datatable/TablePagination.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { If, Pagination } from 'components';
|
||||
import TableContext from './TableContext';
|
||||
import { saveInvoke } from 'utils';
|
||||
|
||||
/**
|
||||
* Table pagination.
|
||||
*/
|
||||
export default function TablePagination() {
|
||||
const {
|
||||
table: {
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
pageCount,
|
||||
state: { pageIndex, pageSize },
|
||||
},
|
||||
props: { pagination, loading, onPaginationChange },
|
||||
} = useContext(TableContext);
|
||||
|
||||
const triggerOnPaginationChange = useCallback((payload) => {
|
||||
saveInvoke(onPaginationChange, payload)
|
||||
}, [onPaginationChange]);
|
||||
|
||||
// Handles the page changing.
|
||||
const handlePageChange = useCallback(
|
||||
({ page, pageSize }) => {
|
||||
const pageIndex = page - 1;
|
||||
|
||||
gotoPage(pageIndex);
|
||||
triggerOnPaginationChange({ pageIndex, pageSize });
|
||||
},
|
||||
[gotoPage, triggerOnPaginationChange],
|
||||
);
|
||||
|
||||
// Handles the page size changing.
|
||||
const handlePageSizeChange = useCallback(
|
||||
({ pageSize, page }) => {
|
||||
const pageIndex = 0;
|
||||
|
||||
gotoPage(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
|
||||
triggerOnPaginationChange({ pageIndex, pageSize });
|
||||
},
|
||||
[gotoPage, setPageSize, triggerOnPaginationChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<If condition={pagination && !loading}>
|
||||
<Pagination
|
||||
currentPage={pageIndex + 1}
|
||||
total={pageSize * pageCount}
|
||||
size={pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</If>
|
||||
);
|
||||
}
|
||||
90
src/components/Datatable/TableRow.js
Normal file
90
src/components/Datatable/TableRow.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { ContextMenu } from 'components';
|
||||
import classNames from 'classnames';
|
||||
import useContextMenu from 'react-use-context-menu';
|
||||
|
||||
import TableContext from './TableContext';
|
||||
import { saveInvoke, ConditionalWrapper } from 'utils';
|
||||
|
||||
/**
|
||||
* Table row context wrapper.
|
||||
*/
|
||||
function TableRowContextMenu({ children, row }) {
|
||||
// Table context.
|
||||
const {
|
||||
props: { ContextMenu: ContextMenuContent },
|
||||
table,
|
||||
} = useContext(TableContext);
|
||||
|
||||
const [
|
||||
bindMenu,
|
||||
bindMenuItem,
|
||||
useContextTrigger,
|
||||
{ coords, setVisible, isVisible },
|
||||
] = useContextMenu();
|
||||
|
||||
const [bindTrigger] = useContextTrigger({
|
||||
collect: () => 'Title',
|
||||
});
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, [setVisible]);
|
||||
|
||||
return (
|
||||
<div class="tr-context" {...bindTrigger}>
|
||||
{children}
|
||||
|
||||
<ContextMenu
|
||||
bindMenu={bindMenu}
|
||||
isOpen={isVisible}
|
||||
coords={coords}
|
||||
onClosed={handleClose}
|
||||
>
|
||||
<ContextMenuContent {...table} row={row} />
|
||||
</ContextMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table row.
|
||||
*/
|
||||
export default function TableRow({ row, className, style }) {
|
||||
const {
|
||||
props: {
|
||||
TableCellRenderer,
|
||||
rowClassNames,
|
||||
ContextMenu: ContextMenuContent,
|
||||
},
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({
|
||||
className: classNames(
|
||||
'tr',
|
||||
{ 'is-expanded': row.isExpanded && row.canExpand },
|
||||
saveInvoke(rowClassNames, row),
|
||||
className,
|
||||
),
|
||||
style,
|
||||
})}
|
||||
>
|
||||
<ConditionalWrapper
|
||||
condition={ContextMenuContent}
|
||||
wrapper={TableRowContextMenu}
|
||||
row={row}
|
||||
>
|
||||
{row.cells.map((cell, index) => (
|
||||
<TableCellRenderer
|
||||
key={index}
|
||||
cell={cell}
|
||||
row={row}
|
||||
index={index + 1}
|
||||
/>
|
||||
))}
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/components/Datatable/TableRows.js
Normal file
17
src/components/Datatable/TableRows.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useContext } from "react";
|
||||
import TableContext from "./TableContext";
|
||||
|
||||
/**
|
||||
* Table rows.
|
||||
*/
|
||||
export default function TableRows() {
|
||||
const {
|
||||
table: { prepareRow, page },
|
||||
props: { TableRowRenderer, TableCellRenderer },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return page.map((row, index) => {
|
||||
prepareRow(row);
|
||||
return <TableRowRenderer key={index} row={row} TableCellRenderer={TableCellRenderer} />;
|
||||
});
|
||||
}
|
||||
44
src/components/Datatable/TableSkeletonRows.js
Normal file
44
src/components/Datatable/TableSkeletonRows.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
import { Skeleton } from 'components';
|
||||
|
||||
/**
|
||||
* Table header cell.
|
||||
*/
|
||||
function TableHeaderCell({ column }) {
|
||||
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: 'td',
|
||||
})}
|
||||
>
|
||||
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table skeleton rows.
|
||||
*/
|
||||
export default function TableSkeletonRows({}) {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
} = useContext(TableContext);
|
||||
const skeletonRows = 10;
|
||||
|
||||
return Array.from({ length: skeletonRows }).map(() => {
|
||||
return headerGroups.map((headerGroup) => (
|
||||
<div
|
||||
{...headerGroup.getHeaderGroupProps({
|
||||
className: 'tr',
|
||||
})}
|
||||
>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<TableHeaderCell column={column} />
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
});
|
||||
}
|
||||
21
src/components/Datatable/TableTBody.js
Normal file
21
src/components/Datatable/TableTBody.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
export default function TableTBody({
|
||||
children
|
||||
}) {
|
||||
const {
|
||||
table: { getTableBodyProps }
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div {...getTableBodyProps()} className="tbody">
|
||||
<div class="tbody-inner">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</ScrollSyncPane>
|
||||
);
|
||||
}
|
||||
67
src/components/Datatable/TableVirtualizedRows.js
Normal file
67
src/components/Datatable/TableVirtualizedRows.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { WindowScroller, AutoSizer, List } from 'react-virtualized';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table virtualized list row.
|
||||
*/
|
||||
function TableVirtualizedListRow({
|
||||
index,
|
||||
isScrolling,
|
||||
isVisible,
|
||||
style,
|
||||
}) {
|
||||
const {
|
||||
table: { page, prepareRow },
|
||||
props: { TableRowRenderer },
|
||||
} = useContext(TableContext);
|
||||
|
||||
const row = page[index];
|
||||
prepareRow(row);
|
||||
|
||||
return (<TableRowRenderer row={row} style={style} />);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table virtualized list rows.
|
||||
*/
|
||||
export default function TableVirtualizedListRows() {
|
||||
const {
|
||||
table: { page },
|
||||
props: { vListrowHeight, vListOverscanRowCount },
|
||||
} = useContext(TableContext);
|
||||
|
||||
// Dashboard content pane.
|
||||
const dashboardContentPane = React.useMemo(()=> document.querySelector(
|
||||
`.${CLASSES.DASHBOARD_CONTENT_PANE}`,
|
||||
), []);
|
||||
|
||||
const rowRenderer = React.useCallback(({ key, ...args }) => (
|
||||
<TableVirtualizedListRow {...args} key={key} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<WindowScroller scrollElement={dashboardContentPane}>
|
||||
{({ height, isScrolling, onChildScroll, scrollTop }) => (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
autoHeight={true}
|
||||
className={'List'}
|
||||
height={height}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={vListOverscanRowCount}
|
||||
rowCount={page.length}
|
||||
rowHeight={vListrowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
scrollTop={scrollTop}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
35
src/components/Datatable/TableWrapper.js
Normal file
35
src/components/Datatable/TableWrapper.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ScrollSync } from 'react-scroll-sync';
|
||||
import TableContext from './TableContext';
|
||||
|
||||
/**
|
||||
* Table wrapper.
|
||||
*/
|
||||
export default function TableWrapper({ children }) {
|
||||
const {
|
||||
table: { getTableProps },
|
||||
props: { sticky, pagination, loading, expandable, virtualizedRows, className },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('bigcapital-datatable', className, {
|
||||
'has-sticky': sticky,
|
||||
'has-pagination': pagination,
|
||||
'is-expandable': expandable,
|
||||
'is-loading': loading,
|
||||
'has-virtualized-rows': virtualizedRows,
|
||||
})}
|
||||
>
|
||||
<ScrollSync>
|
||||
<div
|
||||
{...getTableProps({ style: { minWidth: 'none' } })}
|
||||
className="table"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ScrollSync>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/components/Datatable/utils.js
Normal file
35
src/components/Datatable/utils.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
export const isCellLoading = (loading, cellsCoords, rowIndex, columnId) => {
|
||||
if (!loading) {
|
||||
return false;
|
||||
}
|
||||
return !cellsCoords
|
||||
? true
|
||||
: cellsCoords.some(
|
||||
(cellCoord) => cellCoord[0] === rowIndex && cellCoord[1] === columnId,
|
||||
);
|
||||
};
|
||||
|
||||
export const useResizeObserver = (state, callback) => {
|
||||
// This Ref will contain the id of the column being resized or undefined
|
||||
const columnResizeRef = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
// We are interested in calling the resize event only when "state.columnResizing?.isResizingColumn" changes from
|
||||
// a string to undefined, because it indicates that it WAS resizing but it no longer is.
|
||||
if (
|
||||
state.columnResizing &&
|
||||
!state.columnResizing?.isResizingColumn &&
|
||||
columnResizeRef.current
|
||||
) {
|
||||
// Trigger resize event
|
||||
callback(
|
||||
columnResizeRef.current,
|
||||
state.columnResizing.columnWidths[columnResizeRef.current],
|
||||
state.columnResizing,
|
||||
);
|
||||
}
|
||||
columnResizeRef.current = state.columnResizing?.isResizingColumn;
|
||||
}, [callback, state.columnResizing]);
|
||||
};
|
||||
69
src/components/Details/index.js
Normal file
69
src/components/Details/index.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import 'style/components/Details.scss';
|
||||
|
||||
const DIRECTION = {
|
||||
VERTICAL: 'vertical',
|
||||
HORIZANTAL: 'horizantal',
|
||||
};
|
||||
|
||||
const DetailsMenuContext = React.createContext();
|
||||
const useDetailsMenuContext = () => React.useContext(DetailsMenuContext);
|
||||
|
||||
/**
|
||||
* Details menu.
|
||||
*/
|
||||
export function DetailsMenu({
|
||||
children,
|
||||
direction = DIRECTION.VERTICAL,
|
||||
minLabelSize,
|
||||
className,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'details-menu',
|
||||
{
|
||||
'details-menu--vertical': direction === DIRECTION.VERTICAL,
|
||||
'details-menu--horizantal': direction === DIRECTION.HORIZANTAL,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DetailsMenuContext.Provider value={{ minLabelSize }}>
|
||||
{children}
|
||||
</DetailsMenuContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail item.
|
||||
*/
|
||||
export function DetailItem({ label, children, name, align, className }) {
|
||||
const { minLabelSize } = useDetailsMenuContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'detail-item',
|
||||
{
|
||||
[`detail-item--${name}`]: name,
|
||||
[`align-${align}`]: align,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
'min-width': minLabelSize,
|
||||
}}
|
||||
class="detail-item__label"
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div class="detail-item__content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/components/Dialog/Dialog.js
Normal file
24
src/components/Dialog/Dialog.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Dialog } from '@blueprintjs/core';
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
import 'style/components/Dialog/Dialog.scss';
|
||||
|
||||
function DialogComponent(props) {
|
||||
const { name, children, closeDialog, onClose } = props;
|
||||
|
||||
const handleClose = (event) => {
|
||||
closeDialog(name)
|
||||
onClose && onClose(event);
|
||||
};
|
||||
return (
|
||||
<Dialog {...props} onClose={handleClose}>
|
||||
{ children }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
)(DialogComponent);
|
||||
14
src/components/Dialog/DialogContent.js
Normal file
14
src/components/Dialog/DialogContent.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Spinner, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function DialogContent(props) {
|
||||
const { isLoading, children } = props;
|
||||
|
||||
const loadingContent = (
|
||||
<div className={classNames(Classes.DIALOG_BODY, 'is-loading')}>
|
||||
<Spinner size={30} />
|
||||
</div>
|
||||
);
|
||||
return isLoading ? loadingContent : children;
|
||||
}
|
||||
18
src/components/Dialog/DialogSuspense.js
Normal file
18
src/components/Dialog/DialogSuspense.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Classes, Spinner } from '@blueprintjs/core';
|
||||
|
||||
function LoadingContent() {
|
||||
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
|
||||
}
|
||||
|
||||
export default function DialogSuspense({
|
||||
children
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={<LoadingContent /> }>
|
||||
<div className={'dialog__suspense-wrapper'}>
|
||||
{ children }
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
19
src/components/DialogReduxConnect.js
Normal file
19
src/components/DialogReduxConnect.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
isDialogOpenFactory,
|
||||
getDialogPayloadFactory,
|
||||
} from 'store/dashboard/dashboard.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const isDialogOpen = isDialogOpenFactory();
|
||||
const getDialogPayload = getDialogPayloadFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
isOpen: isDialogOpen(state, props),
|
||||
payload: getDialogPayload(state, props),
|
||||
};
|
||||
return mapState ? mapState(mapped) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
45
src/components/DialogsContainer.js
Normal file
45
src/components/DialogsContainer.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import AccountDialog from 'containers/Dialogs/AccountDialog';
|
||||
import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
|
||||
import UserFormDialog from 'containers/Dialogs/UserFormDialog';
|
||||
import ItemCategoryDialog from 'containers/Dialogs/ItemCategoryDialog';
|
||||
import CurrencyFormDialog from 'containers/Dialogs/CurrencyFormDialog';
|
||||
import ExchangeRateFormDialog from 'containers/Dialogs/ExchangeRateFormDialog';
|
||||
|
||||
import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog';
|
||||
import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog';
|
||||
import KeyboardShortcutsDialog from 'containers/Dialogs/keyboardShortcutsDialog';
|
||||
import ContactDuplicateDialog from 'containers/Dialogs/ContactDuplicateDialog';
|
||||
import QuickPaymentReceiveFormDialog from 'containers/Dialogs/QuickPaymentReceiveFormDialog';
|
||||
import QuickPaymentMadeFormDialog from 'containers/Dialogs/QuickPaymentMadeFormDialog';
|
||||
import AllocateLandedCostDialog from 'containers/Dialogs/AllocateLandedCostDialog';
|
||||
import InvoicePdfPreviewDialog from 'containers/Dialogs/InvoicePdfPreviewDialog';
|
||||
import EstimatePdfPreviewDialog from 'containers/Dialogs/EstimatePdfPreviewDialog';
|
||||
import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDialog';
|
||||
|
||||
/**
|
||||
* Dialogs container.
|
||||
*/
|
||||
export default function DialogsContainer() {
|
||||
return (
|
||||
<div>
|
||||
<AccountDialog dialogName={'account-form'} />
|
||||
<CurrencyFormDialog dialogName={'currency-form'} />
|
||||
<InviteUserDialog dialogName={'invite-user'} />
|
||||
<UserFormDialog dialogName={'user-form'} />
|
||||
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
|
||||
<ItemCategoryDialog dialogName={'item-category-form'} />
|
||||
<InventoryAdjustmentDialog dialogName={'inventory-adjustment'} />
|
||||
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
|
||||
<KeyboardShortcutsDialog dialogName={'keyboard-shortcuts'} />
|
||||
<ContactDuplicateDialog dialogName={'contact-duplicate'} />
|
||||
<QuickPaymentReceiveFormDialog dialogName={'quick-payment-receive'} />
|
||||
<QuickPaymentMadeFormDialog dialogName={'quick-payment-made'} />
|
||||
<AllocateLandedCostDialog dialogName={'allocate-landed-cost'} />
|
||||
<InvoicePdfPreviewDialog dialogName={'invoice-pdf-preview'} />
|
||||
<EstimatePdfPreviewDialog dialogName={'estimate-pdf-preview'} />
|
||||
<ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user