mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: universal search.
This commit is contained in:
@@ -74,6 +74,8 @@ const CLASSES = {
|
||||
UNIVERSAL_SEARCH_OVERLAY: 'universal-search-overlay',
|
||||
UNIVERSAL_SEARCH_INPUT: 'universal-search__input',
|
||||
UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS: 'universal-search-input-right-elements',
|
||||
UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY: 'universal-search__type-select-overlay',
|
||||
UNIVERSAL_SEARCH_TYPE_SELECT_BTN: 'universal-search__type-select-btn',
|
||||
UNIVERSAL_SEARCH_FOOTER: 'universal-search__footer',
|
||||
|
||||
UNIVERSAL_SEARCH_ACTIONS: 'universal-search__actions',
|
||||
|
||||
14
client/src/common/resourcesTypes.js
Normal file
14
client/src/common/resourcesTypes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export const RESOURCES_TYPES = {
|
||||
INVOICE: 'invoice',
|
||||
ESTIMATE: 'estimate',
|
||||
RECEIPT: 'receipt',
|
||||
PAYMENT_RECEIVE: 'payment_receive',
|
||||
PAYMENT_MADE: 'payment_made',
|
||||
CUSTOMER: 'customer',
|
||||
VENDOR: 'vendor',
|
||||
ITEM: 'item',
|
||||
BILL: 'bill',
|
||||
EXPENSE: 'expense',
|
||||
MANUAL_JOURNAL: 'manual_journal',
|
||||
ACCOUNT: 'account',
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Type1',
|
||||
placeholder: 'Id tempor anim culpa esse id laboris.',
|
||||
},
|
||||
{
|
||||
name: 'Type2',
|
||||
placeholder: 'Laborum aliqua eiusmod voluptate aliqua',
|
||||
},
|
||||
];
|
||||
@@ -7,7 +7,7 @@ import Sidebar from 'components/Sidebar/Sidebar';
|
||||
import DashboardContent from 'components/Dashboard/DashboardContent';
|
||||
import DialogsContainer from 'components/DialogsContainer';
|
||||
import PreferencesPage from 'components/Preferences/PreferencesPage';
|
||||
import Search from 'containers/GeneralSearch/Search';
|
||||
import DashboardUniversalSearch from 'containers/UniversalSearch/DashboardUniversalSearch';
|
||||
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
|
||||
import GlobalHotkeys from './GlobalHotkeys';
|
||||
import DashboardProvider from './DashboardProvider';
|
||||
@@ -35,7 +35,7 @@ export default function Dashboard() {
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Search />
|
||||
<DashboardUniversalSearch />
|
||||
<DialogsContainer />
|
||||
<GlobalHotkeys />
|
||||
<DrawersContainer />
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Divider,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import { Icon } from 'components';
|
||||
@@ -17,6 +18,8 @@ import { Icon } from 'components';
|
||||
*/
|
||||
export default function DashboardActionViewsList({
|
||||
resourceName,
|
||||
allMenuItem,
|
||||
allMenuItemText,
|
||||
views,
|
||||
onChange,
|
||||
}) {
|
||||
@@ -28,9 +31,28 @@ export default function DashboardActionViewsList({
|
||||
<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={<Menu>{viewsMenuItems}</Menu>}
|
||||
content={content}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
|
||||
@@ -27,6 +27,7 @@ export default function DashboardContentRoute() {
|
||||
hint={route.hint}
|
||||
sidebarExpand={route.sidebarExpand}
|
||||
pageType={route.pageType}
|
||||
defaultSearchResource={route.defaultSearchResource}
|
||||
/>
|
||||
</Route>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useEffect, Suspense } from 'react';
|
||||
import { isUndefined } from 'lodash';
|
||||
// import { isUndefined } from 'lodash';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import { compose } from 'utils';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
// import withUniversalSearch from '../../containers/UniversalSearch/withUniversalSearch';
|
||||
import withUniversalSearchActions from '../../containers/UniversalSearch/withUniversalSearchActions';
|
||||
|
||||
/**
|
||||
* Dashboard pages wrapper.
|
||||
*/
|
||||
@@ -16,12 +19,17 @@ function DashboardPage({
|
||||
Component,
|
||||
name,
|
||||
hint,
|
||||
defaultSearchResource,
|
||||
|
||||
// #withDashboardActions
|
||||
changePageTitle,
|
||||
setDashboardBackLink,
|
||||
changePageHint,
|
||||
toggleSidebarExpand
|
||||
toggleSidebarExpand,
|
||||
|
||||
// #withUniversalSearch
|
||||
setResourceTypeUniversalSearch,
|
||||
resetResourceTypeUniversalSearch,
|
||||
}) {
|
||||
// Hydrate the given page title.
|
||||
useEffect(() => {
|
||||
@@ -38,7 +46,7 @@ function DashboardPage({
|
||||
|
||||
return () => {
|
||||
hint && changePageHint('');
|
||||
}
|
||||
};
|
||||
}, [hint, changePageHint]);
|
||||
|
||||
// Hydrate the dashboard back link status.
|
||||
@@ -53,7 +61,7 @@ function DashboardPage({
|
||||
useEffect(() => {
|
||||
const className = `page-${name}`;
|
||||
name && document.body.classList.add(className);
|
||||
|
||||
|
||||
return () => {
|
||||
name && document.body.classList.remove(className);
|
||||
};
|
||||
@@ -61,15 +69,30 @@ function DashboardPage({
|
||||
|
||||
useEffect(() => {
|
||||
toggleSidebarExpand(sidebarExpand);
|
||||
}, [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>
|
||||
}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div class="dashboard__fallback-loading">
|
||||
<Spinner size={40} value={null} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Component />
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -78,4 +101,6 @@ function DashboardPage({
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
// withUniversalSearch,
|
||||
withUniversalSearchActions,
|
||||
)(DashboardPage);
|
||||
|
||||
@@ -16,7 +16,7 @@ import DashboardBreadcrumbs from 'components/Dashboard/DashboardBreadcrumbs';
|
||||
import DashboardBackLink from 'components/Dashboard/DashboardBackLink';
|
||||
import { Icon, Hint, If } from 'components';
|
||||
|
||||
import withUniversalSearch from 'containers/UniversalSearch/withUniversalSearch';
|
||||
import withUniversalSearchActions from 'containers/UniversalSearch/withUniversalSearchActions';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withSettings from 'containers/Settings/withSettings';
|
||||
@@ -155,7 +155,7 @@ function DashboardTopbar({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withUniversalSearch,
|
||||
withUniversalSearchActions,
|
||||
withDashboard(({ pageTitle, pageHint, editViewId, sidebarExpended }) => ({
|
||||
pageTitle,
|
||||
editViewId,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Omnibar } from '@blueprintjs/select';
|
||||
import { MenuItem, Spinner } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, Icon, ListSelect } from 'components';
|
||||
|
||||
import withSearch from 'containers/GeneralSearch/withSearch';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function UniversalSearch({
|
||||
results,
|
||||
onClose,
|
||||
|
||||
// withSearch
|
||||
globalSearchShow,
|
||||
closeGlobalSearch,
|
||||
...props
|
||||
}) {
|
||||
const SearchRenderer = (
|
||||
{ name, code, amount },
|
||||
{ handleClick, modifiers, query },
|
||||
) => {
|
||||
return (
|
||||
<MenuItem
|
||||
text={`${name} - ${code}`}
|
||||
label={amount}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
closeGlobalSearch(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Omnibar
|
||||
className={'navbar--omnibar'}
|
||||
items={results}
|
||||
itemRenderer={SearchRenderer}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
resetOnSelect={true}
|
||||
onClose={handleClose}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withSearch)(UniversalSearch);
|
||||
227
client/src/components/UniversalSearch/UniversalSearch.js
Normal file
227
client/src/components/UniversalSearch/UniversalSearch.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { isUndefined } from 'lodash';
|
||||
import {
|
||||
Overlay,
|
||||
InputGroup,
|
||||
Tag,
|
||||
MenuItem,
|
||||
Spinner,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { QueryList } from '@blueprintjs/select';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
import { Icon, If, ListSelect, FormattedMessage as T } from 'components';
|
||||
import {
|
||||
UniversalSearchProvider,
|
||||
useUniversalSearchContext,
|
||||
} from './UniversalSearchProvider';
|
||||
import { filterItemsByResourceType } from './utils';
|
||||
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
|
||||
|
||||
/**
|
||||
* Universal search input action.
|
||||
*/
|
||||
function UniversalSearchInputRightElements({ onSearchTypeChange }) {
|
||||
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } =
|
||||
useUniversalSearchContext();
|
||||
|
||||
// Handle search type option change.
|
||||
const handleSearchTypeChange = (option) => {
|
||||
onSearchTypeChange && onSearchTypeChange(option);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}>
|
||||
<If condition={isLoading}>
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} />
|
||||
</If>
|
||||
|
||||
<ListSelect
|
||||
items={searchTypeOptions}
|
||||
onItemSelect={handleSearchTypeChange}
|
||||
filterable={false}
|
||||
initialSelectedItem={defaultSearchResource}
|
||||
selectedItem={searchType}
|
||||
selectedItemProp={'key'}
|
||||
textProp={'label'}
|
||||
// defaultText={intl.get('type')}
|
||||
popoverProps={{
|
||||
minimal: true,
|
||||
captureDismiss: true,
|
||||
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_OVERLAY,
|
||||
}}
|
||||
buttonProps={{
|
||||
minimal: true,
|
||||
className: CLASSES.UNIVERSAL_SEARCH_TYPE_SELECT_BTN,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search query list.
|
||||
*/
|
||||
function UniversalSearchQueryList(props) {
|
||||
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<QueryList
|
||||
{...restProps}
|
||||
initialContent={null}
|
||||
renderer={(listProps) => (
|
||||
<UniversalSearchBar
|
||||
isOpen={isOpen}
|
||||
onSearchTypeChange={onSearchTypeChange}
|
||||
{...listProps}
|
||||
/>
|
||||
)}
|
||||
noResults={
|
||||
!isLoading ? (
|
||||
<MenuItem disabled={true} text={<T id={'no_results'} />} />
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={true}
|
||||
text={<T id={'universal_search.loading'} />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal query search actions.
|
||||
*/
|
||||
function UniversalQuerySearchActions() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}>
|
||||
<Tag>ENTER</Tag>
|
||||
<span class={'text'}>{intl.get('universal_search.enter_text')}</span>
|
||||
</div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}>
|
||||
<Tag>ESC</Tag>{' '}
|
||||
<span class={'text'}>{intl.get('universal_search.close_text')}</span>
|
||||
</div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}>
|
||||
<Tag>
|
||||
<Icon icon={'arrow-up-24'} iconSize={16} />
|
||||
</Tag>
|
||||
<Tag>
|
||||
<Icon icon={'arrow-down-24'} iconSize={16} />
|
||||
</Tag>
|
||||
<span class="text">{intl.get('universal_seach.navigate_text')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search input bar with items list.
|
||||
*/
|
||||
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
|
||||
const { handleKeyDown, handleKeyUp } = listProps;
|
||||
const handlers = isOpen
|
||||
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
|
||||
listProps.className,
|
||||
)}
|
||||
{...handlers}
|
||||
>
|
||||
<InputGroup
|
||||
autoFocus={true}
|
||||
large={true}
|
||||
leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
|
||||
placeholder={intl.get('universal_search.placeholder')}
|
||||
onChange={listProps.handleQueryChange}
|
||||
value={listProps.query}
|
||||
rightElement={
|
||||
<UniversalSearchInputRightElements
|
||||
onSearchTypeChange={onSearchTypeChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{listProps.itemList}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search.
|
||||
*/
|
||||
export function UniversalSearch({
|
||||
defaultSearchResource,
|
||||
searchResource,
|
||||
|
||||
overlayProps,
|
||||
isOpen,
|
||||
isLoading,
|
||||
onSearchTypeChange,
|
||||
items,
|
||||
searchTypeOptions,
|
||||
...queryListProps
|
||||
}) {
|
||||
// Search type state.
|
||||
const [searchType, setSearchType] = React.useState(
|
||||
defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
|
||||
);
|
||||
// Handle search resource type controlled mode.
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isUndefined(searchResource) &&
|
||||
searchResource !== defaultSearchResource
|
||||
) {
|
||||
setSearchType(searchResource);
|
||||
}
|
||||
}, [searchResource, defaultSearchResource]);
|
||||
|
||||
// Handle search type change.
|
||||
const handleSearchTypeChange = (searchTypeResource) => {
|
||||
setSearchType(searchTypeResource.key);
|
||||
onSearchTypeChange && onSearchTypeChange(searchTypeResource);
|
||||
};
|
||||
// Filters query list items based on the given search type.
|
||||
const filteredItems = filterItemsByResourceType(items, searchType);
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
hasBackdrop={true}
|
||||
isOpen={isOpen}
|
||||
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)}
|
||||
{...overlayProps}
|
||||
>
|
||||
<UniversalSearchProvider
|
||||
isLoading={isLoading}
|
||||
searchType={searchType}
|
||||
defaultSearchResource={defaultSearchResource}
|
||||
searchTypeOptions={searchTypeOptions}
|
||||
>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}>
|
||||
<UniversalSearchQueryList
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
searchType={searchType}
|
||||
onSearchTypeChange={handleSearchTypeChange}
|
||||
{...queryListProps}
|
||||
items={filteredItems}
|
||||
/>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}>
|
||||
<UniversalQuerySearchActions />
|
||||
</div>
|
||||
</div>
|
||||
</UniversalSearchProvider>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const UniversalSearchContext = createContext();
|
||||
|
||||
/**
|
||||
* Universal search data provider.
|
||||
*/
|
||||
function UniversalSearchProvider({
|
||||
isLoading,
|
||||
defaultSearchResource,
|
||||
searchType,
|
||||
searchTypeOptions,
|
||||
...props
|
||||
}) {
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
isLoading,
|
||||
searchType,
|
||||
defaultSearchResource,
|
||||
searchTypeOptions,
|
||||
};
|
||||
|
||||
return <UniversalSearchContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useUniversalSearchContext = () =>
|
||||
React.useContext(UniversalSearchContext);
|
||||
|
||||
export { UniversalSearchProvider, useUniversalSearchContext };
|
||||
4
client/src/components/UniversalSearch/utils.js
Normal file
4
client/src/components/UniversalSearch/utils.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export const filterItemsByResourceType = (items, type) => {
|
||||
return items.filter((item) => item._type === type);
|
||||
}
|
||||
@@ -58,7 +58,6 @@ import AccountsSuggestField from './AccountsSuggestField';
|
||||
import MaterialProgressBar from './MaterialProgressBar';
|
||||
import { MoneyFieldCell } from './DataTableCells';
|
||||
import Card from './Card';
|
||||
import UniversalSearch from './UniversalSearch';
|
||||
|
||||
import { ItemsMultiSelect } from './Items';
|
||||
|
||||
@@ -139,5 +138,4 @@ export {
|
||||
MoneyFieldCell,
|
||||
ItemsMultiSelect,
|
||||
Card,
|
||||
UniversalSearch,
|
||||
};
|
||||
|
||||
@@ -54,19 +54,20 @@ function ManualJournalActionsBar({
|
||||
const handleBulkDelete = () => {};
|
||||
|
||||
// Handle tab change.
|
||||
const handleTabChange = (customView) => {
|
||||
setManualJournalsTableState({ customViewId: customView.id || null });
|
||||
const handleTabChange = (view) => {
|
||||
setManualJournalsTableState({ viewSlug: view ? view.slig : null });
|
||||
};
|
||||
// Handle click a refresh Journals
|
||||
const handleRefreshBtnClick = () => { refresh(); };
|
||||
|
||||
console.log(manualJournalsFilterConditions, fields, 'XXX');
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'manual-journals'}
|
||||
allMenuItem={true}
|
||||
views={journalsViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
@@ -135,5 +136,5 @@ export default compose(
|
||||
withManualJournalsActions,
|
||||
withManualJournals(({ manualJournalsTableState }) => ({
|
||||
manualJournalsFilterConditions: manualJournalsTableState.filterRoles,
|
||||
}))
|
||||
})),
|
||||
)(ManualJournalActionsBar);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
/**
|
||||
* Universal search manual journal item select action.
|
||||
*/
|
||||
function JournalUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.MANUAL_JOURNAL) {
|
||||
openDrawer('journal-drawer', { manualJournalId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const JournalUniversalSearchSelectAction = withDrawerActions(
|
||||
JournalUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Mappes the manual journal item to search item.
|
||||
*/
|
||||
const manualJournalsToSearch = (manualJournal) => ({
|
||||
text: manualJournal.journal_number,
|
||||
subText: manualJournal.formatted_date,
|
||||
label: manualJournal.formatted_amount,
|
||||
reference: manualJournal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search invoice configure.
|
||||
*/
|
||||
export const universalSearchJournalBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||
optionItemLabel: 'Manual journal',
|
||||
selectItemAction: JournalUniversalSearchSelectAction,
|
||||
itemSelect: manualJournalsToSearch,
|
||||
});
|
||||
41
client/src/containers/Accounts/AccountUniversalSearch.js
Normal file
41
client/src/containers/Accounts/AccountUniversalSearch.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
function AccountUniversalSearchItemSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.ACCOUNT) {
|
||||
openDrawer('account-drawer', { accountId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const AccountUniversalSearchItemSelect = withDrawerActions(
|
||||
AccountUniversalSearchItemSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Transformes account item to search item.
|
||||
* @param {*} account
|
||||
* @returns
|
||||
*/
|
||||
const accountToSearch = (account) => ({
|
||||
text: `${account.name} - ${account.code}`,
|
||||
label: account.formatted_amount,
|
||||
reference: account,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search account configure.
|
||||
*/
|
||||
export const universalSearchAccountBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.ACCOUNT,
|
||||
optionItemLabel: 'Account',
|
||||
selectItemAction: AccountUniversalSearchItemSelect,
|
||||
itemSelect: accountToSearch,
|
||||
});
|
||||
@@ -78,8 +78,8 @@ function AccountsActionsBar({
|
||||
};
|
||||
|
||||
// Handle tab changing.
|
||||
const handleTabChange = (customView) => {
|
||||
setAccountsTableState({ customViewId: customView.id || null });
|
||||
const handleTabChange = (view) => {
|
||||
setAccountsTableState({ viewSlug: view ? view.slug : null });
|
||||
};
|
||||
|
||||
// Handle inactive switch changing.
|
||||
@@ -98,6 +98,8 @@ function AccountsActionsBar({
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'accounts'}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all_accounts'} />}
|
||||
views={resourceViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Position,
|
||||
Classes,
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
MenuItem,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
Intent,
|
||||
Popover,
|
||||
Button,
|
||||
Intent
|
||||
} from '@blueprintjs/core';
|
||||
import { Icon, Money, If } from 'components';
|
||||
import intl from 'react-intl-universal';
|
||||
import { safeCallback } from 'utils';
|
||||
|
||||
|
||||
/**
|
||||
* Accounts table actions menu.
|
||||
*/
|
||||
|
||||
@@ -62,9 +62,9 @@ function CustomerActionsBar({
|
||||
openAlert('customers-bulk-delete', { customersIds: customersSelectedRows });
|
||||
};
|
||||
|
||||
const handleTabChange = (viewId) => {
|
||||
const handleTabChange = (view) => {
|
||||
setCustomersTableState({
|
||||
customViewId: viewId.id || null,
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
// Handle inactive switch changing.
|
||||
@@ -82,6 +82,8 @@ function CustomerActionsBar({
|
||||
<DashboardActionViewsList
|
||||
resourceName={'customers'}
|
||||
views={customersViews}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all'} />}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
|
||||
33
client/src/containers/Customers/CustomersUniversalSearch.js
Normal file
33
client/src/containers/Customers/CustomersUniversalSearch.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
function CustomerUniversalSearchSelectComponent({ resourceType, resourceId }) {
|
||||
if (resourceType === RESOURCES_TYPES.CUSTOMER) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const CustomerUniversalSearchSelectAction = withDrawerActions(
|
||||
CustomerUniversalSearchSelectComponent
|
||||
);
|
||||
|
||||
/**
|
||||
* Transformes customers to search.
|
||||
* @param {*} contact
|
||||
* @returns
|
||||
*/
|
||||
const customersToSearch = (contact) => ({
|
||||
text: contact.display_name,
|
||||
label: contact.formatted_balance,
|
||||
reference: contact,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search invoice configure.
|
||||
*/
|
||||
export const universalSearchCustomerBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.CUSTOMER,
|
||||
optionItemLabel: 'Customers',
|
||||
selectItemAction: CustomerUniversalSearchSelectAction,
|
||||
itemSelect: customersToSearch,
|
||||
});
|
||||
24
client/src/containers/Expenses/ExpenseUniversalSearch.js
Normal file
24
client/src/containers/Expenses/ExpenseUniversalSearch.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
|
||||
/**
|
||||
* Universal search bill item select action.
|
||||
*/
|
||||
function ExpenseUniversalSearchItemSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.EXPENSE) {
|
||||
openDrawer('expense-drawer', { expenseId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ExpenseUniversalSearchItemSelect = withDrawerActions(
|
||||
ExpenseUniversalSearchItemSelectComponent,
|
||||
);
|
||||
@@ -37,7 +37,7 @@ function ExpensesActionsBar({
|
||||
setExpensesTableState,
|
||||
|
||||
// #withExpenses
|
||||
expensesFilterConditions
|
||||
expensesFilterConditions,
|
||||
}) {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
@@ -57,9 +57,9 @@ function ExpensesActionsBar({
|
||||
const handleBulkDelete = () => {};
|
||||
|
||||
// Handles the tab chaning.
|
||||
const handleTabChange = (viewId) => {
|
||||
const handleTabChange = (view) => {
|
||||
setExpensesTableState({
|
||||
customViewId: viewId.id || null,
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ function ExpensesActionsBar({
|
||||
<DashboardActionViewsList
|
||||
resourceName={'expenses'}
|
||||
views={expensesViews}
|
||||
allMenuItem={true}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
@@ -140,5 +141,5 @@ export default compose(
|
||||
withExpensesActions,
|
||||
withExpenses(({ expensesTableState }) => ({
|
||||
expensesFilterConditions: expensesTableState.filterRoles,
|
||||
}))
|
||||
})),
|
||||
)(ExpensesActionsBar);
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Intent, Spinner } from '@blueprintjs/core';
|
||||
import {
|
||||
UniversalSearch,
|
||||
ListSelect,
|
||||
If,
|
||||
FormattedMessage as T,
|
||||
} from 'components';
|
||||
import { defaultTo } from 'lodash';
|
||||
import intl from 'react-intl-universal';
|
||||
import { useAccounts } from 'hooks/query';
|
||||
import UniversalSearchOptions from 'common/universalSearchOptions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
import withSearch from 'containers/GeneralSearch/withSearch';
|
||||
|
||||
function Search({ globalSearchShow }) {
|
||||
const [query, setQuery] = React.useState();
|
||||
const [labelState, setLabelState] = React.useState();
|
||||
|
||||
const { data: accounts, isFetching: isAccountsFetching } = useAccounts({
|
||||
search_keyword: query,
|
||||
});
|
||||
|
||||
const handleClick = (placeholder) => {
|
||||
setLabelState(placeholder);
|
||||
};
|
||||
|
||||
const MenuSelectType = (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<If condition={isAccountsFetching}>
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={20} value={null} />
|
||||
</If>
|
||||
<ListSelect
|
||||
items={UniversalSearchOptions}
|
||||
onItemSelect={(holder) => handleClick(holder)}
|
||||
filterable={false}
|
||||
selectedItem={labelState?.name}
|
||||
selectedItemProp={'name'}
|
||||
textProp={'name'}
|
||||
defaultText={intl.get('type')}
|
||||
popoverProps={{ minimal: false, captureDismiss: true }}
|
||||
buttonProps={{
|
||||
minimal: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<UniversalSearch
|
||||
results={accounts}
|
||||
isOpen={globalSearchShow}
|
||||
onQueryChange={(q) => setQuery(q)}
|
||||
inputProps={{
|
||||
rightElement: MenuSelectType,
|
||||
placeholder: `${defaultTo(labelState?.placeholder, '')}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withSearch)(Search);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
resultSearch: state.globalSearch.searches,
|
||||
globalSearchShow: state.globalSearch.isOpen,
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
openGlobalSearch: (result) => dispatch({ type: t.OPEN_SEARCH, }),
|
||||
closeGlobalSearch: (result) => dispatch({ type: t.CLOSE_SEARCH }),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -58,8 +58,8 @@ function ItemsActionsBar({
|
||||
};
|
||||
|
||||
// Handle tab changing.
|
||||
const handleTabChange = (viewId) => {
|
||||
setItemsTableState({ customViewId: viewId.id || null });
|
||||
const handleTabChange = (view) => {
|
||||
setItemsTableState({ viewSlug: view ? view.slug : null });
|
||||
};
|
||||
|
||||
// Handle cancel/confirm items bulk.
|
||||
@@ -82,6 +82,8 @@ function ItemsActionsBar({
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'items'}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all_items'} />}
|
||||
views={itemsViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
45
client/src/containers/Items/ItemsUniversalSearch.js
Normal file
45
client/src/containers/Items/ItemsUniversalSearch.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
/**
|
||||
* Item univrsal search item select action.
|
||||
*/
|
||||
function ItemUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.ITEM) {
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ItemUniversalSearchSelectAction = withDrawerActions(
|
||||
ItemUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Transformes items to search.
|
||||
* @param {*} item
|
||||
* @returns
|
||||
*/
|
||||
const transfromItemsToSearch = (item) => ({
|
||||
text: item.name,
|
||||
subText: item.code,
|
||||
label: item.type,
|
||||
reference: item,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search invoice configure.
|
||||
*/
|
||||
export const universalSearchItemBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.ITEM,
|
||||
optionItemLabel: 'Items',
|
||||
selectItemAction: ItemUniversalSearchSelectAction,
|
||||
itemSelect: transfromItemsToSearch,
|
||||
});
|
||||
@@ -130,4 +130,4 @@ export function transformItemsTableState(tableState) {
|
||||
...transformTableStateToQuery(tableState),
|
||||
inactive_mode: tableState.inactiveMode,
|
||||
};
|
||||
}
|
||||
}
|
||||
116
client/src/containers/Purchases/Bills/BillUniversalSearch.js
Normal file
116
client/src/containers/Purchases/Bills/BillUniversalSearch.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { formattedAmount } from 'utils';
|
||||
import { T, Icon, Choose, If } from 'components';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import withDrawerActions from '../../Drawer/withDrawerActions';
|
||||
|
||||
/**
|
||||
* Universal search bill item select action.
|
||||
*/
|
||||
function BillUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.INVOICE) {
|
||||
openDrawer('bill-drawer', { billId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const BillUniversalSearchSelect = withDrawerActions(
|
||||
BillUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Status accessor.
|
||||
*/
|
||||
export function BillStatus({ bill }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={bill.is_fully_paid && bill.is_open}>
|
||||
<span class="fully-paid-text">
|
||||
<T id={'paid'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.When condition={bill.is_open}>
|
||||
<Choose>
|
||||
<Choose.When condition={bill.is_overdue}>
|
||||
<span className={'overdue-status'}>
|
||||
{intl.get('overdue_by', { overdue: bill.overdue_days })}
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span className={'due-status'}>
|
||||
{intl.get('due_in', { due: bill.remaining_days })}
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
<If condition={bill.is_partially_paid}>
|
||||
<span className="partial-paid">
|
||||
{intl.get('day_partially_paid', {
|
||||
due: formattedAmount(bill.due_amount, bill.currency_code),
|
||||
})}
|
||||
</span>
|
||||
</If>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill universal search item.
|
||||
*/
|
||||
export function BillUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{item.text}</div>
|
||||
<span class="bp3-text-muted">
|
||||
{item.reference.bill_number}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{item.reference.formatted_bill_date}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<div class="amount">{item.reference.formatted_amount}</div>
|
||||
<BillStatus bill={item.reference} />
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--bill'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const billsToSearch = (bill) => ({
|
||||
text: bill.vendor.display_name,
|
||||
reference: bill,
|
||||
});
|
||||
|
||||
export const universalSearchBillBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.BILL,
|
||||
optionItemLabel: 'Bills',
|
||||
selectItemAction: BillUniversalSearchSelect,
|
||||
itemRenderer: BillUniversalSearchItem,
|
||||
itemSelect: billsToSearch,
|
||||
});
|
||||
@@ -34,7 +34,7 @@ function BillActionsBar({
|
||||
setBillsTableState,
|
||||
|
||||
// #withBills
|
||||
billsConditionsRoles
|
||||
billsConditionsRoles,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -50,13 +50,15 @@ function BillActionsBar({
|
||||
};
|
||||
|
||||
// Handle tab change.
|
||||
const handleTabChange = (customView) => {
|
||||
const handleTabChange = (view) => {
|
||||
setBillsTableState({
|
||||
customViewId: customView.id || null,
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
// Handle click a refresh bills
|
||||
const handleRefreshBtnClick = () => { refresh(); };
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -64,6 +66,8 @@ function BillActionsBar({
|
||||
<DashboardActionViewsList
|
||||
resourceName={'bills'}
|
||||
views={billsViews}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all'} />}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
@@ -127,6 +131,6 @@ function BillActionsBar({
|
||||
export default compose(
|
||||
withBillsActions,
|
||||
withBills(({ billsTableState }) => ({
|
||||
billsConditionsRoles: billsTableState.filterRoles
|
||||
}))
|
||||
billsConditionsRoles: billsTableState.filterRoles,
|
||||
})),
|
||||
)(BillActionsBar);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { Icon } from 'components';
|
||||
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import withDrawerActions from '../../Drawer/withDrawerActions';
|
||||
|
||||
import { highlightText } from 'utils';
|
||||
|
||||
/**
|
||||
* Universal search bill item select action.
|
||||
*/
|
||||
function PaymentMadeUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.PAYMENT_MADE) {
|
||||
openDrawer('payment-made-detail-drawer', { paymentMadeId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const PaymentMadeUniversalSearchSelect = withDrawerActions(
|
||||
PaymentMadeUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Payment made universal search item.
|
||||
*/
|
||||
export function PaymentMadeUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{highlightText(item.text, query)}</div>
|
||||
|
||||
<span class="bp3-text-muted">
|
||||
{highlightText(item.reference.payment_number, query)}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{highlightText(item.reference.formatted_payment_date, query)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={<div class="amount">{item.reference.formatted_amount}</div>}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--payment-made'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Payment made resource item to search item.
|
||||
*/
|
||||
const paymentMadeToSearch = (payment) => ({
|
||||
text: payment.vendor.display_name,
|
||||
subText: payment.formatted_payment_date,
|
||||
label: payment.formatted_amount,
|
||||
reference: payment,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search payment made configure.
|
||||
*/
|
||||
export const universalSearchPaymentMadeBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.PAYMENT_MADE,
|
||||
optionItemLabel: 'Payment made',
|
||||
selectItemAction: PaymentMadeUniversalSearchSelect,
|
||||
itemRenderer: PaymentMadeUniversalSearchItem,
|
||||
itemSelect: paymentMadeToSearch,
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { Choose, T, Icon } from 'components';
|
||||
|
||||
import { RESOURCES_TYPES } from "../../../../common/resourcesTypes";
|
||||
import withDrawerActions from "../../../Drawer/withDrawerActions";
|
||||
|
||||
/**
|
||||
* Estimate universal search item select action.
|
||||
*/
|
||||
function EstimateUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.ESTIMATE) {
|
||||
openDrawer('estimate-drawer', { estimateId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const EstimateUniversalSearchSelect = withDrawerActions(
|
||||
EstimateUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Status accessor.
|
||||
*/
|
||||
export const EstimateStatus = ({ estimate }) => (
|
||||
<Choose>
|
||||
<Choose.When condition={estimate.is_delivered && estimate.is_approved}>
|
||||
<span class="approved">
|
||||
<T id={'approved'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.When condition={estimate.is_delivered && estimate.is_rejected}>
|
||||
<span class="reject">
|
||||
<T id={'rejected'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.When
|
||||
condition={
|
||||
estimate.is_delivered && !estimate.is_rejected && !estimate.is_approved
|
||||
}
|
||||
>
|
||||
<span class="delivered">
|
||||
<T id={'delivered'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
|
||||
/**
|
||||
* Estimate universal search item.
|
||||
*/
|
||||
export function EstimateUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
text={
|
||||
<div>
|
||||
<div>{item.text}</div>
|
||||
<span class="bp3-text-muted">
|
||||
{item.reference.estimate_number}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{item.reference.formatted_estimate_date}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<div class="amount">{item.reference.formatted_amount}</div>
|
||||
<EstimateStatus estimate={item.reference} />
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--estimate'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const transformEstimatesToSearch = (estimate) => ({
|
||||
text: estimate.customer.display_name,
|
||||
label: estimate.formatted_balance,
|
||||
reference: estimate,
|
||||
});
|
||||
|
||||
export const universalSearchEstimateBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.ESTIMATE,
|
||||
optionItemLabel: 'Estimates',
|
||||
selectItemAction: EstimateUniversalSearchSelect,
|
||||
itemRenderer: EstimateUniversalSearchItem,
|
||||
itemSelect: transformEstimatesToSearch
|
||||
});
|
||||
@@ -35,7 +35,7 @@ function EstimateActionsBar({
|
||||
setEstimatesTableState,
|
||||
|
||||
// #withEstimates
|
||||
estimatesFilterRoles
|
||||
estimatesFilterRoles,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -51,20 +51,24 @@ function EstimateActionsBar({
|
||||
const { refresh } = useRefreshEstimates();
|
||||
|
||||
// Handle tab change.
|
||||
const handleTabChange = (customView) => {
|
||||
const handleTabChange = (view) => {
|
||||
setEstimatesTableState({
|
||||
customViewId: customView.id || null,
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle click a refresh sale estimates
|
||||
const handleRefreshBtnClick = () => { refresh(); };
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'estimates'}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all'} />}
|
||||
views={estimatesViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
121
client/src/containers/Sales/Invoices/InvoiceUniversalSearch.js
Normal file
121
client/src/containers/Sales/Invoices/InvoiceUniversalSearch.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { T, Choose, Icon } from 'components';
|
||||
|
||||
import { highlightText } from 'utils';
|
||||
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import withDrawerActions from '../../Drawer/withDrawerActions';
|
||||
|
||||
/**
|
||||
* Universal search invoice item select action.
|
||||
*/
|
||||
function InvoiceUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.INVOICE) {
|
||||
openDrawer('invoice-drawer', { invoiceId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const InvoiceUniversalSearchSelect = withDrawerActions(
|
||||
InvoiceUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Invoice status.
|
||||
*/
|
||||
function InvoiceStatus(customer) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={customer.is_fully_paid && customer.is_delivered}>
|
||||
<span class="status status-success">
|
||||
<T id={'paid'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.When condition={customer.is_delivered}>
|
||||
<Choose>
|
||||
<Choose.When condition={customer.is_overdue}>
|
||||
<span className={'status status-warning'}>
|
||||
{intl.get('overdue_by', { overdue: customer.overdue_days })}
|
||||
</span>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span className={'status status-warning'}>
|
||||
{intl.get('due_in', { due: customer.remaining_days })}
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
</Choose.When>
|
||||
<Choose.Otherwise>
|
||||
<span class="status status--gray">
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search invoice item.
|
||||
*/
|
||||
export function InvoiceUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{highlightText(item.text, query)}</div>
|
||||
<span class="bp3-text-muted">
|
||||
{highlightText(item.reference.invoice_no, query)}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{highlightText(item.reference.formatted_invoice_date, query)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<div class="amount">${item.reference.balance}</div>
|
||||
<InvoiceStatus customer={item.reference} />
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--invoice'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes invoices to search.
|
||||
* @param {*} invoice
|
||||
* @returns
|
||||
*/
|
||||
const transformInvoicesToSearch = (invoice) => ({
|
||||
id: invoice.id,
|
||||
text: invoice.customer.display_name,
|
||||
label: invoice.formatted_balance,
|
||||
reference: invoice,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search invoice configure.
|
||||
*/
|
||||
export const universalSearchInvoiceBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.INVOICE,
|
||||
optionItemLabel: 'Invoices',
|
||||
selectItemAction: InvoiceUniversalSearchSelect,
|
||||
itemRenderer: InvoiceUniversalSearchItem,
|
||||
itemSelect: transformInvoicesToSearch,
|
||||
});
|
||||
@@ -51,8 +51,8 @@ function InvoiceActionsBar({
|
||||
const { refresh } = useRefreshInvoices();
|
||||
|
||||
// Handle views tab change.
|
||||
const handleTabChange = (customView) => {
|
||||
setInvoicesTableState({ customViewId: customView.id || null });
|
||||
const handleTabChange = (view) => {
|
||||
setInvoicesTableState({ viewSlug: view ? view.slug : null });
|
||||
};
|
||||
|
||||
// Handle click a refresh sale invoices
|
||||
@@ -64,6 +64,7 @@ function InvoiceActionsBar({
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
allMenuItem={true}
|
||||
resourceName={'invoices'}
|
||||
views={invoicesViews}
|
||||
onChange={handleTabChange}
|
||||
|
||||
@@ -210,4 +210,4 @@ export function useInvoicesTableColumns() {
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { RESOURCES_TYPES } from "../../../common/resourcesTypes";
|
||||
import withDrawerActions from "../../Drawer/withDrawerActions";
|
||||
import { highlightText } from 'utils';
|
||||
import { Icon } from 'components';
|
||||
|
||||
/**
|
||||
* Payment receive universal search item select action.
|
||||
*/
|
||||
function PaymentReceiveUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.PAYMENT_RECEIVE) {
|
||||
openDrawer('payment-receive-drawer', { paymentReceiveId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const PaymentReceiveUniversalSearchSelect = withDrawerActions(
|
||||
PaymentReceiveUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Payment receive universal search item.
|
||||
*/
|
||||
export function PaymentReceiveUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{highlightText(item.text, query)}</div>
|
||||
|
||||
<span class="bp3-text-muted">
|
||||
{highlightText(item.reference.payment_receive_no, query)}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{highlightText(item.reference.formatted_payment_date, query)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={<div class="amount">{item.reference.formatted_amount}</div>}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--invoice'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes payment receives to search.
|
||||
* @param {*} payment
|
||||
* @returns
|
||||
*/
|
||||
const paymentReceivesToSearch = (payment) => ({
|
||||
text: payment.customer.display_name,
|
||||
subText: payment.formatted_payment_date,
|
||||
label: payment.formatted_amount,
|
||||
reference: payment,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search payment receive configure.
|
||||
*/
|
||||
export const universalSearchPaymentReceiveBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.PAYMENT_RECEIVE,
|
||||
optionItemLabel: 'Payment receive',
|
||||
selectItemAction: PaymentReceiveUniversalSearchSelect,
|
||||
itemRenderer: PaymentReceiveUniversalSearchItem,
|
||||
itemSelect: paymentReceivesToSearch,
|
||||
});
|
||||
100
client/src/containers/Sales/Receipts/ReceiptUniversalSearch.js
Normal file
100
client/src/containers/Sales/Receipts/ReceiptUniversalSearch.js
Normal file
@@ -0,0 +1,100 @@
|
||||
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { Icon, Choose, T } from 'components';
|
||||
|
||||
import { RESOURCES_TYPES } from "../../../common/resourcesTypes";
|
||||
import withDrawerActions from "../../Drawer/withDrawerActions";
|
||||
|
||||
|
||||
/**
|
||||
* Receipt universal search item select action.
|
||||
*/
|
||||
function ReceiptUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
onAction,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.RECEIPT) {
|
||||
openDrawer('receipt-drawer', { estimateId: resourceId });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ReceiptUniversalSearchSelect = withDrawerActions(
|
||||
ReceiptUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Status accessor.
|
||||
*/
|
||||
function ReceiptStatus({ receipt }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={receipt.is_closed}>
|
||||
<span class="closed"><T id={'closed'} /></span>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<span class="draft"><T id={'draft'} /></span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt universal search item.
|
||||
*/
|
||||
export function ReceiptUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{item.text}</div>
|
||||
<span class="bp3-text-muted">
|
||||
{item.reference.receipt_number}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{item.reference.formatted_receipt_date}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<div class="amount">${item.reference.amount}</div>
|
||||
<ReceiptStatus receipt={item.reference} />
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--receipt'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes receipt resource item to search item.
|
||||
*/
|
||||
const transformReceiptsToSearch = (receipt) => ({
|
||||
text: receipt.customer.display_name,
|
||||
label: receipt.formatted_amount,
|
||||
reference: receipt,
|
||||
});
|
||||
|
||||
/**
|
||||
* Receipt universal search bind configuration.
|
||||
*/
|
||||
export const universalSearchReceiptBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.RECEIPT,
|
||||
optionItemLabel: 'Receipts',
|
||||
selectItemAction: ReceiptUniversalSearchSelect,
|
||||
itemRenderer: ReceiptUniversalSearchItem,
|
||||
itemSelect: transformReceiptsToSearch,
|
||||
});
|
||||
@@ -49,22 +49,24 @@ function ReceiptActionsBar({
|
||||
// Sale receipt refresh action.
|
||||
const { refresh } = useRefreshReceipts();
|
||||
|
||||
const handleTabChange = (customView) => {
|
||||
const handleTabChange = (view) => {
|
||||
setReceiptsTableState({
|
||||
customViewId: customView.id || null,
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle click a refresh sale estimates
|
||||
const handleRefreshBtnClick = () => { refresh(); };
|
||||
|
||||
console.log(receiptsFilterConditions, fields, 'XXX');
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'receipts'}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all'} />}
|
||||
views={receiptsViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { isUndefined } from 'lodash';
|
||||
|
||||
import { useUniversalSearch } from 'hooks/query';
|
||||
import { UniversalSearch } from 'components';
|
||||
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
import { compose } from 'utils';
|
||||
import withUniversalSearchActions from './withUniversalSearchActions';
|
||||
import withUniversalSearch from './withUniversalSearch';
|
||||
|
||||
import DashboardUniversalSearchItemActions from './DashboardUniversalSearchItemActions';
|
||||
import { DashboardUniversalSearchItem } from './components';
|
||||
|
||||
import DashboardUniversalSearchHotkeys from './DashboardUniversalSearchHotkeys';
|
||||
import { getUniversalSearchTypeOptions } from './utils';
|
||||
|
||||
/**
|
||||
* Dashboard universal search.
|
||||
*/
|
||||
function DashboardUniversalSearch({
|
||||
// #withUniversalSearchActions
|
||||
setSelectedItemUniversalSearch,
|
||||
|
||||
// #withUniversalSearch
|
||||
globalSearchShow,
|
||||
closeGlobalSearch,
|
||||
defaultUniversalResourceType,
|
||||
}) {
|
||||
// Search keyword.
|
||||
const [searchKeyword, setSearchKeyword] = React.useState('');
|
||||
|
||||
// Default search type.
|
||||
const [defaultSearchType, setDefaultSearchType] = React.useState(
|
||||
defaultUniversalResourceType || RESOURCES_TYPES.CUSTOMR,
|
||||
);
|
||||
// Search type.
|
||||
const [searchType, setSearchType] = React.useState(defaultSearchType);
|
||||
|
||||
// Sync default search type with default universal resource type.
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isUndefined(defaultUniversalResourceType) &&
|
||||
defaultSearchType !== defaultUniversalResourceType
|
||||
) {
|
||||
setSearchType(defaultUniversalResourceType);
|
||||
setDefaultSearchType(defaultUniversalResourceType);
|
||||
}
|
||||
}, [defaultSearchType, defaultUniversalResourceType]);
|
||||
|
||||
// Fetch accounts list according to the given custom view id.
|
||||
const {
|
||||
data,
|
||||
remove,
|
||||
isFetching: isSearchFetching,
|
||||
isLoading: isSearchLoading,
|
||||
refetch,
|
||||
} = useUniversalSearch(searchType, searchKeyword, {
|
||||
keepPreviousData: true,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
// Handle query change.
|
||||
const handleQueryChange = (query) => {
|
||||
setSearchKeyword(query);
|
||||
};
|
||||
// Handle search type change.
|
||||
const handleSearchTypeChange = (searchType) => {
|
||||
remove();
|
||||
setSearchType(searchType.key);
|
||||
|
||||
if (searchKeyword && searchType) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
// Handle overlay of universal search close.
|
||||
const handleClose = () => {
|
||||
closeGlobalSearch();
|
||||
};
|
||||
|
||||
// Handle universal search item select.
|
||||
const handleItemSelect = (item) => {
|
||||
setSelectedItemUniversalSearch(searchType, item.id);
|
||||
closeGlobalSearch();
|
||||
setSearchKeyword('');
|
||||
};
|
||||
const debounceFetch = React.useRef(
|
||||
debounce(() => {
|
||||
refetch();
|
||||
}, 200),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (searchKeyword) {
|
||||
debounceFetch.current();
|
||||
}
|
||||
}, [searchKeyword]);
|
||||
|
||||
// Handles the overlay once be closed.
|
||||
const handleOverlayClosed = () => {
|
||||
setSearchKeyword('');
|
||||
};
|
||||
|
||||
const searchTypeOptions = React.useMemo(
|
||||
() => getUniversalSearchTypeOptions(),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="dashboard__universal-search">
|
||||
<UniversalSearch
|
||||
isOpen={globalSearchShow}
|
||||
isLoading={isSearchFetching}
|
||||
items={data}
|
||||
overlayProps={{
|
||||
onClose: handleClose,
|
||||
onClosed: handleOverlayClosed,
|
||||
}}
|
||||
searchResource={searchType}
|
||||
onQueryChange={handleQueryChange}
|
||||
onSearchTypeChange={handleSearchTypeChange}
|
||||
onItemSelect={handleItemSelect}
|
||||
itemRenderer={DashboardUniversalSearchItem}
|
||||
query={searchKeyword}
|
||||
searchTypeOptions={searchTypeOptions}
|
||||
/>
|
||||
<DashboardUniversalSearchItemActions />
|
||||
<DashboardUniversalSearchHotkeys />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withUniversalSearchActions,
|
||||
withUniversalSearch(({ globalSearchShow, defaultUniversalResourceType }) => ({
|
||||
globalSearchShow,
|
||||
defaultUniversalResourceType,
|
||||
})),
|
||||
)(DashboardUniversalSearch);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { universalSearchInvoiceBind } from '../Sales/Invoices/InvoiceUniversalSearch';
|
||||
import { universalSearchReceiptBind } from '../Sales/Receipts/ReceiptUniversalSearch';
|
||||
import { universalSearchBillBind } from '../Purchases/Bills/BillUniversalSearch';
|
||||
import { universalSearchEstimateBind } from '../Sales/Estimates/EstimatesLanding/EstimateUniversalSearch';
|
||||
import { universalSearchPaymentReceiveBind } from '../Sales/PaymentReceives/PaymentReceiveUniversalSearch';
|
||||
import { universalSearchPaymentMadeBind } from '../Purchases/PaymentMades/PaymentMadeUniversalSearch';
|
||||
import { universalSearchItemBind } from '../Items/ItemsUniversalSearch';
|
||||
import { universalSearchCustomerBind } from '../Customers/CustomersUniversalSearch';
|
||||
import { universalSearchJournalBind } from '../Accounting/ManualJournalUniversalSearch';
|
||||
import { universalSearchAccountBind } from '../Accounts/AccountUniversalSearch';
|
||||
import { universalSearchVendorBind } from '../Vendors/VendorsUniversalSearch';
|
||||
|
||||
// Universal search binds.
|
||||
export const universalSearchBinds = [
|
||||
universalSearchItemBind,
|
||||
universalSearchAccountBind,
|
||||
universalSearchInvoiceBind,
|
||||
universalSearchReceiptBind,
|
||||
universalSearchEstimateBind,
|
||||
universalSearchBillBind,
|
||||
universalSearchPaymentReceiveBind,
|
||||
universalSearchPaymentMadeBind,
|
||||
universalSearchCustomerBind,
|
||||
universalSearchVendorBind,
|
||||
universalSearchJournalBind,
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as R from 'ramda';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import withUniversalSearchActions from './withUniversalSearchActions';
|
||||
|
||||
/**
|
||||
* Universal search hotkey.
|
||||
*/
|
||||
function DashboardUniversalSearchHotkey({
|
||||
openGlobalSearch,
|
||||
}) {
|
||||
useHotkeys('ctrl+o', (event, handle) => {
|
||||
openGlobalSearch();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withUniversalSearchActions
|
||||
)(DashboardUniversalSearchHotkey);
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import withUniversalSearch from './withUniversalSearch';
|
||||
|
||||
import { getUniversalSearchItemsActions } from './utils';
|
||||
|
||||
/**
|
||||
* Universal search selected item action based on each resource type.
|
||||
*/
|
||||
function DashboardUniversalSearchItemActions({
|
||||
searchSelectedResourceType,
|
||||
searchSelectedResourceId,
|
||||
}) {
|
||||
const components = getUniversalSearchItemsActions();
|
||||
|
||||
return components.map((COMPONENT) => (
|
||||
<COMPONENT
|
||||
resourceId={searchSelectedResourceId}
|
||||
resourceType={searchSelectedResourceType}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export default R.compose(
|
||||
withUniversalSearch(
|
||||
({ searchSelectedResourceType, searchSelectedResourceId }) => ({
|
||||
searchSelectedResourceType,
|
||||
searchSelectedResourceId,
|
||||
}),
|
||||
),
|
||||
)(DashboardUniversalSearchItemActions);
|
||||
44
client/src/containers/UniversalSearch/components.js
Normal file
44
client/src/containers/UniversalSearch/components.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { highlightText } from 'utils';
|
||||
import { getUniversalSearchBind } from './utils';
|
||||
|
||||
/**
|
||||
* Default univesal search item component.
|
||||
*/
|
||||
function UniversalSearchItemDetail(item, { handleClick, modifiers, query }) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
disabled={modifiers.disabled}
|
||||
text={
|
||||
<div>
|
||||
<div>{highlightText(item.text, query)}</div>
|
||||
|
||||
{item.subText && (
|
||||
<span class="bp3-text-muted">
|
||||
{highlightText(item.subText, query)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
label={item.label ? highlightText(item.label, query) : ''}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} props
|
||||
* @param {*} actions
|
||||
* @returns
|
||||
*/
|
||||
export const DashboardUniversalSearchItem = (props, actions) => {
|
||||
const itemRenderer = getUniversalSearchBind(props._type, 'itemRenderer');
|
||||
|
||||
return typeof itemRenderer !== 'undefined'
|
||||
? itemRenderer(props, actions)
|
||||
: UniversalSearchItemDetail(props, actions);
|
||||
};
|
||||
44
client/src/containers/UniversalSearch/utils.js
Normal file
44
client/src/containers/UniversalSearch/utils.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { get } from 'lodash';
|
||||
import { universalSearchBinds } from './DashboardUniversalSearchBinds';
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getUniversalSearchBinds = () => {
|
||||
return universalSearchBinds.map((binder) => binder());
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} resourceType
|
||||
* @param {*} key
|
||||
* @returns
|
||||
*/
|
||||
export const getUniversalSearchBind = (resourceType, key) => {
|
||||
const resourceConfig = getUniversalSearchBinds().find(
|
||||
(meta) => meta.resourceType === resourceType,
|
||||
);
|
||||
return key ? get(resourceConfig, key) : resourceConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getUniversalSearchTypeOptions = () => {
|
||||
return getUniversalSearchBinds().map((bind) => ({
|
||||
key: bind.resourceType,
|
||||
label: bind.optionItemLabel,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const getUniversalSearchItemsActions = () => {
|
||||
return getUniversalSearchBinds()
|
||||
.filter((bind) => bind.selectItemAction)
|
||||
.map((bind) => bind.selectItemAction);
|
||||
}
|
||||
18
client/src/containers/UniversalSearch/withUniversalSearch.js
Normal file
18
client/src/containers/UniversalSearch/withUniversalSearch.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default (mapState) => {
|
||||
const mapStateToProps = (state, props) => {
|
||||
const { globalSearch } = state;
|
||||
|
||||
const mapped = {
|
||||
globalSearchShow: globalSearch.isOpen,
|
||||
defaultUniversalResourceType: globalSearch.defaultResourceType,
|
||||
|
||||
searchSelectedResourceType: globalSearch.selectedItem.resourceType,
|
||||
searchSelectedResourceId: globalSearch.selectedItem.resourceId,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import {
|
||||
universalSearchResetResourceType,
|
||||
universalSearchSetResourceType,
|
||||
universalSearchSetSelectedItem,
|
||||
universalSearchResetSelectedItem
|
||||
} from '../../store/search/search.actions';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
openGlobalSearch: () => dispatch({ type: t.OPEN_SEARCH }),
|
||||
closeGlobalSearch: () => dispatch({ type: t.CLOSE_SEARCH }),
|
||||
|
||||
setResourceTypeUniversalSearch: (resourceType) =>
|
||||
dispatch(universalSearchSetResourceType(resourceType)),
|
||||
|
||||
resetResourceTypeUniversalSearch: () =>
|
||||
dispatch(universalSearchResetResourceType()),
|
||||
|
||||
setSelectedItemUniversalSearch: (resourceType, resourceId) =>
|
||||
dispatch(universalSearchSetSelectedItem(resourceType, resourceId)),
|
||||
|
||||
resetSelectedItemUniversalSearch: () =>
|
||||
dispatch(universalSearchResetSelectedItem()),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
31
client/src/containers/Vendors/VendorsUniversalSearch.js
Normal file
31
client/src/containers/Vendors/VendorsUniversalSearch.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { RESOURCES_TYPES } from '../../common/resourcesTypes';
|
||||
import withDrawerActions from '../Drawer/withDrawerActions';
|
||||
|
||||
function VendorUniversalSearchSelectComponent({ resourceType, resourceId }) {
|
||||
if (resourceType === RESOURCES_TYPES.VENDOR) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const VendorUniversalSearchSelectAction = withDrawerActions(
|
||||
VendorUniversalSearchSelectComponent
|
||||
);
|
||||
|
||||
/**
|
||||
* Transformes vendor resource item to search.
|
||||
*/
|
||||
const vendorToSearch = (contact) => ({
|
||||
text: contact.display_name,
|
||||
label: contact.balance > 0 ? contact.formatted_balance + '' : '',
|
||||
reference: contact,
|
||||
});
|
||||
|
||||
/**
|
||||
* Binds universal search invoice configure.
|
||||
*/
|
||||
export const universalSearchVendorBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.VENDOR,
|
||||
optionItemLabel: 'Vendor',
|
||||
selectItemAction: VendorUniversalSearchSelectAction,
|
||||
itemSelect: vendorToSearch,
|
||||
});
|
||||
131
client/src/hooks/query/GenericResource/index.js
Normal file
131
client/src/hooks/query/GenericResource/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useRequestQuery } from '../../useQueryRequest';
|
||||
import { RESOURCES_TYPES } from 'common/resourcesTypes';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} type
|
||||
* @param {string} searchKeyword
|
||||
* @param {*} query
|
||||
* @returns
|
||||
*/
|
||||
export function useResourceData(type, query, props) {
|
||||
const url = getResourceUrlFromType(type);
|
||||
|
||||
return useRequestQuery(
|
||||
['UNIVERSAL_SEARCH', type, query],
|
||||
{ method: 'get', url, params: query },
|
||||
{
|
||||
select: transformResourceData(type),
|
||||
defaultData: {
|
||||
items: [],
|
||||
},
|
||||
...props,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the resource url by the given resource type.
|
||||
* @param {string} type
|
||||
* @returns {string}
|
||||
*/
|
||||
function getResourceUrlFromType(type) {
|
||||
const config = {
|
||||
[RESOURCES_TYPES.INVOICE]: '/sales/invoices',
|
||||
[RESOURCES_TYPES.ESTIMATE]: '/sales/estimates',
|
||||
[RESOURCES_TYPES.ITEM]: '/items',
|
||||
[RESOURCES_TYPES.RECEIPT]: '/sales/receipts',
|
||||
[RESOURCES_TYPES.BILL]: '/purchases/bills',
|
||||
[RESOURCES_TYPES.PAYMENT_RECEIVE]: '/sales/payment_receives',
|
||||
[RESOURCES_TYPES.PAYMENT_MADE]: '/purchases/bill_payments',
|
||||
[RESOURCES_TYPES.CUSTOMER]: '/customers',
|
||||
[RESOURCES_TYPES.VENDOR]: '/vendors',
|
||||
[RESOURCES_TYPES.MANUAL_JOURNAL]: '/manual-journals',
|
||||
[RESOURCES_TYPES.ACCOUNT]: '/accounts',
|
||||
};
|
||||
return config[type] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes invoices to resource data.
|
||||
*/
|
||||
const transformInvoices = (response) => ({
|
||||
items: response.data.sales_invoices,
|
||||
});
|
||||
|
||||
/**
|
||||
* Transformes items to resource data.
|
||||
*/
|
||||
const transformItems = (response) => ({
|
||||
items: response.data.items,
|
||||
});
|
||||
|
||||
/**
|
||||
* Transformes payment receives to resource data.
|
||||
*/
|
||||
const transformPaymentReceives = (response) => ({
|
||||
items: response.data.payment_receives,
|
||||
});
|
||||
|
||||
/**
|
||||
* Transformes customers to resoruce data.
|
||||
*/
|
||||
const transformCustomers = (response) => ({
|
||||
items: response.data.customers,
|
||||
});
|
||||
|
||||
/**
|
||||
* Transformes customers to resoruce data.
|
||||
*/
|
||||
const transformVendors = (response) => ({
|
||||
items: response.data.vendors,
|
||||
});
|
||||
|
||||
|
||||
const transformPaymentMades = (response) => ({
|
||||
items: response.data.bill_payments,
|
||||
});
|
||||
|
||||
const transformSaleReceipts = (response) => ({
|
||||
items: response.data.data,
|
||||
});
|
||||
|
||||
const transformBills = (response) => ({
|
||||
items: response.data.bills,
|
||||
});
|
||||
|
||||
const transformManualJournals = (response) => ({
|
||||
items: response.data.manual_journals,
|
||||
});
|
||||
|
||||
const transformsEstimates = (response) => ({
|
||||
items: response.data.sales_estimates,
|
||||
});
|
||||
|
||||
const transformAccounts = (response) => ({
|
||||
items: response.data.accounts,
|
||||
})
|
||||
|
||||
/**
|
||||
* Detarmines the transformer based on the given resource type.
|
||||
* @param {string} type - Resource type.
|
||||
*/
|
||||
const transformResourceData = (type) => (response) => {
|
||||
const pairs = {
|
||||
[RESOURCES_TYPES.ESTIMATE]: transformsEstimates,
|
||||
[RESOURCES_TYPES.INVOICE]: transformInvoices,
|
||||
[RESOURCES_TYPES.RECEIPT]: transformSaleReceipts,
|
||||
[RESOURCES_TYPES.ITEM]: transformItems,
|
||||
[RESOURCES_TYPES.PAYMENT_RECEIVE]: transformPaymentReceives,
|
||||
[RESOURCES_TYPES.PAYMENT_MADE]: transformPaymentMades,
|
||||
[RESOURCES_TYPES.CUSTOMER]: transformCustomers,
|
||||
[RESOURCES_TYPES.VENDOR]: transformVendors,
|
||||
[RESOURCES_TYPES.BILL]: transformBills,
|
||||
[RESOURCES_TYPES.MANUAL_JOURNAL]: transformManualJournals,
|
||||
[RESOURCES_TYPES.ACCOUNT]: transformAccounts
|
||||
};
|
||||
return {
|
||||
...pairs[type](response),
|
||||
_type: type,
|
||||
};
|
||||
};
|
||||
41
client/src/hooks/query/UniversalSearch/UniversalSearch.js
Normal file
41
client/src/hooks/query/UniversalSearch/UniversalSearch.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getUniversalSearchBind } from '../../../containers/UniversalSearch/utils';
|
||||
import { useResourceData } from '../GenericResource';
|
||||
|
||||
/**
|
||||
* Transformes the resource data to search entries based on
|
||||
* the given resource type.
|
||||
* @param {string} type
|
||||
* @param {any} resource
|
||||
* @returns
|
||||
*/
|
||||
function transfromResourceDataToSearch(resource) {
|
||||
const selectItem = getUniversalSearchBind(resource._type, 'itemSelect');
|
||||
|
||||
return resource.items
|
||||
.map((item) => ({
|
||||
...selectItem ? selectItem(item) : {},
|
||||
_type: resource._type,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} type
|
||||
* @param {*} searchKeyword
|
||||
* @returns
|
||||
*/
|
||||
export function useUniversalSearch(type, searchKeyword, props) {
|
||||
const { data, ...restProps } = useResourceData(
|
||||
type,
|
||||
{
|
||||
search_keyword: searchKeyword,
|
||||
},
|
||||
props,
|
||||
);
|
||||
const searchData = transfromResourceDataToSearch(data);
|
||||
|
||||
return {
|
||||
data: searchData,
|
||||
...restProps,
|
||||
};
|
||||
}
|
||||
@@ -24,3 +24,5 @@ export * from './contacts';
|
||||
export * from './subscriptions';
|
||||
export * from './organization';
|
||||
export * from './landedCost';
|
||||
export * from './UniversalSearch/UniversalSearch';
|
||||
export * from './GenericResource';
|
||||
|
||||
@@ -99,7 +99,7 @@ export function useCloseReceipt(props) {
|
||||
}
|
||||
|
||||
const transformReceipts = (res) => ({
|
||||
receipts: res.data.sale_receipts,
|
||||
receipts: res.data.data,
|
||||
pagination: transformPagination(res.data.pagination),
|
||||
filterMeta: res.data.filter_meta,
|
||||
});
|
||||
|
||||
@@ -1202,6 +1202,7 @@
|
||||
"universal_search.enter_text": "To select",
|
||||
"universal_search.close_text": "To close",
|
||||
"universal_seach.navigate_text": "To navigate",
|
||||
"universal_search.loading": "Loading...",
|
||||
"pdf_preview.dialog.title": "PDF Preview",
|
||||
"pdf_preview.download.button": "Download",
|
||||
"pdf_preview.preview.button": "Preview",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import React, { lazy } from 'react';
|
||||
import { lazy } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { RESOURCES_TYPES } from '../common/resourcesTypes';
|
||||
|
||||
// const BASE_URL = '/dashboard';
|
||||
|
||||
@@ -12,6 +13,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('accounts_chart'),
|
||||
hotkey: 'shift+a',
|
||||
pageTitle: intl.get('accounts_chart'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
|
||||
},
|
||||
// Accounting.
|
||||
{
|
||||
@@ -24,6 +26,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_journal'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||
},
|
||||
{
|
||||
path: `/manual-journals/:id/edit`,
|
||||
@@ -34,6 +37,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_journal'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||
},
|
||||
{
|
||||
path: `/manual-journals`,
|
||||
@@ -43,6 +47,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('manual_journals'),
|
||||
hotkey: 'shift+m',
|
||||
pageTitle: intl.get('manual_journals'),
|
||||
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||
},
|
||||
{
|
||||
path: `/items/categories`,
|
||||
@@ -51,6 +56,7 @@ export const getDashboardRoutes = () => [
|
||||
),
|
||||
breadcrumb: intl.get('categories'),
|
||||
pageTitle: intl.get('category_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
// Items.
|
||||
{
|
||||
@@ -60,6 +66,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('edit_item'),
|
||||
pageTitle: intl.get('edit_item'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
{
|
||||
path: `/items/new?duplicate=/:id`,
|
||||
@@ -67,6 +74,7 @@ export const getDashboardRoutes = () => [
|
||||
loader: () => import('containers/Items/ItemFormPage'),
|
||||
}),
|
||||
breadcrumb: intl.get('duplicate_item'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
{
|
||||
path: `/items/new`,
|
||||
@@ -76,6 +84,7 @@ export const getDashboardRoutes = () => [
|
||||
hotkey: 'ctrl+shift+w',
|
||||
pageTitle: intl.get('new_item'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
{
|
||||
path: `/items`,
|
||||
@@ -83,6 +92,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('items'),
|
||||
hotkey: 'shift+w',
|
||||
pageTitle: intl.get('items_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
|
||||
// Inventory adjustments.
|
||||
@@ -93,6 +103,7 @@ export const getDashboardRoutes = () => [
|
||||
),
|
||||
breadcrumb: intl.get('inventory_adjustments'),
|
||||
pageTitle: intl.get('inventory_adjustment_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||
},
|
||||
|
||||
// Financial Reports.
|
||||
@@ -107,6 +118,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('general_ledger'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.INVENTORY_ADJUSTMENT,
|
||||
},
|
||||
{
|
||||
path: `/financial-reports/balance-sheet`,
|
||||
@@ -323,6 +335,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_expense'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
|
||||
},
|
||||
{
|
||||
path: `/expenses/:id/edit`,
|
||||
@@ -333,6 +346,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_expense'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
|
||||
},
|
||||
{
|
||||
path: `/expenses`,
|
||||
@@ -342,6 +356,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('expenses_list'),
|
||||
pageTitle: intl.get('expenses_list'),
|
||||
hotkey: 'shift+x',
|
||||
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
|
||||
},
|
||||
|
||||
// Customers
|
||||
@@ -354,6 +369,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('edit_customer'),
|
||||
pageTitle: intl.get('edit_customer'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||
},
|
||||
{
|
||||
path: `/customers/new`,
|
||||
@@ -365,6 +381,7 @@ export const getDashboardRoutes = () => [
|
||||
hotkey: 'ctrl+shift+c',
|
||||
pageTitle: intl.get('new_customer'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||
},
|
||||
{
|
||||
path: `/customers`,
|
||||
@@ -374,6 +391,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('customers'),
|
||||
hotkey: 'shift+c',
|
||||
pageTitle: intl.get('customers_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||
},
|
||||
{
|
||||
path: `/customers/contact_duplicate=/:id`,
|
||||
@@ -384,6 +402,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('duplicate_customer'),
|
||||
pageTitle: intl.get('new_customer'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||
},
|
||||
|
||||
// Vendors
|
||||
@@ -396,6 +415,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('edit_vendor'),
|
||||
pageTitle: intl.get('edit_vendor'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.VENDOR,
|
||||
},
|
||||
{
|
||||
path: `/vendors/new`,
|
||||
@@ -407,6 +427,7 @@ export const getDashboardRoutes = () => [
|
||||
hotkey: 'ctrl+shift+v',
|
||||
pageTitle: intl.get('new_vendor'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.VENDOR,
|
||||
},
|
||||
{
|
||||
path: `/vendors`,
|
||||
@@ -416,6 +437,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('vendors'),
|
||||
hotkey: 'shift+v',
|
||||
pageTitle: intl.get('vendors_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.VENDOR,
|
||||
},
|
||||
{
|
||||
path: `/vendors/contact_duplicate=/:id`,
|
||||
@@ -426,6 +448,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('duplicate_vendor'),
|
||||
pageTitle: intl.get('new_vendor'),
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.VENDOR,
|
||||
},
|
||||
|
||||
// Estimates
|
||||
@@ -439,6 +462,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_estimate'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
|
||||
},
|
||||
{
|
||||
path: `/invoices/new?from_estimate_id=/:id`,
|
||||
@@ -450,6 +474,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_estimate'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.INVOICE,
|
||||
},
|
||||
{
|
||||
path: `/estimates/new`,
|
||||
@@ -462,6 +487,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_estimate'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
|
||||
},
|
||||
{
|
||||
path: `/estimates`,
|
||||
@@ -472,6 +498,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('estimates_list'),
|
||||
hotkey: 'shift+e',
|
||||
pageTitle: intl.get('estimates_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
|
||||
},
|
||||
|
||||
// Invoices.
|
||||
@@ -485,6 +512,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_invoice'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.INVOICE,
|
||||
},
|
||||
{
|
||||
path: `/invoices/new`,
|
||||
@@ -497,6 +525,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_invoice'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.INVOICE,
|
||||
},
|
||||
{
|
||||
path: `/invoices`,
|
||||
@@ -506,6 +535,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('invoices_list'),
|
||||
hotkey: 'shift+i',
|
||||
pageTitle: intl.get('invoices_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.INVOICE,
|
||||
},
|
||||
|
||||
// Sales Receipts.
|
||||
@@ -519,6 +549,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_receipt'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
|
||||
},
|
||||
{
|
||||
path: `/receipts/new`,
|
||||
@@ -531,6 +562,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_receipt'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
|
||||
},
|
||||
{
|
||||
path: `/receipts`,
|
||||
@@ -540,6 +572,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('receipts_list'),
|
||||
hotkey: 'shift+r',
|
||||
pageTitle: intl.get('receipts_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
|
||||
},
|
||||
|
||||
// Payment receives
|
||||
@@ -555,6 +588,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_payment_receive'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
|
||||
},
|
||||
{
|
||||
path: `/payment-receives/new`,
|
||||
@@ -568,6 +602,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_payment_receive'),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
|
||||
},
|
||||
{
|
||||
path: `/payment-receives`,
|
||||
@@ -578,6 +613,7 @@ export const getDashboardRoutes = () => [
|
||||
),
|
||||
breadcrumb: intl.get('payment_receives_list'),
|
||||
pageTitle: intl.get('payment_receives_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
|
||||
},
|
||||
|
||||
// Bills
|
||||
@@ -591,6 +627,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_bill'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.BILL,
|
||||
},
|
||||
{
|
||||
path: `/bills/new`,
|
||||
@@ -603,6 +640,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_bill'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.BILL,
|
||||
},
|
||||
{
|
||||
path: `/bills`,
|
||||
@@ -612,6 +650,7 @@ export const getDashboardRoutes = () => [
|
||||
breadcrumb: intl.get('bills_list'),
|
||||
hotkey: 'shift+b',
|
||||
pageTitle: intl.get('bills_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.BILL,
|
||||
},
|
||||
|
||||
// Subscription billing.
|
||||
@@ -633,6 +672,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('edit_payment_made'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
|
||||
},
|
||||
{
|
||||
path: `/payment-mades/new`,
|
||||
@@ -646,6 +686,7 @@ export const getDashboardRoutes = () => [
|
||||
pageTitle: intl.get('new_payment_made'),
|
||||
sidebarExpand: false,
|
||||
backLink: true,
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
|
||||
},
|
||||
{
|
||||
path: `/payment-mades`,
|
||||
@@ -656,6 +697,7 @@ export const getDashboardRoutes = () => [
|
||||
),
|
||||
breadcrumb: intl.get('payment_made_list'),
|
||||
pageTitle: intl.get('payment_made_list'),
|
||||
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
|
||||
},
|
||||
// Homepage
|
||||
{
|
||||
|
||||
@@ -444,4 +444,28 @@ export default {
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
'universal-search': {
|
||||
path: [
|
||||
'M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z'
|
||||
],
|
||||
viewBox: '0 0 20 20',
|
||||
},
|
||||
"arrow-down-24": {
|
||||
path: [
|
||||
'M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z',
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
"arrow-up-24": {
|
||||
path: [
|
||||
'M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z'
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
"caret-right-16": {
|
||||
path: [
|
||||
'M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z'
|
||||
],
|
||||
viewBox: '0 0 16 16',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,3 +20,36 @@ export function generalSearch(name, result) {
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
export function universalSearchSetResourceType(resourceType) {
|
||||
return {
|
||||
type: t.UNIVERSAL_SEARCH_SET_RESOURCE_TYPE,
|
||||
payload: {
|
||||
resourceType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function universalSearchResetResourceType() {
|
||||
return {
|
||||
type: t.UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function universalSearchSetSelectedItem(resourceType, resourceId) {
|
||||
return {
|
||||
type: t.UNIVERSAL_SEARCH_SET_ITEM_SELECT,
|
||||
payload: {
|
||||
resourceType,
|
||||
resourceId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function universalSearchResetSelectedItem() {
|
||||
return {
|
||||
type: t.UNIVERSAL_SEARCH_RESET_ITEM_SELECT,
|
||||
payload: {}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import t from 'store/types';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
const DEFAULT_RESOURCE_TYPE = 'customer';
|
||||
|
||||
const initialState = {
|
||||
isOpen: false,
|
||||
defaultResourceType: DEFAULT_RESOURCE_TYPE,
|
||||
selectedItem: {},
|
||||
};
|
||||
|
||||
export default createReducer(initialState, {
|
||||
@@ -13,4 +17,23 @@ export default createReducer(initialState, {
|
||||
[t.CLOSE_SEARCH]: (state, action) => {
|
||||
state.isOpen = false;
|
||||
},
|
||||
|
||||
[t.UNIVERSAL_SEARCH_SET_RESOURCE_TYPE]: (state, action) => {
|
||||
state.defaultResourceType = action.payload.resourceType;
|
||||
},
|
||||
|
||||
[t.UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE]: (state, action) => {
|
||||
state.defaultResourceType = DEFAULT_RESOURCE_TYPE;
|
||||
},
|
||||
|
||||
[t.UNIVERSAL_SEARCH_SET_ITEM_SELECT]: (state, action) => {
|
||||
state.selectedItem = {
|
||||
resourceId: action.payload.resourceId,
|
||||
resourceType: action.payload.resourceType,
|
||||
};
|
||||
},
|
||||
|
||||
[t.UNIVERSAL_SEARCH_RESET_ITEM_SELECT]: (state, action) => {
|
||||
state.selectedItem = {};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,4 +2,8 @@ export default {
|
||||
SEARCH_TYPE: 'SEARCH_TYPE',
|
||||
OPEN_SEARCH: 'OPEN_SEARCH',
|
||||
CLOSE_SEARCH: 'CLOSE_SEARCH',
|
||||
UNIVERSAL_SEARCH_SET_RESOURCE_TYPE: 'UNIVERSAL_SEARCH_SET_RESOURCE_TYPE',
|
||||
UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE: 'UNIVERSAL_SEARCH_RESET_RESOURCE_TYPE',
|
||||
UNIVERSAL_SEARCH_SET_ITEM_SELECT: 'UNIVERSAL_SEARCH_SET_ITEM_SELECT',
|
||||
UNIVERSAL_SEARCH_RESET_ITEM_SELECT: 'UNIVERSAL_SEARCH_RESET_ITEM_SELECT'
|
||||
};
|
||||
|
||||
@@ -226,16 +226,16 @@ html[lang^="ar"] {
|
||||
z-index: 9999999;
|
||||
margin: 6px;
|
||||
|
||||
.bp3-button + .bp3-button{
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.bp3-button{
|
||||
border-color: rgba(0, 0, 0, 0.25);
|
||||
color: rgb(25, 32, 37);
|
||||
min-height: 30px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
|
||||
& + .bp3-button{
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
219
client/src/style/components/UniversalSearch.scss
Normal file
219
client/src/style/components/UniversalSearch.scss
Normal file
@@ -0,0 +1,219 @@
|
||||
.universal-search {
|
||||
position: fixed;
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(16, 22, 26, .1),
|
||||
0 4px 8px rgba(16, 22, 26, .2),
|
||||
0 18px 46px 6px rgba(16, 22, 26, .2);
|
||||
left: calc(50% - 250px);
|
||||
top: 20vh;
|
||||
width: 500px;
|
||||
z-index: 20;
|
||||
|
||||
&.bp3-overlay-appear,
|
||||
&.bp3-overlay-enter {
|
||||
filter: blur(20px);
|
||||
opacity: .2
|
||||
}
|
||||
|
||||
&.bp3-overlay-appear-active,
|
||||
&.bp3-overlay-enter-active {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transition-delay: 0;
|
||||
transition-duration: .2s;
|
||||
transition-property: filter, opacity;
|
||||
transition-timing-function: cubic-bezier(.4, 1, .75, .9)
|
||||
}
|
||||
|
||||
&.bp3-overlay-exit {
|
||||
filter: blur(0);
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
&.bp3-overlay-exit-active {
|
||||
filter: blur(20px);
|
||||
opacity: .2;
|
||||
transition-delay: 0;
|
||||
transition-duration: .2s;
|
||||
transition-property: filter, opacity;
|
||||
transition-timing-function: cubic-bezier(.4, 1, .75, .9)
|
||||
}
|
||||
|
||||
&__omnibar {
|
||||
|
||||
.bp3-input-group {
|
||||
|
||||
.bp3-icon {
|
||||
|
||||
svg {
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
fill-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bp3-input-group .bp3-input {
|
||||
border: 0;
|
||||
box-shadow: 0 0 0 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.bp3-input-group.bp3-large .bp3-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.bp3-input {}
|
||||
|
||||
.bp3-input-group {
|
||||
|
||||
.bp3-icon {
|
||||
margin: 16px;
|
||||
color: #5c707f;
|
||||
|
||||
svg {
|
||||
stroke-width: 2;
|
||||
--text-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-menu {
|
||||
border-top: 1px solid #d3dce2;
|
||||
|
||||
.bp3-menu-item {
|
||||
.bp3-text-muted {
|
||||
font-size: 12px;
|
||||
|
||||
.bp3-icon {
|
||||
color: #8499a7;
|
||||
}
|
||||
}
|
||||
&.bp3-intent-primary{
|
||||
|
||||
&.bp3-active{
|
||||
background-color: rgb(235, 241, 246);
|
||||
color: #252b30;
|
||||
|
||||
.bp3-menu-item-label{
|
||||
color: #5c7080;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&-label {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-input-action {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__type-select-overlay {
|
||||
|
||||
.bp3-button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 12px 12px;
|
||||
border-top: 1px solid #d3dce2;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__action {
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.bp3-tag {
|
||||
background: #708392;
|
||||
}
|
||||
|
||||
&--arrows {
|
||||
|
||||
.bp3-tag {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
margin-left: 4px;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
height: 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {}
|
||||
|
||||
|
||||
&-input-right-elements {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
|
||||
.bp3-spinner {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
&--invoice,
|
||||
&--estimate,
|
||||
&--bill,
|
||||
&--receipt {
|
||||
|
||||
.amount {
|
||||
color: #252b30;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 13px;
|
||||
|
||||
&.status-warning {
|
||||
color: rgb(236, 91, 10);
|
||||
}
|
||||
|
||||
&.status-success {
|
||||
color: #249017;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.universal-search-overlay .bp3-overlay-backdrop {
|
||||
background: rgba(0, 10, 30, 0.3);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
@@ -751,4 +752,39 @@ export const RESORUCE_TYPE = {
|
||||
ACCOUNTS: 'account',
|
||||
ITEMS: 'items',
|
||||
|
||||
}
|
||||
|
||||
function escapeRegExpChars(text) {
|
||||
return text.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
|
||||
}
|
||||
|
||||
export function highlightText(text, query) {
|
||||
let lastIndex = 0;
|
||||
const words = query
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 0)
|
||||
.map(escapeRegExpChars);
|
||||
if (words.length === 0) {
|
||||
return [text];
|
||||
}
|
||||
const regexp = new RegExp(words.join('|'), 'gi');
|
||||
const tokens = [];
|
||||
while (true) {
|
||||
const match = regexp.exec(text);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
const length = match[0].length;
|
||||
const before = text.slice(lastIndex, regexp.lastIndex - length);
|
||||
if (before.length > 0) {
|
||||
tokens.push(before);
|
||||
}
|
||||
lastIndex = regexp.lastIndex;
|
||||
tokens.push(<strong key={lastIndex}>{match[0]}</strong>);
|
||||
}
|
||||
const rest = text.slice(lastIndex);
|
||||
if (rest.length > 0) {
|
||||
tokens.push(rest);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
Reference in New Issue
Block a user