diff --git a/web/src/components/ui/list/BusterList/BusterList.stories.tsx b/web/src/components/ui/list/BusterList/BusterList.stories.tsx
index 8f95f0b8d..5c1242b15 100644
--- a/web/src/components/ui/list/BusterList/BusterList.stories.tsx
+++ b/web/src/components/ui/list/BusterList/BusterList.stories.tsx
@@ -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([]);
+
+ return (
+
+
+
+ Selected rows: {selectedKeys.join(', ') || 'None'}
+
+
+
+
+ );
+ }
};
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([]);
+
+ return (
+
+
+
+ Selected rows: {selectedKeys.join(', ') || 'None'}
+
+
+
+
+ );
+ }
};
export const WithoutHeader: Story = {
@@ -216,38 +227,59 @@ 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)
- .filter((_, index) => index % 3 === 0)
- .map((row) => row.id)
- },
- render: (args) => (
-
-
-
Border Variant with Many Rows
-
- 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
-
-
-
+ render: () => {
+ const [selectedKeys, setSelectedKeys] = React.useState
(
+ generateSampleRows(30)
+ .filter((_, index) => index % 3 === 0)
+ .map((row) => row.id)
+ );
+ const rows = generateSampleRows(30);
+
+ return (
+
+
+
Border Variant with Many Rows
+
+ 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
+
+
+
+ Selected rows: {selectedKeys.join(', ') || 'None'}
+
+
+
+
+
+
+
+
Default Variant with Many Rows
+
+ This variant shows rows without container styling.
+
+
+
+
-
-
Default Variant with Many Rows
-
This variant shows rows without container styling.
-
-
-
-
-
- )
+ );
+ }
};
// Story with many rows to demonstrate virtualization
@@ -279,3 +311,28 @@ export const ManyRowsWithContextMenu: Story = {
)
};
+
+export const InteractiveSelection: Story = {
+ render: () => {
+ const [selectedKeys, setSelectedKeys] = React.useState
([]);
+
+ return (
+
+
+
+ Selected rows: {selectedKeys.join(', ') || 'None'}
+
+
+
+
+ );
+ }
+};
diff --git a/web/src/components/ui/list/BusterList/BusterListRowComponent.tsx b/web/src/components/ui/list/BusterList/BusterListRowComponent.tsx
index 36063e6c1..5bf29eed9 100644
--- a/web/src/components/ui/list/BusterList/BusterListRowComponent.tsx
+++ b/web/src/components/ui/list/BusterList/BusterListRowComponent.tsx
@@ -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, id: string) => void;
style?: React.CSSProperties;
hideLastRowBorder: NonNullable;
@@ -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(() => {
diff --git a/web/src/components/ui/list/BusterList/BusterListRowComponentSelector.tsx b/web/src/components/ui/list/BusterList/BusterListRowComponentSelector.tsx
index ec502df12..b61018bdb 100644
--- a/web/src/components/ui/list/BusterList/BusterListRowComponentSelector.tsx
+++ b/web/src/components/ui/list/BusterList/BusterListRowComponentSelector.tsx
@@ -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, id: string) => void;
selectedRowKeys?: string[];
diff --git a/web/src/components/ui/list/BusterList/BusterListVirtua.tsx b/web/src/components/ui/list/BusterList/BusterListVirtua.tsx
index bcbb85aae..8c72c796d 100644
--- a/web/src/components/ui/list/BusterList/BusterListVirtua.tsx
+++ b/web/src/components/ui/list/BusterList/BusterListVirtua.tsx
@@ -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(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 (
diff --git a/web/src/components/ui/list/BusterList/CheckboxColumn.tsx b/web/src/components/ui/list/BusterList/CheckboxColumn.tsx
index e90209830..55c607ac5 100644
--- a/web/src/components/ui/list/BusterList/CheckboxColumn.tsx
+++ b/web/src/components/ui/list/BusterList/CheckboxColumn.tsx
@@ -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 (
);
diff --git a/web/src/components/ui/list/BusterList/MemoizedCheckbox.tsx b/web/src/components/ui/list/BusterList/MemoizedCheckbox.tsx
index c7f787309..7e94f3f41 100644
--- a/web/src/components/ui/list/BusterList/MemoizedCheckbox.tsx
+++ b/web/src/components/ui/list/BusterList/MemoizedCheckbox.tsx
@@ -11,14 +11,20 @@ export const MemoizedCheckbox = React.memo(
}: {
checked: boolean;
indeterminate: boolean;
- onChange: (v: boolean) => void;
+ onChange: (v: boolean, e: React.MouseEvent) => void;
}) => {
- const handleChange = useMemoizedFn((checkedState: CheckedState) => {
- onChange?.(checkedState === true);
- });
+ const handleChange = useMemoizedFn(
+ (checkedState: CheckedState, e: React.MouseEvent) => {
+ onChange?.(checkedState === true, e);
+ }
+ );
return (
-
+ handleChange(!checked, e)}
+ />
);
}
);
diff --git a/web/src/controllers/DashboardController/DashboardController.tsx b/web/src/controllers/DashboardController/DashboardController.tsx
index 5ffa990c0..db5bdc801 100644
--- a/web/src/controllers/DashboardController/DashboardController.tsx
+++ b/web/src/controllers/DashboardController/DashboardController.tsx
@@ -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);
});
diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderButtons/DashboardContainerHeaderButtons.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderButtons/DashboardContainerHeaderButtons.tsx
index a5343c365..e893b9ff1 100644
--- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderButtons/DashboardContainerHeaderButtons.tsx
+++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/DashboardContainerHeaderButtons/DashboardContainerHeaderButtons.tsx
@@ -41,7 +41,7 @@ SaveToCollectionButton.displayName = 'SaveToCollectionButton';
const AddContentToDashboardButton = React.memo(() => {
return (
-
+
} />
);
diff --git a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx
index a93c7e0b5..bb6c5b6fc 100644
--- a/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx
+++ b/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/MetricContainerHeaderButtons/MetricThreeDotMenu.tsx
@@ -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: ,
items: [{dashboardSubMenu} ]