|
@@ -2,12 +2,19 @@
|
|
|
|
|
|
|
|
import { DataTablePagination } from '@/vdb/components/data-table/data-table-pagination.js';
|
|
import { DataTablePagination } from '@/vdb/components/data-table/data-table-pagination.js';
|
|
|
import { DataTableViewOptions } from '@/vdb/components/data-table/data-table-view-options.js';
|
|
import { DataTableViewOptions } from '@/vdb/components/data-table/data-table-view-options.js';
|
|
|
|
|
+import { GlobalViewsBar } from '@/vdb/components/data-table/global-views-bar.js';
|
|
|
|
|
+import { MyViewsButton } from '@/vdb/components/data-table/my-views-button.js';
|
|
|
import { RefreshButton } from '@/vdb/components/data-table/refresh-button.js';
|
|
import { RefreshButton } from '@/vdb/components/data-table/refresh-button.js';
|
|
|
|
|
+import { SaveViewButton } from '@/vdb/components/data-table/save-view-button.js';
|
|
|
|
|
+import { Button } from '@/vdb/components/ui/button.js';
|
|
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
import { Input } from '@/vdb/components/ui/input.js';
|
|
|
import { Skeleton } from '@/vdb/components/ui/skeleton.js';
|
|
import { Skeleton } from '@/vdb/components/ui/skeleton.js';
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
|
|
|
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
|
|
|
+import { usePage } from '@/vdb/hooks/use-page.js';
|
|
|
|
|
+import { useSavedViews } from '@/vdb/hooks/use-saved-views.js';
|
|
|
|
|
+import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
|
import {
|
|
import {
|
|
|
ColumnDef,
|
|
ColumnDef,
|
|
|
ColumnFilter,
|
|
ColumnFilter,
|
|
@@ -25,6 +32,7 @@ import { RowSelectionState, TableOptions } from '@tanstack/table-core';
|
|
|
import React, { Suspense, useEffect } from 'react';
|
|
import React, { Suspense, useEffect } from 'react';
|
|
|
import { AddFilterMenu } from './add-filter-menu.js';
|
|
import { AddFilterMenu } from './add-filter-menu.js';
|
|
|
import { DataTableBulkActions } from './data-table-bulk-actions.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 { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
|
|
|
import { DataTableFilterBadge } from './data-table-filter-badge.js';
|
|
import { DataTableFilterBadge } from './data-table-filter-badge.js';
|
|
|
|
|
|
|
@@ -106,7 +114,12 @@ export function DataTable<TData>({
|
|
|
}: Readonly<DataTableProps<TData>>) {
|
|
}: Readonly<DataTableProps<TData>>) {
|
|
|
const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
|
|
const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
|
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
|
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
|
|
|
|
|
+ const [searchTerm, setSearchTerm] = React.useState<string>('');
|
|
|
const { activeChannel } = useChannel();
|
|
const { activeChannel } = useChannel();
|
|
|
|
|
+ const { pageId } = usePage();
|
|
|
|
|
+ const savedViewsResult = useSavedViews();
|
|
|
|
|
+ const globalViews = pageId && onFilterChange ? savedViewsResult.globalViews : [];
|
|
|
|
|
+ const { i18n } = useLingui();
|
|
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
|
pageIndex: (page ?? 1) - 1,
|
|
pageIndex: (page ?? 1) - 1,
|
|
|
pageSize: itemsPerPage ?? 10,
|
|
pageSize: itemsPerPage ?? 10,
|
|
@@ -175,19 +188,37 @@ export function DataTable<TData>({
|
|
|
}, [columnVisibility]);
|
|
}, [columnVisibility]);
|
|
|
|
|
|
|
|
const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
|
|
const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
|
|
|
|
|
+
|
|
|
|
|
+ const handleSearchChange = (value: string) => {
|
|
|
|
|
+ setSearchTerm(value);
|
|
|
|
|
+ onSearchTermChange?.(value);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <>
|
|
|
|
|
- <div className="flex justify-between items-start">
|
|
|
|
|
- <div className="flex flex-col space-y-2">
|
|
|
|
|
- <div className="flex items-center justify-start gap-2">
|
|
|
|
|
|
|
+ <DataTableProvider
|
|
|
|
|
+ columnFilters={columnFilters}
|
|
|
|
|
+ setColumnFilters={setColumnFilters}
|
|
|
|
|
+ searchTerm={searchTerm}
|
|
|
|
|
+ setSearchTerm={setSearchTerm}
|
|
|
|
|
+ sorting={sorting}
|
|
|
|
|
+ setSorting={setSorting}
|
|
|
|
|
+ pageId={pageId}
|
|
|
|
|
+ onFilterChange={onFilterChange}
|
|
|
|
|
+ onSearchTermChange={onSearchTermChange}
|
|
|
|
|
+ onRefresh={onRefresh}
|
|
|
|
|
+ isLoading={isLoading}
|
|
|
|
|
+ table={table}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <div className="flex items-center justify-between gap-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
{onSearchTermChange && (
|
|
{onSearchTermChange && (
|
|
|
- <div className="flex items-center">
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder="Filter..."
|
|
|
|
|
- onChange={event => onSearchTermChange(event.target.value)}
|
|
|
|
|
- className="max-w-sm w-md"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder={i18n.t('Filter...')}
|
|
|
|
|
+ value={searchTerm}
|
|
|
|
|
+ onChange={event => handleSearchChange(event.target.value)}
|
|
|
|
|
+ className="w-64"
|
|
|
|
|
+ />
|
|
|
)}
|
|
)}
|
|
|
<Suspense>
|
|
<Suspense>
|
|
|
{Object.entries(facetedFilters ?? {}).map(([key, filter]) => (
|
|
{Object.entries(facetedFilters ?? {}).map(([key, filter]) => (
|
|
@@ -201,99 +232,120 @@ export function DataTable<TData>({
|
|
|
))}
|
|
))}
|
|
|
</Suspense>
|
|
</Suspense>
|
|
|
{onFilterChange && <AddFilterMenu columns={table.getAllColumns()} />}
|
|
{onFilterChange && <AddFilterMenu columns={table.getAllColumns()} />}
|
|
|
|
|
+ {pageId && onFilterChange && <MyViewsButton />}
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="flex gap-1">
|
|
|
|
|
- {columnFilters
|
|
|
|
|
- .filter(f => !facetedFilters?.[f.id])
|
|
|
|
|
- .map(f => {
|
|
|
|
|
- const column = table.getColumn(f.id);
|
|
|
|
|
- const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
|
|
|
|
|
- return (
|
|
|
|
|
- <DataTableFilterBadge
|
|
|
|
|
- key={f.id}
|
|
|
|
|
- filter={f}
|
|
|
|
|
- currencyCode={currency}
|
|
|
|
|
- dataType={
|
|
|
|
|
- (column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'
|
|
|
|
|
- }
|
|
|
|
|
- onRemove={() =>
|
|
|
|
|
- setColumnFilters(old => old.filter(x => x.id !== f.id))
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ {pageId && onFilterChange && <SaveViewButton />}
|
|
|
|
|
+ {!disableViewOptions && <DataTableViewOptions table={table} />}
|
|
|
|
|
+ {onRefresh && <RefreshButton onRefresh={onRefresh} isLoading={isLoading ?? false} />}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="flex items-center justify-start gap-2">
|
|
|
|
|
- {!disableViewOptions && <DataTableViewOptions table={table} />}
|
|
|
|
|
- {onRefresh && <RefreshButton onRefresh={onRefresh} isLoading={isLoading ?? false} />}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <div className="rounded-md border my-2 relative">
|
|
|
|
|
- <Table>
|
|
|
|
|
- <TableHeader>
|
|
|
|
|
- {table.getHeaderGroups().map(headerGroup => (
|
|
|
|
|
- <TableRow key={headerGroup.id}>
|
|
|
|
|
- {headerGroup.headers.map(header => {
|
|
|
|
|
|
|
+ {(pageId && onFilterChange && globalViews.length > 0) ||
|
|
|
|
|
+ columnFilters.filter(f => !facetedFilters?.[f.id]).length > 0 ? (
|
|
|
|
|
+ <div className="flex items-center justify-between bg-muted/40 rounded border border-border p-2">
|
|
|
|
|
+ <div className="flex items-center">
|
|
|
|
|
+ {pageId && onFilterChange && <GlobalViewsBar />}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex gap-1 items-center">
|
|
|
|
|
+ {columnFilters
|
|
|
|
|
+ .filter(f => !facetedFilters?.[f.id])
|
|
|
|
|
+ .map(f => {
|
|
|
|
|
+ const column = table.getColumn(f.id);
|
|
|
|
|
+ const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
|
|
|
return (
|
|
return (
|
|
|
- <TableHead key={header.id}>
|
|
|
|
|
- {header.isPlaceholder
|
|
|
|
|
- ? null
|
|
|
|
|
- : flexRender(
|
|
|
|
|
- header.column.columnDef.header,
|
|
|
|
|
- header.getContext(),
|
|
|
|
|
- )}
|
|
|
|
|
- </TableHead>
|
|
|
|
|
|
|
+ <DataTableFilterBadge
|
|
|
|
|
+ key={f.id}
|
|
|
|
|
+ filter={f}
|
|
|
|
|
+ currencyCode={currency}
|
|
|
|
|
+ dataType={
|
|
|
|
|
+ (column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'
|
|
|
|
|
+ }
|
|
|
|
|
+ onRemove={() =>
|
|
|
|
|
+ setColumnFilters(old => old.filter(x => x.id !== f.id))
|
|
|
|
|
+ }
|
|
|
|
|
+ />
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
|
- </TableRow>
|
|
|
|
|
- ))}
|
|
|
|
|
- </TableHeader>
|
|
|
|
|
- <TableBody>
|
|
|
|
|
- {isLoading && !data?.length ? (
|
|
|
|
|
- Array.from({ length: pagination.pageSize }).map((_, index) => (
|
|
|
|
|
- <TableRow
|
|
|
|
|
- key={`skeleton-${index}`}
|
|
|
|
|
- className="animate-in fade-in duration-100"
|
|
|
|
|
|
|
+ {columnFilters.filter(f => !facetedFilters?.[f.id]).length > 0 && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => setColumnFilters([])}
|
|
|
|
|
+ className="text-xs opacity-60 hover:opacity-100"
|
|
|
>
|
|
>
|
|
|
- {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
|
|
|
|
|
- <TableCell
|
|
|
|
|
- key={`skeleton-cell-${index}-${cellIndex}`}
|
|
|
|
|
- className="h-12"
|
|
|
|
|
- >
|
|
|
|
|
- <Skeleton className="h-4 my-2 w-full" />
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ <Trans>Clear all</Trans>
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+
|
|
|
|
|
+ <div className="rounded-md border my-2 relative shadow-sm">
|
|
|
|
|
+ <Table>
|
|
|
|
|
+ <TableHeader className="bg-muted/50">
|
|
|
|
|
+ {table.getHeaderGroups().map(headerGroup => (
|
|
|
|
|
+ <TableRow key={headerGroup.id}>
|
|
|
|
|
+ {headerGroup.headers.map(header => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <TableHead key={header.id}>
|
|
|
|
|
+ {header.isPlaceholder
|
|
|
|
|
+ ? null
|
|
|
|
|
+ : flexRender(
|
|
|
|
|
+ header.column.columnDef.header,
|
|
|
|
|
+ header.getContext(),
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TableHead>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
</TableRow>
|
|
</TableRow>
|
|
|
- ))
|
|
|
|
|
- ) : table.getRowModel().rows?.length ? (
|
|
|
|
|
- table.getRowModel().rows.map(row => (
|
|
|
|
|
- <TableRow
|
|
|
|
|
- key={row.id}
|
|
|
|
|
- data-state={row.getIsSelected() && 'selected'}
|
|
|
|
|
- className="animate-in fade-in duration-100"
|
|
|
|
|
- >
|
|
|
|
|
- {row.getVisibleCells().map(cell => (
|
|
|
|
|
- <TableCell key={cell.id} className="h-12">
|
|
|
|
|
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </TableHeader>
|
|
|
|
|
+ <TableBody>
|
|
|
|
|
+ {isLoading && !data?.length ? (
|
|
|
|
|
+ Array.from({ length: pagination.pageSize }).map((_, index) => (
|
|
|
|
|
+ <TableRow
|
|
|
|
|
+ key={`skeleton-${index}`}
|
|
|
|
|
+ className="animate-in fade-in duration-100"
|
|
|
|
|
+ >
|
|
|
|
|
+ {Array.from({ length: visibleColumnCount }).map((_, cellIndex) => (
|
|
|
|
|
+ <TableCell
|
|
|
|
|
+ key={`skeleton-cell-${index}-${cellIndex}`}
|
|
|
|
|
+ className="h-12"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Skeleton className="h-4 my-2 w-full" />
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : table.getRowModel().rows?.length ? (
|
|
|
|
|
+ table.getRowModel().rows.map(row => (
|
|
|
|
|
+ <TableRow
|
|
|
|
|
+ key={row.id}
|
|
|
|
|
+ data-state={row.getIsSelected() && 'selected'}
|
|
|
|
|
+ className="animate-in fade-in duration-100"
|
|
|
|
|
+ >
|
|
|
|
|
+ {row.getVisibleCells().map(cell => (
|
|
|
|
|
+ <TableCell key={cell.id} className="h-12">
|
|
|
|
|
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <TableRow className="animate-in fade-in duration-100">
|
|
|
|
|
+ <TableCell colSpan={columns.length} className="h-24 text-center">
|
|
|
|
|
+ <Trans>No results</Trans>
|
|
|
|
|
+ </TableCell>
|
|
|
</TableRow>
|
|
</TableRow>
|
|
|
- ))
|
|
|
|
|
- ) : (
|
|
|
|
|
- <TableRow className="animate-in fade-in duration-100">
|
|
|
|
|
- <TableCell colSpan={columns.length} className="h-24 text-center">
|
|
|
|
|
- No results.
|
|
|
|
|
- </TableCell>
|
|
|
|
|
- </TableRow>
|
|
|
|
|
- )}
|
|
|
|
|
- {children}
|
|
|
|
|
- </TableBody>
|
|
|
|
|
- </Table>
|
|
|
|
|
- <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
|
|
|
|
|
|
|
+ )}
|
|
|
|
|
+ {children}
|
|
|
|
|
+ </TableBody>
|
|
|
|
|
+ </Table>
|
|
|
|
|
+ <DataTableBulkActions bulkActions={bulkActions ?? []} table={table} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {onPageChange && totalItems != null && <DataTablePagination table={table} />}
|
|
|
</div>
|
|
</div>
|
|
|
- {onPageChange && totalItems != null && <DataTablePagination table={table} />}
|
|
|
|
|
- </>
|
|
|
|
|
|
|
+ </DataTableProvider>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|