@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.
Files changed (54) hide show
  1. package/dist/vite/vite-plugin-config.js +1 -0
  2. package/package.json +11 -7
  3. package/src/app/common/duplicate-bulk-action.tsx +37 -23
  4. package/src/app/common/duplicate-entity-dialog.tsx +117 -0
  5. package/src/app/routes/_authenticated/_administrators/administrators.tsx +1 -2
  6. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +39 -0
  7. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +18 -7
  8. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +206 -0
  9. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +226 -0
  10. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +217 -0
  11. package/src/app/routes/_authenticated/_channels/channels.tsx +1 -2
  12. package/src/app/routes/_authenticated/_collections/collections.tsx +2 -16
  13. package/src/app/routes/_authenticated/_countries/countries.tsx +1 -2
  14. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +1 -2
  15. package/src/app/routes/_authenticated/_customers/customers.tsx +1 -2
  16. package/src/app/routes/_authenticated/_facets/facets.tsx +0 -1
  17. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +1 -2
  18. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +1 -2
  19. package/src/app/routes/_authenticated/_products/products.tsx +1 -2
  20. package/src/app/routes/_authenticated/_promotions/promotions.tsx +1 -2
  21. package/src/app/routes/_authenticated/_roles/roles.tsx +1 -2
  22. package/src/app/routes/_authenticated/_sellers/sellers.tsx +1 -2
  23. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +1 -2
  24. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +1 -2
  25. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +1 -2
  26. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +1 -2
  27. package/src/app/routes/_authenticated/_zones/zones.tsx +1 -2
  28. package/src/lib/components/data-input/rich-text-input.tsx +2 -115
  29. package/src/lib/components/data-table/data-table-bulk-actions.tsx +5 -14
  30. package/src/lib/components/data-table/use-all-bulk-actions.ts +19 -0
  31. package/src/lib/components/data-table/use-generated-columns.tsx +12 -3
  32. package/src/lib/components/layout/nav-main.tsx +50 -25
  33. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  34. package/src/lib/components/shared/asset/asset-gallery.tsx +83 -50
  35. package/src/lib/components/shared/paginated-list-data-table.tsx +1 -0
  36. package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +223 -0
  37. package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +151 -0
  38. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +439 -0
  39. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +338 -0
  40. package/src/lib/components/shared/rich-text-editor/table-delete-menu.tsx +104 -0
  41. package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +225 -0
  42. package/src/lib/components/shared/vendure-image.tsx +9 -1
  43. package/src/lib/framework/defaults.ts +24 -0
  44. package/src/lib/framework/extension-api/types/navigation.ts +8 -0
  45. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +26 -0
  46. package/src/lib/framework/page/list-page.tsx +7 -0
  47. package/src/lib/graphql/common-operations.ts +19 -0
  48. package/src/lib/graphql/fragments.ts +23 -13
  49. package/src/lib/hooks/use-custom-field-config.ts +19 -2
  50. package/src/lib/index.ts +0 -1
  51. package/src/lib/providers/channel-provider.tsx +22 -6
  52. package/src/lib/providers/server-config.tsx +1 -0
  53. package/src/app/routes/_authenticated/_collections/components/move-single-collection.tsx +0 -33
  54. 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 { deleteSellerDocument, sellerListQuery } from './sellers.graphql.js';
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 { deleteShippingMethodDocument, shippingMethodListQuery } from './shipping-methods.graphql.js';
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 { deleteStockLocationDocument, stockLocationListQuery } from './stock-locations.graphql.js';
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 { deleteTaxCategoryDocument, taxCategoryListQuery } from './tax-categories.graphql.js';
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 { deleteTaxRateDocument, taxRateListQuery } from './tax-rates.graphql.js';
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 { deleteZoneDocument, zoneListQuery } from './zones.graphql.js';
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 TextStyle from '@tiptap/extension-text-style';
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
- function CustomBubbleMenu({ editor, disabled }: { editor: Editor | null; disabled?: boolean }) {
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 client';
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 { pageId } = usePage();
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 ? currentPath.replace(new RegExp(`^${escapeRegexChars(basePath)}`), '') : currentPath;
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('/') ? normalizedCurrentPath : `/${normalizedCurrentPath}`;
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
- ? { ...section, items: section.items?.slice().sort(sortByOrder) }
110
- : section,
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
- <SidebarGroup className="mt-auto">
291
- <SidebarGroupLabel>Administration</SidebarGroupLabel>
292
- <SidebarMenu>{bottomSections.map(renderBottomSection)}</SidebarMenu>
293
- </SidebarGroup>
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
  }
@@ -17,7 +17,7 @@ export interface AssetFocalPointEditorProps {
17
17
  children?: React.ReactNode;
18
18
  }
19
19
 
20
- interface Point {
20
+ export interface Point {
21
21
  x: number;
22
22
  y: number;
23
23
  }