dashboard and select updates

This commit is contained in:
Nate Kelley 2025-03-20 16:41:00 -06:00
parent 869eecc80c
commit 5d632413ec
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 195 additions and 106 deletions

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { BusterList } from './index';
import { BusterListRow } from './interfaces';
import React from 'react';
import React, { useMemo } from 'react';
import { faker } from '@faker-js/faker';
import { ContextMenuProps } from '../../context/ContextMenu';
@ -40,23 +40,13 @@ const sampleColumns = [
{
dataIndex: 'name',
title: 'Name',
width: 200
width: 100
},
{
dataIndex: 'age',
title: 'Age',
width: 100
},
{
dataIndex: 'address',
title: 'Address',
width: 200
},
{
dataIndex: 'email',
title: 'Email',
width: 100
},
{
dataIndex: 'actions',
title: 'Actions',
@ -83,7 +73,7 @@ const generateSampleRows = (count: number): BusterListRow[] => {
if (i === 3) {
rows.push({
id: 'section1',
id: 'section' + i,
data: null,
rowSection: {
title: faker.company.name(),
@ -96,7 +86,7 @@ const generateSampleRows = (count: number): BusterListRow[] => {
// Add a section row in the middle
const sectionIndex = Math.floor(count / 2);
rows.splice(sectionIndex, 0, {
id: 'section1',
id: 'section' + sectionIndex,
data: null,
rowSection: {
title: faker.company.name(),
@ -142,19 +132,31 @@ export const Default: Story = {
};
export const WithSelection: Story = {
args: {
columns: sampleColumns,
rows: sampleRows,
selectedRowKeys: [sampleRows[0].id, sampleRows[2].id],
showHeader: true,
showSelectAll: true,
onSelectChange: (selectedRowKeys) => alert(`Selected ${selectedRowKeys.join(', ')}`)
},
render: (args) => (
render: () => {
const sampleRows = useMemo(() => {
return generateSampleRows(50);
}, []);
const [selectedKeys, setSelectedKeys] = React.useState<string[]>([]);
return (
<div style={{ height: '400px', width: '800px' }}>
<BusterList {...args} />
<div className="mb-4">
<p className="rounded border border-blue-200 bg-blue-300 p-1 text-sm text-blue-900">
Selected rows: {selectedKeys.join(', ') || 'None'}
</p>
</div>
)
<BusterList
columns={sampleColumns}
rows={sampleRows}
selectedRowKeys={selectedKeys}
onSelectChange={setSelectedKeys}
showHeader={true}
showSelectAll={true}
/>
</div>
);
}
};
export const WithContextMenu: Story = {
@ -173,19 +175,28 @@ export const WithContextMenu: Story = {
};
export const WithRowClickSelection: Story = {
args: {
columns: sampleColumns,
rows: sampleRows,
useRowClickSelectChange: true,
showHeader: true,
showSelectAll: true,
onSelectChange: (selectedRowKeys) => alert(`Selected ${selectedRowKeys.join(', ')}`)
},
render: (args) => (
render: () => {
const [selectedKeys, setSelectedKeys] = React.useState<string[]>([]);
return (
<div style={{ height: '400px', width: '800px' }}>
<BusterList {...args} />
<div className="mb-4">
<p className="text-sm text-gray-500">
Selected rows: {selectedKeys.join(', ') || 'None'}
</p>
</div>
)
<BusterList
columns={sampleColumns}
rows={sampleRows}
selectedRowKeys={selectedKeys}
onSelectChange={setSelectedKeys}
useRowClickSelectChange={true}
showHeader={true}
showSelectAll={true}
/>
</div>
);
}
};
export const WithoutHeader: Story = {
@ -216,18 +227,15 @@ export const EmptyState: Story = {
};
export const BorderVariant: Story = {
args: {
columns: sampleColumns,
rows: generateSampleRows(30),
hideLastRowBorder: true,
showHeader: true,
showSelectAll: true,
onSelectChange: (selectedRowKeys) => alert(`Selected ${selectedRowKeys.join(', ')}`),
selectedRowKeys: generateSampleRows(30)
render: () => {
const [selectedKeys, setSelectedKeys] = React.useState<string[]>(
generateSampleRows(30)
.filter((_, index) => index % 3 === 0)
.map((row) => row.id)
},
render: (args) => (
);
const rows = generateSampleRows(30);
return (
<div className="flex flex-col gap-4" style={{ height: '900px', width: '800px' }}>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Border Variant with Many Rows</h3>
@ -235,19 +243,43 @@ export const BorderVariant: Story = {
This variant remove the border of the last row. This is useful when you want to put this
list inside of a container that already contains a border
</p>
<div className="mb-4">
<p className="text-sm text-gray-500">
Selected rows: {selectedKeys.join(', ') || 'None'}
</p>
</div>
<div className="min-h-[300px]">
<BusterList {...args} />
<BusterList
columns={sampleColumns}
rows={rows}
selectedRowKeys={selectedKeys}
onSelectChange={setSelectedKeys}
hideLastRowBorder={true}
showHeader={true}
showSelectAll={true}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-medium">Default Variant with Many Rows</h3>
<p className="text-sm text-gray-500">This variant shows rows without container styling.</p>
<p className="text-sm text-gray-500">
This variant shows rows without container styling.
</p>
<div className="min-h-[300px]">
<BusterList {...args} hideLastRowBorder={false} />
<BusterList
columns={sampleColumns}
rows={rows}
selectedRowKeys={selectedKeys}
onSelectChange={setSelectedKeys}
hideLastRowBorder={false}
showHeader={true}
showSelectAll={true}
/>
</div>
</div>
</div>
)
);
}
};
// Story with many rows to demonstrate virtualization
@ -279,3 +311,28 @@ export const ManyRowsWithContextMenu: Story = {
</div>
)
};
export const InteractiveSelection: Story = {
render: () => {
const [selectedKeys, setSelectedKeys] = React.useState<string[]>([]);
return (
<div style={{ height: '400px', width: '800px' }}>
<div className="mb-4">
<p className="text-sm text-gray-500">
Selected rows: {selectedKeys.join(', ') || 'None'}
</p>
</div>
<BusterList
columns={sampleColumns}
rows={sampleRows}
selectedRowKeys={selectedKeys}
onSelectChange={setSelectedKeys}
showHeader={true}
showSelectAll={true}
useRowClickSelectChange={true}
/>
</div>
);
}
};

View File

@ -14,7 +14,7 @@ export const BusterListRowComponent = React.memo(
row: BusterListRow;
columns: BusterListColumn[];
checked: boolean;
onSelectChange?: (v: boolean, id: string) => void;
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void;
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void;
style?: React.CSSProperties;
hideLastRowBorder: NonNullable<BusterListProps['hideLastRowBorder']>;
@ -44,13 +44,13 @@ export const BusterListRowComponent = React.memo(
onContextMenuClick?.(e, row.id);
});
const onChange = useMemoizedFn((newChecked: boolean) => {
onSelectChange?.(newChecked, row.id);
const onChange = useMemoizedFn((newChecked: boolean, e: React.MouseEvent) => {
onSelectChange?.(newChecked, row.id, e);
});
const onContainerClick = useMemoizedFn(() => {
const onContainerClick = useMemoizedFn((e: React.MouseEvent) => {
if (useRowClickSelectChange) {
onChange(!checked);
onChange(!checked, e);
}
row.onClick?.();
});
@ -110,7 +110,7 @@ const BusterListCellComponent: React.FC<{
isFirstCell?: boolean;
isLastCell?: boolean;
width?: number | undefined;
onSelectChange?: (v: boolean, id: string) => void;
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void;
render?: (data: string | number | React.ReactNode, row: BusterListRowItem) => React.ReactNode;
}> = React.memo(({ data, width, row, render, isFirstCell, isLastCell, onSelectChange }) => {
const memoizedStyle = useMemo(() => {

View File

@ -9,7 +9,7 @@ export const BusterListRowComponentSelector = React.forwardRef<
row: BusterListRow;
columns: BusterListColumn[];
id: string;
onSelectChange?: (v: boolean, id: string) => void;
onSelectChange?: (v: boolean, id: string, e: React.MouseEvent) => void;
onSelectSectionChange?: (v: boolean, id: string) => void;
onContextMenuClick?: (e: React.MouseEvent<HTMLDivElement>, id: string) => void;
selectedRowKeys?: string[];

View File

@ -1,7 +1,7 @@
'use client';
import { VList } from 'virtua';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import { BusterListProps } from './interfaces';
import { useMemoizedFn } from '@/hooks';
import { getAllIdsInSection } from './helpers';
@ -26,6 +26,7 @@ export const BusterListVirtua = React.memo(
}: BusterListProps) => {
const showEmptyState = (!rows || rows.length === 0) && !!emptyState;
const lastChildIndex = rows.length - 1;
const lastSelectedIdRef = useRef<string | null>(null);
const globalCheckStatus = useMemo(() => {
if (!selectedRowKeys) return 'unchecked';
@ -49,15 +50,45 @@ export const BusterListVirtua = React.memo(
}
});
const onSelectChangePreflight = useMemoizedFn((v: boolean, id: string) => {
if (!onSelectChange || !selectedRowKeys) return;
if (v === false) {
onSelectChange(selectedRowKeys?.filter((d) => d !== id));
} else {
onSelectChange(selectedRowKeys?.concat(id) || []);
}
const getItemsBetween = useMemoizedFn((startId: string, endId: string) => {
const startIndex = rows.findIndex((row) => row.id === startId);
const endIndex = rows.findIndex((row) => row.id === endId);
if (startIndex === -1 || endIndex === -1) return [];
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
return rows
.slice(start, end + 1)
.filter((row) => !row.rowSection && !row.hidden)
.map((row) => row.id);
});
const onSelectChangePreflight = useMemoizedFn(
(v: boolean, id: string, event?: React.MouseEvent) => {
if (!onSelectChange || !selectedRowKeys) return;
if (event?.shiftKey && lastSelectedIdRef.current) {
const itemsBetween = getItemsBetween(lastSelectedIdRef.current, id);
if (v) {
const newSelectedKeys = Array.from(new Set([...selectedRowKeys, ...itemsBetween]));
onSelectChange(newSelectedKeys);
} else {
onSelectChange(selectedRowKeys.filter((key) => !itemsBetween.includes(key)));
}
} else {
if (v === false) {
onSelectChange(selectedRowKeys.filter((d) => d !== id));
} else {
onSelectChange(selectedRowKeys.concat(id));
}
}
lastSelectedIdRef.current = id;
}
);
const itemSize = useMemoizedFn((index: number) => {
const row = rows[index];
return row.rowSection ? HEIGHT_OF_SECTION_ROW : HEIGHT_OF_ROW;
@ -83,10 +114,11 @@ export const BusterListVirtua = React.memo(
hideLastRowBorder
]);
const WrapperNode = !!contextMenu ? ContextMenu : React.Fragment;
const wrapperNodeProps: ContextMenuProps = !!contextMenu
? contextMenu
: ({} as ContextMenuProps);
const [WrapperNode, wrapperNodeProps] = useMemo(() => {
const node = !!contextMenu ? ContextMenu : React.Fragment;
const props: ContextMenuProps = !!contextMenu ? contextMenu : ({} as ContextMenuProps);
return [node, props];
}, [contextMenu]);
return (
<WrapperNode {...wrapperNodeProps}>

View File

@ -6,7 +6,7 @@ import { cn } from '@/lib/classMerge';
export const CheckboxColumn: React.FC<{
checkStatus: 'checked' | 'unchecked' | 'indeterminate' | undefined;
onChange: (v: boolean) => void;
onChange: (v: boolean, e: React.MouseEvent) => void;
className?: string;
}> = React.memo(({ checkStatus, onChange, className = '' }) => {
const showBox = checkStatus === 'checked'; //|| checkStatus === 'indeterminate';
@ -16,10 +16,6 @@ export const CheckboxColumn: React.FC<{
e.preventDefault();
});
const onChangePreflight = useMemoizedFn((e: boolean) => {
onChange(e);
});
return (
<div
onClick={onClickStopPropagation}
@ -35,7 +31,7 @@ export const CheckboxColumn: React.FC<{
<MemoizedCheckbox
checked={checkStatus === 'checked'}
indeterminate={checkStatus === 'indeterminate'}
onChange={onChangePreflight}
onChange={onChange}
/>
</div>
);

View File

@ -11,14 +11,20 @@ export const MemoizedCheckbox = React.memo(
}: {
checked: boolean;
indeterminate: boolean;
onChange: (v: boolean) => void;
onChange: (v: boolean, e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}) => {
const handleChange = useMemoizedFn((checkedState: CheckedState) => {
onChange?.(checkedState === true);
});
const handleChange = useMemoizedFn(
(checkedState: CheckedState, e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
onChange?.(checkedState === true, e);
}
);
return (
<Checkbox checked={checked} indeterminate={indeterminate} onCheckedChange={handleChange} />
<Checkbox
checked={checked}
indeterminate={indeterminate}
onClick={(e) => handleChange(!checked, e)}
/>
);
}
);

View File

@ -8,13 +8,11 @@ import { AddTypeModal } from '@/components/features/modal/AddTypeModal';
import { useMemoizedFn } from '@/hooks';
import { useGetDashboard } from '@/api/buster_rest/dashboards';
export const DashboardController: React.FC<{ dashboardId: string; readOnly?: boolean }> = ({
dashboardId,
readOnly = false
}) => {
export const DashboardController: React.FC<{ dashboardId: string }> = ({ dashboardId }) => {
const { data: dashboardResponse, isFetched: isFetchedDashboard } = useGetDashboard(dashboardId);
const selectedFileView = useChatLayoutContextSelector((x) => x.selectedFileView) || 'dashboard';
const [openAddTypeModal, setOpenAddTypeModal] = useState(false);
const onCloseModal = useMemoizedFn(() => {
setOpenAddTypeModal(false);
});

View File

@ -41,7 +41,7 @@ SaveToCollectionButton.displayName = 'SaveToCollectionButton';
const AddContentToDashboardButton = React.memo(() => {
return (
<AppTooltip title="Add to dashboard">
<AppTooltip title="Add content">
<Button variant="ghost" prefix={<Plus />} />
</AppTooltip>
);

View File

@ -164,7 +164,7 @@ const useDashboardSelectMenu = ({ metricId }: { metricId: string }) => {
const dashboardDropdownItem: DropdownItem = useMemo(
() => ({
label: 'Add to dashboard',
label: 'Add content',
value: 'add-to-dashboard',
icon: <ASSET_ICONS.dashboardAdd />,
items: [<React.Fragment key="dashboard-sub-menu">{dashboardSubMenu}</React.Fragment>]