From 302f7db61b69af76edcaca9d7fd84973484d73bf Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Thu, 4 Dec 2025 17:40:51 +0100 Subject: [PATCH 01/12] feat(DataList): add row selection functionality with checkboxes --- .../components/DataList/DataList.stories.tsx | 26 +++++++++ .../ui/src/components/DataList/DataList.tsx | 53 +++++++++++++++++++ .../src/components/DataList/DataList.types.ts | 15 ++++++ .../ui/src/components/DataView/DataView.tsx | 10 +++- .../src/components/DataView/DataView.types.ts | 3 ++ 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/DataList/DataList.stories.tsx b/packages/ui/src/components/DataList/DataList.stories.tsx index bb246917a..c5e0901af 100644 --- a/packages/ui/src/components/DataList/DataList.stories.tsx +++ b/packages/ui/src/components/DataList/DataList.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; +import React, { useState } from 'react'; import { DataList } from './DataList'; import type { DataListColumnConfig } from './DataList.types'; @@ -210,3 +211,28 @@ export const WithCustomStyling: Story = { className: 'border-2 border-gray-300', }, }; + +export const WithRowSelection: Story = { + render: () => { + const Component = () => { + const [selectedRows, setSelectedRows] = useState>(new Set()); + + return ( +
+
+ {selectedRows.size} of {sampleTickets.length} row(s) selected. +
+ []} + columns={ticketColumns as DataListColumnConfig>[]} + enableRowSelection={true} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + /> +
+ ); + }; + + return ; + }, +}; diff --git a/packages/ui/src/components/DataList/DataList.tsx b/packages/ui/src/components/DataList/DataList.tsx index 65dbc0959..0362c2762 100644 --- a/packages/ui/src/components/DataList/DataList.tsx +++ b/packages/ui/src/components/DataList/DataList.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { Checkbox } from '../../elements/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../elements/table'; import { renderCell } from '../../lib/renderCell'; import { cn } from '../../lib/utils'; @@ -17,6 +18,9 @@ export function DataList>({ getRowKey, className, getRowClassName, + enableRowSelection = false, + selectedRows, + onSelectionChange, }: DataListProps) { // Default row key extractor const defaultGetRowKey = (item: T, index: number) => { @@ -28,10 +32,49 @@ export function DataList>({ const rowKeyExtractor = getRowKey || defaultGetRowKey; + // Row selection handlers + const handleSelectAll = (checked: boolean) => { + if (!onSelectionChange) return; + + if (checked) { + const allKeys = new Set(data.map((item, index) => rowKeyExtractor(item, index))); + onSelectionChange(allKeys); + } else { + onSelectionChange(new Set()); + } + }; + + const handleRowSelect = (rowKey: string | number, checked: boolean) => { + if (!onSelectionChange || !selectedRows) return; + + const newSelected = new Set(selectedRows); + if (checked) { + newSelected.add(rowKey); + } else { + newSelected.delete(rowKey); + } + onSelectionChange(newSelected); + }; + + // Calculate selection state + const allRowKeys = data.map((item, index) => rowKeyExtractor(item, index)); + const selectedCount = selectedRows?.size || 0; + const allSelected = enableRowSelection && data.length > 0 && selectedCount === allRowKeys.length; + const someSelected = enableRowSelection && selectedCount > 0 && selectedCount < allRowKeys.length; + return ( + {enableRowSelection && ( + + + + )} {columns.map((column) => ( >({ {data.map((item, index) => { const rowKey = rowKeyExtractor(item, index); const rowClassName = getRowClassName ? getRowClassName(item) : undefined; + const isRowSelected = enableRowSelection && selectedRows?.has(rowKey); return ( + {enableRowSelection && ( + + handleRowSelect(rowKey, !!checked)} + aria-label="Select row" + /> + + )} {columns.map((column) => { const value = item[column.id]; const cellContent = renderCell(value, item, column); diff --git a/packages/ui/src/components/DataList/DataList.types.ts b/packages/ui/src/components/DataList/DataList.types.ts index 687a5cec7..eff0ef9d3 100644 --- a/packages/ui/src/components/DataList/DataList.types.ts +++ b/packages/ui/src/components/DataList/DataList.types.ts @@ -151,4 +151,19 @@ export interface DataListProps { * Optional row className function */ getRowClassName?: (item: T) => string; + + /** + * Enable row selection with checkboxes + */ + enableRowSelection?: boolean; + + /** + * Set of selected row keys (controlled mode) + */ + selectedRows?: Set; + + /** + * Callback when selection changes + */ + onSelectionChange?: (selected: Set) => void; } diff --git a/packages/ui/src/components/DataView/DataView.tsx b/packages/ui/src/components/DataView/DataView.tsx index 85e317250..fa06f6a22 100644 --- a/packages/ui/src/components/DataView/DataView.tsx +++ b/packages/ui/src/components/DataView/DataView.tsx @@ -10,6 +10,9 @@ export const DataView = >({ viewMode, cardHeaderSlots, columnsCount, + enableRowSelection, + selectedRows, + onSelectionChange, ...props }: DataViewProps) => { return ( @@ -17,7 +20,12 @@ export const DataView = >({ {viewMode === 'grid' ? ( ) : ( - + )} ); diff --git a/packages/ui/src/components/DataView/DataView.types.ts b/packages/ui/src/components/DataView/DataView.types.ts index 952af3084..0c29f7521 100644 --- a/packages/ui/src/components/DataView/DataView.types.ts +++ b/packages/ui/src/components/DataView/DataView.types.ts @@ -16,4 +16,7 @@ export interface DataViewProps { data: T[]; columns: DataListColumnConfig[]; actions?: DataListActionsConfig; + enableRowSelection?: boolean; + selectedRows?: Set; + onSelectionChange?: (selected: Set) => void; } From 7d9f2cca528efdb2298e50f2097795116b24c752 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Fri, 5 Dec 2025 13:45:39 +0100 Subject: [PATCH 02/12] feat: implement row selection feature across multiple list blocks --- .../invoice-list/src/frontend/InvoiceList.client.tsx | 7 +++++++ .../blocks/invoice-list/src/frontend/InvoiceList.types.ts | 1 + .../src/frontend/NotificationList.client.tsx | 7 +++++++ .../src/frontend/NotificationList.types.ts | 1 + .../blocks/order-list/src/frontend/OrderList.client.tsx | 7 +++++++ packages/blocks/order-list/src/frontend/OrderList.types.ts | 1 + .../product-list/src/frontend/ProductList.client.tsx | 6 ++++++ .../blocks/product-list/src/frontend/ProductList.types.ts | 1 + .../blocks/ticket-list/src/frontend/TicketList.client.tsx | 7 +++++++ .../blocks/ticket-list/src/frontend/TicketList.types.ts | 1 + packages/ui/src/components/DataView/DataView.types.ts | 1 + 11 files changed, 40 insertions(+) diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx index 0f5c9e926..89d5a9e80 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx @@ -44,6 +44,7 @@ export const InvoiceListPure: React.FC = ({ locale, access const [data, setData] = useState(component); const [filters, setFilters] = useState(initialFilters); const [viewMode, setViewMode] = useState<'list' | 'grid'>(initialViewMode); + const [selectedRows, setSelectedRows] = useState>(new Set()); const [isPending, startTransition] = useTransition(); const handleFilter = (data: Partial) => { @@ -53,6 +54,7 @@ export const InvoiceListPure: React.FC = ({ locale, access setFilters(newFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -62,6 +64,7 @@ export const InvoiceListPure: React.FC = ({ locale, access setFilters(initialFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -179,6 +182,10 @@ export const InvoiceListPure: React.FC = ({ locale, access columns={columns} actions={actions} cardHeaderSlots={data.cardHeaderSlots} + enableRowSelection={component.enableRowSelection} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + getRowKey={(item) => item.id} /> {data.pagination && ( diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts b/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts index 911279c12..cdec7068a 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts @@ -8,6 +8,7 @@ export interface InvoiceListProps { locale: string; routing: ReturnType; hasPriority?: boolean; + enableRowSelection?: boolean; } export type InvoiceListPureProps = InvoiceListProps & Model.InvoiceListBlock; diff --git a/packages/blocks/notification-list/src/frontend/NotificationList.client.tsx b/packages/blocks/notification-list/src/frontend/NotificationList.client.tsx index eda8cd35f..4d25dcfb0 100644 --- a/packages/blocks/notification-list/src/frontend/NotificationList.client.tsx +++ b/packages/blocks/notification-list/src/frontend/NotificationList.client.tsx @@ -47,6 +47,7 @@ export const NotificationListPure: React.FC = ({ const [data, setData] = useState(component); const [filters, setFilters] = useState(initialFilters); const [viewMode, setViewMode] = useState<'list' | 'grid'>(initialViewMode); + const [selectedRows, setSelectedRows] = useState>(new Set()); const [isPending, startTransition] = useTransition(); const handleFilter = (data: Partial) => { @@ -56,6 +57,7 @@ export const NotificationListPure: React.FC = ({ setFilters(newFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -65,6 +67,7 @@ export const NotificationListPure: React.FC = ({ setFilters(initialFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -172,6 +175,10 @@ export const NotificationListPure: React.FC = ({ columns={columns} actions={actions} cardHeaderSlots={data.cardHeaderSlots} + enableRowSelection={component.enableRowSelection} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + getRowKey={(item) => item.id} /> {data.pagination && ( diff --git a/packages/blocks/notification-list/src/frontend/NotificationList.types.ts b/packages/blocks/notification-list/src/frontend/NotificationList.types.ts index 89da92ebd..0a3705b26 100644 --- a/packages/blocks/notification-list/src/frontend/NotificationList.types.ts +++ b/packages/blocks/notification-list/src/frontend/NotificationList.types.ts @@ -8,6 +8,7 @@ export interface NotificationListProps { locale: string; routing: ReturnType; hasPriority?: boolean; + enableRowSelection?: boolean; } export type NotificationListPureProps = NotificationListProps & Model.NotificationListBlock; diff --git a/packages/blocks/order-list/src/frontend/OrderList.client.tsx b/packages/blocks/order-list/src/frontend/OrderList.client.tsx index 243b76013..8724c780d 100644 --- a/packages/blocks/order-list/src/frontend/OrderList.client.tsx +++ b/packages/blocks/order-list/src/frontend/OrderList.client.tsx @@ -47,6 +47,7 @@ export const OrderListPure: React.FC = ({ locale, accessToke const [data, setData] = useState(component); const [filters, setFilters] = useState(initialFilters); const [viewMode, setViewMode] = useState<'list' | 'grid'>(initialViewMode); + const [selectedRows, setSelectedRows] = useState>(new Set()); const [isPending, startTransition] = useTransition(); @@ -56,6 +57,7 @@ export const OrderListPure: React.FC = ({ locale, accessToke const newData = await sdk.blocks.getOrderList(newFilters, { 'x-locale': locale }, accessToken); setFilters(newFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -64,6 +66,7 @@ export const OrderListPure: React.FC = ({ locale, accessToke const newData = await sdk.blocks.getOrderList(initialFilters, { 'x-locale': locale }, accessToken); setFilters(initialFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -186,6 +189,10 @@ export const OrderListPure: React.FC = ({ locale, accessToke columns={columns} actions={actions} cardHeaderSlots={data.cardHeaderSlots} + enableRowSelection={component.enableRowSelection} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + getRowKey={(item) => item.id.value} /> {data.pagination && ( diff --git a/packages/blocks/order-list/src/frontend/OrderList.types.ts b/packages/blocks/order-list/src/frontend/OrderList.types.ts index 21d2932fc..d145e1949 100644 --- a/packages/blocks/order-list/src/frontend/OrderList.types.ts +++ b/packages/blocks/order-list/src/frontend/OrderList.types.ts @@ -8,6 +8,7 @@ export interface OrderListProps { locale: string; routing: ReturnType; hasPriority?: boolean; + enableRowSelection?: boolean; } export interface OrderListRendererProps extends Omit { diff --git a/packages/blocks/product-list/src/frontend/ProductList.client.tsx b/packages/blocks/product-list/src/frontend/ProductList.client.tsx index e9b74bb39..dde283d38 100644 --- a/packages/blocks/product-list/src/frontend/ProductList.client.tsx +++ b/packages/blocks/product-list/src/frontend/ProductList.client.tsx @@ -35,6 +35,7 @@ export const ProductListPure: React.FC = ({ locale, access const [filters, setFilters] = useState(initialFilters); const [isPending, startTransition] = useTransition(); const [viewMode, setViewMode] = useState('grid'); + const [selectedRows, setSelectedRows] = useState>(new Set()); const handleFilter = (data: Partial) => { startTransition(async () => { @@ -42,6 +43,7 @@ export const ProductListPure: React.FC = ({ locale, access const newData = await sdk.blocks.getProductList(newFilters, { 'x-locale': locale }, accessToken); setFilters(newFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -50,6 +52,7 @@ export const ProductListPure: React.FC = ({ locale, access const newData = await sdk.blocks.getProductList(initialFilters, { 'x-locale': locale }, accessToken); setFilters(initialFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -162,6 +165,9 @@ export const ProductListPure: React.FC = ({ locale, access columns={columns} actions={actions} getRowKey={(item) => item.id} + enableRowSelection={component.enableRowSelection} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} /> )} diff --git a/packages/blocks/product-list/src/frontend/ProductList.types.ts b/packages/blocks/product-list/src/frontend/ProductList.types.ts index 41b6cbfc0..4e9699024 100644 --- a/packages/blocks/product-list/src/frontend/ProductList.types.ts +++ b/packages/blocks/product-list/src/frontend/ProductList.types.ts @@ -7,6 +7,7 @@ export interface ProductListProps { accessToken?: string; locale: string; routing: ReturnType; + enableRowSelection?: boolean; } export type ProductListPureProps = ProductListProps & Model.ProductListBlock; diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx index 132247220..e9dc7715e 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx +++ b/packages/blocks/ticket-list/src/frontend/TicketList.client.tsx @@ -44,6 +44,7 @@ export const TicketListPure: React.FC = ({ locale, accessTo const [data, setData] = useState(component); const [filters, setFilters] = useState(initialFilters); const [viewMode, setViewMode] = useState<'list' | 'grid'>(initialViewMode); + const [selectedRows, setSelectedRows] = useState>(new Set()); const [isPending, startTransition] = useTransition(); @@ -53,6 +54,7 @@ export const TicketListPure: React.FC = ({ locale, accessTo const newData = await sdk.blocks.getTicketList(newFilters, { 'x-locale': locale }, accessToken); setFilters(newFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -61,6 +63,7 @@ export const TicketListPure: React.FC = ({ locale, accessTo const newData = await sdk.blocks.getTicketList(initialFilters, { 'x-locale': locale }, accessToken); setFilters(initialFilters); setData(newData); + setSelectedRows(new Set()); }); }; @@ -188,6 +191,10 @@ export const TicketListPure: React.FC = ({ locale, accessTo columns={columns} actions={actions} cardHeaderSlots={data.cardHeaderSlots} + enableRowSelection={component.enableRowSelection} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + getRowKey={(item) => item.id} /> {data.pagination && ( diff --git a/packages/blocks/ticket-list/src/frontend/TicketList.types.ts b/packages/blocks/ticket-list/src/frontend/TicketList.types.ts index 62c2a7c11..2c6813235 100644 --- a/packages/blocks/ticket-list/src/frontend/TicketList.types.ts +++ b/packages/blocks/ticket-list/src/frontend/TicketList.types.ts @@ -9,6 +9,7 @@ export interface TicketListProps { routing: ReturnType; hasPriority?: boolean; isDraftModeEnabled?: boolean; + enableRowSelection?: boolean; } export type TicketListPureProps = TicketListProps & Model.TicketListBlock; diff --git a/packages/ui/src/components/DataView/DataView.types.ts b/packages/ui/src/components/DataView/DataView.types.ts index 0c29f7521..6ccf3e472 100644 --- a/packages/ui/src/components/DataView/DataView.types.ts +++ b/packages/ui/src/components/DataView/DataView.types.ts @@ -16,6 +16,7 @@ export interface DataViewProps { data: T[]; columns: DataListColumnConfig[]; actions?: DataListActionsConfig; + getRowKey?: (item: T, index: number) => string | number; enableRowSelection?: boolean; selectedRows?: Set; onSelectionChange?: (selected: Set) => void; From 7ba37e3e9faaad8b65d9d78e7b1f16c8616f9f5f Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 8 Dec 2025 12:15:10 +0100 Subject: [PATCH 03/12] feat: add bulk download functionality and improve row selection in invoice list --- .../src/frontend/InvoiceList.client.tsx | 52 +++++++++++++++++- .../components/DataList/DataList.stories.tsx | 53 +++++++++++++++++++ .../ui/src/components/DataList/DataList.tsx | 20 ++++--- .../ui/src/components/DataView/DataView.tsx | 41 +++++++++++++- .../src/components/DataView/DataView.types.ts | 4 ++ 5 files changed, 161 insertions(+), 9 deletions(-) diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx index 055852bde..27a792be3 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx @@ -55,7 +55,6 @@ export const InvoiceListPure: React.FC = ({ locale, access setFilters(newFilters); setData(newData); - setSelectedRows(new Set()); } catch (_error) { toast({ variant: 'destructive', @@ -97,6 +96,39 @@ export const InvoiceListPure: React.FC = ({ locale, access } }; + const handleBulkDownload = async (selectedInvoices: Model.Invoice[]) => { + if (selectedInvoices.length === 0) return; + + startTransition(async () => { + try { + // Download invoices sequentially to avoid overwhelming the server + for (const invoice of selectedInvoices) { + try { + const response = await sdk.blocks.getInvoicePdf( + invoice.id, + { 'x-locale': locale }, + accessToken, + ); + Utils.DownloadFile.downloadFile( + response, + data.downloadFileName?.replace('{id}', invoice.id) || `invoice-${invoice.id}.pdf`, + ); + // Small delay between downloads + await new Promise((resolve) => setTimeout(resolve, 100)); + } catch (error) { + console.error(`Failed to download invoice ${invoice.id}:`, error); + } + } + } catch (_error) { + toast({ + variant: 'destructive', + title: labels.errors.requestError.title, + description: labels.errors.requestError.content, + }); + } + }); + }; + // Define columns configuration outside JSX for better readability const columns = data.table.data.columns.map((column) => { switch (column.id) { @@ -159,6 +191,8 @@ export const InvoiceListPure: React.FC = ({ locale, access } : undefined; + component.enableRowSelection = true; // TODO: remove this after testing + return (
{initialData.length > 0 ? ( @@ -202,6 +236,22 @@ export const InvoiceListPure: React.FC = ({ locale, access selectedRows={selectedRows} onSelectionChange={setSelectedRows} getRowKey={(item) => item.id} + bulkActions={ + component.enableRowSelection + ? (selectedItems) => ( + + ) + : undefined + } + bulkActionsLabel={(count) => `${count} item(s) selected`} /> {data.pagination && ( diff --git a/packages/ui/src/components/DataList/DataList.stories.tsx b/packages/ui/src/components/DataList/DataList.stories.tsx index c5e0901af..ef7b37651 100644 --- a/packages/ui/src/components/DataList/DataList.stories.tsx +++ b/packages/ui/src/components/DataList/DataList.stories.tsx @@ -1,6 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { Download, Trash2 } from 'lucide-react'; import React, { useState } from 'react'; +import { Button } from '../../elements/button'; + import { DataList } from './DataList'; import type { DataListColumnConfig } from './DataList.types'; @@ -236,3 +239,53 @@ export const WithRowSelection: Story = { return ; }, }; + +export const WithRowSelectionAndBulkActions: Story = { + render: () => { + const Component = () => { + const [selectedRows, setSelectedRows] = useState>(new Set()); + + const selectedItems = sampleTickets.filter((ticket, index) => selectedRows.has(ticket.id || index)); + + const handleBulkDelete = () => { + console.log('Deleting tickets:', selectedItems); + setSelectedRows(new Set()); + }; + + const handleBulkExport = () => { + console.log('Exporting tickets:', selectedItems); + }; + + return ( +
+ {selectedRows.size > 0 && ( +
+ + {selectedRows.size} {selectedRows.size === 1 ? 'ticket' : 'tickets'} selected + +
+ + +
+
+ )} + []} + columns={ticketColumns as DataListColumnConfig>[]} + enableRowSelection={true} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + /> +
+ ); + }; + + return ; + }, +}; diff --git a/packages/ui/src/components/DataList/DataList.tsx b/packages/ui/src/components/DataList/DataList.tsx index 0362c2762..6b27259db 100644 --- a/packages/ui/src/components/DataList/DataList.tsx +++ b/packages/ui/src/components/DataList/DataList.tsx @@ -36,12 +36,18 @@ export function DataList>({ const handleSelectAll = (checked: boolean) => { if (!onSelectionChange) return; + const allKeysOnCurrentPage = new Set(data.map((item, index) => rowKeyExtractor(item, index))); + const currentSelected = new Set(selectedRows || []); + if (checked) { - const allKeys = new Set(data.map((item, index) => rowKeyExtractor(item, index))); - onSelectionChange(allKeys); + // Add all keys from current page to existing selection + allKeysOnCurrentPage.forEach((key) => currentSelected.add(key)); } else { - onSelectionChange(new Set()); + // Remove only keys from current page from selection + allKeysOnCurrentPage.forEach((key) => currentSelected.delete(key)); } + + onSelectionChange(currentSelected); }; const handleRowSelect = (rowKey: string | number, checked: boolean) => { @@ -58,9 +64,11 @@ export function DataList>({ // Calculate selection state const allRowKeys = data.map((item, index) => rowKeyExtractor(item, index)); - const selectedCount = selectedRows?.size || 0; - const allSelected = enableRowSelection && data.length > 0 && selectedCount === allRowKeys.length; - const someSelected = enableRowSelection && selectedCount > 0 && selectedCount < allRowKeys.length; + // Count only selected items on current page + const selectedOnCurrentPage = allRowKeys.filter((key) => selectedRows?.has(key)).length; + const allSelected = + enableRowSelection && data.length > 0 && selectedOnCurrentPage === allRowKeys.length && allRowKeys.length > 0; + const someSelected = enableRowSelection && selectedOnCurrentPage > 0 && selectedOnCurrentPage < allRowKeys.length; return (
diff --git a/packages/ui/src/components/DataView/DataView.tsx b/packages/ui/src/components/DataView/DataView.tsx index fa06f6a22..2fbceaa43 100644 --- a/packages/ui/src/components/DataView/DataView.tsx +++ b/packages/ui/src/components/DataView/DataView.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { DataGrid } from '@o2s/ui/components/DataGrid'; import { DataList } from '@o2s/ui/components/DataList'; @@ -13,15 +13,52 @@ export const DataView = >({ enableRowSelection, selectedRows, onSelectionChange, + bulkActions, + bulkActionsLabel, + data, + getRowKey, ...props }: DataViewProps) => { + // Map selected row keys to full item objects + const selectedItems = useMemo(() => { + if (!selectedRows || selectedRows.size === 0 || !bulkActions) { + return []; + } + + const defaultGetRowKey = (item: T, index: number) => { + if ('id' in item) { + return String(item.id); + } + return index; + }; + + const rowKeyExtractor = getRowKey || defaultGetRowKey; + + return data.filter((item, index) => { + const key = rowKeyExtractor(item, index); + return selectedRows.has(key); + }); + }, [selectedRows, data, getRowKey, bulkActions]); + + const selectedCount = selectedRows?.size || 0; + const showBulkActions = enableRowSelection && bulkActions && selectedCount > 0 && viewMode !== 'grid'; + return ( <> + {showBulkActions && ( +
+ {bulkActionsLabel && ( + {bulkActionsLabel(selectedCount)} + )} +
{bulkActions(selectedItems, selectedCount)}
+
+ )} {viewMode === 'grid' ? ( - + ) : ( { @@ -20,4 +22,6 @@ export interface DataViewProps { enableRowSelection?: boolean; selectedRows?: Set; onSelectionChange?: (selected: Set) => void; + bulkActions?: (selectedItems: T[], selectedCount: number) => ReactNode; + bulkActionsLabel?: (count: number) => string; } From b8c8ea49466f31ce04e44ac947fafd961e87ad85 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 8 Dec 2025 16:43:36 +0100 Subject: [PATCH 04/12] feat: adding new fields in cms for invoice-list block --- .../api-harmonization/invoice-list.mapper.ts | 3 + .../api-harmonization/invoice-list.model.ts | 3 + .../src/frontend/InvoiceList.client.tsx | 58 ++++++++++--------- .../src/frontend/InvoiceList.types.ts | 1 - .../cms/models/blocks/invoice-list.model.ts | 3 + .../mappers/blocks/cms.invoice-list.mapper.ts | 10 ++++ .../mappers/blocks/cms.invoice-list.mapper.ts | 10 ++++ .../mappers/blocks/cms.invoice-list.mapper.ts | 3 + 8 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.mapper.ts b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.mapper.ts index df1ca42a7..cb935430b 100644 --- a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.mapper.ts +++ b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.mapper.ts @@ -29,6 +29,9 @@ export const mapInvoiceList = ( downloadButtonAriaDescription: cms.downloadButtonAriaDescription, initialFilters: cms.initialFilters, cardHeaderSlots: cms.cardHeaderSlots, + enableRowSelection: cms.enableRowSelection, + bulkActionsLabel: cms.bulkActionsLabel, + downloadAllButtonLabel: cms.downloadAllButtonLabel, }; }; diff --git a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts index d80311fe0..d2b9f0ebd 100644 --- a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts +++ b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts @@ -30,6 +30,9 @@ export class InvoiceListBlock extends ApiModels.Block.Block { right?: string; bottom?: string; }; + enableRowSelection?: boolean; + bulkActionsLabel?: string; + downloadAllButtonLabel?: string; } export class Invoice { diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx index 27a792be3..7879507e1 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx @@ -1,6 +1,8 @@ 'use client'; +import { IntlMessageFormat } from 'intl-messageformat'; import { Download } from 'lucide-react'; +import { useLocale } from 'next-intl'; import React, { useState, useTransition } from 'react'; import { Mappings, Utils } from '@o2s/utils.frontend'; @@ -27,6 +29,7 @@ import { InvoiceListPureProps } from './InvoiceList.types'; export const InvoiceListPure: React.FC = ({ locale, accessToken, routing, ...component }) => { const { labels } = useGlobalContext(); + const currentLocale = useLocale(); const initialFilters: Request.GetInvoiceListBlockQuery = { id: component.id, @@ -96,27 +99,23 @@ export const InvoiceListPure: React.FC = ({ locale, access } }; - const handleBulkDownload = async (selectedInvoices: Model.Invoice[]) => { - if (selectedInvoices.length === 0) return; + const handleBulkDownload = async (selectedInvoiceIds: string[]) => { + if (selectedInvoiceIds.length === 0) return; startTransition(async () => { try { // Download invoices sequentially to avoid overwhelming the server - for (const invoice of selectedInvoices) { + for (const invoiceId of selectedInvoiceIds) { try { - const response = await sdk.blocks.getInvoicePdf( - invoice.id, - { 'x-locale': locale }, - accessToken, - ); + const response = await sdk.blocks.getInvoicePdf(invoiceId, { 'x-locale': locale }, accessToken); Utils.DownloadFile.downloadFile( response, - data.downloadFileName?.replace('{id}', invoice.id) || `invoice-${invoice.id}.pdf`, + data.downloadFileName?.replace('{id}', invoiceId) || `invoice-${invoiceId}.pdf`, ); // Small delay between downloads await new Promise((resolve) => setTimeout(resolve, 100)); } catch (error) { - console.error(`Failed to download invoice ${invoice.id}:`, error); + console.error(`Failed to download invoice ${invoiceId}:`, error); } } } catch (_error) { @@ -191,7 +190,26 @@ export const InvoiceListPure: React.FC = ({ locale, access } : undefined; - component.enableRowSelection = true; // TODO: remove this after testing + const bulkActions = + component.enableRowSelection && component.downloadAllButtonLabel + ? (selectedItems: Model.Invoice[]) => ( + + ) + : undefined; + + const bulkActionsLabel = component.bulkActionsLabel + ? (count: number) => { + const msg = new IntlMessageFormat(component.bulkActionsLabel!, currentLocale); + return String(msg.format({ count })); + } + : undefined; return (
@@ -236,22 +254,8 @@ export const InvoiceListPure: React.FC = ({ locale, access selectedRows={selectedRows} onSelectionChange={setSelectedRows} getRowKey={(item) => item.id} - bulkActions={ - component.enableRowSelection - ? (selectedItems) => ( - - ) - : undefined - } - bulkActionsLabel={(count) => `${count} item(s) selected`} + bulkActions={bulkActions} + bulkActionsLabel={bulkActionsLabel} /> {data.pagination && ( diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts b/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts index cdec7068a..911279c12 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.types.ts @@ -8,7 +8,6 @@ export interface InvoiceListProps { locale: string; routing: ReturnType; hasPriority?: boolean; - enableRowSelection?: boolean; } export type InvoiceListPureProps = InvoiceListProps & Model.InvoiceListBlock; diff --git a/packages/framework/src/modules/cms/models/blocks/invoice-list.model.ts b/packages/framework/src/modules/cms/models/blocks/invoice-list.model.ts index 5fe09e2e4..cac643526 100644 --- a/packages/framework/src/modules/cms/models/blocks/invoice-list.model.ts +++ b/packages/framework/src/modules/cms/models/blocks/invoice-list.model.ts @@ -27,4 +27,7 @@ export class InvoiceListBlock extends Block.Block { right?: string; bottom?: string; }; + enableRowSelection?: boolean; + bulkActionsLabel?: string; + downloadAllButtonLabel?: string; } diff --git a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts index 0545b248c..1bbe59f03 100644 --- a/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts +++ b/packages/integrations/contentful-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts @@ -114,6 +114,9 @@ const MOCK_INVOICE_LIST_BLOCK_EN: CMS.Model.InvoiceListBlock.InvoiceListBlock = }, downloadFileName: 'invoice-{id}.pdf', downloadButtonAriaDescription: 'Download invoice {id}', + enableRowSelection: true, + bulkActionsLabel: '{count, plural, one {# item selected} other {# items selected}}', + downloadAllButtonLabel: 'Download selected', }; const MOCK_INVOICE_LIST_BLOCK_DE: CMS.Model.InvoiceListBlock.InvoiceListBlock = { @@ -230,6 +233,9 @@ const MOCK_INVOICE_LIST_BLOCK_DE: CMS.Model.InvoiceListBlock.InvoiceListBlock = }, downloadFileName: 'rechnung-{id}.pdf', downloadButtonAriaDescription: 'Rechnung {id} herunterladen', + enableRowSelection: true, + bulkActionsLabel: '{count, plural, one {# Element ausgewählt} other {# Elemente ausgewählt}}', + downloadAllButtonLabel: 'Ausgewählte herunterladen', }; const MOCK_INVOICE_LIST_BLOCK_PL: CMS.Model.InvoiceListBlock.InvoiceListBlock = { @@ -346,6 +352,10 @@ const MOCK_INVOICE_LIST_BLOCK_PL: CMS.Model.InvoiceListBlock.InvoiceListBlock = }, downloadFileName: 'faktura-{id}.pdf', downloadButtonAriaDescription: 'Pobierz fakturę {id}', + enableRowSelection: true, + bulkActionsLabel: + '{count, plural, one {Wybrano # element} few {Wybrano # elementy} many {Wybrano # elementów} other {Wybrano # elementów}}', + downloadAllButtonLabel: 'Pobierz zaznaczone', }; export const mapInvoiceListBlock = (locale: string): CMS.Model.InvoiceListBlock.InvoiceListBlock => { diff --git a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts index f2bec90f4..6cf745698 100644 --- a/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts +++ b/packages/integrations/mocked/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts @@ -132,6 +132,9 @@ const MOCK_INVOICE_LIST_BLOCK_EN: CMS.Model.InvoiceListBlock.InvoiceListBlock = right: 'paymentStatus', bottom: 'paymentDueDate', }, + enableRowSelection: true, + bulkActionsLabel: '{count, plural, one {# item selected} other {# items selected}}', + downloadAllButtonLabel: 'Download selected', }; const MOCK_INVOICE_LIST_BLOCK_DE: CMS.Model.InvoiceListBlock.InvoiceListBlock = { @@ -266,6 +269,9 @@ const MOCK_INVOICE_LIST_BLOCK_DE: CMS.Model.InvoiceListBlock.InvoiceListBlock = right: 'paymentStatus', bottom: 'paymentDueDate', }, + enableRowSelection: true, + bulkActionsLabel: '{count, plural, one {# Element ausgewählt} other {# Elemente ausgewählt}}', + downloadAllButtonLabel: 'Ausgewählte herunterladen', }; const MOCK_INVOICE_LIST_BLOCK_PL: CMS.Model.InvoiceListBlock.InvoiceListBlock = { @@ -400,6 +406,10 @@ const MOCK_INVOICE_LIST_BLOCK_PL: CMS.Model.InvoiceListBlock.InvoiceListBlock = right: 'paymentStatus', bottom: 'paymentDueDate', }, + enableRowSelection: true, + bulkActionsLabel: + '{count, plural, one {Wybrano # element} few {Wybrano # elementy} many {Wybrano # elementów} other {Wybrano # elementów}}', + downloadAllButtonLabel: 'Pobierz zaznaczone', }; export const mapInvoiceListBlock = (locale: string): CMS.Model.InvoiceListBlock.InvoiceListBlock => { diff --git a/packages/integrations/strapi-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts b/packages/integrations/strapi-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts index 44b9fc4c6..b416e4bcd 100644 --- a/packages/integrations/strapi-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts +++ b/packages/integrations/strapi-cms/src/modules/cms/mappers/blocks/cms.invoice-list.mapper.ts @@ -39,6 +39,9 @@ export const mapInvoiceListBlock = (data: GetComponentQuery): CMS.Model.InvoiceL downloadFileName: component.downloadFileName, downloadButtonAriaDescription: component.downloadButtonAriaDescription, initialFilters: undefined, // TODO: add initial filters field in CMS + enableRowSelection: undefined, // TODO: add enableRowSelection field in CMS + bulkActionsLabel: undefined, // TODO: add bulkActionsLabel field in CMS + downloadAllButtonLabel: undefined, // TODO: add downloadAllButtonLabel field in CMS }; } From f1b90621eb5b93f3fc1200b7e18eebd73e6be5d0 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 8 Dec 2025 17:34:26 +0100 Subject: [PATCH 05/12] refactor: simplify bulk actions handling in DataView and InvoiceList components --- .../src/frontend/InvoiceList.client.tsx | 21 +++++++-------- .../ui/src/components/DataView/DataView.tsx | 27 +++---------------- .../src/components/DataView/DataView.types.ts | 2 +- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx index 7879507e1..23d33ae36 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx @@ -104,7 +104,6 @@ export const InvoiceListPure: React.FC = ({ locale, access startTransition(async () => { try { - // Download invoices sequentially to avoid overwhelming the server for (const invoiceId of selectedInvoiceIds) { try { const response = await sdk.blocks.getInvoicePdf(invoiceId, { 'x-locale': locale }, accessToken); @@ -112,7 +111,6 @@ export const InvoiceListPure: React.FC = ({ locale, access response, data.downloadFileName?.replace('{id}', invoiceId) || `invoice-${invoiceId}.pdf`, ); - // Small delay between downloads await new Promise((resolve) => setTimeout(resolve, 100)); } catch (error) { console.error(`Failed to download invoice ${invoiceId}:`, error); @@ -192,16 +190,15 @@ export const InvoiceListPure: React.FC = ({ locale, access const bulkActions = component.enableRowSelection && component.downloadAllButtonLabel - ? (selectedItems: Model.Invoice[]) => ( - - ) + ? (selectedRowKeys: Set) => { + const selectedIds = Array.from(selectedRowKeys) as string[]; + return ( + + ); + } : undefined; const bulkActionsLabel = component.bulkActionsLabel diff --git a/packages/ui/src/components/DataView/DataView.tsx b/packages/ui/src/components/DataView/DataView.tsx index 2fbceaa43..983119874 100644 --- a/packages/ui/src/components/DataView/DataView.tsx +++ b/packages/ui/src/components/DataView/DataView.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { DataGrid } from '@o2s/ui/components/DataGrid'; import { DataList } from '@o2s/ui/components/DataList'; @@ -19,38 +19,17 @@ export const DataView = >({ getRowKey, ...props }: DataViewProps) => { - // Map selected row keys to full item objects - const selectedItems = useMemo(() => { - if (!selectedRows || selectedRows.size === 0 || !bulkActions) { - return []; - } - - const defaultGetRowKey = (item: T, index: number) => { - if ('id' in item) { - return String(item.id); - } - return index; - }; - - const rowKeyExtractor = getRowKey || defaultGetRowKey; - - return data.filter((item, index) => { - const key = rowKeyExtractor(item, index); - return selectedRows.has(key); - }); - }, [selectedRows, data, getRowKey, bulkActions]); - const selectedCount = selectedRows?.size || 0; const showBulkActions = enableRowSelection && bulkActions && selectedCount > 0 && viewMode !== 'grid'; return ( <> - {showBulkActions && ( + {showBulkActions && selectedRows && bulkActions && (
{bulkActionsLabel && ( {bulkActionsLabel(selectedCount)} )} -
{bulkActions(selectedItems, selectedCount)}
+
{bulkActions(selectedRows)}
)} {viewMode === 'grid' ? ( diff --git a/packages/ui/src/components/DataView/DataView.types.ts b/packages/ui/src/components/DataView/DataView.types.ts index c703bd744..c47d333a1 100644 --- a/packages/ui/src/components/DataView/DataView.types.ts +++ b/packages/ui/src/components/DataView/DataView.types.ts @@ -22,6 +22,6 @@ export interface DataViewProps { enableRowSelection?: boolean; selectedRows?: Set; onSelectionChange?: (selected: Set) => void; - bulkActions?: (selectedItems: T[], selectedCount: number) => ReactNode; + bulkActions?: (selectedRowKeys: Set) => ReactNode; bulkActionsLabel?: (count: number) => string; } From deffd23cc7a9e07ea6f3521220664e29b01815d2 Mon Sep 17 00:00:00 2001 From: "lukasz.bielecki" Date: Mon, 8 Dec 2025 17:54:21 +0100 Subject: [PATCH 06/12] refactor: enhance bulk actions logic in InvoiceList and DataView components --- .../src/frontend/InvoiceList.client.tsx | 23 +++++++++---------- .../ui/src/components/DataView/DataView.tsx | 5 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx index 23d33ae36..5751176b9 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.tsx @@ -188,18 +188,17 @@ export const InvoiceListPure: React.FC = ({ locale, access } : undefined; - const bulkActions = - component.enableRowSelection && component.downloadAllButtonLabel - ? (selectedRowKeys: Set) => { - const selectedIds = Array.from(selectedRowKeys) as string[]; - return ( - - ); - } - : undefined; + const bulkActions = component.downloadAllButtonLabel + ? (selectedRowKeys: Set) => { + const selectedIds = Array.from(selectedRowKeys).map(String); + return ( + + ); + } + : undefined; const bulkActionsLabel = component.bulkActionsLabel ? (count: number) => { diff --git a/packages/ui/src/components/DataView/DataView.tsx b/packages/ui/src/components/DataView/DataView.tsx index 983119874..fa08e3845 100644 --- a/packages/ui/src/components/DataView/DataView.tsx +++ b/packages/ui/src/components/DataView/DataView.tsx @@ -20,11 +20,12 @@ export const DataView = >({ ...props }: DataViewProps) => { const selectedCount = selectedRows?.size || 0; - const showBulkActions = enableRowSelection && bulkActions && selectedCount > 0 && viewMode !== 'grid'; + const showBulkActions = + enableRowSelection && bulkActions && selectedRows && selectedCount > 0 && viewMode !== 'grid'; return ( <> - {showBulkActions && selectedRows && bulkActions && ( + {showBulkActions && (
{bulkActionsLabel && ( {bulkActionsLabel(selectedCount)} From 5f943c32bce9559c0b28d1dbb9a128981609ca27 Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:03:36 +0100 Subject: [PATCH 07/12] fix: fixed incorrect price rendering in `DataView` component --- .changeset/mean-shoes-behave.md | 6 ++++++ .../src/api-harmonization/invoice-list.model.ts | 8 ++------ packages/ui/src/lib/renderCell.tsx | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 .changeset/mean-shoes-behave.md diff --git a/.changeset/mean-shoes-behave.md b/.changeset/mean-shoes-behave.md new file mode 100644 index 000000000..ed32e482a --- /dev/null +++ b/.changeset/mean-shoes-behave.md @@ -0,0 +1,6 @@ +--- +'@o2s/blocks.invoice-list': patch +'@o2s/ui': patch +--- + +fixed incorrect price rendering in `DataView` component diff --git a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts index d2b9f0ebd..bd06f0308 100644 --- a/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts +++ b/packages/blocks/invoice-list/src/api-harmonization/invoice-list.model.ts @@ -50,10 +50,6 @@ export class Invoice { value: Invoices.Model.Invoice['paymentDueDate']; displayValue: string; }; - totalAmountDue!: { - value: Invoices.Model.Invoice['totalAmountDue']['value']; - }; - totalNetAmountDue!: { - value: Invoices.Model.Invoice['totalNetAmountDue']['value']; - }; + totalAmountDue!: Invoices.Model.Invoice['totalAmountDue']; + totalNetAmountDue!: Invoices.Model.Invoice['totalNetAmountDue']; } diff --git a/packages/ui/src/lib/renderCell.tsx b/packages/ui/src/lib/renderCell.tsx index 4317ce933..55e7b1567 100644 --- a/packages/ui/src/lib/renderCell.tsx +++ b/packages/ui/src/lib/renderCell.tsx @@ -56,7 +56,7 @@ export function renderCell( case 'price': { if (typeof value === 'object' && value !== null && 'value' in value) { - const priceValue = value.value as { value: number; currency?: string }; + const priceValue = value as { value: number; currency?: string }; const currency = ( column.config?.currencyKey ? String(item[column.config.currencyKey]) : priceValue.currency || 'USD' ) as 'USD' | 'EUR' | 'GBP' | 'PLN'; From 6290a35c2030eb9a462ac167c5f3a30fc1ce20bc Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:05:57 +0100 Subject: [PATCH 08/12] fix: removed unnecessary prop destructuring --- .changeset/shy-bobcats-count.md | 5 +++++ packages/ui/src/components/DataView/DataView.tsx | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/shy-bobcats-count.md diff --git a/.changeset/shy-bobcats-count.md b/.changeset/shy-bobcats-count.md new file mode 100644 index 000000000..6f196b29d --- /dev/null +++ b/.changeset/shy-bobcats-count.md @@ -0,0 +1,5 @@ +--- +'@o2s/ui': patch +--- + +removed unnecessary prop destructuring diff --git a/packages/ui/src/components/DataView/DataView.tsx b/packages/ui/src/components/DataView/DataView.tsx index fa08e3845..58ff50ca6 100644 --- a/packages/ui/src/components/DataView/DataView.tsx +++ b/packages/ui/src/components/DataView/DataView.tsx @@ -16,7 +16,6 @@ export const DataView = >({ bulkActions, bulkActionsLabel, data, - getRowKey, ...props }: DataViewProps) => { const selectedCount = selectedRows?.size || 0; From bc9e3618693740704df704a083d1871d63449f2a Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:15:53 +0100 Subject: [PATCH 09/12] fix: formatting and imports improvements --- .../Cards/InformativeCard/InformativeCard.tsx | 3 +-- .../Cards/InformativeCard/InformativeCard.types.ts | 2 +- .../src/components/Cards/PricingCard/PricingCard.tsx | 3 +-- .../ui/src/components/DataGrid/DataGrid.stories.tsx | 4 ++-- packages/ui/src/components/DataGrid/DataGrid.tsx | 6 +++--- .../ui/src/components/DataList/DataList.stories.tsx | 2 +- packages/ui/src/components/DataList/DataList.tsx | 11 ++++++----- packages/ui/src/components/LinkList/LinkList.tsx | 4 ++-- 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx index bd87b440f..39ea23dd6 100644 --- a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx +++ b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.tsx @@ -4,11 +4,10 @@ import React from 'react'; import { cn } from '@o2s/ui/lib/utils'; import { DynamicIcon } from '@o2s/ui/components/DynamicIcon'; +import { RichText } from '@o2s/ui/components/RichText'; import { Typography } from '@o2s/ui/elements/typography'; -import { RichText } from '../../RichText'; - import { InformativeCardProps } from './InformativeCard.types'; const InformativeCardContent: React.FC> = ({ diff --git a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.types.ts b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.types.ts index 0a70465b5..bdcfb6c88 100644 --- a/packages/ui/src/components/Cards/InformativeCard/InformativeCard.types.ts +++ b/packages/ui/src/components/Cards/InformativeCard/InformativeCard.types.ts @@ -1,6 +1,6 @@ import { Models } from '@o2s/utils.frontend'; -import { DynamicIconProps } from '../../DynamicIcon'; +import { DynamicIconProps } from '@o2s/ui/components/DynamicIcon'; export interface InformativeCardMeta { __id: string; diff --git a/packages/ui/src/components/Cards/PricingCard/PricingCard.tsx b/packages/ui/src/components/Cards/PricingCard/PricingCard.tsx index 5c2e8a8eb..f4efee806 100644 --- a/packages/ui/src/components/Cards/PricingCard/PricingCard.tsx +++ b/packages/ui/src/components/Cards/PricingCard/PricingCard.tsx @@ -6,6 +6,7 @@ import { cn } from '@o2s/ui/lib/utils'; import { DynamicIcon } from '@o2s/ui/components/DynamicIcon'; import { Image } from '@o2s/ui/components/Image'; +import { LinkList } from '@o2s/ui/components/LinkList'; import { Price } from '@o2s/ui/components/Price'; import { RichText } from '@o2s/ui/components/RichText'; import { TooltipHover } from '@o2s/ui/components/TooltipHover'; @@ -14,8 +15,6 @@ import { Badge } from '@o2s/ui/elements/badge'; import { Button } from '@o2s/ui/elements/button'; import { Typography } from '@o2s/ui/elements/typography'; -import { LinkList } from '../../LinkList'; - import { FeatureItemProps, PricingCardProps } from './PricingCard.types'; export const FeatureItem: React.FC = ({ title, description, icon }) => { diff --git a/packages/ui/src/components/DataGrid/DataGrid.stories.tsx b/packages/ui/src/components/DataGrid/DataGrid.stories.tsx index 8c318c2a8..2031206d1 100644 --- a/packages/ui/src/components/DataGrid/DataGrid.stories.tsx +++ b/packages/ui/src/components/DataGrid/DataGrid.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ArrowRight, Download } from 'lucide-react'; -import { Button } from '@o2s/ui/elements/button'; +import type { DataListColumnConfig } from '@o2s/ui/components/DataList'; -import type { DataListColumnConfig } from '../DataList/DataList.types'; +import { Button } from '@o2s/ui/elements/button'; import { DataGrid } from './DataGrid'; import type { DataGridProps } from './DataGrid.types'; diff --git a/packages/ui/src/components/DataGrid/DataGrid.tsx b/packages/ui/src/components/DataGrid/DataGrid.tsx index 7cd2e1ad8..d2996bf91 100644 --- a/packages/ui/src/components/DataGrid/DataGrid.tsx +++ b/packages/ui/src/components/DataGrid/DataGrid.tsx @@ -1,14 +1,14 @@ import React from 'react'; +import { renderCell } from '@o2s/ui/lib/renderCell'; import { cn } from '@o2s/ui/lib/utils'; +import { DataListColumnConfig } from '@o2s/ui/components/DataList'; + import { Card, CardContent, CardFooter, CardHeader } from '@o2s/ui/elements/card'; import { Separator } from '@o2s/ui/elements/separator'; import { Typography } from '@o2s/ui/elements/typography'; -import { renderCell } from '../../lib/renderCell'; -import { DataListColumnConfig } from '../DataList/DataList.types'; - import { DataGridProps } from './DataGrid.types'; const GRID_COLS_MAP: Record = { diff --git a/packages/ui/src/components/DataList/DataList.stories.tsx b/packages/ui/src/components/DataList/DataList.stories.tsx index ef7b37651..854521abd 100644 --- a/packages/ui/src/components/DataList/DataList.stories.tsx +++ b/packages/ui/src/components/DataList/DataList.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Download, Trash2 } from 'lucide-react'; import React, { useState } from 'react'; -import { Button } from '../../elements/button'; +import { Button } from '@o2s/ui/elements/button'; import { DataList } from './DataList'; import type { DataListColumnConfig } from './DataList.types'; diff --git a/packages/ui/src/components/DataList/DataList.tsx b/packages/ui/src/components/DataList/DataList.tsx index 6b27259db..2795b2842 100644 --- a/packages/ui/src/components/DataList/DataList.tsx +++ b/packages/ui/src/components/DataList/DataList.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useMemo } from 'react'; -import { Checkbox } from '../../elements/checkbox'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../elements/table'; -import { renderCell } from '../../lib/renderCell'; -import { cn } from '../../lib/utils'; +import { renderCell } from '@o2s/ui/lib/renderCell'; +import { cn } from '@o2s/ui/lib/utils'; + +import { Checkbox } from '@o2s/ui/elements/checkbox'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@o2s/ui/elements/table'; import { DataListProps } from './DataList.types'; diff --git a/packages/ui/src/components/LinkList/LinkList.tsx b/packages/ui/src/components/LinkList/LinkList.tsx index 58323e024..32df7f29d 100644 --- a/packages/ui/src/components/LinkList/LinkList.tsx +++ b/packages/ui/src/components/LinkList/LinkList.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { cn } from '@o2s/ui/lib/utils'; -import { Link } from '@o2s/ui/elements/link'; +import { DynamicIcon } from '@o2s/ui/components/DynamicIcon'; -import { DynamicIcon } from '../DynamicIcon'; +import { Link } from '@o2s/ui/elements/link'; import { LinkListProps } from './LinkList.types'; From 0842bcc87b7e5551fcd5fee55bce4d68b0b694fc Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:22:27 +0100 Subject: [PATCH 10/12] fix: added missing values --- .../src/frontend/InvoiceList.client.stories.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.stories.tsx b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.stories.tsx index e7c6d2c44..75e5b5021 100644 --- a/packages/blocks/invoice-list/src/frontend/InvoiceList.client.stories.tsx +++ b/packages/blocks/invoice-list/src/frontend/InvoiceList.client.stories.tsx @@ -175,9 +175,11 @@ export const Default: Story = { }, totalAmountDue: { value: 1250.5, + currency: 'EUR', }, totalNetAmountDue: { value: 1016.67, + currency: 'EUR', }, paymentDueDate: { displayValue: '07/15/2024', @@ -197,9 +199,11 @@ export const Default: Story = { }, totalAmountDue: { value: 3450.75, + currency: 'EUR', }, totalNetAmountDue: { value: 2805.49, + currency: 'EUR', }, paymentDueDate: { displayValue: '06/10/2024', @@ -219,9 +223,11 @@ export const Default: Story = { }, totalAmountDue: { value: 780.25, + currency: 'EUR', }, totalNetAmountDue: { value: 634.35, + currency: 'EUR', }, paymentDueDate: { displayValue: '07/05/2024', @@ -241,9 +247,11 @@ export const Default: Story = { }, totalAmountDue: { value: 0, + currency: 'EUR', }, totalNetAmountDue: { value: 0, + currency: 'EUR', }, paymentDueDate: { displayValue: '05/20/2024', @@ -263,9 +271,11 @@ export const Default: Story = { }, totalAmountDue: { value: 5670.3, + currency: 'EUR', }, totalNetAmountDue: { value: 4610, + currency: 'EUR', }, paymentDueDate: { displayValue: '06/15/2024', From ef740d55e3e5d8844876665d1811723ced19d102 Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:24:37 +0100 Subject: [PATCH 11/12] feat: wrapped checking for selected rows with `useMemo` --- packages/ui/src/components/DataList/DataList.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/DataList/DataList.tsx b/packages/ui/src/components/DataList/DataList.tsx index 2795b2842..a9c89a4e7 100644 --- a/packages/ui/src/components/DataList/DataList.tsx +++ b/packages/ui/src/components/DataList/DataList.tsx @@ -64,9 +64,12 @@ export function DataList>({ }; // Calculate selection state - const allRowKeys = data.map((item, index) => rowKeyExtractor(item, index)); + const allRowKeys = useMemo(() => data.map((item, index) => rowKeyExtractor(item, index)), [data, rowKeyExtractor]); // Count only selected items on current page - const selectedOnCurrentPage = allRowKeys.filter((key) => selectedRows?.has(key)).length; + const selectedOnCurrentPage = useMemo( + () => allRowKeys.filter((key) => selectedRows?.has(key)).length, + [allRowKeys, selectedRows], + ); const allSelected = enableRowSelection && data.length > 0 && selectedOnCurrentPage === allRowKeys.length && allRowKeys.length > 0; const someSelected = enableRowSelection && selectedOnCurrentPage > 0 && selectedOnCurrentPage < allRowKeys.length; From 973ad932bd79c8c1958254af1d6ddecb283b4390 Mon Sep 17 00:00:00 2001 From: Marcin Krasowski Date: Thu, 18 Dec 2025 12:39:45 +0100 Subject: [PATCH 12/12] feat: aligned logic for selecting rows --- packages/ui/src/components/DataGrid/DataGrid.tsx | 3 ++- packages/ui/src/components/DataList/DataList.tsx | 7 ++++--- packages/ui/src/lib/renderCell.tsx | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/components/DataGrid/DataGrid.tsx b/packages/ui/src/components/DataGrid/DataGrid.tsx index d2996bf91..96e975cf9 100644 --- a/packages/ui/src/components/DataGrid/DataGrid.tsx +++ b/packages/ui/src/components/DataGrid/DataGrid.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { renderCell } from '@o2s/ui/lib/renderCell'; import { cn } from '@o2s/ui/lib/utils'; import { DataListColumnConfig } from '@o2s/ui/components/DataList'; @@ -9,6 +8,8 @@ import { Card, CardContent, CardFooter, CardHeader } from '@o2s/ui/elements/card import { Separator } from '@o2s/ui/elements/separator'; import { Typography } from '@o2s/ui/elements/typography'; +import { renderCell } from '../../lib/renderCell'; + import { DataGridProps } from './DataGrid.types'; const GRID_COLS_MAP: Record = { diff --git a/packages/ui/src/components/DataList/DataList.tsx b/packages/ui/src/components/DataList/DataList.tsx index a9c89a4e7..9a6cd61a8 100644 --- a/packages/ui/src/components/DataList/DataList.tsx +++ b/packages/ui/src/components/DataList/DataList.tsx @@ -1,11 +1,12 @@ import React, { useMemo } from 'react'; -import { renderCell } from '@o2s/ui/lib/renderCell'; import { cn } from '@o2s/ui/lib/utils'; import { Checkbox } from '@o2s/ui/elements/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@o2s/ui/elements/table'; +import { renderCell } from '../../lib/renderCell'; + import { DataListProps } from './DataList.types'; /** @@ -52,9 +53,9 @@ export function DataList>({ }; const handleRowSelect = (rowKey: string | number, checked: boolean) => { - if (!onSelectionChange || !selectedRows) return; + if (!onSelectionChange) return; - const newSelected = new Set(selectedRows); + const newSelected = new Set(selectedRows || []); if (checked) { newSelected.add(rowKey); } else { diff --git a/packages/ui/src/lib/renderCell.tsx b/packages/ui/src/lib/renderCell.tsx index 55e7b1567..50afab775 100644 --- a/packages/ui/src/lib/renderCell.tsx +++ b/packages/ui/src/lib/renderCell.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import { DataListColumnConfig } from '@o2s/ui/components/DataList'; import { Price } from '@o2s/ui/components/Price'; import { Badge } from '@o2s/ui/elements/badge'; @@ -7,8 +8,6 @@ import { BadgeStatus } from '@o2s/ui/elements/badge-status'; import { Button } from '@o2s/ui/elements/button'; import { Link } from '@o2s/ui/elements/link'; -import { DataListColumnConfig } from '../components/DataList/DataList.types'; - /** * Default cell renderer based on column type * Shared utility for both DataList and DataGrid components