@vendure/dashboard 3.4.3-master-202509180227 → 3.4.3-master-202509200226
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/vite/vite-plugin-config.js +1 -0
- 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/_administrators/administrators.tsx +1 -2
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
- package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
- package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
- package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
- package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
- package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
- package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
- package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
- package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
- package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
- package/src/app/routes/_authenticated/_products/products.tsx +1 -2
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
- package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
- package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
- package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
- package/src/lib/components/data-input/rich-text-input.tsx +2 -115
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
- package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
- package/src/lib/components/layout/nav-main.tsx +50 -25
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
- package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
- package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
- 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/components/shared/vendure-image.tsx +9 -1
- package/src/lib/framework/defaults.ts +24 -0
- package/src/lib/framework/extension-api/types/navigation.ts +8 -0
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
- package/src/lib/framework/page/list-page.tsx +7 -0
- package/src/lib/graphql/common-operations.ts +19 -0
- package/src/lib/graphql/fragments.ts +23 -13
- package/src/lib/hooks/use-custom-field-config.ts +19 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/providers/channel-provider.tsx +22 -6
- package/src/lib/providers/server-config.tsx +1 -0
- package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
- package/src/lib/components/shared/asset/focal-point-control.tsx +0 -57
|
@@ -7,7 +7,7 @@ import { Trans } from '@/vdb/lib/trans.js';
|
|
|
7
7
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
8
8
|
import { PlusIcon } from 'lucide-react';
|
|
9
9
|
import { DeleteSellersBulkAction } from './components/seller-bulk-actions.js';
|
|
10
|
-
import {
|
|
10
|
+
import { sellerListQuery } from './sellers.graphql.js';
|
|
11
11
|
|
|
12
12
|
export const Route = createFileRoute('/_authenticated/_sellers/sellers')({
|
|
13
13
|
component: SellerListPage,
|
|
@@ -19,7 +19,6 @@ function SellerListPage() {
|
|
|
19
19
|
<ListPage
|
|
20
20
|
pageId="seller-list"
|
|
21
21
|
listQuery={sellerListQuery}
|
|
22
|
-
deleteMutation={deleteSellerDocument}
|
|
23
22
|
route={Route}
|
|
24
23
|
title="Sellers"
|
|
25
24
|
defaultVisibility={{
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
RemoveShippingMethodsFromChannelBulkAction,
|
|
13
13
|
} from './components/shipping-method-bulk-actions.js';
|
|
14
14
|
import { TestShippingMethodDialog } from './components/test-shipping-method-dialog.js';
|
|
15
|
-
import {
|
|
15
|
+
import { shippingMethodListQuery } from './shipping-methods.graphql.js';
|
|
16
16
|
|
|
17
17
|
export const Route = createFileRoute('/_authenticated/_shipping-methods/shipping-methods')({
|
|
18
18
|
component: ShippingMethodListPage,
|
|
@@ -24,7 +24,6 @@ function ShippingMethodListPage() {
|
|
|
24
24
|
<ListPage
|
|
25
25
|
pageId="shipping-method-list"
|
|
26
26
|
listQuery={shippingMethodListQuery}
|
|
27
|
-
deleteMutation={deleteShippingMethodDocument}
|
|
28
27
|
route={Route}
|
|
29
28
|
title="Shipping Methods"
|
|
30
29
|
defaultVisibility={{
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
DeleteStockLocationsBulkAction,
|
|
12
12
|
RemoveStockLocationsFromChannelBulkAction,
|
|
13
13
|
} from './components/stock-location-bulk-actions.js';
|
|
14
|
-
import {
|
|
14
|
+
import { stockLocationListQuery } from './stock-locations.graphql.js';
|
|
15
15
|
|
|
16
16
|
export const Route = createFileRoute('/_authenticated/_stock-locations/stock-locations')({
|
|
17
17
|
component: StockLocationListPage,
|
|
@@ -24,7 +24,6 @@ function StockLocationListPage() {
|
|
|
24
24
|
pageId="stock-location-list"
|
|
25
25
|
title="Stock Locations"
|
|
26
26
|
listQuery={stockLocationListQuery}
|
|
27
|
-
deleteMutation={deleteStockLocationDocument}
|
|
28
27
|
route={Route}
|
|
29
28
|
customizeColumns={{
|
|
30
29
|
name: {
|
|
@@ -8,7 +8,7 @@ import { Trans } from '@/vdb/lib/trans.js';
|
|
|
8
8
|
import { createFileRoute, Link } from '@tanstack/react-router';
|
|
9
9
|
import { PlusIcon } from 'lucide-react';
|
|
10
10
|
import { DeleteTaxCategoriesBulkAction } from './components/tax-category-bulk-actions.js';
|
|
11
|
-
import {
|
|
11
|
+
import { taxCategoryListQuery } from './tax-categories.graphql.js';
|
|
12
12
|
|
|
13
13
|
export const Route = createFileRoute('/_authenticated/_tax-categories/tax-categories')({
|
|
14
14
|
component: TaxCategoryListPage,
|
|
@@ -20,7 +20,6 @@ function TaxCategoryListPage() {
|
|
|
20
20
|
<ListPage
|
|
21
21
|
pageId="tax-category-list"
|
|
22
22
|
listQuery={taxCategoryListQuery}
|
|
23
|
-
deleteMutation={deleteTaxCategoryDocument}
|
|
24
23
|
route={Route}
|
|
25
24
|
title="Tax Categories"
|
|
26
25
|
defaultVisibility={{
|
|
@@ -11,7 +11,7 @@ import { PlusIcon } from 'lucide-react';
|
|
|
11
11
|
import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
|
|
12
12
|
import { zoneListQuery } from '../_zones/zones.graphql.js';
|
|
13
13
|
import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
|
|
14
|
-
import {
|
|
14
|
+
import { taxRateListQuery } from './tax-rates.graphql.js';
|
|
15
15
|
|
|
16
16
|
export const Route = createFileRoute('/_authenticated/_tax-rates/tax-rates')({
|
|
17
17
|
component: TaxRateListPage,
|
|
@@ -23,7 +23,6 @@ function TaxRateListPage() {
|
|
|
23
23
|
<ListPage
|
|
24
24
|
pageId="tax-rate-list"
|
|
25
25
|
listQuery={taxRateListQuery}
|
|
26
|
-
deleteMutation={deleteTaxRateDocument}
|
|
27
26
|
route={Route}
|
|
28
27
|
title="Tax Rates"
|
|
29
28
|
defaultVisibility={{
|
|
@@ -8,7 +8,7 @@ import { createFileRoute, Link } from '@tanstack/react-router';
|
|
|
8
8
|
import { PlusIcon } from 'lucide-react';
|
|
9
9
|
import { DeleteZonesBulkAction } from './components/zone-bulk-actions.js';
|
|
10
10
|
import { ZoneCountriesSheet } from './components/zone-countries-sheet.js';
|
|
11
|
-
import {
|
|
11
|
+
import { zoneListQuery } from './zones.graphql.js';
|
|
12
12
|
|
|
13
13
|
export const Route = createFileRoute('/_authenticated/_zones/zones')({
|
|
14
14
|
component: ZoneListPage,
|
|
@@ -20,7 +20,6 @@ function ZoneListPage() {
|
|
|
20
20
|
<ListPage
|
|
21
21
|
pageId="zone-list"
|
|
22
22
|
listQuery={zoneListQuery}
|
|
23
|
-
deleteMutation={deleteZoneDocument}
|
|
24
23
|
route={Route}
|
|
25
24
|
title="Zones"
|
|
26
25
|
defaultVisibility={{
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
'use
|
|
2
|
-
|
|
1
|
+
import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
|
|
3
2
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
3
|
import {
|
|
5
4
|
DropdownMenu,
|
|
@@ -7,11 +6,8 @@ import {
|
|
|
7
6
|
DropdownMenuItem,
|
|
8
7
|
DropdownMenuTrigger,
|
|
9
8
|
} from '@/vdb/components/ui/dropdown-menu.js';
|
|
10
|
-
import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
|
|
11
9
|
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
12
10
|
import { useFloatingBulkActions } from '@/vdb/hooks/use-floating-bulk-actions.js';
|
|
13
|
-
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
14
|
-
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
15
11
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
16
12
|
import { Table } from '@tanstack/react-table';
|
|
17
13
|
import { ChevronDown } from 'lucide-react';
|
|
@@ -26,9 +22,7 @@ export function DataTableBulkActions<TData>({
|
|
|
26
22
|
table,
|
|
27
23
|
bulkActions,
|
|
28
24
|
}: Readonly<DataTableBulkActionsProps<TData>>) {
|
|
29
|
-
const
|
|
30
|
-
const pageBlock = usePageBlock();
|
|
31
|
-
const blockId = pageBlock?.blockId;
|
|
25
|
+
const allBulkActions = useAllBulkActions(bulkActions);
|
|
32
26
|
|
|
33
27
|
// Cache to store selected items across page changes
|
|
34
28
|
const selectedItemsCache = useRef<Map<string, TData>>(new Map());
|
|
@@ -63,18 +57,15 @@ export function DataTableBulkActions<TData>({
|
|
|
63
57
|
if (!shouldShow) {
|
|
64
58
|
return null;
|
|
65
59
|
}
|
|
66
|
-
const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
|
|
67
|
-
const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
|
|
68
|
-
allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
|
|
69
60
|
|
|
70
61
|
return (
|
|
71
62
|
<div
|
|
72
63
|
className="flex items-center gap-4 px-8 py-2 animate-in fade-in duration-200 fixed transform -translate-x-1/2 bg-white shadow-2xl rounded-md border z-50"
|
|
73
|
-
style={{
|
|
74
|
-
height: 'auto',
|
|
64
|
+
style={{
|
|
65
|
+
height: 'auto',
|
|
75
66
|
maxHeight: '60px',
|
|
76
67
|
bottom: position.bottom,
|
|
77
|
-
left: position.left
|
|
68
|
+
left: position.left,
|
|
78
69
|
}}
|
|
79
70
|
>
|
|
80
71
|
<span className="text-sm text-muted-foreground">
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getBulkActions } from '@/vdb/framework/data-table/data-table-extensions.js';
|
|
2
|
+
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
3
|
+
import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
|
|
4
|
+
import { usePage } from '@/vdb/hooks/use-page.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Augments the provided Bulk Actions with any user-defined actions for the current
|
|
9
|
+
* page & block, and returns all of the bulk actions sorted by the `order` property.
|
|
10
|
+
*/
|
|
11
|
+
export function useAllBulkActions(bulkActions: BulkAction[]): BulkAction[] {
|
|
12
|
+
const { pageId } = usePage();
|
|
13
|
+
const pageBlock = usePageBlock();
|
|
14
|
+
const blockId = pageBlock?.blockId;
|
|
15
|
+
const extendedBulkActions = pageId ? getBulkActions(pageId, blockId) : [];
|
|
16
|
+
const allBulkActions = [...extendedBulkActions, ...(bulkActions ?? [])];
|
|
17
|
+
allBulkActions.sort((a, b) => (a.order ?? 10_000) - (b.order ?? 10_000));
|
|
18
|
+
return allBulkActions;
|
|
19
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { useAllBulkActions } from '@/vdb/components/data-table/use-all-bulk-actions.js';
|
|
1
2
|
import { DisplayComponent } from '@/vdb/framework/component-registry/display-component.js';
|
|
2
3
|
import {
|
|
3
4
|
FieldInfo,
|
|
4
5
|
getOperationVariablesFields,
|
|
5
6
|
getTypeFieldInfo,
|
|
6
7
|
} from '@/vdb/framework/document-introspection/get-document-structure.js';
|
|
8
|
+
import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
|
|
7
9
|
import { api } from '@/vdb/graphql/api.js';
|
|
8
10
|
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
9
11
|
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
|
|
@@ -51,6 +53,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
51
53
|
fields,
|
|
52
54
|
customizeColumns,
|
|
53
55
|
rowActions,
|
|
56
|
+
bulkActions,
|
|
54
57
|
deleteMutation,
|
|
55
58
|
additionalColumns,
|
|
56
59
|
defaultColumnOrder,
|
|
@@ -62,6 +65,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
62
65
|
fields: FieldInfo[];
|
|
63
66
|
customizeColumns?: CustomizeColumnConfig<T>;
|
|
64
67
|
rowActions?: RowAction<PaginatedListItemFields<T>>[];
|
|
68
|
+
bulkActions?: BulkAction[];
|
|
65
69
|
deleteMutation?: TypedDocumentNode<any, any>;
|
|
66
70
|
additionalColumns?: AdditionalColumns<T>;
|
|
67
71
|
defaultColumnOrder?: Array<string | number | symbol>;
|
|
@@ -71,6 +75,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
71
75
|
enableSorting?: boolean;
|
|
72
76
|
}>) {
|
|
73
77
|
const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
|
|
78
|
+
const allBulkActions = useAllBulkActions(bulkActions ?? []);
|
|
74
79
|
|
|
75
80
|
const { columns, customFieldColumnNames } = useMemo(() => {
|
|
76
81
|
const columnConfigs: Array<{ fieldInfo: FieldInfo; isCustomField: boolean }> = [];
|
|
@@ -169,8 +174,8 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
169
174
|
finalColumns = [...orderedColumns, ...remainingColumns];
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
if (includeActionsColumn && (rowActions || deleteMutation)) {
|
|
173
|
-
const rowActionColumn = getRowActions(rowActions, deleteMutation);
|
|
177
|
+
if (includeActionsColumn && (rowActions || deleteMutation || bulkActions)) {
|
|
178
|
+
const rowActionColumn = getRowActions(rowActions, deleteMutation, allBulkActions);
|
|
174
179
|
if (rowActionColumn) {
|
|
175
180
|
finalColumns.push(rowActionColumn);
|
|
176
181
|
}
|
|
@@ -212,13 +217,14 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
|
|
|
212
217
|
function getRowActions(
|
|
213
218
|
rowActions?: RowAction<any>[],
|
|
214
219
|
deleteMutation?: TypedDocumentNode<any, any>,
|
|
220
|
+
bulkActions?: BulkAction[],
|
|
215
221
|
): AccessorKeyColumnDef<any> | undefined {
|
|
216
222
|
return {
|
|
217
223
|
id: 'actions',
|
|
218
224
|
accessorKey: 'actions',
|
|
219
225
|
header: () => <Trans>Actions</Trans>,
|
|
220
226
|
enableColumnFilter: false,
|
|
221
|
-
cell: ({ row }) => {
|
|
227
|
+
cell: ({ row, table }) => {
|
|
222
228
|
return (
|
|
223
229
|
<DropdownMenu>
|
|
224
230
|
<DropdownMenuTrigger asChild>
|
|
@@ -235,6 +241,9 @@ function getRowActions(
|
|
|
235
241
|
{action.label}
|
|
236
242
|
</DropdownMenuItem>
|
|
237
243
|
))}
|
|
244
|
+
{bulkActions?.map((action, index) => (
|
|
245
|
+
<action.component key={`bulk-action-${index}`} selection={[row]} table={table} />
|
|
246
|
+
))}
|
|
238
247
|
{deleteMutation && (
|
|
239
248
|
<DeleteMutationRowAction deleteMutation={deleteMutation} row={row} />
|
|
240
249
|
)}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
NavMenuSection,
|
|
15
15
|
NavMenuSectionPlacement,
|
|
16
16
|
} from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
|
|
17
|
+
import { usePermissions } from '@/vdb/hooks/use-permissions.js';
|
|
17
18
|
import { Link, useRouter, useRouterState } from '@tanstack/react-router';
|
|
18
19
|
import { ChevronRight } from 'lucide-react';
|
|
19
20
|
import * as React from 'react';
|
|
@@ -39,6 +40,7 @@ function escapeRegexChars(str: string): string {
|
|
|
39
40
|
export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavMenuItem> }>) {
|
|
40
41
|
const router = useRouter();
|
|
41
42
|
const routerState = useRouterState();
|
|
43
|
+
const { hasPermissions } = usePermissions();
|
|
42
44
|
const currentPath = routerState.location.pathname;
|
|
43
45
|
const basePath = router.basepath || '';
|
|
44
46
|
|
|
@@ -46,16 +48,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
46
48
|
const isPathActive = React.useCallback(
|
|
47
49
|
(itemUrl: string) => {
|
|
48
50
|
// Remove basepath prefix from current path for comparison
|
|
49
|
-
const normalizedCurrentPath = basePath
|
|
50
|
-
|
|
51
|
+
const normalizedCurrentPath = basePath
|
|
52
|
+
? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '')
|
|
53
|
+
: currentPath;
|
|
54
|
+
|
|
51
55
|
// Ensure normalized path starts with /
|
|
52
|
-
const cleanPath = normalizedCurrentPath.startsWith('/')
|
|
53
|
-
|
|
56
|
+
const cleanPath = normalizedCurrentPath.startsWith('/')
|
|
57
|
+
? normalizedCurrentPath
|
|
58
|
+
: `/${normalizedCurrentPath}`;
|
|
59
|
+
|
|
54
60
|
// Special handling for root path
|
|
55
61
|
if (itemUrl === '/') {
|
|
56
62
|
return cleanPath === '/' || cleanPath === '';
|
|
57
63
|
}
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
// For other paths, check exact match or prefix match
|
|
60
66
|
return cleanPath === itemUrl || cleanPath.startsWith(`${itemUrl}/`);
|
|
61
67
|
},
|
|
@@ -97,6 +103,20 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
97
103
|
return activeTopSections;
|
|
98
104
|
});
|
|
99
105
|
|
|
106
|
+
// Helper to check if an item is allowed based on permissions
|
|
107
|
+
const isItemAllowed = React.useCallback(
|
|
108
|
+
(item: NavMenuItem) => {
|
|
109
|
+
if (!item.requiresPermission) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const permissions = Array.isArray(item.requiresPermission)
|
|
113
|
+
? item.requiresPermission
|
|
114
|
+
: [item.requiresPermission];
|
|
115
|
+
return hasPermissions(permissions);
|
|
116
|
+
},
|
|
117
|
+
[hasPermissions],
|
|
118
|
+
);
|
|
119
|
+
|
|
100
120
|
// Helper to build a sorted list of sections for a given placement, memoized for stability
|
|
101
121
|
const getSortedSections = React.useCallback(
|
|
102
122
|
(placement: NavMenuSectionPlacement) => {
|
|
@@ -104,13 +124,24 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
104
124
|
.filter(item => item.placement === placement)
|
|
105
125
|
.slice()
|
|
106
126
|
.sort(sortByOrder)
|
|
107
|
-
.map(section =>
|
|
108
|
-
'items' in section
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
.map(section => {
|
|
128
|
+
if ('items' in section) {
|
|
129
|
+
// Filter items based on permissions
|
|
130
|
+
const allowedItems = (section.items ?? []).filter(isItemAllowed).sort(sortByOrder);
|
|
131
|
+
return { ...section, items: allowedItems };
|
|
132
|
+
}
|
|
133
|
+
return section;
|
|
134
|
+
})
|
|
135
|
+
.filter(section => {
|
|
136
|
+
// Drop sections that have no items after permission filtering
|
|
137
|
+
if ('items' in section) {
|
|
138
|
+
return section.items && section.items.length > 0;
|
|
139
|
+
}
|
|
140
|
+
// For single items, check if they're allowed
|
|
141
|
+
return isItemAllowed(section as NavMenuItem);
|
|
142
|
+
});
|
|
112
143
|
},
|
|
113
|
-
[items],
|
|
144
|
+
[items, isItemAllowed],
|
|
114
145
|
);
|
|
115
146
|
|
|
116
147
|
const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
|
|
@@ -154,11 +185,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
154
185
|
return (
|
|
155
186
|
<NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
|
|
156
187
|
<SidebarMenuItem>
|
|
157
|
-
<SidebarMenuButton
|
|
158
|
-
tooltip={item.title}
|
|
159
|
-
asChild
|
|
160
|
-
isActive={isPathActive(item.url)}
|
|
161
|
-
>
|
|
188
|
+
<SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
|
|
162
189
|
<Link to={item.url}>
|
|
163
190
|
{item.icon && <item.icon />}
|
|
164
191
|
<span>{item.title}</span>
|
|
@@ -220,11 +247,7 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
220
247
|
return (
|
|
221
248
|
<NavItemWrapper key={item.title} locationId={item.id} order={item.order} offset={true}>
|
|
222
249
|
<SidebarMenuItem>
|
|
223
|
-
<SidebarMenuButton
|
|
224
|
-
tooltip={item.title}
|
|
225
|
-
asChild
|
|
226
|
-
isActive={isPathActive(item.url)}
|
|
227
|
-
>
|
|
250
|
+
<SidebarMenuButton tooltip={item.title} asChild isActive={isPathActive(item.url)}>
|
|
228
251
|
<Link to={item.url}>
|
|
229
252
|
{item.icon && <item.icon />}
|
|
230
253
|
<span>{item.title}</span>
|
|
@@ -287,10 +310,12 @@ export function NavMain({ items }: Readonly<{ items: Array<NavMenuSection | NavM
|
|
|
287
310
|
</SidebarGroup>
|
|
288
311
|
|
|
289
312
|
{/* Bottom sections - will be pushed to the bottom by CSS */}
|
|
290
|
-
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
313
|
+
{bottomSections.length ? (
|
|
314
|
+
<SidebarGroup className="mt-auto">
|
|
315
|
+
<SidebarGroupLabel>Administration</SidebarGroupLabel>
|
|
316
|
+
<SidebarMenu>{bottomSections.map(renderBottomSection)}</SidebarMenu>
|
|
317
|
+
</SidebarGroup>
|
|
318
|
+
) : null}
|
|
294
319
|
</>
|
|
295
320
|
);
|
|
296
321
|
}
|