@vendure/dashboard 3.2.3 → 3.3.0

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 (123) 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} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  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-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
@@ -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,12 @@ 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';
26
+ import { RefreshButton } from '@/components/data-table/refresh-button.js';
25
27
 
26
28
  export interface FacetedFilter {
27
29
  title: string;
@@ -30,8 +32,8 @@ export interface FacetedFilter {
30
32
  options?: DataTableFacetedFilterOption[];
31
33
  }
32
34
 
33
- interface DataTableProps<TData, TValue> {
34
- columns: ColumnDef<TData, TValue>[];
35
+ interface DataTableProps<TData> {
36
+ columns: ColumnDef<TData, any>[];
35
37
  data: TData[];
36
38
  totalItems: number;
37
39
  page?: number;
@@ -41,6 +43,7 @@ interface DataTableProps<TData, TValue> {
41
43
  onPageChange?: (table: TableType<TData>, page: number, itemsPerPage: number) => void;
42
44
  onSortChange?: (table: TableType<TData>, sorting: SortingState) => void;
43
45
  onFilterChange?: (table: TableType<TData>, columnFilters: ColumnFilter[]) => void;
46
+ onColumnVisibilityChange?: (table: TableType<TData>, columnVisibility: VisibilityState) => void;
44
47
  onSearchTermChange?: (searchTerm: string) => void;
45
48
  defaultColumnVisibility?: VisibilityState;
46
49
  facetedFilters?: { [key: string]: FacetedFilter | undefined };
@@ -50,27 +53,31 @@ interface DataTableProps<TData, TValue> {
50
53
  * when needed.
51
54
  */
52
55
  setTableOptions?: (table: TableOptions<TData>) => TableOptions<TData>;
56
+ onRefresh?: () => void;
53
57
  }
54
58
 
55
- export function DataTable<TData, TValue>({
56
- columns,
57
- data,
58
- totalItems,
59
- page,
60
- itemsPerPage,
61
- sorting: sortingInitialState,
62
- columnFilters: filtersInitialState,
63
- onPageChange,
64
- onSortChange,
65
- onFilterChange,
66
- onSearchTermChange,
67
- defaultColumnVisibility,
68
- facetedFilters,
69
- disableViewOptions,
70
- setTableOptions,
71
- }: DataTableProps<TData, TValue>) {
59
+ export function DataTable<TData>({
60
+ columns,
61
+ data,
62
+ totalItems,
63
+ page,
64
+ itemsPerPage,
65
+ sorting: sortingInitialState,
66
+ columnFilters: filtersInitialState,
67
+ onPageChange,
68
+ onSortChange,
69
+ onFilterChange,
70
+ onSearchTermChange,
71
+ onColumnVisibilityChange,
72
+ defaultColumnVisibility,
73
+ facetedFilters,
74
+ disableViewOptions,
75
+ setTableOptions,
76
+ onRefresh,
77
+ }: DataTableProps<TData>) {
72
78
  const [sorting, setSorting] = React.useState<SortingState>(sortingInitialState || []);
73
79
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(filtersInitialState || []);
80
+ const { activeChannel } = useChannel();
74
81
  const [pagination, setPagination] = React.useState<PaginationState>({
75
82
  pageIndex: (page ?? 1) - 1,
76
83
  pageSize: itemsPerPage ?? 10,
@@ -117,6 +124,11 @@ export function DataTable<TData, TValue>({
117
124
  useEffect(() => {
118
125
  onFilterChange?.(table, columnFilters);
119
126
  }, [columnFilters]);
127
+
128
+ useEffect(() => {
129
+ onColumnVisibilityChange?.(table, columnVisibility);
130
+ }, [columnVisibility]);
131
+
120
132
  return (
121
133
  <>
122
134
  <div className="flex justify-between items-start">
@@ -142,34 +154,27 @@ export function DataTable<TData, TValue>({
142
154
  />
143
155
  ))}
144
156
  </Suspense>
157
+ <AddFilterMenu columns={table.getAllColumns()} />
145
158
  </div>
146
159
  <div className="flex gap-1">
147
160
  {columnFilters
148
161
  .filter(f => !facetedFilters?.[f.id])
149
162
  .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
- );
163
+ const column = table.getColumn(f.id);
164
+ const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
165
+ return <DataTableFilterBadge
166
+ key={f.id}
167
+ filter={f}
168
+ currencyCode={currency}
169
+ dataType={(column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'}
170
+ onRemove={() => setColumnFilters(old => old.filter(x => x.id !== f.id))} />;
169
171
  })}
170
172
  </div>
171
173
  </div>
172
- {!disableViewOptions && <DataTableViewOptions table={table} />}
174
+ <div className="flex items-center justify-start gap-2">
175
+ {!disableViewOptions && <DataTableViewOptions table={table} />}
176
+ {onRefresh && <RefreshButton onRefresh={onRefresh} />}
177
+ </div>
173
178
  </div>
174
179
  <div className="rounded-md border my-2">
175
180
  <Table>
@@ -182,9 +187,9 @@ export function DataTable<TData, TValue>({
182
187
  {header.isPlaceholder
183
188
  ? null
184
189
  : flexRender(
185
- header.column.columnDef.header,
186
- header.getContext(),
187
- )}
190
+ header.column.columnDef.header,
191
+ header.getContext(),
192
+ )}
188
193
  </TableHead>
189
194
  );
190
195
  })}
@@ -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
+ }
@@ -0,0 +1,62 @@
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 DataTableStringFilterProps {
11
+ value: Record<string, any> | undefined;
12
+ onChange: (filter: Record<string, any>) => void;
13
+ }
14
+
15
+ export const STRING_OPERATORS = ['eq', 'notEq', 'contains', 'notContains', 'in', 'notIn', 'regex', 'isNull'] as const;
16
+
17
+ export function DataTableStringFilter({ value: incomingValue, onChange }: DataTableStringFilterProps) {
18
+ const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'contains';
19
+ const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
20
+ const [operator, setOperator] = useState<string>(initialOperator ?? 'contains');
21
+ const [value, setValue] = useState((initialValue as string) ?? '');
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
+ if (typeof value === 'string') {
29
+ const values = value.split(',').map(v => v.trim()).filter(v => v);
30
+ onChange({ [operator]: values });
31
+ } else {
32
+ onChange({ [operator]: [] });
33
+ }
34
+ } else {
35
+ onChange({ [operator]: value });
36
+ }
37
+ }, [operator, value]);
38
+
39
+ return (
40
+ <div className="flex flex-col md:flex-row gap-2">
41
+ <Select value={operator} onValueChange={value => setOperator(value)}>
42
+ <SelectTrigger>
43
+ <SelectValue placeholder="Select operator" />
44
+ </SelectTrigger>
45
+ <SelectContent>
46
+ {STRING_OPERATORS.map(op => (
47
+ <SelectItem key={op} value={op}>
48
+ <HumanReadableOperator operator={op} />
49
+ </SelectItem>
50
+ ))}
51
+ </SelectContent>
52
+ </Select>
53
+ {operator !== 'isNull' && (
54
+ <Input
55
+ placeholder={operator === 'in' || operator === 'notIn' ? "Enter comma-separated values..." : "Enter filter value..."}
56
+ value={value}
57
+ onChange={e => setValue(e.target.value)}
58
+ />
59
+ )}
60
+ </div>
61
+ )
62
+ }
@@ -0,0 +1,65 @@
1
+ import { DATETIME_OPERATORS } from './filters/data-table-datetime-filter.js';
2
+ import { BOOLEAN_OPERATORS } from './filters/data-table-boolean-filter.js';
3
+ import { ID_OPERATORS } from './filters/data-table-id-filter.js';
4
+ import { NUMBER_OPERATORS } from './filters/data-table-number-filter.js';
5
+ import { STRING_OPERATORS } from './filters/data-table-string-filter.js';
6
+ import { Trans } from '@/lib/trans.js';
7
+
8
+ type Operator =
9
+ | (typeof DATETIME_OPERATORS)[number]
10
+ | (typeof BOOLEAN_OPERATORS)[number]
11
+ | (typeof ID_OPERATORS)[number]
12
+ | (typeof NUMBER_OPERATORS)[number]
13
+ | (typeof STRING_OPERATORS)[number];
14
+
15
+ export function HumanReadableOperator({
16
+ operator,
17
+ mode = 'long',
18
+ }: {
19
+ operator: Operator;
20
+ mode?: 'short' | 'long';
21
+ }) {
22
+ switch (operator) {
23
+ case 'eq':
24
+ return mode === 'short' ? <Trans>=</Trans> : <Trans>is equal to</Trans>;
25
+ case 'notEq':
26
+ return mode === 'short' ? <Trans>!=</Trans> : <Trans>is not equal to</Trans>;
27
+ case 'before':
28
+ return mode === 'short' ? <Trans>before</Trans> : <Trans>is before</Trans>;
29
+ case 'after':
30
+ return mode === 'short' ? <Trans>after</Trans> : <Trans>is after</Trans>;
31
+ case 'between':
32
+ return mode === 'short' ? <Trans>between</Trans> : <Trans>is between</Trans>;
33
+ case 'isNull':
34
+ return mode === 'short' ? <Trans>is null</Trans> : <Trans>is null</Trans>;
35
+ case 'in':
36
+ return mode === 'short' ? <Trans>in</Trans> : <Trans>is in</Trans>;
37
+ case 'notIn':
38
+ return mode === 'short' ? <Trans>not in</Trans> : <Trans>is not in</Trans>;
39
+ case 'gt':
40
+ return mode === 'short' ? <Trans>greater than</Trans> : <Trans>is greater than</Trans>;
41
+ case 'gte':
42
+ return mode === 'short' ? (
43
+ <Trans>greater than or equal</Trans>
44
+ ) : (
45
+ <Trans>is greater than or equal to</Trans>
46
+ );
47
+ case 'lt':
48
+ return mode === 'short' ? <Trans>less than</Trans> : <Trans>is less than</Trans>;
49
+ case 'lte':
50
+ return mode === 'short' ? (
51
+ <Trans>less than or equal</Trans>
52
+ ) : (
53
+ <Trans>is less than or equal to</Trans>
54
+ );
55
+ case 'contains':
56
+ return mode === 'short' ? <Trans>contains</Trans> : <Trans>contains</Trans>;
57
+ case 'notContains':
58
+ return mode === 'short' ? <Trans>does not contain</Trans> : <Trans>does not contain</Trans>;
59
+ case 'regex':
60
+ return mode === 'short' ? <Trans>matches regex</Trans> : <Trans>matches regex</Trans>;
61
+ default:
62
+ operator satisfies never;
63
+ return <Trans>{operator}</Trans>;
64
+ }
65
+ }
@@ -0,0 +1,25 @@
1
+ import React, { useState } from 'react';
2
+ import { Button } from '@/components/ui/button.js';
3
+ import { RefreshCw } from 'lucide-react';
4
+
5
+ export function RefreshButton({ onRefresh }: { onRefresh: () => void }) {
6
+ const [isRotating, setIsRotating] = useState(false);
7
+
8
+ const handleClick = () => {
9
+ if (!isRotating) {
10
+ setIsRotating(true);
11
+ onRefresh();
12
+ }
13
+ };
14
+
15
+ return (
16
+ <Button
17
+ variant="ghost"
18
+ size="sm"
19
+ onClick={handleClick}
20
+ >
21
+ <RefreshCw onAnimationEnd={() => setIsRotating(false)}
22
+ className={isRotating ? 'animate-rotate' : ''} />
23
+ </Button>
24
+ );
25
+ }