@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.
- package/dist/plugin/utils/ast-utils.d.ts +10 -0
- package/dist/plugin/utils/ast-utils.js +96 -0
- package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
- package/dist/plugin/utils/ast-utils.spec.js +120 -0
- package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
- package/dist/plugin/utils/config-loader.js +325 -0
- package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
- package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +7 -1
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
- package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
- package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
- package/dist/plugin/vite-plugin-config-loader.js +18 -9
- package/dist/plugin/vite-plugin-config.js +4 -6
- package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
- package/dist/plugin/vite-plugin-gql-tada.js +2 -2
- package/dist/plugin/vite-plugin-ui-config.js +3 -2
- package/package.json +16 -11
- package/src/app/app-providers.tsx +9 -9
- package/src/app/main.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
- package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
- package/src/app/routes/_authenticated/_products/products.tsx +1 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
- package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
- package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
- package/src/app/routes/_authenticated.tsx +12 -1
- package/src/app/styles.css +15 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
- package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
- package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
- package/src/lib/components/data-table/data-table-types.ts +1 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
- package/src/lib/components/data-table/data-table.tsx +49 -44
- package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
- package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
- package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
- package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
- package/src/lib/components/data-table/refresh-button.tsx +25 -0
- package/src/lib/components/layout/nav-user.tsx +20 -15
- package/src/lib/components/layout/prerelease-popup.tsx +1 -5
- package/src/lib/components/shared/alerts.tsx +19 -1
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
- package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
- package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
- package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
- package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
- package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
- package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
- package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
- package/src/lib/components/shared/custom-fields-form.tsx +4 -3
- package/src/lib/components/shared/customer-selector.tsx +13 -14
- package/src/lib/components/shared/detail-page-button.tsx +2 -2
- package/src/lib/components/shared/entity-assets.tsx +3 -3
- package/src/lib/components/shared/error-page.tsx +2 -2
- package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
- package/src/lib/components/shared/product-variant-selector.tsx +111 -0
- package/src/lib/components/shared/vendure-image.tsx +1 -1
- package/src/lib/components/ui/calendar.tsx +508 -63
- package/src/lib/framework/alert/alert-extensions.tsx +31 -0
- package/src/lib/framework/alert/alert-item.tsx +47 -0
- package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
- package/src/lib/framework/alert/types.ts +13 -0
- package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
- package/src/lib/framework/defaults.ts +34 -0
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
- package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
- package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
- package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
- package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
- package/src/lib/framework/page/detail-page.tsx +62 -9
- package/src/lib/framework/page/list-page.tsx +42 -4
- package/src/lib/framework/page/page-api.ts +1 -1
- package/src/lib/framework/page/use-detail-page.ts +82 -0
- package/src/lib/framework/registry/registry-types.ts +6 -2
- package/src/lib/graphql/fragments.tsx +8 -0
- package/src/lib/graphql/graphql-env.d.ts +25 -9
- package/src/lib/hooks/use-auth.tsx +13 -1
- package/src/lib/hooks/use-channel.ts +13 -0
- package/src/lib/hooks/use-local-format.ts +28 -1
- package/src/lib/hooks/use-page.tsx +2 -3
- package/src/lib/hooks/use-permissions.ts +13 -0
- package/src/lib/index.ts +7 -8
- package/src/lib/providers/auth.tsx +22 -9
- package/src/lib/providers/channel-provider.tsx +9 -1
- package/src/lib/providers/server-config.tsx +7 -1
- package/src/lib/providers/user-settings.tsx +24 -0
- package/vite/utils/ast-utils.spec.ts +128 -0
- package/vite/utils/ast-utils.ts +119 -0
- package/vite/utils/config-loader.ts +410 -0
- package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
- package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
- package/vite/vite-plugin-admin-api-schema.ts +2 -12
- package/vite/vite-plugin-config-loader.ts +25 -13
- package/vite/vite-plugin-config.ts +1 -0
- package/vite/vite-plugin-dashboard-metadata.ts +19 -15
- package/vite/vite-plugin-gql-tada.ts +2 -2
- package/vite/vite-plugin-ui-config.ts +3 -2
- package/dist/plugin/config-loader.js +0 -141
- package/src/lib/components/shared/asset-preview.tsx +0 -345
- package/src/lib/components/ui/avatar.tsx +0 -38
- package/vite/config-loader.ts +0 -181
- /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
|
}
|
package/src/app/styles.css
CHANGED
|
@@ -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
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { DataTableBooleanFilter } from './filters/data-table-boolean-filter.js';
|
|
14
|
+
import { DataTableDateTimeFilter } from './filters/data-table-datetime-filter.js';
|
|
15
|
+
import { DataTableIdFilter } from './filters/data-table-id-filter.js';
|
|
16
|
+
import { DataTableNumberFilter } from './filters/data-table-number-filter.js';
|
|
17
|
+
import { DataTableStringFilter } from './filters/data-table-string-filter.js';
|
|
18
|
+
import { ColumnDataType } from './data-table-types.js';
|
|
15
19
|
|
|
16
20
|
export interface DataTableFilterDialogProps {
|
|
17
21
|
column: Column<any>;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
const STRING_OPERATORS = ['eq', 'notEq', 'contains', 'notContains', 'in', 'notIn', 'regex', 'isNull'];
|
|
21
|
-
|
|
22
24
|
export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
23
25
|
const columnFilter = column.getFilterValue() as Record<string, string> | undefined;
|
|
24
|
-
const [
|
|
25
|
-
|
|
26
|
-
const
|
|
26
|
+
const [filter, setFilter] = useState(columnFilter);
|
|
27
|
+
|
|
28
|
+
const columnDataType = (column.columnDef.meta as any)?.fieldInfo?.type as ColumnDataType;
|
|
27
29
|
const columnId = column.id;
|
|
28
30
|
return (
|
|
29
31
|
<DialogContent>
|
|
@@ -33,25 +35,19 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
|
33
35
|
</DialogTitle>
|
|
34
36
|
<DialogDescription></DialogDescription>
|
|
35
37
|
</DialogHeader>
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<Input
|
|
50
|
-
placeholder="Enter filter value..."
|
|
51
|
-
value={value}
|
|
52
|
-
onChange={e => setValue(e.target.value)}
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
38
|
+
{columnDataType === 'String' ? (
|
|
39
|
+
<DataTableStringFilter value={filter} onChange={e => setFilter(e)} />
|
|
40
|
+
) : columnDataType === 'Int' || columnDataType === 'Float' ? (
|
|
41
|
+
<DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='number' />
|
|
42
|
+
) : columnDataType === 'DateTime' ? (
|
|
43
|
+
<DataTableDateTimeFilter value={filter} onChange={e => setFilter(e)} />
|
|
44
|
+
) : columnDataType === 'Boolean' ? (
|
|
45
|
+
<DataTableBooleanFilter value={filter} onChange={e => setFilter(e)} />
|
|
46
|
+
) : columnDataType === 'ID' ? (
|
|
47
|
+
<DataTableIdFilter value={filter} onChange={e => setFilter(e)} />
|
|
48
|
+
) : columnDataType === 'Money' ? (
|
|
49
|
+
<DataTableNumberFilter value={filter} onChange={e => setFilter(e)} mode='money' />
|
|
50
|
+
) : null}
|
|
55
51
|
<DialogFooter className="sm:justify-end">
|
|
56
52
|
{columnFilter && (
|
|
57
53
|
<Button type="button" variant="secondary" onClick={e => column.setFilterValue(undefined)}>
|
|
@@ -62,7 +58,10 @@ export function DataTableFilterDialog({ column }: DataTableFilterDialogProps) {
|
|
|
62
58
|
<Button
|
|
63
59
|
type="button"
|
|
64
60
|
variant="secondary"
|
|
65
|
-
|
|
61
|
+
onClick={e => {
|
|
62
|
+
column.setFilterValue(filter);
|
|
63
|
+
setFilter(undefined);
|
|
64
|
+
}}
|
|
66
65
|
>
|
|
67
66
|
<Trans>Apply filter</Trans>
|
|
68
67
|
</Button>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ColumnDataType = 'String' | 'Int' | 'Float' | 'DateTime' | 'Boolean' | 'ID' | 'Money';
|
|
@@ -1,51 +1,100 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DndContext, closestCenter } from '@dnd-kit/core';
|
|
4
|
+
import {
|
|
5
|
+
restrictToVerticalAxis,
|
|
6
|
+
} from '@dnd-kit/modifiers';
|
|
7
|
+
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
8
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
4
9
|
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
|
|
5
10
|
import { Table } from '@tanstack/react-table';
|
|
6
|
-
import {
|
|
11
|
+
import { GripVertical, Settings2 } from 'lucide-react';
|
|
7
12
|
|
|
8
13
|
import { Button } from '@/components/ui/button.js';
|
|
9
14
|
import {
|
|
10
15
|
DropdownMenu,
|
|
11
16
|
DropdownMenuCheckboxItem,
|
|
12
|
-
DropdownMenuContent
|
|
13
|
-
DropdownMenuLabel,
|
|
14
|
-
DropdownMenuSeparator,
|
|
17
|
+
DropdownMenuContent
|
|
15
18
|
} from '@/components/ui/dropdown-menu.js';
|
|
19
|
+
import { usePage } from '@/hooks/use-page.js';
|
|
20
|
+
import { useUserSettings } from '@/hooks/use-user-settings.js';
|
|
21
|
+
import { Trans } from '@/lib/trans.js';
|
|
16
22
|
|
|
17
23
|
interface DataTableViewOptionsProps<TData> {
|
|
18
24
|
table: Table<TData>;
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
function SortableItem({ id, children }: { id: string; children: React.ReactNode }) {
|
|
28
|
+
const {
|
|
29
|
+
attributes,
|
|
30
|
+
listeners,
|
|
31
|
+
setNodeRef,
|
|
32
|
+
transform,
|
|
33
|
+
transition,
|
|
34
|
+
} = useSortable({ id });
|
|
35
|
+
|
|
36
|
+
const style = {
|
|
37
|
+
transform: CSS.Transform.toString(transform),
|
|
38
|
+
transition,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div ref={setNodeRef} style={style} className="flex items-center gap-.5">
|
|
43
|
+
<div {...attributes} {...listeners} className="cursor-grab">
|
|
44
|
+
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
45
|
+
</div>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
|
|
52
|
+
const { setTableSettings } = useUserSettings();
|
|
53
|
+
const page = usePage();
|
|
54
|
+
const columns = table
|
|
55
|
+
.getAllColumns()
|
|
56
|
+
.filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide());
|
|
57
|
+
|
|
58
|
+
const handleDragEnd = (event: any) => {
|
|
59
|
+
const { active, over } = event;
|
|
60
|
+
if (active.id !== over.id) {
|
|
61
|
+
const activeIndex = columns.findIndex(col => col.id === active.id);
|
|
62
|
+
const overIndex = columns.findIndex(col => col.id === over.id);
|
|
63
|
+
// update the column order in the `columns` array
|
|
64
|
+
const newColumns = [...columns];
|
|
65
|
+
newColumns.splice(overIndex, 0, newColumns.splice(activeIndex, 1)[0]);
|
|
66
|
+
if (page?.pageId) {
|
|
67
|
+
setTableSettings(page.pageId, 'columnOrder', newColumns.map(col => col.id));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
22
72
|
return (
|
|
23
73
|
<div className="flex items-center gap-2">
|
|
24
74
|
<DropdownMenu>
|
|
25
75
|
<DropdownMenuTrigger asChild>
|
|
26
|
-
<Button variant="
|
|
76
|
+
<Button variant="ghost" size="sm" className="ml-auto hidden h-8 lg:flex">
|
|
27
77
|
<Settings2 />
|
|
28
|
-
|
|
78
|
+
<Trans>Columns</Trans>
|
|
29
79
|
</Button>
|
|
30
80
|
</DropdownMenuTrigger>
|
|
31
81
|
<DropdownMenuContent align="end" className="w-[150px]">
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})}
|
|
82
|
+
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToVerticalAxis]}>
|
|
83
|
+
<SortableContext items={columns.map(col => col.id)} strategy={verticalListSortingStrategy}>
|
|
84
|
+
{columns.map(column => (
|
|
85
|
+
<SortableItem key={column.id} id={column.id}>
|
|
86
|
+
<DropdownMenuCheckboxItem
|
|
87
|
+
className="capitalize"
|
|
88
|
+
checked={column.getIsVisible()}
|
|
89
|
+
onCheckedChange={value => column.toggleVisibility(!!value)}
|
|
90
|
+
onSelect={(e) => e.preventDefault()}
|
|
91
|
+
>
|
|
92
|
+
{column.id}
|
|
93
|
+
</DropdownMenuCheckboxItem>
|
|
94
|
+
</SortableItem>
|
|
95
|
+
))}
|
|
96
|
+
</SortableContext>
|
|
97
|
+
</DndContext>
|
|
49
98
|
</DropdownMenuContent>
|
|
50
99
|
</DropdownMenu>
|
|
51
100
|
</div>
|