From 449cf57bdadeb9877908292c5a44518af0f5d6c6 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 20:00:18 +0000 Subject: [PATCH 1/2] Fix Bazza UI data table filters issues 1. Fix server-side filtering to properly use filters from URL params 2. Ensure filters persist on page refresh by using URL as source of truth 3. Fix pagination to allow navigation between pages 4. Update faceted counts to reflect filtered data instead of original data --- .../data-table-bazza-filters.stories.tsx | 70 ++++++------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx b/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx index 3386c36f..d7cdc40f 100644 --- a/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx +++ b/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx @@ -311,60 +311,31 @@ const columns: ColumnDef[] = [ // --- NEW Wrapper Component using Loader Data --- function DataTableWithBazzaFilters() { const loaderData = useLoaderData(); + const location = useLocation(); const navigate = useNavigate(); - const location = useLocation(); // Get location for reading initial params - - // Extract data and meta from loader, provide defaults - const data = useMemo(() => loaderData?.data ?? [], [loaderData?.data]); - const pageCount = useMemo(() => loaderData?.meta.pageCount ?? 0, [loaderData?.meta.pageCount]); - - // NEW: Convert to Map of Maps - const facetedCounts = useMemo(() => { - const rawCounts = loaderData?.facetedCounts ?? {}; - const mapOfMaps = new Map>(); - for (const columnId in rawCounts) { - const innerObject = rawCounts[columnId as keyof typeof rawCounts]; - const innerMap = new Map(); - if (innerObject) { - // Check if innerObject is not undefined - for (const optionValue in innerObject) { - innerMap.set(optionValue, innerObject[optionValue as keyof typeof innerObject]); - } - } - mapOfMaps.set(columnId, innerMap); - } - return mapOfMaps; - }, [loaderData?.facetedCounts]); - - // Use filter sync hook (this manages filters in the URL) - const [filters, setFilters] = useFilterSync(); - // Ensure all filter changes update the URL and trigger loader reload - const handleFiltersChange: React.Dispatch> = (updater) => { - if (typeof updater === 'function') { - setFilters((prev) => updater(prev)); - } else { - setFilters(updater); - } - }; + // Ensure we have data even if loaderData is undefined + const data = loaderData?.data ?? []; + const pageCount = loaderData?.meta.pageCount ?? 0; + const facetedCounts = loaderData?.facetedCounts ?? {}; - // --- REVISED PAGINATION STATE MANAGEMENT --- - // Define defaults for pagination - const defaultPageIndex = dataTableRouterParsers.page.defaultValue; - const defaultPageSize = dataTableRouterParsers.pageSize.defaultValue; + // Default pagination values + const defaultPageIndex = 0; + const defaultPageSize = 10; - // Get current pagination values from URL (via loaderData) or use defaults - const currentPageIndexFromUrl = loaderData?.meta.page ?? defaultPageIndex; - const currentPageSizeFromUrl = loaderData?.meta.pageSize ?? defaultPageSize; + // Use useFilterSync to synchronize filters with URL + const { filters, handleFiltersChange } = useFilterSync({ + defaultValue: [], + paramName: 'filters', + }); - // Manage local pagination and sorting state - // Initialize from URL-derived values + // Local state for pagination and sorting const [pagination, setPagination] = useState({ - pageIndex: currentPageIndexFromUrl, - pageSize: currentPageSizeFromUrl, + pageIndex: loaderData?.meta.page ?? defaultPageIndex, + pageSize: loaderData?.meta.pageSize ?? defaultPageSize, }); - // Initialize sorting state from URL params if they exist + // Extract sorting from URL const [sorting, setSorting] = useState(() => { const params = new URLSearchParams(location.search); const sortField = params.get('sortField'); @@ -444,7 +415,7 @@ function DataTableWithBazzaFilters() { // Setup TanStack Table instance const table = useReactTable({ data, - columns: columns, // <-- FIX: Use original columns for cell rendering + columns: columns, // <-- Use original columns for cell rendering state: { pagination, // Controlled by local state, which is synced from URL sorting, // Controlled by local state, which is synced from URL @@ -571,9 +542,10 @@ const handleDataFetch = async ({ request }: LoaderFunctionArgs): Promise = ['status', 'assignee', 'priority']; - const facetedCounts = calculateFacetedCounts(mockDatabase, facetedColumns, allDefinedOptions); + const facetedCounts = calculateFacetedCounts(processedData, facetedColumns, allDefinedOptions); const response: DataResponse = { data: paginatedData, From d3580c6a43d26f79ae67dcd5cdc43078b8015fbe Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Fri, 9 May 2025 20:11:24 +0000 Subject: [PATCH 2/2] Add storybook tests for data-table-bazza-filters --- .../data-table-bazza-filters.stories.tsx | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx b/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx index d7cdc40f..1568de24 100644 --- a/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx +++ b/apps/docs/src/remix-hook-form/data-table-bazza-filters.stories.tsx @@ -14,12 +14,13 @@ import { useDataTableFilters } from '@lambdacurry/forms/ui/utils/use-data-table- import { useFilterSync } from '@lambdacurry/forms/ui/utils/use-filter-sync'; // Ensure this is the correct path for filter sync // Add icon imports import { CalendarIcon, CheckCircledIcon, PersonIcon, StarIcon, TextIcon } from '@radix-ui/react-icons'; -import type { Meta, StoryObj } from '@storybook/react'; // FIX: Add Meta, StoryObj +import type { Meta, StoryContext, StoryObj } from '@storybook/react'; // FIX: Add Meta, StoryObj, StoryContext import type { ColumnDef, PaginationState, SortingState } from '@tanstack/react-table'; // Added PaginationState, SortingState import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; // Added useState, useEffect import { type LoaderFunctionArgs, useLoaderData, useLocation, useNavigate } from 'react-router'; // Added LoaderFunctionArgs, useLoaderData, useNavigate, useLocation import { withReactRouterStubDecorator } from '../lib/storybook/react-router-stub'; // FIX: Add withReactRouterStubDecorator +import { expect, userEvent, within } from '@storybook/test'; // Add storybook test imports // --- Use MockIssue Schema and Data --- interface MockIssue { @@ -584,6 +585,90 @@ const meta = { export default meta; type Story = StoryObj; +// Test functions for the data table with Bazza filters +const testInitialRender = async ({ canvasElement }: StoryContext) => { + const canvas = within(canvasElement); + + // Check if the table is rendered with the correct title + const title = canvas.getByText('Issues Table (Bazza UI Server Filters via Loader)'); + expect(title).toBeInTheDocument(); + + // Check if the table has the correct number of rows initially (should be pageSize) + const rows = canvas.getAllByRole('row'); + // First row is header, so we expect pageSize + 1 rows + expect(rows.length).toBeGreaterThan(1); // At least header + 1 data row + + // Check if pagination is rendered + const paginationControls = canvas.getByRole('navigation'); + expect(paginationControls).toBeInTheDocument(); +}; + +const testFiltering = async ({ canvasElement }: StoryContext) => { + const canvas = within(canvasElement); + + // Open the filter dropdown + const filterButton = canvas.getByRole('button', { name: /filter/i }); + await userEvent.click(filterButton); + + // Select a filter type (e.g., Status) + const statusFilter = await canvas.findByText('Status'); + await userEvent.click(statusFilter); + + // Select a filter value (e.g., "Todo") + const todoOption = await canvas.findByText('Todo'); + await userEvent.click(todoOption); + + // Apply the filter + const applyButton = canvas.getByRole('button', { name: /apply/i }); + await userEvent.click(applyButton); + + // Wait for the table to update + await new Promise(resolve => setTimeout(resolve, 500)); + + // Check if the URL has been updated with the filter + expect(window.location.search).toContain('filters'); + + // Check if the filter chip is displayed + const filterChip = await canvas.findByText('Status: Todo'); + expect(filterChip).toBeInTheDocument(); +}; + +const testPagination = async ({ canvasElement }: StoryContext) => { + const canvas = within(canvasElement); + + // Get the initial page number + const initialPageButton = canvas.getByLabelText(/page 1/i); + expect(initialPageButton).toHaveAttribute('aria-current', 'page'); + + // Click on the next page button + const nextPageButton = canvas.getByLabelText(/go to next page/i); + await userEvent.click(nextPageButton); + + // Wait for the table to update + await new Promise(resolve => setTimeout(resolve, 500)); + + // Check if the URL has been updated with the new page + expect(window.location.search).toContain('page=1'); + + // Check if the page 2 button is now selected + const page2Button = canvas.getByLabelText(/page 2/i); + expect(page2Button).toHaveAttribute('aria-current', 'page'); +}; + +const testFilterPersistence = async ({ canvasElement }: StoryContext) => { + const canvas = within(canvasElement); + + // Simulate a page refresh by manually setting the URL with filters + // This is done by checking if the filter chip is still present after pagination + const filterChips = canvas.getAllByRole('button', { name: /remove filter/i }); + expect(filterChips.length).toBeGreaterThan(0); + + // Check if the filtered data is still displayed correctly + // We can verify this by checking if the filter chip is still present + const statusFilterChip = canvas.getByText(/Status:/i); + expect(statusFilterChip).toBeInTheDocument(); +}; + export const ServerDriven: Story = { args: {}, parameters: { @@ -594,4 +679,11 @@ export const ServerDriven: Story = { }, }, }, + play: async (context) => { + // Run the tests in sequence + await testInitialRender(context); + await testFiltering(context); + await testPagination(context); + await testFilterPersistence(context); + }, };