@vendure/dashboard 3.4.3-master-202509120226 → 3.4.3-master-202509190229
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/package.json +11 -7
- package/src/app/common/duplicate-bulk-action.tsx +37 -23
- package/src/app/common/duplicate-entity-dialog.tsx +117 -0
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +21 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +30 -2
- package/src/app/routes/_authenticated/_products/products.graphql.ts +25 -16
- package/src/lib/components/data-input/rich-text-input.tsx +2 -115
- package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +223 -0
- package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +151 -0
- package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +439 -0
- package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +338 -0
- package/src/lib/components/shared/rich-text-editor/table-delete-menu.tsx +104 -0
- package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +225 -0
- package/src/lib/graphql/common-operations.ts +19 -0
- package/src/lib/graphql/fragments.ts +23 -13
- package/src/lib/hooks/use-floating-bulk-actions.ts +3 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.4.3-master-
|
|
4
|
+
"version": "3.4.3-master-202509190229",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -94,14 +94,18 @@
|
|
|
94
94
|
"@tanstack/react-table": "^8.21.2",
|
|
95
95
|
"@tanstack/router-devtools": "^1.105.0",
|
|
96
96
|
"@tanstack/router-plugin": "^1.105.0",
|
|
97
|
-
"@tiptap/
|
|
98
|
-
"@tiptap/
|
|
99
|
-
"@tiptap/
|
|
97
|
+
"@tiptap/extension-floating-menu": "^3.4.4",
|
|
98
|
+
"@tiptap/extension-image": "^3.4.4",
|
|
99
|
+
"@tiptap/extension-table": "^3.4.4",
|
|
100
|
+
"@tiptap/extension-text-style": "^3.4.4",
|
|
101
|
+
"@tiptap/pm": "^3.4.4",
|
|
102
|
+
"@tiptap/react": "^3.4.4",
|
|
103
|
+
"@tiptap/starter-kit": "^3.4.4",
|
|
100
104
|
"@types/react": "^19.0.10",
|
|
101
105
|
"@types/react-dom": "^19.0.4",
|
|
102
106
|
"@uidotdev/usehooks": "^2.4.1",
|
|
103
|
-
"@vendure/common": "^3.4.3-master-
|
|
104
|
-
"@vendure/core": "^3.4.3-master-
|
|
107
|
+
"@vendure/common": "^3.4.3-master-202509190229",
|
|
108
|
+
"@vendure/core": "^3.4.3-master-202509190229",
|
|
105
109
|
"@vitejs/plugin-react": "^4.3.4",
|
|
106
110
|
"acorn": "^8.11.3",
|
|
107
111
|
"acorn-walk": "^8.3.2",
|
|
@@ -152,5 +156,5 @@
|
|
|
152
156
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
153
157
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
154
158
|
},
|
|
155
|
-
"gitHead": "
|
|
159
|
+
"gitHead": "1a94626a2e07d33d881a751ff394f8351c3521b4"
|
|
156
160
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
|
|
2
3
|
import { CopyIcon } from 'lucide-react';
|
|
3
4
|
import { useState } from 'react';
|
|
4
5
|
import { toast } from 'sonner';
|
|
@@ -8,13 +9,13 @@ import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-ta
|
|
|
8
9
|
import { api } from '@/vdb/graphql/api.js';
|
|
9
10
|
import { duplicateEntityDocument } from '@/vdb/graphql/common-operations.js';
|
|
10
11
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
12
|
+
import { DuplicateEntityDialog } from './duplicate-entity-dialog.js';
|
|
11
13
|
|
|
12
14
|
interface DuplicateBulkActionProps {
|
|
13
15
|
entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
|
|
14
16
|
duplicatorCode: string;
|
|
15
|
-
duplicatorArguments?: Array<{ name: string; value: string }>;
|
|
16
17
|
requiredPermissions: string[];
|
|
17
|
-
entityName: string;
|
|
18
|
+
entityName: string;
|
|
18
19
|
onSuccess?: () => void;
|
|
19
20
|
selection: any[];
|
|
20
21
|
table: any;
|
|
@@ -23,23 +24,28 @@ interface DuplicateBulkActionProps {
|
|
|
23
24
|
export function DuplicateBulkAction({
|
|
24
25
|
entityType,
|
|
25
26
|
duplicatorCode,
|
|
26
|
-
duplicatorArguments = [],
|
|
27
27
|
requiredPermissions,
|
|
28
28
|
entityName,
|
|
29
29
|
onSuccess,
|
|
30
30
|
selection,
|
|
31
31
|
table,
|
|
32
|
-
}: DuplicateBulkActionProps) {
|
|
32
|
+
}: Readonly<DuplicateBulkActionProps>) {
|
|
33
33
|
const { refetchPaginatedList } = usePaginatedList();
|
|
34
34
|
const { i18n } = useLingui();
|
|
35
35
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
|
36
36
|
const [progress, setProgress] = useState({ completed: 0, total: 0 });
|
|
37
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
37
38
|
|
|
38
39
|
const { mutateAsync } = useMutation({
|
|
39
40
|
mutationFn: api.mutate(duplicateEntityDocument),
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
+
const handleStartDuplication = () => {
|
|
44
|
+
if (isDuplicating) return;
|
|
45
|
+
setDialogOpen(true);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleConfirmDuplication = async (duplicatorInput: ConfigurableOperationInput) => {
|
|
43
49
|
if (isDuplicating) return;
|
|
44
50
|
|
|
45
51
|
setIsDuplicating(true);
|
|
@@ -61,10 +67,7 @@ export function DuplicateBulkAction({
|
|
|
61
67
|
input: {
|
|
62
68
|
entityName: entityType,
|
|
63
69
|
entityId: entity.id,
|
|
64
|
-
duplicatorInput
|
|
65
|
-
code: duplicatorCode,
|
|
66
|
-
arguments: duplicatorArguments,
|
|
67
|
-
},
|
|
70
|
+
duplicatorInput,
|
|
68
71
|
},
|
|
69
72
|
});
|
|
70
73
|
|
|
@@ -116,19 +119,30 @@ export function DuplicateBulkAction({
|
|
|
116
119
|
};
|
|
117
120
|
|
|
118
121
|
return (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
<>
|
|
123
|
+
<DataTableBulkActionItem
|
|
124
|
+
requiresPermission={requiredPermissions}
|
|
125
|
+
onClick={handleStartDuplication}
|
|
126
|
+
label={
|
|
127
|
+
isDuplicating ? (
|
|
128
|
+
<Trans>
|
|
129
|
+
Duplicating... ({progress.completed}/{progress.total})
|
|
130
|
+
</Trans>
|
|
131
|
+
) : (
|
|
132
|
+
<Trans>Duplicate</Trans>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
icon={CopyIcon}
|
|
136
|
+
/>
|
|
137
|
+
<DuplicateEntityDialog
|
|
138
|
+
open={dialogOpen}
|
|
139
|
+
onOpenChange={setDialogOpen}
|
|
140
|
+
entityType={entityType}
|
|
141
|
+
entityName={entityName}
|
|
142
|
+
entities={selection}
|
|
143
|
+
duplicatorCode={duplicatorCode}
|
|
144
|
+
onConfirm={handleConfirmDuplication}
|
|
145
|
+
/>
|
|
146
|
+
</>
|
|
133
147
|
);
|
|
134
148
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ConfigurableOperationInput as ConfigurableOperationInputComponent } from '@/vdb/components/shared/configurable-operation-input.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
10
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
11
|
+
import { getEntityDuplicatorsDocument } from '@/vdb/graphql/common-operations.js';
|
|
12
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
13
|
+
import { useQuery } from '@tanstack/react-query';
|
|
14
|
+
import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
|
|
15
|
+
import React, { useState } from 'react';
|
|
16
|
+
|
|
17
|
+
interface DuplicateEntityDialogProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onOpenChange: (open: boolean) => void;
|
|
20
|
+
entityType: 'Product' | 'Collection' | 'Facet' | 'Promotion';
|
|
21
|
+
entityName: string;
|
|
22
|
+
entities: Array<{ id: string; name?: string }>;
|
|
23
|
+
duplicatorCode: string;
|
|
24
|
+
onConfirm: (duplicatorInput: ConfigurableOperationInput) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DuplicateEntityDialog({
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
entityType,
|
|
31
|
+
entityName,
|
|
32
|
+
duplicatorCode,
|
|
33
|
+
onConfirm,
|
|
34
|
+
}: Readonly<DuplicateEntityDialogProps>) {
|
|
35
|
+
const [selectedDuplicator, setSelectedDuplicator] = useState<ConfigurableOperationInput | undefined>();
|
|
36
|
+
|
|
37
|
+
const { data } = useQuery({
|
|
38
|
+
queryKey: ['entityDuplicators'],
|
|
39
|
+
queryFn: () => api.query(getEntityDuplicatorsDocument),
|
|
40
|
+
staleTime: 1000 * 60 * 60 * 5,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Find the duplicator that matches the provided code and supports this entity type
|
|
44
|
+
const matchingDuplicator = data?.entityDuplicators?.find(
|
|
45
|
+
duplicator => duplicator.code === duplicatorCode && duplicator.forEntities.includes(entityType),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Auto-initialize the duplicator when found
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
if (matchingDuplicator && !selectedDuplicator) {
|
|
51
|
+
setSelectedDuplicator({
|
|
52
|
+
code: matchingDuplicator.code,
|
|
53
|
+
arguments:
|
|
54
|
+
matchingDuplicator.args?.map(arg => ({
|
|
55
|
+
name: arg.name,
|
|
56
|
+
value: arg.defaultValue != null ? arg.defaultValue.toString() : '',
|
|
57
|
+
})) || [],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}, [matchingDuplicator, selectedDuplicator]);
|
|
61
|
+
|
|
62
|
+
const onDuplicatorValueChange = (newVal: ConfigurableOperationInput) => {
|
|
63
|
+
setSelectedDuplicator(newVal);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleConfirm = () => {
|
|
67
|
+
if (!selectedDuplicator) return;
|
|
68
|
+
onConfirm(selectedDuplicator);
|
|
69
|
+
onOpenChange(false);
|
|
70
|
+
setSelectedDuplicator(undefined);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleCancel = () => {
|
|
74
|
+
onOpenChange(false);
|
|
75
|
+
setSelectedDuplicator(undefined);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
80
|
+
<DialogContent className="sm:max-w-lg">
|
|
81
|
+
<DialogHeader>
|
|
82
|
+
<DialogTitle>
|
|
83
|
+
<Trans>Duplicate {entityName.toLowerCase()}s</Trans>
|
|
84
|
+
</DialogTitle>
|
|
85
|
+
</DialogHeader>
|
|
86
|
+
|
|
87
|
+
<div className="space-y-4">
|
|
88
|
+
{selectedDuplicator && matchingDuplicator && (
|
|
89
|
+
<ConfigurableOperationInputComponent
|
|
90
|
+
operationDefinition={matchingDuplicator}
|
|
91
|
+
value={selectedDuplicator}
|
|
92
|
+
onChange={onDuplicatorValueChange}
|
|
93
|
+
removable={false}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{!matchingDuplicator && (
|
|
98
|
+
<div className="text-sm text-muted-foreground">
|
|
99
|
+
<Trans>
|
|
100
|
+
No duplicator found with code "{duplicatorCode}" for {entityName}s
|
|
101
|
+
</Trans>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<DialogFooter>
|
|
107
|
+
<Button variant="outline" onClick={handleCancel}>
|
|
108
|
+
<Trans>Cancel</Trans>
|
|
109
|
+
</Button>
|
|
110
|
+
<Button onClick={handleConfirm} disabled={!selectedDuplicator}>
|
|
111
|
+
<Trans>Duplicate</Trans>
|
|
112
|
+
</Button>
|
|
113
|
+
</DialogFooter>
|
|
114
|
+
</DialogContent>
|
|
115
|
+
</Dialog>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ResultOf } from 'gql.tada';
|
|
|
13
13
|
import { Folder, FolderOpen, FolderTreeIcon, PlusIcon } from 'lucide-react';
|
|
14
14
|
import { useState } from 'react';
|
|
15
15
|
|
|
16
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
16
17
|
import { collectionListDocument, deleteCollectionDocument } from './collections.graphql.js';
|
|
17
18
|
import {
|
|
18
19
|
AssignCollectionsToChannelBulkAction,
|
|
@@ -150,6 +151,26 @@ function CollectionListPage() {
|
|
|
150
151
|
);
|
|
151
152
|
},
|
|
152
153
|
},
|
|
154
|
+
children: {
|
|
155
|
+
cell: ({ row }) => {
|
|
156
|
+
const children = row.original.children ?? [];
|
|
157
|
+
const count = children.length;
|
|
158
|
+
const maxDisplay = 5;
|
|
159
|
+
const leftOver = Math.max(count - maxDisplay, 0);
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex flex-wrap gap-2">
|
|
162
|
+
{children.slice(0, maxDisplay).map(child => (
|
|
163
|
+
<Badge variant="outline">{child.name}</Badge>
|
|
164
|
+
))}
|
|
165
|
+
{leftOver > 0 ? (
|
|
166
|
+
<Badge variant="outline">
|
|
167
|
+
<Trans>+ {leftOver} more</Trans>
|
|
168
|
+
</Badge>
|
|
169
|
+
) : null}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
153
174
|
}}
|
|
154
175
|
defaultColumnOrder={[
|
|
155
176
|
'featuredAsset',
|
|
@@ -9,6 +9,12 @@ import { graphql } from '@/vdb/graphql/graphql.js';
|
|
|
9
9
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
10
10
|
import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
|
|
11
11
|
import { useState } from 'react';
|
|
12
|
+
import {
|
|
13
|
+
AssignFacetValuesToProductVariantsBulkAction,
|
|
14
|
+
AssignProductVariantsToChannelBulkAction,
|
|
15
|
+
DeleteProductVariantsBulkAction,
|
|
16
|
+
RemoveProductVariantsFromChannelBulkAction,
|
|
17
|
+
} from '../../_product-variants/components/product-variant-bulk-actions.js';
|
|
12
18
|
import { productVariantListDocument } from '../products.graphql.js';
|
|
13
19
|
|
|
14
20
|
export const deleteProductVariantDocument = graphql(`
|
|
@@ -47,9 +53,31 @@ export function ProductVariantsTable({
|
|
|
47
53
|
productId,
|
|
48
54
|
})}
|
|
49
55
|
defaultVisibility={{
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
featuredAsset: true,
|
|
57
|
+
name: true,
|
|
58
|
+
enabled: true,
|
|
59
|
+
price: true,
|
|
60
|
+
priceWithTax: true,
|
|
61
|
+
stockLevels: true,
|
|
52
62
|
}}
|
|
63
|
+
bulkActions={[
|
|
64
|
+
{
|
|
65
|
+
component: AssignProductVariantsToChannelBulkAction,
|
|
66
|
+
order: 100,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
component: RemoveProductVariantsFromChannelBulkAction,
|
|
70
|
+
order: 200,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
component: AssignFacetValuesToProductVariantsBulkAction,
|
|
74
|
+
order: 300,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
component: DeleteProductVariantsBulkAction,
|
|
78
|
+
order: 400,
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
53
81
|
customizeColumns={{
|
|
54
82
|
name: {
|
|
55
83
|
header: 'Variant name',
|
|
@@ -61,25 +61,34 @@ export const productDetailFragment = graphql(
|
|
|
61
61
|
[assetFragment],
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
-
export const productVariantListDocument = graphql(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
export const productVariantListDocument = graphql(
|
|
65
|
+
`
|
|
66
|
+
query ProductVariantList($options: ProductVariantListOptions, $productId: ID) {
|
|
67
|
+
productVariants(options: $options, productId: $productId) {
|
|
68
|
+
items {
|
|
69
|
+
id
|
|
70
|
+
createdAt
|
|
71
|
+
updatedAt
|
|
72
|
+
featuredAsset {
|
|
73
|
+
...Asset
|
|
74
|
+
}
|
|
75
|
+
name
|
|
76
|
+
sku
|
|
77
|
+
enabled
|
|
78
|
+
currencyCode
|
|
79
|
+
price
|
|
80
|
+
priceWithTax
|
|
81
|
+
stockLevels {
|
|
82
|
+
stockOnHand
|
|
83
|
+
stockAllocated
|
|
84
|
+
}
|
|
77
85
|
}
|
|
86
|
+
totalItems
|
|
78
87
|
}
|
|
79
|
-
totalItems
|
|
80
88
|
}
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
`,
|
|
90
|
+
[assetFragment],
|
|
91
|
+
);
|
|
83
92
|
|
|
84
93
|
export const productDetailDocument = graphql(
|
|
85
94
|
`
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
|
|
2
2
|
import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
|
|
3
|
-
import
|
|
4
|
-
import { BubbleMenu, Editor, EditorContent, useEditor } from '@tiptap/react';
|
|
5
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
6
|
-
import { BoldIcon, ItalicIcon, StrikethroughIcon } from 'lucide-react';
|
|
7
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
8
|
-
import { Button } from '../ui/button.js';
|
|
9
|
-
|
|
10
|
-
// define your extension array
|
|
11
|
-
const extensions = [
|
|
12
|
-
TextStyle.configure(),
|
|
13
|
-
StarterKit.configure({
|
|
14
|
-
bulletList: {
|
|
15
|
-
keepMarks: true,
|
|
16
|
-
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
|
17
|
-
},
|
|
18
|
-
orderedList: {
|
|
19
|
-
keepMarks: true,
|
|
20
|
-
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
|
21
|
-
},
|
|
22
|
-
}),
|
|
23
|
-
];
|
|
3
|
+
import { RichTextEditor } from '../shared/rich-text-editor/rich-text-editor.js';
|
|
24
4
|
|
|
25
5
|
/**
|
|
26
6
|
* @description
|
|
@@ -31,99 +11,6 @@ const extensions = [
|
|
|
31
11
|
*/
|
|
32
12
|
export function RichTextInput({ value, onChange, fieldDef }: Readonly<DashboardFormComponentProps>) {
|
|
33
13
|
const readOnly = isReadonlyField(fieldDef);
|
|
34
|
-
const isInternalUpdate = useRef(false);
|
|
35
|
-
|
|
36
|
-
const editor = useEditor({
|
|
37
|
-
parseOptions: {
|
|
38
|
-
preserveWhitespace: 'full',
|
|
39
|
-
},
|
|
40
|
-
extensions: extensions,
|
|
41
|
-
content: value,
|
|
42
|
-
editable: !readOnly,
|
|
43
|
-
onUpdate: ({ editor }) => {
|
|
44
|
-
if (!readOnly) {
|
|
45
|
-
isInternalUpdate.current = true;
|
|
46
|
-
console.log('onUpdate');
|
|
47
|
-
const newValue = editor.getHTML();
|
|
48
|
-
if (value !== newValue) {
|
|
49
|
-
onChange(newValue);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
editorProps: {
|
|
54
|
-
attributes: {
|
|
55
|
-
class: `border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/10 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[500px] overflow-y-auto ${readOnly ? 'cursor-not-allowed opacity-50' : ''}`,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
useLayoutEffect(() => {
|
|
61
|
-
if (editor && !isInternalUpdate.current) {
|
|
62
|
-
const currentContent = editor.getHTML();
|
|
63
|
-
if (currentContent !== value) {
|
|
64
|
-
const { from, to } = editor.state.selection;
|
|
65
|
-
editor.commands.setContent(value, false);
|
|
66
|
-
editor.commands.setTextSelection({ from, to });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
isInternalUpdate.current = false;
|
|
70
|
-
}, [value, editor]);
|
|
71
|
-
|
|
72
|
-
// Update editor's editable state when disabled prop changes
|
|
73
|
-
useLayoutEffect(() => {
|
|
74
|
-
if (editor) {
|
|
75
|
-
editor.setEditable(!readOnly, false);
|
|
76
|
-
}
|
|
77
|
-
}, [readOnly, editor]);
|
|
78
|
-
|
|
79
|
-
if (!editor) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<>
|
|
85
|
-
<EditorContent editor={editor} />
|
|
86
|
-
<CustomBubbleMenu editor={editor} disabled={readOnly} />
|
|
87
|
-
</>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
14
|
|
|
91
|
-
|
|
92
|
-
if (!editor || disabled) return null;
|
|
93
|
-
return (
|
|
94
|
-
<BubbleMenu editor={editor}>
|
|
95
|
-
<div className="flex items-center gap-2 bg-background p-2 rounded-md border">
|
|
96
|
-
<Button
|
|
97
|
-
type="button"
|
|
98
|
-
variant="ghost"
|
|
99
|
-
size="icon"
|
|
100
|
-
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
101
|
-
className={editor.isActive('bold') ? 'bg-accent' : ''}
|
|
102
|
-
disabled={disabled}
|
|
103
|
-
>
|
|
104
|
-
<BoldIcon className="w-4 h-4" />
|
|
105
|
-
</Button>
|
|
106
|
-
<Button
|
|
107
|
-
type="button"
|
|
108
|
-
variant="ghost"
|
|
109
|
-
size="icon"
|
|
110
|
-
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
111
|
-
className={editor.isActive('italic') ? 'bg-accent' : ''}
|
|
112
|
-
disabled={disabled}
|
|
113
|
-
>
|
|
114
|
-
<ItalicIcon className="w-4 h-4" />
|
|
115
|
-
</Button>
|
|
116
|
-
<Button
|
|
117
|
-
type="button"
|
|
118
|
-
variant="ghost"
|
|
119
|
-
size="icon"
|
|
120
|
-
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
121
|
-
className={editor.isActive('strike') ? 'bg-accent' : ''}
|
|
122
|
-
disabled={disabled}
|
|
123
|
-
>
|
|
124
|
-
<StrikethroughIcon className="w-4 h-4" />
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
127
|
-
</BubbleMenu>
|
|
128
|
-
);
|
|
15
|
+
return <RichTextEditor value={value} onChange={onChange} disabled={readOnly} />;
|
|
129
16
|
}
|