@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.
Files changed (92) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +6 -0
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  14. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  15. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  16. package/package.json +8 -6
  17. package/src/app/app-providers.tsx +8 -8
  18. package/src/app/main.tsx +1 -1
  19. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  20. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  21. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  22. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  23. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  24. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  25. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  26. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  27. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  29. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  30. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -1
  31. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  32. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  33. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  34. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  35. package/src/app/routes/_authenticated.tsx +12 -1
  36. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  37. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  38. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  39. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  40. package/src/lib/components/data-table/data-table-types.ts +1 -0
  41. package/src/lib/components/data-table/data-table-view-options.tsx +72 -23
  42. package/src/lib/components/data-table/data-table.tsx +23 -24
  43. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  44. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  45. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  46. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  47. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  48. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  49. package/src/lib/components/layout/nav-user.tsx +4 -4
  50. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  51. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  52. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  53. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  54. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  55. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  56. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  57. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  58. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  59. package/src/lib/components/shared/customer-selector.tsx +13 -14
  60. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  61. package/src/lib/components/shared/entity-assets.tsx +3 -3
  62. package/src/lib/components/shared/navigation-confirmation.tsx +39 -0
  63. package/src/lib/components/shared/paginated-list-data-table.tsx +9 -1
  64. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  65. package/src/lib/components/shared/vendure-image.tsx +1 -1
  66. package/src/lib/components/ui/calendar.tsx +508 -63
  67. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  68. package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
  69. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  70. package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
  71. package/src/lib/framework/page/list-page.tsx +23 -4
  72. package/src/lib/framework/page/use-detail-page.ts +1 -0
  73. package/src/lib/graphql/fragments.tsx +8 -0
  74. package/src/lib/index.ts +5 -5
  75. package/src/lib/providers/auth.tsx +12 -9
  76. package/src/lib/providers/channel-provider.tsx +1 -0
  77. package/src/lib/providers/server-config.tsx +7 -1
  78. package/src/lib/providers/user-settings.tsx +24 -0
  79. package/vite/utils/ast-utils.spec.ts +128 -0
  80. package/vite/utils/ast-utils.ts +119 -0
  81. package/vite/utils/config-loader.ts +410 -0
  82. package/vite/{schema-generator.ts → utils/schema-generator.ts} +7 -1
  83. package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
  84. package/vite/vite-plugin-admin-api-schema.ts +2 -2
  85. package/vite/vite-plugin-config-loader.ts +25 -13
  86. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  87. package/vite/vite-plugin-gql-tada.ts +2 -2
  88. package/vite/vite-plugin-ui-config.ts +3 -2
  89. package/dist/plugin/config-loader.js +0 -141
  90. package/src/lib/components/shared/asset-preview.tsx +0 -345
  91. package/vite/config-loader.ts +0 -181
  92. /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 React, { useState } from 'react';
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 [initialOperator, initialValue] = columnFilter ? Object.entries(columnFilter as any)[0] : [];
25
- const [operator, setOperator] = useState<string>(initialOperator ?? 'contains');
26
- const [value, setValue] = useState((initialValue as string) ?? '');
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
- <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
- {STRING_OPERATORS.map(op => (
43
- <SelectItem key={op} value={op}>
44
- <Trans context="filter-operator">{op}</Trans>
45
- </SelectItem>
46
- ))}
47
- </SelectContent>
48
- </Select>
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
- onClick={e => column.setFilterValue({ [operator]: value })}
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 { Badge } from '@/components/ui/badge.js';
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 { CircleX, Cross, Filter, Settings2 } from 'lucide-react';
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
- View
78
+ <Trans>Columns</Trans>
29
79
  </Button>
30
80
  </DropdownMenuTrigger>
31
81
  <DropdownMenuContent align="end" className="w-[150px]">
32
- <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
33
- <DropdownMenuSeparator />
34
- {table
35
- .getAllColumns()
36
- .filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide())
37
- .map(column => {
38
- return (
39
- <DropdownMenuCheckboxItem
40
- key={column.id}
41
- className="capitalize"
42
- checked={column.getIsVisible()}
43
- onCheckedChange={value => column.toggleVisibility(!!value)}
44
- >
45
- {column.id}
46
- </DropdownMenuCheckboxItem>
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 [operator, value] = Object.entries(
151
- f.value as Record<string, string>,
152
- )[0];
153
- return (
154
- <Badge key={f.id} className="flex gap-1 items-center" variant="secondary">
155
- <Filter size="12" className="opacity-50" />
156
- <div>{f.id}</div>
157
- <div>{operator}</div>
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
- header.column.columnDef.header,
186
- header.getContext(),
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
+ }