update timeouts for switching layouts

This commit is contained in:
Nate Kelley 2025-04-01 12:34:26 -06:00
parent f417ada44a
commit e2f5e64475
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
7 changed files with 225 additions and 38 deletions

View File

@ -20,7 +20,7 @@ import {
DropdownMenuLink DropdownMenuLink
} from './DropdownBase'; } from './DropdownBase';
import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader'; import { CircleSpinnerLoader } from '../loaders/CircleSpinnerLoader';
import { useMemoizedFn, useMount } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
import { Input } from '../inputs/Input'; import { Input } from '../inputs/Input';
import { useDebounceSearch } from '@/hooks'; import { useDebounceSearch } from '@/hooks';
@ -159,6 +159,8 @@ export const DropdownContent = <T,>({
debounceTime: 50 debounceTime: 50
}); });
console.log(filteredItems, searchText);
const hasShownItem = useMemo(() => { const hasShownItem = useMemo(() => {
return filteredItems.length > 0 && filteredItems.some((item) => (item as DropdownItem).value); return filteredItems.length > 0 && filteredItems.some((item) => (item as DropdownItem).value);
}, [filteredItems]); }, [filteredItems]);
@ -264,13 +266,13 @@ export const DropdownContent = <T,>({
return ( return (
<DropdownItemSelector <DropdownItemSelector
item={item as DropdownItems<T>[number]} key={dropdownItemKey(item, hotkeyIndex)}
item={item}
index={hotkeyIndex} index={hotkeyIndex}
selectType={selectType} selectType={selectType}
onSelect={onSelect} onSelect={onSelect}
onSelectItem={onSelectItem} onSelectItem={onSelectItem}
closeOnSelect={closeOnSelect} closeOnSelect={closeOnSelect}
key={dropdownItemKey(item, hotkeyIndex)}
showIndex={showIndex} showIndex={showIndex}
/> />
); );
@ -419,7 +421,7 @@ const DropdownItem = <T,>({
} }
//I do not think this selected check is stable... look into refactoring //I do not think this selected check is stable... look into refactoring
if (selectType === 'single' || selected) { if (selectType === 'single') {
return ( return (
<DropdownMenuCheckboxItemSingle <DropdownMenuCheckboxItemSingle
checked={selected} checked={selected}

View File

@ -2,9 +2,10 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { SelectMultiple } from './SelectMultiple'; import { SelectMultiple } from './SelectMultiple';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { type SelectItem } from './Select'; import { type SelectItem } from './Select';
import { fn } from '@storybook/test'; import { fn } from '@storybook/test';
import { faker } from '@faker-js/faker';
const meta = { const meta = {
title: 'UI/Select/SelectMultiple', title: 'UI/Select/SelectMultiple',
@ -113,19 +114,40 @@ export const CustomWidth: Story = {
) )
}; };
const WithHundredItemsWithHooks = () => {
const [value, setValue] = useState<string[]>([]);
const items = useMemo(
() =>
Array.from({ length: 100 }, (_, index) => ({
value: index.toString(),
label: faker.company.name()
})),
[]
);
const handleSelect = (selectedValues: string[]) => {
setValue(selectedValues);
};
return (
<div className="w-[300px]">
<SelectMultiple
items={items}
onChange={handleSelect}
placeholder="Select multiple options..."
value={value}
/>
</div>
);
};
export const WithHundredItems: Story = { export const WithHundredItems: Story = {
args: { args: {
items: Array.from({ length: 100 }, (_, index) => ({ items: [],
value: index.toString(),
label: `Option ${index + 1}`
})),
value: [], value: [],
onChange: fn(), onChange: fn(),
placeholder: 'Select multiple options...' placeholder: 'Select multiple options...'
}, },
render: (args) => ( render: () => <WithHundredItemsWithHooks />
<div className="w-[300px]">
<SelectMultiple {...args} />
</div>
)
}; };

View File

@ -16,6 +16,7 @@ interface SelectMultipleProps extends VariantProps<typeof selectVariants> {
placeholder?: string; placeholder?: string;
value: string[]; value: string[];
disabled?: boolean; disabled?: boolean;
useSearch?: boolean;
} }
export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo( export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
@ -27,7 +28,8 @@ export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
size = 'default', size = 'default',
variant = 'default', variant = 'default',
value, value,
disabled disabled,
useSearch = true
}) => { }) => {
const selectedRecord = useMemo(() => { const selectedRecord = useMemo(() => {
return itemsProp.reduce<Record<string, boolean>>((acc, item) => { return itemsProp.reduce<Record<string, boolean>>((acc, item) => {
@ -72,6 +74,7 @@ export const SelectMultiple: React.FC<SelectMultipleProps> = React.memo(
<Dropdown <Dropdown
items={items} items={items}
onSelect={handleSelect} onSelect={handleSelect}
menuHeader={useSearch ? 'Search...' : undefined}
selectType="multiple" selectType="multiple"
align="start" align="start"
modal={false} modal={false}

View File

@ -1,8 +1,13 @@
import type { FileType, AllFileTypes } from '@/api/asset_interfaces'; import type { FileType, AllFileTypes } from '@/api/asset_interfaces';
import { BusterRoutes, createBusterRoute } from '@/routes'; import { BusterRoutes, createBusterRoute } from '@/routes';
import type { FileView } from './useLayoutConfig'; import type {
DashboardFileViewSecondary,
FileView,
FileViewSecondary,
MetricFileViewSecondary
} from './useLayoutConfig';
const chatRouteRecord: Record<AllFileTypes, (chatId: string, assetId: string) => string> = { const chatRouteRecord: Record<AllFileTypes, (chatId: string, assetId: string) => string | null> = {
collection: (chatId, assetId) => collection: (chatId, assetId) =>
createBusterRoute({ createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_COLLECTION_ID, route: BusterRoutes.APP_CHAT_ID_COLLECTION_ID,
@ -48,18 +53,57 @@ const chatRouteRecord: Record<AllFileTypes, (chatId: string, assetId: string) =>
empty: () => '' empty: () => ''
}; };
const assetRouteRecord: Record<AllFileTypes, (assetId: string) => string | null> = {
collection: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_COLLECTIONS_ID,
collectionId: assetId
}),
dataset: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_DATASETS_ID,
datasetId: assetId
}),
metric: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_METRIC_ID,
metricId: assetId
}),
dashboard: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_DASHBOARD_ID,
dashboardId: assetId
}),
term: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_TERMS_ID,
termId: assetId
}),
value: (assetId) =>
createBusterRoute({
route: BusterRoutes.APP_VALUE_ID,
valueId: assetId
}),
reasoning: () => null,
empty: () => null
};
export const createChatAssetRoute = ({ export const createChatAssetRoute = ({
chatId, chatId,
assetId, assetId,
type type
}: { }: {
chatId: string; chatId: string | undefined;
assetId: string; assetId: string;
type: FileType; type: FileType;
}) => { }) => {
const routeBuilder = chatRouteRecord[type]; const routeBuilder = chatRouteRecord[type];
if (!routeBuilder) return null; if (!routeBuilder) return null;
return routeBuilder(chatId, assetId); if (chatId) return routeBuilder(chatId, assetId);
const assetRouteBuilder = assetRouteRecord[type];
if (!assetRouteBuilder) return null;
return assetRouteBuilder(assetId);
}; };
const routeToFileView: Partial<Record<BusterRoutes, FileView>> = { const routeToFileView: Partial<Record<BusterRoutes, FileView>> = {
@ -86,3 +130,88 @@ export const DEFAULT_FILE_VIEW: Record<FileType, FileView> = {
// term: 'results', // term: 'results',
// dataset: 'results', // dataset: 'results',
}; };
export const assetParamsToRoute = ({
chatId,
assetId,
type,
secondaryView: secondaryViewProp
}: {
chatId: string | undefined;
assetId: string;
type: FileType;
secondaryView?: FileViewSecondary;
}) => {
if (type === 'metric') {
const secondaryView = secondaryViewProp as MetricFileViewSecondary | undefined;
if (chatId) {
switch (secondaryView) {
case 'chart-edit':
return createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_METRIC_ID_CHART,
chatId,
metricId: assetId
});
case 'sql-edit':
return createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_METRIC_ID_RESULTS,
chatId,
metricId: assetId
});
case 'version-history':
return createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_METRIC_ID_CHART,
chatId,
metricId: assetId
});
default:
const test: never | undefined = secondaryView;
return '';
}
}
switch (secondaryView) {
case 'chart-edit':
return createBusterRoute({
route: BusterRoutes.APP_METRIC_ID_CHART,
metricId: assetId
});
case 'sql-edit':
return createBusterRoute({
route: BusterRoutes.APP_METRIC_ID_RESULTS,
metricId: assetId
});
case 'version-history':
return createBusterRoute({
route: BusterRoutes.APP_METRIC_ID_CHART,
metricId: assetId
});
default:
const test: never | undefined = secondaryView;
return '';
}
}
if (type === 'dashboard') {
const secondaryView = secondaryViewProp as DashboardFileViewSecondary | undefined;
if (chatId) {
switch (secondaryView) {
case 'version-history':
return createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_DASHBOARD_ID,
chatId,
dashboardId: assetId
});
}
}
return createBusterRoute({
route: BusterRoutes.APP_DASHBOARD_ID,
dashboardId: assetId
});
}
console.warn('Asset params to route has not been implemented for this file type', type);
return createChatAssetRoute({ chatId, assetId, type }) || '';
};

View File

@ -88,7 +88,7 @@ export const useLayoutConfig = ({
if (secondaryView) { if (secondaryView) {
animateOpenSplitter('right'); animateOpenSplitter('right');
await timeout(250); //wait for splitter to close before opening secondary view await timeout(chatId ? 250 : 0); //wait for splitter to close before opening secondary view
} else if (chatId) { } else if (chatId) {
animateOpenSplitter('both'); animateOpenSplitter('both');
} }

View File

@ -6,12 +6,13 @@ import { useTransition } from 'react';
export const useCloseVersionHistory = () => { export const useCloseVersionHistory = () => {
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const chatId = useChatLayoutContextSelector((x) => x.chatId);
const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams); const onChangeQueryParams = useAppLayoutContextSelector((x) => x.onChangeQueryParams);
const closeSecondaryView = useChatLayoutContextSelector((x) => x.closeSecondaryView); const closeSecondaryView = useChatLayoutContextSelector((x) => x.closeSecondaryView);
const removeVersionHistoryQueryParams = useMemoizedFn(async () => { const removeVersionHistoryQueryParams = useMemoizedFn(async () => {
closeSecondaryView(); closeSecondaryView();
await timeout(250); await timeout(chatId ? 250 : 0);
startTransition(() => { startTransition(() => {
onChangeQueryParams({ metric_version_number: null, dashboard_version_number: null }); onChangeQueryParams({ metric_version_number: null, dashboard_version_number: null });
}); });

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { FileContainerButtonsProps } from '../interfaces'; import { FileContainerButtonsProps } from '../interfaces';
import { MetricFileViewSecondary, useChatLayoutContextSelector } from '../../../ChatLayoutContext'; import { MetricFileViewSecondary, useChatLayoutContextSelector } from '../../../ChatLayoutContext';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
@ -14,6 +14,12 @@ import { SquareChartPen, SquareCode } from '@/components/ui/icons';
import { useGetMetric } from '@/api/buster_rest/metrics'; import { useGetMetric } from '@/api/buster_rest/metrics';
import { ThreeDotMenuButton } from './MetricThreeDotMenu'; import { ThreeDotMenuButton } from './MetricThreeDotMenu';
import { canEdit, getIsEffectiveOwner } from '@/lib/share'; import { canEdit, getIsEffectiveOwner } from '@/lib/share';
import Link from 'next/link';
import { BusterRoutes, createBusterRoute } from '@/routes';
import {
assetParamsToRoute,
createChatAssetRoute
} from '@/layouts/ChatLayout/ChatLayoutContext/helpers';
export const MetricContainerHeaderButtons: React.FC<FileContainerButtonsProps> = React.memo(() => { export const MetricContainerHeaderButtons: React.FC<FileContainerButtonsProps> = React.memo(() => {
const selectedLayout = useChatLayoutContextSelector((x) => x.selectedLayout); const selectedLayout = useChatLayoutContextSelector((x) => x.selectedLayout);
@ -32,8 +38,8 @@ export const MetricContainerHeaderButtons: React.FC<FileContainerButtonsProps> =
return ( return (
<FileButtonContainer> <FileButtonContainer>
{isEditor && <EditChartButton />} {isEditor && <EditChartButton metricId={metricId} />}
{isEffectiveOwner && <EditSQLButton />} {isEffectiveOwner && <EditSQLButton metricId={metricId} />}
<SaveToCollectionButton metricId={metricId} /> <SaveToCollectionButton metricId={metricId} />
<SaveToDashboardButton metricId={metricId} /> <SaveToDashboardButton metricId={metricId} />
{isEffectiveOwner && <ShareMetricButton metricId={metricId} />} {isEffectiveOwner && <ShareMetricButton metricId={metricId} />}
@ -47,50 +53,74 @@ export const MetricContainerHeaderButtons: React.FC<FileContainerButtonsProps> =
MetricContainerHeaderButtons.displayName = 'MetricContainerHeaderButtons'; MetricContainerHeaderButtons.displayName = 'MetricContainerHeaderButtons';
const EditChartButton = React.memo(() => { const EditChartButton = React.memo(({ metricId }: { metricId: string }) => {
const selectedFileViewSecondary = useChatLayoutContextSelector( const selectedFileViewSecondary = useChatLayoutContextSelector(
(x) => x.selectedFileViewSecondary (x) => x.selectedFileViewSecondary
); );
const chatId = useChatIndividualContextSelector((x) => x.chatId);
const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView);
const editableSecondaryView: MetricFileViewSecondary = 'chart-edit'; const editableSecondaryView: MetricFileViewSecondary = 'chart-edit';
const isSelectedView = selectedFileViewSecondary === editableSecondaryView; const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
const href = useMemo(() => {
return assetParamsToRoute({
chatId,
assetId: metricId,
type: 'metric',
secondaryView: 'chart-edit'
});
}, [chatId, metricId]);
const onClickButton = useMemoizedFn(() => { const onClickButton = useMemoizedFn(() => {
const secondaryView = isSelectedView ? null : editableSecondaryView; const secondaryView = isSelectedView ? null : editableSecondaryView;
onSetFileView({ secondaryView, fileView: 'chart' }); onSetFileView({ secondaryView, fileView: 'chart' });
}); });
return ( return (
<SelectableButton <Link href={href}>
tooltipText="Edit chart" <SelectableButton
icon={<SquareChartPen />} tooltipText="Edit chart"
onClick={onClickButton} icon={<SquareChartPen />}
selected={isSelectedView} onClick={onClickButton}
/> selected={isSelectedView}
/>
</Link>
); );
}); });
EditChartButton.displayName = 'EditChartButton'; EditChartButton.displayName = 'EditChartButton';
const EditSQLButton = React.memo(() => { const EditSQLButton = React.memo(({ metricId }: { metricId: string }) => {
const selectedFileViewSecondary = useChatLayoutContextSelector( const selectedFileViewSecondary = useChatLayoutContextSelector(
(x) => x.selectedFileViewSecondary (x) => x.selectedFileViewSecondary
); );
const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView); const onSetFileView = useChatLayoutContextSelector((x) => x.onSetFileView);
const chatId = useChatIndividualContextSelector((x) => x.chatId);
const editableSecondaryView: MetricFileViewSecondary = 'sql-edit'; const editableSecondaryView: MetricFileViewSecondary = 'sql-edit';
const isSelectedView = selectedFileViewSecondary === editableSecondaryView; const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
const href = useMemo(() => {
return assetParamsToRoute({
chatId,
assetId: metricId,
type: 'metric',
secondaryView: 'sql-edit'
});
}, [chatId, metricId]);
const onClickButton = useMemoizedFn(() => { const onClickButton = useMemoizedFn(() => {
const secondaryView = isSelectedView ? null : editableSecondaryView; const secondaryView = isSelectedView ? null : editableSecondaryView;
onSetFileView({ secondaryView, fileView: 'results' }); onSetFileView({ secondaryView, fileView: 'results' });
}); });
return ( return (
<SelectableButton <Link href={href}>
tooltipText="SQL editor" <SelectableButton
icon={<SquareCode />} tooltipText="SQL editor"
onClick={onClickButton} icon={<SquareCode />}
selected={isSelectedView} onClick={onClickButton}
/> selected={isSelectedView}
/>
</Link>
); );
}); });
EditSQLButton.displayName = 'EditSQLButton'; EditSQLButton.displayName = 'EditSQLButton';