mirror of https://github.com/buster-so/buster.git
list type safety
This commit is contained in:
parent
9f26eb9412
commit
ad0231f031
|
@ -37,7 +37,7 @@ import {
|
||||||
Trash,
|
Trash,
|
||||||
} from '@/components/ui/icons';
|
} from '@/components/ui/icons';
|
||||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||||
import { useIsChatMode } from '@/context/Chats/useIsChatMode';
|
import { useIsChatMode } from '@/context/Chats/useMode';
|
||||||
import { useDownloadMetricDataCSV } from '@/context/Metrics/useDownloadMetricDataCSV';
|
import { useDownloadMetricDataCSV } from '@/context/Metrics/useDownloadMetricDataCSV';
|
||||||
import { useDownloadPNGSelectMenu } from '@/context/Metrics/useDownloadMetricDataPNG';
|
import { useDownloadPNGSelectMenu } from '@/context/Metrics/useDownloadMetricDataPNG';
|
||||||
import { useRenameMetricOnPage } from '@/context/Metrics/useRenameMetricOnPage';
|
import { useRenameMetricOnPage } from '@/context/Metrics/useRenameMetricOnPage';
|
||||||
|
|
|
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './create-link-items';
|
||||||
export * from './interfaces';
|
export * from './interfaces';
|
||||||
export * from './ListSelectedOptionPopup';
|
export * from './ListSelectedOptionPopup';
|
||||||
|
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import type { OptionsTo } from '@/types/routes';
|
import type { ILinkProps } from '@/types/routes';
|
||||||
import type { ContextMenuProps } from '../../context-menu/ContextMenu';
|
import type { ContextMenuProps } from '../../context-menu/ContextMenu';
|
||||||
|
|
||||||
export interface BusterListProps<T = unknown> {
|
export interface BusterListProps<T = unknown> {
|
||||||
|
@ -31,8 +31,12 @@ export type BusterListColumn<T = unknown> = {
|
||||||
};
|
};
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
|
||||||
type BusterListRowLink = {
|
type BusterListRowLink<
|
||||||
link: OptionsTo;
|
TRouter extends RegisteredRouter = RegisteredRouter,
|
||||||
|
TOptions = Record<string, unknown>,
|
||||||
|
TFrom extends string = string,
|
||||||
|
> = {
|
||||||
|
link: ILinkProps<TRouter, TOptions, TFrom>;
|
||||||
preloadDelay?: LinkProps['preloadDelay'];
|
preloadDelay?: LinkProps['preloadDelay'];
|
||||||
preload?: LinkProps['preload'];
|
preload?: LinkProps['preload'];
|
||||||
};
|
};
|
||||||
|
@ -41,7 +45,12 @@ type BusterListRowNotLink = {
|
||||||
link?: never;
|
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;
|
id: string;
|
||||||
data: T | null;
|
data: T | null;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
@ -49,7 +58,7 @@ export type BusterListRowItem<T = unknown> = {
|
||||||
rowSection?: BusterListSectionRow;
|
rowSection?: BusterListSectionRow;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
} & (BusterListRowLink | BusterListRowNotLink);
|
} & (BusterListRowLink<TRouter, TOptions, TFrom> | BusterListRowNotLink);
|
||||||
|
|
||||||
export interface BusterListSectionRow {
|
export interface BusterListSectionRow {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -8,3 +8,11 @@ export const useIsChatMode = () => {
|
||||||
});
|
});
|
||||||
return chatId !== undefined;
|
return chatId !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useIsFileMode = () => {
|
||||||
|
const chatId = useParams({
|
||||||
|
select: stableSelect,
|
||||||
|
strict: false,
|
||||||
|
});
|
||||||
|
return chatId === undefined;
|
||||||
|
};
|
|
@ -8,13 +8,12 @@ import {
|
||||||
} from '@/components/features/metrics/StatusBadgeIndicator';
|
} from '@/components/features/metrics/StatusBadgeIndicator';
|
||||||
import { Avatar } from '@/components/ui/avatar';
|
import { Avatar } from '@/components/ui/avatar';
|
||||||
import type { BusterListColumn, BusterListRowItem } from '@/components/ui/list';
|
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 { useCreateListByDate } from '@/components/ui/list/useCreateListByDate';
|
||||||
import { Text } from '@/components/ui/typography';
|
import { Text } from '@/components/ui/typography';
|
||||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||||
import { formatDate } from '@/lib/date';
|
import { formatDate } from '@/lib/date';
|
||||||
import { makeHumanReadble } from '@/lib/text';
|
import { makeHumanReadble } from '@/lib/text';
|
||||||
import type { OptionsTo } from '@/types/routes';
|
|
||||||
import { MetricSelectedOptionPopup } from './MetricItemsSelectedPopup';
|
import { MetricSelectedOptionPopup } from './MetricItemsSelectedPopup';
|
||||||
|
|
||||||
export const MetricItemsContainer: React.FC<{
|
export const MetricItemsContainer: React.FC<{
|
||||||
|
@ -36,7 +35,8 @@ export const MetricItemsContainer: React.FC<{
|
||||||
const metricsByDate: BusterListRowItem<BusterMetricListItem>[] = useMemo(() => {
|
const metricsByDate: BusterListRowItem<BusterMetricListItem>[] = useMemo(() => {
|
||||||
return Object.entries(logsRecord).flatMap<BusterListRowItem<BusterMetricListItem>>(
|
return Object.entries(logsRecord).flatMap<BusterListRowItem<BusterMetricListItem>>(
|
||||||
([key, metrics]) => {
|
([key, metrics]) => {
|
||||||
const records = metrics.map((metric) => ({
|
const records = metrics.map((metric) =>
|
||||||
|
createLinkItem({
|
||||||
id: metric.id,
|
id: metric.id,
|
||||||
data: metric,
|
data: metric,
|
||||||
link: {
|
link: {
|
||||||
|
@ -44,8 +44,9 @@ export const MetricItemsContainer: React.FC<{
|
||||||
params: {
|
params: {
|
||||||
metricId: metric.id,
|
metricId: metric.id,
|
||||||
},
|
},
|
||||||
} satisfies OptionsTo,
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
const hasRecords = records.length > 0;
|
const hasRecords = records.length > 0;
|
||||||
if (!hasRecords) {
|
if (!hasRecords) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Link } from '@tanstack/react-router';
|
import { Link } from '@tanstack/react-router';
|
||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { useGetMetric } from '@/api/buster_rest/metrics';
|
import { useGetMetric } from '@/api/buster_rest/metrics';
|
||||||
import { CreateChatButton } from '@/components/features/AssetLayout/CreateChatButton';
|
import { CreateChatButton } from '@/components/features/AssetLayout/CreateChatButton';
|
||||||
import { SaveMetricToCollectionButton } from '@/components/features/buttons/SaveMetricToCollectionButton';
|
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 { ShareMetricButton } from '@/components/features/buttons/ShareMetricButton';
|
||||||
import { ThreeDotMenuButton } from '@/components/features/metrics/MetricThreeDotMenu';
|
import { ThreeDotMenuButton } from '@/components/features/metrics/MetricThreeDotMenu';
|
||||||
import { SquareChartPen } from '@/components/ui/icons';
|
import { SquareChartPen } from '@/components/ui/icons';
|
||||||
|
import { useIsFileMode } from '@/context/Chats/useMode';
|
||||||
import { useIsMetricReadOnly } from '@/context/Metrics/useIsMetricReadOnly';
|
import { useIsMetricReadOnly } from '@/context/Metrics/useIsMetricReadOnly';
|
||||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
|
||||||
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
|
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
|
||||||
import { FileButtonContainer } from '../FileButtonContainer';
|
import { FileButtonContainer } from '../FileButtonContainer';
|
||||||
import { HideButtonContainer } from '../HideButtonContainer';
|
import { HideButtonContainer } from '../HideButtonContainer';
|
||||||
|
@ -18,6 +18,7 @@ export const MetricContainerHeaderButtons: React.FC<{
|
||||||
metricId: string;
|
metricId: string;
|
||||||
metricVersionNumber: number;
|
metricVersionNumber: number;
|
||||||
}> = React.memo(({ metricId, metricVersionNumber }) => {
|
}> = React.memo(({ metricId, metricVersionNumber }) => {
|
||||||
|
const isFileMode = useIsFileMode();
|
||||||
const { isViewingOldVersion } = useIsMetricReadOnly({
|
const { isViewingOldVersion } = useIsMetricReadOnly({
|
||||||
metricId: metricId || '',
|
metricId: metricId || '',
|
||||||
});
|
});
|
||||||
|
@ -43,7 +44,7 @@ export const MetricContainerHeaderButtons: React.FC<{
|
||||||
isViewingOldVersion={isViewingOldVersion}
|
isViewingOldVersion={isViewingOldVersion}
|
||||||
versionNumber={metricVersionNumber}
|
versionNumber={metricVersionNumber}
|
||||||
/>
|
/>
|
||||||
<HideButtonContainer show={selectedLayout === 'file-only'}>
|
<HideButtonContainer show={isFileMode}>
|
||||||
<CreateChatButton assetId={metricId} assetType="metric" />
|
<CreateChatButton assetId={metricId} assetType="metric" />
|
||||||
</HideButtonContainer>
|
</HideButtonContainer>
|
||||||
</FileButtonContainer>
|
</FileButtonContainer>
|
||||||
|
@ -53,62 +54,49 @@ export const MetricContainerHeaderButtons: React.FC<{
|
||||||
MetricContainerHeaderButtons.displayName = 'MetricContainerHeaderButtons';
|
MetricContainerHeaderButtons.displayName = 'MetricContainerHeaderButtons';
|
||||||
|
|
||||||
const EditChartButton = React.memo(({ metricId }: { metricId: string }) => {
|
const EditChartButton = React.memo(({ metricId }: { metricId: string }) => {
|
||||||
const onChangePage = useAppLayoutContextSelector((x) => x.onChangePage);
|
const isEditorOpen = true;
|
||||||
const selectedFileViewSecondary = useChatLayoutContextSelector(
|
// const metricVersionNumber = useChatLayoutContextSelector((x) => x.metricVersionNumber);
|
||||||
(x) => x.selectedFileViewSecondary
|
// const editableSecondaryView: MetricFileViewSecondary = 'chart-edit';
|
||||||
);
|
// const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
|
||||||
const chatId = useChatIndividualContextSelector((x) => x.chatId);
|
|
||||||
const metricVersionNumber = useChatLayoutContextSelector((x) => x.metricVersionNumber);
|
|
||||||
const editableSecondaryView: MetricFileViewSecondary = 'chart-edit';
|
|
||||||
const isSelectedView = selectedFileViewSecondary === editableSecondaryView;
|
|
||||||
|
|
||||||
const href = useMemo(() => {
|
// const href = useMemo(() => {
|
||||||
if (isSelectedView) {
|
// if (isSelectedView) {
|
||||||
return assetParamsToRoute({
|
// return assetParamsToRoute({
|
||||||
chatId,
|
// chatId,
|
||||||
assetId: metricId,
|
// assetId: metricId,
|
||||||
type: 'metric',
|
// type: 'metric',
|
||||||
secondaryView: undefined,
|
// secondaryView: undefined,
|
||||||
versionNumber: metricVersionNumber,
|
// versionNumber: metricVersionNumber,
|
||||||
page: 'chart',
|
// page: 'chart',
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return assetParamsToRoute({
|
// return assetParamsToRoute({
|
||||||
chatId,
|
// chatId,
|
||||||
assetId: metricId,
|
// assetId: metricId,
|
||||||
type: 'metric',
|
// type: 'metric',
|
||||||
secondaryView: 'chart-edit',
|
// secondaryView: 'chart-edit',
|
||||||
versionNumber: metricVersionNumber,
|
// versionNumber: metricVersionNumber,
|
||||||
page: 'chart',
|
// page: 'chart',
|
||||||
});
|
// });
|
||||||
}, [chatId, metricId, isSelectedView, metricVersionNumber]);
|
// }, [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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
to="/app/metrics/$metricId/chart"
|
||||||
prefetch={true}
|
params={{
|
||||||
passHref
|
metricId,
|
||||||
|
}}
|
||||||
data-testid="edit-chart-button"
|
data-testid="edit-chart-button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onClickButton();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<SelectableButton
|
<SelectableButton
|
||||||
tooltipText="Edit chart"
|
tooltipText="Edit chart"
|
||||||
icon={<SquareChartPen />}
|
icon={<SquareChartPen />}
|
||||||
selected={isSelectedView}
|
selected={isEditorOpen}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { FileRouteTypes } from '@/routeTree.gen';
|
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||||
import type { OptionsTo } from '@/types/routes';
|
import type { ILinkProps } from '@/types/routes';
|
||||||
|
|
||||||
type RouteFilePaths = FileRouteTypes['to'];
|
type RouteFilePaths = FileRouteTypes['to'];
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
|
||||||
/**
|
/**
|
||||||
* Build navigation options with route and params
|
* Build navigation options with route and params
|
||||||
*/
|
*/
|
||||||
buildNavigationOptions(): OptionsTo {
|
buildNavigationOptions(): ILinkProps {
|
||||||
const route = this.build();
|
const route = this.build();
|
||||||
const params = this.getParams();
|
const params = this.getParams();
|
||||||
const search = this.getSearchParams();
|
const search = this.getSearchParams();
|
||||||
|
@ -289,7 +289,7 @@ class RouteBuilder<T extends RouteBuilderState = NonNullable<unknown>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type assertion through unknown for complex generic type
|
// 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 } }
|
* // 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();
|
const builder = new RouteBuilder();
|
||||||
|
|
||||||
// Build route based on asset type and additional params
|
// Build route based on asset type and additional params
|
||||||
|
|
|
@ -14,7 +14,7 @@ export type ILinkOptions = Partial<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ILinkProps<
|
export type ILinkProps<
|
||||||
TRouter extends RegisteredRouter,
|
TRouter extends RegisteredRouter = RegisteredRouter,
|
||||||
TOptions,
|
TOptions = Record<string, unknown>,
|
||||||
TFrom extends string = string,
|
TFrom extends string = string,
|
||||||
> = ValidateLinkOptions<TRouter, TOptions, TFrom> & ILinkOptions;
|
> = ValidateLinkOptions<TRouter, TOptions, TFrom> & ILinkOptions;
|
||||||
|
|
Loading…
Reference in New Issue