@vendure/dashboard 3.2.2 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/utils/ast-utils.d.ts +10 -0
- package/dist/plugin/utils/ast-utils.js +96 -0
- package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
- package/dist/plugin/utils/ast-utils.spec.js +120 -0
- package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
- package/dist/plugin/utils/config-loader.js +325 -0
- package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
- package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +6 -0
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
- package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
- package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
- package/dist/plugin/vite-plugin-config-loader.js +18 -9
- package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
- package/dist/plugin/vite-plugin-gql-tada.js +2 -2
- package/dist/plugin/vite-plugin-ui-config.js +3 -2
- package/package.json +8 -6
- package/src/app/app-providers.tsx +8 -8
- package/src/app/main.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -1
- package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
- package/src/app/routes/_authenticated/_products/products.tsx +1 -1
- package/src/app/routes/_authenticated.tsx +12 -1
- package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
- package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
- package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
- package/src/lib/components/data-table/data-table-types.ts +1 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +72 -23
- package/src/lib/components/data-table/data-table.tsx +23 -24
- package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
- package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
- package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
- package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
- package/src/lib/components/layout/nav-user.tsx +4 -4
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
- package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
- package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
- package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
- package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
- package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
- package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
- package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
- package/src/lib/components/shared/custom-fields-form.tsx +4 -3
- package/src/lib/components/shared/customer-selector.tsx +13 -14
- package/src/lib/components/shared/detail-page-button.tsx +2 -2
- package/src/lib/components/shared/entity-assets.tsx +3 -3
- package/src/lib/components/shared/navigation-confirmation.tsx +39 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +9 -1
- package/src/lib/components/shared/product-variant-selector.tsx +111 -0
- package/src/lib/components/shared/vendure-image.tsx +1 -1
- package/src/lib/components/ui/calendar.tsx +508 -63
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
- package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
- package/src/lib/framework/page/list-page.tsx +23 -4
- package/src/lib/framework/page/use-detail-page.ts +1 -0
- package/src/lib/graphql/fragments.tsx +8 -0
- package/src/lib/index.ts +5 -5
- package/src/lib/providers/auth.tsx +12 -9
- package/src/lib/providers/channel-provider.tsx +1 -0
- package/src/lib/providers/server-config.tsx +7 -1
- package/src/lib/providers/user-settings.tsx +24 -0
- package/vite/utils/ast-utils.spec.ts +128 -0
- package/vite/utils/ast-utils.ts +119 -0
- package/vite/utils/config-loader.ts +410 -0
- package/vite/{schema-generator.ts → utils/schema-generator.ts} +7 -1
- package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
- package/vite/vite-plugin-admin-api-schema.ts +2 -2
- package/vite/vite-plugin-config-loader.ts +25 -13
- package/vite/vite-plugin-dashboard-metadata.ts +19 -15
- package/vite/vite-plugin-gql-tada.ts +2 -2
- package/vite/vite-plugin-ui-config.ts +3 -2
- package/dist/plugin/config-loader.js +0 -141
- package/src/lib/components/shared/asset-preview.tsx +0 -345
- package/vite/config-loader.ts +0 -181
- /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
|
@@ -7,23 +7,25 @@ import {
|
|
|
7
7
|
DialogHeader,
|
|
8
8
|
DialogTitle,
|
|
9
9
|
} from '@/components/ui/dialog.js';
|
|
10
|
-
import { Input } from '@/components/ui/input.js';
|
|
11
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
|
|
12
10
|
import { Trans } from '@/lib/trans.js';
|
|
13
11
|
import { Column } from '@tanstack/react-table';
|
|
14
|
-
import
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { DataTableBooleanFilter } from './filters/data-table-boolean-filter.js';
|
|
14
|
+
import { DataTableDateTimeFilter } from './filters/data-table-datetime-filter.js';
|
|
15
|
+
import { DataTableIdFilter } from './filters/data-table-id-filter.js';
|
|
16
|
+
import { DataTableNumberFilter } from './filters/data-table-number-filter.js';
|
|
17
|
+
import { DataTableStringFilter } from './filters/data-table-string-filter.js';
|
|
18
|
+
import { ColumnDataType } from './data-table-types.js';
|
|
15
19
|
|
|
16
20
|
export interface DataTableFilterDialogProps {
|
|
17
21
|
column: Column<any>;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
const STRING_OPERATORS = ['eq', 'notEq', 'contains', 'notContains', 'in', 'notIn', 'regex', 'isNull'];
|
|
21
|
-
|
|
22
24
|
export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
23
25
|
const columnFilter = column.getFilterValue() as Record<string, string> | undefined;
|
|
24
|
-
const [
|
|
25
|
-
|
|
26
|
-
const
|
|
26
|
+
const [filter, setFilter] = useState(columnFilter);
|
|
27
|
+
|
|
28
|
+
const columnDataType = (column.columnDef.meta as any)?.fieldInfo?.type as ColumnDataType;
|
|
27
29
|
const columnId = column.id;
|
|
28
30
|
return (
|
|
29
31
|
<DialogContent>
|
|
@@ -33,25 +35,19 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
|
33
35
|
</DialogTitle>
|
|
34
36
|
<DialogDescription></DialogDescription>
|
|
35
37
|
</DialogHeader>
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<Input
|
|
50
|
-
placeholder="Enter filter value..."
|
|
51
|
-
value={value}
|
|
52
|
-
onChange={e => setValue(e.target.value)}
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
38
|
+
{columnDataType === 'String' ? (
|
|
39
|
+
<DataTableStringFilter value={filter} onChange={e => setFilter(e)} />
|
|
40
|
+
) : columnDataType === 'Int' || columnDataType === 'Float' ? (
|
|
41
|
+
<DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='number' />
|
|
42
|
+
) : columnDataType === 'DateTime' ? (
|
|
43
|
+
<DataTableDateTimeFilter value={filter} onChange={e => setFilter(e)} />
|
|
44
|
+
) : columnDataType === 'Boolean' ? (
|
|
45
|
+
<DataTableBooleanFilter value={filter} onChange={e => setFilter(e)} />
|
|
46
|
+
) : columnDataType === 'ID' ? (
|
|
47
|
+
<DataTableIdFilter value={filter} onChange={e => setFilter(e)} />
|
|
48
|
+
) : columnDataType === 'Money' ? (
|
|
49
|
+
<DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='money' />
|
|
50
|
+
) : null}
|
|
55
51
|
<DialogFooter className="sm:justify-end">
|
|
56
52
|
{columnFilter && (
|
|
57
53
|
<Button type="button" variant="secondary" onClick={e => column.setFilterValue(undefined)}>
|
|
@@ -62,7 +58,10 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
|
62
58
|
<Button
|
|
63
59
|
type="button"
|
|
64
60
|
variant="secondary"
|
|
65
|
-
|
|
61
|
+
onClick={e => {
|
|
62
|
+
column.setFilterValue(filter);
|
|
63
|
+
setFilter(undefined);
|
|
64
|
+
}}
|
|
66
65
|
>
|
|
67
66
|
<Trans>Apply filter</Trans>
|
|
68
67
|
</Button>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ColumnDataType = 'String' | 'Int' | 'Float' | 'DateTime' | 'Boolean' | 'ID' | 'Money';
|
|
@@ -1,51 +1,100 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DndContext, closestCenter } from '@dnd-kit/core';
|
|
4
|
+
import {
|
|
5
|
+
restrictToVerticalAxis,
|
|
6
|
+
} from '@dnd-kit/modifiers';
|
|
7
|
+
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
8
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
4
9
|
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
|
5
10
|
import { Table } from '@tanstack/react-table';
|
|
6
|
-
import {
|
|
11
|
+
import { GripVertical, Settings2 } from 'lucide-react';
|
|
7
12
|
|
|
8
13
|
import { Button } from '@/components/ui/button.js';
|
|
9
14
|
import {
|
|
10
15
|
DropdownMenu,
|
|
11
16
|
DropdownMenuCheckboxItem,
|
|
12
|
-
DropdownMenuContent
|
|
13
|
-
DropdownMenuLabel,
|
|
14
|
-
DropdownMenuSeparator,
|
|
17
|
+
DropdownMenuContent
|
|
15
18
|
} from '@/components/ui/dropdown-menu.js';
|
|
19
|
+
import { usePage } from '@/hooks/use-page.js';
|
|
20
|
+
import { useUserSettings } from '@/hooks/use-user-settings.js';
|
|
21
|
+
import { Trans } from '@/lib/trans.js';
|
|
16
22
|
|
|
17
23
|
interface DataTableViewOptionsProps<TData> {
|
|
18
24
|
table: Table<TData>;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
function SortableItem({ id, children }: { id: string; children: React.ReactNode }) {
|
|
28
|
+
const {
|
|
29
|
+
attributes,
|
|
30
|
+
listeners,
|
|
31
|
+
setNodeRef,
|
|
32
|
+
transform,
|
|
33
|
+
transition,
|
|
34
|
+
} = useSortable({ id });
|
|
35
|
+
|
|
36
|
+
const style = {
|
|
37
|
+
transform: CSS.Transform.toString(transform),
|
|
38
|
+
transition,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div ref={setNodeRef} style={style} className="flex items-center gap-.5">
|
|
43
|
+
<div {...attributes} {...listeners} className="cursor-grab">
|
|
44
|
+
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
45
|
+
</div>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
|
|
52
|
+
const { setTableSettings } = useUserSettings();
|
|
53
|
+
const page = usePage();
|
|
54
|
+
const columns = table
|
|
55
|
+
.getAllColumns()
|
|
56
|
+
.filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide());
|
|
57
|
+
|
|
58
|
+
const handleDragEnd = (event: any) => {
|
|
59
|
+
const { active, over } = event;
|
|
60
|
+
if (active.id !== over.id) {
|
|
61
|
+
const activeIndex = columns.findIndex(col => col.id === active.id);
|
|
62
|
+
const overIndex = columns.findIndex(col => col.id === over.id);
|
|
63
|
+
// update the column order in the `columns` array
|
|
64
|
+
const newColumns = [...columns];
|
|
65
|
+
newColumns.splice(overIndex, 0, newColumns.splice(activeIndex, 1)[0]);
|
|
66
|
+
if (page?.pageId) {
|
|
67
|
+
setTableSettings(page.pageId, 'columnOrder', newColumns.map(col => col.id));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
22
72
|
return (
|
|
23
73
|
<div className="flex items-center gap-2">
|
|
24
74
|
<DropdownMenu>
|
|
25
75
|
<DropdownMenuTrigger asChild>
|
|
26
76
|
<Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex">
|
|
27
77
|
<Settings2 />
|
|
28
|
-
|
|
78
|
+
<Trans>Columns</Trans>
|
|
29
79
|
</Button>
|
|
30
80
|
</DropdownMenuTrigger>
|
|
31
81
|
<DropdownMenuContent align="end" className="w-[150px]">
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})}
|
|
82
|
+
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis]}>
|
|
83
|
+
<SortableContext items={columns.map(col => col.id)} strategy={verticalListSortingStrategy}>
|
|
84
|
+
{columns.map(column => (
|
|
85
|
+
<SortableItem key={column.id} id={column.id}>
|
|
86
|
+
<DropdownMenuCheckboxItem
|
|
87
|
+
className="capitalize"
|
|
88
|
+
checked={column.getIsVisible()}
|
|
89
|
+
onCheckedChange={value => column.toggleVisibility(!!value)}
|
|
90
|
+
onSelect={(e) => e.preventDefault()}
|
|
91
|
+
>
|
|
92
|
+
{column.id}
|
|
93
|
+
</DropdownMenuCheckboxItem>
|
|
94
|
+
</SortableItem>
|
|
95
|
+
))}
|
|
96
|
+
</SortableContext>
|
|
97
|
+
</DndContext>
|
|
49
98
|
</DropdownMenuContent>
|
|
50
99
|
</DropdownMenu>
|
|
51
100
|
</div>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
|
|
4
4
|
import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
|
|
5
|
-
import { Badge } from '@/components/ui/badge.js';
|
|
6
5
|
import { Input } from '@/components/ui/input.js';
|
|
7
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
|
|
8
7
|
import {
|
|
@@ -19,9 +18,11 @@ import {
|
|
|
19
18
|
VisibilityState,
|
|
20
19
|
} from '@tanstack/react-table';
|
|
21
20
|
import { TableOptions } from '@tanstack/table-core';
|
|
22
|
-
import { CircleX, Filter } from 'lucide-react';
|
|
23
21
|
import React, { Suspense, useEffect } from 'react';
|
|
22
|
+
import { AddFilterMenu } from './add-filter-menu.js';
|
|
24
23
|
import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
|
|
24
|
+
import { DataTableFilterBadge } from './data-table-filter-badge.js';
|
|
25
|
+
import { useChannel } from '@/hooks/use-channel.js';
|
|
25
26
|
|
|
26
27
|
export interface FacetedFilter {
|
|
27
28
|
title: string;
|
|
@@ -41,6 +42,7 @@ interface DataTableProps<TData, TValue> {
|
|
|
41
42
|
onPageChange?: (table: TableType<TData>, page: number, itemsPerPage: number) => void;
|
|
42
43
|
onSortChange?: (table: TableType<TData>, sorting: SortingState) => void;
|
|
43
44
|
onFilterChange?: (table: TableType<TData>, columnFilters: ColumnFilter[]) => void;
|
|
45
|
+
onColumnVisibilityChange?: (table: TableType<TData>, columnVisibility: VisibilityState) => void;
|
|
44
46
|
onSearchTermChange?: (searchTerm: string) => void;
|
|
45
47
|
defaultColumnVisibility?: VisibilityState;
|
|
46
48
|
facetedFilters?: { [key: string]: FacetedFilter | undefined };
|
|
@@ -64,6 +66,7 @@ export function DataTable<TData, TValue>({
|
|
|
64
66
|
onSortChange,
|
|
65
67
|
onFilterChange,
|
|
66
68
|
onSearchTermChange,
|
|
69
|
+
onColumnVisibilityChange,
|
|
67
70
|
defaultColumnVisibility,
|
|
68
71
|
facetedFilters,
|
|
69
72
|
disableViewOptions,
|
|
@@ -71,6 +74,7 @@ export function DataTable<TData, TValue>({
|
|
|
71
74
|
}: DataTableProps<TData, TValue>) {
|
|
72
75
|
const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
|
|
73
76
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
|
|
77
|
+
const { activeChannel } = useChannel();
|
|
74
78
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
|
75
79
|
pageIndex: (page ?? 1) - 1,
|
|
76
80
|
pageSize: itemsPerPage ?? 10,
|
|
@@ -117,6 +121,11 @@ export function DataTable<TData, TValue>({
|
|
|
117
121
|
useEffect(() => {
|
|
118
122
|
onFilterChange?.(table, columnFilters);
|
|
119
123
|
}, [columnFilters]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
onColumnVisibilityChange?.(table, columnVisibility);
|
|
127
|
+
}, [columnVisibility]);
|
|
128
|
+
|
|
120
129
|
return (
|
|
121
130
|
<>
|
|
122
131
|
<div className="flex justify-between items-start">
|
|
@@ -142,30 +151,20 @@ export function DataTable<TData, TValue>({
|
|
|
142
151
|
/>
|
|
143
152
|
))}
|
|
144
153
|
</Suspense>
|
|
154
|
+
<AddFilterMenu columns={table.getAllColumns()} />
|
|
145
155
|
</div>
|
|
146
156
|
<div className="flex gap-1">
|
|
147
157
|
{columnFilters
|
|
148
158
|
.filter(f => !facetedFilters?.[f.id])
|
|
149
159
|
.map(f => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<div>{value}</div>
|
|
159
|
-
<button
|
|
160
|
-
className="cursor-pointer"
|
|
161
|
-
onClick={() =>
|
|
162
|
-
setColumnFilters(old => old.filter(x => x.id !== f.id))
|
|
163
|
-
}
|
|
164
|
-
>
|
|
165
|
-
<CircleX size="14" />
|
|
166
|
-
</button>
|
|
167
|
-
</Badge>
|
|
168
|
-
);
|
|
160
|
+
const column = table.getColumn(f.id);
|
|
161
|
+
const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
|
|
162
|
+
return <DataTableFilterBadge
|
|
163
|
+
key={f.id}
|
|
164
|
+
filter={f}
|
|
165
|
+
currencyCode={currency}
|
|
166
|
+
dataType={(column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'}
|
|
167
|
+
onRemove={() => setColumnFilters(old => old.filter(x => x.id !== f.id))} />;
|
|
169
168
|
})}
|
|
170
169
|
</div>
|
|
171
170
|
</div>
|
|
@@ -182,9 +181,9 @@ export function DataTable<TData, TValue>({
|
|
|
182
181
|
{header.isPlaceholder
|
|
183
182
|
? null
|
|
184
183
|
: flexRender(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
header.column.columnDef.header,
|
|
185
|
+
header.getContext(),
|
|
186
|
+
)}
|
|
188
187
|
</TableHead>
|
|
189
188
|
);
|
|
190
189
|
})}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Trans } from "@/lib/trans.js";
|
|
2
|
+
|
|
3
|
+
import { Select, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select.js";
|
|
4
|
+
|
|
5
|
+
import { SelectContent } from "@/components/ui/select.js";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { HumanReadableOperator } from "../human-readable-operator.js";
|
|
8
|
+
|
|
9
|
+
export interface DataTableBooleanFilterProps {
|
|
10
|
+
value: Record<string, any> | undefined;
|
|
11
|
+
onChange: (filter: Record<string, any>) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BOOLEAN_OPERATORS = ['eq', 'isNull'] as const;
|
|
15
|
+
|
|
16
|
+
export function DataTableBooleanFilter({ value: incomingValue, onChange }: DataTableBooleanFilterProps) {
|
|
17
|
+
const initialOperator = incomingValue ? Object.keys(incomingValue)[0] ?? 'eq' : 'eq';
|
|
18
|
+
const initialValue = incomingValue ? Object.values(incomingValue)[0] : true;
|
|
19
|
+
const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
|
|
20
|
+
const [value, setValue] = useState<boolean>(initialValue as boolean ?? true);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
onChange({ [operator]: value });
|
|
24
|
+
}, [operator, value]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
28
|
+
<Select value={operator} onValueChange={value => setOperator(value)}>
|
|
29
|
+
<SelectTrigger>
|
|
30
|
+
<SelectValue placeholder="Select operator" />
|
|
31
|
+
</SelectTrigger>
|
|
32
|
+
<SelectContent>
|
|
33
|
+
{BOOLEAN_OPERATORS.map(op => (
|
|
34
|
+
<SelectItem key={op} value={op}>
|
|
35
|
+
<HumanReadableOperator operator={op} />
|
|
36
|
+
</SelectItem>
|
|
37
|
+
))}
|
|
38
|
+
</SelectContent>
|
|
39
|
+
</Select>
|
|
40
|
+
{operator !== 'isNull' && (
|
|
41
|
+
<Select value={value.toString()} onValueChange={v => setValue(v === 'true')}>
|
|
42
|
+
<SelectTrigger>
|
|
43
|
+
<SelectValue placeholder="Select value" />
|
|
44
|
+
</SelectTrigger>
|
|
45
|
+
<SelectContent>
|
|
46
|
+
<SelectItem value="true">
|
|
47
|
+
<Trans>True</Trans>
|
|
48
|
+
</SelectItem>
|
|
49
|
+
<SelectItem value="false">
|
|
50
|
+
<Trans>False</Trans>
|
|
51
|
+
</SelectItem>
|
|
52
|
+
</SelectContent>
|
|
53
|
+
</Select>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Trans } from "@/lib/trans.js";
|
|
2
|
+
import { Select, SelectValue, SelectTrigger, SelectItem } from "@/components/ui/select.js";
|
|
3
|
+
import { SelectContent } from "@/components/ui/select.js";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { DateTimeInput } from "@/components/data-input/datetime-input.js";
|
|
6
|
+
import { HumanReadableOperator } from "../human-readable-operator.js";
|
|
7
|
+
|
|
8
|
+
export interface DataTableDateTimeFilterProps {
|
|
9
|
+
value: Record<string, any> | undefined;
|
|
10
|
+
onChange: (filter: Record<string, any>) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DATETIME_OPERATORS = ['eq', 'before', 'after', 'between', 'isNull'] as const;
|
|
14
|
+
|
|
15
|
+
export function DataTableDateTimeFilter({ value: incomingValue, onChange }: DataTableDateTimeFilterProps) {
|
|
16
|
+
const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'eq';
|
|
17
|
+
const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
|
|
18
|
+
const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
|
|
19
|
+
const [value, setValue] = useState<Date | undefined>(initialValue ? new Date(initialValue) : undefined);
|
|
20
|
+
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
|
21
|
+
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
|
22
|
+
const [error, setError] = useState<string>('');
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (operator === 'isNull') {
|
|
26
|
+
onChange({ [operator]: true });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (operator === 'between') {
|
|
31
|
+
if (!startDate && !endDate) {
|
|
32
|
+
onChange({});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!startDate || !endDate) {
|
|
36
|
+
setError('Please enter both start and end dates');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (startDate > endDate) {
|
|
40
|
+
setError('Start date must be before end date');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
setError('');
|
|
44
|
+
onChange({ [operator]: { start: startDate.toISOString(), end: endDate.toISOString() } });
|
|
45
|
+
} else {
|
|
46
|
+
if (!value) {
|
|
47
|
+
onChange({});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setError('');
|
|
51
|
+
onChange({ [operator]: value.toISOString() });
|
|
52
|
+
}
|
|
53
|
+
}, [operator, value, startDate, endDate]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col gap-2">
|
|
57
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
58
|
+
<Select value={operator} onValueChange={value => setOperator(value)}>
|
|
59
|
+
<SelectTrigger>
|
|
60
|
+
<SelectValue placeholder="Select operator" />
|
|
61
|
+
</SelectTrigger>
|
|
62
|
+
<SelectContent>
|
|
63
|
+
{DATETIME_OPERATORS.map(op => (
|
|
64
|
+
<SelectItem key={op} value={op}>
|
|
65
|
+
<HumanReadableOperator operator={op} />
|
|
66
|
+
</SelectItem>
|
|
67
|
+
))}
|
|
68
|
+
</SelectContent>
|
|
69
|
+
</Select>
|
|
70
|
+
{operator !== 'isNull' && (
|
|
71
|
+
operator === 'between' ? (
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<DateTimeInput
|
|
74
|
+
value={startDate}
|
|
75
|
+
onChange={setStartDate}
|
|
76
|
+
/>
|
|
77
|
+
<DateTimeInput
|
|
78
|
+
value={endDate}
|
|
79
|
+
onChange={setEndDate}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
) : (
|
|
83
|
+
<DateTimeInput
|
|
84
|
+
value={value}
|
|
85
|
+
onChange={setValue}
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Trans } from "@/lib/trans.js";
|
|
2
|
+
|
|
3
|
+
import { Select, SelectValue, SelectTrigger, SelectItem } from "@/components/ui/select.js";
|
|
4
|
+
|
|
5
|
+
import { SelectContent } from "@/components/ui/select.js";
|
|
6
|
+
import { Input } from "@/components/ui/input.js";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { HumanReadableOperator } from "../human-readable-operator.js";
|
|
9
|
+
|
|
10
|
+
export interface DataTableIdFilterProps {
|
|
11
|
+
value: Record<string, any> | undefined;
|
|
12
|
+
onChange: (filter: Record<string, any>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ID_OPERATORS = ['eq', 'notEq', 'in', 'notIn', 'isNull'] as const;
|
|
16
|
+
|
|
17
|
+
export function DataTableIdFilter({ value: incomingValue, onChange }: DataTableIdFilterProps) {
|
|
18
|
+
const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'eq';
|
|
19
|
+
const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
|
|
20
|
+
const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
|
|
21
|
+
const [value, setValue] = useState<string>(initialValue ?? '');
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (operator === 'isNull') {
|
|
25
|
+
onChange({ [operator]: true });
|
|
26
|
+
} else if (operator === 'in' || operator === 'notIn') {
|
|
27
|
+
// Split by comma and trim whitespace
|
|
28
|
+
const values = value.split(',').map(v => v.trim()).filter(v => v);
|
|
29
|
+
onChange({ [operator]: values });
|
|
30
|
+
} else {
|
|
31
|
+
onChange({ [operator]: value });
|
|
32
|
+
}
|
|
33
|
+
}, [operator, value]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
37
|
+
<Select value={operator} onValueChange={value => setOperator(value)}>
|
|
38
|
+
<SelectTrigger>
|
|
39
|
+
<SelectValue placeholder="Select operator" />
|
|
40
|
+
</SelectTrigger>
|
|
41
|
+
<SelectContent>
|
|
42
|
+
{ID_OPERATORS.map(op => (
|
|
43
|
+
<SelectItem key={op} value={op}>
|
|
44
|
+
<HumanReadableOperator operator={op} />
|
|
45
|
+
</SelectItem>
|
|
46
|
+
))}
|
|
47
|
+
</SelectContent>
|
|
48
|
+
</Select>
|
|
49
|
+
{operator !== 'isNull' && (
|
|
50
|
+
<Input
|
|
51
|
+
placeholder={operator === 'in' || operator === 'notIn' ? "Enter comma-separated IDs..." : "Enter ID..."}
|
|
52
|
+
value={value}
|
|
53
|
+
onChange={e => setValue(e.target.value)}
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Trans } from "@/lib/trans.js";
|
|
2
|
+
import { Select, SelectValue, SelectTrigger, SelectItem } from "@/components/ui/select.js";
|
|
3
|
+
import { SelectContent } from "@/components/ui/select.js";
|
|
4
|
+
import { Input } from "@/components/ui/input.js";
|
|
5
|
+
import { MoneyInput } from "@/components/data-input/money-input.js";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { useChannel } from "@/hooks/use-channel.js";
|
|
8
|
+
import { HumanReadableOperator } from "../human-readable-operator.js";
|
|
9
|
+
|
|
10
|
+
export interface DataTableNumberFilterProps {
|
|
11
|
+
mode: 'number' | 'money';
|
|
12
|
+
value: Record<string, any> | undefined;
|
|
13
|
+
onChange: (filter: Record<string, any>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const NUMBER_OPERATORS = ['eq', 'gt', 'gte', 'lt', 'lte', 'isNull', 'between'] as const;
|
|
17
|
+
|
|
18
|
+
export function DataTableNumberFilter({ mode, value: incomingValue, onChange }: DataTableNumberFilterProps) {
|
|
19
|
+
const { activeChannel } = useChannel();
|
|
20
|
+
const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'eq';
|
|
21
|
+
const initialValue = incomingValue ? Object.values(incomingValue)[0] : 0;
|
|
22
|
+
const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
|
|
23
|
+
const [value, setValue] = useState<string>(initialValue?.toString() ?? '');
|
|
24
|
+
const [minValue, setMinValue] = useState<string>('');
|
|
25
|
+
const [maxValue, setMaxValue] = useState<string>('');
|
|
26
|
+
const [error, setError] = useState<string>('');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (operator === 'isNull') {
|
|
30
|
+
onChange({ [operator]: true });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (operator === 'between') {
|
|
35
|
+
if (!minValue && !maxValue) {
|
|
36
|
+
onChange({});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!minValue || !maxValue) {
|
|
40
|
+
setError('Please enter both min and max values');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const minNum = parseFloat(minValue);
|
|
44
|
+
const maxNum = parseFloat(maxValue);
|
|
45
|
+
if (isNaN(minNum) || isNaN(maxNum)) {
|
|
46
|
+
setError('Please enter valid numbers');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (minNum > maxNum) {
|
|
50
|
+
setError('Min value must be less than max value');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setError('');
|
|
54
|
+
onChange({ [operator]: { start: minNum, end: maxNum } });
|
|
55
|
+
} else {
|
|
56
|
+
if (!value) {
|
|
57
|
+
onChange({});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const numValue = parseFloat(value);
|
|
61
|
+
if (isNaN(numValue)) {
|
|
62
|
+
setError('Please enter a valid number');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setError('');
|
|
66
|
+
onChange({ [operator]: numValue });
|
|
67
|
+
}
|
|
68
|
+
}, [operator, value, minValue, maxValue]);
|
|
69
|
+
|
|
70
|
+
const renderInput = (value: string, onChange: (value: string) => void, placeholder: string) => {
|
|
71
|
+
if (mode === 'money') {
|
|
72
|
+
return (
|
|
73
|
+
<MoneyInput
|
|
74
|
+
value={parseFloat(value) || 0}
|
|
75
|
+
onChange={(newValue) => onChange(newValue.toString())}
|
|
76
|
+
currency={activeChannel?.defaultCurrencyCode ?? 'USD'}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return (
|
|
81
|
+
<Input
|
|
82
|
+
type="number"
|
|
83
|
+
placeholder={placeholder}
|
|
84
|
+
value={value}
|
|
85
|
+
onChange={e => onChange(e.target.value)}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex flex-col gap-2">
|
|
92
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
93
|
+
<Select value={operator} onValueChange={value => setOperator(value)}>
|
|
94
|
+
<SelectTrigger>
|
|
95
|
+
<SelectValue placeholder="Select operator" />
|
|
96
|
+
</SelectTrigger>
|
|
97
|
+
<SelectContent>
|
|
98
|
+
{NUMBER_OPERATORS.map(op => (
|
|
99
|
+
<SelectItem key={op} value={op}>
|
|
100
|
+
<HumanReadableOperator operator={op} />
|
|
101
|
+
</SelectItem>
|
|
102
|
+
))}
|
|
103
|
+
</SelectContent>
|
|
104
|
+
</Select>
|
|
105
|
+
{operator !== 'isNull' && (
|
|
106
|
+
operator === 'between' ? (
|
|
107
|
+
<div className="flex gap-2">
|
|
108
|
+
{renderInput(minValue, setMinValue, "Min")}
|
|
109
|
+
{renderInput(maxValue, setMaxValue, "Max")}
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
renderInput(value, setValue, "Enter value...")
|
|
113
|
+
)
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|