mirror of https://github.com/buster-so/buster.git
loading state
This commit is contained in:
parent
75e0d1bb42
commit
3e606bff40
|
@ -0,0 +1 @@
|
|||
|
|
@ -8,8 +8,6 @@ export const IndeterminateLinearLoader: React.FC<{
|
|||
trackColor?: string;
|
||||
valueColor?: string;
|
||||
}> = React.memo(({ className = '', trackColor, valueColor, style, height = 2 }) => {
|
||||
// const { styles, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`h-1.5 w-full overflow-hidden bg-gray-200 ${className}`}
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import type { SearchModalContentProps } from './search-modal.types';
|
||||
|
||||
export const SearchEmptyState: React.FC<Pick<SearchModalContentProps, 'emptyState'>> = React.memo(
|
||||
({ emptyState }) => {
|
||||
({ emptyState = 'No results found' }) => {
|
||||
return (
|
||||
<Command.Empty className="text-gray-light h-full w-full flex-1 flex justify-center items-center text-lg">
|
||||
{emptyState}
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import { Command } from 'cmdk';
|
||||
import React from 'react';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SearchModalContentProps } from './search-modal.types';
|
||||
|
||||
export const SearchInput: React.FC<
|
||||
Pick<SearchModalContentProps, 'onSearchChange' | 'placeholder' | 'filterContent'> & {
|
||||
Pick<
|
||||
SearchModalContentProps,
|
||||
'placeholder' | 'filterContent' | 'isModalOpen' | 'onChangeValue'
|
||||
> & {
|
||||
searchValue: string;
|
||||
}
|
||||
> = React.memo(({ searchValue, onSearchChange, placeholder, filterContent }) => {
|
||||
> = React.memo(({ searchValue, onChangeValue, placeholder, filterContent, isModalOpen }) => {
|
||||
const debouncedAutoFocus = useDebounce(isModalOpen, { wait: 100 });
|
||||
|
||||
return (
|
||||
<div className="flex min-h-12 items-center space-x-3 justify-between mx-6">
|
||||
<Command.Input
|
||||
className={cn('text-md placeholder:text-gray-light')}
|
||||
value={searchValue}
|
||||
placeholder={placeholder}
|
||||
onValueChange={onSearchChange}
|
||||
autoFocus
|
||||
onValueChange={onChangeValue}
|
||||
autoFocus={debouncedAutoFocus}
|
||||
/>
|
||||
{filterContent}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { Command } from 'cmdk';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { IndeterminateLinearLoader } from '../../loaders';
|
||||
|
||||
export const SearchLoading = ({ loading = false }: { loading?: boolean }) => {
|
||||
return (
|
||||
<Command.Loading className="w-full border-b swag relative">
|
||||
{loading && (
|
||||
<IndeterminateLinearLoader
|
||||
className={cn('w-full absolute top-0 left-0 right-0')}
|
||||
height={0.5}
|
||||
/>
|
||||
)}
|
||||
</Command.Loading>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,16 @@
|
|||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '../../modal/ModalBase';
|
||||
import { SearchModalContent } from './SearchModalContent';
|
||||
import type { SearchModalProps } from './search-modal.types';
|
||||
|
||||
export const SearchModal = () => {
|
||||
return <div>SearchModal</div>;
|
||||
export const SearchModal: React.FC<SearchModalProps> = ({ open, onClose, ...props }) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(x) => x}>
|
||||
<DialogTitle hidden>{'Search Modal'}</DialogTitle>
|
||||
<DialogDescription hidden>{'This modal is used to search for items'}</DialogDescription>
|
||||
<DialogContent showClose={false} className="overflow-hidden max-w-fit max-h-fit">
|
||||
<SearchModalContent {...props} isModalOpen={open} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { fn } from 'storybook/test';
|
||||
import HouseIcon from '@/components/ui/icons/NucleoIconOutlined/house';
|
||||
import { createDayjsDate } from '@/lib/date';
|
||||
import { SearchModal } from './SearchModal';
|
||||
import { SearchModalContent } from './SearchModalContent';
|
||||
import type { SearchItem, SearchItems } from './search-modal.types';
|
||||
|
||||
|
@ -52,46 +52,6 @@ const createMockSearchItems = (includeSecondary: boolean): SearchItems[] => [
|
|||
})),
|
||||
],
|
||||
},
|
||||
|
||||
// {
|
||||
// icon: <HouseIcon />,
|
||||
// label: 'Search Result 1',
|
||||
// secondaryLabel: 'This is a secondary label',
|
||||
// tertiaryLabel: createDayjsDate(new Date()).format('LL'),
|
||||
// value: 'result-1',
|
||||
// keywords: ['search', 'result', 'example'],
|
||||
// type: 'item',
|
||||
// },
|
||||
// {
|
||||
// icon: <HouseIcon />,
|
||||
// label: 'Document',
|
||||
// secondaryLabel: 'A document file',
|
||||
// tertiaryLabel: createDayjsDate(new Date()).format('LL'),
|
||||
// value: 'document-1',
|
||||
// keywords: ['document', 'file', 'pdf'],
|
||||
// type: 'item',
|
||||
// onSelect: fn(),
|
||||
// },
|
||||
// {
|
||||
// icon: <HouseIcon />,
|
||||
// label: 'Dashboard',
|
||||
// secondaryLabel: 'Analytics dashboard',
|
||||
// tertiaryLabel: createDayjsDate(new Date()).format('LL'),
|
||||
// value: 'dashboard-1',
|
||||
// keywords: ['dashboard', 'analytics', 'charts'],
|
||||
// type: 'item',
|
||||
// onSelect: fn(),
|
||||
// },
|
||||
// ...Array.from({ length: 10 }).map<SearchItem>((_, index) => ({
|
||||
// icon: <HouseIcon />,
|
||||
// label: `Dashboard ${index} with a super long label that will be truncated`,
|
||||
// secondaryLabel: `Analytics dashboard ${index}`,
|
||||
// tertiaryLabel: createDayjsDate(new Date()).format('LL'),
|
||||
// value: `testing-${index}`,
|
||||
// keywords: ['dashboard', 'analytics', 'charts'],
|
||||
// type: 'item' as const,
|
||||
// onSelect: fn(),
|
||||
// })),
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
|
@ -110,6 +70,7 @@ export const Default: Story = {
|
|||
const [addInSecondaryLabel, setAddInSecondaryLabel] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [secondaryContent, setSecondaryContent] = useState<React.ReactNode>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onViewSearchItem = (item: SearchItem) => {
|
||||
setSecondaryContent(<div>Secondary Content {item.label}</div>);
|
||||
|
@ -122,6 +83,9 @@ export const Default: Story = {
|
|||
useHotkeys('x', () => {
|
||||
setAddInSecondaryLabel((x) => !x);
|
||||
});
|
||||
useHotkeys('l', () => {
|
||||
setLoading((x) => !x);
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchModalContent
|
||||
|
@ -135,6 +99,64 @@ export const Default: Story = {
|
|||
}}
|
||||
openSecondaryContent={open}
|
||||
secondaryContent={secondaryContent}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const SearchModalStory: Story = {
|
||||
render: (args) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openSecondaryContent, setOpenSecondaryContent] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [addInSecondaryLabel, setAddInSecondaryLabel] = useState(false);
|
||||
const [secondaryContent, setSecondaryContent] = useState<React.ReactNode>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onViewSearchItem = (item: SearchItem) => {
|
||||
setSecondaryContent(<div>Secondary Content {item.label}</div>);
|
||||
setOpen(true);
|
||||
setOpenSecondaryContent(true);
|
||||
setAddInSecondaryLabel(true);
|
||||
};
|
||||
|
||||
const searchItems = createMockSearchItems(addInSecondaryLabel);
|
||||
|
||||
useHotkeys('x', () => {
|
||||
setAddInSecondaryLabel((x) => !x);
|
||||
});
|
||||
useHotkeys('l', () => {
|
||||
setLoading((x) => !x);
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'o',
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpen(true);
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<SearchModal
|
||||
{...args}
|
||||
searchItems={searchItems}
|
||||
onViewSearchItem={onViewSearchItem}
|
||||
value={searchValue}
|
||||
onChangeValue={(v) => {
|
||||
setOpenSecondaryContent(false);
|
||||
setSearchValue(v);
|
||||
}}
|
||||
openSecondaryContent={openSecondaryContent}
|
||||
secondaryContent={secondaryContent}
|
||||
loading={loading}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { Command } from 'cmdk';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { createContext, useContextSelector } from 'use-context-selector';
|
||||
import React, { useRef } from 'react';
|
||||
import { SearchEmptyState } from './SearchEmptyState';
|
||||
import { SearchFooter } from './SearchFooter';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { SearchModalContentItems } from './SearchModalContentItems';
|
||||
import { SearchLoading } from './SearchLoading';
|
||||
import { SearchModalItemsContainer } from './SearchModalItemsContainer';
|
||||
import type { SearchItem, SearchModalContentProps } from './search-modal.types';
|
||||
import { useViewSearchItem } from './useViewSearchItem';
|
||||
|
||||
export const SearchModalContent = <M, T extends string>({
|
||||
isModalOpen,
|
||||
searchItems,
|
||||
onChangeValue,
|
||||
onSelect,
|
||||
|
@ -22,6 +22,8 @@ export const SearchModalContent = <M, T extends string>({
|
|||
value: searchValue,
|
||||
secondaryContent,
|
||||
openSecondaryContent,
|
||||
shouldFilter = true,
|
||||
filter,
|
||||
}: SearchModalContentProps<M, T>) => {
|
||||
const { handleKeyDown, focusedValue, setFocusedValue } = useViewSearchItem({
|
||||
searchItems,
|
||||
|
@ -29,10 +31,6 @@ export const SearchModalContent = <M, T extends string>({
|
|||
});
|
||||
const isCommandKeyPressedRef = useRef(false);
|
||||
|
||||
const onSearchChangePreflight = (searchValue: string) => {
|
||||
onChangeValue(searchValue);
|
||||
};
|
||||
|
||||
const handleKeyDownGlobal = (e: React.KeyboardEvent) => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
isCommandKeyPressedRef.current = true;
|
||||
|
@ -60,15 +58,19 @@ export const SearchModalContent = <M, T extends string>({
|
|||
onValueChange={setFocusedValue}
|
||||
onKeyDown={handleKeyDownGlobal}
|
||||
onKeyUp={handleKeyUpGlobal}
|
||||
shouldFilter={true}
|
||||
shouldFilter={shouldFilter}
|
||||
filter={filter}
|
||||
>
|
||||
<SearchInput
|
||||
searchValue={searchValue}
|
||||
onSearchChange={onSearchChangePreflight}
|
||||
onChangeValue={onChangeValue}
|
||||
filterContent={filterContent}
|
||||
placeholder={placeholder}
|
||||
isModalOpen={isModalOpen}
|
||||
/>
|
||||
<div className="border-b" />
|
||||
|
||||
<SearchLoading loading={loading} />
|
||||
|
||||
<SearchModalItemsContainer
|
||||
searchItems={searchItems}
|
||||
secondaryContent={secondaryContent}
|
||||
|
@ -76,6 +78,7 @@ export const SearchModalContent = <M, T extends string>({
|
|||
onSelectGlobal={onSelectGlobal}
|
||||
onViewSearchItem={onViewSearchItem}
|
||||
/>
|
||||
|
||||
<SearchEmptyState emptyState={emptyState} />
|
||||
<SearchFooter />
|
||||
</Command>
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useCommandState } from 'cmdk';
|
|||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SearchLoading } from './SearchLoading';
|
||||
import { SearchModalContentItems } from './SearchModalContentItems';
|
||||
import type { SearchItem, SearchItems, SearchModalContentProps } from './search-modal.types';
|
||||
|
||||
|
@ -23,7 +24,7 @@ export const SearchModalItemsContainer = <M, T extends string>({
|
|||
const hasResults = useCommandState((x) => x.filtered.count) > 0;
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full overflow-hidden flex-1', !hasResults && 'hidden')}>
|
||||
<div className={cn('flex w-full overflow-hidden flex-1 relative', !hasResults && 'hidden')}>
|
||||
<motion.div
|
||||
className="overflow-y-auto flex flex-col shrink-0"
|
||||
initial={false}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { Command } from 'cmdk';
|
||||
|
||||
export type SearchItem<M = unknown, T extends string = string> = {
|
||||
icon?: React.ReactNode;
|
||||
label: string | React.ReactNode;
|
||||
|
@ -30,6 +32,7 @@ export type SearchItems<M = unknown, T extends string = string> =
|
|||
| SearchItemSeperator;
|
||||
|
||||
export type SearchModalContentProps<M = unknown, T extends string = string> = {
|
||||
isModalOpen: boolean;
|
||||
value: string;
|
||||
filterDropdownContent?: React.ReactNode;
|
||||
filterContent?: React.ReactNode;
|
||||
|
@ -42,7 +45,7 @@ export type SearchModalContentProps<M = unknown, T extends string = string> = {
|
|||
loading?: boolean;
|
||||
secondaryContent?: React.ReactNode | null;
|
||||
openSecondaryContent?: boolean; //if undefined it will close and open with the secondary content
|
||||
};
|
||||
} & Pick<React.ComponentProps<typeof Command>, 'filter' | 'shouldFilter'>;
|
||||
|
||||
export type SearchModalProps = SearchModalContentProps & {
|
||||
open: boolean;
|
||||
|
|
Loading…
Reference in New Issue