mirror of https://github.com/buster-so/buster.git
dashboard and select updates
This commit is contained in:
parent
869eecc80c
commit
5d632413ec
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>]
|
||||
|
|
Loading…
Reference in New Issue