mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
re-structure to monorepo.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { FMultiSelect } from '../Forms';
|
||||
import classNames from 'classnames';
|
||||
import { Classes } from '@blueprintjs/popover2';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} account
|
||||
* @param {*} _index
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const accountItemPredicate = (query, account, _index, exactMatch) => {
|
||||
const normalizedTitle = account.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${account.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} account
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const accountItemRenderer = (
|
||||
account,
|
||||
{ handleClick, modifiers, query },
|
||||
{ isSelected },
|
||||
) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon={isSelected ? 'tick' : 'blank'}
|
||||
text={account.name}
|
||||
label={account.code}
|
||||
key={account.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const accountSelectProps = {
|
||||
itemPredicate: accountItemPredicate,
|
||||
itemRenderer: accountItemRenderer,
|
||||
valueAccessor: (item) => item.id,
|
||||
labelAccessor: (item) => item.code,
|
||||
tagRenderer: (item) => item.name,
|
||||
};
|
||||
|
||||
/**
|
||||
* branches mulit select.
|
||||
* @param {*} param0
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function AccountMultiSelect({ accounts, ...rest }) {
|
||||
return (
|
||||
<FMultiSelect
|
||||
items={accounts}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
}}
|
||||
{...accountSelectProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MultiSelect } from '../MultiSelectTaggable';
|
||||
|
||||
export function AccountsMultiSelect({ ...multiSelectProps }) {
|
||||
return (
|
||||
<MultiSelect
|
||||
itemRenderer={(
|
||||
item,
|
||||
{ active, selected, handleClick, modifiers, query },
|
||||
) => {
|
||||
return (
|
||||
<MenuItem
|
||||
active={active}
|
||||
icon={selected ? 'tick' : 'blank'}
|
||||
text={item.name}
|
||||
label={item.code}
|
||||
key={item.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
popoverProps={{ minimal: true }}
|
||||
fill={true}
|
||||
tagRenderer={(item) => item.name}
|
||||
resetOnSelect={true}
|
||||
{...multiSelectProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
177
packages/webapp/src/components/Accounts/AccountsSelectList.tsx
Normal file
177
packages/webapp/src/components/Accounts/AccountsSelectList.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
|
||||
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
// Create new account renderer.
|
||||
const createNewItemRenderer = (query, active, handleClick) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon="add"
|
||||
text={intl.get('list.create', { value: `"${query}"` })}
|
||||
active={active}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Create new item from the given query string.
|
||||
const createNewItemFromQuery = (name) => {
|
||||
return {
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
// Filters accounts items.
|
||||
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
|
||||
const normalizedTitle = account.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accounts select list.
|
||||
*/
|
||||
function AccountsSelectListRoot({
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #ownProps
|
||||
accounts,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
defaultSelectText = 'Select account',
|
||||
onAccountSelected,
|
||||
disabled = false,
|
||||
popoverFill = false,
|
||||
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
filterByRootTypes,
|
||||
|
||||
allowCreate,
|
||||
|
||||
buttonProps = {},
|
||||
}) {
|
||||
const flattenAccounts = useMemo(
|
||||
() => nestedArrayToflatten(accounts),
|
||||
[accounts],
|
||||
);
|
||||
|
||||
// Filters accounts based on filter props.
|
||||
const filteredAccounts = useMemo(() => {
|
||||
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
|
||||
filterByRootTypes,
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
});
|
||||
return filteredAccounts;
|
||||
}, [
|
||||
flattenAccounts,
|
||||
filterByRootTypes,
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
]);
|
||||
|
||||
// Find initial account object to set it as default account in initial render.
|
||||
const initialAccount = useMemo(
|
||||
() => filteredAccounts.find((a) => a.id === initialAccountId),
|
||||
[initialAccountId, filteredAccounts],
|
||||
);
|
||||
|
||||
// Select account item.
|
||||
const [selectedAccount, setSelectedAccount] = useState(
|
||||
initialAccount || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedAccountId !== 'undefined') {
|
||||
const account = selectedAccountId
|
||||
? filteredAccounts.find((a) => a.id === selectedAccountId)
|
||||
: null;
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
|
||||
|
||||
// Account item of select accounts field.
|
||||
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
text={<MenuItemNestedText level={item.level} text={item.name} />}
|
||||
label={item.code}
|
||||
key={item.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Handle the account item select.
|
||||
const handleAccountSelect = useCallback(
|
||||
(account) => {
|
||||
if (account.id) {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
} else {
|
||||
openDialog(DialogsName.AccountForm);
|
||||
}
|
||||
},
|
||||
[setSelectedAccount, onAccountSelected, openDialog],
|
||||
);
|
||||
|
||||
// Maybe inject new item props to select component.
|
||||
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
|
||||
const maybeCreateNewItemFromQuery = allowCreate
|
||||
? createNewItemFromQuery
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={filteredAccounts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccountsPredicater}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
usePortal: !popoverFill,
|
||||
inline: popoverFill,
|
||||
}}
|
||||
filterable={true}
|
||||
onItemSelect={handleAccountSelect}
|
||||
disabled={disabled}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={selectedAccount ? selectedAccount.name : defaultSelectText}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export const AccountsSelectList = R.compose(withDialogActions)(
|
||||
AccountsSelectListRoot,
|
||||
);
|
||||
176
packages/webapp/src/components/Accounts/AccountsSuggestField.tsx
Normal file
176
packages/webapp/src/components/Accounts/AccountsSuggestField.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import * as R from 'ramda';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
import { MenuItemNestedText, FormattedMessage as T } from '@/components';
|
||||
import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
// Create new account renderer.
|
||||
const createNewItemRenderer = (query, active, handleClick) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon="add"
|
||||
text={intl.get('list.create', { value: `"${query}"` })}
|
||||
active={active}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Create new item from the given query string.
|
||||
const createNewItemFromQuery = (name) => {
|
||||
return {
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
// Handle input value renderer.
|
||||
const handleInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.name.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Filters accounts items.
|
||||
const filterAccountsPredicater = (query, account, _index, exactMatch) => {
|
||||
const normalizedTitle = account.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Accounts suggest field.
|
||||
*/
|
||||
function AccountsSuggestFieldRoot({
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #ownProps
|
||||
accounts,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
defaultSelectText = intl.formatMessage({ id: 'select_account' }),
|
||||
popoverFill = false,
|
||||
onAccountSelected,
|
||||
|
||||
filterByParentTypes = [],
|
||||
filterByTypes = [],
|
||||
filterByNormal,
|
||||
filterByRootTypes = [],
|
||||
|
||||
allowCreate,
|
||||
|
||||
...suggestProps
|
||||
}) {
|
||||
const flattenAccounts = useMemo(
|
||||
() => nestedArrayToflatten(accounts),
|
||||
[accounts],
|
||||
);
|
||||
|
||||
// Filters accounts based on filter props.
|
||||
const filteredAccounts = useMemo(() => {
|
||||
let filteredAccounts = filterAccountsByQuery(flattenAccounts, {
|
||||
filterByRootTypes,
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
});
|
||||
return filteredAccounts;
|
||||
}, [
|
||||
flattenAccounts,
|
||||
filterByRootTypes,
|
||||
filterByParentTypes,
|
||||
filterByTypes,
|
||||
filterByNormal,
|
||||
]);
|
||||
|
||||
// Find initial account object to set it as default account in initial render.
|
||||
const initialAccount = useMemo(
|
||||
() => filteredAccounts.find((a) => a.id === initialAccountId),
|
||||
[initialAccountId, filteredAccounts],
|
||||
);
|
||||
|
||||
const [selectedAccount, setSelectedAccount] = useState(
|
||||
initialAccount || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedAccountId !== 'undefined') {
|
||||
const account = selectedAccountId
|
||||
? filteredAccounts.find((a) => a.id === selectedAccountId)
|
||||
: null;
|
||||
setSelectedAccount(account);
|
||||
}
|
||||
}, [selectedAccountId, filteredAccounts, setSelectedAccount]);
|
||||
|
||||
// Account item of select accounts field.
|
||||
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
text={<MenuItemNestedText level={item.level} text={item.name} />}
|
||||
label={item.code}
|
||||
key={item.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleAccountSelect = useCallback(
|
||||
(account) => {
|
||||
if (account.id) {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
} else {
|
||||
openDialog(DialogsName.AccountForm);
|
||||
}
|
||||
},
|
||||
[setSelectedAccount, onAccountSelected, openDialog],
|
||||
);
|
||||
|
||||
// Maybe inject new item props to select component.
|
||||
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
|
||||
const maybeCreateNewItemFromQuery = allowCreate
|
||||
? createNewItemFromQuery
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Suggest
|
||||
items={filteredAccounts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccountsPredicater}
|
||||
onItemSelect={handleAccountSelect}
|
||||
selectedItem={selectedAccount}
|
||||
inputProps={{ placeholder: defaultSelectText }}
|
||||
resetOnClose={true}
|
||||
fill={true}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputValueRenderer={handleInputValueRenderer}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const AccountsSuggestField = R.compose(withDialogActions)(
|
||||
AccountsSuggestFieldRoot,
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ListSelect } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
export function AccountsTypesSelect({
|
||||
accountsTypes,
|
||||
selectedTypeId,
|
||||
defaultSelectText = 'Select account type',
|
||||
onTypeSelected,
|
||||
disabled = false,
|
||||
popoverFill = false,
|
||||
...restProps
|
||||
}) {
|
||||
// Filters accounts types items.
|
||||
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
|
||||
const normalizedTitle = accountType.label.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return normalizedTitle.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle item selected.
|
||||
const handleItemSelected = (accountType) => {
|
||||
onTypeSelected && onTypeSelected(accountType);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListSelect
|
||||
items={accountsTypes}
|
||||
selectedItemProp={'key'}
|
||||
selectedItem={selectedTypeId}
|
||||
textProp={'label'}
|
||||
defaultText={defaultSelectText}
|
||||
onItemSelect={handleItemSelected}
|
||||
itemPredicate={filterAccountTypeItems}
|
||||
disabled={disabled}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
packages/webapp/src/components/Accounts/index.tsx
Normal file
5
packages/webapp/src/components/Accounts/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './AccountMultiSelect';
|
||||
export * from './AccountsMultiSelect';
|
||||
export * from './AccountsSelectList';
|
||||
export * from './AccountsSuggestField';
|
||||
export * from './AccountsTypesSelect';
|
||||
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const getFilterDropdownSchema = () =>
|
||||
Yup.object().shape({
|
||||
conditions: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
fieldKey: Yup.string(),
|
||||
value: Yup.string().nullable(),
|
||||
condition: Yup.string().nullable(),
|
||||
comparator: Yup.string().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { ListSelect } from '../Select';
|
||||
import { getConditionTypeCompatators } from './utils';
|
||||
|
||||
export default function DynamicFilterCompatatorField({
|
||||
dataType,
|
||||
...restProps
|
||||
}) {
|
||||
const options = getConditionTypeCompatators(dataType);
|
||||
|
||||
return (
|
||||
<ListSelect
|
||||
textProp={'label'}
|
||||
selectedItemProp={'value'}
|
||||
items={options}
|
||||
className={Classes.FILL}
|
||||
filterable={false}
|
||||
popoverProps={{
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Classes,
|
||||
InputGroup,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { get, first, defaultTo, isEqual, isEmpty } from 'lodash';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Choose, Icon, FormattedMessage as T, ListSelect } from '@/components';
|
||||
import { useUpdateEffect } from '@/hooks';
|
||||
import {
|
||||
AdvancedFilterDropdownProvider,
|
||||
FilterConditionProvider,
|
||||
useFilterCondition,
|
||||
useAdvancedFilterContext,
|
||||
} from './AdvancedFilterDropdownContext';
|
||||
import AdvancedFilterCompatatorField from './AdvancedFilterCompatatorField';
|
||||
import AdvancedFilterValueField from './AdvancedFilterValueField';
|
||||
import {
|
||||
filterConditionRoles,
|
||||
getConditionalsOptions,
|
||||
transformFieldsToOptions,
|
||||
shouldFilterValueFieldUpdate,
|
||||
getConditionTypeCompatators,
|
||||
} from './utils';
|
||||
import { getFilterDropdownSchema } from './AdvancedFilter.schema';
|
||||
import { useAdvancedFilterAutoSubmit } from './components';
|
||||
|
||||
/**
|
||||
* Condition item list renderer.
|
||||
*/
|
||||
function ConditionItemRenderer(condition, { handleClick, modifiers, query }) {
|
||||
return (
|
||||
<MenuItem
|
||||
text={
|
||||
<>
|
||||
<div>{condition.label}</div>
|
||||
<span className="text-hint">{condition.text}</span>
|
||||
</>
|
||||
}
|
||||
key={condition.value}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition field.
|
||||
*/
|
||||
function FilterConditionField() {
|
||||
const conditionalsOptions = getConditionalsOptions();
|
||||
const { conditionIndex, getConditionFieldPath } = useFilterCondition();
|
||||
|
||||
const conditionFieldPath = getConditionFieldPath('condition');
|
||||
|
||||
return (
|
||||
<FastField name={conditionFieldPath}>
|
||||
{({ form, field }) => (
|
||||
<FormGroup className={'form-group--condition'}>
|
||||
<Choose>
|
||||
<Choose.When condition={conditionIndex === 0}>
|
||||
<InputGroup disabled value={intl.get('filter.when')} />
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<ListSelect
|
||||
selectedItem={field.value}
|
||||
textProp={'label'}
|
||||
selectedItemProp={'value'}
|
||||
labelProp={'text'}
|
||||
items={conditionalsOptions}
|
||||
className={Classes.FILL}
|
||||
filterable={false}
|
||||
onItemSelect={(option) => {
|
||||
form.setFieldValue(conditionFieldPath, option.value);
|
||||
}}
|
||||
popoverProps={{
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
}}
|
||||
itemRenderer={ConditionItemRenderer}
|
||||
/>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatator field.
|
||||
*/
|
||||
function FilterCompatatorFilter() {
|
||||
const { getConditionFieldPath, fieldMeta } = useFilterCondition();
|
||||
|
||||
const comparatorFieldPath = getConditionFieldPath('comparator');
|
||||
const fieldType = get(fieldMeta, 'fieldType');
|
||||
|
||||
return (
|
||||
<FastField name={comparatorFieldPath}>
|
||||
{({ form, field }) => (
|
||||
<FormGroup className={'form-group--comparator'}>
|
||||
<AdvancedFilterCompatatorField
|
||||
dataType={fieldType}
|
||||
className={Classes.FILL}
|
||||
selectedItem={field.value}
|
||||
onItemSelect={(option) => {
|
||||
form.setFieldValue(comparatorFieldPath, option.value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes default value of comparator field in the condition row once the
|
||||
* field option changing.
|
||||
*/
|
||||
function useDefaultComparatorFieldValue({
|
||||
getConditionValue,
|
||||
setConditionValue,
|
||||
fieldMeta,
|
||||
}) {
|
||||
const fieldKeyValue = getConditionValue('fieldKey');
|
||||
|
||||
const comparatorsOptions = React.useMemo(
|
||||
() => getConditionTypeCompatators(fieldMeta.fieldType),
|
||||
[fieldMeta.fieldType],
|
||||
);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (fieldKeyValue) {
|
||||
const defaultValue = get(first(comparatorsOptions), 'value');
|
||||
setConditionValue('comparator', defaultValue);
|
||||
}
|
||||
}, [fieldKeyValue, setConditionValue, comparatorsOptions]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource fields field.
|
||||
*/
|
||||
function FilterFieldsField() {
|
||||
const {
|
||||
getConditionFieldPath,
|
||||
getConditionValue,
|
||||
setConditionValue,
|
||||
fieldMeta,
|
||||
} = useFilterCondition();
|
||||
|
||||
const { fields } = useAdvancedFilterContext();
|
||||
|
||||
const fieldPath = getConditionFieldPath('fieldKey');
|
||||
const valueFieldPath = getConditionFieldPath('value');
|
||||
|
||||
useDefaultComparatorFieldValue({
|
||||
getConditionValue,
|
||||
setConditionValue,
|
||||
fieldMeta,
|
||||
});
|
||||
|
||||
return (
|
||||
<FastField name={fieldPath}>
|
||||
{({ field, form }) => (
|
||||
<FormGroup className={'form-group--fieldKey'}>
|
||||
<ListSelect
|
||||
selectedItem={field.value}
|
||||
textProp={'label'}
|
||||
selectedItemProp={'value'}
|
||||
items={transformFieldsToOptions(fields)}
|
||||
className={Classes.FILL}
|
||||
onItemSelect={(option) => {
|
||||
form.setFieldValue(fieldPath, option.value);
|
||||
|
||||
// Resets the value field to empty once the field option changing.
|
||||
form.setFieldValue(valueFieldPath, '');
|
||||
}}
|
||||
popoverProps={{
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter value field.
|
||||
*/
|
||||
function FilterValueField() {
|
||||
const { conditionIndex, fieldMeta, getConditionFieldPath } =
|
||||
useFilterCondition();
|
||||
|
||||
// Can't continue if the given field key is not selected yet.
|
||||
if (!fieldMeta) {
|
||||
return null;
|
||||
}
|
||||
// Field meta type, name and options.
|
||||
const fieldType = get(fieldMeta, 'fieldType');
|
||||
const fieldName = get(fieldMeta, 'name');
|
||||
const options = get(fieldMeta, 'options');
|
||||
|
||||
const valueFieldPath = getConditionFieldPath('value');
|
||||
|
||||
return (
|
||||
<FastField
|
||||
name={valueFieldPath}
|
||||
fieldKey={fieldType} // Pass to shouldUpdate function.
|
||||
shouldUpdate={shouldFilterValueFieldUpdate}
|
||||
>
|
||||
{({ form: { setFieldValue }, field }) => (
|
||||
<FormGroup className={'form-group--value'}>
|
||||
<AdvancedFilterValueField
|
||||
isFocus={conditionIndex === 0}
|
||||
value={field.value}
|
||||
key={'name'}
|
||||
label={fieldName}
|
||||
fieldType={fieldType}
|
||||
options={options}
|
||||
onChange={(value) => {
|
||||
setFieldValue(valueFieldPath, value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter condition line.
|
||||
*/
|
||||
function AdvancedFilterDropdownCondition({ conditionIndex, onRemoveClick }) {
|
||||
// Handle click remove condition.
|
||||
const handleClickRemoveCondition = () => {
|
||||
onRemoveClick && onRemoveClick(conditionIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown__condition">
|
||||
<FilterConditionProvider conditionIndex={conditionIndex}>
|
||||
<FilterConditionField />
|
||||
<FilterFieldsField />
|
||||
<FilterCompatatorFilter />
|
||||
<FilterValueField />
|
||||
|
||||
<Button
|
||||
icon={<Icon icon="times" iconSize={14} />}
|
||||
minimal={true}
|
||||
onClick={handleClickRemoveCondition}
|
||||
className={'button--remove'}
|
||||
/>
|
||||
</FilterConditionProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter dropdown condition.
|
||||
*/
|
||||
function AdvancedFilterDropdownConditions({ push, remove, replace, form }) {
|
||||
const { initialCondition } = useAdvancedFilterContext();
|
||||
|
||||
// Handle remove condition.
|
||||
const handleClickRemoveCondition = (conditionIndex) => {
|
||||
if (form.values.conditions.length > 1) {
|
||||
remove(conditionIndex);
|
||||
} else {
|
||||
replace(0, { ...initialCondition });
|
||||
}
|
||||
};
|
||||
// Handle new condition button click.
|
||||
const handleNewConditionBtnClick = (index) => {
|
||||
push({ ...initialCondition });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter-dropdonw__conditions-wrap">
|
||||
<div className={'filter-dropdown__conditions'}>
|
||||
{form.values.conditions.map((condition, index) => (
|
||||
<AdvancedFilterDropdownCondition
|
||||
conditionIndex={index}
|
||||
onRemoveClick={handleClickRemoveCondition}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<AdvancedFilterDropdownFooter onClick={handleNewConditionBtnClick} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter dropdown form.
|
||||
*/
|
||||
function AdvancedFilterDropdownForm() {
|
||||
// Advanced filter auto-save.
|
||||
useAdvancedFilterAutoSubmit();
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown__form">
|
||||
<FieldArray
|
||||
name={'conditions'}
|
||||
render={({ ...fieldArrayProps }) => (
|
||||
<AdvancedFilterDropdownConditions {...fieldArrayProps} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter dropdown footer.
|
||||
*/
|
||||
function AdvancedFilterDropdownFooter({ onClick }) {
|
||||
// Handle new filter condition button click.
|
||||
const onClickNewFilter = (event) => {
|
||||
onClick && onClick(event);
|
||||
};
|
||||
return (
|
||||
<div className="filter-dropdown__footer">
|
||||
<Button minimal={true} onClick={onClickNewFilter}>
|
||||
<T id={'new_conditional'} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced filter dropdown.
|
||||
*/
|
||||
export function AdvancedFilterDropdown({
|
||||
fields,
|
||||
conditions,
|
||||
defaultFieldKey,
|
||||
defaultComparator,
|
||||
defaultValue,
|
||||
defaultCondition,
|
||||
onFilterChange,
|
||||
}) {
|
||||
// Initial condition.
|
||||
const initialCondition = {
|
||||
fieldKey: defaultFieldKey,
|
||||
comparator: defaultTo(defaultComparator, 'contain'),
|
||||
condition: defaultTo(defaultCondition, 'or'),
|
||||
value: defaultTo(defaultValue, ''),
|
||||
};
|
||||
// Initial conditions.
|
||||
const initialConditions = !isEmpty(conditions)
|
||||
? conditions
|
||||
: [initialCondition, initialCondition];
|
||||
|
||||
const [prevConditions, setPrevConditions] = React.useState(initialConditions);
|
||||
|
||||
// Handle the filter dropdown form submit.
|
||||
const handleFitlerDropdownSubmit = (values) => {
|
||||
const conditions = filterConditionRoles(values.conditions);
|
||||
|
||||
// Campare the current conditions with previous conditions, if they were equal
|
||||
// there is no need to execute `onFilterChange` function.
|
||||
if (!isEqual(prevConditions, conditions)) {
|
||||
onFilterChange && onFilterChange(conditions);
|
||||
setPrevConditions(conditions);
|
||||
}
|
||||
};
|
||||
// Filter dropdown validation schema.
|
||||
const validationSchema = getFilterDropdownSchema();
|
||||
|
||||
// Initial values.
|
||||
const initialValues = {
|
||||
conditions: initialConditions,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown">
|
||||
<AdvancedFilterDropdownProvider
|
||||
initialCondition={initialCondition}
|
||||
fields={fields}
|
||||
>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
component={AdvancedFilterDropdownForm}
|
||||
onSubmit={handleFitlerDropdownSubmit}
|
||||
/>
|
||||
</AdvancedFilterDropdownProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { get, keyBy } from 'lodash';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
const AdvancedFilterContext = createContext({});
|
||||
const FilterConditionContext = createContext({});
|
||||
|
||||
/**
|
||||
* Advanced filter dropdown context provider.
|
||||
*/
|
||||
function AdvancedFilterDropdownProvider({
|
||||
initialCondition,
|
||||
fields,
|
||||
...props
|
||||
}) {
|
||||
const fieldsByKey = keyBy(fields, 'key');
|
||||
|
||||
// Retrieve field meta by the given field key.
|
||||
const getFieldMetaByKey = React.useCallback(
|
||||
(key) => get(fieldsByKey, key),
|
||||
[fieldsByKey],
|
||||
);
|
||||
// Provider payload.
|
||||
const provider = { initialCondition, fields, fieldsByKey, getFieldMetaByKey };
|
||||
|
||||
return <AdvancedFilterContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter condition row context provider.
|
||||
*/
|
||||
function FilterConditionProvider({ conditionIndex, ...props }) {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
const { getFieldMetaByKey } = useAdvancedFilterContext();
|
||||
|
||||
// Condition value path.
|
||||
const conditionPath = `conditions[${conditionIndex}]`;
|
||||
|
||||
// Sets conditions value.
|
||||
const setConditionValue = React.useCallback(
|
||||
(field, value) => {
|
||||
return setFieldValue(`${conditionPath}.${field}`, value);
|
||||
},
|
||||
[conditionPath, setFieldValue],
|
||||
);
|
||||
|
||||
// Retrieve condition field value.
|
||||
const getConditionValue = React.useCallback(
|
||||
(field) => get(values, `${conditionPath}.${field}`),
|
||||
[conditionPath, values],
|
||||
);
|
||||
|
||||
// The current condition field meta.
|
||||
const fieldMeta = React.useMemo(
|
||||
() => getFieldMetaByKey(getConditionValue('fieldKey')),
|
||||
[getFieldMetaByKey, getConditionValue],
|
||||
);
|
||||
|
||||
// Retrieve the condition field path.
|
||||
const getConditionFieldPath = React.useCallback(
|
||||
(field) => `${conditionPath}.${field}`,
|
||||
[conditionPath],
|
||||
);
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
fieldMeta,
|
||||
conditionIndex,
|
||||
getConditionValue,
|
||||
getConditionFieldPath,
|
||||
setConditionValue,
|
||||
};
|
||||
return <FilterConditionContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useFilterCondition = () => useContext(FilterConditionContext);
|
||||
const useAdvancedFilterContext = () => useContext(AdvancedFilterContext);
|
||||
|
||||
export {
|
||||
AdvancedFilterDropdownProvider,
|
||||
FilterConditionProvider,
|
||||
useAdvancedFilterContext,
|
||||
useFilterCondition,
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Popover, PopoverInteractionKind, Position } from '@blueprintjs/core';
|
||||
import { AdvancedFilterDropdown } from './AdvancedFilterDropdown';
|
||||
|
||||
/**
|
||||
* Advanced filter popover.
|
||||
*/
|
||||
export function AdvancedFilterPopover({
|
||||
popoverProps,
|
||||
advancedFilterProps,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={
|
||||
<AdvancedFilterDropdown
|
||||
{...advancedFilterProps}
|
||||
/>
|
||||
}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
canOutsideClickClose={true}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
{...popoverProps}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
|
||||
import { DateInput } from '@blueprintjs/datetime';
|
||||
import moment from 'moment';
|
||||
import intl from 'react-intl-universal';
|
||||
import { isUndefined } from 'lodash';
|
||||
|
||||
import { useAutofocus } from '@/hooks';
|
||||
import { T, Choose, ListSelect } from '@/components';
|
||||
import { momentFormatter } from '@/utils';
|
||||
|
||||
function AdvancedFilterEnumerationField({ options, value, ...rest }) {
|
||||
return (
|
||||
<ListSelect
|
||||
items={options}
|
||||
selectedItem={value}
|
||||
popoverProps={{
|
||||
fill: true,
|
||||
inline: true,
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
}}
|
||||
defaultText={<T id={'filter.select_option'} />}
|
||||
textProp={'label'}
|
||||
selectedItemProp={'key'}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const IFieldType = {
|
||||
ENUMERATION: 'enumeration',
|
||||
BOOLEAN: 'boolean',
|
||||
NUMBER: 'number',
|
||||
DATE: 'date',
|
||||
};
|
||||
|
||||
function tansformDateValue(date, defaultValue = null) {
|
||||
return date ? moment(date).toDate() : defaultValue;
|
||||
}
|
||||
/**
|
||||
* Advanced filter value field detarminer.
|
||||
*/
|
||||
export default function AdvancedFilterValueField2({
|
||||
value,
|
||||
fieldType,
|
||||
options,
|
||||
onChange,
|
||||
isFocus,
|
||||
}) {
|
||||
const [localValue, setLocalValue] = React.useState(value);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (localValue !== value && !isUndefined(value)) {
|
||||
setLocalValue(value);
|
||||
}
|
||||
}, [localValue, value]);
|
||||
|
||||
// Input field reference.
|
||||
const valueRef = useAutofocus(isFocus);
|
||||
|
||||
const triggerOnChange = (value) => onChange && onChange(value);
|
||||
|
||||
// Handle input change.
|
||||
const handleInputChange = (e) => {
|
||||
if (e.currentTarget.type === 'checkbox') {
|
||||
setLocalValue(e.currentTarget.checked);
|
||||
triggerOnChange(e.currentTarget.checked);
|
||||
} else {
|
||||
setLocalValue(e.currentTarget.value);
|
||||
triggerOnChange(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle enumeration field type change.
|
||||
const handleEnumerationChange = (option) => {
|
||||
setLocalValue(option.key);
|
||||
triggerOnChange(option.key);
|
||||
};
|
||||
|
||||
// Handle date field change.
|
||||
const handleDateChange = (date) => {
|
||||
const formattedDate = moment(date).format('YYYY/MM/DD');
|
||||
|
||||
setLocalValue(formattedDate);
|
||||
triggerOnChange(formattedDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={fieldType === IFieldType.ENUMERATION}>
|
||||
<AdvancedFilterEnumerationField
|
||||
options={options}
|
||||
value={localValue}
|
||||
onItemSelect={handleEnumerationChange}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={fieldType === IFieldType.DATE}>
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={tansformDateValue(localValue)}
|
||||
onChange={handleDateChange}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
position: Position.BOTTOM,
|
||||
}}
|
||||
shortcuts={true}
|
||||
placeholder={intl.get('filter.enter_date')}
|
||||
fill={true}
|
||||
inputProps={{
|
||||
fill: true,
|
||||
}}
|
||||
/>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={fieldType === IFieldType.BOOLEAN}>
|
||||
<Checkbox value={localValue} onChange={handleInputChange} />
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<InputGroup
|
||||
placeholder={intl.get('filter.value')}
|
||||
onChange={handleInputChange}
|
||||
value={localValue}
|
||||
inputRef={valueRef}
|
||||
/>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
23
packages/webapp/src/components/AdvancedFilter/components.tsx
Normal file
23
packages/webapp/src/components/AdvancedFilter/components.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const DEBOUNCE_MS = 100;
|
||||
|
||||
/**
|
||||
* Advanced filter auto-save.
|
||||
*/
|
||||
export function useAdvancedFilterAutoSubmit() {
|
||||
const { submitForm, values } = useFormikContext();
|
||||
const [isSubmit, setIsSubmit] = React.useState(false);
|
||||
|
||||
const debouncedSubmit = React.useCallback(
|
||||
debounce(() => {
|
||||
return submitForm().then(() => setIsSubmit(true));
|
||||
}, DEBOUNCE_MS),
|
||||
[submitForm],
|
||||
);
|
||||
|
||||
React.useEffect(() => debouncedSubmit, [debouncedSubmit, values]);
|
||||
}
|
||||
112
packages/webapp/src/components/AdvancedFilter/interfaces.ts
Normal file
112
packages/webapp/src/components/AdvancedFilter/interfaces.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// @ts-nocheck
|
||||
import { ArrayHelpers } from 'formik';
|
||||
import { IPopoverProps } from '@blueprintjs/core';
|
||||
|
||||
export type IResourceFieldType = 'text' | 'number' | 'enumeration' | 'boolean';
|
||||
|
||||
export interface IResourceField {
|
||||
name: string;
|
||||
key: string;
|
||||
fieldType: IResourceFieldType;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterDropdown {
|
||||
fields: IResourceField[];
|
||||
conditions?: IFilterRole[];
|
||||
defaultFieldKey: string;
|
||||
defaultComparator?: string;
|
||||
defaultValue?: string;
|
||||
defaultCondition?: string;
|
||||
onFilterChange?: (filterRoles: IFilterRole[]) => void;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterDropdownFooter {
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
export interface IFilterFieldsField {
|
||||
fields: IResourceField[];
|
||||
}
|
||||
|
||||
export interface IFilterRole {
|
||||
fieldKey: string;
|
||||
comparator: string;
|
||||
condition: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterContextProps {
|
||||
initialCondition: IFilterRole;
|
||||
fields: IResourceField[];
|
||||
fieldsByKey: { [fieldKey: string]: IResourceField };
|
||||
}
|
||||
|
||||
export interface IFilterConditionContextProps {
|
||||
conditionIndex: number;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterProviderProps {
|
||||
initialCondition: IFilterRole;
|
||||
fields: IResourceField[];
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface IFilterConditionProviderProps {
|
||||
conditionIndex: number;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface IFilterDropdownFormikValues {
|
||||
conditions: IFilterRole[];
|
||||
}
|
||||
|
||||
export type IAdvancedFilterDropdownConditionsProps = ArrayHelpers;
|
||||
|
||||
export interface IAdvancedFilterDropdownCondition {
|
||||
conditionIndex: number;
|
||||
onRemoveClick: Function;
|
||||
}
|
||||
|
||||
export interface IFilterOption {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterValueField {
|
||||
fieldType: string;
|
||||
value?: string;
|
||||
key: string;
|
||||
label: string;
|
||||
options?: IFilterOption[];
|
||||
onChange: Function;
|
||||
}
|
||||
|
||||
export enum IFieldType {
|
||||
TEXT = 'text',
|
||||
NUMBER = 'number',
|
||||
DATE = 'date',
|
||||
ENUMERATION = 'enumeration',
|
||||
BOOLEAN = 'boolean',
|
||||
}
|
||||
|
||||
export interface IConditionTypeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface IConditionOption {
|
||||
label: string;
|
||||
value: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface IAdvancedFilterPopover {
|
||||
popoverProps?: IPopoverProps;
|
||||
advancedFilterProps: IAdvancedFilterDropdown;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
}
|
||||
|
||||
|
||||
export interface IDynamicFilterCompatatorFieldProps {
|
||||
dataType: string;
|
||||
}
|
||||
113
packages/webapp/src/components/AdvancedFilter/utils.tsx
Normal file
113
packages/webapp/src/components/AdvancedFilter/utils.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// @ts-nocheck
|
||||
import intl from 'react-intl-universal';
|
||||
import {
|
||||
defaultFastFieldShouldUpdate,
|
||||
uniqueMultiProps,
|
||||
checkRequiredProperties,
|
||||
} from '@/utils';
|
||||
|
||||
// Conditions options.
|
||||
export const getConditionalsOptions = () => [
|
||||
{
|
||||
value: 'and',
|
||||
label: intl.get('and'),
|
||||
text: intl.get('filter.all_filters_must_match'),
|
||||
},
|
||||
{
|
||||
value: 'or',
|
||||
label: intl.get('or'),
|
||||
text: intl.get('filter.atleast_one_filter_must_match'),
|
||||
},
|
||||
];
|
||||
|
||||
export const getBooleanCompatators = () => [
|
||||
{ value: 'is', label: intl.get('is') },
|
||||
{ value: 'is_not', label: intl.get('is_not') },
|
||||
];
|
||||
|
||||
export const getTextCompatators = () => [
|
||||
{ value: 'contain', label: intl.get('contain') },
|
||||
{ value: 'not_contain', label: intl.get('not_contain') },
|
||||
{ value: 'equal', label: intl.get('equals') },
|
||||
{ value: 'not_equal', label: intl.get('not_equals') },
|
||||
{ value: 'starts_with', label: intl.get('starts_with') },
|
||||
{ value: 'ends_with', label: intl.get('ends_with') },
|
||||
];
|
||||
|
||||
export const getDateCompatators = () => [
|
||||
{ value: 'in', label: intl.get('in') },
|
||||
{ value: 'after', label: intl.get('after') },
|
||||
{ value: 'before', label: intl.get('before') },
|
||||
];
|
||||
|
||||
export const getOptionsCompatators = () => [
|
||||
{ value: 'is', label: intl.get('is') },
|
||||
{ value: 'is_not', label: intl.get('is_not') },
|
||||
];
|
||||
|
||||
export const getNumberCampatators = () => [
|
||||
{ value: 'equal', label: intl.get('equals') },
|
||||
{ value: 'not_equal', label: intl.get('not_equal') },
|
||||
{ value: 'bigger_than', label: intl.get('bigger_than') },
|
||||
{ value: 'bigger_or_equal', label: intl.get('bigger_or_equals') },
|
||||
{ value: 'smaller_than', label: intl.get('smaller_than') },
|
||||
{ value: 'smaller_or_equal', label: intl.get('smaller_or_equals') },
|
||||
];
|
||||
|
||||
export const getConditionTypeCompatators = (
|
||||
dataType,
|
||||
) => {
|
||||
return [
|
||||
...(dataType === 'enumeration'
|
||||
? [...getOptionsCompatators()]
|
||||
: dataType === 'date'
|
||||
? [...getDateCompatators()]
|
||||
: dataType === 'boolean'
|
||||
? [...getBooleanCompatators()]
|
||||
: dataType === 'number'
|
||||
? [...getNumberCampatators()]
|
||||
: [...getTextCompatators()]),
|
||||
];
|
||||
};
|
||||
|
||||
export const getConditionDefaultCompatator = (
|
||||
dataType,
|
||||
) => {
|
||||
const compatators = getConditionTypeCompatators(dataType);
|
||||
return compatators[0];
|
||||
};
|
||||
|
||||
export const transformFieldsToOptions = (fields) =>
|
||||
fields.map((field) => ({
|
||||
value: field.key,
|
||||
label: field.name,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Filtered conditions that don't contain atleast on required fields or
|
||||
* fileds keys that not exists.
|
||||
* @param {IFilterRole[]} conditions
|
||||
* @returns
|
||||
*/
|
||||
export const filterConditionRoles = (
|
||||
conditions,
|
||||
) => {
|
||||
const requiredProps = ['fieldKey', 'condition', 'comparator', 'value'];
|
||||
|
||||
const filteredConditions = conditions.filter(
|
||||
(condition) =>
|
||||
!checkRequiredProperties(condition, requiredProps),
|
||||
);
|
||||
return uniqueMultiProps(filteredConditions, requiredProps);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines the value field when should update.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const shouldFilterValueFieldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.fieldKey !== oldProps.fieldKey ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
61
packages/webapp/src/components/Alert/index.tsx
Normal file
61
packages/webapp/src/components/Alert/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import clsx from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function Alert({ title, description, children, intent, className }) {
|
||||
return (
|
||||
<AlertRoot className={clsx(className)} intent={intent}>
|
||||
{title && <AlertTitle>{title}</AlertTitle>}
|
||||
{description && <AlertDesc>{description}</AlertDesc>}
|
||||
{children && <AlertDesc>{children}</AlertDesc>}
|
||||
</AlertRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const AlertRoot = styled.div`
|
||||
border: 1px solid rgb(223, 227, 230);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
${(props) =>
|
||||
props.intent === 'danger' &&
|
||||
`
|
||||
border-color: rgb(249, 198, 198);
|
||||
background: rgb(255, 248, 248);
|
||||
|
||||
${AlertDesc} {
|
||||
color: #d95759;
|
||||
}
|
||||
${AlertTitle} {
|
||||
color: rgb(205, 43, 49);
|
||||
}
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.intent === 'primary' &&
|
||||
`
|
||||
background: #fff;
|
||||
border-color: #98a8ee;
|
||||
|
||||
${AlertTitle} {
|
||||
color: #1a3bd4;
|
||||
}
|
||||
${AlertDesc} {
|
||||
color: #455883;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const AlertTitle = styled.h3`
|
||||
color: rgb(17, 24, 28);
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const AlertDesc = styled.p`
|
||||
color: rgb(104, 112, 118);
|
||||
margin: 0;
|
||||
`;
|
||||
64
packages/webapp/src/components/App.tsx
Normal file
64
packages/webapp/src/components/App.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Router, Switch, Route } from 'react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
|
||||
import '@/style/App.scss';
|
||||
import 'moment/locale/ar-ly';
|
||||
import 'moment/locale/es-us';
|
||||
|
||||
import AppIntlLoader from './AppIntlLoader';
|
||||
import PrivateRoute from '@/components/Guards/PrivateRoute';
|
||||
import GlobalErrors from '@/containers/GlobalErrors/GlobalErrors';
|
||||
import DashboardPrivatePages from '@/components/Dashboard/PrivatePages';
|
||||
import Authentication from '@/components/Authentication';
|
||||
|
||||
import { SplashScreen, DashboardThemeProvider } from '../components';
|
||||
import { queryConfig } from '../hooks/query/base';
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
*/
|
||||
function AppInsider({ history }) {
|
||||
return (
|
||||
<div className="App">
|
||||
<DashboardThemeProvider>
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'} component={Authentication} />
|
||||
<Route path={'/'}>
|
||||
<PrivateRoute component={DashboardPrivatePages} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
<GlobalErrors />
|
||||
</DashboardThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Core application.
|
||||
*/
|
||||
export default function App() {
|
||||
// Browser history.
|
||||
const history = createBrowserHistory();
|
||||
|
||||
// Query client.
|
||||
const queryClient = new QueryClient(queryConfig);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SplashScreen />
|
||||
|
||||
<AppIntlLoader>
|
||||
<AppInsider history={history} />
|
||||
</AppIntlLoader>
|
||||
|
||||
<ReactQueryDevtools initialIsOpen />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
157
packages/webapp/src/components/AppIntlLoader.tsx
Normal file
157
packages/webapp/src/components/AppIntlLoader.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { setLocale } from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { find } from 'lodash';
|
||||
import rtlDetect from 'rtl-detect';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { AppIntlProvider } from './AppIntlProvider';
|
||||
import { useSplashLoading } from '@/hooks/state';
|
||||
|
||||
import { useWatchImmediate } from '../hooks';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
|
||||
const SUPPORTED_LOCALES = [
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'العربية', value: 'ar' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Retrieve the current local.
|
||||
*/
|
||||
function getCurrentLocal() {
|
||||
let currentLocale = intl.determineLocale({
|
||||
urlLocaleKey: 'lang',
|
||||
cookieLocaleKey: 'locale',
|
||||
localStorageLocaleKey: 'lang',
|
||||
});
|
||||
if (!find(SUPPORTED_LOCALES, { value: currentLocale })) {
|
||||
currentLocale = 'en';
|
||||
}
|
||||
return currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the localization data of the given locale.
|
||||
*/
|
||||
function loadLocales(currentLocale) {
|
||||
return import(`../lang/${currentLocale}/index.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the localization data of yup validation library.
|
||||
*/
|
||||
function loadYupLocales(currentLocale) {
|
||||
return import(`../lang/${currentLocale}/locale`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the html document direction to RTl if it was rtl-language.
|
||||
*/
|
||||
function useDocumentDirectionModifier(locale, isRTL) {
|
||||
React.useEffect(() => {
|
||||
if (isRTL) {
|
||||
const htmlDocument = document.querySelector('html');
|
||||
htmlDocument.setAttribute('dir', 'rtl');
|
||||
htmlDocument.setAttribute('lang', locale);
|
||||
}
|
||||
}, [isRTL, locale]);
|
||||
}
|
||||
|
||||
function transformMomentLocale(currentLocale) {
|
||||
return currentLocale === 'ar' ? 'ar-ly' : currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads application locales of the given current locale.
|
||||
* @param {string} currentLocale
|
||||
* @returns {{ isLoading: boolean }}
|
||||
*/
|
||||
function useAppLoadLocales(currentLocale) {
|
||||
const [startLoading, stopLoading] = useSplashLoading();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Lodas the locales data file.
|
||||
loadLocales(currentLocale)
|
||||
.then((results) => {
|
||||
return intl.init({
|
||||
currentLocale,
|
||||
locales: {
|
||||
[currentLocale]: results,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
moment.locale(transformMomentLocale(currentLocale));
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [currentLocale, stopLoading]);
|
||||
|
||||
// Watches the value to start/stop splash screen.
|
||||
useWatchImmediate(
|
||||
(value) => (value ? startLoading() : stopLoading()),
|
||||
isLoading,
|
||||
);
|
||||
return { isLoading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads application yup locales based on the given current locale.
|
||||
* @param {string} currentLocale
|
||||
* @returns {{ isLoading: boolean }}
|
||||
*/
|
||||
function useAppYupLoadLocales(currentLocale) {
|
||||
const [startLoading, stopLoading] = useSplashLoading();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadYupLocales(currentLocale)
|
||||
.then(({ locale }) => {
|
||||
setLocale(locale);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.then(() => {});
|
||||
}, [currentLocale, stopLoading]);
|
||||
|
||||
// Watches the valiue to start/stop splash screen.
|
||||
useWatchImmediate(
|
||||
(value) => (value ? startLoading() : stopLoading()),
|
||||
isLoading,
|
||||
);
|
||||
return { isLoading };
|
||||
}
|
||||
|
||||
/**
|
||||
* Application Intl loader.
|
||||
*/
|
||||
function AppIntlLoader({ children }) {
|
||||
// Retrieve the current locale.
|
||||
const currentLocale = getCurrentLocal();
|
||||
|
||||
// Detarmines the document direction based on the given locale.
|
||||
const isRTL = rtlDetect.isRtlLang(currentLocale);
|
||||
|
||||
// Modifies the html document direction
|
||||
useDocumentDirectionModifier(currentLocale, isRTL);
|
||||
|
||||
// Loads yup localization of the given locale.
|
||||
const { isLoading: isAppYupLocalesLoading } =
|
||||
useAppYupLoadLocales(currentLocale);
|
||||
|
||||
// Loads application locales of the given locale.
|
||||
const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale);
|
||||
|
||||
// Detarmines whether the app locales loading.
|
||||
const isLoading = isAppYupLocalesLoading || isAppLocalesLoading;
|
||||
|
||||
return (
|
||||
<AppIntlProvider currentLocale={currentLocale} isRTL={isRTL}>
|
||||
{isLoading ? null : children}
|
||||
</AppIntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDashboardActions)(AppIntlLoader);
|
||||
26
packages/webapp/src/components/AppIntlProvider.tsx
Normal file
26
packages/webapp/src/components/AppIntlProvider.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const AppIntlContext = createContext();
|
||||
|
||||
/**
|
||||
* Application intl provider.
|
||||
*/
|
||||
function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
const provider = {
|
||||
currentLocale,
|
||||
isRTL,
|
||||
isLTR: !isRTL,
|
||||
direction: isRTL ? 'rtl' : 'ltr',
|
||||
};
|
||||
|
||||
return (
|
||||
<AppIntlContext.Provider value={provider}>
|
||||
{children}
|
||||
</AppIntlContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useAppIntlContext = () => React.useContext(AppIntlContext);
|
||||
|
||||
export { AppIntlProvider, useAppIntlContext };
|
||||
7
packages/webapp/src/components/AppToaster/index.tsx
Normal file
7
packages/webapp/src/components/AppToaster/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { Position, Toaster, Intent } from '@blueprintjs/core';
|
||||
|
||||
export const AppToaster = Toaster.create({
|
||||
position: Position.RIGHT_BOTTOM,
|
||||
intent: Intent.WARNING,
|
||||
});
|
||||
62
packages/webapp/src/components/Authentication.tsx
Normal file
62
packages/webapp/src/components/Authentication.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import authenticationRoutes from '@/routes/authentication';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import { useIsAuthenticated } from '@/hooks/state';
|
||||
import '@/style/pages/Authentication/Auth.scss';
|
||||
|
||||
function PageFade(props) {
|
||||
return <CSSTransition {...props} classNames="authTransition" timeout={500} />;
|
||||
}
|
||||
|
||||
export default function AuthenticationWrapper({ ...rest }) {
|
||||
const to = { pathname: '/' };
|
||||
const location = useLocation();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const locationKey = location.pathname;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated ? (
|
||||
<Redirect to={to} />
|
||||
) : (
|
||||
<BodyClassName className={'authentication'}>
|
||||
<div class="authentication-page">
|
||||
<a
|
||||
href={'http://bigcapital.ly'}
|
||||
className={'authentication-page__goto-bigcapital'}
|
||||
>
|
||||
<T id={'go_to_bigcapital_com'} />
|
||||
</a>
|
||||
|
||||
<div class="authentication-page__form-wrapper">
|
||||
<div class="authentication-insider">
|
||||
<div className={'authentication-insider__logo-section'}>
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</div>
|
||||
|
||||
<TransitionGroup>
|
||||
<PageFade key={locationKey}>
|
||||
<Switch>
|
||||
{authenticationRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</PageFade>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BodyClassName>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
11
packages/webapp/src/components/AvaterCell.tsx
Normal file
11
packages/webapp/src/components/AvaterCell.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { firstLettersArgs } from '@/utils';
|
||||
|
||||
export default function AvatarCell({ row: { original }, size }) {
|
||||
return (
|
||||
<span className="avatar" data-size={size}>
|
||||
{firstLettersArgs(original?.display_name)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
207
packages/webapp/src/components/BankAccounts/index.tsx
Normal file
207
packages/webapp/src/components/BankAccounts/index.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import styled from 'styled-components';
|
||||
import clsx from 'classnames';
|
||||
import { Classes } from '@blueprintjs/core';
|
||||
import { Icon } from '@/components/Icon';
|
||||
|
||||
const ACCOUNT_TYPE = {
|
||||
CASH: 'cash',
|
||||
BANK: 'bank',
|
||||
CREDIT_CARD: 'credit-card',
|
||||
};
|
||||
|
||||
const ACCOUNT_TYPE_PAIR_ICON = {
|
||||
[ACCOUNT_TYPE.CASH]: 'payments',
|
||||
[ACCOUNT_TYPE.CREDIT_CARD]: 'credit-card',
|
||||
[ACCOUNT_TYPE.BANK]: 'account-balance',
|
||||
};
|
||||
|
||||
function BankAccountMetaLine({ title, value, className }) {
|
||||
return (
|
||||
<MetaLineWrap className={className}>
|
||||
<MetaLineTitle>{title}</MetaLineTitle>
|
||||
{value && <MetaLineValue>{value}</MetaLineValue>}
|
||||
</MetaLineWrap>
|
||||
);
|
||||
}
|
||||
|
||||
function BankAccountBalance({ amount, loading }) {
|
||||
return (
|
||||
<BankAccountBalanceWrap>
|
||||
<BankAccountBalanceAmount
|
||||
className={clsx({
|
||||
[Classes.SKELETON]: loading,
|
||||
})}
|
||||
>
|
||||
{amount}
|
||||
</BankAccountBalanceAmount>
|
||||
<BankAccountBalanceLabel>{intl.get('balance')}</BankAccountBalanceLabel>
|
||||
</BankAccountBalanceWrap>
|
||||
);
|
||||
}
|
||||
|
||||
function BankAccountTypeIcon({ type }) {
|
||||
const icon = ACCOUNT_TYPE_PAIR_ICON[type];
|
||||
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<AccountIconWrap>
|
||||
<Icon icon={icon} iconSize={18} />
|
||||
</AccountIconWrap>
|
||||
);
|
||||
}
|
||||
|
||||
export function BankAccount({
|
||||
title,
|
||||
code,
|
||||
type,
|
||||
balance,
|
||||
loading = false,
|
||||
updatedBeforeText,
|
||||
...restProps
|
||||
}) {
|
||||
return (
|
||||
<BankAccountWrap {...restProps}>
|
||||
<BankAccountHeader>
|
||||
<BankAccountTitle className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{title}
|
||||
</BankAccountTitle>
|
||||
<BnakAccountCode className={clsx({ [Classes.SKELETON]: loading })}>
|
||||
{code}
|
||||
</BnakAccountCode>
|
||||
{!loading && <BankAccountTypeIcon type={type} />}
|
||||
</BankAccountHeader>
|
||||
|
||||
<BankAccountMeta>
|
||||
<BankAccountMetaLine
|
||||
title={intl.get('cash_flow.label_account_transcations')}
|
||||
value={2}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
<BankAccountMetaLine
|
||||
title={updatedBeforeText}
|
||||
className={clsx({ [Classes.SKELETON]: loading })}
|
||||
/>
|
||||
</BankAccountMeta>
|
||||
|
||||
<BankAccountBalance amount={balance} loading={loading} />
|
||||
</BankAccountWrap>
|
||||
);
|
||||
}
|
||||
|
||||
const BankAccountWrap = styled.div`
|
||||
width: 225px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
margin: 8px;
|
||||
border: 1px solid #c8cad0;
|
||||
transition: all 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: #0153cc;
|
||||
}
|
||||
`;
|
||||
|
||||
const BankAccountHeader = styled.div`
|
||||
padding: 10px 12px;
|
||||
padding-top: 16px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const BankAccountTitle = styled.div`
|
||||
font-size: 15px;
|
||||
font-style: inherit;
|
||||
letter-spacing: -0.003em;
|
||||
color: rgb(23, 43, 77);
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0px;
|
||||
padding-right: 24px;
|
||||
`;
|
||||
|
||||
const BnakAccountCode = styled.div`
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
color: rgb(23, 43, 77);
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const BankAccountBalanceWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: auto;
|
||||
border-top: 1px solid #dfdfdf;
|
||||
padding: 10px 12px;
|
||||
`;
|
||||
|
||||
const BankAccountBalanceAmount = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #57657e;
|
||||
`;
|
||||
|
||||
const BankAccountBalanceLabel = styled.div`
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 3px;
|
||||
opacity: 0.6;
|
||||
`;
|
||||
|
||||
const MetaLineWrap = styled.div`
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
color: #2f3c58;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 6px;
|
||||
}
|
||||
`;
|
||||
const MetaLineTitle = styled.div``;
|
||||
|
||||
const MetaLineValue = styled.div`
|
||||
box-sizing: border-box;
|
||||
font-style: inherit;
|
||||
background: rgb(223, 225, 230);
|
||||
line-height: initial;
|
||||
align-content: center;
|
||||
padding: 0px 2px;
|
||||
border-radius: 9.6px;
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
color: rgb(23, 43, 77);
|
||||
font-size: 11px;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const BankAccountMeta = styled.div`
|
||||
padding: 0 12px 10px;
|
||||
`;
|
||||
|
||||
export const BankAccountsList = styled.div`
|
||||
display: flex;
|
||||
margin: -8px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const AccountIconWrap = styled.div`
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
color: #abb3bb;
|
||||
right: 12px;
|
||||
`;
|
||||
@@ -0,0 +1,73 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { FMultiSelect } from '../Forms';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} branch
|
||||
* @param {*} _index
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const branchItemPredicate = (query, branch, _index, exactMatch) => {
|
||||
const normalizedTitle = branch.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} branch
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const branchItemRenderer = (
|
||||
branch,
|
||||
{ handleClick, modifiers, query },
|
||||
{ isSelected },
|
||||
) => {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
icon={isSelected ? 'tick' : 'blank'}
|
||||
text={branch.name}
|
||||
label={branch.code}
|
||||
key={branch.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const branchSelectProps = {
|
||||
itemPredicate: branchItemPredicate,
|
||||
itemRenderer: branchItemRenderer,
|
||||
valueAccessor: (item) => item.id,
|
||||
labelAccessor: (item) => item.code,
|
||||
tagRenderer: (item) => item.name,
|
||||
};
|
||||
|
||||
/**
|
||||
* branches mulit select.
|
||||
* @param {*} param0
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function BranchMultiSelect({ branches, ...rest }) {
|
||||
return (
|
||||
<FMultiSelect
|
||||
items={branches}
|
||||
placeholder={intl.get('branches_multi_select.placeholder')}
|
||||
popoverProps={{ minimal: true }}
|
||||
{...branchSelectProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
70
packages/webapp/src/components/Branches/BranchSelect.tsx
Normal file
70
packages/webapp/src/components/Branches/BranchSelect.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { FSelect } from '../Forms';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} branch
|
||||
* @param {*} _index
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const branchItemPredicate = (query, branch, _index, exactMatch) => {
|
||||
const normalizedTitle = branch.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} film
|
||||
* @param {*} param1
|
||||
* @returns
|
||||
*/
|
||||
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
|
||||
const text = `${branch.name}`;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
label={branch.code}
|
||||
key={branch.id}
|
||||
onClick={handleClick}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const branchSelectProps = {
|
||||
itemPredicate: branchItemPredicate,
|
||||
itemRenderer: branchItemRenderer,
|
||||
valueAccessor: 'id',
|
||||
labelAccessor: 'name',
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function BranchSelect({ branches, ...rest }) {
|
||||
return <FSelect {...branchSelectProps} {...rest} items={branches} />;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export function BranchSelectButton({ label, ...rest }) {
|
||||
return <Button text={label} {...rest} />;
|
||||
}
|
||||
125
packages/webapp/src/components/Branches/BranchSuggestField.tsx
Normal file
125
packages/webapp/src/components/Branches/BranchSuggestField.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
/**
|
||||
* branch suggest field.
|
||||
* @returns
|
||||
*/
|
||||
export function BranchSuggestField({
|
||||
branches,
|
||||
initialBranchId,
|
||||
selectedBranchId,
|
||||
defaultSelectText = intl.get('select_branch'),
|
||||
popoverFill = false,
|
||||
onBranchSelected,
|
||||
...suggestProps
|
||||
}) {
|
||||
const initialBranch = React.useMemo(
|
||||
() => branches.find((b) => b.id === initialBranchId),
|
||||
[initialBranchId, branches],
|
||||
);
|
||||
|
||||
const [selectedBranch, setSelectedBranch] = React.useState(
|
||||
initialBranch || null,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof selectedBranchId !== 'undefined') {
|
||||
const branch = selectedBranchId
|
||||
? branches.find((a) => a.id === selectedBranchId)
|
||||
: null;
|
||||
setSelectedBranch(branch);
|
||||
}
|
||||
}, [selectedBranchId, branches, setSelectedBranch]);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} branch
|
||||
* @returns
|
||||
*/
|
||||
const branchItemRenderer = (branch, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
// active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
label={branch.code}
|
||||
key={branch.id}
|
||||
onClick={handleClick}
|
||||
text={branch.name}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} branch
|
||||
* @param {*} _index
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const branchItemPredicate = (query, branch, _index, exactMatch) => {
|
||||
const normalizedTitle = branch.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${branch.code}. ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} branch
|
||||
* @returns
|
||||
*/
|
||||
const brnachItemSelect = React.useCallback(
|
||||
(branch) => {
|
||||
if (branch.id) {
|
||||
setSelectedBranch({ ...branch });
|
||||
onBranchSelected && onBranchSelected(branch);
|
||||
}
|
||||
},
|
||||
[setSelectedBranch, onBranchSelected],
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} inputVaue
|
||||
* @returns
|
||||
*/
|
||||
const branchInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.name.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Suggest
|
||||
items={branches}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
|
||||
itemRenderer={branchItemRenderer}
|
||||
itemPredicate={branchItemPredicate}
|
||||
onItemSelect={brnachItemSelect}
|
||||
selectedItem={selectedBranch}
|
||||
inputProps={{ placeholder: defaultSelectText }}
|
||||
resetOnClose={true}
|
||||
fill={true}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputValueRenderer={branchInputValueRenderer}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
4
packages/webapp/src/components/Branches/index.tsx
Normal file
4
packages/webapp/src/components/Branches/index.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
// @ts-nocheck
|
||||
export * from './BranchSelect';
|
||||
export * from './BranchMultiSelect';
|
||||
export * from './BranchSuggestField';
|
||||
15
packages/webapp/src/components/Button/ButtonLink.tsx
Normal file
15
packages/webapp/src/components/Button/ButtonLink.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ButtonLink = styled.button`
|
||||
color: #0052cc;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: inherit;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
1
packages/webapp/src/components/Button/index.tsx
Normal file
1
packages/webapp/src/components/Button/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export * from './ButtonLink';
|
||||
28
packages/webapp/src/components/Card/index.tsx
Normal file
28
packages/webapp/src/components/Card/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export function Card({ className, children }) {
|
||||
return <CardRoot className={className}>{children}</CardRoot>;
|
||||
}
|
||||
|
||||
const CardRoot = styled.div`
|
||||
padding: 15px;
|
||||
margin: 15px;
|
||||
background: #fff;
|
||||
border: 1px solid #d2dce2;
|
||||
`;
|
||||
|
||||
export const CardFooterActions = styled.div`
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp3-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp3-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
25
packages/webapp/src/components/CommercialDoc/index.tsx
Normal file
25
packages/webapp/src/components/CommercialDoc/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
import { Card } from '../Card';
|
||||
import { DataTable } from '../Datatable';
|
||||
|
||||
export const CommercialDocBox = styled(Card)`
|
||||
padding: 22px 20px;
|
||||
`;
|
||||
|
||||
export const CommercialDocHeader = styled.div`
|
||||
margin-bottom: 25px;
|
||||
`;
|
||||
|
||||
export const CommercialDocTopHeader = styled.div`
|
||||
margin-bottom: 30px;
|
||||
`;
|
||||
|
||||
export const CommercialDocEntriesTable = styled(DataTable)`
|
||||
.tbody .tr:last-child .td {
|
||||
border-bottom: 1px solid #d2dce2;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CommercialDocFooter = styled.div`
|
||||
margin-top: 28px;
|
||||
`;
|
||||
111
packages/webapp/src/components/Contacts/ContactSelecetList.tsx
Normal file
111
packages/webapp/src/components/Contacts/ContactSelecetList.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
export function ContactSelecetList({
|
||||
contactsList,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
createNewItemFrom,
|
||||
defaultSelectText = <T id={'select_contact'} />,
|
||||
onContactSelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
buttonProps,
|
||||
|
||||
...restProps
|
||||
}) {
|
||||
const contacts = useMemo(
|
||||
() =>
|
||||
contactsList.map((contact) => ({
|
||||
...contact,
|
||||
_id: `${contact.id}_${contact.contact_type}`,
|
||||
})),
|
||||
[contactsList],
|
||||
);
|
||||
|
||||
const initialContact = useMemo(
|
||||
() => contacts.find((a) => a.id === initialContactId),
|
||||
[initialContactId, contacts],
|
||||
);
|
||||
|
||||
const [selecetedContact, setSelectedContact] = useState(
|
||||
initialContact || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedContactId !== 'undefined') {
|
||||
const account = selectedContactId
|
||||
? contacts.find((a) => a.id === selectedContactId)
|
||||
: null;
|
||||
setSelectedContact(account);
|
||||
}
|
||||
}, [selectedContactId, contacts, setSelectedContact]);
|
||||
|
||||
const handleContactRenderer = useCallback(
|
||||
(contact, { handleClick }) => (
|
||||
<MenuItem
|
||||
key={contact.id}
|
||||
text={contact.display_name}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const onContactSelect = useCallback(
|
||||
(contact) => {
|
||||
setSelectedContact({ ...contact });
|
||||
onContactSelected && onContactSelected(contact);
|
||||
},
|
||||
[setSelectedContact, onContactSelected],
|
||||
);
|
||||
|
||||
// Filter Contact List
|
||||
const itemPredicate = (query, contact, index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
|
||||
0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={contacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={handleContactRenderer}
|
||||
itemPredicate={itemPredicate}
|
||||
filterable={true}
|
||||
disabled={disabled}
|
||||
onItemSelect={onContactSelect}
|
||||
popoverProps={{ minimal: true, usePortal: !popoverFill }}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
inputProps={{
|
||||
placeholder: intl.get('filter_'),
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={
|
||||
selecetedContact ? selecetedContact.display_name : defaultSelectText
|
||||
}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import { itemPredicate, handleContactRenderer } from './utils';
|
||||
|
||||
export function ContactSelectField({
|
||||
contacts,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
defaultSelectText = <T id={'select_contact'} />,
|
||||
onContactSelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
buttonProps,
|
||||
|
||||
...restProps
|
||||
}) {
|
||||
const localContacts = useMemo(
|
||||
() =>
|
||||
contacts.map((contact) => ({
|
||||
...contact,
|
||||
_id: `${contact.id}_${contact.contact_type}`,
|
||||
})),
|
||||
[contacts],
|
||||
);
|
||||
|
||||
const initialContact = useMemo(
|
||||
() => contacts.find((a) => a.id === initialContactId),
|
||||
[initialContactId, contacts],
|
||||
);
|
||||
|
||||
const [selecetedContact, setSelectedContact] = useState(
|
||||
initialContact || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedContactId !== 'undefined') {
|
||||
const account = selectedContactId
|
||||
? contacts.find((a) => a.id === selectedContactId)
|
||||
: null;
|
||||
setSelectedContact(account);
|
||||
}
|
||||
}, [selectedContactId, contacts, setSelectedContact]);
|
||||
|
||||
const handleContactSelect = useCallback(
|
||||
(contact) => {
|
||||
setSelectedContact({ ...contact });
|
||||
onContactSelected && onContactSelected(contact);
|
||||
},
|
||||
[setSelectedContact, onContactSelected],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={localContacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={handleContactRenderer}
|
||||
itemPredicate={itemPredicate}
|
||||
filterable={true}
|
||||
disabled={disabled}
|
||||
onItemSelect={handleContactSelect}
|
||||
popoverProps={{ minimal: true, usePortal: !popoverFill }}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
inputProps={{
|
||||
placeholder: intl.get('filter_'),
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={
|
||||
selecetedContact ? selecetedContact.display_name : defaultSelectText
|
||||
}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { MultiSelect } from '../MultiSelectTaggable';
|
||||
|
||||
/**
|
||||
* Contacts multi-select component.
|
||||
*/
|
||||
export function ContactsMultiSelect({ ...multiSelectProps }) {
|
||||
// Filters accounts items.
|
||||
const filterContactsPredicater = useCallback(
|
||||
(query, contact, _index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return normalizedTitle.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
itemRenderer={(contact, { selected, active, handleClick }) => (
|
||||
<MenuItem
|
||||
active={active}
|
||||
icon={selected ? 'tick' : 'blank'}
|
||||
text={contact.display_name}
|
||||
key={contact.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)}
|
||||
popoverProps={{ minimal: true }}
|
||||
fill={true}
|
||||
itemPredicate={filterContactsPredicater}
|
||||
tagRenderer={(item) => item.display_name}
|
||||
{...multiSelectProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
108
packages/webapp/src/components/Contacts/ContactsSuggestField.tsx
Normal file
108
packages/webapp/src/components/Contacts/ContactsSuggestField.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export function ContactsSuggestField({
|
||||
contactsList,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
defaultTextSelect = intl.get('select_contact'),
|
||||
onContactSelected,
|
||||
|
||||
selectedContactType = [],
|
||||
popoverFill = false,
|
||||
|
||||
...suggestProps
|
||||
}) {
|
||||
// filteredContacts
|
||||
const contacts = useMemo(
|
||||
() =>
|
||||
contactsList.map((contact) => ({
|
||||
...contact,
|
||||
_id: `${contact.id}_${contact.contact_type}`,
|
||||
})),
|
||||
[contactsList],
|
||||
);
|
||||
|
||||
const initialContact = useMemo(
|
||||
() => contacts.find((a) => a.id === initialContactId),
|
||||
[initialContactId, contacts],
|
||||
);
|
||||
|
||||
const [selecetedContact, setSelectedContact] = useState(
|
||||
initialContact || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedContactId !== 'undefined') {
|
||||
const contact = selectedContactId
|
||||
? contacts.find((a) => a.id === selectedContactId)
|
||||
: null;
|
||||
setSelectedContact(contact);
|
||||
}
|
||||
}, [selectedContactId, contacts, setSelectedContact]);
|
||||
|
||||
const contactRenderer = useCallback(
|
||||
(contact, { handleClick }) => (
|
||||
<MenuItem
|
||||
key={contact.id}
|
||||
text={contact.display_name}
|
||||
label={contact.formatted_contact_service}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const onContactSelect = useCallback(
|
||||
(contact) => {
|
||||
setSelectedContact({ ...contact });
|
||||
onContactSelected && onContactSelected(contact);
|
||||
},
|
||||
[setSelectedContact, onContactSelected],
|
||||
);
|
||||
|
||||
const handleInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.display_name.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const filterContacts = (query, contact, index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
|
||||
0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Suggest
|
||||
items={contacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={contactRenderer}
|
||||
itemPredicate={filterContacts}
|
||||
onItemSelect={onContactSelect}
|
||||
selectedItem={selecetedContact}
|
||||
inputProps={{ placeholder: defaultTextSelect }}
|
||||
resetOnClose={true}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputValueRenderer={handleInputValueRenderer}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
packages/webapp/src/components/Contacts/index.tsx
Normal file
5
packages/webapp/src/components/Contacts/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-nocheck
|
||||
export * from './ContactSelectField';
|
||||
export * from './ContactsSuggestField';
|
||||
export * from './ContactSelecetList';
|
||||
export * from './ContactsMultiSelect';
|
||||
45
packages/webapp/src/components/Contacts/utils.tsx
Normal file
45
packages/webapp/src/components/Contacts/utils.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
// Filter Contact List
|
||||
export const itemPredicate = (query, contact, index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleContactRenderer = (contact, { handleClick }) => (
|
||||
<MenuItem
|
||||
key={contact.id}
|
||||
text={contact.display_name}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
// Creates a new item from query.
|
||||
export const createNewItemFromQuery = (name) => {
|
||||
return {
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
// Handle quick create new customer.
|
||||
export const createNewItemRenderer = (query, active, handleClick) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon="add"
|
||||
text={intl.get('list.create', { value: `"${query}"` })}
|
||||
active={active}
|
||||
shouldDismissPopover={false}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
53
packages/webapp/src/components/ContextMenu.tsx
Normal file
53
packages/webapp/src/components/ContextMenu.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
import React, { memo } from 'react';
|
||||
import { Popover, Position, Classes } from '@blueprintjs/core';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
const POPPER_MODIFIERS = {
|
||||
preventOverflow: { boundariesElement: 'viewport' },
|
||||
};
|
||||
|
||||
function ContextMenu(props) {
|
||||
const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
|
||||
|
||||
const handleClosed = () => {
|
||||
requestAnimationFrame(() => saveInvoke(onClosed));
|
||||
};
|
||||
|
||||
const handleInteraction = (nextOpenState) => {
|
||||
if (!nextOpenState) {
|
||||
// Delay the actual hiding till the event queue clears
|
||||
// to avoid flicker of opening twice
|
||||
requestAnimationFrame(() => saveInvoke(onClosed));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} {...bindMenu}>
|
||||
<Popover
|
||||
onClosed={handleClosed}
|
||||
modifiers={POPPER_MODIFIERS}
|
||||
content={children}
|
||||
enforceFocus={true}
|
||||
isOpen={isOpen}
|
||||
minimal={true}
|
||||
position={Position.RIGHT_TOP}
|
||||
target={<div />}
|
||||
usePortal={false}
|
||||
onInteraction={handleInteraction}
|
||||
{...popoverProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ContextMenu, (prevProps, nextProps) => {
|
||||
if (
|
||||
prevProps.isOpen === nextProps.isOpen &&
|
||||
prevProps.bindMenu.style === nextProps.bindMenu.style
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
11
packages/webapp/src/components/Currencies/BaseCurrency.tsx
Normal file
11
packages/webapp/src/components/Currencies/BaseCurrency.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { CurrencyTag } from '@/components';
|
||||
|
||||
/**
|
||||
* base currecncy.
|
||||
* @returns
|
||||
*/
|
||||
export function BaseCurrency({ currency }) {
|
||||
return <CurrencyTag>{currency}</CurrencyTag>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
|
||||
export function CurrenciesSelectList({ selectProps, onItemSelect, className }) {
|
||||
const currencies = [
|
||||
{
|
||||
name: 'USD US dollars',
|
||||
key: 'USD',
|
||||
name: 'CAD Canadian dollars',
|
||||
key: 'CAD',
|
||||
},
|
||||
];
|
||||
|
||||
// Handle currency item select.
|
||||
const onCurrencySelect = useCallback(
|
||||
(currency) => {
|
||||
onItemSelect && onItemSelect(currency);
|
||||
},
|
||||
[onItemSelect],
|
||||
);
|
||||
|
||||
// Filters currencies list.
|
||||
const filterCurrenciesPredicator = useCallback(
|
||||
(query, currency, _index, exactMatch) => {
|
||||
const normalizedTitle = currency.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
return `${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Currency item of select currencies field.
|
||||
const currencyItem = (item, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
text={item.name}
|
||||
label={item.code}
|
||||
key={item.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={currencies}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
itemRenderer={currencyItem}
|
||||
itemPredicate={filterCurrenciesPredicator}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onCurrencySelect}
|
||||
{...selectProps}
|
||||
>
|
||||
<Button text={'USD US dollars'} />
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
77
packages/webapp/src/components/Currencies/CurrencySelect.tsx
Normal file
77
packages/webapp/src/components/Currencies/CurrencySelect.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { FSelect } from '../Forms';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} query
|
||||
* @param {*} currency
|
||||
* @param {*} _index
|
||||
* @param {*} exactMatch
|
||||
* @returns
|
||||
*/
|
||||
const currencyItemPredicate = (query, currency, _index, exactMatch) => {
|
||||
const normalizedTitle = currency.currency_code.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${currency.currency_code}. ${normalizedTitle}`.indexOf(
|
||||
normalizedQuery,
|
||||
) >= 0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {*} currency
|
||||
* @returns
|
||||
*/
|
||||
const currencyItemRenderer = (currency, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={currency.currency_name}
|
||||
label={currency.currency_code.toString()}
|
||||
key={currency.id}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const currencySelectProps = {
|
||||
itemPredicate: currencyItemPredicate,
|
||||
itemRenderer: currencyItemRenderer,
|
||||
valueAccessor: 'currency_code',
|
||||
labelAccessor: 'currency_code',
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} currencies
|
||||
* @returns
|
||||
*/
|
||||
export function CurrencySelect({ currencies, ...rest }) {
|
||||
return (
|
||||
<FSelect
|
||||
{...currencySelectProps}
|
||||
{...rest}
|
||||
items={currencies}
|
||||
input={CurrnecySelectButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} label
|
||||
* @returns
|
||||
*/
|
||||
function CurrnecySelectButton({ label }) {
|
||||
return <Button text={label ? label : intl.get('select_currency_code')} />;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import classNames from 'classnames';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
|
||||
export function CurrencySelectList({
|
||||
currenciesList,
|
||||
selectedCurrencyCode,
|
||||
defaultSelectText = <T id={'select_currency_code'} />,
|
||||
onCurrencySelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
}) {
|
||||
const [selectedCurrency, setSelectedCurrency] = useState(null);
|
||||
|
||||
// Filters currencies list.
|
||||
const filterCurrencies = (query, currency, _index, exactMatch) => {
|
||||
const normalizedTitle = currency.currency_code.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${currency.currency_code} ${normalizedTitle}`.indexOf(
|
||||
normalizedQuery,
|
||||
) >= 0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onCurrencySelect = useCallback((currency) => {
|
||||
setSelectedCurrency({ ...currency });
|
||||
onCurrencySelected && onCurrencySelected(currency);
|
||||
});
|
||||
|
||||
const currencyCodeRenderer = useCallback((CurrencyCode, { handleClick }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={CurrencyCode.id}
|
||||
text={CurrencyCode.currency_code}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedCurrencyCode !== 'undefined') {
|
||||
const currency = selectedCurrencyCode
|
||||
? currenciesList.find((a) => a.currency_code === selectedCurrencyCode)
|
||||
: null;
|
||||
setSelectedCurrency(currency);
|
||||
}
|
||||
}, [selectedCurrencyCode, currenciesList, setSelectedCurrency]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={currenciesList}
|
||||
itemRenderer={currencyCodeRenderer}
|
||||
itemPredicate={filterCurrencies}
|
||||
onItemSelect={onCurrencySelect}
|
||||
filterable={true}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
usePortal: !popoverFill,
|
||||
inline: popoverFill,
|
||||
}}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={
|
||||
selectedCurrency ? selectedCurrency.currency_code : defaultSelectText
|
||||
}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
5
packages/webapp/src/components/Currencies/index.tsx
Normal file
5
packages/webapp/src/components/Currencies/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-nocheck
|
||||
export * from './CurrencySelect';
|
||||
export * from './BaseCurrency';
|
||||
export * from './CurrenciesSelectList';
|
||||
export * from './CurrencySelectList';
|
||||
@@ -0,0 +1,32 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { ButtonLink } from '../Button';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
function CustomerDrawerLinkComponent({
|
||||
// #ownProps
|
||||
children,
|
||||
customerId,
|
||||
className,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
// Handle view customer drawer.
|
||||
const handleCustomerDrawer = (event) => {
|
||||
openDrawer('customer-detail-drawer', { customerId });
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonLink className={className} onClick={handleCustomerDrawer}>
|
||||
{children}
|
||||
</ButtonLink>
|
||||
);
|
||||
}
|
||||
|
||||
export const CustomerDrawerLink = R.compose(withDrawerActions)(
|
||||
CustomerDrawerLinkComponent,
|
||||
);
|
||||
119
packages/webapp/src/components/Customers/CustomerSelectField.tsx
Normal file
119
packages/webapp/src/components/Customers/CustomerSelectField.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import {
|
||||
itemPredicate,
|
||||
handleContactRenderer,
|
||||
createNewItemRenderer,
|
||||
createNewItemFromQuery,
|
||||
} from './utils';
|
||||
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
function CustomerSelectFieldRoot({
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
|
||||
// #ownProps
|
||||
contacts,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
defaultSelectText = <T id={'select_contact'} />,
|
||||
onContactSelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
allowCreate,
|
||||
buttonProps,
|
||||
|
||||
...restProps
|
||||
}) {
|
||||
const localContacts = useMemo(
|
||||
() =>
|
||||
contacts.map((contact) => ({
|
||||
...contact,
|
||||
_id: `${contact.id}_${contact.contact_type}`,
|
||||
})),
|
||||
[contacts],
|
||||
);
|
||||
|
||||
const initialContact = useMemo(
|
||||
() => contacts.find((a) => a.id === initialContactId),
|
||||
[initialContactId, contacts],
|
||||
);
|
||||
|
||||
const [selecetedContact, setSelectedContact] = useState(
|
||||
initialContact || null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof selectedContactId !== 'undefined') {
|
||||
const account = selectedContactId
|
||||
? contacts.find((a) => a.id === selectedContactId)
|
||||
: null;
|
||||
setSelectedContact(account);
|
||||
}
|
||||
}, [selectedContactId, contacts, setSelectedContact]);
|
||||
|
||||
const handleContactSelect = useCallback(
|
||||
(contact) => {
|
||||
if (contact.id) {
|
||||
setSelectedContact({ ...contact });
|
||||
onContactSelected && onContactSelected(contact);
|
||||
} else {
|
||||
openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
|
||||
}
|
||||
},
|
||||
[setSelectedContact, onContactSelected, openDrawer],
|
||||
);
|
||||
|
||||
// Maybe inject create new item props to suggest component.
|
||||
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
|
||||
const maybeCreateNewItemFromQuery = allowCreate
|
||||
? createNewItemFromQuery
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={localContacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={handleContactRenderer}
|
||||
itemPredicate={itemPredicate}
|
||||
filterable={true}
|
||||
disabled={disabled}
|
||||
onItemSelect={handleContactSelect}
|
||||
popoverProps={{ minimal: true, usePortal: !popoverFill }}
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
inputProps={{
|
||||
placeholder: intl.get('filter_'),
|
||||
}}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
createNewItemPosition={'top'}
|
||||
{...restProps}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
text={
|
||||
selecetedContact ? selecetedContact.display_name : defaultSelectText
|
||||
}
|
||||
{...buttonProps}
|
||||
/>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export const CustomerSelectField = R.compose(withDrawerActions)(
|
||||
CustomerSelectFieldRoot,
|
||||
);
|
||||
3
packages/webapp/src/components/Customers/index.tsx
Normal file
3
packages/webapp/src/components/Customers/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
// @ts-nocheck
|
||||
export * from './CustomerSelectField';
|
||||
export * from './CustomerDrawerLink';
|
||||
45
packages/webapp/src/components/Customers/utils.tsx
Normal file
45
packages/webapp/src/components/Customers/utils.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
// Filter Contact List
|
||||
export const itemPredicate = (query, contact, index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return (
|
||||
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleContactRenderer = (contact, { handleClick }) => (
|
||||
<MenuItem
|
||||
key={contact.id}
|
||||
text={contact.display_name}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
// Creates a new item from query.
|
||||
export const createNewItemFromQuery = (name) => {
|
||||
return {
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
// Handle quick create new customer.
|
||||
export const createNewItemRenderer = (query, active, handleClick) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon="add"
|
||||
text={intl.get('list.create', { value: `"${query}"` })}
|
||||
active={active}
|
||||
shouldDismissPopover={false}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
import '@/style/components/BigcapitalLoading.scss';
|
||||
|
||||
/**
|
||||
* Bigcapital logo loading.
|
||||
*/
|
||||
export default function BigcapitalLoading({ className }) {
|
||||
return (
|
||||
<div className={classNames('bigcapital-loading', className)}>
|
||||
<div class="center">
|
||||
<Icon icon="bigcapital" height={37} width={228} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
packages/webapp/src/components/Dashboard/Dashboard.tsx
Normal file
63
packages/webapp/src/components/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
|
||||
import '@/style/pages/Dashboard/Dashboard.scss';
|
||||
|
||||
import { Sidebar } from '@/containers/Dashboard/Sidebar/Sidebar';
|
||||
import DashboardContent from '@/components/Dashboard/DashboardContent';
|
||||
import DialogsContainer from '@/components/DialogsContainer';
|
||||
import PreferencesPage from '@/components/Preferences/PreferencesPage';
|
||||
import DashboardUniversalSearch from '@/containers/UniversalSearch/DashboardUniversalSearch';
|
||||
import DashboardSplitPane from '@/components/Dashboard/DashboardSplitePane';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
import DashboardProvider from './DashboardProvider';
|
||||
import DrawersContainer from '@/components/DrawersContainer';
|
||||
import AlertsContainer from '@/containers/AlertsContainer';
|
||||
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
|
||||
|
||||
/**
|
||||
* Dashboard preferences.
|
||||
*/
|
||||
function DashboardPreferences() {
|
||||
return (
|
||||
<EnsureSubscriptionIsActive>
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesPage />
|
||||
</DashboardSplitPane>
|
||||
</EnsureSubscriptionIsActive>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard other routes.
|
||||
*/
|
||||
function DashboardAnyPage() {
|
||||
return (
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<DashboardContent />
|
||||
</DashboardSplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard page.
|
||||
*/
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<DashboardProvider>
|
||||
<Switch>
|
||||
<Route path="/preferences" component={DashboardPreferences} />
|
||||
<Route path="/" component={DashboardAnyPage} />
|
||||
</Switch>
|
||||
|
||||
<DashboardUniversalSearch />
|
||||
<DialogsContainer />
|
||||
<GlobalHotkeys />
|
||||
<DrawersContainer />
|
||||
<AlertsContainer />
|
||||
</DashboardProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Ability } from '@casl/ability';
|
||||
import { createContextualCan } from '@casl/react';
|
||||
|
||||
import { useDashboardMetaBoot } from './DashboardBoot';
|
||||
|
||||
export const AbilityContext = React.createContext();
|
||||
export const Can = createContextualCan(AbilityContext.Consumer);
|
||||
|
||||
/**
|
||||
* Dashboard ability provider.
|
||||
*/
|
||||
export function DashboardAbilityProvider({ children }) {
|
||||
const {
|
||||
meta: { abilities },
|
||||
} = useDashboardMetaBoot();
|
||||
|
||||
// Ability instance.
|
||||
const ability = new Ability(abilities);
|
||||
|
||||
return (
|
||||
<AbilityContext.Provider value={ability}>
|
||||
{children}
|
||||
</AbilityContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
MenuItem,
|
||||
Menu,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Divider,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { Icon } from '@/components';
|
||||
|
||||
/**
|
||||
* Dashboard action views list.
|
||||
*/
|
||||
export function DashboardActionViewsList({
|
||||
resourceName,
|
||||
allMenuItem,
|
||||
allMenuItemText,
|
||||
views,
|
||||
onChange,
|
||||
}) {
|
||||
const handleClickViewItem = (view) => {
|
||||
onChange && onChange(view);
|
||||
};
|
||||
|
||||
const viewsMenuItems = views.map((view) => (
|
||||
<MenuItem onClick={() => handleClickViewItem(view)} text={view.name} />
|
||||
));
|
||||
|
||||
const handleAllTabClick = () => {
|
||||
handleClickViewItem(null);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Menu>
|
||||
{allMenuItem && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={handleAllTabClick}
|
||||
text={allMenuItemText || 'All'}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{viewsMenuItems}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={content}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||
icon={<Icon icon="table-16" iconSize={16} />}
|
||||
text={<T id={'table_views'} />}
|
||||
rightIcon={'caret-down'}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { Navbar } from '@blueprintjs/core';
|
||||
|
||||
export function DashboardActionsBar({ className, children, name }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
{
|
||||
'dashboard__actions-bar': true,
|
||||
[`dashboard__actions-bar--${name}`]: !!name,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Navbar className="navbar--dashboard-actions-bar">{children}</Navbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import { If, Icon } from '@/components';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
function DashboardBackLink({ dashboardBackLink, breadcrumbs }) {
|
||||
const history = useHistory();
|
||||
const crumb = breadcrumbs[breadcrumbs.length - 2];
|
||||
|
||||
const handleClick = (event) => {
|
||||
const url =
|
||||
typeof dashboardBackLink === 'string'
|
||||
? dashboardBackLink
|
||||
: crumb.match.url;
|
||||
history.push(url);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<If condition={dashboardBackLink && crumb}>
|
||||
<div class="dashboard__back-link">
|
||||
<a href="#no-link" onClick={handleClick}>
|
||||
<Icon icon={'arrow-left'} iconSize={18} /> <T id={'back_to_list'} />
|
||||
</a>
|
||||
</div>
|
||||
</If>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withBreadcrumbs([]),
|
||||
withDashboard(({ dashboardBackLink }) => ({
|
||||
dashboardBackLink,
|
||||
})),
|
||||
)(DashboardBackLink);
|
||||
129
packages/webapp/src/components/Dashboard/DashboardBoot.tsx
Normal file
129
packages/webapp/src/components/Dashboard/DashboardBoot.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
useAuthenticatedAccount,
|
||||
useCurrentOrganization,
|
||||
useDashboardMeta,
|
||||
} from '@/hooks/query';
|
||||
import { useSplashLoading } from '@/hooks/state';
|
||||
import { useWatch, useWatchImmediate, useWhen } from '@/hooks';
|
||||
import { useSubscription } from '@/hooks/state';
|
||||
import { setCookie, getCookie } from '@/utils';
|
||||
|
||||
/**
|
||||
* Dashboard meta async booting.
|
||||
* - Fetches the dashboard meta only if the organization subscribe is active.
|
||||
* - Once the dashboard meta query is loading display dashboard splash screen.
|
||||
*/
|
||||
export function useDashboardMetaBoot() {
|
||||
const { isSubscriptionActive } = useSubscription();
|
||||
|
||||
const {
|
||||
data: dashboardMeta,
|
||||
isLoading: isDashboardMetaLoading,
|
||||
isSuccess: isDashboardMetaSuccess,
|
||||
} = useDashboardMeta({
|
||||
keepPreviousData: true,
|
||||
|
||||
// Avoid run the query if the organization subscription is not active.
|
||||
enabled: isSubscriptionActive,
|
||||
});
|
||||
const [startLoading, stopLoading] = useSplashLoading();
|
||||
|
||||
useWatchImmediate((value) => {
|
||||
value && startLoading();
|
||||
}, isDashboardMetaLoading);
|
||||
|
||||
useWatchImmediate(() => {
|
||||
isDashboardMetaSuccess && stopLoading();
|
||||
}, isDashboardMetaSuccess);
|
||||
|
||||
return {
|
||||
meta: dashboardMeta,
|
||||
isLoading: isDashboardMetaLoading,
|
||||
isSuccess: isDashboardMetaSuccess,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Application async booting.
|
||||
*/
|
||||
export function useApplicationBoot() {
|
||||
// Fetches the current user's organization.
|
||||
const {
|
||||
isSuccess: isCurrentOrganizationSuccess,
|
||||
isLoading: isOrgLoading,
|
||||
data: organization,
|
||||
} = useCurrentOrganization();
|
||||
|
||||
// Authenticated user.
|
||||
const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } =
|
||||
useAuthenticatedAccount();
|
||||
|
||||
// Initial locale cookie value.
|
||||
const localeCookie = getCookie('locale');
|
||||
|
||||
// Is the dashboard booted.
|
||||
const isBooted = React.useRef(false);
|
||||
|
||||
// Syns the organization language with locale cookie.
|
||||
React.useEffect(() => {
|
||||
if (organization?.metadata?.language) {
|
||||
setCookie('locale', organization.metadata.language);
|
||||
}
|
||||
}, [organization]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Can't continue if the organization metadata is not loaded yet.
|
||||
if (!organization?.metadata?.language) {
|
||||
return;
|
||||
}
|
||||
// Can't continue if the organization is already booted.
|
||||
if (isBooted.current) {
|
||||
return;
|
||||
}
|
||||
// Reboot the application in case the initial locale not equal
|
||||
// the current organization language.
|
||||
if (localeCookie !== organization.metadata.language) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [localeCookie, organization]);
|
||||
|
||||
const [startLoading, stopLoading] = useSplashLoading();
|
||||
|
||||
// Splash loading when organization request loading and
|
||||
// applicaiton still not booted.
|
||||
useWatchImmediate((value) => {
|
||||
value && !isBooted.current && startLoading();
|
||||
}, isOrgLoading);
|
||||
|
||||
// Splash loading when request authenticated user loading and
|
||||
// application still not booted yet.
|
||||
useWatchImmediate((value) => {
|
||||
value && !isBooted.current && startLoading();
|
||||
}, isAuthUserLoading);
|
||||
|
||||
// Stop splash loading once organization request success.
|
||||
useWatch((value) => {
|
||||
value && stopLoading();
|
||||
}, isCurrentOrganizationSuccess);
|
||||
|
||||
// Stop splash loading once authenticated user request success.
|
||||
useWatch((value) => {
|
||||
value && stopLoading();
|
||||
}, isAuthUserSuccess);
|
||||
|
||||
// Once the all requests complete change the app loading state.
|
||||
useWhen(
|
||||
isAuthUserSuccess &&
|
||||
isCurrentOrganizationSuccess &&
|
||||
localeCookie === organization?.metadata?.language,
|
||||
() => {
|
||||
isBooted.current = true;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: isOrgLoading || isAuthUserLoading,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
CollapsibleList,
|
||||
MenuItem,
|
||||
Classes,
|
||||
Boundary,
|
||||
} from '@blueprintjs/core';
|
||||
import withBreadcrumbs from 'react-router-breadcrumbs-hoc';
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
function DashboardBreadcrumbs({ breadcrumbs }){
|
||||
const history = useHistory();
|
||||
|
||||
return(
|
||||
<CollapsibleList
|
||||
className={Classes.BREADCRUMBS}
|
||||
dropdownTarget={<span className={Classes.BREADCRUMBS_COLLAPSED} />}
|
||||
collapseFrom={Boundary.START}
|
||||
visibleItemCount={0}>
|
||||
{
|
||||
breadcrumbs.map(({ breadcrumb,match })=>{
|
||||
return (<MenuItem
|
||||
key={match.url}
|
||||
icon={'folder-close'}
|
||||
text={breadcrumb}
|
||||
onClick={() => history.push(match.url) } />)
|
||||
})
|
||||
}
|
||||
</CollapsibleList>
|
||||
)
|
||||
}
|
||||
|
||||
export default withBreadcrumbs([])(DashboardBreadcrumbs)
|
||||
18
packages/webapp/src/components/Dashboard/DashboardCard.tsx
Normal file
18
packages/webapp/src/components/Dashboard/DashboardCard.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
// Dashboard card.
|
||||
export function DashboardCard({ children, page }) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(CLASSES.DASHBOARD_CARD, {
|
||||
[CLASSES.DASHBOARD_CARD_PAGE]: page,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import DashboardTopbar from '@/components/Dashboard/DashboardTopbar';
|
||||
import DashboardContentRoutes from '@/components/Dashboard/DashboardContentRoute';
|
||||
import DashboardErrorBoundary from './DashboardErrorBoundary';
|
||||
|
||||
export default React.forwardRef(({}, ref) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
|
||||
<div className="dashboard-content" id="dashboard" ref={ref}>
|
||||
<DashboardTopbar />
|
||||
<DashboardContentRoutes />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
|
||||
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
|
||||
import DashboardPage from './DashboardPage';
|
||||
|
||||
/**
|
||||
* Dashboard inner route content.
|
||||
*/
|
||||
function DashboardContentRouteContent({ route }) {
|
||||
const content = (
|
||||
<DashboardPage
|
||||
name={route.name}
|
||||
Component={route.component}
|
||||
pageTitle={route.pageTitle}
|
||||
backLink={route.backLink}
|
||||
hint={route.hint}
|
||||
sidebarExpand={route.sidebarExpand}
|
||||
pageType={route.pageType}
|
||||
defaultSearchResource={route.defaultSearchResource}
|
||||
/>
|
||||
);
|
||||
return route.subscriptionActive ? (
|
||||
<EnsureSubscriptionsIsInactive
|
||||
subscriptionTypes={route.subscriptionActive}
|
||||
children={content}
|
||||
redirectTo={'/billing'}
|
||||
/>
|
||||
) : route.subscriptionInactive ? (
|
||||
<EnsureSubscriptionsIsActive
|
||||
subscriptionTypes={route.subscriptionInactive}
|
||||
children={content}
|
||||
redirectTo={'/'}
|
||||
/>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard content route.
|
||||
*/
|
||||
export default function DashboardContentRoute() {
|
||||
const routes = getDashboardRoutes();
|
||||
|
||||
return (
|
||||
<Route pathname="/">
|
||||
<Switch>
|
||||
{routes.map((route, index) => (
|
||||
<Route exact={route.exact} key={index} path={`${route.path}`}>
|
||||
<DashboardContentRouteContent route={route} />
|
||||
</Route>
|
||||
))}
|
||||
</Switch>
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
/**
|
||||
* Dashboard content table.
|
||||
*/
|
||||
export function DashboardContentTable({ children }) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>{children}</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { FormattedMessage as T, Icon } from '@/components';
|
||||
|
||||
export default function DashboardErrorBoundary({}) {
|
||||
return (
|
||||
<div class="dashboard__error-boundary">
|
||||
<h1><T id={'sorry_about_that_something_went_wrong'} /></h1>
|
||||
<p><T id={'if_the_problem_stuck_please_contact_us_as_soon_as_possible'} /></p>
|
||||
<Icon icon="bigcapital" height={30} width={160} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import intl from "react-intl-universal";
|
||||
import { Classes, Button } from '@blueprintjs/core';
|
||||
import { T, Icon } from '@/components';
|
||||
|
||||
/**
|
||||
* Dashboard advanced filter button.
|
||||
*/
|
||||
export function DashboardFilterButton({ conditionsCount }) {
|
||||
return (
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter', {
|
||||
'has-active-filters': conditionsCount > 0,
|
||||
})}
|
||||
text={
|
||||
conditionsCount > 0 ? (
|
||||
intl.get('count_filters_applied', { count: conditionsCount })
|
||||
) : (
|
||||
<T id={'filter'} />
|
||||
)
|
||||
}
|
||||
icon={<Icon icon="filter-16" iconSize={16} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
packages/webapp/src/components/Dashboard/DashboardFooter.tsx
Normal file
26
packages/webapp/src/components/Dashboard/DashboardFooter.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { getFooterLinks } from '@/constants/footerLinks';
|
||||
import { For } from '@/components';
|
||||
|
||||
function FooterLinkItem({ title, link }) {
|
||||
return (
|
||||
<div class="">
|
||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DashboardFooter() {
|
||||
const footerLinks = getFooterLinks();
|
||||
|
||||
return (
|
||||
<div class="dashboard__footer">
|
||||
<div class="footer-links">
|
||||
<For render={FooterLinkItem} of={footerLinks} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { LoadingIndicator } from '../Indicator';
|
||||
|
||||
export function DashboardInsider({
|
||||
loading,
|
||||
children,
|
||||
name,
|
||||
mount = false,
|
||||
className,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
{
|
||||
dashboard__insider: true,
|
||||
'dashboard__insider--loading': loading,
|
||||
[`dashboard__insider--${name}`]: !!name,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<LoadingIndicator loading={loading} mount={mount}>
|
||||
{children}
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Choose } from '@/components';
|
||||
import BigcapitalLoading from './BigcapitalLoading';
|
||||
|
||||
/**
|
||||
* Dashboard loading indicator.
|
||||
*/
|
||||
export default function DashboardLoadingIndicator({
|
||||
isLoading = false,
|
||||
className,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={isLoading}>
|
||||
<BigcapitalLoading />
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
{ children }
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
105
packages/webapp/src/components/Dashboard/DashboardPage.tsx
Normal file
105
packages/webapp/src/components/Dashboard/DashboardPage.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect, Suspense } from 'react';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import { compose } from '@/utils';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
||||
|
||||
/**
|
||||
* Dashboard pages wrapper.
|
||||
*/
|
||||
function DashboardPage({
|
||||
// #ownProps
|
||||
pageTitle,
|
||||
backLink,
|
||||
sidebarExpand = true,
|
||||
Component,
|
||||
name,
|
||||
hint,
|
||||
defaultSearchResource,
|
||||
|
||||
// #withDashboardActions
|
||||
changePageTitle,
|
||||
setDashboardBackLink,
|
||||
changePageHint,
|
||||
toggleSidebarExpand,
|
||||
|
||||
// #withUniversalSearch
|
||||
setResourceTypeUniversalSearch,
|
||||
resetResourceTypeUniversalSearch,
|
||||
}) {
|
||||
// Hydrate the given page title.
|
||||
useEffect(() => {
|
||||
pageTitle && changePageTitle(pageTitle);
|
||||
|
||||
return () => {
|
||||
pageTitle && changePageTitle('');
|
||||
};
|
||||
});
|
||||
|
||||
// Hydrate the given page hint.
|
||||
useEffect(() => {
|
||||
hint && changePageHint(hint);
|
||||
|
||||
return () => {
|
||||
hint && changePageHint('');
|
||||
};
|
||||
}, [hint, changePageHint]);
|
||||
|
||||
// Hydrate the dashboard back link status.
|
||||
useEffect(() => {
|
||||
backLink && setDashboardBackLink(backLink);
|
||||
|
||||
return () => {
|
||||
backLink && setDashboardBackLink(false);
|
||||
};
|
||||
}, [backLink, setDashboardBackLink]);
|
||||
|
||||
useEffect(() => {
|
||||
const className = `page-${name}`;
|
||||
name && document.body.classList.add(className);
|
||||
|
||||
return () => {
|
||||
name && document.body.classList.remove(className);
|
||||
};
|
||||
}, [name]);
|
||||
|
||||
useEffect(() => {
|
||||
toggleSidebarExpand(sidebarExpand);
|
||||
}, [toggleSidebarExpand, sidebarExpand]);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultSearchResource) {
|
||||
setResourceTypeUniversalSearch(defaultSearchResource);
|
||||
}
|
||||
return () => {
|
||||
resetResourceTypeUniversalSearch();
|
||||
};
|
||||
}, [
|
||||
defaultSearchResource,
|
||||
resetResourceTypeUniversalSearch,
|
||||
setResourceTypeUniversalSearch,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={CLASSES.DASHBOARD_PAGE}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="dashboard__fallback-loading">
|
||||
<Spinner size={40} value={null} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
// withUniversalSearch,
|
||||
withUniversalSearchActions,
|
||||
)(DashboardPage);
|
||||
@@ -0,0 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Dashboard page content.
|
||||
*/
|
||||
export function DashboardPageContent({ children }) {
|
||||
return <div class="dashboard__page-content">{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { DashboardAbilityProvider } from '../../components';
|
||||
import { useDashboardMetaBoot } from './DashboardBoot';
|
||||
|
||||
/**
|
||||
* Dashboard provider.
|
||||
*/
|
||||
export default function DashboardProvider({ children }) {
|
||||
const { isLoading } = useDashboardMetaBoot();
|
||||
|
||||
// Avoid display any dashboard component before complete booting.
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
return <DashboardAbilityProvider>{children}</DashboardAbilityProvider>;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
PopoverInteractionKind,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Classes,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import clsx from 'classnames';
|
||||
import { Icon, T } from '@/components';
|
||||
|
||||
import Style from './style.module.scss';
|
||||
|
||||
/**
|
||||
* Dashboard rows height button control.
|
||||
*/
|
||||
export function DashboardRowsHeightButton({ initialValue, value, onChange }) {
|
||||
const [localSize, setLocalSize] = React.useState(initialValue);
|
||||
|
||||
// Handle menu item click.
|
||||
const handleItemClick = (size) => (event) => {
|
||||
setLocalSize(size);
|
||||
onChange && onChange(size, event);
|
||||
};
|
||||
// Button icon name.
|
||||
const btnIcon = `table-row-${localSize}`;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={
|
||||
<Menu className={Style.menu}>
|
||||
<MenuDivider title={<T id={'dashboard.rows_height'} />} />
|
||||
<MenuItem
|
||||
onClick={handleItemClick('small')}
|
||||
text={<T id={'dashboard.row_small'} />}
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={handleItemClick('medium')}
|
||||
text={<T id={'dashboard.row_medium'} />}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
>
|
||||
<Tooltip
|
||||
content={<T id={'dashboard.rows_height'} />}
|
||||
minimal={true}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className={clsx(Classes.MINIMAL, Style.button)}
|
||||
icon={<Icon icon={btnIcon} iconSize={16} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardRowsHeightButton.defaultProps = {
|
||||
initialValue: 'medium',
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
.menu{
|
||||
:global .bp3-heading{
|
||||
font-weight: 400;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.button{
|
||||
min-width: 34px;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useRef } from 'react';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
function DashboardSplitPane({
|
||||
sidebarExpended,
|
||||
children
|
||||
}) {
|
||||
const initialSize = 220;
|
||||
|
||||
const [defaultSize, setDefaultSize] = useState(
|
||||
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
|
||||
);
|
||||
const debounceSaveSize = useRef(
|
||||
debounce((size) => {
|
||||
localStorage.setItem('dashboard-size', size);
|
||||
}, 500),
|
||||
);
|
||||
const handleChange = (size) => {
|
||||
debounceSaveSize.current(size);
|
||||
setDefaultSize(size);
|
||||
}
|
||||
return (
|
||||
<SplitPane
|
||||
allowResize={sidebarExpended}
|
||||
split="vertical"
|
||||
minSize={180}
|
||||
maxSize={300}
|
||||
defaultSize={sidebarExpended ? defaultSize : 50}
|
||||
size={sidebarExpended ? defaultSize : 50}
|
||||
onChange={handleChange}
|
||||
className="primary"
|
||||
>
|
||||
{children}
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboard(({ sidebarExpended }) => ({ sidebarExpended }))
|
||||
)(DashboardSplitPane);
|
||||
@@ -0,0 +1,23 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { ThemeProvider, StyleSheetManager } from 'styled-components';
|
||||
import rtlcss from 'stylis-rtlcss';
|
||||
import { useAppIntlContext } from '../AppIntlProvider';
|
||||
|
||||
interface DashboardThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DashboardThemeProvider({
|
||||
children,
|
||||
}: DashboardThemeProviderProps) {
|
||||
const { direction } = useAppIntlContext();
|
||||
|
||||
return (
|
||||
<StyleSheetManager
|
||||
{...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})}
|
||||
>
|
||||
<ThemeProvider theme={{ dir: direction }}>{children}</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
216
packages/webapp/src/components/Dashboard/DashboardTopbar.tsx
Normal file
216
packages/webapp/src/components/Dashboard/DashboardTopbar.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
NavbarDivider,
|
||||
Button,
|
||||
Classes,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
|
||||
import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
|
||||
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
|
||||
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
|
||||
import { Icon, Hint, If } from '@/components';
|
||||
|
||||
import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
|
||||
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
|
||||
import { compose } from '@/utils';
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
import { useGetUniversalSearchTypeOptions } from '@/containers/UniversalSearch/utils';
|
||||
|
||||
function DashboardTopbarSubscriptionMessage() {
|
||||
return (
|
||||
<div class="dashboard__topbar-subscription-msg">
|
||||
<span>
|
||||
<T id={'dashboard.subscription_msg.period_over'} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardHamburgerButton({ ...props }) {
|
||||
return (
|
||||
<Button minimal={true} {...props}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
role="img"
|
||||
focusable="false"
|
||||
>
|
||||
<title>
|
||||
<T id={'menu'} />
|
||||
</title>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-miterlimit="5"
|
||||
stroke-width="2"
|
||||
d="M4 7h15M4 12h15M4 17h15"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard topbar.
|
||||
*/
|
||||
function DashboardTopbar({
|
||||
// #withDashboard
|
||||
pageTitle,
|
||||
editViewId,
|
||||
pageHint,
|
||||
|
||||
// #withDashboardActions
|
||||
toggleSidebarExpand,
|
||||
|
||||
// #withDashboard
|
||||
sidebarExpended,
|
||||
|
||||
// #withGlobalSearch
|
||||
openGlobalSearch,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive,
|
||||
isSubscriptionInactive,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
const handlerClickEditView = () => {
|
||||
history.push(`/custom_views/${editViewId}/edit`);
|
||||
};
|
||||
|
||||
const handleSidebarToggleBtn = () => {
|
||||
toggleSidebarExpand();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="dashboard__topbar">
|
||||
<div class="dashboard__topbar-left">
|
||||
<div class="dashboard__topbar-sidebar-toggle">
|
||||
<Tooltip
|
||||
content={
|
||||
!sidebarExpended ? (
|
||||
<T id={'open_sidebar'} />
|
||||
) : (
|
||||
<T id={'close_sidebar'} />
|
||||
)
|
||||
}
|
||||
position={Position.RIGHT}
|
||||
>
|
||||
<DashboardHamburgerButton onClick={handleSidebarToggleBtn} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="dashboard__title">
|
||||
<h1>{pageTitle}</h1>
|
||||
|
||||
<If condition={pageHint}>
|
||||
<div class="dashboard__hint">
|
||||
<Hint content={pageHint} />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={editViewId}>
|
||||
<Button
|
||||
className={Classes.MINIMAL + ' button--view-edit'}
|
||||
icon={<Icon icon="pen" iconSize={13} />}
|
||||
onClick={handlerClickEditView}
|
||||
/>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div class="dashboard__breadcrumbs">
|
||||
<DashboardBreadcrumbs />
|
||||
</div>
|
||||
<DashboardBackLink />
|
||||
</div>
|
||||
|
||||
<div class="dashboard__topbar-right">
|
||||
<If condition={isSubscriptionInactive}>
|
||||
<DashboardTopbarSubscriptionMessage />
|
||||
</If>
|
||||
|
||||
<Navbar class="dashboard__topbar-navbar">
|
||||
<NavbarGroup>
|
||||
<If condition={isSubscriptionActive}>
|
||||
<DashboardQuickSearchButton
|
||||
onClick={() => openGlobalSearch(true)}
|
||||
/>
|
||||
<QuickNewDropdown />
|
||||
|
||||
<Tooltip
|
||||
content={<T id={'notifications'} />}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'notification-24'} iconSize={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'help-24'} iconSize={20} />}
|
||||
text={<T id={'help'} />}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
|
||||
<div class="dashboard__topbar-user">
|
||||
<DashboardTopbarUser />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withUniversalSearchActions,
|
||||
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
|
||||
pageTitle,
|
||||
editViewId,
|
||||
sidebarExpended,
|
||||
pageHint,
|
||||
})),
|
||||
withDashboardActions,
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive, isSubscriptionInactive }) => ({
|
||||
isSubscriptionActive,
|
||||
isSubscriptionInactive,
|
||||
}),
|
||||
'main',
|
||||
),
|
||||
)(DashboardTopbar);
|
||||
|
||||
/**
|
||||
* Dashboard quick search button.
|
||||
*/
|
||||
function DashboardQuickSearchButton({ ...rest }) {
|
||||
const searchTypeOptions = useGetUniversalSearchTypeOptions();
|
||||
|
||||
// Can't continue if there is no any search type option.
|
||||
if (searchTypeOptions.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'search-24'} iconSize={20} />}
|
||||
text={<T id={'quick_find'} />}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
104
packages/webapp/src/components/Dashboard/DashboardViewsTabs.tsx
Normal file
104
packages/webapp/src/components/Dashboard/DashboardViewsTabs.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
// @ts-nocheck
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router';
|
||||
import { debounce } from 'lodash';
|
||||
import { If, Icon } from '@/components';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
/**
|
||||
* Dashboard views tabs.
|
||||
*/
|
||||
export function DashboardViewsTabs({
|
||||
initialViewSlug = 0,
|
||||
currentViewSlug,
|
||||
tabs,
|
||||
defaultTabText = <T id={'all'} />,
|
||||
allTab = true,
|
||||
newViewTab = true,
|
||||
resourceName,
|
||||
onNewViewTabClick,
|
||||
onChange,
|
||||
OnThrottledChange,
|
||||
throttleTime = 250,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const [currentView, setCurrentView] = useState(initialViewSlug || 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof currentViewSlug !== 'undefined' &&
|
||||
currentViewSlug !== currentView
|
||||
) {
|
||||
setCurrentView(currentViewSlug || 0);
|
||||
}
|
||||
}, [currentView, setCurrentView, currentViewSlug]);
|
||||
|
||||
const throttledOnChange = useRef(
|
||||
debounce((viewId) => saveInvoke(OnThrottledChange, viewId), throttleTime),
|
||||
);
|
||||
|
||||
// Trigger `onChange` and `onThrottledChange` events.
|
||||
const triggerOnChange = (viewSlug) => {
|
||||
const value = viewSlug === 0 ? null : viewSlug;
|
||||
saveInvoke(onChange, value);
|
||||
throttledOnChange.current(value);
|
||||
};
|
||||
|
||||
// Handles click a new view.
|
||||
const handleClickNewView = () => {
|
||||
history.push(`/custom_views/${resourceName}/new`);
|
||||
onNewViewTabClick && onNewViewTabClick();
|
||||
};
|
||||
|
||||
// Handle tabs change.
|
||||
const handleTabsChange = (viewSlug) => {
|
||||
setCurrentView(viewSlug);
|
||||
triggerOnChange(viewSlug);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="dashboard__views-tabs">
|
||||
<Tabs
|
||||
id="navbar"
|
||||
large={true}
|
||||
selectedTabId={currentView}
|
||||
className="tabs--dashboard-views"
|
||||
onChange={handleTabsChange}
|
||||
animate={false}
|
||||
>
|
||||
{allTab && <Tab id={0} title={defaultTabText} />}
|
||||
|
||||
{tabs.map((tab) => (
|
||||
<Tab id={tab.slug} title={tab.name} />
|
||||
))}
|
||||
<If condition={newViewTab}>
|
||||
<Tooltip
|
||||
content={<T id={'create_a_new_view'} />}
|
||||
position={Position.RIGHT}
|
||||
>
|
||||
<Button
|
||||
className="button--new-view"
|
||||
icon={<Icon icon="plus" />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tooltip>
|
||||
</If>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DashboardViewsTabs.propTypes = {
|
||||
tabs: PropTypes.array.isRequired,
|
||||
allTab: PropTypes.bool,
|
||||
newViewTab: PropTypes.bool,
|
||||
|
||||
onNewViewTabClick: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
OnThrottledChange: PropTypes.func,
|
||||
throttleTime: PropTypes.number,
|
||||
};
|
||||
46
packages/webapp/src/components/Dashboard/GlobalHotkeys.tsx
Normal file
46
packages/webapp/src/components/Dashboard/GlobalHotkeys.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getDashboardRoutes } from '@/routes/dashboard';
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
function GlobalHotkeys({
|
||||
// #withDashboardActions
|
||||
toggleSidebarExpend,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const routes = getDashboardRoutes();
|
||||
|
||||
const globalHotkeys = routes
|
||||
.filter(({ hotkey }) => hotkey)
|
||||
.map(({ hotkey }) => hotkey)
|
||||
.toString();
|
||||
|
||||
const handleSidebarToggleBtn = () => {
|
||||
toggleSidebarExpend();
|
||||
};
|
||||
useHotkeys(
|
||||
globalHotkeys,
|
||||
(event, handle) => {
|
||||
routes.map(({ path, hotkey }) => {
|
||||
if (handle.key === hotkey) {
|
||||
history.push(path);
|
||||
}
|
||||
});
|
||||
},
|
||||
[history],
|
||||
);
|
||||
useHotkeys('ctrl+/', (event, handle) => handleSidebarToggleBtn());
|
||||
useHotkeys('shift+d', (event, handle) => openDialog('money-in', {}));
|
||||
useHotkeys('shift+q', (event, handle) => openDialog('money-out', {}));
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default compose(withDashboardActions, withDialogActions)(GlobalHotkeys);
|
||||
35
packages/webapp/src/components/Dashboard/PrivatePages.tsx
Normal file
35
packages/webapp/src/components/Dashboard/PrivatePages.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
|
||||
import Dashboard from '@/components/Dashboard/Dashboard';
|
||||
import SetupWizardPage from '@/containers/Setup/WizardSetupPage';
|
||||
|
||||
import EnsureOrganizationIsReady from '../Guards/EnsureOrganizationIsReady';
|
||||
import EnsureOrganizationIsNotReady from '../Guards/EnsureOrganizationIsNotReady';
|
||||
import { PrivatePagesProvider } from './PrivatePagesProvider';
|
||||
|
||||
import '@/style/pages/Dashboard/Dashboard.scss';
|
||||
|
||||
/**
|
||||
* Dashboard inner private pages.
|
||||
*/
|
||||
export default function DashboardPrivatePages() {
|
||||
return (
|
||||
<PrivatePagesProvider>
|
||||
<Switch>
|
||||
<Route path={'/setup'}>
|
||||
<EnsureOrganizationIsNotReady>
|
||||
<SetupWizardPage />
|
||||
</EnsureOrganizationIsNotReady>
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<EnsureOrganizationIsReady>
|
||||
<Dashboard />
|
||||
</EnsureOrganizationIsReady>
|
||||
</Route>
|
||||
</Switch>
|
||||
</PrivatePagesProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useApplicationBoot } from '@/components';
|
||||
|
||||
/**
|
||||
* Private pages provider.
|
||||
*/
|
||||
export function PrivatePagesProvider({
|
||||
// #ownProps
|
||||
children,
|
||||
}) {
|
||||
const { isLoading } = useApplicationBoot();
|
||||
|
||||
return <React.Fragment>{!isLoading ? children : null}</React.Fragment>;
|
||||
}
|
||||
15
packages/webapp/src/components/Dashboard/SplashScreen.tsx
Normal file
15
packages/webapp/src/components/Dashboard/SplashScreen.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import BigcapitalLoading from './BigcapitalLoading';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
|
||||
function SplashScreenComponent({ splashScreenLoading }) {
|
||||
return splashScreenLoading ? <BigcapitalLoading /> : null;
|
||||
}
|
||||
|
||||
export const SplashScreen = R.compose(
|
||||
withDashboard(({ splashScreenLoading }) => ({
|
||||
splashScreenLoading,
|
||||
})),
|
||||
)(SplashScreenComponent);
|
||||
94
packages/webapp/src/components/Dashboard/TopbarUser.tsx
Normal file
94
packages/webapp/src/components/Dashboard/TopbarUser.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Button,
|
||||
Popover,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, FormattedMessage as T } from '@/components';
|
||||
|
||||
import { firstLettersArgs } from '@/utils';
|
||||
import { useAuthActions } from '@/hooks/state';
|
||||
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions';
|
||||
|
||||
import { useAuthenticatedAccount } from '@/hooks/query';
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Dashboard topbar user.
|
||||
*/
|
||||
function DashboardTopbarUser({
|
||||
openDialog,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { setLogout } = useAuthActions();
|
||||
|
||||
// Retrieve authenticated user information.
|
||||
const { data: user } = useAuthenticatedAccount();
|
||||
|
||||
const onClickLogout = () => {
|
||||
setLogout();
|
||||
};
|
||||
|
||||
const onKeyboardShortcut = () => {
|
||||
openDialog('keyboard-shortcuts');
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<Menu className={'menu--logged-user-dropdown'}>
|
||||
<MenuItem
|
||||
multiline={true}
|
||||
className={'menu-item--profile'}
|
||||
text={
|
||||
<div>
|
||||
<div class="person">
|
||||
{user.first_name} {user.last_name}
|
||||
</div>
|
||||
<div class="org">
|
||||
<T id="organization_id" />: {user.tenant_id}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<MenuDivider />
|
||||
<If condition={isSubscriptionActive}>
|
||||
<MenuItem
|
||||
text={<T id={'keyboard_shortcuts'} />}
|
||||
onClick={onKeyboardShortcut}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'preferences'} />}
|
||||
onClick={() => history.push('/preferences')}
|
||||
/>
|
||||
</If>
|
||||
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM}
|
||||
>
|
||||
<Button>
|
||||
<div className="user-text">
|
||||
{firstLettersArgs(user.first_name, user.last_name)}
|
||||
</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
withSubscriptions(
|
||||
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
|
||||
'main',
|
||||
),
|
||||
)(DashboardTopbarUser);
|
||||
14
packages/webapp/src/components/Dashboard/index.tsx
Normal file
14
packages/webapp/src/components/Dashboard/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
export * from './SplashScreen';
|
||||
export * from './DashboardBoot';
|
||||
export * from './DashboardThemeProvider';
|
||||
export * from './DashboardAbilityProvider';
|
||||
export * from './DashboardCard'
|
||||
export * from './DashboardActionsBar'
|
||||
export * from './DashboardFilterButton'
|
||||
export * from './DashboardRowsHeightButton'
|
||||
export * from './DashboardViewsTabs'
|
||||
export * from './DashboardActionViewsList'
|
||||
export * from './DashboardContentTable'
|
||||
export * from './DashboardPageContent'
|
||||
export * from './DashboardInsider'
|
||||
@@ -0,0 +1,77 @@
|
||||
// @ts-nocheck
|
||||
import React, { useRef, useCallback, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { useCellAutoFocus } from '@/hooks';
|
||||
import { AccountsSuggestField } from '@/components';
|
||||
|
||||
/**
|
||||
* Account cell renderer.
|
||||
*/
|
||||
export default function AccountCellRenderer({
|
||||
column: {
|
||||
id,
|
||||
accountsDataProp,
|
||||
filterAccountsByRootTypes,
|
||||
filterAccountsByTypes,
|
||||
fieldProps,
|
||||
formGroupProps,
|
||||
},
|
||||
row: { index, original },
|
||||
cell: { value: initialValue },
|
||||
payload: {
|
||||
accounts: defaultAccounts,
|
||||
updateData,
|
||||
errors,
|
||||
autoFocus,
|
||||
...restPayloadProps
|
||||
},
|
||||
}) {
|
||||
const accountRef = useRef();
|
||||
|
||||
useCellAutoFocus(accountRef, autoFocus, id, index);
|
||||
|
||||
const handleAccountSelected = useCallback(
|
||||
(account) => {
|
||||
updateData(index, id, account.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
const accounts = useMemo(
|
||||
() => restPayloadProps[accountsDataProp] || defaultAccounts,
|
||||
[restPayloadProps, defaultAccounts, accountsDataProp],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--account',
|
||||
Classes.FILL,
|
||||
)}
|
||||
{...formGroupProps}
|
||||
>
|
||||
<AccountsSuggestField
|
||||
accounts={accounts}
|
||||
onAccountSelected={handleAccountSelected}
|
||||
selectedAccountId={initialValue}
|
||||
filterByRootTypes={filterAccountsByRootTypes}
|
||||
filterByTypes={filterAccountsByTypes}
|
||||
inputProps={{
|
||||
inputRef: (ref) => (accountRef.current = ref),
|
||||
placeholder: intl.get('search'),
|
||||
}}
|
||||
openOnKeyDown={true}
|
||||
blurOnSelectClose={false}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
AccountCellRenderer.cellType = CellType.Field;
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { BranchSuggestField } from '../Branches';
|
||||
|
||||
/**
|
||||
* Branches list field cell.
|
||||
* @returns
|
||||
*/
|
||||
export default function BranchesListFieldCell({
|
||||
column: { id },
|
||||
row: { index, original },
|
||||
payload: { branches, updateData, errors },
|
||||
}) {
|
||||
const handleBranchSelected = React.useCallback(
|
||||
(branch) => {
|
||||
updateData(index, 'branch_id', branch.id);
|
||||
},
|
||||
[updateData, index],
|
||||
);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--contacts-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<BranchSuggestField
|
||||
branches={branches}
|
||||
onBranchSelected={handleBranchSelected}
|
||||
selectedBranchId={original?.branch_id}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
BranchesListFieldCell.cellType = CellType.Field;
|
||||
@@ -0,0 +1,52 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { Classes, Checkbox, FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
const CheckboxEditableCell = ({
|
||||
row: { index, original },
|
||||
column: { id, disabledAccessor, checkboxProps },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
|
||||
const onChange = (e) => {
|
||||
const newValue = e.target.checked;
|
||||
|
||||
setValue(newValue);
|
||||
payload.updateData(index, id, newValue);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = payload.errors?.[index]?.[id];
|
||||
|
||||
// Detarmines whether the checkbox is disabled.
|
||||
const disabled = disabledAccessor ? get(original, disabledAccessor) : false;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(Classes.FILL)}
|
||||
>
|
||||
<Checkbox
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
checked={initialValue}
|
||||
disabled={disabled}
|
||||
minimal={true}
|
||||
className="ml2"
|
||||
{...checkboxProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CheckboxEditableCell.cellType = CellType.Field;
|
||||
|
||||
export default CheckboxEditableCell;
|
||||
@@ -0,0 +1,42 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { ContactsSuggestField } from '@/components';
|
||||
export default function ContactsListCellRenderer({
|
||||
column: { id },
|
||||
row: { index, original },
|
||||
cell: { value },
|
||||
payload: { contacts, updateData, errors },
|
||||
}) {
|
||||
const handleContactSelected = useCallback(
|
||||
(contact) => {
|
||||
updateData(index, 'contact_id', contact.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--contacts-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<ContactsSuggestField
|
||||
contactsList={contacts}
|
||||
onContactSelected={handleContactSelected}
|
||||
selectedContactId={original?.contact_id}
|
||||
selectedContactType={original?.contact_type}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
ContactsListCellRenderer.cellType = CellType.Field;
|
||||
@@ -0,0 +1,21 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export const DivFieldCell = ({ cell: { value: initialValue } }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return <div>${value}</div>;
|
||||
};
|
||||
export const EmptyDiv = ({ cell: { value: initialValue } }) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return <div>{value}</div>;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Classes, InputGroup, FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
const InputEditableCell = ({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const onBlur = () => {
|
||||
payload.updateData(index, id, value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = payload.errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(Classes.FILL)}
|
||||
>
|
||||
<InputGroup
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fill={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
InputEditableCell.cellType = CellType.Field;
|
||||
|
||||
export default InputEditableCell;
|
||||
@@ -0,0 +1,60 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { ItemsSuggestField } from '@/components';
|
||||
|
||||
import { useCellAutoFocus } from '@/hooks';
|
||||
|
||||
/**
|
||||
* Items list cell.
|
||||
*/
|
||||
export default function ItemsListCell({
|
||||
column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
|
||||
row: { index },
|
||||
cell: { value: initialValue },
|
||||
payload: { items, updateData, errors, autoFocus },
|
||||
}) {
|
||||
const fieldRef = useRef();
|
||||
|
||||
// Auto-focus the items list input field.
|
||||
useCellAutoFocus(fieldRef, autoFocus, id, index);
|
||||
|
||||
// Handle the item selected.
|
||||
const handleItemSelected = useCallback(
|
||||
(item) => {
|
||||
updateData(index, id, item.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
{...formGroupProps}
|
||||
>
|
||||
<ItemsSuggestField
|
||||
items={items}
|
||||
onItemSelected={handleItemSelected}
|
||||
selectedItemId={initialValue}
|
||||
sellable={filterSellable}
|
||||
purchasable={filterPurchasable}
|
||||
inputProps={{
|
||||
inputRef: (ref) => (fieldRef.current = ref),
|
||||
placeholder: intl.get('enter_an_item'),
|
||||
}}
|
||||
openOnKeyDown={true}
|
||||
blurOnSelectClose={false}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
ItemsListCell.cellType = CellType.Field;
|
||||
@@ -0,0 +1,56 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { MoneyInputGroup } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
// Input form cell renderer.
|
||||
const MoneyFieldCellRenderer = ({
|
||||
row: { index, moneyInputGroupProps = {} },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload: { errors, updateData },
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const handleFieldChange = useCallback((value) => {
|
||||
setValue(value);
|
||||
}, [setValue]);
|
||||
|
||||
function isNumeric(data) {
|
||||
return (
|
||||
!isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array
|
||||
);
|
||||
}
|
||||
|
||||
const handleFieldBlur = () => {
|
||||
const updateValue = isNumeric(value) ? parseFloat(value) : value;
|
||||
updateData(index, id, updateValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={CLASSES.FILL}>
|
||||
<MoneyInputGroup
|
||||
value={value}
|
||||
// prefix={'$'}
|
||||
onChange={handleFieldChange}
|
||||
onBlur={handleFieldBlur}
|
||||
{...moneyInputGroupProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
MoneyFieldCellRenderer.cellType = CellType.Field;
|
||||
|
||||
export default MoneyFieldCellRenderer;
|
||||
@@ -0,0 +1,48 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FormGroup, NumericInput, Intent } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
/**
|
||||
* Numeric input table cell.
|
||||
*/
|
||||
export default function NumericInputCell({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const handleValueChange = (newValue) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
const onBlur = () => {
|
||||
payload.updateData(index, id, value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = payload.errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(CLASSES.FILL)}
|
||||
>
|
||||
<NumericInput
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
onBlur={onBlur}
|
||||
fill={true}
|
||||
buttonPosition={'none'}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
NumericInputCell.cellType = CellType.Field;
|
||||
@@ -0,0 +1,39 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { PaymentReceiveListField } from '@/components';
|
||||
import { CellType } from '@/constants';
|
||||
function PaymentReceiveListFieldCell({
|
||||
column: { id },
|
||||
row: { index },
|
||||
cell: { value: initialValue },
|
||||
payload: { invoices, updateData, errors },
|
||||
}) {
|
||||
const handleInvoicesSelected = useCallback(
|
||||
(_item) => {
|
||||
updateData(index, id, _item.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames('form-group--selcet-list', Classes.FILL)}
|
||||
>
|
||||
<PaymentReceiveListField
|
||||
invoices={invoices}
|
||||
onInvoiceSelected={handleInvoicesSelected}
|
||||
selectedInvoiceId={initialValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
PaymentReceiveListFieldCell.cellType = CellType.Field;
|
||||
|
||||
export default PaymentReceiveListFieldCell;
|
||||
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { MoneyInputGroup } from '@/components';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
const PercentFieldCell = ({
|
||||
cell: { value: initialValue },
|
||||
row: { index },
|
||||
column: { id },
|
||||
payload: { errors, updateData },
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const handleBlurChange = (newValue) => {
|
||||
const parsedValue = newValue === '' || newValue === undefined
|
||||
? '' : parseInt(newValue, 10);
|
||||
updateData(index, id, parsedValue);
|
||||
};
|
||||
|
||||
const handleChange = useCallback((value) => {
|
||||
setValue(value);
|
||||
}, [setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup intent={error ? Intent.DANGER : null}>
|
||||
<MoneyInputGroup
|
||||
prefix={'%'}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlurValue={handleBlurChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
PercentFieldCell.cellType = CellType.Field;
|
||||
|
||||
export default PercentFieldCell;
|
||||
@@ -0,0 +1,27 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Popover2 } from '@blueprintjs/popover2';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { CellType } from '@/constants';
|
||||
import { Icon, FormattedMessage as T } from '@/components';
|
||||
import ProjectBillableEntries from '@/containers/Projects/containers/ProjectBillableEntries';
|
||||
|
||||
/**
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
export function ProjectBillableEntriesCell() {
|
||||
const content = <ProjectBillableEntries />;
|
||||
return (
|
||||
<Popover2 content={content}>
|
||||
<Button
|
||||
icon={<Icon icon={'info'} iconSize={14} />}
|
||||
className="m12"
|
||||
minimal={true}
|
||||
/>
|
||||
</Popover2>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectBillableEntriesCell.cellType = CellType.Button;
|
||||
@@ -0,0 +1,44 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { FormGroup, Intent, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { ProjectSuggestField } from '@/containers/Projects/components';
|
||||
|
||||
/**
|
||||
* projects list field cell.
|
||||
* @returns
|
||||
*/
|
||||
export function ProjectsListFieldCell({
|
||||
column: { id },
|
||||
row: { index, original },
|
||||
payload: { projects, updateData, errors },
|
||||
}) {
|
||||
const handleProjectSelected = useCallback(
|
||||
(project) => {
|
||||
updateData(index, 'project_id', project.id);
|
||||
},
|
||||
[updateData, index],
|
||||
);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--contacts-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<ProjectSuggestField
|
||||
projects={projects}
|
||||
onProjectSelected={handleProjectSelected}
|
||||
selectedProjectId={original?.project_id}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectsListFieldCell.cellType = CellType.Field;
|
||||
@@ -0,0 +1,55 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Classes, Switch, FormGroup, Intent } from '@blueprintjs/core';
|
||||
|
||||
import { CellType } from '@/constants';
|
||||
import { safeInvoke } from '@/utils';
|
||||
|
||||
/**
|
||||
* Switch editable cell.
|
||||
*/
|
||||
const SwitchEditableCell = ({
|
||||
row: { index, original },
|
||||
column: { id, switchProps, onSwitchChange },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
|
||||
// Handle the switch change.
|
||||
const onChange = (e) => {
|
||||
const newValue = e.target.checked;
|
||||
|
||||
setValue(newValue);
|
||||
|
||||
safeInvoke(payload.updateData, index, id, newValue);
|
||||
safeInvoke(onSwitchChange, e, newValue, original);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = payload.errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(Classes.FILL)}
|
||||
>
|
||||
<Switch
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
checked={initialValue}
|
||||
minimal={true}
|
||||
className="ml2"
|
||||
{...switchProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
SwitchEditableCell.cellType = CellType.Field;
|
||||
|
||||
export default SwitchEditableCell;
|
||||
@@ -0,0 +1,46 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Classes, TextArea, FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
const TextAreaEditableCell = ({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const onBlur = () => {
|
||||
payload.updateData(index, id, value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const error = payload.errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(Classes.FILL)}
|
||||
>
|
||||
<TextArea
|
||||
growVertically={true}
|
||||
large={true}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fill={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
TextAreaEditableCell.cellType = CellType.Field;
|
||||
|
||||
export default TextAreaEditableCell;
|
||||
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Tooltip, Position } from '@blueprintjs/core';
|
||||
|
||||
/**
|
||||
* Text overview tooltip cell.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function TextOverviewTooltipCell({ cell: { value } }) {
|
||||
const SUBMENU_POPOVER_MODIFIERS = {
|
||||
flip: { boundariesElement: 'viewport', padding: 20 },
|
||||
offset: { offset: '0, 10' },
|
||||
preventOverflow: { boundariesElement: 'viewport', padding: 40 },
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={value}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
boundary={'viewport'}
|
||||
minimal={true}
|
||||
modifiers={SUBMENU_POPOVER_MODIFIERS}
|
||||
targetClassName={'table-tooltip-overview-target'}
|
||||
>
|
||||
{value}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
35
packages/webapp/src/components/DataTableCells/index.tsx
Normal file
35
packages/webapp/src/components/DataTableCells/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import AccountsListFieldCell from './AccountsListFieldCell';
|
||||
import MoneyFieldCell from './MoneyFieldCell';
|
||||
import InputGroupCell from './InputGroupCell';
|
||||
import ContactsListFieldCell from './ContactsListFieldCell';
|
||||
import ItemsListCell from './ItemsListCell';
|
||||
import PercentFieldCell from './PercentFieldCell';
|
||||
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
|
||||
import NumericInputCell from './NumericInputCell';
|
||||
import CheckBoxFieldCell from './CheckBoxFieldCell';
|
||||
import SwitchFieldCell from './SwitchFieldCell';
|
||||
import TextAreaCell from './TextAreaCell';
|
||||
import BranchesListFieldCell from './BranchesListFieldCell';
|
||||
import { ProjectsListFieldCell } from './ProjectsListFieldCell';
|
||||
import { ProjectBillableEntriesCell } from './ProjectBillableEntriesCell';
|
||||
import { TextOverviewTooltipCell } from './TextOverviewTooltipCell';
|
||||
|
||||
export {
|
||||
AccountsListFieldCell,
|
||||
MoneyFieldCell,
|
||||
InputGroupCell,
|
||||
ContactsListFieldCell,
|
||||
ItemsListCell,
|
||||
PercentFieldCell,
|
||||
DivFieldCell,
|
||||
EmptyDiv,
|
||||
NumericInputCell,
|
||||
CheckBoxFieldCell,
|
||||
SwitchFieldCell,
|
||||
TextAreaCell,
|
||||
BranchesListFieldCell,
|
||||
ProjectsListFieldCell,
|
||||
ProjectBillableEntriesCell,
|
||||
TextOverviewTooltipCell,
|
||||
};
|
||||
30
packages/webapp/src/components/Datatable/CellForceWidth.tsx
Normal file
30
packages/webapp/src/components/Datatable/CellForceWidth.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { getForceWidth } from '@/utils';
|
||||
|
||||
export function CellForceWidth({
|
||||
value,
|
||||
column: { forceWidthAccess },
|
||||
row: { original },
|
||||
}) {
|
||||
const forceWidthValue = forceWidthAccess
|
||||
? get(original, forceWidthAccess)
|
||||
: value;
|
||||
|
||||
return <ForceWidth forceValue={forceWidthValue}>{value}</ForceWidth>;
|
||||
}
|
||||
|
||||
export function ForceWidth({ children, forceValue }) {
|
||||
const forceWidthValue = forceValue || children;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={'force-width'}
|
||||
style={{ minWidth: getForceWidth(forceWidthValue) }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
6
packages/webapp/src/components/Datatable/Cells.tsx
Normal file
6
packages/webapp/src/components/Datatable/Cells.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
export function CellTextSpan({ cell: { value } }) {
|
||||
return (<span class="cell-text">{ value }</span>)
|
||||
}
|
||||
231
packages/webapp/src/components/Datatable/DataTable.tsx
Normal file
231
packages/webapp/src/components/Datatable/DataTable.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
// @ts-nocheck
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import {
|
||||
useTable,
|
||||
useExpanded,
|
||||
useRowSelect,
|
||||
usePagination,
|
||||
useResizeColumns,
|
||||
useSortBy,
|
||||
useFlexLayout,
|
||||
useAsyncDebounce,
|
||||
} from 'react-table';
|
||||
import { useSticky } from 'react-table-sticky';
|
||||
|
||||
import { useUpdateEffect } from '@/hooks';
|
||||
import { saveInvoke } from '@/utils';
|
||||
|
||||
import '@/style/components/DataTable/DataTable.scss';
|
||||
|
||||
import TableNoResultsRow from './TableNoResultsRow';
|
||||
import TableLoadingRow from './TableLoading';
|
||||
import TableHeader from './TableHeader';
|
||||
import TablePage from './TablePage';
|
||||
import TableFooter from './TableFooter';
|
||||
import TableRow from './TableRow';
|
||||
import TableRows from './TableRows';
|
||||
import TableCell from './TableCell';
|
||||
import TableTBody from './TableTBody';
|
||||
import TableContext from './TableContext';
|
||||
import TablePagination from './TablePagination';
|
||||
import TableWrapper from './TableWrapper';
|
||||
|
||||
import TableIndeterminateCheckboxRow from './TableIndeterminateCheckboxRow';
|
||||
import TableIndeterminateCheckboxHeader from './TableIndeterminateCheckboxHeader';
|
||||
|
||||
import { useResizeObserver } from './utils';
|
||||
|
||||
/**
|
||||
* Datatable component.
|
||||
*/
|
||||
export function DataTable(props) {
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
|
||||
onFetchData,
|
||||
|
||||
onSelectedRowsChange,
|
||||
manualSortBy = false,
|
||||
manualPagination = true,
|
||||
selectionColumn = false,
|
||||
expandSubRows = true,
|
||||
expanded = {},
|
||||
rowClassNames,
|
||||
payload,
|
||||
expandable = false,
|
||||
noInitialFetch = false,
|
||||
|
||||
pagesCount: controlledPageCount,
|
||||
|
||||
// Pagination props.
|
||||
initialPageIndex = 0,
|
||||
initialPageSize = 10,
|
||||
|
||||
updateDebounceTime = 200,
|
||||
selectionColumnWidth = 42,
|
||||
|
||||
autoResetPage,
|
||||
autoResetExpanded,
|
||||
autoResetGroupBy,
|
||||
autoResetSelectedRows,
|
||||
autoResetSortBy,
|
||||
autoResetFilters,
|
||||
autoResetRowState,
|
||||
|
||||
// Components
|
||||
TableHeaderRenderer,
|
||||
TablePageRenderer,
|
||||
TableWrapperRenderer,
|
||||
TableTBodyRenderer,
|
||||
TablePaginationRenderer,
|
||||
TableFooterRenderer,
|
||||
|
||||
onColumnResizing,
|
||||
initialColumnsWidths,
|
||||
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const selectionColumnObj = {
|
||||
id: 'selection',
|
||||
disableResizing: true,
|
||||
minWidth: selectionColumnWidth,
|
||||
width: selectionColumnWidth,
|
||||
maxWidth: selectionColumnWidth,
|
||||
skeletonWidthMin: 100,
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: TableIndeterminateCheckboxHeader,
|
||||
// The cell can use the individual row's getToggleRowSelectedProps method
|
||||
// to the render a checkbox
|
||||
Cell: TableIndeterminateCheckboxRow,
|
||||
className: 'selection',
|
||||
...(typeof selectionColumn === 'object' ? selectionColumn : {}),
|
||||
};
|
||||
|
||||
const table = useTable(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState: {
|
||||
pageIndex: initialPageIndex,
|
||||
pageSize: initialPageSize,
|
||||
expanded,
|
||||
columnResizing: {
|
||||
columnWidths: initialColumnsWidths || {},
|
||||
},
|
||||
},
|
||||
manualPagination,
|
||||
pageCount: controlledPageCount,
|
||||
getSubRows: (row) => row.children,
|
||||
manualSortBy,
|
||||
expandSubRows,
|
||||
payload,
|
||||
|
||||
autoResetPage,
|
||||
autoResetExpanded,
|
||||
autoResetGroupBy,
|
||||
autoResetSelectedRows,
|
||||
autoResetSortBy,
|
||||
autoResetFilters,
|
||||
autoResetRowState,
|
||||
|
||||
...restProps,
|
||||
},
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
useResizeColumns,
|
||||
useFlexLayout,
|
||||
useSticky,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
(hooks) => {
|
||||
hooks.visibleColumns.push((columns) => [
|
||||
// Let's make a column for selection
|
||||
...(selectionColumn ? [selectionColumnObj] : []),
|
||||
...columns,
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
selectedFlatRows,
|
||||
state: { pageIndex, pageSize, sortBy, selectedRowIds },
|
||||
} = table;
|
||||
|
||||
const isInitialMount = useRef(noInitialFetch);
|
||||
|
||||
const onFetchDataDebounced = useAsyncDebounce((...args) => {
|
||||
saveInvoke(onFetchData, ...args);
|
||||
}, updateDebounceTime);
|
||||
|
||||
// When these table states change, fetch new data!
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
} else {
|
||||
onFetchDataDebounced({ pageIndex, pageSize, sortBy });
|
||||
}
|
||||
}, [pageIndex, pageSize, sortBy, onFetchDataDebounced]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
saveInvoke(onSelectedRowsChange, selectedFlatRows);
|
||||
}, [selectedRowIds, onSelectedRowsChange]);
|
||||
|
||||
// Column resizing observer.
|
||||
useResizeObserver(table.state, (current, columnWidth, columnsResizing) => {
|
||||
onColumnResizing && onColumnResizing(current, columnWidth, columnsResizing);
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContext.Provider value={{ table, props }}>
|
||||
<TableWrapperRenderer>
|
||||
<TableHeaderRenderer />
|
||||
|
||||
<TableTBodyRenderer>
|
||||
<TablePageRenderer />
|
||||
</TableTBodyRenderer>
|
||||
|
||||
<TableFooterRenderer />
|
||||
</TableWrapperRenderer>
|
||||
|
||||
<TablePaginationRenderer />
|
||||
</TableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
DataTable.defaultProps = {
|
||||
pagination: false,
|
||||
hidePaginationNoPages: true,
|
||||
hideTableHeader: false,
|
||||
|
||||
size: null,
|
||||
spinnerProps: { size: 30 },
|
||||
|
||||
expandToggleColumn: 1,
|
||||
expandColumnSpace: 0.8,
|
||||
|
||||
autoResetPage: true,
|
||||
autoResetExpanded: true,
|
||||
autoResetGroupBy: true,
|
||||
autoResetSelectedRows: true,
|
||||
autoResetSortBy: true,
|
||||
autoResetFilters: true,
|
||||
autoResetRowState: true,
|
||||
|
||||
TableHeaderRenderer: TableHeader,
|
||||
TableFooterRenderer: TableFooter,
|
||||
TableLoadingRenderer: TableLoadingRow,
|
||||
TablePageRenderer: TablePage,
|
||||
TableRowsRenderer: TableRows,
|
||||
TableRowRenderer: TableRow,
|
||||
TableCellRenderer: TableCell,
|
||||
TableWrapperRenderer: TableWrapper,
|
||||
TableTBodyRenderer: TableTBody,
|
||||
TablePaginationRenderer: TablePagination,
|
||||
TableNoResultsRowRenderer: TableNoResultsRow,
|
||||
|
||||
noResults: '',
|
||||
payload: {},
|
||||
};
|
||||
116
packages/webapp/src/components/Datatable/DatatableEditable.tsx
Normal file
116
packages/webapp/src/components/Datatable/DatatableEditable.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DataTable } from './DataTable';
|
||||
|
||||
/**
|
||||
* Editable datatable.
|
||||
*/
|
||||
export function DataTableEditable({
|
||||
totalRow = false,
|
||||
actions,
|
||||
name,
|
||||
...tableProps
|
||||
}) {
|
||||
return (
|
||||
<DatatableEditableRoot>
|
||||
<DataTable {...tableProps} />
|
||||
</DatatableEditableRoot>
|
||||
);
|
||||
}
|
||||
|
||||
const DatatableEditableRoot = styled.div`
|
||||
.bp3-form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table {
|
||||
border: 1px solid #d2dce2;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
|
||||
.th,
|
||||
.td {
|
||||
border-left: 1px solid #e2e2e2;
|
||||
|
||||
&:first-of-type {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.thead {
|
||||
.tr .th {
|
||||
padding: 9px 14px;
|
||||
background-color: #f2f3fb;
|
||||
font-size: 13px;
|
||||
color: #415060;
|
||||
border-bottom: 1px solid #d2dce2;
|
||||
|
||||
&,
|
||||
.inner-resizer {
|
||||
border-left-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tbody {
|
||||
.tr .td {
|
||||
border-bottom: 0;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
min-height: 38px;
|
||||
padding: 4px 14px;
|
||||
|
||||
&.td-field-type,
|
||||
&.td-button-type {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
.tr:last-of-type .td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.tr {
|
||||
&:hover .td,
|
||||
.bp3-input {
|
||||
background-color: transparent;
|
||||
}
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||
.form-group--select-list .bp3-button {
|
||||
border-color: #ffffff;
|
||||
color: #222;
|
||||
border-radius: 3px;
|
||||
text-align: inherit;
|
||||
}
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input {
|
||||
border-radius: 2px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px #116cd0;
|
||||
}
|
||||
}
|
||||
.form-group--select-list .bp3-button {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.form-group--select-list,
|
||||
.bp3-form-group {
|
||||
&.bp3-intent-danger {
|
||||
.bp3-button:not(.bp3-minimal),
|
||||
.bp3-input {
|
||||
border-color: #f7b6b6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.td.actions {
|
||||
.bp3-button {
|
||||
color: #80858f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user