list type safety

This commit is contained in:
Nate Kelley 2025-08-21 11:10:07 -06:00
parent 9f26eb9412
commit ad0231f031
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 129 additions and 69 deletions

View File

@ -37,7 +37,7 @@ import {
Trash,
} from '@/components/ui/icons';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useIsChatMode } from '@/context/Chats/useIsChatMode';
import { useIsChatMode } from '@/context/Chats/useMode';
import { useDownloadMetricDataCSV } from '@/context/Metrics/useDownloadMetricDataCSV';
import { useDownloadPNGSelectMenu } from '@/context/Metrics/useDownloadMetricDataPNG';
import { useRenameMetricOnPage } from '@/context/Metrics/useRenameMetricOnPage';

View File

@ -0,0 +1,53 @@
import type { RegisteredRouter } from '@tanstack/react-router';
import { create } from 'lodash';
import type { BusterListRowItem } from './interfaces';
export function createLinkItems<
T,
TRouter extends RegisteredRouter,
TOptions,
TFrom extends string = string,
>(
items: BusterListRowItem<T, TRouter, TOptions, TFrom>[]
): BusterListRowItem<T, TRouter, TOptions, TFrom>[] {
return items;
}
export function createLinkItem<
T,
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = Record<string, unknown>,
TFrom extends string = string,
>(
item: BusterListRowItem<T, TRouter, TOptions, TFrom>
): BusterListRowItem<T, TRouter, TOptions, TFrom> {
return item;
}
const test = createLinkItem({
id: '1',
data: {
name: 'Test',
},
link: {
to: '/app/metrics/$metricId',
params: {
metricId: '1',
},
},
});
const test2 = createLinkItem<{
swag: boolean;
}>({
id: '1',
data: {
swag: true,
},
link: {
to: '/app/metrics/$metricId',
params: {
metricId: '1',
},
},
});

View File

@ -1,3 +1,4 @@
export * from './create-link-items';
export * from './interfaces';
export * from './ListSelectedOptionPopup';

View File

@ -1,6 +1,6 @@
import type { LinkProps } from '@tanstack/react-router';
import type { LinkProps, RegisteredRouter } from '@tanstack/react-router';
import type React from 'react';
import type { OptionsTo } from '@/types/routes';
import type { ILinkProps } from '@/types/routes';
import type { ContextMenuProps } from '../../context-menu/ContextMenu';
export interface BusterListProps<T = unknown> {
@ -31,8 +31,12 @@ export type BusterListColumn<T = unknown> = {
};
}[keyof T];
type BusterListRowLink = {
link: OptionsTo;
type BusterListRowLink<
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = Record<string, unknown>,
TFrom extends string = string,
> = {
link: ILinkProps<TRouter, TOptions, TFrom>;
preloadDelay?: LinkProps['preloadDelay'];
preload?: LinkProps['preload'];
};
@ -41,7 +45,12 @@ type BusterListRowNotLink = {
link?: never;
};
export type BusterListRowItem<T = unknown> = {
export type BusterListRowItem<
T = unknown,
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = Record<string, unknown>,
TFrom extends string = string,
> = {
id: string;
data: T | null;
onClick?: () => void;
@ -49,7 +58,7 @@ export type BusterListRowItem<T = unknown> = {
rowSection?: BusterListSectionRow;
hidden?: boolean;
dataTestId?: string;
} & (BusterListRowLink | BusterListRowNotLink);
} & (BusterListRowLink<TRouter, TOptions, TFrom> | BusterListRowNotLink);
export interface BusterListSectionRow {
title: string;

View File

@ -8,3 +8,11 @@ export const useIsChatMode = () => {
});
return chatId !== undefined;
};
export const useIsFileMode = () => {
const chatId = useParams({
select: stableSelect,
strict: false,
});
return chatId === undefined;
};

View File

@ -8,13 +8,12 @@ import {
} from '@/components/features/metrics/StatusBadgeIndicator';
import { Avatar } from '@/components/ui/avatar';
import type { BusterListColumn, BusterListRowItem } from '@/components/ui/list';
import { BusterList, ListEmptyStateWithButton } from '@/components/ui/list';
import { BusterList, createLinkItem, ListEmptyStateWithButton } from '@/components/ui/list';
import { useCreateListByDate } from '@/components/ui/list/useCreateListByDate';
import { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { formatDate } from '@/lib/date';
import { makeHumanReadble } from '@/lib/text';
import type { OptionsTo } from '@/types/routes';
import { MetricSelectedOptionPopup } from './MetricItemsSelectedPopup';
export const MetricItemsContainer: React.FC<{
@ -36,7 +35,8 @@ export const MetricItemsContainer: React.FC<{
const metricsByDate: BusterListRowItem<BusterMetricListItem>[] = useMemo(() => {
return Object.entries(logsRecord).flatMap<BusterListRowItem<BusterMetricListItem>>(
([key, metrics]) => {
const records = metrics.map((metric) => ({
const records = metrics.map((metric) =>
createLinkItem({
id: metric.id,
data: metric,
link: {
@ -44,8 +44,9 @@ export const MetricItemsContainer: React.FC<{
params: {
metricId: metric.id,
},
} satisfies OptionsTo,
}));
},
})
);
const hasRecords = records.length > 0;
if (!hasRecords) {
return [];

View File

@ -1,5 +1,5 @@
import { Link } from '@tanstack/react-router';
import React, { useMemo } from 'react';
import React from 'react';
import { useGetMetric } from '@/api/buster_rest/metrics';
import { CreateChatButton } from '@/components/features/AssetLayout/CreateChatButton';
import { SaveMetricToCollectionButton } from '@/components/features/buttons/SaveMetricToCollectionButton';
@ -7,8 +7,8 @@ import { SaveMetricToDashboardButton } from '@/components/features/buttons/SaveM
import { ShareMetricButton } from '@/components/features/buttons/ShareMetricButton';
import { ThreeDotMenuButton } from '@/components/features/metrics/MetricThreeDotMenu';
import { SquareChartPen } from '@/components/ui/icons';
import { useIsFileMode } from '@/context/Chats/useMode';
import { useIsMetricReadOnly } from '@/context/Metrics/useIsMetricReadOnly';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
import { FileButtonContainer } from '../FileButtonContainer';
import { HideButtonContainer } from '../HideButtonContainer';
@ -18,6 +18,7 @@ export const MetricContainerHeaderButtons: React.FC<{
metricId: string;
metricVersionNumber: number;
}> = React.memo(({ metricId, metricVersionNumber }) => {
const isFileMode = useIsFileMode();
const { isViewingOldVersion } = useIsMetricReadOnly({
metricId: metricId || '',
});
@ -43,7 +44,7 @@ export const MetricContainerHeaderButtons: React.FC<{
isViewingOldVersion={isViewingOldVersion}
versionNumber={metricVersionNumber}
/>
<HideButtonContainer show={selectedLayout === 'file-only'}>
<HideButtonContainer show={isFileMode}>
<CreateChatButton assetId={metricId} assetType="metric" />
</HideButtonContainer>
</FileButtonContainer>
@ -53,62 +54,49 @@ export const MetricContainerHeaderButtons: React.FC<{
MetricContainerHeaderButtons.displayName = 'MetricContainerHeaderButtons';
const EditChartButton = React.memo(({ metricId }: { metricId: string }) => {
const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
const selectedFileViewSecondary = useChatLayoutContextSelector(
(x) => x.selectedFileViewSecondary
);
const chatId = useChatIndividualContextSelector((x) => x.chatId);
const metricVersionNumber = useChatLayoutContextSelector((x) => x.metricVersionNumber);
const editableSecondaryView: MetricFileViewSecondary = 'chart-edit';
const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
const isEditorOpen = true;
// const metricVersionNumber = useChatLayoutContextSelector((x) => x.metricVersionNumber);
// const editableSecondaryView: MetricFileViewSecondary = 'chart-edit';
// const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
const href = useMemo(() => {
if (isSelectedView) {
return assetParamsToRoute({
chatId,
assetId: metricId,
type: 'metric',
secondaryView: undefined,
versionNumber: metricVersionNumber,
page: 'chart',
});
}
// const href = useMemo(() => {
// if (isSelectedView) {
// return assetParamsToRoute({
// chatId,
// assetId: metricId,
// type: 'metric',
// secondaryView: undefined,
// versionNumber: metricVersionNumber,
// page: 'chart',
// });
// }
return assetParamsToRoute({
chatId,
assetId: metricId,
type: 'metric',
secondaryView: 'chart-edit',
versionNumber: metricVersionNumber,
page: 'chart',
});
}, [chatId, metricId, isSelectedView, metricVersionNumber]);
//I HAVE NO IDEA WHY... but onClickButton is called twice if wrapped in a link
const onClickButton = useMemoizedFn(() => {
onChangePage(href, { shallow: true });
});
// return assetParamsToRoute({
// chatId,
// assetId: metricId,
// type: 'metric',
// secondaryView: 'chart-edit',
// versionNumber: metricVersionNumber,
// page: 'chart',
// });
// }, [chatId, metricId, isSelectedView, metricVersionNumber]);
return (
<Link
href={href}
prefetch={true}
passHref
to="/app/metrics/$metricId/chart"
params={{
metricId,
}}
data-testid="edit-chart-button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
onClickButton();
}}
>
<SelectableButton
tooltipText="Edit chart"
icon={<SquareChartPen />}
selected={isSelectedView}
selected={isEditorOpen}
/>
</Link>
);

View File

@ -1,5 +1,5 @@
import type { FileRouteTypes } from '@/routeTree.gen';
import type { OptionsTo } from '@/types/routes';
import type { ILinkProps } from '@/types/routes';
type RouteFilePaths = FileRouteTypes['to'];
@ -269,7 +269,7 @@ class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
/**
* Build navigation options with route and params
*/
buildNavigationOptions(): OptionsTo {
buildNavigationOptions(): ILinkProps {
const route = this.build();
const params = this.getParams();
const search = this.getSearchParams();
@ -289,7 +289,7 @@ class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
}
// Type assertion through unknown for complex generic type
return navOptions as OptionsTo;
return navOptions as unknown as ILinkProps;
}
/**
@ -425,7 +425,7 @@ class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
* });
* // Result: { to: '/app/dashboards/dashboard-456', params: { dashboardId: 'dashboard-456', metricId: 'metric-789' }, search: { dashboard_version_number: 3, metric_version_number: 2 } }
*/
export const assetParamsToRoute = (params: AssetParamsToRoute): OptionsTo => {
export const assetParamsToRoute = (params: AssetParamsToRoute): ILinkProps => {
const builder = new RouteBuilder();
// Build route based on asset type and additional params

View File

@ -14,7 +14,7 @@ export type ILinkOptions = Partial<
>;
export type ILinkProps<
TRouter extends RegisteredRouter,
TOptions,
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = Record<string, unknown>,
TFrom extends string = string,
> = ValidateLinkOptions<TRouter, TOptions, TFrom> & ILinkOptions;