Просмотр исходного кода

refactor(dashboard): Reorganize directory structure

Michael Bromley 10 месяцев назад
Родитель
Сommit
2a9902b43c
46 измененных файлов с 291 добавлено и 319 удалено
  1. 2 2
      packages/dashboard/src/components/data-table/data-table-column-header.tsx
  2. 0 0
      packages/dashboard/src/components/data-table/data-table-filter-dialog.tsx
  3. 0 0
      packages/dashboard/src/components/data-table/data-table-pagination.tsx
  4. 0 0
      packages/dashboard/src/components/data-table/data-table-view-options.tsx
  5. 2 2
      packages/dashboard/src/components/data-table/data-table.tsx
  6. 0 0
      packages/dashboard/src/components/data-type-components/asset.tsx
  7. 1 1
      packages/dashboard/src/components/data-type-components/boolean.tsx
  8. 0 0
      packages/dashboard/src/components/data-type-components/date-time.tsx
  9. 13 7
      packages/dashboard/src/components/layout/app-sidebar.tsx
  10. 0 0
      packages/dashboard/src/components/layout/content-language-selector.tsx
  11. 0 0
      packages/dashboard/src/components/layout/generated-breadcrumbs.tsx
  12. 3 3
      packages/dashboard/src/components/layout/nav-main.tsx
  13. 3 3
      packages/dashboard/src/components/layout/nav-user.tsx
  14. 2 2
      packages/dashboard/src/components/login/login-form.tsx
  15. 11 21
      packages/dashboard/src/components/shared/asset-gallery.tsx
  16. 0 0
      packages/dashboard/src/components/shared/asset-picker-dialog.tsx
  17. 8 2
      packages/dashboard/src/components/shared/asset-preview-dialog.tsx
  18. 12 39
      packages/dashboard/src/components/shared/asset-preview.tsx
  19. 23 38
      packages/dashboard/src/components/shared/entity-assets.tsx
  20. 0 0
      packages/dashboard/src/components/shared/focal-point-control.tsx
  21. 0 0
      packages/dashboard/src/components/shared/translatable-form-field.tsx
  22. 117 0
      packages/dashboard/src/components/shared/vendure-image.tsx
  23. 0 120
      packages/dashboard/src/components/vendure-image.tsx
  24. 3 3
      packages/dashboard/src/framework/component-registry/component-registry.tsx
  25. 1 1
      packages/dashboard/src/framework/defaults.ts
  26. 0 0
      packages/dashboard/src/framework/document-introspection/get-document-structure.ts
  27. 1 4
      packages/dashboard/src/framework/document-introspection/hooks.ts
  28. 3 3
      packages/dashboard/src/framework/extension-api/define-dashboard-extension.ts
  29. 2 2
      packages/dashboard/src/framework/extension-api/extension-api-types.ts
  30. 1 1
      packages/dashboard/src/framework/extension-api/use-dashboard-extensions.ts
  31. 1 1
      packages/dashboard/src/framework/form-engine/form-schema-tools.ts
  32. 20 18
      packages/dashboard/src/framework/form-engine/use-generated-form.tsx
  33. 0 0
      packages/dashboard/src/framework/nav-menu/nav-menu.ts
  34. 2 2
      packages/dashboard/src/framework/page/detail-page.tsx
  35. 6 6
      packages/dashboard/src/framework/page/list-page.tsx
  36. 1 1
      packages/dashboard/src/framework/page/page-api.ts
  37. 0 0
      packages/dashboard/src/framework/page/page-types.ts
  38. 0 0
      packages/dashboard/src/framework/page/page.tsx
  39. 3 3
      packages/dashboard/src/framework/page/use-extended-router.tsx
  40. 2 2
      packages/dashboard/src/index.ts
  41. 13 0
      packages/dashboard/src/lib/utils.ts
  42. 3 3
      packages/dashboard/src/main.tsx
  43. 2 2
      packages/dashboard/src/routes/_authenticated.tsx
  44. 1 1
      packages/dashboard/src/routes/_authenticated/products.tsx
  45. 28 25
      packages/dashboard/src/routes/_authenticated/products_.$id.tsx
  46. 1 1
      packages/dashboard/src/routes/login.tsx

+ 2 - 2
packages/dashboard/src/framework/internal/data-table/data-table-column-header.tsx → packages/dashboard/src/components/data-table/data-table-column-header.tsx

@@ -13,8 +13,8 @@ import {
     DialogTitle,
     DialogTrigger,
 } from '@/components/ui/dialog.js';
-import { DataTableFilterDialog } from '@/framework/internal/data-table/data-table-filter-dialog.js';
-import { FieldInfo } from '@/framework/internal/document-introspection/get-document-structure.js';
+import { DataTableFilterDialog } from '@/components/data-table/data-table-filter-dialog.js';
+import { FieldInfo } from '@/framework/document-introspection/get-document-structure.js';
 import { camelCaseToTitleCase } from '@/lib/utils.js';
 import { Trans } from '@lingui/react/macro';
 import { ColumnDef, HeaderContext } from '@tanstack/table-core';

+ 0 - 0
packages/dashboard/src/framework/internal/data-table/data-table-filter-dialog.tsx → packages/dashboard/src/components/data-table/data-table-filter-dialog.tsx


+ 0 - 0
packages/dashboard/src/framework/internal/data-table/data-table-pagination.tsx → packages/dashboard/src/components/data-table/data-table-pagination.tsx


+ 0 - 0
packages/dashboard/src/framework/internal/data-table/data-table-view-options.tsx → packages/dashboard/src/components/data-table/data-table-view-options.tsx


+ 2 - 2
packages/dashboard/src/framework/internal/data-table/data-table.tsx → packages/dashboard/src/components/data-table/data-table.tsx

@@ -3,8 +3,8 @@
 import { Badge } from '@/components/ui/badge.js';
 import { Input } from '@/components/ui/input.js';
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table.js';
-import { DataTablePagination } from '@/framework/internal/data-table/data-table-pagination.js';
-import { DataTableViewOptions } from '@/framework/internal/data-table/data-table-view-options.js';
+import { DataTablePagination } from '@/components/data-table/data-table-pagination.js';
+import { DataTableViewOptions } from '@/components/data-table/data-table-view-options.js';
 
 import {
     ColumnDef,

+ 0 - 0
packages/dashboard/src/framework/internal/component-registry/data-types/asset.tsx → packages/dashboard/src/components/data-type-components/asset.tsx


+ 1 - 1
packages/dashboard/src/framework/internal/component-registry/data-types/boolean.tsx → packages/dashboard/src/components/data-type-components/boolean.tsx

@@ -1,4 +1,4 @@
-import { CheckIcon, XIcon, LucideIcon } from 'lucide-react';
+import { CheckIcon, XIcon } from 'lucide-react';
 
 export function BooleanDisplayCheckbox({ value }: { value: boolean }) {
     return value ? <CheckIcon className="opacity-70" /> : <XIcon className="opacity-70" />;

+ 0 - 0
packages/dashboard/src/framework/internal/component-registry/data-types/date-time.tsx → packages/dashboard/src/components/data-type-components/date-time.tsx


+ 13 - 7
packages/dashboard/src/components/app-sidebar.tsx → packages/dashboard/src/components/layout/app-sidebar.tsx

@@ -1,10 +1,16 @@
-import { NavMain } from '@/components/nav-main';
-import { NavProjects } from '@/components/nav-projects';
-import { NavUser } from '@/components/nav-user';
-import { TeamSwitcher } from '@/components/team-switcher';
-import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from '@/components/ui/sidebar';
-import { getNavMenuConfig } from '@/framework/internal/nav-menu/nav-menu.js';
-import { useDashboardExtensions } from '@/framework/internal/extension-api/use-dashboard-extensions.js';
+import { NavMain } from '@/components/layout/nav-main.js';
+import { NavProjects } from '@/components/layout/nav-projects.js';
+import { NavUser } from '@/components/layout/nav-user.js';
+import { TeamSwitcher } from '@/components/layout/team-switcher.js';
+import {
+    Sidebar,
+    SidebarContent,
+    SidebarFooter,
+    SidebarHeader,
+    SidebarRail,
+} from '@/components/ui/sidebar.js';
+import { getNavMenuConfig } from '@/framework/nav-menu/nav-menu.js';
+import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
 import { AudioWaveform, Command, Frame, GalleryVerticalEnd, Map, PieChart } from 'lucide-react';
 import * as React from 'react';
 

+ 0 - 0
packages/dashboard/src/components/content-language-selector.tsx → packages/dashboard/src/components/layout/content-language-selector.tsx


+ 0 - 0
packages/dashboard/src/components/generated-breadcrumbs.tsx → packages/dashboard/src/components/layout/generated-breadcrumbs.tsx


+ 3 - 3
packages/dashboard/src/components/nav-main.tsx → packages/dashboard/src/components/layout/nav-main.tsx

@@ -1,4 +1,4 @@
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible.js';
 import {
     SidebarGroup,
     SidebarGroupLabel,
@@ -8,8 +8,8 @@ import {
     SidebarMenuSub,
     SidebarMenuSubButton,
     SidebarMenuSubItem,
-} from '@/components/ui/sidebar';
-import { NavMenuSection } from '@/framework/internal/nav-menu/nav-menu.js';
+} from '@/components/ui/sidebar.js';
+import { NavMenuSection } from '@/framework/nav-menu/nav-menu.js';
 import { Link, rootRouteId, useLocation, useMatch } from '@tanstack/react-router';
 import { ChevronRight } from 'lucide-react';
 

+ 3 - 3
packages/dashboard/src/components/nav-user.tsx → packages/dashboard/src/components/layout/nav-user.tsx

@@ -5,7 +5,7 @@ import { Route } from '@/routes/_authenticated.js';
 import { useRouter } from '@tanstack/react-router';
 import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Sparkles } from 'lucide-react';
 
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar.js';
 import {
     DropdownMenu,
     DropdownMenuContent,
@@ -14,8 +14,8 @@ import {
     DropdownMenuLabel,
     DropdownMenuSeparator,
     DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar';
+} from '@/components/ui/dropdown-menu.js';
+import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar.js';
 
 export function NavUser({
     user,

+ 2 - 2
packages/dashboard/src/components/login-form.tsx → packages/dashboard/src/components/login/login-form.tsx

@@ -8,11 +8,11 @@ import { Label } from '@/components/ui/label.js';
 import { AlertCircle, Loader2 } from 'lucide-react';
 import * as React from 'react';
 import { uiConfig } from 'virtual:vendure-ui-config';
-import { LogoMark } from './shared/logo-mark.js';
+import { LogoMark } from '../shared/logo-mark.js';
 import { z } from 'zod';
 import { useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form.js';
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form.js';
 import { toast } from 'sonner';
 
 export interface LoginFormProps extends React.ComponentProps<'div'> {

+ 11 - 21
packages/dashboard/src/components/asset-gallery.tsx → packages/dashboard/src/components/shared/asset-gallery.tsx

@@ -1,13 +1,7 @@
-import React, { useState, useEffect } from 'react';
-import { useQuery } from '@tanstack/react-query';
-import { graphql } from '@/graphql/graphql.js';
-import { VendureImage } from '@/components/vendure-image.js';
-import { api } from '@/graphql/api.js';
-import { useDebounce } from 'use-debounce';
+import { Button } from '@/components/ui/button.js';
 import { Card, CardContent } from '@/components/ui/card.js';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
+import { Checkbox } from '@/components/ui/checkbox.js';
 import { Input } from '@/components/ui/input.js';
-import { Button } from '@/components/ui/button.js';
 import {
     Pagination,
     PaginationContent,
@@ -17,20 +11,16 @@ import {
     PaginationNext,
     PaginationPrevious,
 } from '@/components/ui/pagination.js';
-import { Checkbox } from '@/components/ui/checkbox.js';
-import { Loader2, X, Search } from 'lucide-react';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
+import { VendureImage } from '@/components/shared/vendure-image.js';
+import { api } from '@/graphql/api.js';
 import { AssetFragment } from '@/graphql/fragments.js';
-
-// Helper function to format file size
-function formatFileSize(bytes: number): string {
-    if (bytes === 0) return '0 Bytes';
-
-    const k = 1024;
-    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-}
+import { graphql } from '@/graphql/graphql.js';
+import { formatFileSize } from '@/lib/utils.js';
+import { useQuery } from '@tanstack/react-query';
+import { Loader2, Search, X } from 'lucide-react';
+import React, { useState } from 'react';
+import { useDebounce } from 'use-debounce';
 
 const getAssetListDocument = graphql(`
     query GetAssetList($options: AssetListOptions) {

+ 0 - 0
packages/dashboard/src/components/asset-picker-dialog.tsx → packages/dashboard/src/components/shared/asset-picker-dialog.tsx


+ 8 - 2
packages/dashboard/src/components/asset-preview-dialog.tsx → packages/dashboard/src/components/shared/asset-preview-dialog.tsx

@@ -1,6 +1,12 @@
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog.js';
+import {
+    Dialog,
+    DialogContent,
+    DialogHeader,
+    DialogTitle,
+    DialogDescription,
+} from '@/components/ui/dialog.js';
 import { Asset, AssetPreview } from './asset-preview.js';
-import { SheetDescription } from './ui/sheet.js';
+import { SheetDescription } from '../ui/sheet.js';
 
 interface AssetPreviewDialogProps {
     open: boolean;

+ 12 - 39
packages/dashboard/src/components/asset-preview.tsx → packages/dashboard/src/components/shared/asset-preview.tsx

@@ -1,25 +1,16 @@
-import { useEffect, useRef, useState } from 'react';
 import { Button } from '@/components/ui/button.js';
 import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
+import { FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form.js';
 import { Input } from '@/components/ui/input.js';
 import { Label } from '@/components/ui/label.js';
-import { Separator } from '@/components/ui/separator.js';
-import { Form, FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form.js';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
+import { VendureImage } from '@/components/shared/vendure-image.js';
+import { AssetFragment } from '@/graphql/fragments.js';
+import { cn, formatFileSize } from '@/lib/utils.js';
+import { ChevronLeft, ChevronRight, Crosshair, Edit, ExternalLink, X } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
 import { useForm } from 'react-hook-form';
-import { VendureImage } from '@/components/vendure-image.js';
-import {
-    ChevronLeft,
-    ChevronRight,
-    Edit,
-    ExternalLink,
-    Tags,
-    X,
-    Crosshair
-} from 'lucide-react';
-import { cn } from '@/lib/utils.js';
 import { FocalPointControl } from './focal-point-control.js';
-import { AssetFragment } from '@/graphql/fragments.js';
 
 export type PreviewPreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | '';
 
@@ -28,7 +19,7 @@ interface Point {
     y: number;
 }
 
-export type Asset = AssetFragment;
+export type Asset = AssetFragment & { tags?: { value: string }[] };
 
 interface AssetPreviewProps {
     asset: Asset;
@@ -168,11 +159,7 @@ export function AssetPreview({
                 <Card>
                     <CardContent className="space-y-4 pt-6">
                         {!editable && onEditClick && (
-                            <Button
-                                variant="ghost"
-                                className="w-full justify-start"
-                                onClick={onEditClick}
-                            >
+                            <Button variant="ghost" className="w-full justify-start" onClick={onEditClick}>
                                 <Edit className="mr-2 h-4 w-4" />
                                 Edit
                                 <ChevronRight className="ml-auto h-4 w-4" />
@@ -195,9 +182,7 @@ export function AssetPreview({
                         ) : (
                             <div>
                                 <Label>Name</Label>
-                                <p className="truncate text-sm text-muted-foreground">
-                                    {activeAsset.name}
-                                </p>
+                                <p className="truncate text-sm text-muted-foreground">{activeAsset.name}</p>
                             </div>
                         )}
 
@@ -319,13 +304,10 @@ export function AssetPreview({
                     className={cn(
                         'relative',
                         centered && 'flex items-center justify-center',
-                        settingFocalPoint && 'cursor-crosshair'
+                        settingFocalPoint && 'cursor-crosshair',
                     )}
                 >
-                    <div
-                        className="relative"
-                        style={{ width: `${width}px`, height: `${height}px` }}
-                    >
+                    <div className="relative" style={{ width: `${width}px`, height: `${height}px` }}>
                         <VendureImage
                             ref={imageRef}
                             asset={activeAsset}
@@ -361,12 +343,3 @@ export function AssetPreview({
         </div>
     );
 }
-
-// Helper function to format file size
-function formatFileSize(bytes: number): string {
-    if (bytes === 0) return '0 Bytes';
-    const k = 1024;
-    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
-}

+ 23 - 38
packages/dashboard/src/components/entity-assets.tsx → packages/dashboard/src/components/shared/entity-assets.tsx

@@ -1,34 +1,33 @@
-import React, { useState, useCallback, useEffect, useRef } from 'react';
-import { EllipsisIcon } from 'lucide-react';
+import { Button } from '@/components/ui/button.js';
+import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu.js';
+import { AssetFragment } from '@/graphql/fragments.js';
 import {
-    DndContext,
     closestCenter,
+    DndContext,
+    DragEndEvent,
     KeyboardSensor,
     PointerSensor,
     useSensor,
     useSensors,
-    DragEndEvent,
 } from '@dnd-kit/core';
 import {
     arrayMove,
+    horizontalListSortingStrategy,
     SortableContext,
     sortableKeyboardCoordinates,
     useSortable,
-    horizontalListSortingStrategy,
 } from '@dnd-kit/sortable';
 import { CSS } from '@dnd-kit/utilities';
-import {
-    DropdownMenu,
-    DropdownMenuContent,
-    DropdownMenuItem,
-    DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu.js';
-import { Button } from '@/components/ui/button.js';
-import { ImageIcon, PaperclipIcon } from 'lucide-react';
-import { VendureImage } from './vendure-image.js';
+import { EllipsisIcon, ImageIcon, PaperclipIcon } from 'lucide-react';
+import React, { useCallback, useEffect, useState } from 'react';
 import { AssetPickerDialog } from './asset-picker-dialog.js';
 import { AssetPreviewDialog } from './asset-preview-dialog.js';
-import { AssetFragment } from '@/graphql/fragments.js';
+import { VendureImage } from './vendure-image.js';
 
 type Asset = AssetFragment;
 
@@ -82,19 +81,15 @@ function SortableAsset({
             <div
                 {...listeners}
                 className={`
-                    flex items-center justify-center 
-                    ${compact ? 'w-12 h-12' : 'w-16 h-16'} 
+                    flex items-center justify-center
+                    ${compact ? 'w-12 h-12' : 'w-16 h-16'}
                     border rounded-md overflow-hidden cursor-grab
                     ${isFeatured ? 'border-primary ring-1 ring-primary/30' : 'border-border'}
                     ${updatePermissions ? 'hover:border-muted-foreground' : ''}
                     ${isDragging ? 'opacity-50 cursor-grabbing' : ''}
                 `}
             >
-                <VendureImage 
-                    asset={asset} 
-                    mode="crop" 
-                    preset="tiny" 
-                />
+                <VendureImage asset={asset} mode="crop" preset="tiny" />
             </div>
 
             {/* Menu Trigger */}
@@ -102,28 +97,20 @@ function SortableAsset({
                 <div className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 transition-opacity">
                     <DropdownMenu>
                         <DropdownMenuTrigger asChild>
-                            <Button 
-                                variant="secondary" 
-                                size="icon" 
+                            <Button
+                                variant="secondary"
+                                size="icon"
                                 className="h-6 w-6 rounded-full shadow-md"
                             >
                                 <EllipsisIcon className="h-4 w-4" />
                             </Button>
                         </DropdownMenuTrigger>
                         <DropdownMenuContent align="end">
-                            <DropdownMenuItem onClick={() => onPreview(asset)}>
-                                Preview
-                            </DropdownMenuItem>
-                            <DropdownMenuItem
-                                disabled={isFeatured}
-                                onClick={() => onSetAsFeatured(asset)}
-                            >
+                            <DropdownMenuItem onClick={() => onPreview(asset)}>Preview</DropdownMenuItem>
+                            <DropdownMenuItem disabled={isFeatured} onClick={() => onSetAsFeatured(asset)}>
                                 Set as featured asset
                             </DropdownMenuItem>
-                            <DropdownMenuItem
-                                className="text-destructive"
-                                onClick={() => onRemove(asset)}
-                            >
+                            <DropdownMenuItem className="text-destructive" onClick={() => onRemove(asset)}>
                                 Remove asset
                             </DropdownMenuItem>
                         </DropdownMenuContent>
@@ -343,11 +330,9 @@ export function EntityAssets({
                     asset={previewAsset}
                     assets={assets}
                     onOpenChange={() => setPreviewAsset(null)}
-                    onAssetChange={asset => setPreviewAsset(asset)}
                     open={!!previewAsset}
                 />
             )}
         </>
     );
 }
-

+ 0 - 0
packages/dashboard/src/components/focal-point-control.tsx → packages/dashboard/src/components/shared/focal-point-control.tsx


+ 0 - 0
packages/dashboard/src/framework/internal/form/field.tsx → packages/dashboard/src/components/shared/translatable-form-field.tsx


+ 117 - 0
packages/dashboard/src/components/shared/vendure-image.tsx

@@ -0,0 +1,117 @@
+import React from 'react';
+
+export interface Asset {
+    id: string;
+    preview: string; // Base URL of the asset
+    name?: string | null;
+    focalPoint?: { x: number; y: number } | null;
+}
+
+export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | null;
+export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'avif' | null;
+export type ImageMode = 'crop' | 'resize' | null;
+
+export interface VendureImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
+    asset: Asset | null | undefined;
+    preset?: ImagePreset;
+    mode?: ImageMode;
+    width?: number;
+    height?: number;
+    format?: ImageFormat;
+    quality?: number;
+    useFocalPoint?: boolean; // Whether to use the asset's focal point in crop mode
+    fallback?: React.ReactNode; // What to show if no asset is provided
+    ref?: React.Ref<HTMLImageElement>;
+}
+
+export function VendureImage({
+    asset,
+    preset = null,
+    mode = null,
+    width,
+    height,
+    format = null,
+    quality,
+    useFocalPoint = true,
+    fallback = null,
+    alt,
+    className,
+    style,
+    ref,
+    ...imgProps
+}: VendureImageProps) {
+    if (!asset) {
+        return fallback ? <>{fallback}</> : null;
+    }
+
+    // Build the URL with query parameters
+    const url = new URL(asset.preview);
+
+    // Handle preset if specified
+    if (preset) {
+        url.searchParams.set('preset', preset);
+    } else {
+        // Or handle custom dimensions and mode
+        if (width) url.searchParams.set('w', width.toString());
+        if (height) url.searchParams.set('h', height.toString());
+        if (mode) url.searchParams.set('mode', mode);
+    }
+
+    // Add format if specified
+    if (format) {
+        url.searchParams.set('format', format);
+    }
+
+    // Add quality if specified
+    if (quality) {
+        url.searchParams.set('q', quality.toString());
+    }
+
+    // Apply focal point if available and requested
+    if (useFocalPoint && asset.focalPoint && mode === 'crop') {
+        url.searchParams.set('fpx', asset.focalPoint.x.toString());
+        url.searchParams.set('fpy', asset.focalPoint.y.toString());
+    }
+
+    return (
+        <img
+            src={url.toString()}
+            alt={alt || asset.name || ''}
+            className={className}
+            style={style}
+            loading="lazy"
+            ref={ref}
+            {...imgProps}
+        />
+    );
+}
+
+// Convenience components for common use cases
+export function Thumbnail({
+    asset,
+    size = 'thumb',
+    ...props
+}: Omit<VendureImageProps, 'preset'> & { size?: ImagePreset }) {
+    return <VendureImage asset={asset} preset={size} {...props} />;
+}
+
+export function CroppedImage({ asset, width, height, ...props }: Omit<VendureImageProps, 'mode'>) {
+    return <VendureImage asset={asset} mode="crop" width={width} height={height} {...props} />;
+}
+
+export function ResponsiveImage({
+    asset,
+    sizes = '100vw',
+    ...props
+}: VendureImageProps & { sizes?: string }) {
+    // Setup srcset with multiple sizes
+    const srcSet = [
+        `${asset?.preview}?w=320&mode=resize 320w`,
+        `${asset?.preview}?w=480&mode=resize 480w`,
+        `${asset?.preview}?w=640&mode=resize 640w`,
+        `${asset?.preview}?w=1024&mode=resize 1024w`,
+        `${asset?.preview}?w=1280&mode=resize 1280w`,
+    ].join(', ');
+
+    return <VendureImage asset={asset} srcSet={srcSet} sizes={sizes} {...props} />;
+}

+ 0 - 120
packages/dashboard/src/components/vendure-image.tsx

@@ -1,120 +0,0 @@
-import React from 'react';
-
-export interface Asset {
-  id: string;
-  preview: string; // Base URL of the asset
-  name?: string | null;
-  focalPoint?: { x: number; y: number } | null;
-}
-
-export type ImagePreset = 'tiny' | 'thumb' | 'small' | 'medium' | 'large' | null;
-export type ImageFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'avif' | null;
-export type ImageMode = 'crop' | 'resize' | null;
-
-export interface VendureImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
-  asset: Asset | null | undefined;
-  preset?: ImagePreset;
-  mode?: ImageMode;
-  width?: number;
-  height?: number;
-  format?: ImageFormat;
-  quality?: number;
-  useFocalPoint?: boolean; // Whether to use the asset's focal point in crop mode
-  fallback?: React.ReactNode; // What to show if no asset is provided
-  ref: React.Ref<HTMLImageElement>;
-}
-
-export function VendureImage({
-  asset,
-  preset = null,
-  mode = null,
-  width,
-  height,
-  format = null,
-  quality,
-  useFocalPoint = true,
-  fallback = null,
-  alt,
-  className,
-  style,
-  ref,
-  ...imgProps
-}: VendureImageProps) {
-  if (!asset) {
-    return fallback ? <>{fallback}</> : null;
-  }
-  
-  // Build the URL with query parameters
-  const url = new URL(asset.preview);
-  
-  // Handle preset if specified
-  if (preset) {
-    url.searchParams.set('preset', preset);
-  } else {
-    // Or handle custom dimensions and mode
-    if (width) url.searchParams.set('w', width.toString());
-    if (height) url.searchParams.set('h', height.toString());
-    if (mode) url.searchParams.set('mode', mode);
-  }
-  
-  // Add format if specified
-  if (format) {
-    url.searchParams.set('format', format);
-  }
-  
-  // Add quality if specified
-  if (quality) {
-    url.searchParams.set('q', quality.toString());
-  }
-  
-  // Apply focal point if available and requested
-  if (useFocalPoint && asset.focalPoint && mode === 'crop') {
-    url.searchParams.set('fpx', asset.focalPoint.x.toString());
-    url.searchParams.set('fpy', asset.focalPoint.y.toString());
-  }
-  
-  return (
-    <img 
-      src={url.toString()}
-      alt={alt || asset.name || ''}
-      className={className}
-      style={style}
-      loading="lazy"
-      ref={ref}
-      {...imgProps}
-    />
-  );
-}
-
-// Convenience components for common use cases
-export function Thumbnail({ asset, size = 'thumb', ...props }: Omit<VendureImageProps, 'preset'> & { size?: ImagePreset }) {
-  return <VendureImage asset={asset} preset={size} {...props} />;
-}
-
-export function CroppedImage({ asset, width, height, ...props }: Omit<VendureImageProps, 'mode'>) {
-  return <VendureImage asset={asset} mode="crop" width={width} height={height} {...props} />;
-}
-
-export function ResponsiveImage({ 
-  asset, 
-  sizes = '100vw', 
-  ...props 
-}: VendureImageProps & { sizes?: string }) {
-  // Setup srcset with multiple sizes
-  const srcSet = [
-    `${asset?.preview}?w=320&mode=resize 320w`,
-    `${asset?.preview}?w=480&mode=resize 480w`,
-    `${asset?.preview}?w=640&mode=resize 640w`,
-    `${asset?.preview}?w=1024&mode=resize 1024w`,
-    `${asset?.preview}?w=1280&mode=resize 1280w`,
-  ].join(', ');
-
-  return (
-    <VendureImage 
-      asset={asset} 
-      srcSet={srcSet}
-      sizes={sizes}
-      {...props}
-    />
-  );
-}

+ 3 - 3
packages/dashboard/src/framework/internal/component-registry/component-registry.tsx → packages/dashboard/src/framework/component-registry/component-registry.tsx

@@ -1,6 +1,6 @@
-import { AssetThumbnail } from '@/framework/internal/component-registry/data-types/asset.js';
-import { BooleanDisplayCheckbox } from '@/framework/internal/component-registry/data-types/boolean.js';
-import { DateTime } from './data-types/date-time.js';
+import { AssetThumbnail } from '@/components/data-type-components/asset.js';
+import { BooleanDisplayCheckbox } from '@/components/data-type-components/boolean.js';
+import { DateTime } from '@/components/data-type-components/date-time.js';
 
 export interface ComponentRegistryEntry {
     component: React.ComponentType<any>;

+ 1 - 1
packages/dashboard/src/framework/defaults.ts

@@ -1,4 +1,4 @@
-import { navMenu } from '@/framework/internal/nav-menu/nav-menu.js';
+import { navMenu } from '@/framework/nav-menu/nav-menu.js';
 import { BookOpen, Bot, Settings2, SquareTerminal } from 'lucide-react';
 
 navMenu({

+ 0 - 0
packages/dashboard/src/framework/internal/document-introspection/get-document-structure.ts → packages/dashboard/src/framework/document-introspection/get-document-structure.ts


+ 1 - 4
packages/dashboard/src/framework/internal/document-introspection/hooks.ts → packages/dashboard/src/framework/document-introspection/hooks.ts

@@ -1,7 +1,4 @@
-import {
-    FieldInfo,
-    getListQueryFields,
-} from '@/framework/internal/document-introspection/get-document-structure.js';
+import { FieldInfo, getListQueryFields } from '@/framework/document-introspection/get-document-structure.js';
 import { DocumentNode } from 'graphql';
 import { useMemo } from 'react';
 

+ 3 - 3
packages/dashboard/src/framework/internal/extension-api/define-dashboard-extension.ts → packages/dashboard/src/framework/extension-api/define-dashboard-extension.ts

@@ -1,6 +1,6 @@
-import { DashboardExtension } from '@/framework/internal/extension-api/extension-api-types.js';
-import { addNavMenuItem, NavMenuItem } from '@/framework/internal/nav-menu/nav-menu.js';
-import { registerListView } from '@/framework/internal/page/page-api.js';
+import { DashboardExtension } from '@/framework/extension-api/extension-api-types.js';
+import { addNavMenuItem, NavMenuItem } from '@/framework/nav-menu/nav-menu.js';
+import { registerListView } from '@/framework/page/page-api.js';
 
 const extensionSourceChangeCallbacks = new Set<() => void>();
 

+ 2 - 2
packages/dashboard/src/framework/internal/extension-api/extension-api-types.ts → packages/dashboard/src/framework/extension-api/extension-api-types.ts

@@ -1,5 +1,5 @@
-import { NavMenuItem } from '@/framework/internal/nav-menu/nav-menu.js';
-import { ListPageProps, ListQueryOptionsShape, ListQueryShape } from '@/framework/internal/page/list-page.js';
+import { NavMenuItem } from '@/framework/nav-menu/nav-menu.js';
+import { ListPageProps, ListQueryOptionsShape, ListQueryShape } from '@/framework/page/list-page.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import React from 'react';
 

+ 1 - 1
packages/dashboard/src/framework/internal/extension-api/use-dashboard-extensions.ts → packages/dashboard/src/framework/extension-api/use-dashboard-extensions.ts

@@ -1,4 +1,4 @@
-import { onExtensionSourceChange } from '@/framework/internal/extension-api/define-dashboard-extension.js';
+import { onExtensionSourceChange } from '@/framework/extension-api/define-dashboard-extension.js';
 import { useEffect, useState } from 'react';
 import { runDashboardExtensions } from 'virtual:dashboard-extensions';
 

+ 1 - 1
packages/dashboard/src/framework/internal/form-engine/form-schema-tools.ts → packages/dashboard/src/framework/form-engine/form-schema-tools.ts

@@ -2,7 +2,7 @@ import {
     FieldInfo,
     isEnumType,
     isScalarType,
-} from '@/framework/internal/document-introspection/get-document-structure.js';
+} from '@/framework/document-introspection/get-document-structure.js';
 import { z, ZodRawShape, ZodType, ZodTypeAny } from 'zod';
 
 export function createFormSchemaFromFields(fields: FieldInfo[]) {

+ 20 - 18
packages/dashboard/src/framework/internal/form-engine/use-generated-form.tsx → packages/dashboard/src/framework/form-engine/use-generated-form.tsx

@@ -1,8 +1,8 @@
-import { getOperationVariablesFields } from '@/framework/internal/document-introspection/get-document-structure.js';
+import { getOperationVariablesFields } from '@/framework/document-introspection/get-document-structure.js';
 import {
     createFormSchemaFromFields,
     getDefaultValuesFromFields,
-} from '@/framework/internal/form-engine/form-schema-tools.js';
+} from '@/framework/form-engine/form-schema-tools.js';
 import { useServerConfig } from '@/providers/server-config.js';
 import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { zodResolver } from '@hookform/resolvers/zod';
@@ -44,7 +44,7 @@ export function useGeneratedForm<
     const schema = createFormSchemaFromFields(updateFields);
     const defaultValues = getDefaultValuesFromFields(updateFields);
     const processedEntity = ensureTranslationsForAllLanguages(entity, availableLanguages);
-    
+
     const form = useForm({
         resolver: zodResolver(schema),
         defaultValues,
@@ -62,50 +62,52 @@ export function useGeneratedForm<
     return { form, submitHandler };
 }
 
-
 /**
  * Ensures that an entity with translations has entries for all available languages.
  * If a language is missing, it creates an empty translation based on the structure of existing translations.
  */
 function ensureTranslationsForAllLanguages<E extends Record<string, any>>(
     entity: E | null | undefined,
-    availableLanguages: string[] = []
+    availableLanguages: string[] = [],
 ): E | null | undefined {
-    if (!entity || !('translations' in entity) || !Array.isArray((entity as any).translations) || !availableLanguages.length) {
+    if (
+        !entity ||
+        !('translations' in entity) ||
+        !Array.isArray((entity as any).translations) ||
+        !availableLanguages.length
+    ) {
         return entity;
     }
 
     // Create a deep copy of the entity to avoid mutation
     const processedEntity = { ...entity } as any;
     const translations = [...(processedEntity.translations || [])];
-    
+
     // Get existing language codes
-    const existingLanguageCodes = new Set(
-        translations.map((t: any) => t.languageCode)
-    );
-    
+    const existingLanguageCodes = new Set(translations.map((t: any) => t.languageCode));
+
     // Add missing language translations
     for (const langCode of availableLanguages) {
         if (!existingLanguageCodes.has(langCode)) {
             // Find a translation to use as template for field structure
             const template = translations[0] || {};
-            const emptyTranslation: Record<string, any> = { 
-                languageCode: langCode 
+            const emptyTranslation: Record<string, any> = {
+                languageCode: langCode,
             };
-            
+
             // Add empty fields based on template (excluding languageCode)
             Object.keys(template).forEach(key => {
                 if (key !== 'languageCode') {
                     emptyTranslation[key] = '';
                 }
             });
-            
+
             translations.push(emptyTranslation);
         }
     }
-    
+
     // Update the processed entity with complete translations
     processedEntity.translations = translations;
-    
+
     return processedEntity as E;
-}
+}

+ 0 - 0
packages/dashboard/src/framework/internal/nav-menu/nav-menu.ts → packages/dashboard/src/framework/nav-menu/nav-menu.ts


+ 2 - 2
packages/dashboard/src/framework/internal/page/detail-page.tsx → packages/dashboard/src/framework/page/detail-page.tsx

@@ -1,5 +1,5 @@
-import { getQueryName } from '@/framework/internal/document-introspection/get-document-structure.js';
-import { PageProps } from '@/framework/internal/page/page-types.js';
+import { getQueryName } from '@/framework/document-introspection/get-document-structure.js';
+import { PageProps } from '@/framework/page/page-types.js';
 import { api } from '@/graphql/api.js';
 import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { queryOptions } from '@tanstack/react-query';

+ 6 - 6
packages/dashboard/src/framework/internal/page/list-page.tsx → packages/dashboard/src/framework/page/list-page.tsx

@@ -1,13 +1,13 @@
-import { useComponentRegistry } from '@/framework/internal/component-registry/component-registry.js';
-import { DataTableColumnHeader } from '@/framework/internal/data-table/data-table-column-header.js';
-import { DataTable } from '@/framework/internal/data-table/data-table.js';
+import { useComponentRegistry } from '@/framework/component-registry/component-registry.js';
+import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header.js';
+import { DataTable } from '@/components/data-table/data-table.js';
 import {
     FieldInfo,
     getListQueryFields,
     getQueryName,
-} from '@/framework/internal/document-introspection/get-document-structure.js';
-import { useListQueryFields } from '@/framework/internal/document-introspection/hooks.js';
-import { PageProps } from '@/framework/internal/page/page-types.js';
+} from '@/framework/document-introspection/get-document-structure.js';
+import { useListQueryFields } from '@/framework/document-introspection/hooks.js';
+import { PageProps } from '@/framework/page/page-types.js';
 import { api } from '@/graphql/api.js';
 import { useDebounce } from 'use-debounce';
 

+ 1 - 1
packages/dashboard/src/framework/internal/page/page-api.ts → packages/dashboard/src/framework/page/page-api.ts

@@ -1,4 +1,4 @@
-import { DashboardListRouteDefinition } from '@/framework/internal/extension-api/extension-api-types.js';
+import { DashboardListRouteDefinition } from '@/framework/extension-api/extension-api-types.js';
 
 export const listViewExtensionRoutes = new Map<string, DashboardListRouteDefinition>();
 

+ 0 - 0
packages/dashboard/src/framework/internal/page/page-types.ts → packages/dashboard/src/framework/page/page-types.ts


+ 0 - 0
packages/dashboard/src/framework/internal/page/page.tsx → packages/dashboard/src/framework/page/page.tsx


+ 3 - 3
packages/dashboard/src/framework/internal/page/use-extended-router.tsx → packages/dashboard/src/framework/page/use-extended-router.tsx

@@ -1,6 +1,6 @@
-import { useDashboardExtensions } from '@/framework/internal/extension-api/use-dashboard-extensions.js';
-import { ListPage } from '@/framework/internal/page/list-page.js';
-import { listViewExtensionRoutes } from '@/framework/internal/page/page-api.js';
+import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
+import { ListPage } from '@/framework/page/list-page.js';
+import { listViewExtensionRoutes } from '@/framework/page/page-api.js';
 import { AUTHENTICATED_ROUTE_PREFIX } from '@/routes/_authenticated.js';
 import { AnyRoute, createRoute, Router } from '@tanstack/react-router';
 import { useMemo } from 'react';

+ 2 - 2
packages/dashboard/src/index.ts

@@ -1,2 +1,2 @@
-export * from './framework/internal/extension-api/define-dashboard-extension.js';
-export * from '@/framework/internal/extension-api/extension-api-types.js';
+export * from '@/framework/extension-api/define-dashboard-extension.js';
+export * from '@/framework/extension-api/extension-api-types.js';

+ 13 - 0
packages/dashboard/src/lib/utils.ts

@@ -25,3 +25,16 @@ export function camelCaseToTitleCase(text: string): string {
             .trim()
     );
 }
+
+/**
+ * Formats a file size in bytes to a human-readable string.
+ */
+export function formatFileSize(bytes: number): string {
+    if (bytes === 0) return '0 Bytes';
+
+    const k = 1024;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)).toString() + ' ' + sizes[i];
+}

+ 3 - 3
packages/dashboard/src/main.tsx

@@ -1,9 +1,9 @@
 import { AppProviders, queryClient, router } from '@/app-providers.js';
 import { useAuth } from '@/providers/auth.js';
-import { useDashboardExtensions } from '@/framework/internal/extension-api/use-dashboard-extensions.js';
-import { useExtendedRouter } from '@/framework/internal/page/use-extended-router.js';
+import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
+import { useExtendedRouter } from '@/framework/page/use-extended-router.js';
 import { defaultLocale, dynamicActivate } from '@/providers/i18n-provider.js';
-import { Toaster } from "@/components/ui/sonner.js"
+import { Toaster } from '@/components/ui/sonner.js';
 
 import '@/framework/defaults.js';
 import { RouterProvider } from '@tanstack/react-router';

+ 2 - 2
packages/dashboard/src/routes/_authenticated.tsx

@@ -1,5 +1,5 @@
-import { AppSidebar } from '@/components/app-sidebar.js';
-import { GeneratedBreadcrumbs } from '@/components/generated-breadcrumbs.js';
+import { AppSidebar } from '@/components/layout/app-sidebar.js';
+import { GeneratedBreadcrumbs } from '@/components/layout/generated-breadcrumbs.js';
 import { Separator } from '@/components/ui/separator.js';
 import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar.js';
 import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';

+ 1 - 1
packages/dashboard/src/routes/_authenticated/products.tsx

@@ -1,5 +1,5 @@
 import { Button } from '@/components/ui/button.js';
-import { ListPage } from '@/framework/internal/page/list-page.js';
+import { ListPage } from '@/framework/page/list-page.js';
 import { graphql } from '@/graphql/graphql.js';
 import { createFileRoute, Link, Outlet } from '@tanstack/react-router';
 import React from 'react';

+ 28 - 25
packages/dashboard/src/routes/_authenticated/products_.$id.tsx

@@ -1,5 +1,5 @@
-import { ContentLanguageSelector } from '@/components/content-language-selector.js';
-import { EntityAssets } from '@/components/entity-assets.js';
+import { ContentLanguageSelector } from '@/components/layout/content-language-selector.js';
+import { EntityAssets } from '@/components/shared/entity-assets.js';
 import { Button } from '@/components/ui/button.js';
 import { Card, CardContent, CardHeader } from '@/components/ui/card.js';
 import {
@@ -14,9 +14,9 @@ import {
 import { Input } from '@/components/ui/input.js';
 import { Switch } from '@/components/ui/switch.js';
 import { Textarea } from '@/components/ui/textarea.js';
-import { useGeneratedForm } from '@/framework/internal/form-engine/use-generated-form.js';
-import { TranslatableFormField } from '@/framework/internal/form/field.js';
-import { DetailPage, getDetailQueryOptions } from '@/framework/internal/page/detail-page.js';
+import { useGeneratedForm } from '@/framework/form-engine/use-generated-form.js';
+import { TranslatableFormField } from '@/components/shared/translatable-form-field.js';
+import { DetailPage, getDetailQueryOptions } from '@/framework/page/detail-page.js';
 import { api } from '@/graphql/api.js';
 import { assetFragment } from '@/graphql/fragments.js';
 import { graphql } from '@/graphql/graphql.js';
@@ -34,31 +34,34 @@ export const Route = createFileRoute('/_authenticated/products_/$id')({
     },
 });
 
-const productDetailFragment = graphql(`
-    fragment ProductDetail on Product {
-        id
-        createdAt
-        updatedAt
-        enabled
-        name
-        slug
-        description
-        featuredAsset {
-            ...Asset
-        }
-        assets {
-            ...Asset
-        }
-        translations {
+const productDetailFragment = graphql(
+    `
+        fragment ProductDetail on Product {
             id
-            languageCode
-
+            createdAt
+            updatedAt
+            enabled
             name
             slug
             description
+            featuredAsset {
+                ...Asset
+            }
+            assets {
+                ...Asset
+            }
+            translations {
+                id
+                languageCode
+
+                name
+                slug
+                description
+            }
         }
-    }
-`, [assetFragment]);
+    `,
+    [assetFragment],
+);
 
 const productDetailDocument = graphql(
     `

+ 1 - 1
packages/dashboard/src/routes/login.tsx

@@ -1,5 +1,5 @@
 import { useAuth } from '@/providers/auth.js';
-import { LoginForm } from '@/components/login-form.js';
+import { LoginForm } from '@/components/login/login-form.js';
 import { createFileRoute, Navigate, redirect, useRouterState } from '@tanstack/react-router';
 import * as React from 'react';
 import { z } from 'zod';