@vendure/dashboard 3.2.2 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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} +6 -0
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
- 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-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 +8 -6
- package/src/app/app-providers.tsx +8 -8
- 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 -1
- 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/_products/products.tsx +1 -1
- package/src/app/routes/_authenticated.tsx +12 -1
- 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 +72 -23
- package/src/lib/components/data-table/data-table.tsx +23 -24
- 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/layout/nav-user.tsx +4 -4
- 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/navigation-confirmation.tsx +39 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +9 -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/document-introspection/get-document-structure.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
- package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
- package/src/lib/framework/page/list-page.tsx +23 -4
- package/src/lib/framework/page/use-detail-page.ts +1 -0
- package/src/lib/graphql/fragments.tsx +8 -0
- package/src/lib/index.ts +5 -5
- package/src/lib/providers/auth.tsx +12 -9
- package/src/lib/providers/channel-provider.tsx +1 -0
- 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} +7 -1
- package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
- package/vite/vite-plugin-admin-api-schema.ts +2 -2
- package/vite/vite-plugin-config-loader.ts +25 -13
- 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/vite/config-loader.ts +0 -181
- /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Trans } from "@/lib/trans.js";
|
|
2
|
+
|
|
3
|
+
import { Select, SelectValue, SelectTrigger, SelectItem } from "@/components/ui/select.js";
|
|
4
|
+
|
|
5
|
+
import { SelectContent } from "@/components/ui/select.js";
|
|
6
|
+
import { Input } from "@/components/ui/input.js";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { HumanReadableOperator } from "../human-readable-operator.js";
|
|
9
|
+
|
|
10
|
+
export interface DataTableStringFilterProps {
|
|
11
|
+
value: Record<string, any> | undefined;
|
|
12
|
+
onChange: (filter: Record<string, any>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const STRING_OPERATORS = ['eq', 'notEq', 'contains', 'notContains', 'in', 'notIn', 'regex', 'isNull'] as const;
|
|
16
|
+
|
|
17
|
+
export function DataTableStringFilter({ value: incomingValue, onChange }: DataTableStringFilterProps) {
|
|
18
|
+
const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'contains';
|
|
19
|
+
const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
|
|
20
|
+
const [operator, setOperator] = useState<string>(initialOperator ?? 'contains');
|
|
21
|
+
const [value, setValue] = useState((initialValue as string) ?? '');
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (operator === 'isNull') {
|
|
25
|
+
onChange({ [operator]: true });
|
|
26
|
+
} else if (operator === 'in' || operator === 'notIn') {
|
|
27
|
+
// Split by comma and trim whitespace
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const values = value.split(',').map(v => v.trim()).filter(v => v);
|
|
30
|
+
onChange({ [operator]: values });
|
|
31
|
+
} else {
|
|
32
|
+
onChange({ [operator]: [] });
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
onChange({ [operator]: value });
|
|
36
|
+
}
|
|
37
|
+
}, [operator, value]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
41
|
+
<Select value={operator} onValueChange={value => setOperator(value)}>
|
|
42
|
+
<SelectTrigger>
|
|
43
|
+
<SelectValue placeholder="Select operator" />
|
|
44
|
+
</SelectTrigger>
|
|
45
|
+
<SelectContent>
|
|
46
|
+
{STRING_OPERATORS.map(op => (
|
|
47
|
+
<SelectItem key={op} value={op}>
|
|
48
|
+
<HumanReadableOperator operator={op} />
|
|
49
|
+
</SelectItem>
|
|
50
|
+
))}
|
|
51
|
+
</SelectContent>
|
|
52
|
+
</Select>
|
|
53
|
+
{operator !== 'isNull' && (
|
|
54
|
+
<Input
|
|
55
|
+
placeholder={operator === 'in' || operator === 'notIn' ? "Enter comma-separated values..." : "Enter filter value..."}
|
|
56
|
+
value={value}
|
|
57
|
+
onChange={e => setValue(e.target.value)}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DATETIME_OPERATORS } from './filters/data-table-datetime-filter.js';
|
|
2
|
+
import { BOOLEAN_OPERATORS } from './filters/data-table-boolean-filter.js';
|
|
3
|
+
import { ID_OPERATORS } from './filters/data-table-id-filter.js';
|
|
4
|
+
import { NUMBER_OPERATORS } from './filters/data-table-number-filter.js';
|
|
5
|
+
import { STRING_OPERATORS } from './filters/data-table-string-filter.js';
|
|
6
|
+
import { Trans } from '@/lib/trans.js';
|
|
7
|
+
|
|
8
|
+
type Operator =
|
|
9
|
+
| (typeof DATETIME_OPERATORS)[number]
|
|
10
|
+
| (typeof BOOLEAN_OPERATORS)[number]
|
|
11
|
+
| (typeof ID_OPERATORS)[number]
|
|
12
|
+
| (typeof NUMBER_OPERATORS)[number]
|
|
13
|
+
| (typeof STRING_OPERATORS)[number];
|
|
14
|
+
|
|
15
|
+
export function HumanReadableOperator({
|
|
16
|
+
operator,
|
|
17
|
+
mode = 'long',
|
|
18
|
+
}: {
|
|
19
|
+
operator: Operator;
|
|
20
|
+
mode?: 'short' | 'long';
|
|
21
|
+
}) {
|
|
22
|
+
switch (operator) {
|
|
23
|
+
case 'eq':
|
|
24
|
+
return mode === 'short' ? <Trans>=</Trans> : <Trans>is equal to</Trans>;
|
|
25
|
+
case 'notEq':
|
|
26
|
+
return mode === 'short' ? <Trans>!=</Trans> : <Trans>is not equal to</Trans>;
|
|
27
|
+
case 'before':
|
|
28
|
+
return mode === 'short' ? <Trans>before</Trans> : <Trans>is before</Trans>;
|
|
29
|
+
case 'after':
|
|
30
|
+
return mode === 'short' ? <Trans>after</Trans> : <Trans>is after</Trans>;
|
|
31
|
+
case 'between':
|
|
32
|
+
return mode === 'short' ? <Trans>between</Trans> : <Trans>is between</Trans>;
|
|
33
|
+
case 'isNull':
|
|
34
|
+
return mode === 'short' ? <Trans>is null</Trans> : <Trans>is null</Trans>;
|
|
35
|
+
case 'in':
|
|
36
|
+
return mode === 'short' ? <Trans>in</Trans> : <Trans>is in</Trans>;
|
|
37
|
+
case 'notIn':
|
|
38
|
+
return mode === 'short' ? <Trans>not in</Trans> : <Trans>is not in</Trans>;
|
|
39
|
+
case 'gt':
|
|
40
|
+
return mode === 'short' ? <Trans>greater than</Trans> : <Trans>is greater than</Trans>;
|
|
41
|
+
case 'gte':
|
|
42
|
+
return mode === 'short' ? (
|
|
43
|
+
<Trans>greater than or equal</Trans>
|
|
44
|
+
) : (
|
|
45
|
+
<Trans>is greater than or equal to</Trans>
|
|
46
|
+
);
|
|
47
|
+
case 'lt':
|
|
48
|
+
return mode === 'short' ? <Trans>less than</Trans> : <Trans>is less than</Trans>;
|
|
49
|
+
case 'lte':
|
|
50
|
+
return mode === 'short' ? (
|
|
51
|
+
<Trans>less than or equal</Trans>
|
|
52
|
+
) : (
|
|
53
|
+
<Trans>is less than or equal to</Trans>
|
|
54
|
+
);
|
|
55
|
+
case 'contains':
|
|
56
|
+
return mode === 'short' ? <Trans>contains</Trans> : <Trans>contains</Trans>;
|
|
57
|
+
case 'notContains':
|
|
58
|
+
return mode === 'short' ? <Trans>does not contain</Trans> : <Trans>does not contain</Trans>;
|
|
59
|
+
case 'regex':
|
|
60
|
+
return mode === 'short' ? <Trans>matches regex</Trans> : <Trans>matches regex</Trans>;
|
|
61
|
+
default:
|
|
62
|
+
operator satisfies never;
|
|
63
|
+
return <Trans>{operator}</Trans>;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -44,14 +44,14 @@ export function NavUser() {
|
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
const avatarFallback = useMemo(() => {
|
|
48
|
+
return user?.firstName?.charAt(0) ?? '' + user?.lastName?.charAt(0) ?? '';
|
|
49
|
+
}, [user]);
|
|
50
|
+
|
|
47
51
|
if (!user) {
|
|
48
52
|
return <></>;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const avatarFallback = useMemo(() => {
|
|
52
|
-
return user.firstName.charAt(0) + user.lastName.charAt(0);
|
|
53
|
-
}, [user]);
|
|
54
|
-
|
|
55
55
|
const isDevMode = (import.meta as any).env?.MODE === 'development';
|
|
56
56
|
|
|
57
57
|
return (
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Button } from "@/components/ui/button.js";
|
|
2
|
+
import { cn } from "@/lib/utils.js";
|
|
3
|
+
import { Crosshair, X } from "lucide-react";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
5
|
+
import { DndContext, useDraggable } from "@dnd-kit/core";
|
|
6
|
+
import { restrictToParentElement } from "@dnd-kit/modifiers";
|
|
7
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
8
|
+
import { Trans } from "@/lib/trans.js";
|
|
9
|
+
|
|
10
|
+
export interface AssetFocalPointEditorProps {
|
|
11
|
+
settingFocalPoint: boolean;
|
|
12
|
+
focalPoint: Point | undefined;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
onFocalPointChange: (point: Point) => void;
|
|
16
|
+
onCancel: () => void;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Point {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DraggableFocalPoint({ point }: { point: Point }) {
|
|
26
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
|
27
|
+
id: "focal-point",
|
|
28
|
+
});
|
|
29
|
+
const style = {
|
|
30
|
+
left: `${point.x * 100}%`,
|
|
31
|
+
top: `${point.y * 100}%`,
|
|
32
|
+
transform: isDragging ? `translate(-50%, -50%) ${CSS.Translate.toString(transform)}` : `translate(-50%, -50%)`,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
ref={setNodeRef}
|
|
38
|
+
style={style}
|
|
39
|
+
{...listeners}
|
|
40
|
+
{...attributes}
|
|
41
|
+
className="absolute w-8 h-8 rounded-full border-4 border-white bg-brand/20 shadow-lg cursor-move"
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function AssetFocalPointEditor({ settingFocalPoint, focalPoint, width, height, onFocalPointChange, onCancel, children }: AssetFocalPointEditorProps) {
|
|
47
|
+
|
|
48
|
+
const [focalPointCurrent, setFocalPointCurrent] = useState<Point>(focalPoint ?? { x: 0.5, y: 0.5 });
|
|
49
|
+
|
|
50
|
+
const handleDragEnd = (event: any) => {
|
|
51
|
+
const { delta } = event;
|
|
52
|
+
const newX = Math.max(0, Math.min(1, focalPointCurrent.x + delta.x / width));
|
|
53
|
+
const newY = Math.max(0, Math.min(1, focalPointCurrent.y + delta.y / height));
|
|
54
|
+
const newPoint = { x: newX, y: newY };
|
|
55
|
+
setFocalPointCurrent(newPoint);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cn(
|
|
61
|
+
'relative',
|
|
62
|
+
'flex items-center justify-center',
|
|
63
|
+
settingFocalPoint && 'cursor-crosshair',
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<div className="relative" style={{ width: `${width}px`, height: `${height}px` }}>
|
|
67
|
+
{children}
|
|
68
|
+
{settingFocalPoint && (
|
|
69
|
+
<DndContext onDragEnd={handleDragEnd} modifiers={[restrictToParentElement]}>
|
|
70
|
+
<DraggableFocalPoint
|
|
71
|
+
point={focalPointCurrent}
|
|
72
|
+
/>
|
|
73
|
+
</DndContext>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{settingFocalPoint && (
|
|
78
|
+
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
|
|
79
|
+
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
80
|
+
<X className="mr-2 h-4 w-4" />
|
|
81
|
+
<Trans>Cancel</Trans>
|
|
82
|
+
</Button>
|
|
83
|
+
<Button type="button" onClick={() => {
|
|
84
|
+
onFocalPointChange(focalPointCurrent);
|
|
85
|
+
}}>
|
|
86
|
+
<Crosshair className="mr-2 h-4 w-4" />
|
|
87
|
+
<Trans>Set Focal Point</Trans>
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -23,6 +23,7 @@ import { Loader2, Search, Upload, X } from 'lucide-react';
|
|
|
23
23
|
import { useCallback, useState } from 'react';
|
|
24
24
|
import { useDropzone } from 'react-dropzone';
|
|
25
25
|
import { useDebounce } from '@uidotdev/usehooks';
|
|
26
|
+
import { DetailPageButton } from '../detail-page-button.js';
|
|
26
27
|
|
|
27
28
|
const getAssetListDocument = graphql(
|
|
28
29
|
`
|
|
@@ -72,7 +73,14 @@ export type Asset = AssetFragment;
|
|
|
72
73
|
export interface AssetGalleryProps {
|
|
73
74
|
onSelect?: (assets: Asset[]) => void;
|
|
74
75
|
selectable?: boolean;
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* @description
|
|
78
|
+
* Defines whether multiple assets can be selected.
|
|
79
|
+
*
|
|
80
|
+
* If set to 'auto', the asset selection will be toggled when the user clicks on an asset.
|
|
81
|
+
* If set to 'manual', multiple selection will occur only if the user holds down the control/cmd key.
|
|
82
|
+
*/
|
|
83
|
+
multiSelect?: 'auto' | 'manual';
|
|
76
84
|
initialSelectedAssets?: Asset[];
|
|
77
85
|
pageSize?: number;
|
|
78
86
|
fixedHeight?: boolean;
|
|
@@ -84,7 +92,7 @@ export interface AssetGalleryProps {
|
|
|
84
92
|
export function AssetGallery({
|
|
85
93
|
onSelect,
|
|
86
94
|
selectable = true,
|
|
87
|
-
multiSelect =
|
|
95
|
+
multiSelect = undefined,
|
|
88
96
|
initialSelectedAssets = [],
|
|
89
97
|
pageSize = 24,
|
|
90
98
|
fixedHeight = false,
|
|
@@ -149,24 +157,44 @@ export function AssetGallery({
|
|
|
149
157
|
const totalPages = Math.ceil(totalItems / pageSize);
|
|
150
158
|
|
|
151
159
|
// Handle selection
|
|
152
|
-
const handleSelect = (asset: Asset) => {
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
const handleSelect = (asset: Asset, event: React.MouseEvent) => {
|
|
161
|
+
if (multiSelect === 'auto') {
|
|
162
|
+
const isSelected = selected.some(a => a.id === asset.id);
|
|
163
|
+
let newSelected: Asset[];
|
|
164
|
+
|
|
165
|
+
if (isSelected) {
|
|
166
|
+
newSelected = selected.filter(a => a.id !== asset.id);
|
|
167
|
+
} else {
|
|
168
|
+
newSelected = [...selected, asset];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setSelected(newSelected);
|
|
172
|
+
onSelect?.(newSelected);
|
|
156
173
|
return;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
|
-
const isSelected = selected.some(a => a.id === asset.id);
|
|
160
|
-
let newSelected: Asset[];
|
|
161
176
|
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
// Manual mode - check for modifier key
|
|
178
|
+
const isModifierKeyPressed = event.metaKey || event.ctrlKey;
|
|
179
|
+
|
|
180
|
+
if (multiSelect === 'manual' && isModifierKeyPressed) {
|
|
181
|
+
// Toggle selection
|
|
182
|
+
const isSelected = selected.some(a => a.id === asset.id);
|
|
183
|
+
let newSelected: Asset[];
|
|
184
|
+
|
|
185
|
+
if (isSelected) {
|
|
186
|
+
newSelected = selected.filter(a => a.id !== asset.id);
|
|
187
|
+
} else {
|
|
188
|
+
newSelected = [...selected, asset];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setSelected(newSelected);
|
|
192
|
+
onSelect?.(newSelected);
|
|
164
193
|
} else {
|
|
165
|
-
|
|
194
|
+
// No modifier key - single select
|
|
195
|
+
setSelected([asset]);
|
|
196
|
+
onSelect?.([asset]);
|
|
166
197
|
}
|
|
167
|
-
|
|
168
|
-
setSelected(newSelected);
|
|
169
|
-
onSelect?.(newSelected);
|
|
170
198
|
};
|
|
171
199
|
|
|
172
200
|
// Check if an asset is selected
|
|
@@ -272,7 +300,7 @@ export function AssetGallery({
|
|
|
272
300
|
${isSelected(asset as Asset) ? 'ring-2 ring-primary' : ''}
|
|
273
301
|
flex flex-col min-w-[120px]
|
|
274
302
|
`}
|
|
275
|
-
onClick={() => handleSelect(asset as Asset)}
|
|
303
|
+
onClick={(e) => handleSelect(asset as Asset, e)}
|
|
276
304
|
>
|
|
277
305
|
<div
|
|
278
306
|
className="relative w-full bg-muted/30"
|
|
@@ -296,11 +324,14 @@ export function AssetGallery({
|
|
|
296
324
|
<p className="text-xs line-clamp-2 min-h-[2.5rem]" title={asset.name}>
|
|
297
325
|
{asset.name}
|
|
298
326
|
</p>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
327
|
+
<div className='flex justify-between items-center'>
|
|
328
|
+
{asset.fileSize && (
|
|
329
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
330
|
+
{formatFileSize(asset.fileSize)}
|
|
331
|
+
</p>
|
|
332
|
+
)}
|
|
333
|
+
<DetailPageButton id={asset.id} label={<Trans>Edit</Trans>} />
|
|
334
|
+
</div>
|
|
304
335
|
</CardContent>
|
|
305
336
|
</Card>
|
|
306
337
|
))
|
package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx}
RENAMED
|
@@ -12,9 +12,7 @@ interface AssetPreviewDialogProps {
|
|
|
12
12
|
onOpenChange: (open: boolean) => void;
|
|
13
13
|
asset: AssetWithTags;
|
|
14
14
|
assets?: AssetWithTags[];
|
|
15
|
-
editable?: boolean;
|
|
16
15
|
customFields?: any[];
|
|
17
|
-
onAssetChange?: (asset: Partial<AssetWithTags>) => void;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function AssetPreviewDialog({
|
|
@@ -22,24 +20,20 @@ export function AssetPreviewDialog({
|
|
|
22
20
|
onOpenChange,
|
|
23
21
|
asset,
|
|
24
22
|
assets,
|
|
25
|
-
editable,
|
|
26
23
|
customFields,
|
|
27
|
-
onAssetChange,
|
|
28
24
|
}: AssetPreviewDialogProps) {
|
|
29
25
|
return (
|
|
30
26
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
31
27
|
<DialogContent className="sm:max-w-[800px] lg:max-w-[95vw] w-[95vw] p-0">
|
|
32
28
|
<DialogHeader className="p-6 pb-0">
|
|
33
29
|
<DialogTitle>Asset</DialogTitle>
|
|
34
|
-
<DialogDescription>
|
|
30
|
+
<DialogDescription>Preview of {asset.name}</DialogDescription>
|
|
35
31
|
</DialogHeader>
|
|
36
32
|
<div className="h-full p-6">
|
|
37
33
|
<AssetPreview
|
|
38
34
|
asset={asset}
|
|
39
35
|
assets={assets}
|
|
40
|
-
editable={editable}
|
|
41
36
|
customFields={customFields}
|
|
42
|
-
onAssetChange={onAssetChange}
|
|
43
37
|
/>
|
|
44
38
|
</div>
|
|
45
39
|
</DialogContent>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select.js";
|
|
4
|
+
import { PreviewPreset } from "./asset-preview.js";
|
|
5
|
+
|
|
6
|
+
export interface AssetPreviewSelectorProps {
|
|
7
|
+
size: PreviewPreset;
|
|
8
|
+
setSize: (size: PreviewPreset) => void;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AssetPreviewSelector({ size, setSize, width, height }: AssetPreviewSelectorProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center gap-2">
|
|
16
|
+
<Select value={size} onValueChange={value => setSize(value as PreviewPreset)}>
|
|
17
|
+
<SelectTrigger>
|
|
18
|
+
<SelectValue placeholder="Select size" />
|
|
19
|
+
</SelectTrigger>
|
|
20
|
+
<SelectContent>
|
|
21
|
+
<SelectItem value="tiny">Tiny</SelectItem>
|
|
22
|
+
<SelectItem value="thumb">Thumb</SelectItem>
|
|
23
|
+
<SelectItem value="small">Small</SelectItem>
|
|
24
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
25
|
+
<SelectItem value="large">Large</SelectItem>
|
|
26
|
+
<SelectItem value="full">Full Size</SelectItem>
|
|
27
|
+
</SelectContent>
|
|
28
|
+
</Select>
|
|
29
|
+
<p className="text-sm text-muted-foreground">
|
|
30
|
+
{width} x {height}
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { VendureImage } from '@/components/shared/vendure-image.js';
|
|
2
|
+
import { Button } from '@/components/ui/button.js';
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card.js';
|
|
4
|
+
import { AssetFragment } from '@/graphql/fragments.js';
|
|
5
|
+
import { cn } from '@/lib/utils.js';
|
|
6
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
7
|
+
import { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import { useForm } from 'react-hook-form';
|
|
9
|
+
import { AssetPreviewSelector } from './asset-preview-selector.js';
|
|
10
|
+
import { AssetProperties } from './asset-properties.js';
|
|
11
|
+
|
|
12
|
+
export type PreviewPreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | '';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export type AssetWithTags = AssetFragment & { tags?: { value: string }[] };
|
|
16
|
+
|
|
17
|
+
interface AssetPreviewProps {
|
|
18
|
+
asset: AssetWithTags;
|
|
19
|
+
assets?: AssetWithTags[];
|
|
20
|
+
customFields?: any[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AssetPreview({
|
|
24
|
+
asset,
|
|
25
|
+
assets,
|
|
26
|
+
customFields = [],
|
|
27
|
+
}: AssetPreviewProps) {
|
|
28
|
+
const [size, setSize] = useState<PreviewPreset>('medium');
|
|
29
|
+
const [width, setWidth] = useState(0);
|
|
30
|
+
const [height, setHeight] = useState(0);
|
|
31
|
+
const [centered, setCentered] = useState(true);
|
|
32
|
+
const [assetIndex, setAssetIndex] = useState(assets?.indexOf(asset) || 0);
|
|
33
|
+
|
|
34
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
35
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
const form = useForm({
|
|
38
|
+
defaultValues: {
|
|
39
|
+
name: asset.name,
|
|
40
|
+
tags: asset.tags?.map(t => t.value) || [],
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const activeAsset = assets?.[assetIndex] ?? asset;
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (assets?.length) {
|
|
47
|
+
const index = assets.findIndex(a => a.id === asset.id);
|
|
48
|
+
setAssetIndex(index === -1 ? 0 : index);
|
|
49
|
+
}
|
|
50
|
+
}, [assets, asset.id]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const handleResize = () => {
|
|
54
|
+
updateDimensions();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
window.addEventListener('resize', handleResize);
|
|
58
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const updateDimensions = () => {
|
|
62
|
+
if (!imageRef.current || !containerRef.current) return;
|
|
63
|
+
|
|
64
|
+
const img = imageRef.current;
|
|
65
|
+
const container = containerRef.current;
|
|
66
|
+
const imgWidth = img.naturalWidth;
|
|
67
|
+
const imgHeight = img.naturalHeight;
|
|
68
|
+
const containerWidth = container.offsetWidth;
|
|
69
|
+
const containerHeight = container.offsetHeight;
|
|
70
|
+
setWidth(imgWidth);
|
|
71
|
+
setHeight(imgHeight);
|
|
72
|
+
setCentered(imgWidth <= containerWidth && imgHeight <= containerHeight);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-4 h-full">
|
|
77
|
+
<div className="space-y-4">
|
|
78
|
+
<Card>
|
|
79
|
+
<CardContent className="pt-6 space-y-4">
|
|
80
|
+
<AssetProperties asset={activeAsset} />
|
|
81
|
+
<AssetPreviewSelector size={size} setSize={setSize} width={width} height={height} />
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="relative flex items-center justify-center bg-muted/30 rounded-lg">
|
|
87
|
+
{assets && assets.length > 1 && (
|
|
88
|
+
<>
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
size="icon"
|
|
92
|
+
className="absolute left-4 z-10"
|
|
93
|
+
onClick={() => setAssetIndex(i => i - 1)}
|
|
94
|
+
disabled={assetIndex === 0}
|
|
95
|
+
>
|
|
96
|
+
<ChevronLeft className="h-4 w-4" />
|
|
97
|
+
</Button>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="icon"
|
|
101
|
+
className="absolute right-4 z-10"
|
|
102
|
+
onClick={() => setAssetIndex(i => i + 1)}
|
|
103
|
+
disabled={assetIndex === assets.length - 1}
|
|
104
|
+
>
|
|
105
|
+
<ChevronRight className="h-4 w-4" />
|
|
106
|
+
</Button>
|
|
107
|
+
</>
|
|
108
|
+
)}
|
|
109
|
+
<div
|
|
110
|
+
ref={containerRef}
|
|
111
|
+
className={cn(
|
|
112
|
+
'relative',
|
|
113
|
+
centered && 'flex items-center justify-center',
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<VendureImage
|
|
117
|
+
ref={imageRef}
|
|
118
|
+
asset={activeAsset}
|
|
119
|
+
preset={size || undefined}
|
|
120
|
+
mode="resize"
|
|
121
|
+
onLoad={updateDimensions}
|
|
122
|
+
className="max-w-full max-h-full object-contain"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { formatFileSize } from "@/lib/utils.js";
|
|
2
|
+
|
|
3
|
+
import { Label } from "@/components/ui/label.js";
|
|
4
|
+
import { AssetFragment } from "@/graphql/fragments.js";
|
|
5
|
+
import { ExternalLink } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export interface AssetPropertiesProps {
|
|
8
|
+
asset: AssetFragment;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AssetProperties({ asset }: AssetPropertiesProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-4">
|
|
14
|
+
<div>
|
|
15
|
+
<Label>Name</Label>
|
|
16
|
+
<p className="truncate text-sm text-muted-foreground">{asset.name}</p>
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<Label>Source File</Label>
|
|
20
|
+
<a
|
|
21
|
+
href={asset.source}
|
|
22
|
+
target="_blank"
|
|
23
|
+
rel="noopener noreferrer"
|
|
24
|
+
className="text-sm text-primary hover:underline"
|
|
25
|
+
>
|
|
26
|
+
{asset.source.split('/').pop()}
|
|
27
|
+
<ExternalLink className="ml-1 h-3 w-3 inline" />
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<Label>File Size</Label>
|
|
33
|
+
<p className="text-sm text-muted-foreground">
|
|
34
|
+
{formatFileSize(asset.fileSize)}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div>
|
|
39
|
+
<Label>Dimensions</Label>
|
|
40
|
+
<p className="text-sm text-muted-foreground">
|
|
41
|
+
{asset.width} x {asset.height}
|
|
42
|
+
</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -22,9 +22,10 @@ type CustomFieldConfig = ResultOf<typeof customFieldConfigFragment>;
|
|
|
22
22
|
interface CustomFieldsFormProps {
|
|
23
23
|
entityType: string;
|
|
24
24
|
control: Control<any, any>;
|
|
25
|
+
formPathPrefix?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps) {
|
|
28
|
+
export function CustomFieldsForm({ entityType, control, formPathPrefix }: CustomFieldsFormProps) {
|
|
28
29
|
const {
|
|
29
30
|
settings: { displayLanguage },
|
|
30
31
|
} = useUserSettings();
|
|
@@ -39,7 +40,7 @@ export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps)
|
|
|
39
40
|
{fieldDef.type === 'localeString' || fieldDef.type === 'localeText' ? (
|
|
40
41
|
<TranslatableFormField
|
|
41
42
|
control={control}
|
|
42
|
-
name={`customFields.${fieldDef.name}`}
|
|
43
|
+
name={formPathPrefix ? `${formPathPrefix}.customFields.${fieldDef.name}` : `customFields.${fieldDef.name}`}
|
|
43
44
|
render={({ field }) => (
|
|
44
45
|
<FormItem>
|
|
45
46
|
<FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
|
|
@@ -52,7 +53,7 @@ export function CustomFieldsForm({ entityType, control }: CustomFieldsFormProps)
|
|
|
52
53
|
) : (
|
|
53
54
|
<FormField
|
|
54
55
|
control={control}
|
|
55
|
-
name={`customFields.${fieldDef.name}`}
|
|
56
|
+
name={formPathPrefix ? `${formPathPrefix}.customFields.${fieldDef.name}` : `customFields.${fieldDef.name}`}
|
|
56
57
|
render={({ field }) => (
|
|
57
58
|
<FormItem>
|
|
58
59
|
<FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
|