diff --git a/apps/dashboard/src/app/[locale]/@dashboard/(root)/vault/[[...folders]]/page.tsx b/apps/dashboard/src/app/[locale]/@dashboard/(root)/vault/[[...folders]]/page.tsx index baadf33d90..c2364a452f 100644 --- a/apps/dashboard/src/app/[locale]/@dashboard/(root)/vault/[[...folders]]/page.tsx +++ b/apps/dashboard/src/app/[locale]/@dashboard/(root)/vault/[[...folders]]/page.tsx @@ -1,5 +1,7 @@ import { Breadcrumbs } from "@/components/breadcrumbs"; import { Table } from "@/components/tables/vault"; +import { Button } from "@midday/ui/button"; +import { Icons } from "@midday/ui/icons"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -7,9 +9,26 @@ export const metadata: Metadata = { }; export default function Vault({ params }) { + const disableActions = ["transactions", "inbox", "exports"].includes( + params.folders?.slice(-1)?.at(0) + ); + return (
- +
+ + + {!disableActions && ( +
+ + +
+ )} +
); diff --git a/apps/dashboard/src/components/breadcrumbs.tsx b/apps/dashboard/src/components/breadcrumbs.tsx index f98d792af1..1d88e886d5 100644 --- a/apps/dashboard/src/components/breadcrumbs.tsx +++ b/apps/dashboard/src/components/breadcrumbs.tsx @@ -1,3 +1,31 @@ -export function Breadcrumbs({ folders }) { - return folders?.map((folder) => {folder}); +"use client"; + +import { useI18n } from "@/locales/client"; +import { Icons } from "@midday/ui/icons"; +import Link from "next/link"; +import { translatedFolderName } from "./tables/vault/data-table-row"; + +export function Breadcrumbs({ folders = [] }) { + const t = useI18n(); + + const allFolders = ["all", ...folders]; + + const links = allFolders?.map((folder, index) => { + const isLast = folders.length === index; + const href = folder === "all" ? "/vault" : `/vault/${folder}`; + + return ( +
+ + {translatedFolderName(t, folder)} + + + {folders.length > 0 && !isLast && ( + + )} +
+ ); + }); + + return
{links}
; } diff --git a/apps/dashboard/src/components/file-icon.tsx b/apps/dashboard/src/components/file-icon.tsx index 8cca63392c..6252d66692 100644 --- a/apps/dashboard/src/components/file-icon.tsx +++ b/apps/dashboard/src/components/file-icon.tsx @@ -1,12 +1,12 @@ import { Icons } from "@midday/ui/icons"; -export function FileIcon({ mimetype, name }) { +export function FileIcon({ mimetype, name, isFolder }) { if (name === "exports") { return ; } if (name === "inbox") { - return ; + return ; } if (name === "transactions") { @@ -17,6 +17,10 @@ export function FileIcon({ mimetype, name }) { return ; } + if (isFolder) { + return ; + } + switch (mimetype) { case "application/pdf": return ; diff --git a/apps/dashboard/src/components/tables/vault/data-table-row.tsx b/apps/dashboard/src/components/tables/vault/data-table-row.tsx index 72a4e33e34..c01625f28c 100644 --- a/apps/dashboard/src/components/tables/vault/data-table-row.tsx +++ b/apps/dashboard/src/components/tables/vault/data-table-row.tsx @@ -1,37 +1,89 @@ "use client"; import { FileIcon } from "@/components/file-icon"; +import { useI18n } from "@/locales/client"; import { formatSize } from "@/utils/format"; -import { Icons } from "@midday/ui/icons"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from "@midday/ui/context-menu"; import { TableCell, TableRow } from "@midday/ui/table"; import { format } from "date-fns"; import { usePathname, useRouter } from "next/navigation"; +export const translatedFolderName = (t: any, folder: string) => { + switch (folder) { + case "all": + return t("folders.all"); + case "inbox": + return t("folders.inbox"); + case "transactions": + return t("folders.transactions"); + case "exports": + return t("folders.exports"); + + default: + return folder; + } +}; + export function DataTableRow({ data }) { + const t = useI18n(); const router = useRouter(); const pathname = usePathname(); - const handleNavigate = () => router.push(`${pathname}/${data.name}`); + const handleNavigate = () => { + if (data.isFolder) { + router.push(`${pathname}/${data.name}`); + } + }; return ( - - -
- - {data.name} - {data?.metadata?.size && ( - - {formatSize(data.metadata.size)} - - )} -
-
- - {data?.created_at && format(new Date(data.created_at), "MMM d, yyyy")} - - - - -
+ + + + +
+ + {translatedFolderName(t, data.name)} + {data?.metadata?.size && ( + + {formatSize(data.metadata.size)} + + )} +
+
+ + {data?.created_at ? format(new Date(data.created_at), "Pp") : "-"} + + + {data?.updated_at ? format(new Date(data.updated_at), "Pp") : "-"} + +
+
+ + + + Get URL + + Expire in 1 week + Expire in 1 month + Expire in 1 year + + + Rename + Download + Delete + +
); } diff --git a/apps/dashboard/src/components/tables/vault/data-table.tsx b/apps/dashboard/src/components/tables/vault/data-table.tsx index f67f0f6589..aaf7a2aefc 100644 --- a/apps/dashboard/src/components/tables/vault/data-table.tsx +++ b/apps/dashboard/src/components/tables/vault/data-table.tsx @@ -13,8 +13,8 @@ export function DataTable({ data }) { Name - Uploaded - Actions + Created at + Last modified at diff --git a/apps/dashboard/src/components/tables/vault/empty-table.tsx b/apps/dashboard/src/components/tables/vault/empty-table.tsx new file mode 100644 index 0000000000..cbde73fb02 --- /dev/null +++ b/apps/dashboard/src/components/tables/vault/empty-table.tsx @@ -0,0 +1,32 @@ +type Props = { + type: string; +}; + +export function EmptyTable({ type }: Props) { + switch (type) { + case "inbox": + return ( +
+
+

This is your inbox

+

+ Everything that will be sent to your
+ Midday email will end up here. +

+
+
+ ); + + default: + return ( +
+
+

Drop your files here

+

+ Or upload them via the
"Upload file" button above +

+
+
+ ); + } +} diff --git a/apps/dashboard/src/components/tables/vault/index.tsx b/apps/dashboard/src/components/tables/vault/index.tsx index a271b431dc..9f79ace63f 100644 --- a/apps/dashboard/src/components/tables/vault/index.tsx +++ b/apps/dashboard/src/components/tables/vault/index.tsx @@ -1,12 +1,14 @@ import { getVault } from "@midday/supabase/cached-queries"; import { DataTable } from "./data-table"; +import { EmptyTable } from "./empty-table"; export async function Table({ path }) { const { data } = await getVault({ path }); return ( -
+
+ {data.length === 0 && }
); } diff --git a/apps/dashboard/src/locales/en.ts b/apps/dashboard/src/locales/en.ts index 3b56168d9c..bc0231101f 100644 --- a/apps/dashboard/src/locales/en.ts +++ b/apps/dashboard/src/locales/en.ts @@ -46,4 +46,10 @@ export default { profit_loss: "Profit/Loss", income: "Income", }, + folders: { + all: "All", + exports: "Exports", + inbox: "Inbox", + transactions: "Transactions", + }, } as const; diff --git a/apps/dashboard/src/locales/sv.ts b/apps/dashboard/src/locales/sv.ts index 43004db72f..4cc6d87596 100644 --- a/apps/dashboard/src/locales/sv.ts +++ b/apps/dashboard/src/locales/sv.ts @@ -46,4 +46,10 @@ export default { profit_loss: "Vinst/Förlust", income: "Inkomst", }, + folders: { + all: "Alla", + exports: "Exporteringar", + inbox: "Inbox", + transactions: "Transaktioner", + }, } as const; diff --git a/bun.lockb b/bun.lockb index 47e126a92d..fa35c93f6b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/supabase/src/queries/index.ts b/packages/supabase/src/queries/index.ts index 3bb0916a9a..f14db6f6d2 100644 --- a/packages/supabase/src/queries/index.ts +++ b/packages/supabase/src/queries/index.ts @@ -475,10 +475,17 @@ type GetVaultParams = { path?: string; }; -const defaultFolders = [{ id: "inbox" }, { id: "exports" }]; - export async function getVaultQuery(supabase: Client, params: GetVaultParams) { const { teamId, path } = params; + + const defaultFolders = path + ? [] + : [ + { name: "inbox", isFolder: true }, + { name: "exports", isFolder: true }, + { name: "transactions", isFolder: true }, + ]; + let basePath = teamId; if (path) { @@ -487,7 +494,18 @@ export async function getVaultQuery(supabase: Client, params: GetVaultParams) { const { data } = await supabase.storage.from("vault").list(basePath); + const filteredData = + data + ?.filter((file) => file.name !== ".emptyFolderPlaceholder") + .map((item) => ({ ...item, isFolder: !item.id })) ?? []; + + const mergedMap = new Map( + [...defaultFolders, ...filteredData].map((obj) => [obj.name, obj]) + ); + + const mergedArray = Array.from(mergedMap.values()); + return { - data: data?.filter((file) => file.name !== ".emptyFolderPlaceholder"), + data: mergedArray, }; } diff --git a/packages/ui/package.json b/packages/ui/package.json index 74abea7187..9383874848 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -22,6 +22,7 @@ "exports": { "./calendar": "./src/components/calendar.tsx", "./card": "./src/components/card.tsx", + "./context-menu": "./src/components/context-menu.tsx", "./alert": "./src/components/alert.tsx", "./alert-dialog": "./src/components/alert-dialog.tsx", "./avatar": "./src/components/avatar.tsx", @@ -61,6 +62,7 @@ "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx new file mode 100644 index 0000000000..cde5d435e2 --- /dev/null +++ b/packages/ui/src/components/context-menu.tsx @@ -0,0 +1,199 @@ +"use client"; + +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; +import * as React from "react"; +import { cn } from "../utils"; + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = "ContextMenuShortcut"; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}; diff --git a/packages/ui/src/components/icons.tsx b/packages/ui/src/components/icons.tsx index 1d21a018ef..9ae61cfb95 100644 --- a/packages/ui/src/components/icons.tsx +++ b/packages/ui/src/components/icons.tsx @@ -2,9 +2,11 @@ import { ArchiveIcon } from "@radix-ui/react-icons"; import { Settings } from "lucide-react"; import { MdApartment, + MdArrowRight, MdBarChart, MdBrokenImage, MdCelebration, + MdCreateNewFolder, MdDescription, MdDesk, MdDevices, @@ -13,8 +15,10 @@ import { MdExpandMore, MdFastfood, MdFence, + MdFileUpload, MdFlightTakeoff, - MdFolderOpen, + MdFolder, + MdFolderSpecial, MdFolderZip, MdHomeWork, MdInventory2, @@ -34,6 +38,7 @@ import { MdPictureAsPdf, MdRefresh, MdSave, + MdSearch, MdSensors, MdTopic, MdTrendingDown, @@ -327,9 +332,14 @@ export const Icons = { MoreHoriz: MdMoreHoriz, Pdf: MdPictureAsPdf, DriveFileMove: MdDriveFileMove, - FolderOpen: MdFolderOpen, + FolderSpecial: MdFolderSpecial, Topic: MdTopic, BrokenImage: MdBrokenImage, Description: MdDescription, FolderZip: MdFolderZip, + ArrowRight: MdArrowRight, + Folder: MdFolder, + FileUpload: MdFileUpload, + Search: MdSearch, + CreateNewFolder: MdCreateNewFolder, };