@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
@@ -0,0 +1,241 @@
1
+ import { FullWidthPageBlock, Page, PageLayout, PageTitle } from '@/framework/layout-engine/page-layout.js';
2
+ import { api } from '@/graphql/api.js';
3
+ import { graphql } from '@/graphql/graphql.js';
4
+ import { DataTable } from '@/components/data-table/data-table.js';
5
+ import { Trans, useLingui } from '@/lib/trans.js';
6
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
7
+ import { createFileRoute } from '@tanstack/react-router';
8
+ import { createColumnHelper } from '@tanstack/react-table';
9
+ import { ResultOf } from '@/graphql/graphql.js';
10
+ import { PayloadDialog } from './components/payload-dialog.js';
11
+ import { Button } from '@/components/ui/button.js';
12
+ import { Badge } from '@/components/ui/badge.js';
13
+ import { useLocalFormat } from '@/hooks/use-local-format.js';
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ } from '@/components/ui/dropdown-menu.js';
20
+ import { CirclePlay, EllipsisIcon } from 'lucide-react';
21
+ import { toast } from 'sonner';
22
+
23
+ export const Route = createFileRoute('/_authenticated/_system/scheduled-tasks')({
24
+ component: ScheduledTasksPage,
25
+ loader: () => ({ breadcrumb: () => <Trans>Scheduled Tasks</Trans> }),
26
+ });
27
+
28
+ const getScheduledTasksDocument = graphql(`
29
+ query ScheduledTasks {
30
+ scheduledTasks {
31
+ id
32
+ description
33
+ schedule
34
+ scheduleDescription
35
+ lastExecutedAt
36
+ nextExecutionAt
37
+ isRunning
38
+ lastResult
39
+ enabled
40
+ }
41
+ }
42
+ `);
43
+
44
+ const updateScheduledTaskDocument = graphql(`
45
+ mutation UpdateScheduledTask($input: UpdateScheduledTaskInput!) {
46
+ updateScheduledTask(input: $input) {
47
+ id
48
+ enabled
49
+ }
50
+ }
51
+ `);
52
+
53
+ const runScheduledTaskDocument = graphql(`
54
+ mutation RunScheduledTask($id: String!) {
55
+ runScheduledTask(id: $id) {
56
+ success
57
+ }
58
+ }
59
+ `);
60
+
61
+ type ScheduledTask = ResultOf<typeof getScheduledTasksDocument>['scheduledTasks'][number];
62
+
63
+ function ScheduledTasksPage() {
64
+ const { i18n } = useLingui();
65
+ const { data } = useQuery({
66
+ queryKey: ['scheduledTasks'],
67
+ queryFn: () => api.query(getScheduledTasksDocument),
68
+ });
69
+ const queryClient = useQueryClient();
70
+ const { mutate: updateScheduledTask } = useMutation({
71
+ mutationFn: api.mutate(updateScheduledTaskDocument),
72
+ onSuccess: (result) => {
73
+ refreshScheduledTasks();
74
+ },
75
+ });
76
+ const refreshScheduledTasks = () => {
77
+ queryClient.invalidateQueries({ queryKey: ['scheduledTasks'] });
78
+ }
79
+ const { mutate: runScheduledTask } = useMutation({
80
+ mutationFn: api.mutate(runScheduledTaskDocument),
81
+ onSuccess: (result) => {
82
+ if ((result as ResultOf<typeof runScheduledTaskDocument>).runScheduledTask.success) {
83
+ toast.success(i18n.t(`Scheduled task will be executed`));
84
+ queryClient.invalidateQueries({ queryKey: ['scheduledTasks'] });
85
+ } else {
86
+ toast.error(i18n.t(`Scheduled task could not be executed`));
87
+ }
88
+ },
89
+ });
90
+ const { formatDate, formatRelativeDate } = useLocalFormat();
91
+ const intlDateOptions = {
92
+ year: 'numeric',
93
+ month: 'short',
94
+ day: 'numeric',
95
+ hour: 'numeric',
96
+ minute: 'numeric',
97
+ second: 'numeric',
98
+ } as const;
99
+
100
+ const columnHelper = createColumnHelper<ScheduledTask>();
101
+ const columns = [
102
+ columnHelper.accessor('id', {
103
+ header: 'ID',
104
+ }),
105
+ columnHelper.accessor('description', {
106
+ header: 'Description',
107
+ }),
108
+ columnHelper.accessor('enabled', {
109
+ header: 'Enabled',
110
+ cell: ({ row }) => {
111
+ return row.original.enabled ? (
112
+ <Badge variant="success">
113
+ <Trans>Enabled</Trans>
114
+ </Badge>
115
+ ) : (
116
+ <Badge variant="secondary">
117
+ <Trans>Disabled</Trans>
118
+ </Badge>
119
+ );
120
+ },
121
+ }),
122
+ columnHelper.accessor('schedule', {
123
+ header: 'Schedule Pattern',
124
+ }),
125
+ columnHelper.accessor('scheduleDescription', {
126
+ header: 'Schedule',
127
+ }),
128
+ columnHelper.accessor('lastExecutedAt', {
129
+ header: 'Last Executed',
130
+ cell: ({ row }) => {
131
+ return row.original.lastExecutedAt ? (
132
+ <div title={row.original.lastExecutedAt}>
133
+ {formatRelativeDate(row.original.lastExecutedAt)}
134
+ </div>
135
+ ) : (
136
+ <Trans>Never</Trans>
137
+ );
138
+ },
139
+ }),
140
+ columnHelper.accessor('nextExecutionAt', {
141
+ header: 'Next Execution',
142
+ cell: ({ row }) => {
143
+ return row.original.nextExecutionAt ? (
144
+ formatDate(row.original.nextExecutionAt, intlDateOptions)
145
+ ) : (
146
+ <Trans>Never</Trans>
147
+ );
148
+ },
149
+ }),
150
+ columnHelper.accessor('isRunning', {
151
+ header: 'Running',
152
+ cell: ({ row }) => {
153
+ return row.original.isRunning ? (
154
+ <Badge variant="success">
155
+ <Trans>Running</Trans>
156
+ </Badge>
157
+ ) : (
158
+ <Badge variant="secondary">
159
+ <Trans>Not Running</Trans>
160
+ </Badge>
161
+ );
162
+ },
163
+ }),
164
+ columnHelper.accessor('lastResult', {
165
+ header: 'Last Result',
166
+ cell: ({ row }) => {
167
+ return row.original.lastResult ? (
168
+ <PayloadDialog
169
+ payload={row.original.lastResult}
170
+ title={<Trans>View job result</Trans>}
171
+ description={<Trans>The result of the job</Trans>}
172
+ trigger={
173
+ <Button size="sm" variant="secondary">
174
+ View result
175
+ </Button>
176
+ }
177
+ />
178
+ ) : (
179
+ <div className="text-muted-foreground">
180
+ <Trans>No result yet</Trans>
181
+ </div>
182
+ );
183
+ },
184
+ }),
185
+ columnHelper.display({
186
+ id: 'actions',
187
+ header: 'Actions',
188
+ cell: ({ row }) => {
189
+ return (
190
+ <DropdownMenu>
191
+ <DropdownMenuTrigger asChild>
192
+ <Button variant="ghost" size="icon">
193
+ <EllipsisIcon />
194
+ </Button>
195
+ </DropdownMenuTrigger>
196
+ <DropdownMenuContent>
197
+ {row.original.enabled && <DropdownMenuItem
198
+ onClick={() =>
199
+ runScheduledTask({
200
+ id: row.original.id,
201
+ })
202
+ }
203
+ >
204
+ <CirclePlay className="w-4 h-4" />
205
+ <Trans>Run</Trans>
206
+ </DropdownMenuItem>}
207
+ <DropdownMenuItem
208
+ onClick={() =>
209
+ updateScheduledTask({
210
+ input: { id: row.original.id, enabled: !row.original.enabled },
211
+ })
212
+ }
213
+ >
214
+ {row.original.enabled ? <Trans>Disable</Trans> : <Trans>Enable</Trans>}
215
+ </DropdownMenuItem>
216
+ </DropdownMenuContent>
217
+ </DropdownMenu>
218
+ );
219
+ },
220
+ }),
221
+ ];
222
+
223
+ return (
224
+ <Page pageId="scheduled-tasks-list">
225
+ <PageTitle>Scheduled Tasks</PageTitle>
226
+ <PageLayout>
227
+ <FullWidthPageBlock blockId="list-table">
228
+ <DataTable
229
+ onRefresh={refreshScheduledTasks}
230
+ columns={columns}
231
+ data={data?.scheduledTasks ?? []}
232
+ totalItems={data?.scheduledTasks?.length ?? 0}
233
+ defaultColumnVisibility={{
234
+ schedule: false,
235
+ }}
236
+ />
237
+ </FullWidthPageBlock>
238
+ </PageLayout>
239
+ </Page>
240
+ );
241
+ }
@@ -1,7 +1,8 @@
1
1
  import { AppLayout } from '@/components/layout/app-layout.js';
2
- import { createFileRoute, redirect } from '@tanstack/react-router';
2
+ import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
3
3
  import { AUTHENTICATED_ROUTE_PREFIX } from '@/constants.js';
4
4
  import * as React from 'react';
5
+ import { useAuth } from '@/hooks/use-auth.js';
5
6
 
6
7
  export const Route = createFileRoute(AUTHENTICATED_ROUTE_PREFIX)({
7
8
  beforeLoad: ({ context, location }) => {
@@ -21,5 +22,15 @@ export const Route = createFileRoute(AUTHENTICATED_ROUTE_PREFIX)({
21
22
  });
22
23
 
23
24
  function AuthLayout() {
25
+ const navigate = useNavigate();
26
+ const { isAuthenticated } = useAuth();
27
+
28
+ if (!isAuthenticated) {
29
+ navigate({
30
+ to: '/login'
31
+ });
32
+ return <></>;
33
+ }
34
+
24
35
  return <AppLayout />;
25
36
  }
@@ -76,6 +76,21 @@
76
76
  grid-column: span 2 / span 2;
77
77
  }
78
78
 
79
+ @layer utilities {
80
+ @keyframes rotate {
81
+ 0% {
82
+ transform: rotate(0deg);
83
+ }
84
+ 100% {
85
+ transform: rotate(360deg);
86
+ }
87
+ }
88
+
89
+ .animate-rotate {
90
+ animation: rotate 0.5s linear;
91
+ }
92
+ }
93
+
79
94
  /* Overrides for the react-grid-layout library */
80
95
  .react-grid-item {
81
96
  transition: none !important;
@@ -0,0 +1,61 @@
1
+ import { Button } from '@/components/ui/button.js';
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuItem,
6
+ DropdownMenuTrigger,
7
+ } from '@/components/ui/dropdown-menu.js';
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from '@/components/ui/dialog.js';
16
+ import { DataTableFilterDialog } from '@/components/data-table/data-table-filter-dialog.js';
17
+ import { Column, ColumnDef } from '@tanstack/react-table';
18
+ import { PlusCircle } from 'lucide-react';
19
+ import { Trans } from '@/lib/trans.js';
20
+ import React, { useState } from 'react';
21
+ import { camelCaseToTitleCase } from '@/lib/utils.js';
22
+
23
+ export interface AddFilterMenuProps {
24
+ columns: Column<any, unknown>[];
25
+ }
26
+
27
+ export function AddFilterMenu({ columns }: AddFilterMenuProps) {
28
+ const [selectedColumn, setSelectedColumn] = useState<ColumnDef<any> | null>(null);
29
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
30
+
31
+ const filterableColumns = columns.filter(column => column.getCanFilter());
32
+
33
+ return (
34
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
35
+ <DropdownMenu>
36
+ <DropdownMenuTrigger asChild>
37
+ <Button variant="outline" size="sm" className="h-8 border-dashed">
38
+ <PlusCircle className="mr-2 h-4 w-4" />
39
+ <Trans>Add filter</Trans>
40
+ </Button>
41
+ </DropdownMenuTrigger>
42
+ <DropdownMenuContent align="end" className="w-[200px]">
43
+ {filterableColumns.map(column => (
44
+ <DropdownMenuItem
45
+ key={column.id}
46
+ onSelect={() => {
47
+ setSelectedColumn(column);
48
+ setIsDialogOpen(true);
49
+ }}
50
+ >
51
+ {camelCaseToTitleCase(column.id)}
52
+ </DropdownMenuItem>
53
+ ))}
54
+ </DropdownMenuContent>
55
+ </DropdownMenu>
56
+ {selectedColumn && (
57
+ <DataTableFilterDialog column={selectedColumn as any} />
58
+ )}
59
+ </Dialog>
60
+ );
61
+ }
@@ -29,7 +29,6 @@ export interface DataTableColumnHeaderProps {
29
29
  export function DataTableColumnHeader({ headerContext, customConfig }: DataTableColumnHeaderProps) {
30
30
  const { column } = headerContext;
31
31
  const isSortable = column.getCanSort();
32
- const isFilterable = column.getCanFilter();
33
32
 
34
33
  const customHeader = customConfig.header;
35
34
  let display = camelCaseToTitleCase(column.id);
@@ -40,7 +39,6 @@ export function DataTableColumnHeader({ headerContext, customConfig }: DataTable
40
39
  }
41
40
 
42
41
  const columSort = column.getIsSorted();
43
- const columnFilter = column.getFilterValue();
44
42
  const nextSort = columSort === 'asc' ? true : columSort === 'desc' ? undefined : false;
45
43
 
46
44
  return (
@@ -57,17 +55,6 @@ export function DataTableColumnHeader({ headerContext, customConfig }: DataTable
57
55
  </Button>
58
56
  )}
59
57
  <div>{display}</div>
60
-
61
- {isFilterable && (
62
- <Dialog>
63
- <DialogTrigger asChild>
64
- <Button size="icon-sm" variant="ghost">
65
- <Filter className={columnFilter ? '' : 'opacity-50'} />
66
- </Button>
67
- </DialogTrigger>
68
- <DataTableFilterDialog column={column} />
69
- </Dialog>
70
- )}
71
58
  </div>
72
59
  );
73
60
  }
@@ -0,0 +1,75 @@
1
+ import { Filter } from 'lucide-react';
2
+
3
+ import { CircleX } from 'lucide-react';
4
+ import { Badge } from '../ui/badge.js';
5
+ import { useLocalFormat } from '@/hooks/use-local-format.js';
6
+ import { ColumnDataType } from './data-table-types.js';
7
+ import { HumanReadableOperator } from './human-readable-operator.js';
8
+
9
+ export function DataTableFilterBadge({
10
+ filter,
11
+ onRemove,
12
+ dataType,
13
+ currencyCode,
14
+ }: {
15
+ filter: any;
16
+ onRemove: (filter: any) => void;
17
+ dataType: ColumnDataType;
18
+ currencyCode: string;
19
+ }) {
20
+ const [operator, value] = Object.entries(filter.value as Record<string, unknown>)[0];
21
+ return (
22
+ <Badge key={filter.id} className="flex gap-1 items-center" variant="secondary">
23
+ <Filter size="12" className="opacity-50" />
24
+ <div>{filter.id}</div>
25
+ <div className="text-muted-foreground"><HumanReadableOperator operator={operator} mode="short" /></div>
26
+ <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
27
+ <button className="cursor-pointer" onClick={() => onRemove(filter)}>
28
+ <CircleX size="14" />
29
+ </button>
30
+ </Badge>
31
+ );
32
+ }
33
+
34
+ function FilterValue({ value, dataType, currencyCode }: { value: unknown, dataType: ColumnDataType, currencyCode: string }) {
35
+ const { formatDate, formatCurrency } = useLocalFormat();
36
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
37
+ return Object.entries(value as Record<string, unknown>).map(([key, value]) => (
38
+ <div key={key} className="flex gap-1 items-center">
39
+ <span className="text-muted-foreground">{key}: </span>
40
+ <FilterValue value={value} dataType={dataType} currencyCode={currencyCode} />
41
+ </div>
42
+ ));
43
+ }
44
+ if (Array.isArray(value)) {
45
+ return (
46
+ <div className="flex gap-1 items-center">
47
+ [
48
+ {value.map(v => (
49
+ <FilterValue value={v} dataType={dataType} currencyCode={currencyCode} key={v} />
50
+ ))}
51
+ ]
52
+ </div>
53
+ );
54
+ }
55
+ if (typeof value === 'string' && isDateIsoString(value)) {
56
+ return <div>{formatDate(value, { dateStyle: 'short', timeStyle: 'short' })}</div>;
57
+ }
58
+ if (typeof value === 'boolean') {
59
+ return <div>{value ? 'true' : 'false'}</div>;
60
+ }
61
+ if (typeof value === 'number' && dataType === 'Money') {
62
+ return <div>{formatCurrency(value, currencyCode)}</div>;
63
+ }
64
+ if (typeof value === 'number') {
65
+ return <div>{value}</div>;
66
+ }
67
+ if (typeof value === 'string') {
68
+ return <div>{value}</div>;
69
+ }
70
+ return <div>{value as string}</div>;
71
+ }
72
+
73
+ function isDateIsoString(value: string) {
74
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value);
75
+ }
@@ -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
- <Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex">
76
+ <Button variant="ghost" 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>