mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
refactor: update UniversalSearch components with TypeScript and TextStatus
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import React, { KeyboardEvent, ReactNode } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { isUndefined } from 'lodash';
|
||||
import {
|
||||
Overlay,
|
||||
@@ -10,11 +8,14 @@ import {
|
||||
MenuItem,
|
||||
Spinner,
|
||||
Intent,
|
||||
OverlayProps,
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { QueryList } from '@blueprintjs/select';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import { Icon, If, ListSelect, FormattedMessage as T } from '@/components';
|
||||
import { QueryList, ItemRenderer } from '@blueprintjs/select';
|
||||
import { x } from '@xstyled/emotion';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon, If, FormattedMessage as T } from '@/components';
|
||||
import { Select } from '@blueprintjs-formik/select';
|
||||
import {
|
||||
UniversalSearchProvider,
|
||||
useUniversalSearchContext,
|
||||
@@ -22,59 +23,297 @@ import {
|
||||
import { filterItemsByResourceType } from './utils';
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
|
||||
// Resource type from RESOURCES_TYPES constant
|
||||
type ResourceType = string;
|
||||
|
||||
// Search type option item
|
||||
interface SearchTypeOption {
|
||||
key: ResourceType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Universal search item
|
||||
interface UniversalSearchItem {
|
||||
id: number | string;
|
||||
_type: ResourceType;
|
||||
text: string;
|
||||
subText?: string;
|
||||
label?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// CSS styles for complex selectors
|
||||
const overlayStyles = css`
|
||||
.bp4-overlay-appear,
|
||||
.bp4-overlay-enter {
|
||||
filter: blur(20px);
|
||||
opacity: 0.2;
|
||||
}
|
||||
.bp4-overlay-appear-active,
|
||||
.bp4-overlay-enter-active {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
transition:
|
||||
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
|
||||
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
|
||||
}
|
||||
.bp4-overlay-exit {
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
.bp4-overlay-exit-active {
|
||||
filter: blur(20px);
|
||||
opacity: 0.2;
|
||||
transition:
|
||||
filter 0.2s cubic-bezier(0.4, 1, 0.75, 0.9),
|
||||
opacity 0.2s cubic-bezier(0.4, 1, 0.75, 0.9);
|
||||
}
|
||||
`;
|
||||
|
||||
const containerStyles = css`
|
||||
position: fixed;
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
background-color: var(--color-universal-search-background);
|
||||
border-radius: 3px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(16, 22, 26, 0.1),
|
||||
0 4px 8px rgba(16, 22, 26, 0.2),
|
||||
0 18px 46px 6px rgba(16, 22, 26, 0.2);
|
||||
left: calc(50% - 250px);
|
||||
top: 20vh;
|
||||
width: 500px;
|
||||
z-index: 20;
|
||||
|
||||
.bp4-input-group {
|
||||
.bp4-icon {
|
||||
margin: 16px;
|
||||
color: var(--color-universal-search-icon);
|
||||
|
||||
svg {
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
fill-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
--text-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-input-group .bp4-input {
|
||||
border: 0;
|
||||
box-shadow: 0 0 0 0;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.bp4-input-group.bp4-large .bp4-input:not(:first-child) {
|
||||
padding-left: 50px !important;
|
||||
}
|
||||
.bp4-input-group.bp4-large .bp4-input:not(:last-child) {
|
||||
padding-right: 130px !important;
|
||||
}
|
||||
|
||||
.bp4-menu {
|
||||
border-top: 1px solid var(--color-universal-search-menu-border);
|
||||
max-height: calc(60vh - 20px);
|
||||
overflow: auto;
|
||||
|
||||
.bp4-menu-item {
|
||||
.bp4-text-muted {
|
||||
font-size: 12px;
|
||||
|
||||
.bp4-icon {
|
||||
color: var(--bp4-gray-600);
|
||||
}
|
||||
}
|
||||
&.bp4-intent-primary {
|
||||
&.bp4-active {
|
||||
background-color: var(--bp4-blue-100);
|
||||
color: var(--bp4-dark-gray-800);
|
||||
|
||||
.bp4-menu-item-label {
|
||||
color: var(--bp4-gray-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
flex-direction: row;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp4-input-action {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const inputRightElementsStyles = css`
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
|
||||
.bp4-spinner {
|
||||
margin-right: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const footerStyles = css`
|
||||
padding: 12px 12px;
|
||||
border-top: 1px solid var(--color-universal-search-footer-divider);
|
||||
`;
|
||||
|
||||
const actionBaseStyles = css`
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.bp4-tag {
|
||||
background: var(--color-universal-search-tag-background);
|
||||
color: var(--color-universal-search-tag-text);
|
||||
}
|
||||
`;
|
||||
|
||||
const actionArrowsStyles = css`
|
||||
&:not(:first-of-type) {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.bp4-tag {
|
||||
background: var(--color-universal-search-tag-background);
|
||||
color: var(--color-universal-search-tag-text);
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
margin-left: 4px;
|
||||
|
||||
svg {
|
||||
fill: var(--color-universal-search-tag-text);
|
||||
height: 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// UniversalSearchInputRightElements props
|
||||
interface UniversalSearchInputRightElementsProps {
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search input action.
|
||||
*/
|
||||
function UniversalSearchInputRightElements({ onSearchTypeChange }) {
|
||||
const { isLoading, searchType, defaultSearchResource, searchTypeOptions } =
|
||||
function UniversalSearchInputRightElements({
|
||||
onSearchTypeChange,
|
||||
}: UniversalSearchInputRightElementsProps) {
|
||||
const { isLoading, searchType, searchTypeOptions } =
|
||||
useUniversalSearchContext();
|
||||
|
||||
// Find the currently selected item object.
|
||||
const selectedItem = searchTypeOptions.find(
|
||||
(item) => item.key === searchType,
|
||||
);
|
||||
|
||||
// Handle search type option change.
|
||||
const handleSearchTypeChange = (option) => {
|
||||
onSearchTypeChange && onSearchTypeChange(option);
|
||||
const handleSearchTypeChange = (option: SearchTypeOption) => {
|
||||
onSearchTypeChange?.(option);
|
||||
};
|
||||
|
||||
// Item renderer for the select dropdown.
|
||||
const itemRenderer: ItemRenderer<SearchTypeOption> = (
|
||||
item,
|
||||
{ handleClick },
|
||||
) => {
|
||||
return <MenuItem text={item.label} key={item.key} onClick={handleClick} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={CLASSES.UNIVERSAL_SEARCH_INPUT_RIGHT_ELEMENTS}>
|
||||
<x.div display="flex" m="10px" className={inputRightElementsStyles}>
|
||||
<If condition={isLoading}>
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={18} value={null} />
|
||||
<Spinner tagName="div" intent={Intent.NONE} size={18} />
|
||||
</If>
|
||||
|
||||
<ListSelect
|
||||
<Select<SearchTypeOption>
|
||||
items={searchTypeOptions}
|
||||
itemRenderer={itemRenderer}
|
||||
onItemSelect={handleSearchTypeChange}
|
||||
selectedValue={selectedItem?.key}
|
||||
valueAccessor={'key'}
|
||||
labelAccessor={'label'}
|
||||
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,
|
||||
}}
|
||||
input={({ activeItem }) => (
|
||||
<Button minimal={true} text={activeItem?.label} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// QueryList renderer props
|
||||
interface QueryListRendererProps {
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
handleQueryChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
/** Item list element */
|
||||
itemList: ReactNode;
|
||||
/** Class name */
|
||||
className?: string;
|
||||
/** Handle key down */
|
||||
handleKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
||||
/** Handle key up */
|
||||
handleKeyUp?: (event: KeyboardEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
// UniversalSearchQueryList props
|
||||
interface UniversalSearchQueryListProps {
|
||||
/** Whether the search is open */
|
||||
isOpen: boolean;
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
/** Current search type */
|
||||
searchType: ResourceType;
|
||||
/** Items to display */
|
||||
items: UniversalSearchItem[];
|
||||
/** Renderer for items */
|
||||
itemRenderer?: ItemRenderer<UniversalSearchItem>;
|
||||
/** Callback when an item is selected */
|
||||
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
onQueryChange?: (query: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search query list.
|
||||
*/
|
||||
function UniversalSearchQueryList(props) {
|
||||
const { isOpen, isLoading, onSearchTypeChange, searchType, ...restProps } =
|
||||
props;
|
||||
|
||||
function UniversalSearchQueryList({
|
||||
isOpen,
|
||||
isLoading,
|
||||
onSearchTypeChange,
|
||||
...restProps
|
||||
}: UniversalSearchQueryListProps) {
|
||||
return (
|
||||
<QueryList
|
||||
{...restProps}
|
||||
<QueryList<UniversalSearchItem>
|
||||
{...(restProps as any)}
|
||||
initialContent={null}
|
||||
renderer={(listProps) => (
|
||||
renderer={(listProps: QueryListRendererProps) => (
|
||||
<UniversalSearchBar
|
||||
isOpen={isOpen}
|
||||
onSearchTypeChange={onSearchTypeChange}
|
||||
@@ -100,47 +339,53 @@ function UniversalSearchQueryList(props) {
|
||||
*/
|
||||
function UniversalQuerySearchActions() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTIONS)}>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_SELECT)}>
|
||||
<x.div display="flex">
|
||||
<x.div className={actionBaseStyles}>
|
||||
<Tag>ENTER</Tag>
|
||||
<span class={'text'}>{intl.get('universal_search.enter_text')}</span>
|
||||
</div>
|
||||
<x.span ml="6px">{intl.get('universal_search.enter_text')}</x.span>
|
||||
</x.div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_CLOSE)}>
|
||||
<x.div className={actionBaseStyles}>
|
||||
<Tag>ESC</Tag>{' '}
|
||||
<span class={'text'}>{intl.get('universal_search.close_text')}</span>
|
||||
</div>
|
||||
<x.span ml="6px">{intl.get('universal_search.close_text')}</x.span>
|
||||
</x.div>
|
||||
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_ACTION_ARROWS)}>
|
||||
<x.div className={actionArrowsStyles}>
|
||||
<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>
|
||||
<x.span ml="6px">{intl.get('universal_seach.navigate_text')}</x.span>
|
||||
</x.div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// UniversalSearchBar props
|
||||
interface UniversalSearchBarProps extends QueryListRendererProps {
|
||||
/** Whether the search is open */
|
||||
isOpen: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (option: SearchTypeOption) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search input bar with items list.
|
||||
*/
|
||||
function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
|
||||
function UniversalSearchBar({
|
||||
isOpen,
|
||||
onSearchTypeChange,
|
||||
...listProps
|
||||
}: UniversalSearchBarProps) {
|
||||
const { handleKeyDown, handleKeyUp } = listProps;
|
||||
const handlers = isOpen
|
||||
? { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.UNIVERSAL_SEARCH_OMNIBAR,
|
||||
listProps.className,
|
||||
)}
|
||||
{...handlers}
|
||||
>
|
||||
<x.div {...handlers}>
|
||||
<InputGroup
|
||||
large={true}
|
||||
leftIcon={<Icon icon={'universal-search'} iconSize={20} />}
|
||||
@@ -155,17 +400,44 @@ function UniversalSearchBar({ isOpen, onSearchTypeChange, ...listProps }) {
|
||||
autoFocus={true}
|
||||
/>
|
||||
{listProps.itemList}
|
||||
</div>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
// UniversalSearch props
|
||||
export interface UniversalSearchProps {
|
||||
/** Default search resource type */
|
||||
defaultSearchResource?: ResourceType;
|
||||
/** Controlled search resource type */
|
||||
searchResource?: ResourceType;
|
||||
/** Overlay props */
|
||||
overlayProps?: OverlayProps;
|
||||
/** Whether the search overlay is open */
|
||||
isOpen: boolean;
|
||||
/** Whether the search is loading */
|
||||
isLoading: boolean;
|
||||
/** Callback when search type changes */
|
||||
onSearchTypeChange?: (resource: SearchTypeOption) => void;
|
||||
/** Items to display */
|
||||
items: UniversalSearchItem[];
|
||||
/** Available search type options */
|
||||
searchTypeOptions: SearchTypeOption[];
|
||||
/** Renderer for items */
|
||||
itemRenderer?: ItemRenderer<UniversalSearchItem>;
|
||||
/** Callback when an item is selected */
|
||||
onItemSelect?: (item: UniversalSearchItem, event?: any) => void;
|
||||
/** Current query string */
|
||||
query: string;
|
||||
/** Callback when query changes */
|
||||
onQueryChange?: (query: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal search.
|
||||
*/
|
||||
export function UniversalSearch({
|
||||
defaultSearchResource,
|
||||
searchResource,
|
||||
|
||||
overlayProps,
|
||||
isOpen,
|
||||
isLoading,
|
||||
@@ -173,9 +445,9 @@ export function UniversalSearch({
|
||||
items,
|
||||
searchTypeOptions,
|
||||
...queryListProps
|
||||
}) {
|
||||
}: UniversalSearchProps) {
|
||||
// Search type state.
|
||||
const [searchType, setSearchType] = React.useState(
|
||||
const [searchType, setSearchType] = React.useState<ResourceType>(
|
||||
defaultSearchResource || RESOURCES_TYPES.CUSTOMER,
|
||||
);
|
||||
// Handle search resource type controlled mode.
|
||||
@@ -189,9 +461,9 @@ export function UniversalSearch({
|
||||
}, [searchResource, defaultSearchResource]);
|
||||
|
||||
// Handle search type change.
|
||||
const handleSearchTypeChange = (searchTypeResource) => {
|
||||
const handleSearchTypeChange = (searchTypeResource: SearchTypeOption) => {
|
||||
setSearchType(searchTypeResource.key);
|
||||
onSearchTypeChange && onSearchTypeChange(searchTypeResource);
|
||||
onSearchTypeChange?.(searchTypeResource);
|
||||
};
|
||||
// Filters query list items based on the given search type.
|
||||
const filteredItems = filterItemsByResourceType(items, searchType);
|
||||
@@ -200,7 +472,7 @@ export function UniversalSearch({
|
||||
<Overlay
|
||||
hasBackdrop={true}
|
||||
isOpen={isOpen}
|
||||
className={classNames(CLASSES.UNIVERSAL_SEARCH_OVERLAY)}
|
||||
className={overlayStyles}
|
||||
{...overlayProps}
|
||||
>
|
||||
<UniversalSearchProvider
|
||||
@@ -209,7 +481,7 @@ export function UniversalSearch({
|
||||
defaultSearchResource={defaultSearchResource}
|
||||
searchTypeOptions={searchTypeOptions}
|
||||
>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH)}>
|
||||
<x.div className={containerStyles}>
|
||||
<UniversalSearchQueryList
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
@@ -218,10 +490,10 @@ export function UniversalSearch({
|
||||
{...queryListProps}
|
||||
items={filteredItems}
|
||||
/>
|
||||
<div className={classNames(CLASSES.UNIVERSAL_SEARCH_FOOTER)}>
|
||||
<x.div className={footerStyles}>
|
||||
<UniversalQuerySearchActions />
|
||||
</div>
|
||||
</div>
|
||||
</x.div>
|
||||
</x.div>
|
||||
</UniversalSearchProvider>
|
||||
</Overlay>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user