Преглед изворни кода

fix(dashboard): Make filter badges editable, improve styling

Michael Bromley пре 3 месеци
родитељ
комит
7cf0f7e449

+ 6 - 1
packages/dashboard/src/lib/components/data-table/add-filter-menu.tsx

@@ -53,7 +53,12 @@ export function AddFilterMenu({ columns }: Readonly<AddFilterMenuProps>) {
                     ))}
                 </DropdownMenuContent>
             </DropdownMenu>
-            {selectedColumn && <DataTableFilterDialog column={selectedColumn as any} />}
+            {selectedColumn && (
+                <DataTableFilterDialog
+                    column={selectedColumn as any}
+                    onEnter={() => setIsDialogOpen(false)}
+                />
+            )}
         </Dialog>
     );
 }

+ 35 - 0
packages/dashboard/src/lib/components/data-table/data-table-filter-badge-editable.tsx

@@ -0,0 +1,35 @@
+import { Dialog } from '@/vdb/components/ui/dialog.js';
+import { Column } from '@tanstack/react-table';
+import { useState } from 'react';
+import { DataTableFilterBadge } from './data-table-filter-badge.js';
+import { DataTableFilterDialog } from './data-table-filter-dialog.js';
+import { ColumnDataType } from './types.js';
+
+export function DataTableFilterBadgeEditable({
+    filter,
+    column,
+    onRemove,
+    dataType,
+    currencyCode,
+}: {
+    filter: any;
+    column: Column<any> | undefined;
+    onRemove: (filter: any) => void;
+    dataType: ColumnDataType;
+    currencyCode: string;
+}) {
+    const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+    return (
+        <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
+            <DataTableFilterBadge
+                filter={filter}
+                onRemove={onRemove}
+                onClick={() => setIsDialogOpen(true)}
+                dataType={dataType}
+                currencyCode={currencyCode}
+            />
+            {column && <DataTableFilterDialog column={column} onEnter={() => setIsDialogOpen(false)} />}
+        </Dialog>
+    );
+}

+ 23 - 16
packages/dashboard/src/lib/components/data-table/data-table-filter-badge.tsx

@@ -7,33 +7,40 @@ import { ColumnDataType } from './types.js';
 export function DataTableFilterBadge({
     filter,
     onRemove,
+    onClick,
     dataType,
     currencyCode,
 }: {
     filter: any;
     onRemove: (filter: any) => void;
+    onClick?: (filter: any) => void;
     dataType: ColumnDataType;
     currencyCode: string;
 }) {
     const [operator, value] = Object.entries(filter.value as Record<string, unknown>)[0];
     return (
-        <Badge
-            key={filter.id}
-            className="flex gap-1 flex-wrap items-center font-mono cursor-pointer"
-            variant="outline"
-            onClick={() => onRemove(filter)}
-        >
-            <Filter size="12" className="opacity-50 flex-shrink-0" />
-            <div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap" title={filter.id}>
-                {filter.id}
+        <Badge key={filter.id} className="flex gap-2 flex-wrap items-center" variant="outline">
+            <div
+                className="flex gap-1 flex-wrap items-center cursor-pointer flex-1"
+                onClick={() => onClick?.(filter)}
+            >
+                <Filter size="12" className="opacity-50 flex-shrink-0" />
+                <div
+                    className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap"
+                    title={filter.id}
+                >
+                    {filter.id}
+                </div>
+                <div className="text-muted-foreground flex-shrink-0">
+                    <HumanReadableOperator operator={operator as Operator} mode="short" />
+                </div>
+                <div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap flex flex-col @xl:flex-row @2xl:gap-1">
+                    <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
+                </div>
             </div>
-            <div className="text-muted-foreground flex-shrink-0">
-                <HumanReadableOperator operator={operator as Operator} mode="short" />
-            </div>
-            <div className="@xs:overflow-hidden @xs:text-ellipsis @xs:whitespace-nowrap flex flex-col @xl:flex-row @2xl:gap-1">
-                <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
-            </div>
-            <XIcon className="h-4 flex-shrink-0" />
+            <button className="border-l -mr-2" onClick={() => onRemove(filter)}>
+                <XIcon className="h-4 flex-shrink-0 cursor-pointer" />
+            </button>
         </Badge>
     );
 }

+ 26 - 8
packages/dashboard/src/lib/components/data-table/data-table-filter-dialog.tsx

@@ -2,14 +2,13 @@ import { Button } from '@/vdb/components/ui/button.js';
 import {
     DialogClose,
     DialogContent,
-    DialogDescription,
     DialogFooter,
     DialogHeader,
     DialogTitle,
 } from '@/vdb/components/ui/dialog.js';
 import { Trans } from '@lingui/react/macro';
 import { Column } from '@tanstack/react-table';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import { DataTableBooleanFilter } from './filters/data-table-boolean-filter.js';
 import { DataTableDateTimeFilter } from './filters/data-table-datetime-filter.js';
 import { DataTableIdFilter } from './filters/data-table-id-filter.js';
@@ -19,22 +18,38 @@ import { ColumnDataType } from './types.js';
 
 export interface DataTableFilterDialogProps {
     column: Column<any>;
+    onEnter?: () => void;
 }
 
-export function DataTableFilterDialog({ column }: Readonly<DataTableFilterDialogProps>) {
+export function DataTableFilterDialog({ column, onEnter }: Readonly<DataTableFilterDialogProps>) {
     const columnFilter = column.getFilterValue() as Record<string, string> | undefined;
     const [filter, setFilter] = useState(columnFilter);
 
+    useEffect(() => {
+        setFilter(columnFilter);
+    }, [columnFilter]);
+
     const columnDataType = (column.columnDef.meta as any)?.fieldInfo?.type as ColumnDataType;
     const columnId = column.id;
     const isEmpty = !filter || Object.keys(filter).length === 0;
+    const setFilterOnColumn = () => {
+        column.setFilterValue(filter);
+        setFilter(undefined);
+    };
+    const handleEnter = (e: React.KeyboardEvent<any>) => {
+        if (e.key === 'Enter') {
+            if (!isEmpty) {
+                setFilterOnColumn();
+                onEnter?.();
+            }
+        }
+    };
     return (
-        <DialogContent>
+        <DialogContent onKeyDown={handleEnter}>
             <DialogHeader>
                 <DialogTitle>
                     <Trans>Filter by {columnId}</Trans>
                 </DialogTitle>
-                <DialogDescription></DialogDescription>
             </DialogHeader>
             {columnDataType === 'String' ? (
                 <DataTableStringFilter value={filter} onChange={e => setFilter(e)} />
@@ -51,7 +66,11 @@ export function DataTableFilterDialog({ column }: Readonly<DataTableFilterDialog
             ) : null}
             <DialogFooter className="sm:justify-end">
                 {columnFilter && (
-                    <Button type="button" variant="secondary" onClick={e => column.setFilterValue(undefined)}>
+                    <Button
+                        type="button"
+                        variant="secondary"
+                        onClick={() => column.setFilterValue(undefined)}
+                    >
                         <Trans>Clear filter</Trans>
                     </Button>
                 )}
@@ -61,8 +80,7 @@ export function DataTableFilterDialog({ column }: Readonly<DataTableFilterDialog
                         variant="secondary"
                         disabled={isEmpty}
                         onClick={() => {
-                            column.setFilterValue(filter);
-                            setFilter(undefined);
+                            setFilterOnColumn();
                         }}
                     >
                         <Trans>Apply filter</Trans>

+ 4 - 3
packages/dashboard/src/lib/components/data-table/data-table.tsx

@@ -34,7 +34,7 @@ import { AddFilterMenu } from './add-filter-menu.js';
 import { DataTableBulkActions } from './data-table-bulk-actions.js';
 import { DataTableProvider } from './data-table-context.js';
 import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
-import { DataTableFilterBadge } from './data-table-filter-badge.js';
+import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
 
 export interface FacetedFilter {
     title: string;
@@ -212,7 +212,7 @@ export function DataTable<TData>({
             pagination.pageIndex;
         }
         prevColumnFiltersRef.current = columnFilters;
-    }, [columnFilters.length]);
+    }, [columnFilters]);
 
     const handleSearchChange = (value: string) => {
         setSearchTerm(value);
@@ -281,9 +281,10 @@ export function DataTable<TData>({
                                     const column = table.getColumn(f.id);
                                     const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
                                     return (
-                                        <DataTableFilterBadge
+                                        <DataTableFilterBadgeEditable
                                             key={f.id}
                                             filter={f}
+                                            column={column}
                                             currencyCode={currency}
                                             dataType={
                                                 (column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'

+ 79 - 34
packages/dashboard/src/lib/components/data-table/filters/data-table-datetime-filter.tsx

@@ -10,47 +10,92 @@ export interface DataTableDateTimeFilterProps {
 
 export const DATETIME_OPERATORS = ['eq', 'before', 'after', 'between', 'isNull'] as const;
 
+export interface DateTimeFilterResult {
+    filter: Record<string, any>;
+    error?: string;
+}
+
+export interface ParsedDateTimeFilter {
+    operator: string;
+    value?: Date;
+    startDate?: Date;
+    endDate?: Date;
+}
+
+export function parseDateTimeFilter(incomingValue: Record<string, any> | undefined): ParsedDateTimeFilter {
+    if (!incomingValue || Object.keys(incomingValue).length === 0) {
+        return { operator: 'eq' };
+    }
+
+    const operator = Object.keys(incomingValue)[0];
+    const value = Object.values(incomingValue)[0];
+
+    if (operator === 'isNull') {
+        return { operator };
+    }
+
+    if (operator === 'between' && typeof value === 'object' && value !== null) {
+        return {
+            operator,
+            startDate: value.start ? new Date(value.start) : undefined,
+            endDate: value.end ? new Date(value.end) : undefined,
+        };
+    }
+
+    // For eq, before, after operators
+    return {
+        operator,
+        value: typeof value === 'string' ? new Date(value) : undefined,
+    };
+}
+
+export function buildDateTimeFilter(
+    operator: string,
+    value?: Date,
+    startDate?: Date,
+    endDate?: Date,
+): DateTimeFilterResult {
+    if (operator === 'isNull') {
+        return { filter: { [operator]: true } };
+    }
+
+    if (operator === 'between') {
+        if (!startDate && !endDate) {
+            return { filter: {} };
+        }
+        if (!startDate || !endDate) {
+            return { filter: {}, error: 'Please enter both start and end dates' };
+        }
+        if (startDate > endDate) {
+            return { filter: {}, error: 'Start date must be before end date' };
+        }
+        return {
+            filter: { [operator]: { start: startDate.toISOString(), end: endDate.toISOString() } },
+        };
+    } else {
+        if (!value) {
+            return { filter: {} };
+        }
+        return { filter: { [operator]: value.toISOString() } };
+    }
+}
+
 export function DataTableDateTimeFilter({
     value: incomingValue,
     onChange,
 }: Readonly<DataTableDateTimeFilterProps>) {
-    const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'eq';
-    const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
-    const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
-    const [value, setValue] = useState<Date | undefined>(initialValue ? new Date(initialValue) : undefined);
-    const [startDate, setStartDate] = useState<Date | undefined>(undefined);
-    const [endDate, setEndDate] = useState<Date | undefined>(undefined);
+    const parsed = parseDateTimeFilter(incomingValue);
+    const [operator, setOperator] = useState<string>(parsed.operator);
+    const [value, setValue] = useState<Date | undefined>(parsed.value);
+    const [startDate, setStartDate] = useState<Date | undefined>(parsed.startDate);
+    const [endDate, setEndDate] = useState<Date | undefined>(parsed.endDate);
     const [error, setError] = useState<string>('');
 
-    useEffect(() => {
-        if (operator === 'isNull') {
-            onChange({ [operator]: true });
-            return;
-        }
 
-        if (operator === 'between') {
-            if (!startDate && !endDate) {
-                onChange({});
-                return;
-            }
-            if (!startDate || !endDate) {
-                setError('Please enter both start and end dates');
-                return;
-            }
-            if (startDate > endDate) {
-                setError('Start date must be before end date');
-                return;
-            }
-            setError('');
-            onChange({ [operator]: { start: startDate.toISOString(), end: endDate.toISOString() } });
-        } else {
-            if (!value) {
-                onChange({});
-                return;
-            }
-            setError('');
-            onChange({ [operator]: value.toISOString() });
-        }
+    useEffect(() => {
+        const result = buildDateTimeFilter(operator, value, startDate, endDate);
+        onChange(result.filter);
+        setError(result.error ?? '');
     }, [operator, value, startDate, endDate]);
 
     const parseToDate = (input: unknown): Date | undefined => {