@vendure/dashboard 3.3.8 → 3.4.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/README.md +62 -0
- package/dist/plugin/api/api-extensions.d.ts +1 -0
- package/dist/plugin/api/api-extensions.js +38 -0
- package/dist/plugin/api/metrics.resolver.d.ts +8 -0
- package/dist/plugin/api/metrics.resolver.js +40 -0
- package/dist/plugin/config/metrics-strategies.d.ts +39 -0
- package/dist/plugin/config/metrics-strategies.js +74 -0
- package/dist/plugin/constants.d.ts +4 -3
- package/dist/plugin/constants.js +10 -277
- package/dist/plugin/dashboard.plugin.d.ts +95 -0
- package/dist/plugin/dashboard.plugin.js +168 -0
- package/dist/plugin/index.d.ts +2 -1
- package/dist/plugin/index.js +18 -1
- package/dist/plugin/package.json +3 -0
- package/dist/plugin/service/metrics.service.d.ts +15 -0
- package/dist/plugin/service/metrics.service.js +145 -0
- package/dist/plugin/types.d.ts +20 -37
- package/dist/plugin/types.js +13 -1
- package/dist/vite/constants.d.ts +5 -0
- package/dist/vite/constants.js +277 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +1 -0
- package/dist/vite/types.d.ts +40 -0
- package/dist/vite/utils/config-loader.js +1 -0
- package/dist/{plugin → vite}/utils/plugin-discovery.js +1 -1
- package/dist/vite/utils/ui-config.d.ts +3 -0
- package/dist/vite/utils/ui-config.js +30 -0
- package/dist/vite/vite-plugin-ui-config.d.ts +123 -0
- package/dist/{plugin → vite}/vite-plugin-ui-config.js +3 -11
- package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.js +1 -1
- package/index.html +1 -1
- package/package.json +16 -7
- package/src/app/app-providers.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +20 -35
- package/src/app/routes/_authenticated/_facets/facets.graphql.ts +40 -0
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +147 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +380 -33
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
- package/src/app/routes/_authenticated/_system/healthchecks.tsx +1 -1
- package/src/app/routes/_authenticated/_system/job-queue.tsx +1 -0
- package/src/app/routes/_authenticated/index.tsx +2 -2
- package/src/app/routes/_authenticated.tsx +1 -1
- package/src/lib/components/data-input/rich-text-input.tsx +14 -8
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +17 -4
- package/src/lib/components/layout/app-layout.tsx +2 -7
- package/src/lib/components/layout/channel-switcher.tsx +166 -57
- package/src/lib/components/layout/dev-mode-indicator.tsx +18 -0
- package/src/lib/components/layout/language-dialog.tsx +2 -1
- package/src/lib/components/layout/manage-languages-dialog.tsx +77 -40
- package/src/lib/components/layout/nav-item-wrapper.tsx +107 -0
- package/src/lib/components/layout/nav-main.tsx +196 -107
- package/src/lib/components/login/login-form.tsx +80 -45
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +19 -4
- package/src/lib/components/shared/asset/asset-gallery.tsx +2 -2
- package/src/lib/components/shared/detail-page-button.tsx +42 -0
- package/src/lib/components/shared/history-timeline/history-entry-date.tsx +37 -0
- package/src/lib/components/shared/history-timeline/history-entry.tsx +135 -65
- package/src/lib/components/shared/history-timeline/history-note-input.tsx +4 -4
- package/src/lib/components/shared/history-timeline/history-timeline.tsx +7 -54
- package/src/lib/components/shared/translatable-form-field.tsx +16 -2
- package/src/lib/framework/defaults.ts +4 -10
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +4 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +11 -2
- package/src/lib/framework/extension-api/logic/index.ts +1 -0
- package/src/lib/framework/extension-api/logic/login.ts +17 -0
- package/src/lib/framework/extension-api/logic/navigation.ts +1 -0
- package/src/lib/framework/extension-api/types/data-table.ts +12 -3
- package/src/lib/framework/extension-api/types/detail-forms.ts +13 -0
- package/src/lib/framework/extension-api/types/form-components.ts +11 -0
- package/src/lib/framework/extension-api/types/index.ts +1 -0
- package/src/lib/framework/extension-api/types/layout.ts +3 -6
- package/src/lib/framework/extension-api/types/login.ts +96 -0
- package/src/lib/framework/extension-api/types/navigation.ts +57 -0
- package/src/lib/framework/extension-api/types/widgets.ts +0 -4
- package/src/lib/framework/extension-api/use-login-extensions.ts +26 -0
- package/src/lib/framework/layout-engine/dev-mode-button.tsx +24 -0
- package/src/lib/framework/layout-engine/location-wrapper.tsx +5 -12
- package/src/lib/framework/registry/global-registry.ts +4 -0
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/graphql/api.ts +25 -3
- package/src/lib/graphql/graphql-env.d.ts +28 -28
- package/src/lib/graphql/settings-store-operations.ts +17 -0
- package/src/lib/hooks/use-floating-bulk-actions.ts +82 -0
- package/src/lib/hooks/use-local-format.ts +20 -5
- package/src/lib/index.ts +2 -1
- package/src/lib/providers/channel-provider.tsx +13 -11
- package/src/lib/providers/user-settings.tsx +78 -3
- package/src/lib/virtual.d.ts +26 -2
- package/src/vite-env.d.ts +2 -0
- package/vite/utils/plugin-discovery.ts +1 -1
- package/vite/utils/ui-config.ts +30 -42
- package/vite/vite-plugin-ui-config.ts +119 -17
- package/vite/vite-plugin-vendure-dashboard.ts +1 -1
- package/dist/plugin/utils/ui-config.d.ts +0 -3
- package/dist/plugin/utils/ui-config.js +0 -34
- package/dist/plugin/vite-plugin-ui-config.d.ts +0 -15
- package/src/app/routes/_authenticated/_facets/components/add-facet-value-dialog.tsx +0 -146
- package/src/lib/components/shared/rich-text-editor.tsx +0 -0
- /package/dist/{plugin/utils/ast-utils.spec.d.ts → vite/types.js} +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.js +0 -0
- /package/dist/{plugin/utils/config-loader.d.ts → vite/utils/ast-utils.spec.d.ts} +0 -0
- /package/dist/{plugin → vite}/utils/ast-utils.spec.js +0 -0
- /package/dist/{plugin → vite}/utils/compiler.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/compiler.js +0 -0
- /package/dist/{plugin/utils/config-loader.js → vite/utils/config-loader.d.ts} +0 -0
- /package/dist/{plugin → vite}/utils/logger.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/logger.js +0 -0
- /package/dist/{plugin → vite}/utils/plugin-discovery.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/schema-generator.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/schema-generator.js +0 -0
- /package/dist/{plugin → vite}/utils/tsconfig-utils.d.ts +0 -0
- /package/dist/{plugin → vite}/utils/tsconfig-utils.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-admin-api-schema.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config-loader.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config-loader.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-config.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-dashboard-metadata.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-gql-tada.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-gql-tada.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-tailwind-source.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-tailwind-source.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-theme.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-theme.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-transform-index.d.ts +0 -0
- /package/dist/{plugin → vite}/vite-plugin-transform-index.js +0 -0
- /package/dist/{plugin → vite}/vite-plugin-vendure-dashboard.d.ts +0 -0
|
@@ -2,6 +2,48 @@ import { Link } from '@tanstack/react-router';
|
|
|
2
2
|
import { ChevronRight } from 'lucide-react';
|
|
3
3
|
import { Button } from '../ui/button.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* DetailPageButton is a reusable navigation component designed to provide consistent UX
|
|
7
|
+
* across list views when linking to detail pages. It renders as a ghost button with
|
|
8
|
+
* a chevron indicator, making it easy for users to identify clickable links that
|
|
9
|
+
* navigate to detail views.
|
|
10
|
+
*
|
|
11
|
+
* @component
|
|
12
|
+
* @example
|
|
13
|
+
* // Basic usage with ID (relative navigation)
|
|
14
|
+
* <DetailPageButton id="123" label="Product Name" />
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Custom href with search params
|
|
18
|
+
* <DetailPageButton
|
|
19
|
+
* href="/products/detail/456"
|
|
20
|
+
* label="Custom Product"
|
|
21
|
+
* search={{ tab: 'variants' }}
|
|
22
|
+
* />
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Disabled state
|
|
26
|
+
* <DetailPageButton
|
|
27
|
+
* id="789"
|
|
28
|
+
* label="Unavailable Item"
|
|
29
|
+
* disabled={true}
|
|
30
|
+
* />
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} props - Component props
|
|
33
|
+
* @param {string|React.ReactNode} props.label - The text or content to display in the button
|
|
34
|
+
* @param {string} [props.id] - The ID for relative navigation (creates href as `./${id}`)
|
|
35
|
+
* @param {string} [props.href] - Custom href for navigation (takes precedence over id)
|
|
36
|
+
* @param {boolean} [props.disabled=false] - Whether the button is disabled (prevents navigation)
|
|
37
|
+
* @param {Record<string, string>} [props.search] - Search parameters to include in the navigation
|
|
38
|
+
*
|
|
39
|
+
* @returns {React.ReactElement} A styled button component that navigates to detail pages
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* - Uses TanStack Router's Link component for client-side navigation
|
|
43
|
+
* - Includes a chevron icon (hidden when disabled) to indicate navigation
|
|
44
|
+
* - Preloading is disabled by default for performance optimization
|
|
45
|
+
* - Styled as a ghost button variant for subtle, consistent appearance
|
|
46
|
+
*/
|
|
5
47
|
export function DetailPageButton({
|
|
6
48
|
id,
|
|
7
49
|
href,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
|
|
2
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
3
|
+
|
|
4
|
+
interface HistoryEntryDateProps {
|
|
5
|
+
date: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function HistoryEntryDate({ date, className }: Readonly<HistoryEntryDateProps>) {
|
|
10
|
+
const { formatRelativeDate, formatDate } = useLocalFormat();
|
|
11
|
+
|
|
12
|
+
const formatFullDateTime = (dateString: string) => {
|
|
13
|
+
return formatDate(dateString, {
|
|
14
|
+
year: 'numeric',
|
|
15
|
+
month: 'long',
|
|
16
|
+
day: 'numeric',
|
|
17
|
+
hour: 'numeric',
|
|
18
|
+
minute: 'numeric',
|
|
19
|
+
second: 'numeric',
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<TooltipProvider>
|
|
25
|
+
<Tooltip>
|
|
26
|
+
<TooltipTrigger asChild>
|
|
27
|
+
<div className={className}>
|
|
28
|
+
{formatRelativeDate(date)}
|
|
29
|
+
</div>
|
|
30
|
+
</TooltipTrigger>
|
|
31
|
+
<TooltipContent>
|
|
32
|
+
<p>{formatFullDateTime(date)}</p>
|
|
33
|
+
</TooltipContent>
|
|
34
|
+
</Tooltip>
|
|
35
|
+
</TooltipProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
1
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
2
3
|
import {
|
|
3
4
|
DropdownMenu,
|
|
@@ -6,11 +7,10 @@ import {
|
|
|
6
7
|
DropdownMenuTrigger,
|
|
7
8
|
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
8
9
|
import { Separator } from '@/vdb/components/ui/separator.js';
|
|
9
|
-
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
10
10
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
11
|
+
import { cn } from '@/vdb/lib/utils.js';
|
|
11
12
|
import { MoreVerticalIcon, PencilIcon, TrashIcon } from 'lucide-react';
|
|
12
|
-
import {
|
|
13
|
-
import { useHistoryTimeline } from './history-timeline.js';
|
|
13
|
+
import { HistoryEntryDate } from './history-entry-date.js';
|
|
14
14
|
|
|
15
15
|
export interface HistoryEntryItem {
|
|
16
16
|
id: string;
|
|
@@ -25,12 +25,21 @@ export interface HistoryEntryItem {
|
|
|
25
25
|
data: any;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
interface OrderCustomer {
|
|
29
|
+
firstName: string;
|
|
30
|
+
lastName: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
interface HistoryEntryProps {
|
|
29
34
|
entry: HistoryEntryItem;
|
|
30
35
|
isNoteEntry: boolean;
|
|
31
36
|
timelineIcon: React.ReactNode;
|
|
32
37
|
title: string | React.ReactNode;
|
|
33
38
|
children: React.ReactNode;
|
|
39
|
+
isPrimary?: boolean;
|
|
40
|
+
customer?: OrderCustomer | null;
|
|
41
|
+
onEditNote?: (noteId: string, note: string, isPrivate: boolean) => void;
|
|
42
|
+
onDeleteNote?: (noteId: string) => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export function HistoryEntry({
|
|
@@ -39,80 +48,141 @@ export function HistoryEntry({
|
|
|
39
48
|
timelineIcon,
|
|
40
49
|
title,
|
|
41
50
|
children,
|
|
51
|
+
isPrimary = true,
|
|
52
|
+
customer,
|
|
53
|
+
onEditNote,
|
|
54
|
+
onDeleteNote,
|
|
42
55
|
}: Readonly<HistoryEntryProps>) {
|
|
43
|
-
const
|
|
44
|
-
|
|
56
|
+
const getIconColor = (type: string) => {
|
|
57
|
+
// Check for success states (payment settled, order delivered)
|
|
58
|
+
if (type === 'ORDER_PAYMENT_TRANSITION' && entry.data.to === 'Settled') {
|
|
59
|
+
return 'bg-success text-success-foreground';
|
|
60
|
+
}
|
|
61
|
+
if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Delivered') {
|
|
62
|
+
return 'bg-success text-success-foreground';
|
|
63
|
+
}
|
|
64
|
+
if (type === 'ORDER_FULFILLMENT_TRANSITION' && entry.data.to === 'Delivered') {
|
|
65
|
+
return 'bg-success text-success-foreground';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check for destructive states (cancellations)
|
|
69
|
+
if (type === 'ORDER_CANCELLATION') {
|
|
70
|
+
return 'bg-destructive text-destructive-foreground';
|
|
71
|
+
}
|
|
72
|
+
if (type === 'ORDER_STATE_TRANSITION' && entry.data.to === 'Cancelled') {
|
|
73
|
+
return 'bg-destructive text-destructive-foreground';
|
|
74
|
+
}
|
|
75
|
+
if (type === 'ORDER_PAYMENT_TRANSITION' && (entry.data.to === 'Declined' || entry.data.to === 'Cancelled')) {
|
|
76
|
+
return 'bg-destructive text-destructive-foreground';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// All other entries use neutral colors
|
|
80
|
+
return 'bg-muted text-muted-foreground';
|
|
81
|
+
};
|
|
45
82
|
|
|
46
|
-
const
|
|
47
|
-
(
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
second: 'numeric',
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
[formatDate],
|
|
58
|
-
);
|
|
83
|
+
const getActorName = () => {
|
|
84
|
+
if (entry.administrator) {
|
|
85
|
+
return `${entry.administrator.firstName} ${entry.administrator.lastName}`;
|
|
86
|
+
} else if (customer) {
|
|
87
|
+
return `${customer.firstName} ${customer.lastName}`;
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
};
|
|
59
91
|
|
|
60
92
|
return (
|
|
61
|
-
<div key={entry.id} className="relative
|
|
62
|
-
<div
|
|
63
|
-
|
|
93
|
+
<div key={entry.id} className="relative group">
|
|
94
|
+
<div
|
|
95
|
+
className={`flex gap-3 p-3 rounded-lg hover:bg-muted/30 transition-colors ${!isPrimary ? 'opacity-75' : ''}`}
|
|
96
|
+
>
|
|
97
|
+
<div className={cn(`relative z-10 flex-shrink-0 ${isNoteEntry ? 'ml-2' : ''}`, isPrimary ? '-ml-1' : '')}>
|
|
64
98
|
<div
|
|
65
|
-
className={`rounded-full
|
|
99
|
+
className={`rounded-full flex items-center justify-center ${isPrimary ? 'h-8 w-8' : 'h-6 w-6'} ${getIconColor(entry.type)} border-2 border-background ${isPrimary ? 'shadow-sm' : 'shadow-none'}`}
|
|
66
100
|
>
|
|
67
|
-
{
|
|
101
|
+
<div className={isPrimary ? 'text-current' : 'text-current scale-75'}>
|
|
102
|
+
{timelineIcon}
|
|
103
|
+
</div>
|
|
68
104
|
</div>
|
|
69
105
|
</div>
|
|
70
|
-
</div>
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
<div className="flex-1 min-w-0">
|
|
108
|
+
<div className="flex items-start justify-between">
|
|
109
|
+
<div className="flex-1 min-w-0">
|
|
110
|
+
<h4
|
|
111
|
+
className={`text-sm ${isPrimary ? 'font-medium text-foreground' : 'font-normal text-muted-foreground'}`}
|
|
112
|
+
>
|
|
113
|
+
{title}
|
|
114
|
+
</h4>
|
|
115
|
+
<div className="mt-1">
|
|
116
|
+
{entry.type === 'ORDER_NOTE' ? (
|
|
117
|
+
<div className={`space-y-${isPrimary ? '2' : '1'}`}>
|
|
118
|
+
<p className={`${isPrimary ? 'text-sm' : 'text-xs'} text-foreground`}>
|
|
119
|
+
{entry.data.note}
|
|
120
|
+
</p>
|
|
121
|
+
<div className="flex items-center gap-2">
|
|
122
|
+
<Badge
|
|
123
|
+
variant={entry.isPublic ? 'outline' : 'secondary'}
|
|
124
|
+
className="text-xs"
|
|
125
|
+
>
|
|
126
|
+
{entry.isPublic ? 'Public' : 'Private'}
|
|
127
|
+
</Badge>
|
|
128
|
+
{isPrimary && onEditNote && onDeleteNote && (
|
|
129
|
+
<DropdownMenu>
|
|
130
|
+
<DropdownMenuTrigger asChild>
|
|
131
|
+
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
|
|
132
|
+
<MoreVerticalIcon className="h-3 w-3" />
|
|
133
|
+
</Button>
|
|
134
|
+
</DropdownMenuTrigger>
|
|
135
|
+
<DropdownMenuContent align="end">
|
|
136
|
+
<DropdownMenuItem
|
|
137
|
+
onClick={() => {
|
|
138
|
+
onEditNote(
|
|
139
|
+
entry.id,
|
|
140
|
+
entry.data.note,
|
|
141
|
+
!entry.isPublic,
|
|
142
|
+
);
|
|
143
|
+
}}
|
|
144
|
+
className="cursor-pointer"
|
|
145
|
+
>
|
|
146
|
+
<PencilIcon className="mr-2 h-4 w-4" />
|
|
147
|
+
<Trans>Edit</Trans>
|
|
148
|
+
</DropdownMenuItem>
|
|
149
|
+
<Separator className="my-1" />
|
|
150
|
+
<DropdownMenuItem
|
|
151
|
+
onClick={() => onDeleteNote(entry.id)}
|
|
152
|
+
className="cursor-pointer text-red-600 focus:text-red-600"
|
|
153
|
+
>
|
|
154
|
+
<TrashIcon className="mr-2 h-4 w-4" />
|
|
155
|
+
<span>Delete</span>
|
|
156
|
+
</DropdownMenuItem>
|
|
157
|
+
</DropdownMenuContent>
|
|
158
|
+
</DropdownMenu>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
children
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
86
167
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<DropdownMenuItem
|
|
104
|
-
onClick={() => deleteNote(entry.id)}
|
|
105
|
-
className="cursor-pointer text-red-600 focus:text-red-600"
|
|
106
|
-
>
|
|
107
|
-
<TrashIcon className="mr-2 h-4 w-4" />
|
|
108
|
-
<span>Delete</span>
|
|
109
|
-
</DropdownMenuItem>
|
|
110
|
-
</DropdownMenuContent>
|
|
111
|
-
</DropdownMenu>
|
|
112
|
-
)}
|
|
168
|
+
<div className="flex items-center gap-2 ml-4 flex-shrink-0">
|
|
169
|
+
<div className="text-right">
|
|
170
|
+
<HistoryEntryDate
|
|
171
|
+
date={entry.createdAt}
|
|
172
|
+
className={`text-xs cursor-help ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
|
|
173
|
+
/>
|
|
174
|
+
{getActorName() && (
|
|
175
|
+
<div
|
|
176
|
+
className={`text-xs ${isPrimary ? 'text-muted-foreground' : 'text-muted-foreground/70'}`}
|
|
177
|
+
>
|
|
178
|
+
{getActorName()}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
113
184
|
</div>
|
|
114
185
|
</div>
|
|
115
|
-
<div className="border-b border-muted my-4 mx-4"></div>
|
|
116
186
|
</div>
|
|
117
187
|
);
|
|
118
188
|
}
|
|
@@ -19,17 +19,17 @@ export function HistoryNoteInput({ onAddNote }: Readonly<HistoryNoteInputProps>)
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
|
-
<div className="
|
|
23
|
-
<div className="flex flex-col space-y-
|
|
22
|
+
<div className="bg-muted/20 rounded-lg p-3 mb-4">
|
|
23
|
+
<div className="flex flex-col space-y-2">
|
|
24
24
|
<Textarea
|
|
25
25
|
placeholder="Add a note..."
|
|
26
26
|
value={note}
|
|
27
27
|
onChange={e => setNote(e.target.value)}
|
|
28
|
-
className="min-h-[
|
|
28
|
+
className="min-h-[50px] resize-none bg-background/50 focus:bg-background transition-colors text-sm"
|
|
29
29
|
/>
|
|
30
30
|
<div className="flex items-center justify-between">
|
|
31
31
|
<HistoryNoteCheckbox value={noteIsPrivate} onChange={setNoteIsPrivate} />
|
|
32
|
-
<Button onClick={handleAddNote} disabled={!note.trim()} size="sm">
|
|
32
|
+
<Button onClick={handleAddNote} disabled={!note.trim()} size="sm" className="h-7 px-3 text-xs">
|
|
33
33
|
Add note
|
|
34
34
|
</Button>
|
|
35
35
|
</div>
|
|
@@ -1,63 +1,16 @@
|
|
|
1
1
|
import { ScrollArea } from '@/vdb/components/ui/scroll-area.js';
|
|
2
|
-
import { createContext, useContext, useState } from 'react';
|
|
3
|
-
import { HistoryNoteEditor } from './history-note-editor.js';
|
|
4
2
|
|
|
5
3
|
interface HistoryTimelineProps {
|
|
6
4
|
children: React.ReactNode;
|
|
7
|
-
onEditNote?: (entryId: string, note: string, isPublic: boolean) => void;
|
|
8
|
-
onDeleteNote?: (entryId: string) => void;
|
|
9
5
|
}
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
// HistoryEntry component
|
|
13
|
-
const HistoryTimelineContext = createContext<{
|
|
14
|
-
editNote: (noteId: string, note: string, isPrivate: boolean) => void;
|
|
15
|
-
deleteNote: (noteId: string) => void;
|
|
16
|
-
}>({
|
|
17
|
-
editNote: () => {},
|
|
18
|
-
deleteNote: () => {},
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
type NoteEditorNote = { noteId: string; note: string; isPrivate: boolean };
|
|
22
|
-
|
|
23
|
-
export function useHistoryTimeline() {
|
|
24
|
-
return useContext(HistoryTimelineContext);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function HistoryTimeline({ children, onEditNote, onDeleteNote }: Readonly<HistoryTimelineProps>) {
|
|
28
|
-
const [noteEditorOpen, setNoteEditorOpen] = useState(false);
|
|
29
|
-
const [noteEditorNote, setNoteEditorNote] = useState<NoteEditorNote>({
|
|
30
|
-
noteId: '',
|
|
31
|
-
note: '',
|
|
32
|
-
isPrivate: true,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const editNote = (noteId: string, note: string, isPrivate: boolean) => {
|
|
36
|
-
setNoteEditorNote({ noteId, note, isPrivate });
|
|
37
|
-
setNoteEditorOpen(true);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const deleteNote = (noteId: string) => {
|
|
41
|
-
setNoteEditorNote({ noteId, note: '', isPrivate: true });
|
|
42
|
-
};
|
|
43
|
-
|
|
7
|
+
export function HistoryTimeline({ children }: Readonly<HistoryTimelineProps>) {
|
|
44
8
|
return (
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<div className="
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
</ScrollArea>
|
|
52
|
-
<HistoryNoteEditor
|
|
53
|
-
key={noteEditorNote.noteId}
|
|
54
|
-
note={noteEditorNote.note}
|
|
55
|
-
onNoteChange={(...args) => onEditNote?.(...args)}
|
|
56
|
-
open={noteEditorOpen}
|
|
57
|
-
onOpenChange={setNoteEditorOpen}
|
|
58
|
-
noteId={noteEditorNote.noteId}
|
|
59
|
-
isPrivate={noteEditorNote.isPrivate}
|
|
60
|
-
/>
|
|
61
|
-
</HistoryTimelineContext.Provider>
|
|
9
|
+
<ScrollArea className="pr-2">
|
|
10
|
+
<div className="relative">
|
|
11
|
+
<div className="absolute left-6 top-6 bottom-0 w-px bg-gradient-to-b from-border via-border/50 to-transparent" />
|
|
12
|
+
<div className="space-y-0.5">{children}</div>
|
|
13
|
+
</div>
|
|
14
|
+
</ScrollArea>
|
|
62
15
|
);
|
|
63
16
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { OverriddenFormComponent } from '@/vdb/framework/form-engine/overridden-form-component.js';
|
|
2
2
|
import { LocationWrapper } from '@/vdb/framework/layout-engine/location-wrapper.js';
|
|
3
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
3
4
|
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
5
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
4
6
|
import { Controller, ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
|
5
7
|
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '../ui/form.js';
|
|
6
8
|
import { FormFieldWrapper } from './form-field-wrapper.js';
|
|
@@ -13,6 +15,7 @@ export type TranslatableFormFieldProps<TFieldValues extends TranslatableEntity |
|
|
|
13
15
|
ControllerProps<TFieldValues>,
|
|
14
16
|
'name'
|
|
15
17
|
> & {
|
|
18
|
+
label?: React.ReactNode;
|
|
16
19
|
name: TFieldValues extends TranslatableEntity
|
|
17
20
|
? keyof Omit<NonNullable<TFieldValues['translations']>[number], 'languageCode'>
|
|
18
21
|
: TFieldValues extends TranslatableEntity[]
|
|
@@ -24,16 +27,26 @@ export const TranslatableFormField = <
|
|
|
24
27
|
TFieldValues extends TranslatableEntity | TranslatableEntity[] = TranslatableEntity,
|
|
25
28
|
>({
|
|
26
29
|
name,
|
|
30
|
+
label,
|
|
27
31
|
...props
|
|
28
32
|
}: TranslatableFormFieldProps<TFieldValues>) => {
|
|
33
|
+
const { formatLanguageName } = useLocalFormat();
|
|
29
34
|
const { contentLanguage } = useUserSettings().settings;
|
|
30
35
|
const formValues = props.control?._formValues;
|
|
31
36
|
const translations = Array.isArray(formValues) ? formValues?.[0].translations : formValues?.translations;
|
|
32
|
-
const
|
|
37
|
+
const existingIndex = translations?.findIndex(
|
|
33
38
|
(translation: any) => translation?.languageCode === contentLanguage,
|
|
34
39
|
);
|
|
40
|
+
const index = existingIndex === -1 ? translations?.length : existingIndex;
|
|
35
41
|
if (index === undefined || index === -1) {
|
|
36
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
<FormItem>
|
|
44
|
+
{label && <FormLabel>{label}</FormLabel>}
|
|
45
|
+
<div className="text-sm text-muted-foreground">
|
|
46
|
+
<Trans>No translation found for {formatLanguageName(contentLanguage)}</Trans>
|
|
47
|
+
</div>
|
|
48
|
+
</FormItem>
|
|
49
|
+
);
|
|
37
50
|
}
|
|
38
51
|
const translationName = `translations.${index}.${String(name)}` as FieldPath<TFieldValues>;
|
|
39
52
|
return <Controller {...props} name={translationName} key={translationName} />;
|
|
@@ -57,6 +70,7 @@ export const TranslatableFormFieldWrapper = <
|
|
|
57
70
|
return (
|
|
58
71
|
<LocationWrapper identifier={name as string}>
|
|
59
72
|
<TranslatableFormField
|
|
73
|
+
label={label}
|
|
60
74
|
control={props.control}
|
|
61
75
|
name={name}
|
|
62
76
|
render={renderArgs => (
|
|
@@ -18,8 +18,8 @@ export function registerDefaults() {
|
|
|
18
18
|
setNavMenuConfig({
|
|
19
19
|
sections: [
|
|
20
20
|
{
|
|
21
|
-
id: '
|
|
22
|
-
title: '
|
|
21
|
+
id: 'insights',
|
|
22
|
+
title: 'Insights',
|
|
23
23
|
placement: 'top',
|
|
24
24
|
icon: LayoutDashboardIcon,
|
|
25
25
|
url: '/',
|
|
@@ -29,7 +29,6 @@ export function registerDefaults() {
|
|
|
29
29
|
id: 'catalog',
|
|
30
30
|
title: 'Catalog',
|
|
31
31
|
icon: SquareTerminal,
|
|
32
|
-
defaultOpen: true,
|
|
33
32
|
placement: 'top',
|
|
34
33
|
order: 200,
|
|
35
34
|
items: [
|
|
@@ -69,7 +68,6 @@ export function registerDefaults() {
|
|
|
69
68
|
id: 'sales',
|
|
70
69
|
title: 'Sales',
|
|
71
70
|
icon: ShoppingCart,
|
|
72
|
-
defaultOpen: true,
|
|
73
71
|
placement: 'top',
|
|
74
72
|
order: 300,
|
|
75
73
|
items: [
|
|
@@ -85,7 +83,6 @@ export function registerDefaults() {
|
|
|
85
83
|
id: 'customers',
|
|
86
84
|
title: 'Customers',
|
|
87
85
|
icon: Users,
|
|
88
|
-
defaultOpen: false,
|
|
89
86
|
placement: 'top',
|
|
90
87
|
order: 400,
|
|
91
88
|
items: [
|
|
@@ -107,7 +104,6 @@ export function registerDefaults() {
|
|
|
107
104
|
id: 'marketing',
|
|
108
105
|
title: 'Marketing',
|
|
109
106
|
icon: Mail,
|
|
110
|
-
defaultOpen: false,
|
|
111
107
|
placement: 'top',
|
|
112
108
|
order: 500,
|
|
113
109
|
items: [
|
|
@@ -123,9 +119,8 @@ export function registerDefaults() {
|
|
|
123
119
|
id: 'system',
|
|
124
120
|
title: 'System',
|
|
125
121
|
icon: Terminal,
|
|
126
|
-
defaultOpen: false,
|
|
127
122
|
placement: 'bottom',
|
|
128
|
-
order:
|
|
123
|
+
order: 200,
|
|
129
124
|
items: [
|
|
130
125
|
{
|
|
131
126
|
id: 'job-queue',
|
|
@@ -151,9 +146,8 @@ export function registerDefaults() {
|
|
|
151
146
|
id: 'settings',
|
|
152
147
|
title: 'Settings',
|
|
153
148
|
icon: Settings2,
|
|
154
|
-
defaultOpen: false,
|
|
155
149
|
placement: 'bottom',
|
|
156
|
-
order:
|
|
150
|
+
order: 100,
|
|
157
151
|
items: [
|
|
158
152
|
{
|
|
159
153
|
id: 'sellers',
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
registerDetailFormExtensions,
|
|
8
8
|
registerFormComponentExtensions,
|
|
9
9
|
registerLayoutExtensions,
|
|
10
|
+
registerLoginExtensions,
|
|
10
11
|
registerNavigationExtensions,
|
|
11
12
|
registerWidgetExtensions,
|
|
12
13
|
} from './logic/index.js';
|
|
@@ -57,6 +58,9 @@ export function defineDashboardExtension(extension: DashboardExtension) {
|
|
|
57
58
|
// Register alert extensions
|
|
58
59
|
registerAlertExtensions(extension.alerts);
|
|
59
60
|
|
|
61
|
+
// Register login extensions
|
|
62
|
+
registerLoginExtensions(extension.login);
|
|
63
|
+
|
|
60
64
|
// Execute extension source change callbacks
|
|
61
65
|
const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');
|
|
62
66
|
if (callbacks.size) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
DashboardCustomFormComponents,
|
|
6
6
|
DashboardDataTableExtensionDefinition,
|
|
7
7
|
DashboardDetailFormExtensionDefinition,
|
|
8
|
+
DashboardLoginExtensions,
|
|
8
9
|
DashboardNavSectionDefinition,
|
|
9
10
|
DashboardPageBlockDefinition,
|
|
10
11
|
DashboardRouteDefinition,
|
|
@@ -54,8 +55,7 @@ export interface DashboardExtension {
|
|
|
54
55
|
widgets?: DashboardWidgetDefinition[];
|
|
55
56
|
/**
|
|
56
57
|
* @description
|
|
57
|
-
* Unified registration for custom form
|
|
58
|
-
* input components, and display components.
|
|
58
|
+
* Unified registration for custom form custom field components.
|
|
59
59
|
*/
|
|
60
60
|
customFormComponents?: DashboardCustomFormComponents;
|
|
61
61
|
/**
|
|
@@ -63,5 +63,14 @@ export interface DashboardExtension {
|
|
|
63
63
|
* Allows you to customize aspects of existing data tables in the dashboard.
|
|
64
64
|
*/
|
|
65
65
|
dataTables?: DashboardDataTableExtensionDefinition[];
|
|
66
|
+
/**
|
|
67
|
+
* @description
|
|
68
|
+
* Allows you to customize the detail form for any page in the dashboard.
|
|
69
|
+
*/
|
|
66
70
|
detailForms?: DashboardDetailFormExtensionDefinition[];
|
|
71
|
+
/**
|
|
72
|
+
* @description
|
|
73
|
+
* Allows you to customize the login page with custom components.
|
|
74
|
+
*/
|
|
75
|
+
login?: DashboardLoginExtensions;
|
|
67
76
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { globalRegistry } from '../../registry/global-registry.js';
|
|
2
|
+
import { DashboardLoginExtensions } from '../types/login.js';
|
|
3
|
+
|
|
4
|
+
export function registerLoginExtensions(loginExtensions?: DashboardLoginExtensions) {
|
|
5
|
+
if (!loginExtensions) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const registryKey = 'loginExtensions';
|
|
10
|
+
|
|
11
|
+
globalRegistry.set(registryKey, (oldValue: DashboardLoginExtensions) => {
|
|
12
|
+
return {
|
|
13
|
+
...oldValue,
|
|
14
|
+
...loginExtensions,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -25,6 +25,7 @@ export function registerNavigationExtensions(
|
|
|
25
25
|
url: route.navMenuItem.url ?? route.path,
|
|
26
26
|
id: route.navMenuItem.id ?? route.path,
|
|
27
27
|
title: route.navMenuItem.title ?? route.path,
|
|
28
|
+
order: route.navMenuItem.order,
|
|
28
29
|
};
|
|
29
30
|
addNavMenuItem(item, route.navMenuItem.sectionId);
|
|
30
31
|
}
|