From de8e9ffd35e5345c553c5c4f7e05b9da1e0d55a6 Mon Sep 17 00:00:00 2001 From: fccview Date: Thu, 9 Jul 2026 07:33:24 +0100 Subject: [PATCH 1/2] server actions are pretty much all done --- .../admin/checklist/[uuid]/page.tsx | 2 +- .../admin/note/[uuid]/page.tsx | 2 +- .../checklist/[...categoryPath]/page.tsx | 86 ++------- .../checklist/[uuid]/page.tsx | 92 +++++++++ .../note/[...categoryPath]/page.tsx | 85 ++------- app/(loggedInRoutes)/note/[uuid]/page.tsx | 93 ++++++++++ .../Parts/Common/LastModifiedCreatedInfo.tsx | 8 +- .../FeatureComponents/Kanban/Kanban.tsx | 8 +- .../Parts/NoteEditor/NoteEditorHeader.tsx | 38 +--- .../Notes/Parts/SwipeNavigationWrapper.tsx | 7 +- .../Sidebar/Parts/CategoryList.tsx | 86 +-------- .../Sidebar/Parts/CategoryRenderer.tsx | 16 +- .../Sidebar/Parts/SharedItemsList.tsx | 10 +- .../Sidebar/Parts/SidebarItem.tsx | 28 +-- .../FeatureComponents/Tags/TagHoverCard.tsx | 11 +- app/_consts/identity.ts | 4 + app/_consts/notes.ts | 1 + app/_hooks/useNoteEditor.tsx | 32 +--- app/_hooks/useSearch.ts | 8 +- app/_hooks/useSidebar.tsx | 13 +- app/_providers/PermissionsProvider.tsx | 13 +- app/_server/actions/category/move.ts | 37 +++- app/_server/actions/checklist-item/archive.ts | 29 ++- .../actions/checklist-item/bulk-operations.ts | 48 ++--- app/_server/actions/checklist-item/crud.ts | 64 +++---- app/_server/actions/checklist-item/drop.ts | 10 +- app/_server/actions/checklist-item/reorder.ts | 28 +-- app/_server/actions/checklist-item/status.ts | 15 +- .../actions/checklist-item/sub-items.ts | 34 ++-- app/_server/actions/checklist/converters.ts | 33 ++-- app/_server/actions/checklist/crud.ts | 135 ++++---------- app/_server/actions/checklist/queries.ts | 108 +++-------- app/_server/actions/checklist/readers.ts | 18 +- app/_server/actions/config/helpers.ts | 7 +- app/_server/actions/dashboard/index.ts | 56 ++---- app/_server/actions/history/index.ts | 28 +-- app/_server/actions/kanban/calendar.ts | 8 +- app/_server/actions/kanban/items.ts | 39 ++-- app/_server/actions/kanban/search.ts | 4 +- app/_server/actions/kanban/tempo.ts | 4 +- app/_server/actions/kanban/time-entries.ts | 21 ++- app/_server/actions/note/crud.ts | 174 ++++++------------ app/_server/actions/note/queries.ts | 125 ++++--------- app/_server/actions/note/readers.ts | 38 +++- app/_server/actions/notifications/index.ts | 4 +- app/_server/actions/sharing/helpers.ts | 24 --- app/_server/actions/sharing/permissions.ts | 128 +++---------- app/_server/actions/sharing/queries.ts | 115 ++++-------- .../actions/sharing/share-operations.ts | 90 +++------ app/_server/actions/sharing/types.ts | 4 +- app/_server/actions/sharing/updates.ts | 72 +------- app/_server/lib/legacy-lookup.ts | 153 +++++++++++++++ app/_server/reminders/scanner.ts | 13 +- app/_types/sharing.ts | 8 +- app/_utils/global-utils.ts | 23 +-- app/_utils/sharing-utils.ts | 21 +-- .../[listId]/items/reorder/route.ts | 21 ++- app/api/notes/[noteId]/route.ts | 41 +++-- .../checklist/[...categoryPath]/page.tsx | 114 ++---------- app/public/checklist/[uuid]/page.tsx | 100 ++++++++++ app/public/note/[...categoryPath]/page.tsx | 108 +---------- app/public/note/[uuid]/page.tsx | 96 ++++++++++ 62 files changed, 1220 insertions(+), 1621 deletions(-) create mode 100644 app/(loggedInRoutes)/checklist/[uuid]/page.tsx create mode 100644 app/(loggedInRoutes)/note/[uuid]/page.tsx create mode 100644 app/_consts/identity.ts create mode 100644 app/_server/lib/legacy-lookup.ts create mode 100644 app/public/checklist/[uuid]/page.tsx create mode 100644 app/public/note/[uuid]/page.tsx diff --git a/app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx b/app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx index fcbfc361..0803bc3a 100644 --- a/app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx +++ b/app/(loggedInRoutes)/admin/checklist/[uuid]/page.tsx @@ -24,7 +24,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata(props: AdminChecklistPageProps): Promise { const params = await props.params; const { uuid } = params; - return getMedatadaTitle(Modes.CHECKLISTS, uuid, "Admin"); + return getMedatadaTitle(Modes.CHECKLISTS, uuid); } export default async function AdminChecklistPage(props: AdminChecklistPageProps) { diff --git a/app/(loggedInRoutes)/admin/note/[uuid]/page.tsx b/app/(loggedInRoutes)/admin/note/[uuid]/page.tsx index 666eaf92..9f39649d 100644 --- a/app/(loggedInRoutes)/admin/note/[uuid]/page.tsx +++ b/app/(loggedInRoutes)/admin/note/[uuid]/page.tsx @@ -24,7 +24,7 @@ export const dynamic = "force-dynamic"; export async function generateMetadata(props: AdminNotePageProps): Promise { const params = await props.params; const { uuid } = params; - return getMedatadaTitle(Modes.NOTES, uuid, "Admin"); + return getMedatadaTitle(Modes.NOTES, uuid); } export default async function AdminNotePage(props: AdminNotePageProps) { diff --git a/app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx b/app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx index 09d1fee8..c28e21d1 100644 --- a/app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx +++ b/app/(loggedInRoutes)/checklist/[...categoryPath]/page.tsx @@ -1,17 +1,9 @@ -import { redirect } from "next/navigation"; -import { getListById } from "@/app/_server/actions/checklist"; -import { getCategories } from "@/app/_server/actions/category"; -import { getCurrentUser, canAccessAllContent } from "@/app/_server/actions/users"; -import { ChecklistClient } from "@/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient"; +import { redirect, permanentRedirect } from "next/navigation"; import { Modes } from "@/app/_types/enums"; -import type { Metadata } from "next"; -import { getMedatadaTitle } from "@/app/_server/actions/config"; -import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils"; -import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; -import { MetadataProvider } from "@/app/_providers/MetadataProvider"; -import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils"; +import { legacyResolve } from "@/app/_server/lib/legacy-lookup"; +import { decodeCategoryPath } from "@/app/_utils/global-utils"; -interface ChecklistPageProps { +interface LegacyChecklistPageProps { params: Promise<{ categoryPath: string[]; }>; @@ -19,71 +11,19 @@ interface ChecklistPageProps { export const dynamic = "force-dynamic"; -export async function generateMetadata(props: ChecklistPageProps): Promise { +export default async function LegacyChecklistPage( + props: LegacyChecklistPageProps, +) { const params = await props.params; const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); + const id = decodeURIComponent(categoryPath[categoryPath.length - 1]); + const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/")); - return getMedatadaTitle(Modes.CHECKLISTS, id, category); -} - -export default async function ChecklistPage(props: ChecklistPageProps) { - const params = await props.params; - const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); - const userRecord = await getCurrentUser(); - const username = userRecord?.username || ""; - const hasContentAccess = await canAccessAllContent(); - - const categoriesResult = await getCategories(Modes.CHECKLISTS); - - let checklist = await getListById(id, username, category); + const uuid = await legacyResolve(Modes.CHECKLISTS, category, id); - if (!checklist && hasContentAccess) { - checklist = await getListById(id, undefined, category); + if (uuid) { + permanentRedirect(`/checklist/${uuid}`); } - if (!checklist) { - redirect("/"); - } - - const categories = - categoriesResult.success && categoriesResult.data - ? categoriesResult.data - : []; - - const metadata = { - id: checklist.id, - uuid: checklist.uuid, - title: checklist.title, - category: checklist.category || "Uncategorized", - owner: checklist.owner, - createdAt: checklist.createdAt, - updatedAt: checklist.updatedAt, - type: "checklist" as const, - }; - - const user = sanitizeUserForClient(userRecord); - - return ( - - - - - - ); + redirect("/"); } diff --git a/app/(loggedInRoutes)/checklist/[uuid]/page.tsx b/app/(loggedInRoutes)/checklist/[uuid]/page.tsx new file mode 100644 index 00000000..93189a96 --- /dev/null +++ b/app/(loggedInRoutes)/checklist/[uuid]/page.tsx @@ -0,0 +1,92 @@ +import { redirect, permanentRedirect } from "next/navigation"; +import { getListById } from "@/app/_server/actions/checklist"; +import { getCategories } from "@/app/_server/actions/category"; +import { getCurrentUser, canAccessAllContent } from "@/app/_server/actions/users"; +import { ChecklistClient } from "@/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient"; +import { Modes } from "@/app/_types/enums"; +import type { Metadata } from "next"; +import { getMedatadaTitle } from "@/app/_server/actions/config"; +import { isUuid } from "@/app/_consts/identity"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; +import { MetadataProvider } from "@/app/_providers/MetadataProvider"; +import { sanitizeUserForClient } from "@/app/_utils/user-sanitize-utils"; + +interface ChecklistPageProps { + params: Promise<{ + uuid: string; + }>; +} + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(props: ChecklistPageProps): Promise { + const params = await props.params; + return getMedatadaTitle(Modes.CHECKLISTS, params.uuid); +} + +export default async function ChecklistPage(props: ChecklistPageProps) { + const params = await props.params; + const { uuid } = params; + + if (!isUuid(uuid)) { + const { legacyResolve } = await import("@/app/_server/lib/legacy-lookup"); + const resolved = await legacyResolve( + Modes.CHECKLISTS, + UNCATEGORIZED, + decodeURIComponent(uuid), + ); + + if (resolved) { + permanentRedirect(`/checklist/${resolved}`); + } + + redirect("/"); + } + + const userRecord = await getCurrentUser(); + const username = userRecord?.username || ""; + const hasContentAccess = await canAccessAllContent(); + + const categoriesResult = await getCategories(Modes.CHECKLISTS); + + let checklist = await getListById(uuid, username); + + if (!checklist && hasContentAccess) { + checklist = await getListById(uuid); + } + + if (!checklist) { + redirect("/"); + } + + const categories = + categoriesResult.success && categoriesResult.data + ? categoriesResult.data + : []; + + const metadata = { + id: checklist.id, + uuid: checklist.uuid, + title: checklist.title, + category: checklist.category || UNCATEGORIZED, + owner: checklist.owner, + createdAt: checklist.createdAt, + updatedAt: checklist.updatedAt, + type: "checklist" as const, + }; + + const user = sanitizeUserForClient(userRecord); + + return ( + + + + + + ); +} diff --git a/app/(loggedInRoutes)/note/[...categoryPath]/page.tsx b/app/(loggedInRoutes)/note/[...categoryPath]/page.tsx index 46ccab8e..9a93d8ef 100644 --- a/app/(loggedInRoutes)/note/[...categoryPath]/page.tsx +++ b/app/(loggedInRoutes)/note/[...categoryPath]/page.tsx @@ -1,22 +1,9 @@ -import { redirect } from "next/navigation"; -import { - CheckForNeedsMigration, - getNoteById, -} from "@/app/_server/actions/note"; -import { - getCurrentUser, - canAccessAllContent, -} from "@/app/_server/actions/users"; -import { NoteClient } from "@/app/_components/FeatureComponents/Notes/NoteClient"; +import { redirect, permanentRedirect } from "next/navigation"; import { Modes } from "@/app/_types/enums"; -import { getCategories } from "@/app/_server/actions/category"; -import type { Metadata } from "next"; -import { getMedatadaTitle } from "@/app/_server/actions/config"; -import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils"; -import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; -import { MetadataProvider } from "@/app/_providers/MetadataProvider"; +import { legacyResolve } from "@/app/_server/lib/legacy-lookup"; +import { decodeCategoryPath } from "@/app/_utils/global-utils"; -interface NotePageProps { +interface LegacyNotePageProps { params: Promise<{ categoryPath: string[]; }>; @@ -24,67 +11,17 @@ interface NotePageProps { export const dynamic = "force-dynamic"; -export async function generateMetadata(props: NotePageProps): Promise { +export default async function LegacyNotePage(props: LegacyNotePageProps) { const params = await props.params; const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); + const id = decodeURIComponent(categoryPath[categoryPath.length - 1]); + const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/")); - return getMedatadaTitle(Modes.NOTES, id, category); -} - -export default async function NotePage(props: NotePageProps) { - const params = await props.params; - const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); - const user = await getCurrentUser(); - const username = user?.username || ""; - const hasContentAccess = await canAccessAllContent(); - - await CheckForNeedsMigration(); - - const categoriesResult = await getCategories(Modes.NOTES); + const uuid = await legacyResolve(Modes.NOTES, category, id); - let note = await getNoteById(id, category, username); - - if (!note && hasContentAccess) { - note = await getNoteById(id, category); - } - - if (!note) { - redirect("/"); + if (uuid) { + permanentRedirect(`/note/${uuid}`); } - const docsCategories = - categoriesResult.success && categoriesResult.data - ? categoriesResult.data - : []; - - const metadata = { - id: note.id, - uuid: note.uuid, - title: note.title, - category: note.category || "Uncategorized", - owner: note.owner, - createdAt: note.createdAt, - updatedAt: note.updatedAt, - type: "note" as const, - }; - - return ( - - - - - - ); + redirect("/"); } diff --git a/app/(loggedInRoutes)/note/[uuid]/page.tsx b/app/(loggedInRoutes)/note/[uuid]/page.tsx new file mode 100644 index 00000000..8c81bc09 --- /dev/null +++ b/app/(loggedInRoutes)/note/[uuid]/page.tsx @@ -0,0 +1,93 @@ +import { redirect, permanentRedirect } from "next/navigation"; +import { + CheckForNeedsMigration, + getNoteById, +} from "@/app/_server/actions/note"; +import { + getCurrentUser, + canAccessAllContent, +} from "@/app/_server/actions/users"; +import { NoteClient } from "@/app/_components/FeatureComponents/Notes/NoteClient"; +import { Modes } from "@/app/_types/enums"; +import { getCategories } from "@/app/_server/actions/category"; +import type { Metadata } from "next"; +import { getMedatadaTitle } from "@/app/_server/actions/config"; +import { isUuid } from "@/app/_consts/identity"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; +import { MetadataProvider } from "@/app/_providers/MetadataProvider"; + +interface NotePageProps { + params: Promise<{ + uuid: string; + }>; +} + +export const dynamic = "force-dynamic"; + +export async function generateMetadata(props: NotePageProps): Promise { + const params = await props.params; + return getMedatadaTitle(Modes.NOTES, params.uuid); +} + +export default async function NotePage(props: NotePageProps) { + const params = await props.params; + const { uuid } = params; + + if (!isUuid(uuid)) { + const { legacyResolve } = await import("@/app/_server/lib/legacy-lookup"); + const resolved = await legacyResolve( + Modes.NOTES, + UNCATEGORIZED, + decodeURIComponent(uuid), + ); + + if (resolved) { + permanentRedirect(`/note/${resolved}`); + } + + redirect("/"); + } + + const user = await getCurrentUser(); + const username = user?.username || ""; + const hasContentAccess = await canAccessAllContent(); + + await CheckForNeedsMigration(); + + const categoriesResult = await getCategories(Modes.NOTES); + + let note = await getNoteById(uuid, username); + + if (!note && hasContentAccess) { + note = await getNoteById(uuid); + } + + if (!note) { + redirect("/"); + } + + const docsCategories = + categoriesResult.success && categoriesResult.data + ? categoriesResult.data + : []; + + const metadata = { + id: note.id, + uuid: note.uuid, + title: note.title, + category: note.category || UNCATEGORIZED, + owner: note.owner, + createdAt: note.createdAt, + updatedAt: note.updatedAt, + type: "note" as const, + }; + + return ( + + + + + + ); +} diff --git a/app/_components/FeatureComponents/Checklists/Parts/Common/LastModifiedCreatedInfo.tsx b/app/_components/FeatureComponents/Checklists/Parts/Common/LastModifiedCreatedInfo.tsx index 3997c70b..5dc9d7fb 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/Common/LastModifiedCreatedInfo.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/Common/LastModifiedCreatedInfo.tsx @@ -2,7 +2,6 @@ import { Checklist, Item } from "@/app/_types"; import { PencilEdit02Icon, Clock01Icon } from "hugeicons-react"; import { UserAvatar } from "@/app/_components/GlobalComponents/User/UserAvatar"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { usePreferredDateTime } from "@/app/_hooks/usePreferredDateTime"; import { useTranslations } from "next-intl"; @@ -18,14 +17,9 @@ const LastModifiedCreatedInfo = ({ const { allSharedItems } = useAppMode(); const { formatDateTimeString } = usePreferredDateTime(); const t = useTranslations(); - const encodedCategory = encodeCategoryPath( - checklist.category || "Uncategorized" - ); const isShared = allSharedItems?.checklists.some( - (sharedChecklist) => - sharedChecklist.id === checklist.id && - sharedChecklist.category === encodedCategory + (sharedChecklist) => sharedChecklist.uuid === checklist.uuid ); return ( <> diff --git a/app/_components/FeatureComponents/Kanban/Kanban.tsx b/app/_components/FeatureComponents/Kanban/Kanban.tsx index 9782916f..7534d3c2 100644 --- a/app/_components/FeatureComponents/Kanban/Kanban.tsx +++ b/app/_components/FeatureComponents/Kanban/Kanban.tsx @@ -15,7 +15,6 @@ import { ItemTypes, TaskStatus, TaskStatusLabels } from "@/app/_types/enums"; import { ReferencedBySection } from "../Notes/Parts/ReferencedBySection"; import { getReferences } from "@/app/_utils/indexes-utils"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { usePermissions } from "@/app/_providers/PermissionsProvider"; import { Settings01Icon, @@ -64,14 +63,9 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { >(null); const { linkIndex, notes, checklists, appSettings, allSharedItems } = useAppMode(); - const encodedCategory = encodeCategoryPath( - checklist.category || "Uncategorized", - ); const isShared = allSharedItems?.checklists.some( - (sharedChecklist) => - sharedChecklist.id === checklist.id && - sharedChecklist.category === encodedCategory, + (sharedChecklist) => sharedChecklist.uuid === checklist.uuid, ) || false; const { permissions } = usePermissions(); const { diff --git a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx index 15c0d273..a341bc7d 100644 --- a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx @@ -216,59 +216,29 @@ export const NoteEditorHeader = ({ const handlePermanentDecryption = async (newContent: string) => { const formData = new FormData(); - formData.append("id", note.id); formData.append("title", title); formData.append("content", newContent); formData.append("category", category); - formData.append("originalCategory", note.category || "Uncategorized"); - if (note.uuid) { - formData.append("uuid", note.uuid); - } + formData.append("uuid", note.uuid!); const result = await updateNote(formData); if (result.success && result.data) { - const categoryPath = buildCategoryPath( - result.data.category || t("notes.uncategorized"), - result.data.id - ); - const newPath = `/note/${categoryPath}`; - const currentPath = window.location.pathname; - - if (newPath === currentPath) { - window.location.reload(); - } else { - router.push(newPath); - } + window.location.reload(); } }; const handleEncryptionSuccess = async (newContent: string) => { const formData = new FormData(); - formData.append("id", note.id); formData.append("title", title); formData.append("content", newContent); formData.append("category", category); - formData.append("originalCategory", note.category || "Uncategorized"); - if (note.uuid) { - formData.append("uuid", note.uuid); - } + formData.append("uuid", note.uuid!); const result = await updateNote(formData); if (result.success && result.data) { - const categoryPath = buildCategoryPath( - result.data.category || t("notes.uncategorized"), - result.data.id - ); - const newPath = `/note/${categoryPath}`; - const currentPath = window.location.pathname; - - if (newPath === currentPath) { - window.location.reload(); - } else { - router.push(newPath); - } + window.location.reload(); } }; diff --git a/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx b/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx index 8d61a15e..1b40f66d 100644 --- a/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx @@ -6,7 +6,8 @@ import { Note } from "@/app/_types"; import { useAdjacentNotes } from "@/app/_hooks/useAdjacentNotes"; import { useSwipeNavigation } from "@/app/_hooks/useSwipeNavigation"; import { useNavigationGuard } from "@/app/_providers/NavigationGuardProvider"; -import { isMobileDevice, buildCategoryPath } from "@/app/_utils/global-utils"; +import { isMobileDevice, itemHref } from "@/app/_utils/global-utils"; +import { ItemTypes } from "@/app/_types/enums"; interface SwipeNavigationWrapperProps { children: ReactNode; @@ -16,8 +17,8 @@ interface SwipeNavigationWrapperProps { } const getNoteUrl = (note: Partial | null, embed = false): string | null => { - if (!note?.id) return null; - const base = `/note/${buildCategoryPath(note.category || "Uncategorized", note.id)}`; + if (!note?.uuid) return null; + const base = itemHref(ItemTypes.NOTE, note.uuid); return embed ? `${base}?embed=true` : base; }; diff --git a/app/_components/FeatureComponents/Sidebar/Parts/CategoryList.tsx b/app/_components/FeatureComponents/Sidebar/Parts/CategoryList.tsx index ad66b5aa..278c5500 100644 --- a/app/_components/FeatureComponents/Sidebar/Parts/CategoryList.tsx +++ b/app/_components/FeatureComponents/Sidebar/Parts/CategoryList.tsx @@ -22,12 +22,7 @@ import { moveNode } from "@/app/_server/actions/category"; import { CategoryRenderer } from "@/app/_components/FeatureComponents/Sidebar/Parts/CategoryRenderer"; import { DropIndicator } from "@/app/_components/FeatureComponents/Sidebar/Parts/DropIndicator"; import { useState } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { - buildCategoryPath, - encodeCategoryPath, -} from "@/app/_utils/global-utils"; -import { Modes } from "@/app/_types/enums"; +import { useRouter } from "next/navigation"; interface CategoryListProps { categories: Category[]; @@ -49,7 +44,6 @@ interface CategoryListProps { export const CategoryList = (props: CategoryListProps) => { const { categories, mode } = props; const [overTimeout, setOverTimeout] = useState(null); - const pathname = usePathname(); const router = useRouter(); const sensors = useSensors( @@ -117,23 +111,12 @@ export const CategoryList = (props: CategoryListProps) => { return; } - let currentItemPath: string | null = null; - if (activeNode.type === "item") { - const routePrefix = mode === Modes.CHECKLISTS ? "/checklist" : "/note"; - const currentCategoryPath = buildCategoryPath( - activeNode.category, - activeNode.id, - ); - currentItemPath = `${routePrefix}/${currentCategoryPath}`; - } - const formData = new FormData(); formData.append("mode", mode); formData.append("activeType", activeNode.type); if (activeNode.type === "item") { - formData.append("activeId", activeNode.id); - formData.append("activeItemCategory", activeNode.category); + formData.append("activeUuid", activeNode.uuid); } else { formData.append("activeCategoryPath", activeNode.categoryPath); } @@ -152,70 +135,7 @@ export const CategoryList = (props: CategoryListProps) => { await moveNode(formData); - if ( - activeNode.type === "item" && - currentItemPath && - pathname === currentItemPath - ) { - let newCategory = ""; - if (overNode.type === "category") { - newCategory = overNode.categoryPath; - } else if (overNode.type === "drop-indicator") { - newCategory = overNode.parentPath || "Uncategorized"; - } - - if (newCategory === "") { - newCategory = "Uncategorized"; - } - - const routePrefix = mode === Modes.CHECKLISTS ? "/checklist" : "/note"; - const newItemPath = `${routePrefix}/${buildCategoryPath( - newCategory, - activeNode.id, - )}`; - - router.push(newItemPath); - } else if (activeNode.type === "category" && pathname) { - const routePrefix = mode === Modes.CHECKLISTS ? "/checklist" : "/note"; - const oldCategoryPath = activeNode.categoryPath; - const categoryName = activeNode.categoryPath.split("/").pop() || ""; - - let newCategoryPath = ""; - if (overNode.type === "category") { - newCategoryPath = `${overNode.categoryPath}/${categoryName}`; - } else if (overNode.type === "drop-indicator") { - const parentPath = overNode.parentPath || ""; - newCategoryPath = - parentPath === "" ? categoryName : `${parentPath}/${categoryName}`; - } - - const oldCategoryUrl = `${routePrefix}/${encodeCategoryPath(oldCategoryPath)}/`; - const pathnameParts = pathname.split("/"); - - let itemPart = ""; - let matched = false; - - if (pathname.startsWith(oldCategoryUrl) && pathnameParts.length > 3) { - const categoryPathParts = - encodeCategoryPath(oldCategoryPath).split("/"); - const startIndex = - routePrefix.split("/").length + categoryPathParts.length; - itemPart = pathnameParts.slice(startIndex).join("/"); - matched = true; - } - - if (matched) { - const newPath = `${routePrefix}/${buildCategoryPath( - newCategoryPath, - decodeURIComponent(itemPart), - )}`; - router.push(newPath); - } else { - router.refresh(); - } - } else { - router.refresh(); - } + router.refresh(); }; return ( diff --git a/app/_components/FeatureComponents/Sidebar/Parts/CategoryRenderer.tsx b/app/_components/FeatureComponents/Sidebar/Parts/CategoryRenderer.tsx index ff0719de..c7ff9fed 100644 --- a/app/_components/FeatureComponents/Sidebar/Parts/CategoryRenderer.tsx +++ b/app/_components/FeatureComponents/Sidebar/Parts/CategoryRenderer.tsx @@ -116,8 +116,7 @@ export const CategoryRenderer = (props: CategoryRendererProps) => { const firstChildId = subCategories[0] ? `category::${subCategories[0].path}` : categoryItems[0] - ? `item::${categoryItems[0].category || "Uncategorized"}::${categoryItems[0].id - }` + ? `item::${categoryItems[0].uuid}` : undefined; return ( @@ -242,13 +241,12 @@ export const CategoryRenderer = (props: CategoryRendererProps) => { ))} {categoryItems.map((item) => ( -
+
{ /> diff --git a/app/_components/FeatureComponents/Sidebar/Parts/SharedItemsList.tsx b/app/_components/FeatureComponents/Sidebar/Parts/SharedItemsList.tsx index fc73d7ca..b5f24e1d 100644 --- a/app/_components/FeatureComponents/Sidebar/Parts/SharedItemsList.tsx +++ b/app/_components/FeatureComponents/Sidebar/Parts/SharedItemsList.tsx @@ -10,7 +10,7 @@ import { CheckmarkSquare04Icon, TaskDaily01Icon, } from "hugeicons-react"; -import { cn, buildCategoryPath } from "@/app/_utils/global-utils"; +import { cn, itemHref } from "@/app/_utils/global-utils"; import { AppMode, Checklist, Note } from "@/app/_types"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { capitalize } from "lodash"; @@ -87,9 +87,11 @@ export const SharedItemsList = ({ }); }; - const getItemHref = (item: Checklist | Note) => { - return `/${mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST}/${buildCategoryPath(item.category || "Uncategorized", item.id)}`; - }; + const getItemHref = (item: Checklist | Note) => + itemHref( + mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST, + item.uuid!, + ); const handleItemClick = (e: React.MouseEvent, item: Checklist | Note) => { if (checkWouldBlock()) { diff --git a/app/_components/FeatureComponents/Sidebar/Parts/SidebarItem.tsx b/app/_components/FeatureComponents/Sidebar/Parts/SidebarItem.tsx index 97c13b99..8e5a7ad9 100644 --- a/app/_components/FeatureComponents/Sidebar/Parts/SidebarItem.tsx +++ b/app/_components/FeatureComponents/Sidebar/Parts/SidebarItem.tsx @@ -17,7 +17,7 @@ import { Share08Icon, } from "hugeicons-react"; import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; -import { cn, buildCategoryPath } from "@/app/_utils/global-utils"; +import { cn, itemHref as itemUrl } from "@/app/_utils/global-utils"; import { DropdownMenu } from "@/app/_components/GlobalComponents/Dropdowns/DropdownMenu"; import { AppMode, Checklist, Note } from "@/app/_types"; import { isKanbanType, ItemTypes, Modes } from "@/app/_types/enums"; @@ -78,23 +78,20 @@ export const SidebarItem = ({ const [isTogglingPin, setIsTogglingPin] = useState(null); - const itemHref = `/${mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST}/${buildCategoryPath(item.category || "Uncategorized", item.id)}`; + const itemType = + mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST; + const itemHref = itemUrl(itemType, item.uuid!); const handleDeleteItem = async () => { const formData = new FormData(); + formData.append("uuid", item.uuid!); if (mode === Modes.CHECKLISTS) { - formData.append("id", item.id); - formData.append("category", item.category || "Uncategorized"); - if (item.uuid) formData.append("uuid", item.uuid); const result = await deleteList(formData); if (result.success) { router.refresh(); } } else { - formData.append("id", item.id); - formData.append("category", item.category || "Uncategorized"); - if (item.uuid) formData.append("uuid", item.uuid); const result = await deleteNote(formData); if (result.success) { router.refresh(); @@ -106,13 +103,9 @@ export const SidebarItem = ({ const handleTogglePin = async () => { if (!user || isTogglingPin) return; - setIsTogglingPin(item.id); + setIsTogglingPin(item.uuid!); try { - const result = await togglePin( - item.uuid || item.id, - item.category || "Uncategorized", - mode === Modes.CHECKLISTS ? ItemTypes.CHECKLIST : ItemTypes.NOTE, - ); + const result = await togglePin(item.uuid!, itemType); if (result.success) { router.refresh(); } @@ -129,10 +122,9 @@ export const SidebarItem = ({ mode === Modes.CHECKLISTS ? user.pinnedLists : user.pinnedNotes; if (!pinnedItems) return false; - const itemPath = `${item.category || "Uncategorized"}/${ - item.uuid || item.id - }`; - return pinnedItems.includes(itemPath); + return pinnedItems.some( + (entry) => entry === item.uuid || entry.split("/").pop() === item.uuid, + ); }; const dropdownItems = [ diff --git a/app/_components/FeatureComponents/Tags/TagHoverCard.tsx b/app/_components/FeatureComponents/Tags/TagHoverCard.tsx index 5cd992f8..fc6ee9fd 100644 --- a/app/_components/FeatureComponents/Tags/TagHoverCard.tsx +++ b/app/_components/FeatureComponents/Tags/TagHoverCard.tsx @@ -3,7 +3,8 @@ import { Note, Checklist } from "@/app/_types"; import { File02Icon, CheckmarkSquare04Icon } from "hugeicons-react"; import { useRouter } from "next/navigation"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; +import { ItemTypes } from "@/app/_types/enums"; import { capitalize } from "lodash"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { useTranslations } from "next-intl"; @@ -21,9 +22,7 @@ export const TagHoverCard = ({ notes, checklists }: TagHoverCardProps) => { const handleNoteClick = (e: React.MouseEvent, note: Note) => { e.preventDefault(); e.stopPropagation(); - router.push( - `/note/${buildCategoryPath(note.category || "Uncategorized", note.id)}`, - ); + router.push(itemHref(ItemTypes.NOTE, note.uuid!)); }; const handleChecklistClick = ( @@ -32,9 +31,7 @@ export const TagHoverCard = ({ notes, checklists }: TagHoverCardProps) => { ) => { e.preventDefault(); e.stopPropagation(); - router.push( - `/checklist/${buildCategoryPath(list.category || "Uncategorized", list.id!)}`, - ); + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid!)); }; const showSectionLabels = notes.length > 0 && checklists.length > 0; diff --git a/app/_consts/identity.ts b/app/_consts/identity.ts new file mode 100644 index 00000000..a0c14893 --- /dev/null +++ b/app/_consts/identity.ts @@ -0,0 +1,4 @@ +export const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export const isUuid = (value: string): boolean => UUID_REGEX.test(value); diff --git a/app/_consts/notes.ts b/app/_consts/notes.ts index 4147594d..542dadd4 100644 --- a/app/_consts/notes.ts +++ b/app/_consts/notes.ts @@ -2,6 +2,7 @@ import { Modes } from "../_types/enums"; export const NOTES_FOLDER = Modes.NOTES; export const DEPRECATED_DOCS_FOLDER = Modes.DEPRECATED_DOCS; +export const UNCATEGORIZED = "Uncategorized"; export const QUOTES = [ "Nothing... a whole lot of nothing.", diff --git a/app/_hooks/useNoteEditor.tsx b/app/_hooks/useNoteEditor.tsx index d23038d4..0ecbfa67 100644 --- a/app/_hooks/useNoteEditor.tsx +++ b/app/_hooks/useNoteEditor.tsx @@ -13,14 +13,10 @@ import { deleteNote, updateNote } from "@/app/_server/actions/note"; import { encryptNoteContent } from "@/app/_server/actions/pgp"; import { encryptXChaCha } from "@/app/_server/actions/xchacha"; import { logContentEvent, logAudit } from "@/app/_server/actions/log"; -import { - buildCategoryPath, - encodeCategoryPath, - encodeId, -} from "@/app/_utils/global-utils"; +import { itemHref, publicHref } from "@/app/_utils/global-utils"; import { Note } from "@/app/_types"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { getUserByNote } from "../_server/actions/users"; +import { ItemTypes } from "@/app/_types/enums"; import { extractYamlMetadata } from "@/app/_utils/yaml-metadata-utils"; import { ConfirmModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/ConfirmModal"; @@ -260,11 +256,12 @@ export const useNoteEditor = ({ } const formData = new FormData(); - formData.append("id", note.id); formData.append("title", useAutosave ? note.title : title); formData.append("content", contentToSave); - formData.append("category", useAutosave ? (note.category || "Uncategorized") : category); - formData.append("originalCategory", note.category || "Uncategorized"); + formData.append( + "category", + useAutosave ? note.category || "Uncategorized" : category + ); formData.append("user", note.owner || user?.username || ""); formData.append("uuid", note.uuid || ""); @@ -283,15 +280,11 @@ export const useNoteEditor = ({ setContentIsDirty(false); setProviderUnsaved(false); - const categoryPath = buildCategoryPath( - category || "Uncategorized", - result.data.id - ); - router.push(`/note/${categoryPath}`); + router.push(itemHref(ItemTypes.NOTE, note.uuid!)); } }, [ - note.id, + note.uuid, note.encryptionMethod, title, derivedMarkdownContent, @@ -402,14 +395,7 @@ export const useNoteEditor = ({ const handlePrint = () => { setIsPrinting(true); - const categoryUrlPath = - note.category && note.category !== "Uncategorized" - ? encodeCategoryPath(note.category) + "/" - : ""; - - const printUrl = `/public/note/${categoryUrlPath}${encodeId( - note.id - )}?view_mode=print`; + const printUrl = `${publicHref(ItemTypes.NOTE, note.uuid!)}?view_mode=print`; const iframe = document.createElement("iframe"); iframe.style.position = "absolute"; diff --git a/app/_hooks/useSearch.ts b/app/_hooks/useSearch.ts index 37261096..f8566ac7 100644 --- a/app/_hooks/useSearch.ts +++ b/app/_hooks/useSearch.ts @@ -4,7 +4,7 @@ import { AppMode, ItemType } from "@/app/_types"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { capitalize } from "lodash"; import { ItemTypes } from "@/app/_types/enums"; -import { encodeCategoryPath, encodeId } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; import { search } from "@/app/_server/actions/search"; interface useSearchProps { @@ -39,9 +39,9 @@ export const useSearch = ({ const handleSelectResult = useCallback( (result: SearchResult) => { - const targetPath = `/${result.type}/${encodeCategoryPath( - result.category || "Uncategorized" - )}/${encodeId(result.id)}`; + if (!result.uuid) return; + + const targetPath = itemHref(result.type as ItemTypes, result.uuid); const targetMode = `${result.type}s` as AppMode; if (mode !== targetMode && onModeChange) { diff --git a/app/_hooks/useSidebar.tsx b/app/_hooks/useSidebar.tsx index 7d60705a..cafc214b 100644 --- a/app/_hooks/useSidebar.tsx +++ b/app/_hooks/useSidebar.tsx @@ -4,7 +4,7 @@ import { useNavigationGuard } from "../_providers/NavigationGuardProvider"; import { useState, useEffect, useCallback, useMemo } from "react"; import { Checklist, Category, Note, AppMode, SanitisedUser } from "../_types"; import { ItemTypes, Modes } from "../_types/enums"; -import { buildCategoryPath } from "../_utils/global-utils"; +import { itemHref } from "../_utils/global-utils"; import { deleteCategory, renameCategory } from "../_server/actions/category"; import { useSidebarStore } from "../_utils/sidebar-store"; @@ -168,15 +168,14 @@ export const useSidebar = (props: SidebarProps) => { }; const isItemSelected = (item: Checklist | Note) => { - const expectedPath = buildCategoryPath( - item.category || "Uncategorized", - item.id - )?.toLowerCase(); + if (!item.uuid) return false; return ( pathname?.toLowerCase() === - `/${mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST - }/${expectedPath}`.toLowerCase() + itemHref( + mode === Modes.NOTES ? ItemTypes.NOTE : ItemTypes.CHECKLIST, + item.uuid, + ).toLowerCase() ); }; diff --git a/app/_providers/PermissionsProvider.tsx b/app/_providers/PermissionsProvider.tsx index ecfb47a8..0ded0da5 100644 --- a/app/_providers/PermissionsProvider.tsx +++ b/app/_providers/PermissionsProvider.tsx @@ -4,7 +4,6 @@ import { createContext, useContext, useMemo } from "react"; import { Checklist, Note } from "@/app/_types"; import { getPermissions } from "@/app/_utils/sharing-utils"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { Modes } from "../_types/enums"; const permissionsCache = new Map< @@ -46,8 +45,7 @@ export const PermissionsProvider = ({ const permissions = getPermissions( globalSharing, user?.username || "", - item.uuid || item.id, - encodeCategoryPath(item.category || "Uncategorized"), + item.uuid!, itemType, ); return { @@ -67,9 +65,9 @@ export const PermissionsProvider = ({ }; } - const cacheKey = `${user?.username || ""}-${item.uuid || item.id}-${encodeCategoryPath( - item.category || "Uncategorized", - )}-${JSON.stringify(globalSharing || {})}`; + const cacheKey = `${user?.username || ""}-${item.uuid}-${JSON.stringify( + globalSharing || {}, + )}`; const now = Date.now(); const cached = permissionsCache.get(cacheKey); @@ -81,8 +79,7 @@ export const PermissionsProvider = ({ const permissions = getPermissions( globalSharing, user?.username || "", - item.uuid || item.id, - encodeCategoryPath(item.category || "Uncategorized"), + item.uuid!, itemType, ); diff --git a/app/_server/actions/category/move.ts b/app/_server/actions/category/move.ts index c996be6d..0667c21c 100644 --- a/app/_server/actions/category/move.ts +++ b/app/_server/actions/category/move.ts @@ -32,11 +32,26 @@ const _nameExtraction = ( ): string => { if (!dndId) return ""; const parts = dndId.split("::"); - if (type === "item") return parts[2] || ""; + if (type === "item") return parts[1] || ""; if (type === "category") return (parts[1] || "").split("/").pop() || ""; return ""; }; +const _findItemByUuid = async ( + baseDir: string, + uuid: string, +): Promise<{ id: string; category: string } | null> => { + if (!uuid) return null; + + const { grepFindFileByUuid } = await import("@/app/_utils/grep-utils"); + const absDir = path.isAbsolute(baseDir) + ? baseDir + : path.join(process.cwd(), baseDir); + const found = await grepFindFileByUuid(absDir, uuid); + + return found ? { id: found.id, category: found.category } : null; +}; + const _reorderList = ( list: string[], activeName: string, @@ -92,8 +107,15 @@ export const moveNode = async (formData: FormData) => { let activeParentPath: string; if (activeType === "item") { - activeName = formData.get("activeId") as string; - activeParentPath = formData.get("activeItemCategory") as string; + const activeUuid = formData.get("activeUuid") as string; + const found = await _findItemByUuid(baseDir, activeUuid); + + if (!found) { + return { error: "Item not found" }; + } + + activeName = found.id; + activeParentPath = found.category; } else { const catPath = formData.get("activeCategoryPath") as string; @@ -130,7 +152,14 @@ export const moveNode = async (formData: FormData) => { if (targetDndId) { if (targetType === activeType) { - targetName = _nameExtraction(targetDndId, targetType); + const extracted = _nameExtraction(targetDndId, targetType); + + if (targetType === "item") { + const target = await _findItemByUuid(baseDir, extracted); + targetName = target?.id || null; + } else { + targetName = extracted; + } } else { crossTypeTarget = true; } diff --git a/app/_server/actions/checklist-item/archive.ts b/app/_server/actions/checklist-item/archive.ts index d51444b0..49a3190f 100644 --- a/app/_server/actions/checklist-item/archive.ts +++ b/app/_server/actions/checklist-item/archive.ts @@ -12,6 +12,7 @@ import { import { listToMarkdown } from "@/app/_utils/checklist-utils"; import { getUsername } from "@/app/_server/actions/users"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { Checklist, Result } from "@/app/_types"; import { ItemTypes, @@ -26,7 +27,6 @@ export const archiveItem = async ( try { const listId = formData.get("listId") as string; const itemId = formData.get("itemId") as string; - const category = formData.get("category") as string; const currentUser = await getUsername(); @@ -34,14 +34,13 @@ export const archiveItem = async ( return { success: false, error: "List ID and item ID are required" }; } - const list = await getListById(listId, currentUser, category); + const list = await getListById(listId, currentUser); if (!list) { return { success: false, error: "List not found" }; } const canEdit = await checkUserPermission( - list.uuid || listId, - category, + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT @@ -86,16 +85,16 @@ export const archiveItem = async ( CHECKLISTS_FOLDER, list.owner! ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList)); try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -103,7 +102,7 @@ export const archiveItem = async ( ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: currentUser }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); return { success: true, data: updatedList as Checklist }; } catch (error) { @@ -118,7 +117,6 @@ export const unarchiveItem = async ( try { const listId = formData.get("listId") as string; const itemId = formData.get("itemId") as string; - const category = formData.get("category") as string; const currentUser = await getUsername(); @@ -126,14 +124,13 @@ export const unarchiveItem = async ( return { success: false, error: "List ID and item ID are required" }; } - const list = await getListById(listId, currentUser, category); + const list = await getListById(listId, currentUser); if (!list) { return { success: false, error: "List not found" }; } const canEdit = await checkUserPermission( - list.uuid || listId, - category, + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT @@ -182,16 +179,16 @@ export const unarchiveItem = async ( CHECKLISTS_FOLDER, list.owner! ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList)); try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -199,7 +196,7 @@ export const unarchiveItem = async ( ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: currentUser }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); return { success: true, data: updatedList as Checklist }; } catch (error) { diff --git a/app/_server/actions/checklist-item/bulk-operations.ts b/app/_server/actions/checklist-item/bulk-operations.ts index fb4d36ea..6b12dab0 100644 --- a/app/_server/actions/checklist-item/bulk-operations.ts +++ b/app/_server/actions/checklist-item/bulk-operations.ts @@ -7,7 +7,6 @@ import { serverWriteFile, } from "@/app/_server/actions/file"; import { - getUserChecklists, getListById, } from "@/app/_server/actions/checklist"; import { @@ -16,6 +15,7 @@ import { } from "@/app/_utils/checklist-utils"; import { getUsername } from "@/app/_server/actions/users"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { Checklist, Result } from "@/app/_types"; import { ItemTypes, @@ -33,24 +33,16 @@ export const createBulkItems = async ( try { const listId = formData.get("listId") as string; const itemsText = formData.get("itemsText") as string; - const category = formData.get("category") as string; - const lists = await getUserChecklists(); - if (!lists.success || !lists.data) { - throw new Error(lists.error || "Failed to fetch lists"); - } + const currentUser = await getUsername(); - const list = lists.data.find( - (l) => l.id === listId && (!category || l.category === category) - ); + const list = await getListById(listId, currentUser); if (!list) { throw new Error("List not found"); } - const currentUser = await getUsername(); const canEdit = await checkUserPermission( - list.uuid || listId, - category || "Uncategorized", + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT @@ -106,15 +98,15 @@ export const createBulkItems = async ( ); filePath = path.join( ownerDir, - list.category || "Uncategorized", - `${listId}.md` + list.category || UNCATEGORIZED, + `${list.id}.md` ); } else { const userDir = await getUserModeDir(Modes.CHECKLISTS); filePath = path.join( userDir, - list.category || "Uncategorized", - `${listId}.md` + list.category || UNCATEGORIZED, + `${list.id}.md` ); } @@ -129,7 +121,7 @@ export const createBulkItems = async ( ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: currentUser }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); return { success: true, data: updatedList as Checklist }; } catch (error) { @@ -145,7 +137,6 @@ export const bulkToggleItems = async ( const completed = formData.get("completed") === "true"; const itemIdsStr = formData.get("itemIds") as string; const completedStatesStr = formData.get("completedStates") as string; - const category = formData.get("category") as string; let currentUser = formData.get("username") as string; if (!currentUser) { @@ -161,14 +152,13 @@ export const bulkToggleItems = async ( ? JSON.parse(completedStatesStr) : null; - const list = await getListById(listId, currentUser, category); + const list = await getListById(listId, currentUser); if (!list) { return { success: false, error: "List not found" }; } const canEdit = await checkUserPermission( - list.uuid || listId, - category, + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT @@ -286,15 +276,15 @@ export const bulkToggleItems = async ( ); filePath = path.join( ownerDir, - list.category || "Uncategorized", - `${listId}.md` + list.category || UNCATEGORIZED, + `${list.id}.md` ); } else { const userDir = await getUserModeDir(Modes.CHECKLISTS); filePath = path.join( userDir, - list.category || "Uncategorized", - `${listId}.md` + list.category || UNCATEGORIZED, + `${list.id}.md` ); } @@ -308,7 +298,7 @@ export const bulkToggleItems = async ( error ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: currentUser }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); return { success: true, data: updatedList as Checklist }; } catch (error) { @@ -324,7 +314,6 @@ export const bulkDeleteItems = async ( const listId = formData.get("listId") as string; const itemIdsStr = formData.get("itemIds") as string; const itemIdsToDelete = JSON.parse(itemIdsStr) as string[]; - const category = formData.get("category") as string; let currentUser = formData.get("username") as string; if (!currentUser) { @@ -335,14 +324,13 @@ export const bulkDeleteItems = async ( return { success: true }; } - const list = await getListById(listId, currentUser, category); + const list = await getListById(listId, currentUser); if (!list) { return { success: false, error: "List not found" }; } const canEdit = await checkUserPermission( - list.uuid || listId, - category, + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT diff --git a/app/_server/actions/checklist-item/crud.ts b/app/_server/actions/checklist-item/crud.ts index fc2e571a..7f2ee57f 100644 --- a/app/_server/actions/checklist-item/crud.ts +++ b/app/_server/actions/checklist-item/crud.ts @@ -7,11 +7,8 @@ import { serverWriteFile, ensureDir, } from "@/app/_server/actions/file"; -import { - getAllLists, - getUserChecklists, - getListById, -} from "@/app/_server/actions/checklist"; +import { getListById } from "@/app/_server/actions/checklist"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { listToMarkdown, areAllItemsCompleted, @@ -41,18 +38,15 @@ export const updateItem = async ( skipRevalidation = false, ): Promise> => { try { - const listId = formData.get("listId") as string; const itemId = formData.get("itemId") as string; const completedRaw = formData.get("completed"); const text = formData.get("text") as string; const description = formData.get("description") as string; - const category = formData.get("category") as string; const currentUser = username || (await getUsername()); const canEdit = await checkUserPermission( - checklist.uuid || listId, - category || "Uncategorized", + checklist.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT, @@ -136,18 +130,18 @@ export const updateItem = async ( ); const categoryDir = path.join( ownerDir, - checklist.category || "Uncategorized", + checklist.category || UNCATEGORIZED, ); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${checklist.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList)); if (!skipRevalidation) { try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${checklist.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -159,7 +153,7 @@ export const updateItem = async ( await broadcast({ type: "checklist", action: "updated", - entityId: listId, + entityId: checklist.uuid, username: currentUser, }); @@ -184,18 +178,15 @@ export const createItem = async ( skipRevalidation = false, ) => { try { - const listId = formData.get("listId") as string; const text = formData.get("text") as string; const status = formData.get("status") as string; const timeStr = formData.get("time") as string; - const category = formData.get("category") as string; const description = formData.get("description") as string; const currentUser = username || (await getUsername()); const recurrenceStr = formData.get("recurrence") as string; const canEdit = await checkUserPermission( - list.uuid || listId, - category || "Uncategorized", + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT, @@ -256,7 +247,7 @@ export const createItem = async ( let isSharedBoard = false; if (isKanbanType(list.type)) { const { getUsersWithAccess } = await import("@/app/_server/actions/sharing"); - const sharedUsers = await getUsersWithAccess(listId, list.uuid); + const sharedUsers = await getUsersWithAccess(list.uuid!); isSharedBoard = sharedUsers.length > 0; } @@ -266,7 +257,7 @@ export const createItem = async ( })); const newItem = { - id: `${listId}-${Date.now()}`, + id: `${list.uuid}-${Date.now()}`, text, completed: false, order: 0, @@ -310,18 +301,18 @@ export const createItem = async ( CHECKLISTS_FOLDER, list.owner!, ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList as Checklist)); if (!skipRevalidation) { try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -333,7 +324,7 @@ export const createItem = async ( await broadcast({ type: "checklist", action: "updated", - entityId: listId, + entityId: list.uuid, username: currentUser, }); @@ -353,24 +344,15 @@ export const deleteItem = async ( try { const listId = formData.get("listId") as string; const itemId = formData.get("itemId") as string; - const category = formData.get("category") as string; - - const lists = await getUserChecklists(); - if (!lists.success || !lists.data) { - throw new Error(lists.error || "Failed to fetch lists"); - } - const list = lists.data.find( - (l) => l.id === listId && (!category || l.category === category), - ); + const currentUser = await getUsername(); + const list = await getListById(listId, currentUser); if (!list) { throw new Error("List not found"); } - const currentUser = await getUsername(); const canDelete = await checkUserPermission( - list.uuid || listId, - category || "Uncategorized", + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.DELETE, @@ -431,15 +413,15 @@ export const deleteItem = async ( ); filePath = path.join( ownerDir, - list.category || "Uncategorized", - `${listId}.md`, + list.category || UNCATEGORIZED, + `${list.id}.md`, ); } else { const userDir = await getUserModeDir(Modes.CHECKLISTS); filePath = path.join( userDir, - list.category || "Uncategorized", - `${listId}.md`, + list.category || UNCATEGORIZED, + `${list.id}.md`, ); } @@ -447,7 +429,7 @@ export const deleteItem = async ( try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -458,7 +440,7 @@ export const deleteItem = async ( await broadcast({ type: "checklist", action: "updated", - entityId: listId, + entityId: list.uuid, username: currentUser, }); diff --git a/app/_server/actions/checklist-item/drop.ts b/app/_server/actions/checklist-item/drop.ts index 4c6981f8..c933f400 100644 --- a/app/_server/actions/checklist-item/drop.ts +++ b/app/_server/actions/checklist-item/drop.ts @@ -10,6 +10,7 @@ import { broadcast } from "@/app/_server/ws/broadcast"; import { applyDrop } from "@/app/_utils/kanban/board-utils"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { DEFAULT_KANBAN_STATUSES } from "@/app/_consts/kanban"; import { Checklist, Result } from "@/app/_types"; import { ItemTypes, PermissionTypes } from "@/app/_types/enums"; @@ -48,8 +49,7 @@ export const dropItem = async ( } const canEdit = await checkUserPermission( - list.uuid || list.id, - list.category || "Uncategorized", + list.uuid!, ItemTypes.CHECKLIST, username, PermissionTypes.EDIT, @@ -86,7 +86,7 @@ export const dropItem = async ( "data", CHECKLISTS_FOLDER, list.owner!, - list.category || "Uncategorized", + list.category || UNCATEGORIZED, ); await ensureDir(categoryDir); await serverWriteFile( @@ -96,7 +96,7 @@ export const dropItem = async ( try { revalidatePath("/"); - revalidatePath(`/checklist/${list.id}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -108,7 +108,7 @@ export const dropItem = async ( await broadcast({ type: "checklist", action: "updated", - entityId: list.id, + entityId: list.uuid, username, }); } catch (error) { diff --git a/app/_server/actions/checklist-item/reorder.ts b/app/_server/actions/checklist-item/reorder.ts index 46b750d6..5a1cdff1 100644 --- a/app/_server/actions/checklist-item/reorder.ts +++ b/app/_server/actions/checklist-item/reorder.ts @@ -6,13 +6,11 @@ import { serverWriteFile, ensureDir, } from "@/app/_server/actions/file"; -import { - getAllLists, - getUserChecklists, -} from "@/app/_server/actions/checklist"; +import { getListById } from "@/app/_server/actions/checklist"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; -import { isAdmin, getUsername } from "@/app/_server/actions/users"; +import { getUsername } from "@/app/_server/actions/users"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { broadcast } from "@/app/_server/ws/broadcast"; export const reorderItems = async (formData: FormData) => { @@ -20,19 +18,11 @@ export const reorderItems = async (formData: FormData) => { const listId = formData.get("listId") as string; const activeItemId = formData.get("activeItemId") as string; const overItemId = formData.get("overItemId") as string; - const category = formData.get("category") as string; const isDropInto = formData.get("isDropInto") === "true"; const position = (formData.get("position") as string) || "before"; - const isAdminUser = await isAdmin(); - const lists = await (isAdminUser ? getAllLists() : getUserChecklists()); - if (!lists.success || !lists.data) { - throw new Error(lists.error || "Failed to fetch lists"); - } - - const list = lists.data.find( - (l) => l.id === listId && (!category || l.category === category) - ); + const currentUser = await getUsername(); + const list = await getListById(listId, currentUser); if (!list) { throw new Error("List not found"); } @@ -140,10 +130,10 @@ export const reorderItems = async (formData: FormData) => { CHECKLISTS_FOLDER, list.owner! ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); const markdownContent = listToMarkdown(updatedList as any); @@ -151,7 +141,7 @@ export const reorderItems = async (formData: FormData) => { try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -159,7 +149,7 @@ export const reorderItems = async (formData: FormData) => { ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: (await getUsername()) }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/app/_server/actions/checklist-item/status.ts b/app/_server/actions/checklist-item/status.ts index d2efc3cc..52a4910c 100644 --- a/app/_server/actions/checklist-item/status.ts +++ b/app/_server/actions/checklist-item/status.ts @@ -12,6 +12,7 @@ import { import { listToMarkdown } from "@/app/_utils/checklist-utils"; import { getUsername } from "@/app/_server/actions/users"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { Checklist, Result, TimeEntry } from "@/app/_types"; import { ItemTypes, @@ -31,7 +32,6 @@ export const updateItemStatus = async ( const itemId = formData.get("itemId") as string; const status = formData.get("status") as string; const timeEntriesStr = formData.get("timeEntries") as string; - const category = formData.get("category") as string; const formDataUsername = formData.get("username") as string; const username = @@ -71,14 +71,13 @@ export const updateItemStatus = async ( } } - const list = await getListById(listId, username, category); + const list = await getListById(listId, username); if (!list) { return { success: false, error: "List not found" }; } const canEdit = await checkUserPermission( - list.uuid || listId, - category, + list.uuid!, ItemTypes.CHECKLIST, username, PermissionTypes.EDIT @@ -120,23 +119,23 @@ export const updateItemStatus = async ( CHECKLISTS_FOLDER, list.owner! ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList)); try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", error ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username }); return { success: true, data: updatedList as Checklist }; } catch (error) { diff --git a/app/_server/actions/checklist-item/sub-items.ts b/app/_server/actions/checklist-item/sub-items.ts index 0795b95c..49d313d6 100644 --- a/app/_server/actions/checklist-item/sub-items.ts +++ b/app/_server/actions/checklist-item/sub-items.ts @@ -6,13 +6,11 @@ import { serverWriteFile, ensureDir, } from "@/app/_server/actions/file"; -import { - getAllLists, - getUserChecklists, -} from "@/app/_server/actions/checklist"; +import { getListById } from "@/app/_server/actions/checklist"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; -import { isAdmin, getUsername } from "@/app/_server/actions/users"; +import { getUsername } from "@/app/_server/actions/users"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { Checklist, Result } from "@/app/_types"; import { ItemTypes, @@ -30,25 +28,15 @@ export const createSubItem = async ( const listId = formData.get("listId") as string; const parentId = formData.get("parentId") as string; const text = formData.get("text") as string; - const category = formData.get("category") as string; - - const isAdminUser = await isAdmin(); - const lists = await (isAdminUser ? getAllLists() : getUserChecklists()); - if (!lists.success || !lists.data) { - throw new Error(lists.error || "Failed to fetch lists"); - } - const list = lists.data.find( - (l) => l.id === listId && (!category || l.category === category) - ); + const currentUser = await getUsername(); + const list = await getListById(listId, currentUser); if (!list) { throw new Error("List not found"); } - const currentUser = await getUsername(); const canEdit = await checkUserPermission( - list.uuid || listId, - category || "Uncategorized", + list.uuid!, ItemTypes.CHECKLIST, currentUser, PermissionTypes.EDIT @@ -85,7 +73,7 @@ export const createSubItem = async ( const now = new Date().toISOString(); const newSubItem: any = { - id: `${listId}-sub-${Date.now()}`, + id: `${list.uuid}-sub-${Date.now()}`, text, completed: false, order: 0, @@ -134,16 +122,16 @@ export const createSubItem = async ( CHECKLISTS_FOLDER, list.owner! ); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); - const filePath = path.join(categoryDir, `${listId}.md`); + const filePath = path.join(categoryDir, `${list.id}.md`); await serverWriteFile(filePath, listToMarkdown(updatedList as Checklist)); try { revalidatePath("/"); - revalidatePath(`/checklist/${listId}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -151,7 +139,7 @@ export const createSubItem = async ( ); } - await broadcast({ type: "checklist", action: "updated", entityId: listId, username: currentUser }); + await broadcast({ type: "checklist", action: "updated", entityId: list.uuid, username: currentUser }); return { success: true, data: updatedList as Checklist }; } catch (error) { diff --git a/app/_server/actions/checklist/converters.ts b/app/_server/actions/checklist/converters.ts index dca85830..90bdd49e 100644 --- a/app/_server/actions/checklist/converters.ts +++ b/app/_server/actions/checklist/converters.ts @@ -14,7 +14,8 @@ import { getCurrentUser } from "@/app/_server/actions/users"; import { getUserModeDir, serverWriteFile } from "@/app/_server/actions/file"; import { revalidatePath } from "next/cache"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; -import { buildCategoryPath, getFormData } from "@/app/_utils/global-utils"; +import { getFormData } from "@/app/_utils/global-utils"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { updateIndexForItem, parseInternalLinks, @@ -57,7 +58,7 @@ export const convertChecklistType = async (formData: FormData) => { } let filePath: string; - const categoryDir = list.category || "Uncategorized"; + const categoryDir = list.category || UNCATEGORIZED; const filename = `${list.id}.md`; if (list.owner) { @@ -140,7 +141,7 @@ export const convertChecklistType = async (formData: FormData) => { await broadcast({ type: "checklist", action: "updated", - entityId: updatedList.uuid || updatedList.id, + entityId: updatedList.uuid, username: currentUser?.username || "", }); return { success: true, data: updatedList }; @@ -227,14 +228,14 @@ export const updateChecklistStatuses = async (formData: FormData) => { ); filePath = path.join( ownerDir, - list.category || "Uncategorized", + list.category || UNCATEGORIZED, `${list.id}.md`, ); } else { const userDir = await getUserModeDir(Modes.CHECKLISTS); filePath = path.join( userDir, - list.category || "Uncategorized", + list.category || UNCATEGORIZED, `${list.id}.md`, ); } @@ -251,7 +252,7 @@ export const updateChecklistStatuses = async (formData: FormData) => { try { revalidatePath("/", "layout"); - revalidatePath(`/checklist/${list.id}`); + revalidatePath(`/checklist/${list.uuid}`); if (list.category) { revalidatePath(`/category/${list.category}`); } @@ -270,8 +271,7 @@ export const updateChecklistStatuses = async (formData: FormData) => { export const clearAllChecklistItems = async (formData: FormData) => { try { - const id = formData.get("id") as string; - const category = formData.get("category") as string; + const uuid = formData.get("uuid") as string; const ownerUsername = formData.get("user") as string | null; const type = formData.get("type") as "completed" | "incomplete"; const apiUser = formData.get("apiUser") as string | null; @@ -289,19 +289,14 @@ export const clearAllChecklistItems = async (formData: FormData) => { return { error: "Not authenticated" }; } - const checklist = await getListById( - id, - ownerUsername || undefined, - category, - ); + const checklist = await getListById(uuid, ownerUsername || undefined); if (!checklist) { return { error: "Checklist not found" }; } const canEdit = await checkUserPermission( - id, - category, + checklist.uuid!, ItemTypes.CHECKLIST, actingUser.username, PermissionTypes.EDIT, @@ -333,7 +328,7 @@ export const clearAllChecklistItems = async (formData: FormData) => { ); const categoryDir = path.join( ownerDir, - checklist.category || "Uncategorized", + checklist.category || UNCATEGORIZED, ); const filePath = path.join(categoryDir, `${checklist.id}.md`); @@ -358,11 +353,7 @@ export const clearAllChecklistItems = async (formData: FormData) => { try { revalidatePath("/"); - const categoryPath = buildCategoryPath( - checklist.category || "Uncategorized", - checklist.id, - ); - revalidatePath(`/checklist/${categoryPath}`); + revalidatePath(`/checklist/${checklist.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", diff --git a/app/_server/actions/checklist/crud.ts b/app/_server/actions/checklist/crud.ts index 68328346..a4a365e1 100644 --- a/app/_server/actions/checklist/crud.ts +++ b/app/_server/actions/checklist/crud.ts @@ -14,7 +14,8 @@ import { import { revalidatePath } from "next/cache"; import { generateUniqueFilename, sanitizeFilename } from "@/app/_utils/filename-utils"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; -import { buildCategoryPath, getFormData } from "@/app/_utils/global-utils"; +import { getFormData } from "@/app/_utils/global-utils"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { updateIndexForItem, parseInternalLinks, @@ -30,7 +31,7 @@ import { broadcast } from "@/app/_server/ws/broadcast"; export const createList = async (formData: FormData) => { try { const title = formData.get("title") as string; - const category = (formData.get("category") as string) || "Uncategorized"; + const category = (formData.get("category") as string) || UNCATEGORIZED; const type = (formData.get("type") as ChecklistType) || "simple"; const userParam = formData.get("user") as string | null; @@ -123,12 +124,9 @@ export const createList = async (formData: FormData) => { export const updateList = async (formData: FormData) => { try { - const id = formData.get("id") as string; - const uuid = formData.get("uuid") as string | null; + const uuid = formData.get("uuid") as string; const title = formData.get("title") as string; const category = formData.get("category") as string; - const originalCategory = formData.get("originalCategory") as string; - const unarchive = formData.get("unarchive") as string; const apiUser = formData.get("apiUser") as string | null; let actingUser = await getCurrentUser(); @@ -144,20 +142,14 @@ export const updateList = async (formData: FormData) => { return { error: "Not authenticated" }; } - const currentList = await getListById( - uuid || id, - undefined, - originalCategory, - unarchive === "true" - ); + const currentList = await getListById(uuid); if (!currentList) { throw new Error("List not found"); } const canEdit = await checkUserPermission( - currentList.uuid || id, - originalCategory, + currentList.uuid!, ItemTypes.CHECKLIST, actingUser.username, PermissionTypes.EDIT @@ -183,16 +175,17 @@ export const updateList = async (formData: FormData) => { ); const categoryDir = path.join( ownerDir, - updatedList.category || "Uncategorized" + updatedList.category || UNCATEGORIZED ); await ensureDir(categoryDir); + const currentId = currentList.id; let newFilename: string; - let newId = id; + let newId = currentId; const fileRenameMode = actingUser?.fileRenameMode || "minimal"; const sanitizedTitle = sanitizeFilename(title, fileRenameMode); - const currentFilename = `${id}.md`; + const currentFilename = `${currentId}.md`; const expectedFilename = `${sanitizedTitle}.md`; if (title !== currentList.title || currentFilename !== expectedFilename) { @@ -204,27 +197,24 @@ export const updateList = async (formData: FormData) => { ); newId = path.basename(newFilename, ".md"); } else { - newFilename = `${id}.md`; + newFilename = `${currentId}.md`; } - if (newId !== id) { + if (newId !== currentId) { updatedList.id = newId; } const filePath = path.join(categoryDir, newFilename); let oldFilePath: string | null = null; - if (category && category !== currentList.category) { - oldFilePath = path.join( - ownerDir, - currentList.category || "Uncategorized", - `${id}.md` - ); - } else if (newId !== id) { + if ( + (category && category !== currentList.category) || + newId !== currentId + ) { oldFilePath = path.join( ownerDir, - currentList.category || "Uncategorized", - `${id}.md` + currentList.category || UNCATEGORIZED, + `${currentId}.md` ); } @@ -233,11 +223,11 @@ export const updateList = async (formData: FormData) => { try { const content = updatedList.items.map((i) => i.text).join("\n"); const links = await parseInternalLinks(content); - const newItemKey = `${updatedList.category || "Uncategorized"}/${ + const newItemKey = `${updatedList.category || UNCATEGORIZED}/${ updatedList.id }`; - const oldItemKey = `${currentList.category || "Uncategorized"}/${id}`; + const oldItemKey = `${currentList.category || UNCATEGORIZED}/${currentId}`; if (oldItemKey !== newItemKey) { await rebuildLinkIndex(currentList.owner!); revalidatePath("/"); @@ -257,47 +247,13 @@ export const updateList = async (formData: FormData) => { ); } - if (newId !== id || (category && category !== currentList.category)) { - const { updateSharingData } = await import( - "@/app/_server/actions/sharing" - ); - - await updateSharingData( - { - id, - category: currentList.category || "Uncategorized", - itemType: ItemTypes.CHECKLIST, - sharer: currentList.owner!, - }, - { - id: newId, - category: updatedList.category || "Uncategorized", - itemType: ItemTypes.CHECKLIST, - sharer: currentList.owner!, - } - ); - } - if (oldFilePath && oldFilePath !== filePath) { await serverDeleteFile(oldFilePath); } try { revalidatePath("/"); - const oldCategoryPath = buildCategoryPath( - currentList.category || "UncategorCgorized", - id - ); - const newCategoryPath = buildCategoryPath( - updatedList.category || "Uncategorized", - newId !== id ? newId : id - ); - - revalidatePath(`/checklist/${oldCategoryPath}`); - - if (newId !== id || currentList.category !== updatedList.category) { - revalidatePath(`/checklist/${newCategoryPath}`); - } + revalidatePath(`/checklist/${currentList.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -334,11 +290,8 @@ export const updateList = async (formData: FormData) => { export const deleteList = async (formData: FormData) => { try { - const id = formData.get("id") as string; - const category = (formData.get("category") as string) || "Uncategorized"; - const uuid = formData.get("uuid") as string | null; + const uuid = formData.get("uuid") as string; const apiUser = formData.get("apiUser") as string | null; - const itemIdentifier = uuid || id; let currentUser = await getCurrentUser(); if (!currentUser && apiUser) { @@ -353,19 +306,14 @@ export const deleteList = async (formData: FormData) => { return { error: "Not authenticated" }; } - const list = await getListById( - itemIdentifier, - undefined, - category - ); + const list = await getListById(uuid); if (!list) { return { error: "List not found" }; } const canDelete = await checkUserPermission( - list.uuid || itemIdentifier, - category, + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.DELETE @@ -384,7 +332,7 @@ export const deleteList = async (formData: FormData) => { ); const filePath = path.join( ownerDir, - list.category || "Uncategorized", + list.category || UNCATEGORIZED, `${list.id}.md` ); @@ -407,8 +355,6 @@ export const deleteList = async (formData: FormData) => { await updateSharingData( { uuid: list.uuid, - id: list.id, - category: list.category || "Uncategorized", itemType: ItemTypes.CHECKLIST, sharer: list.owner, }, @@ -418,11 +364,7 @@ export const deleteList = async (formData: FormData) => { try { revalidatePath("/"); - const categoryPath = buildCategoryPath( - list.category || "Uncategorized", - list.id - ); - revalidatePath(`/checklist/${categoryPath}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -457,16 +399,11 @@ export const deleteList = async (formData: FormData) => { export const cloneChecklist = async (formData: FormData) => { try { - const id = formData.get("id") as string; - const originalCategory = formData.get("originalCategory") as string | null; + const uuid = formData.get("uuid") as string; const targetCategory = formData.get("category") as string; const ownerUsername = formData.get("user") as string | null; - const checklist = await getListById( - id, - ownerUsername || undefined, - originalCategory || undefined - ); + const checklist = await getListById(uuid, ownerUsername || undefined); if (!checklist) { return { error: "Checklist not found" }; } @@ -477,8 +414,8 @@ export const cloneChecklist = async (formData: FormData) => { const isOwnedByCurrentUser = !checklist.owner || checklist.owner === currentUser?.username; const finalTargetCategory = isOwnedByCurrentUser - ? targetCategory || "Uncategorized" - : "Uncategorized"; + ? targetCategory || UNCATEGORIZED + : UNCATEGORIZED; const categoryDir = path.join(userDir, finalTargetCategory); await ensureDir(categoryDir); @@ -494,8 +431,9 @@ export const cloneChecklist = async (formData: FormData) => { const filePath = path.join(categoryDir, filename); const content = listToMarkdown(checklist); + const cloneUuid = generateUuid(); const updatedContent = updateYamlMetadata(content, { - uuid: generateUuid(), + uuid: cloneUuid, title: cloneTitle, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -503,12 +441,7 @@ export const cloneChecklist = async (formData: FormData) => { await serverWriteFile(filePath, updatedContent); - const newId = path.basename(filename, ".md"); - const clonedChecklist = await getListById( - newId, - currentUser?.username, - finalTargetCategory - ); + const clonedChecklist = await getListById(cloneUuid, currentUser?.username); try { revalidatePath("/"); @@ -519,7 +452,7 @@ export const cloneChecklist = async (formData: FormData) => { ); } - await broadcast({ type: "checklist", action: "created", entityId: newId, username: currentUser?.username || "" }); + await broadcast({ type: "checklist", action: "created", entityId: cloneUuid, username: currentUser?.username || "" }); return { success: true, data: clonedChecklist }; } catch (error) { diff --git a/app/_server/actions/checklist/queries.ts b/app/_server/actions/checklist/queries.ts index e23e7958..0098fb1d 100644 --- a/app/_server/actions/checklist/queries.ts +++ b/app/_server/actions/checklist/queries.ts @@ -4,6 +4,7 @@ import path from "path"; import fs from "fs/promises"; import { Checklist, User, GetChecklistsOptions } from "@/app/_types"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { USERS_FILE } from "@/app/_consts/files"; import { Modes } from "@/app/_types/enums"; import { getCurrentUser } from "@/app/_server/actions/users"; @@ -12,9 +13,7 @@ import { readJsonFile } from "@/app/_server/actions/file"; import { parseChecklistContent } from "@/app/_utils/client-parser-utils"; import { extractChecklistType, - generateUuid, toIso, - updateYamlMetadata, } from "@/app/_utils/yaml-metadata-utils"; import { readListsRecursively, type ChecklistReadResult } from "./readers"; import { checkAndRefreshRecurringItems } from "./parsers"; @@ -104,7 +103,7 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { for (const sharedItem of sharedData.checklists) { try { - const itemIdentifier = sharedItem.uuid || sharedItem.id; + const itemIdentifier = sharedItem.uuid; if (!itemIdentifier) continue; const sharerDir = path.join( @@ -140,7 +139,7 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { ); const sharedChecklist = sharerLists.find( - (list) => list.uuid === itemIdentifier || list.id === itemIdentifier, + (list) => list.uuid === itemIdentifier, ); if (sharedChecklist) { @@ -151,7 +150,7 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { } } catch (error) { console.error( - `Error reading shared checklist ${sharedItem.uuid || sharedItem.id}:`, + `Error reading shared checklist ${sharedItem.uuid}:`, error, ); continue; @@ -161,7 +160,7 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { if (filter) { if (filter.type === "category") { lists = lists.filter((list: any) => { - const listCategory = list.category || "Uncategorized"; + const listCategory = list.category || UNCATEGORIZED; return ( listCategory === filter.value || listCategory.startsWith(filter.value + "/") @@ -195,9 +194,8 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { limitNum !== undefined ) { const pathMatches = (list: ChecklistReadResult, p: string) => { - const c = list.category || "Uncategorized"; const u = (list as { uuid?: string }).uuid || list.id; - return `${c}/${u}` === p || `${c}/${list.id}` === p; + return p === u || p.split("/").pop() === u; }; const pinned: ChecklistReadResult[] = []; for (const p of pinnedPaths) { @@ -257,10 +255,8 @@ export const getUserChecklists = async (options: GetChecklistsOptions = {}) => { }; export const getListById = async ( - id: string, + uuid: string, username?: string, - category?: string, - unarchive?: boolean, ): Promise => { const { grepFindFileByUuid } = await import("@/app/_utils/grep-utils"); const { serverReadFile } = await import("@/app/_server/actions/file"); @@ -268,12 +264,11 @@ export const getListById = async ( if (!username) { const { getUserByChecklistUuid } = await import("@/app/_server/actions/users"); - const userByUuid = await getUserByChecklistUuid(id); - if (userByUuid.success && userByUuid.data) { - username = userByUuid.data.username; - } else { + const userByUuid = await getUserByChecklistUuid(uuid); + if (!userByUuid.success || !userByUuid.data) { return undefined; } + username = userByUuid.data.username; } let ownerUsername = username; @@ -284,40 +279,14 @@ export const getListById = async ( username, ); let filePath: string | null = null; - let listId = id; - let listCategory = category || "Uncategorized"; - const isUuid = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id); - - if (isUuid) { - const found = await grepFindFileByUuid(absUserDir, id); - if (found) { - filePath = found.filePath; - listId = found.id; - listCategory = found.category; - } - } - - if (!filePath && category) { - const directPath = path.join(absUserDir, category, `${id}.md`); - try { - await fs.access(directPath); - filePath = directPath; - } catch { - if (unarchive) { - const archivedPath = path.join( - absUserDir, - ".archive", - category, - `${id}.md`, - ); - try { - await fs.access(archivedPath); - filePath = archivedPath; - listCategory = `.archive/${category}`; - } catch {} - } - } + let listId = uuid; + let listCategory = UNCATEGORIZED; + + const found = await grepFindFileByUuid(absUserDir, uuid); + if (found) { + filePath = found.filePath; + listId = found.id; + listCategory = found.category || UNCATEGORIZED; } let isShared = false; @@ -327,8 +296,7 @@ export const getListById = async ( await import("@/app/_server/actions/sharing"); const sharedData = await getAllSharedItemsForUser(username); for (const sharedItem of sharedData.checklists) { - if (!sharedItem.uuid && !sharedItem.id) continue; - if (sharedItem.uuid !== id && sharedItem.id !== id) continue; + if (sharedItem.uuid !== uuid) continue; const sharerDir = path.join( process.cwd(), @@ -336,27 +304,16 @@ export const getListById = async ( CHECKLISTS_FOLDER, sharedItem.sharer, ); - const found = isUuid && (await grepFindFileByUuid(sharerDir, id)); + const sharedFound = await grepFindFileByUuid(sharerDir, uuid); - if (found) { - filePath = found.filePath; - listId = found.id; - listCategory = found.category; + if (sharedFound) { + filePath = sharedFound.filePath; + listId = sharedFound.id; + listCategory = sharedFound.category || UNCATEGORIZED; isShared = true; ownerUsername = sharedItem.sharer; break; } - - if (!isUuid && category) { - const sharedPath = path.join(sharerDir, category, `${id}.md`); - try { - await fs.access(sharedPath); - filePath = sharedPath; - isShared = true; - ownerUsername = sharedItem.sharer; - break; - } catch {} - } } } @@ -371,24 +328,9 @@ export const getListById = async ( const parsedData = parseChecklistContent(rawContent, listId); const checklistType = extractChecklistType(rawContent) || "task"; - let finalUuid = parsedData.uuid; - if (!finalUuid) { - finalUuid = generateUuid(); - try { - const updatedContent = updateYamlMetadata(rawContent, { - uuid: finalUuid, - title: parsedData.title || listId.replace(/-/g, " "), - checklistType: checklistType, - }); - await fs.writeFile(filePath, updatedContent, "utf-8"); - } catch (error) { - console.warn("Failed to save UUID to checklist file:", error); - } - } - return { id: listId, - uuid: finalUuid, + uuid: parsedData.uuid || uuid, title: parsedData.title, type: checklistType as Checklist["type"], items: parsedData.items, diff --git a/app/_server/actions/checklist/readers.ts b/app/_server/actions/checklist/readers.ts index 60ab2390..702bc4da 100644 --- a/app/_server/actions/checklist/readers.ts +++ b/app/_server/actions/checklist/readers.ts @@ -29,6 +29,20 @@ const execAsync = promisify(exec); const debugCrud = isDebugFlag("crud"); +const _stampUuid = async (filePath: string): Promise => { + try { + const content = await serverReadFile(filePath); + if (!content) return undefined; + + const uuid = generateUuid(); + await serverWriteFile(filePath, updateYamlMetadata(content, { uuid })); + return uuid; + } catch (error) { + console.warn("Failed to stamp UUID on checklist:", filePath, error); + return undefined; + } +}; + export type ChecklistReadResult = | Partial | Checklist @@ -201,7 +215,9 @@ export const readListsRecursively = async ( return { id, uuid: - typeof metadata?.uuid === "string" ? metadata.uuid : undefined, + typeof metadata?.uuid === "string" + ? metadata.uuid + : await _stampUuid(filePath), title: typeof metadata?.title === "string" ? metadata.title : id, type: isKanbanType(metadata?.checklistType as string) ? "kanban" diff --git a/app/_server/actions/config/helpers.ts b/app/_server/actions/config/helpers.ts index e463861c..e3a4ec86 100644 --- a/app/_server/actions/config/helpers.ts +++ b/app/_server/actions/config/helpers.ts @@ -12,8 +12,7 @@ import { getSettings } from "./settings"; export const getMedatadaTitle = async ( appMode: Modes, - id: string, - category?: string + uuid: string ): Promise => { const user = await getCurrentUser(); const settings = await getSettings(); @@ -24,8 +23,8 @@ export const getMedatadaTitle = async ( const item = appMode === Modes.CHECKLISTS - ? await getListById(id, undefined, category) - : await getNoteById(id, category); + ? await getListById(uuid) + : await getNoteById(uuid); return { title: `${item?.title || defaultTitle} - ${appName}`, diff --git a/app/_server/actions/dashboard/index.ts b/app/_server/actions/dashboard/index.ts index 533c3a91..2dc25b85 100644 --- a/app/_server/actions/dashboard/index.ts +++ b/app/_server/actions/dashboard/index.ts @@ -1,4 +1,4 @@ -import { AppMode, Checklist, ItemType, Note, Result, User } from "@/app/_types"; +import { AppMode, Checklist, ItemType, Note, Result } from "@/app/_types"; import { ItemTypes, Modes } from "@/app/_types/enums"; import { updateList } from "../checklist"; import { updateNote, getNoteById } from "../note"; @@ -6,9 +6,11 @@ import { getCurrentUser, getUserIndex } from "../users"; import { readJsonFile, writeJsonFile } from "../file"; import { ARCHIVED_DIR_NAME, USERS_FILE } from "@/app/_consts/files"; +const _pinMatches = (entry: string, uuid: string): boolean => + entry === uuid || entry.split("/").pop() === uuid; + export const togglePin = async ( - itemId: string, - category: string, + uuid: string, type: ItemType ): Promise> => { try { @@ -21,30 +23,21 @@ export const togglePin = async ( const userIndex = await getUserIndex(currentUser.username); const user = allUsers[userIndex]; - const itemPath = `${category}/${itemId}`; if (type === ItemTypes.CHECKLIST) { - const pinnedLists = user.pinnedLists || []; - const isPinned = pinnedLists.includes(itemPath); - - if (isPinned) { - user.pinnedLists = pinnedLists.filter( - (path: string) => path !== itemPath - ); - } else { - user.pinnedLists = [...pinnedLists, itemPath]; - } + const pinnedLists: string[] = user.pinnedLists || []; + const isPinned = pinnedLists.some((entry) => _pinMatches(entry, uuid)); + + user.pinnedLists = isPinned + ? pinnedLists.filter((entry) => !_pinMatches(entry, uuid)) + : [...pinnedLists, uuid]; } else { - const pinnedNotes = user.pinnedNotes || []; - const isPinned = pinnedNotes.includes(itemPath); - - if (isPinned) { - user.pinnedNotes = pinnedNotes.filter( - (path: string) => path !== itemPath - ); - } else { - user.pinnedNotes = [...pinnedNotes, itemPath]; - } + const pinnedNotes: string[] = user.pinnedNotes || []; + const isPinned = pinnedNotes.some((entry) => _pinMatches(entry, uuid)); + + user.pinnedNotes = isPinned + ? pinnedNotes.filter((entry) => !_pinMatches(entry, uuid)) + : [...pinnedNotes, uuid]; } allUsers[userIndex] = user; @@ -97,7 +90,7 @@ export const toggleArchive = async ( const isOwner = !item.owner || currentUser?.username === item.owner; const formData = new FormData(); - formData.append("id", item.id); + formData.append("uuid", item.uuid!); formData.append("title", item.title); if (item.owner) { @@ -108,27 +101,16 @@ export const toggleArchive = async ( const noteItem = item as Note; let content = noteItem.content; if (content === undefined || content === null) { - const fullNote = await getNoteById( - noteItem.uuid || noteItem.id, - noteItem.category || "Uncategorized", - noteItem.owner - ); + const fullNote = await getNoteById(noteItem.uuid!, noteItem.owner); content = fullNote?.content || ""; } formData.append("content", content); - } else { - const checklistItem = item as Checklist; - if (checklistItem.uuid) { - formData.append("uuid", checklistItem.uuid); - } } if (isOwner) { formData.append("category", newCategory || ARCHIVED_DIR_NAME); } - formData.append("originalCategory", item.category || "Uncategorized"); - let result: Result; if (mode === Modes.NOTES) { result = (await updateNote(formData, false)) as Result; diff --git a/app/_server/actions/history/index.ts b/app/_server/actions/history/index.ts index 655a772b..996022ad 100644 --- a/app/_server/actions/history/index.ts +++ b/app/_server/actions/history/index.ts @@ -262,8 +262,6 @@ export const commitNote = async ( export const getHistory = async ( noteUuid: string, - noteId: string, - noteCategory: string, noteOwner: string, page: number = 1, pageSize: number = 20 @@ -280,7 +278,6 @@ export const getHistory = async ( const canRead = await checkUserPermission( noteUuid, - noteCategory || "Uncategorized", "note", currentUser.username, PermissionTypes.READ @@ -298,7 +295,7 @@ export const getHistory = async ( const git = _getGitInstance(userDir); const { getNoteById } = await import("@/app/_server/actions/note"); - const note = await getNoteById(noteUuid, undefined, username); + const note = await getNoteById(noteUuid, username); if (!note) { return { success: false, error: "Note not found" }; @@ -356,8 +353,6 @@ export const getHistory = async ( export const getVersion = async ( noteUuid: string, - noteId: string, - noteCategory: string, noteOwner: string, commitHash: string ): Promise> => { @@ -377,7 +372,6 @@ export const getVersion = async ( const canRead = await checkUserPermission( noteUuid, - noteCategory || "Uncategorized", "note", currentUser.username, PermissionTypes.READ @@ -446,7 +440,7 @@ export const getVersion = async ( commitHash, date: commitDate, content: contentWithoutMetadata, - title: metadata.title || noteId, + title: metadata.title || "Untitled", }, }; } catch (error) { @@ -456,8 +450,6 @@ export const getVersion = async ( export const restoreNoteVersion = async ( noteUuid: string, - noteId: string, - noteCategory: string, noteOwner: string, commitHash: string ): Promise> => { @@ -476,8 +468,7 @@ export const restoreNoteVersion = async ( } const canEdit = await checkUserPermission( - noteId, - noteCategory || "Uncategorized", + noteUuid, "note", currentUser.username, PermissionTypes.EDIT @@ -487,13 +478,7 @@ export const restoreNoteVersion = async ( return { success: false, error: "Permission denied" }; } - const versionResult = await getVersion( - noteUuid, - noteId, - noteCategory, - noteOwner, - commitHash - ); + const versionResult = await getVersion(noteUuid, noteOwner, commitHash); if (!versionResult.success || !versionResult.data) { return { success: false, error: versionResult.error }; @@ -502,12 +487,9 @@ export const restoreNoteVersion = async ( const { updateNote } = await import("@/app/_server/actions/note"); const formData = new FormData(); - formData.append("id", noteId); - formData.append("uuid", noteUuid || ""); + formData.append("uuid", noteUuid); formData.append("title", versionResult.data.title); formData.append("content", versionResult.data.content); - formData.append("category", noteCategory || "Uncategorized"); - formData.append("originalCategory", noteCategory || "Uncategorized"); const result = await updateNote(formData); diff --git a/app/_server/actions/kanban/calendar.ts b/app/_server/actions/kanban/calendar.ts index b0b51d6e..78b3cb49 100644 --- a/app/_server/actions/kanban/calendar.ts +++ b/app/_server/actions/kanban/calendar.ts @@ -7,9 +7,9 @@ import { parseItemsForCalendar, CalendarEvent } from "@/app/_utils/kanban/calend export const exportBoardAsICS = async (formData: FormData) => { try { - const { listId, category } = getFormData(formData, ["listId", "category"]); + const { listId } = getFormData(formData, ["listId"]); - const list = await getListById(listId, undefined, category); + const list = await getListById(listId); if (!list) return { error: "Board not found" }; const icsContent = generateICS(list.items, list.title); @@ -22,9 +22,9 @@ export const exportBoardAsICS = async (formData: FormData) => { export const getCalendarEvents = async (formData: FormData) => { try { - const { listId, category } = getFormData(formData, ["listId", "category"]); + const { listId } = getFormData(formData, ["listId"]); - const list = await getListById(listId, undefined, category); + const list = await getListById(listId); if (!list) return { error: "Board not found" }; const events: CalendarEvent[] = parseItemsForCalendar(list.items); diff --git a/app/_server/actions/kanban/items.ts b/app/_server/actions/kanban/items.ts index 1c3a1219..cbc79472 100644 --- a/app/_server/actions/kanban/items.ts +++ b/app/_server/actions/kanban/items.ts @@ -3,6 +3,7 @@ import path from "path"; import { Checklist, KanbanPriority, KanbanReminder } from "@/app/_types"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { ItemTypes, Modes, PermissionTypes } from "@/app/_types/enums"; import { getCurrentUser } from "@/app/_server/actions/users"; import { getUserModeDir, serverWriteFile } from "@/app/_server/actions/file"; @@ -16,7 +17,7 @@ import { createNotificationForUser } from "@/app/_server/actions/notifications"; import { findItem, updateItem } from "@/app/_utils/item-tree-utils"; const _getFilePath = async (list: Checklist): Promise => { - const categoryDir = list.category || "Uncategorized"; + const categoryDir = list.category || UNCATEGORIZED; const filename = `${list.id}.md`; if (list.owner) { @@ -36,7 +37,7 @@ async function _saveAndBroadcast(list: Checklist, username: string) { await broadcast({ type: "checklist", action: "updated", - entityId: list.uuid || list.id, + entityId: list.uuid, username, }); @@ -47,19 +48,19 @@ async function _saveAndBroadcast(list: Checklist, username: string) { export const updateKanbanItemPriority = async (formData: FormData) => { try { - const { listId, itemId, priority, category } = getFormData(formData, [ - "listId", "itemId", "priority", "category", + const { listId, itemId, priority } = getFormData(formData, [ + "listId", "itemId", "priority", ]); const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; @@ -85,19 +86,19 @@ export const updateKanbanItemPriority = async (formData: FormData) => { export const updateKanbanItemScore = async (formData: FormData) => { try { - const { listId, itemId, score, category } = getFormData(formData, [ - "listId", "itemId", "score", "category", + const { listId, itemId, score } = getFormData(formData, [ + "listId", "itemId", "score", ]); const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; @@ -123,19 +124,19 @@ export const updateKanbanItemScore = async (formData: FormData) => { export const assignKanbanItem = async (formData: FormData) => { try { - const { listId, itemId, assignee, category } = getFormData(formData, [ - "listId", "itemId", "assignee", "category", + const { listId, itemId, assignee } = getFormData(formData, [ + "listId", "itemId", "assignee", ]); const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; @@ -159,7 +160,7 @@ export const assignKanbanItem = async (formData: FormData) => { type: "assignment", title: assignedItem?.text || "New task assigned", message: `${currentUser.username} assigned you to a task in "${updatedList.title}"`, - data: { itemId: updatedList.uuid || listId, itemType: "checklist", taskId: itemId }, + data: { itemId: updatedList.uuid, itemType: "checklist", taskId: itemId }, }); } @@ -172,19 +173,19 @@ export const assignKanbanItem = async (formData: FormData) => { export const setKanbanItemReminder = async (formData: FormData) => { try { - const { listId, itemId, reminder: reminderStr, category } = getFormData(formData, [ - "listId", "itemId", "reminder", "category", + const { listId, itemId, reminder: reminderStr } = getFormData(formData, [ + "listId", "itemId", "reminder", ]); const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; diff --git a/app/_server/actions/kanban/search.ts b/app/_server/actions/kanban/search.ts index 791e03f9..72bc6dfc 100644 --- a/app/_server/actions/kanban/search.ts +++ b/app/_server/actions/kanban/search.ts @@ -47,14 +47,14 @@ const _filterItems = ( export const searchKanbanItems = async (formData: FormData) => { try { - const { listId, category } = getFormData(formData, ["listId", "category"]); + const { listId } = getFormData(formData, ["listId"]); const query = formData.get("query") as string | null; const priority = formData.get("priority") as KanbanPriority | null; const assignee = formData.get("assignee") as string | null; const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; diff --git a/app/_server/actions/kanban/tempo.ts b/app/_server/actions/kanban/tempo.ts index 6ddbb772..dc7f546e 100644 --- a/app/_server/actions/kanban/tempo.ts +++ b/app/_server/actions/kanban/tempo.ts @@ -70,7 +70,7 @@ const _aggregateForItem = ( export const getTempoData = async (formData: FormData) => { try { - const { listId, category } = getFormData(formData, ["listId", "category"]); + const { listId } = getFormData(formData, ["listId"]); const startDate = formData.get("startDate") as string; const endDate = formData.get("endDate") as string; @@ -78,7 +78,7 @@ export const getTempoData = async (formData: FormData) => { const [currentUser, list] = await Promise.all([ getCurrentUser(), - getListById(listId, undefined, category), + getListById(listId), ]); if (!currentUser) return { error: "Not authenticated" }; diff --git a/app/_server/actions/kanban/time-entries.ts b/app/_server/actions/kanban/time-entries.ts index 9ef533bb..567a0585 100644 --- a/app/_server/actions/kanban/time-entries.ts +++ b/app/_server/actions/kanban/time-entries.ts @@ -9,6 +9,7 @@ import { getFormData } from "@/app/_utils/global-utils"; import { updateItem, findItem } from "@/app/_utils/item-tree-utils"; import path from "path"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { getUserModeDir, serverWriteFile } from "@/app/_server/actions/file"; import { listToMarkdown } from "@/app/_utils/checklist-utils"; import { Modes } from "@/app/_types/enums"; @@ -16,7 +17,7 @@ import { revalidatePath } from "next/cache"; import { broadcast } from "@/app/_server/ws/broadcast"; const _getFilePath = async (list: Checklist): Promise => { - const categoryDir = list.category || "Uncategorized"; + const categoryDir = list.category || UNCATEGORIZED; const filename = `${list.id}.md`; if (list.owner) { @@ -36,7 +37,7 @@ async function _saveAndBroadcast(list: Checklist, username: string) { await broadcast({ type: "checklist", action: "updated", - entityId: list.uuid || list.id, + entityId: list.uuid, username, }); @@ -47,8 +48,8 @@ async function _saveAndBroadcast(list: Checklist, username: string) { export const editTimeEntry = async (formData: FormData) => { try { - const { listId, itemId, entryId, category } = getFormData(formData, [ - "listId", "itemId", "entryId", "category", + const { listId, itemId, entryId } = getFormData(formData, [ + "listId", "itemId", "entryId", ]); const startTime = formData.get("startTime") as string; @@ -58,11 +59,11 @@ export const editTimeEntry = async (formData: FormData) => { const currentUser = await getCurrentUser(); if (!currentUser) return { error: "Not authenticated" }; - const list = await getListById(listId, currentUser.username, category); + const list = await getListById(listId, currentUser.username); if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; @@ -101,18 +102,18 @@ export const editTimeEntry = async (formData: FormData) => { export const deleteTimeEntry = async (formData: FormData) => { try { - const { listId, itemId, entryId, category } = getFormData(formData, [ - "listId", "itemId", "entryId", "category", + const { listId, itemId, entryId } = getFormData(formData, [ + "listId", "itemId", "entryId", ]); const currentUser = await getCurrentUser(); if (!currentUser) return { error: "Not authenticated" }; - const list = await getListById(listId, currentUser.username, category); + const list = await getListById(listId, currentUser.username); if (!list) return { error: "List not found" }; const canEdit = await checkUserPermission( - list.uuid || listId, category, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT + list.uuid!, ItemTypes.CHECKLIST, currentUser.username, PermissionTypes.EDIT ); if (!canEdit) return { error: "Permission denied" }; diff --git a/app/_server/actions/note/crud.ts b/app/_server/actions/note/crud.ts index b8567eef..e3e8a766 100644 --- a/app/_server/actions/note/crud.ts +++ b/app/_server/actions/note/crud.ts @@ -19,7 +19,8 @@ import { NOTES_DIR } from "@/app/_consts/files"; import { PermissionTypes, Modes } from "@/app/_types/enums"; import { sanitizeMarkdown } from "@/app/_utils/markdown-utils"; import { extractHashtagsFromContent } from "@/app/_utils/tag-utils"; -import { buildCategoryPath, getFormData } from "@/app/_utils/global-utils"; +import { getFormData } from "@/app/_utils/global-utils"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { updateIndexForItem, parseInternalLinks, @@ -144,16 +145,13 @@ export const createNote = async (formData: FormData) => { export const updateNote = async (formData: FormData, autosaveNotes = false) => { try { - const { id, title, content, category, originalCategory, user, uuid } = - getFormData(formData, [ - "id", - "title", - "content", - "category", - "originalCategory", - "user", - "uuid", - ]); + const { title, content, category, user, uuid } = getFormData(formData, [ + "title", + "content", + "category", + "user", + "uuid", + ]); const settings = await getSettings(); let currentUser = user; @@ -171,27 +169,14 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { return { error: "Not authenticated" }; } - const sanitizedContent = sanitizeMarkdown(content); - const { contentWithoutMetadata } = stripYaml(sanitizedContent); - const processedContent = settings?.editor?.enableBilateralLinks - ? await convertInternalLinksToNewFormat( - contentWithoutMetadata, - currentUser, - originalCategory - ) - : contentWithoutMetadata; - - const convertedContent = processedContent; - - const note = await getNoteById(uuid || id, originalCategory, undefined); + const note = await getNoteById(uuid); if (!note) { throw new Error("Note not found"); } const canEdit = await checkUserPermission( - note.uuid || id, - originalCategory, + note.uuid!, "note", actingUsername, PermissionTypes.EDIT @@ -201,6 +186,18 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { return { error: "Permission denied" }; } + const sanitizedContent = sanitizeMarkdown(content); + const { contentWithoutMetadata } = stripYaml(sanitizedContent); + const processedContent = settings?.editor?.enableBilateralLinks + ? await convertInternalLinksToNewFormat( + contentWithoutMetadata, + currentUser, + note.category + ) + : contentWithoutMetadata; + + const convertedContent = processedContent; + const encryptionMethod = detectEncryptionMethod(convertedContent) || undefined; @@ -221,12 +218,13 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { const ownerDir = NOTES_DIR(note.owner!); const categoryDir = path.join( ownerDir, - updatedDoc.category || "Uncategorized" + updatedDoc.category || UNCATEGORIZED ); await ensureDir(categoryDir); + const currentId = note.id; let newFilename: string; - let newId = id; + let newId = currentId; if (title !== note.title) { const ownerUser = await getCurrentUser(); @@ -239,27 +237,24 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { ); newId = path.basename(newFilename, ".md"); } else { - newFilename = `${id}.md`; + newFilename = `${currentId}.md`; } - if (newId !== id) { + if (newId !== currentId) { updatedDoc.id = newId; } const filePath = path.join(categoryDir, newFilename); let oldFilePath: string | null = null; - if (category && category !== note.category) { - oldFilePath = path.join( - ownerDir, - note.category || "Uncategorized", - `${id}.md` - ); - } else if (newId !== id) { + if ( + (category && category !== note.category) || + newId !== currentId + ) { oldFilePath = path.join( ownerDir, - note.category || "Uncategorized", - `${id}.md` + note.category || UNCATEGORIZED, + `${currentId}.md` ); } @@ -267,7 +262,7 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { if (!autosaveNotes && !updatedDoc.encrypted) { const historyRelativePath = path.join( - updatedDoc.category || "Uncategorized", + updatedDoc.category || UNCATEGORIZED, `${newId}.md` ); @@ -276,9 +271,12 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { const historyMetadata = isCategoryChange ? { - oldCategory: note.category || "Uncategorized", - newCategory: updatedDoc.category || "Uncategorized", - oldPath: path.join(note.category || "Uncategorized", `${id}.md`), + oldCategory: note.category || UNCATEGORIZED, + newCategory: updatedDoc.category || UNCATEGORIZED, + oldPath: path.join( + note.category || UNCATEGORIZED, + `${currentId}.md` + ), } : undefined; @@ -294,11 +292,11 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { if (settings?.editor?.enableBilateralLinks) { try { const links = (await parseInternalLinks(updatedDoc.content)) || []; - const newItemKey = `${updatedDoc.category || "Uncategorized"}/${ + const newItemKey = `${updatedDoc.category || UNCATEGORIZED}/${ updatedDoc.id }`; - const oldItemKey = `${note.category || "Uncategorized"}/${id}`; + const oldItemKey = `${note.category || UNCATEGORIZED}/${currentId}`; if (oldItemKey !== newItemKey) { await rebuildLinkIndex(note.owner!); @@ -315,27 +313,6 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { } } - if (newId !== id || (category && category !== note.category)) { - const { updateSharingData } = await import( - "@/app/_server/actions/sharing" - ); - - await updateSharingData( - { - id, - category: note.category || "Uncategorized", - itemType: "note", - sharer: note.owner!, - }, - { - id: newId, - category: updatedDoc.category || "Uncategorized", - itemType: "note", - sharer: note.owner!, - } - ); - } - if (oldFilePath && oldFilePath !== filePath) { await serverDeleteFile(oldFilePath); } @@ -343,20 +320,7 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { try { if (!autosaveNotes) { revalidatePath("/"); - const oldCategoryPath = buildCategoryPath( - note.category || "Uncategorized", - id - ); - const newCategoryPath = buildCategoryPath( - updatedDoc.category || "Uncategorized", - newId !== id ? newId : id - ); - - revalidatePath(`/note/${oldCategoryPath}`); - - if (newId !== id || note.category !== updatedDoc.category) { - revalidatePath(`/note/${newCategoryPath}`); - } + revalidatePath(`/note/${note.uuid}`); } } catch (error) { console.warn( @@ -394,12 +358,7 @@ export const updateNote = async (formData: FormData, autosaveNotes = false) => { export const deleteNote = async (formData: FormData, username?: string) => { try { - const { id, category, uuid } = getFormData(formData, [ - "id", - "category", - "uuid", - ]); - const itemIdentifier = uuid || id; + const { uuid } = getFormData(formData, ["uuid"]); let currentUser: any = null; if (username) { @@ -418,15 +377,14 @@ export const deleteNote = async (formData: FormData, username?: string) => { return { error: "Not authenticated" }; } - const note = await getNoteById(itemIdentifier, category, undefined); + const note = await getNoteById(uuid!); if (!note) { return { error: "Document not found" }; } const canDelete = await checkUserPermission( - note.uuid || itemIdentifier, - category, + note.uuid!, "note", currentUser.username, PermissionTypes.DELETE @@ -440,7 +398,7 @@ export const deleteNote = async (formData: FormData, username?: string) => { const ownerDir = NOTES_DIR(ownerUsername); const filePath = path.join( ownerDir, - note.category || "Uncategorized", + note.category || UNCATEGORIZED, `${note.id}.md` ); @@ -448,7 +406,7 @@ export const deleteNote = async (formData: FormData, username?: string) => { if (!note.encrypted) { const deleteRelativePath = path.join( - note.category || "Uncategorized", + note.category || UNCATEGORIZED, `${note.id}.md` ); commitNote( @@ -472,8 +430,6 @@ export const deleteNote = async (formData: FormData, username?: string) => { await updateSharingData( { uuid: note.uuid, - id: note.id, - category: note.category || "Uncategorized", itemType: "note", sharer: note.owner, }, @@ -483,11 +439,7 @@ export const deleteNote = async (formData: FormData, username?: string) => { try { revalidatePath("/"); - const categoryPath = buildCategoryPath( - note.category || "Uncategorized", - note.id - ); - revalidatePath(`/note/${categoryPath}`); + revalidatePath(`/note/${note.uuid}`); } catch (error) { console.warn( "Cache revalidation failed, but data was saved successfully:", @@ -509,7 +461,7 @@ export const deleteNote = async (formData: FormData, username?: string) => { return { success: true }; } catch (error) { const { uuid } = getFormData(formData, ["uuid"]); - const note = await getNoteById(uuid!, ""); + const note = await getNoteById(uuid!); await logContentEvent( "note_deleted", "note", @@ -523,17 +475,11 @@ export const deleteNote = async (formData: FormData, username?: string) => { export const cloneNote = async (formData: FormData) => { try { - const id = formData.get("id") as string; const uuid = formData.get("uuid") as string; - const originalCategory = formData.get("originalCategory") as string | null; const targetCategory = formData.get("category") as string; const ownerUsername = formData.get("user") as string | null; - const note = await getNoteById( - uuid || id, - originalCategory || undefined, - ownerUsername || undefined - ); + const note = await getNoteById(uuid, ownerUsername || undefined); if (!note) { return { error: "Note not found" }; } @@ -544,8 +490,8 @@ export const cloneNote = async (formData: FormData) => { const isOwnedByCurrentUser = !note.owner || note.owner === currentUser?.username; const finalTargetCategory = isOwnedByCurrentUser - ? targetCategory || "Uncategorized" - : "Uncategorized"; + ? targetCategory || UNCATEGORIZED + : UNCATEGORIZED; const categoryDir = path.join(userDir, finalTargetCategory); await ensureDir(categoryDir); @@ -561,8 +507,9 @@ export const cloneNote = async (formData: FormData) => { const filePath = path.join(categoryDir, filename); const content = note.content || ""; + const cloneUuid = generateUuid(); const updatedContent = updateYamlMetadata(content, { - uuid: generateUuid(), + uuid: cloneUuid, title: cloneTitle, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -570,12 +517,7 @@ export const cloneNote = async (formData: FormData) => { await serverWriteFile(filePath, updatedContent); - const newId = path.basename(filename, ".md"); - const clonedNote = await getNoteById( - newId, - finalTargetCategory, - currentUser?.username - ); + const clonedNote = await getNoteById(cloneUuid, currentUser?.username); try { revalidatePath("/"); @@ -586,7 +528,7 @@ export const cloneNote = async (formData: FormData) => { ); } - await broadcast({ type: "note", action: "created", entityId: newId, username: currentUser?.username || "" }); + await broadcast({ type: "note", action: "created", entityId: cloneUuid, username: currentUser?.username || "" }); return { success: true, data: clonedNote }; } catch (error) { diff --git a/app/_server/actions/note/queries.ts b/app/_server/actions/note/queries.ts index dffbc3e7..731507ae 100644 --- a/app/_server/actions/note/queries.ts +++ b/app/_server/actions/note/queries.ts @@ -3,18 +3,14 @@ import path from "path"; import fs from "fs/promises"; import { Note, User, GetNotesOptions } from "@/app/_types"; -import { NOTES_DIR } from "@/app/_consts/files"; +import { NOTES_DIR, USERS_FILE } from "@/app/_consts/files"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; import { Modes } from "@/app/_types/enums"; -import { getCurrentUser, getUserByNote } from "@/app/_server/actions/users"; +import { getCurrentUser } from "@/app/_server/actions/users"; import { getUserModeDir, ensureDir } from "@/app/_server/actions/file"; import { readJsonFile } from "@/app/_server/actions/file"; -import { USERS_FILE } from "@/app/_consts/files"; import { parseNoteContent } from "@/app/_utils/client-parser-utils"; -import { - generateUuid, - toIso, - updateYamlMetadata, -} from "@/app/_utils/yaml-metadata-utils"; +import { toIso } from "@/app/_utils/yaml-metadata-utils"; import { readNotesRecursively } from "./readers"; import { isDebugFlag } from "@/app/_utils/env-utils"; import { getOrCompute, metaCacheKey } from "@/app/_server/lib/metadata-cache"; @@ -50,8 +46,7 @@ export const getAllNotes = async (allowArchived?: boolean) => { }; export const getNoteById = async ( - id: string, - category?: string, + uuid: string, username?: string, ): Promise => { const { grepFindFileByUuid } = await import("@/app/_utils/grep-utils"); @@ -59,57 +54,26 @@ export const getNoteById = async ( if (!username) { const { getUserByNoteUuid } = await import("@/app/_server/actions/users"); - const userByUuid = await getUserByNoteUuid(id); + const userByUuid = await getUserByNoteUuid(uuid); - if (userByUuid.success && userByUuid.data) { - username = userByUuid.data.username; - } else { - const user = await getUserByNote(id, category || "Uncategorized"); - - if (user.success && user.data) { - username = user.data.username; - } else { - return undefined; - } + if (!userByUuid.success || !userByUuid.data) { + return undefined; } + + username = userByUuid.data.username; } let ownerUsername = username; - const userDir = NOTES_DIR(username); - const absUserDir = path.join(process.cwd(), userDir); + const absUserDir = path.join(process.cwd(), NOTES_DIR(username)); let filePath: string | null = null; - let noteId = id; - let noteCategory = category || "Uncategorized"; - const isUuid = - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id); - - if (isUuid) { - const found = await grepFindFileByUuid(absUserDir, id); - if (found) { - filePath = found.filePath; - noteId = found.id; - noteCategory = found.category; - } - } - - if (!filePath && category) { - const directPath = path.join(absUserDir, category, `${id}.md`); - try { - await fs.access(directPath); - filePath = directPath; - } catch { - const archivedPath = path.join( - absUserDir, - ".archive", - category, - `${id}.md`, - ); - try { - await fs.access(archivedPath); - filePath = archivedPath; - noteCategory = `.archive/${category}`; - } catch { } - } + let noteId = uuid; + let noteCategory = UNCATEGORIZED; + + const found = await grepFindFileByUuid(absUserDir, uuid); + if (found) { + filePath = found.filePath; + noteId = found.id; + noteCategory = found.category || UNCATEGORIZED; } let isShared = false; @@ -119,31 +83,19 @@ export const getNoteById = async ( await import("@/app/_server/actions/sharing"); const sharedData = await getAllSharedItemsForUser(username); for (const sharedItem of sharedData.notes) { - if (!sharedItem.uuid && !sharedItem.id) continue; - if (sharedItem.uuid !== id && sharedItem.id !== id) continue; + if (sharedItem.uuid !== uuid) continue; const sharerDir = path.join(process.cwd(), NOTES_DIR(sharedItem.sharer)); - const found = isUuid && (await grepFindFileByUuid(sharerDir, id)); + const sharedFound = await grepFindFileByUuid(sharerDir, uuid); - if (found) { - filePath = found.filePath; - noteId = found.id; - noteCategory = found.category; + if (sharedFound) { + filePath = sharedFound.filePath; + noteId = sharedFound.id; + noteCategory = sharedFound.category || UNCATEGORIZED; isShared = true; ownerUsername = sharedItem.sharer; break; } - - if (!isUuid && category) { - const sharedPath = path.join(sharerDir, category, `${id}.md`); - try { - await fs.access(sharedPath); - filePath = sharedPath; - isShared = true; - ownerUsername = sharedItem.sharer; - break; - } catch { } - } } } @@ -157,23 +109,9 @@ export const getNoteById = async ( const stats = await fs.stat(filePath); const parsedData = parseNoteContent(rawContent, noteId); - let finalUuid = parsedData.uuid; - if (!finalUuid) { - finalUuid = generateUuid(); - try { - const updatedContent = updateYamlMetadata(rawContent, { - uuid: finalUuid, - title: parsedData.title || noteId.replace(/-/g, " "), - }); - await fs.writeFile(filePath, updatedContent, "utf-8"); - } catch (error) { - console.warn("Failed to save UUID to note file:", error); - } - } - return { id: noteId, - uuid: finalUuid, + uuid: parsedData.uuid || uuid, title: parsedData.title, content: parsedData.content, category: noteCategory, @@ -275,7 +213,7 @@ export const getUserNotes = async (options: GetNotesOptions = {}) => { for (const sharedItem of sharedData.notes) { try { - const itemIdentifier = sharedItem.uuid || sharedItem.id; + const itemIdentifier = sharedItem.uuid; if (!itemIdentifier) continue; const sharerDir = NOTES_DIR(sharedItem.sharer); @@ -311,7 +249,7 @@ export const getUserNotes = async (options: GetNotesOptions = {}) => { ); const sharedNote = sharerNotes.find( - (note) => note.uuid === itemIdentifier || note.id === itemIdentifier, + (note) => note.uuid === itemIdentifier, ); if (sharedNote) { @@ -322,7 +260,7 @@ export const getUserNotes = async (options: GetNotesOptions = {}) => { } } catch (error) { console.error( - `Error reading shared note ${sharedItem.uuid || sharedItem.id}:`, + `Error reading shared note ${sharedItem.uuid}:`, error, ); continue; @@ -333,7 +271,7 @@ export const getUserNotes = async (options: GetNotesOptions = {}) => { if (filter) { if (filter.type === "category") { filteredNotes = notes.filter((note: any) => { - const noteCategory = note.category || "Uncategorized"; + const noteCategory = note.category || UNCATEGORIZED; return ( noteCategory === filter.value || noteCategory.startsWith(filter.value + "/") @@ -369,9 +307,8 @@ export const getUserNotes = async (options: GetNotesOptions = {}) => { note: { category?: string; uuid?: string; id: string }, p: string, ) => { - const c = note.category || "Uncategorized"; const u = note.uuid || note.id; - return `${c}/${u}` === p || `${c}/${note.id}` === p; + return p === u || p.split("/").pop() === u; }; const pinned: typeof filteredNotes = []; for (const p of pinnedPaths) { diff --git a/app/_server/actions/note/readers.ts b/app/_server/actions/note/readers.ts index 95a42672..0fae744b 100644 --- a/app/_server/actions/note/readers.ts +++ b/app/_server/actions/note/readers.ts @@ -26,6 +26,24 @@ import { exec } from "child_process"; const execAsync = promisify(exec); +const _stampUuid = async (filePath: string): Promise => { + try { + const content = await serverReadFile(filePath); + if (!content) return undefined; + + const uuid = generateUuid(); + await fs.writeFile( + filePath, + updateYamlMetadata(content, { uuid }), + "utf-8", + ); + return uuid; + } catch (error) { + console.warn("Failed to stamp UUID on note:", filePath, error); + return undefined; + } +}; + export const readNotesRecursively = async ( dir: string, basePath: string = "", @@ -194,7 +212,10 @@ export const readNotesRecursively = async ( return { id, - uuid: typeof metadata?.uuid === "string" ? metadata.uuid : undefined, + uuid: + typeof metadata?.uuid === "string" + ? metadata.uuid + : await _stampUuid(filePath), title: typeof metadata?.title === "string" ? metadata.title : id, category: categoryPath, createdAt: toIso(stats.birthtime), @@ -215,7 +236,10 @@ export const readNotesRecursively = async ( return { id, - uuid: typeof metadata?.uuid === "string" ? metadata.uuid : undefined, + uuid: + typeof metadata?.uuid === "string" + ? metadata.uuid + : await _stampUuid(filePath), title: typeof metadata?.title === "string" ? metadata.title : id, content: excerpt, category: categoryPath, @@ -233,7 +257,15 @@ export const readNotesRecursively = async ( let uuid = metadata.uuid; if (!uuid) { uuid = generateUuid(); - updateYamlMetadata(content, { uuid }); + try { + await fs.writeFile( + filePath, + updateYamlMetadata(content, { uuid }), + "utf-8", + ); + } catch (error) { + console.warn("Failed to save UUID to note file:", error); + } } return { id, diff --git a/app/_server/actions/notifications/index.ts b/app/_server/actions/notifications/index.ts index 41d43124..8b17ec40 100644 --- a/app/_server/actions/notifications/index.ts +++ b/app/_server/actions/notifications/index.ts @@ -58,11 +58,11 @@ const _resolveLink = async (data?: AppNotificationData): Promise => { - try { - const dataDir = path.join(process.cwd(), "data"); - const modeDir = - itemType === ItemTypes.CHECKLIST ? Modes.CHECKLISTS : Modes.NOTES; - const userDir = path.join(dataDir, modeDir, user); - const categoryDir = path.join(userDir, category || "Uncategorized"); - const filePath = path.join(categoryDir, `${itemId}.md`); - - const content = await fs.readFile(filePath, "utf-8"); - return extractUuid(content); - } catch (error) { - console.warn(`Could not get UUID for item ${itemId}:`, error); - return undefined; - } -}; - export const getSharingFilePath = async ( itemType: ItemType, ): Promise => { diff --git a/app/_server/actions/sharing/permissions.ts b/app/_server/actions/sharing/permissions.ts index f74f170c..24fde4ce 100644 --- a/app/_server/actions/sharing/permissions.ts +++ b/app/_server/actions/sharing/permissions.ts @@ -1,110 +1,64 @@ "use server"; import path from "path"; -import fs from "fs/promises"; import { ItemType, SharingPermissions } from "@/app/_types/core"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { ItemTypes, PermissionTypes } from "@/app/_types/enums"; -import { - isAdmin, - getUserByChecklist, - getUserByNote, -} from "@/app/_server/actions/users"; +import { isAdmin } from "@/app/_server/actions/users"; import { CHECKLISTS_DIR, NOTES_DIR } from "@/app/_consts/files"; import { readShareFile } from "./io"; export const isItemSharedWith = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, ): Promise => { const sharingData = await readShareFile(itemType); - const userShares = sharingData[username] || []; - let found = userShares.some((entry) => entry.uuid === item); - - if (!found && categoryPath) { - const encodedCategory = encodeCategoryPath(categoryPath || "Uncategorized"); - found = userShares.some( - (entry) => entry.id === item && entry.category === encodedCategory, - ); - } - - return found; + return userShares.some((entry) => entry.uuid === uuid); }; export const getItemPermissions = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, ): Promise => { const sharingData = await readShareFile(itemType); - const userShares = sharingData[username] || []; - - let entry = userShares.find((entry) => entry.uuid === item); - - if (!entry && categoryPath) { - const encodedCategory = encodeCategoryPath(categoryPath || "Uncategorized"); - entry = userShares.find( - (entry) => entry.id === item && entry.category === encodedCategory, - ); - } + const entry = userShares.find((share) => share.uuid === uuid); return entry ? entry.permissions : null; }; export const canUserReadItem = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, ): Promise => { - const permissions = await getItemPermissions( - item, - categoryPath, - itemType, - username, - ); + const permissions = await getItemPermissions(uuid, itemType, username); return permissions?.canRead === true; }; export const canUserWriteItem = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, ): Promise => { - const permissions = await getItemPermissions( - item, - categoryPath, - itemType, - username, - ); + const permissions = await getItemPermissions(uuid, itemType, username); return permissions?.canEdit === true; }; export const canUserDeleteItem = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, ): Promise => { - const permissions = await getItemPermissions( - item, - categoryPath, - itemType, - username, - ); + const permissions = await getItemPermissions(uuid, itemType, username); return permissions?.canDelete === true; }; export const checkUserPermission = async ( - itemId: string, - itemCategory: string, + uuid: string, itemType: ItemType, currentUsername: string, permission: PermissionTypes, @@ -119,58 +73,36 @@ export const checkUserPermission = async ( const isAdminUser = await isAdmin(); if (isAdminUser) return true; - const userDir = + const { grepCheckUuidExists } = await import("@/app/_utils/grep-utils"); + const userDir = path.join( + process.cwd(), itemType === ItemTypes.CHECKLIST ? CHECKLISTS_DIR(username) - : NOTES_DIR(username); - const categoryDir = path.join(userDir, itemCategory); - const filePath = path.join(categoryDir, `${itemId}.md`); + : NOTES_DIR(username), + ); - try { - await fs.access(filePath); + if (await grepCheckUuidExists(userDir, uuid)) { return true; - } catch {} - - let owner = null; - if (itemType === ItemTypes.CHECKLIST) { - const { getUserByChecklistUuid } = - await import("@/app/_server/actions/users"); - const ownerResult = await getUserByChecklistUuid(itemId); - if (ownerResult.success) { - owner = ownerResult.data; - } - } else { - const { getUserByNoteUuid } = await import("@/app/_server/actions/users"); - const ownerResult = await getUserByNoteUuid(itemId); - if (ownerResult.success) { - owner = ownerResult.data; - } } - if (!owner) { - const ownerResult = - itemType === ItemTypes.CHECKLIST - ? await getUserByChecklist(itemId, itemCategory) - : await getUserByNote(itemId, itemCategory); - - if (!ownerResult.success) return false; - owner = ownerResult.data; - } + const { getUserByChecklistUuid, getUserByNoteUuid } = + await import("@/app/_server/actions/users"); + const ownerResult = + itemType === ItemTypes.CHECKLIST + ? await getUserByChecklistUuid(uuid) + : await getUserByNoteUuid(uuid); + const owner = ownerResult.success ? ownerResult.data : null; - if (owner?.username === username) return true; + if (!owner) return false; + if (owner.username === username) return true; switch (permission) { case PermissionTypes.READ: - return await canUserReadItem(itemId, itemCategory, itemType, username); + return await canUserReadItem(uuid, itemType, username); case PermissionTypes.EDIT: - return await canUserWriteItem(itemId, itemCategory, itemType, username); + return await canUserWriteItem(uuid, itemType, username); case PermissionTypes.DELETE: - return await canUserDeleteItem( - itemId, - itemCategory, - itemType, - username, - ); + return await canUserDeleteItem(uuid, itemType, username); default: return false; } diff --git a/app/_server/actions/sharing/queries.ts b/app/_server/actions/sharing/queries.ts index 31106522..ec76ee12 100644 --- a/app/_server/actions/sharing/queries.ts +++ b/app/_server/actions/sharing/queries.ts @@ -3,6 +3,7 @@ import { ItemTypes } from "@/app/_types/enums"; import { readShareFile } from "./io"; import { SharedItemEntry } from "./types"; +import { SharedItemSummary } from "@/app/_types/sharing"; export const getAllSharedItemsForUser = async ( username: string @@ -17,110 +18,72 @@ export const getAllSharedItemsForUser = async ( }; export const getUsersWithAccess = async ( - checklistId: string, - checklistUuid?: string + checklistUuid: string ): Promise => { const sharingData = await readShareFile(ItemTypes.CHECKLIST); const users: string[] = []; for (const [username, entries] of Object.entries(sharingData)) { if (username === "public") continue; - for (const entry of entries) { - if ( - (checklistUuid && entry.uuid === checklistUuid) || - (entry.id === checklistId) - ) { - users.push(username); - break; - } + if (entries.some((entry) => entry.uuid === checklistUuid)) { + users.push(username); } } return users; }; -export const getAllSharedItems = async (): Promise<{ - notes: Array<{ id: string; category: string }>; - checklists: Array<{ id: string; category: string }>; - public: { - notes: Array<{ id: string; category: string }>; - checklists: Array<{ id: string; category: string }>; - }; -}> => { - const [notesSharing, checklistsSharing] = await Promise.all([ - readShareFile(ItemTypes.NOTE), - readShareFile(ItemTypes.CHECKLIST), - ]); - - const allNotes: Array<{ id: string; category: string }> = []; - const allChecklists: Array<{ id: string; category: string }> = []; - const publicNotes: Array<{ id: string; category: string }> = []; - const publicChecklists: Array<{ id: string; category: string }> = []; +const _summaries = ( + sharing: Record +): SharedItemSummary[] => { + const items: SharedItemSummary[] = []; - Object.values(notesSharing).forEach((userShares) => { + Object.values(sharing).forEach((userShares) => { userShares.forEach((entry) => { - if (entry.id && entry.category) { - allNotes.push({ id: entry.id, category: entry.category }); + if (entry.uuid) { + items.push({ uuid: entry.uuid }); } }); }); - Object.values(checklistsSharing).forEach((userShares) => { - userShares.forEach((entry) => { - if (entry.id && entry.category) { - allChecklists.push({ id: entry.id, category: entry.category }); - } - }); - }); + return items; +}; - if (notesSharing.public) { - for (const entry of notesSharing.public) { - if (entry.id && entry.category) { - publicNotes.push({ id: entry.id, category: entry.category }); - } else if (entry.uuid && entry.id) { - const { getNoteById } = await import("@/app/_server/actions/note"); - const note = await getNoteById(entry.uuid); - if (note) { - publicNotes.push({ - id: note.id, - category: note.category || "Uncategorized", - }); - } - } +const _publicOnes = ( + sharing: Record +): SharedItemSummary[] => + (sharing.public || []).reduce((acc, entry) => { + if (entry.uuid) { + acc.push({ uuid: entry.uuid }); } - } + return acc; + }, []); - if (checklistsSharing.public) { - for (const entry of checklistsSharing.public) { - if (entry.id && entry.category) { - publicChecklists.push({ id: entry.id, category: entry.category }); - } else if (entry.uuid && entry.id) { - const { getListById } = await import("@/app/_server/actions/checklist"); - const checklist = await getListById(entry.uuid); - if (checklist) { - publicChecklists.push({ - id: checklist.id, - category: checklist.category || "Uncategorized", - }); - } - } - } - } +export const getAllSharedItems = async (): Promise<{ + notes: SharedItemSummary[]; + checklists: SharedItemSummary[]; + public: { + notes: SharedItemSummary[]; + checklists: SharedItemSummary[]; + }; +}> => { + const [notesSharing, checklistsSharing] = await Promise.all([ + readShareFile(ItemTypes.NOTE), + readShareFile(ItemTypes.CHECKLIST), + ]); - const deduplicate = (items: Array<{ id: string; category: string }>) => + const deduplicate = (items: SharedItemSummary[]) => items.filter( (item, index, array) => - array.findIndex( - (i) => i.id === item.id && i.category === item.category - ) === index + array.findIndex((i) => i.uuid === item.uuid) === index ); return { - notes: deduplicate(allNotes), - checklists: deduplicate(allChecklists), + notes: deduplicate(_summaries(notesSharing)), + checklists: deduplicate(_summaries(checklistsSharing)), public: { - notes: deduplicate(publicNotes), - checklists: deduplicate(publicChecklists), + notes: deduplicate(_publicOnes(notesSharing)), + checklists: deduplicate(_publicOnes(checklistsSharing)), }, }; }; diff --git a/app/_server/actions/sharing/share-operations.ts b/app/_server/actions/sharing/share-operations.ts index 8ac7ebbc..88aaaee0 100644 --- a/app/_server/actions/sharing/share-operations.ts +++ b/app/_server/actions/sharing/share-operations.ts @@ -3,9 +3,7 @@ import { ItemType, Result, SharingPermissions } from "@/app/_types/core"; import { broadcast } from "@/app/_server/ws/broadcast"; import { ItemTypes } from "@/app/_types/enums"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { logAudit } from "@/app/_server/actions/log"; -import { getItemUuid } from "./helpers"; import { readShareFile, writeShareFile } from "./io"; import { SharedItemEntry } from "./types"; import { revalidateTag } from "next/cache"; @@ -13,8 +11,7 @@ import { getTranslations } from "next-intl/server"; import { createNotificationForUser } from "@/app/_server/actions/notifications"; export const shareWith = async ( - item: string, - categoryPath: string, + uuid: string, sharerUsername: string, receiverUsername: string, itemType: ItemType, @@ -25,16 +22,7 @@ export const shareWith = async ( } ): Promise> => { try { - const sharingData = await readShareFile(itemType); - - const itemUuid = await getItemUuid( - sharerUsername, - itemType === ItemTypes.CHECKLIST ? "checklist" : "note", - item, - categoryPath || "Uncategorized" - ); - - if (!itemUuid) { + if (!uuid) { return { success: false, error: @@ -42,9 +30,10 @@ export const shareWith = async ( }; } + const sharingData = await readShareFile(itemType); + const newEntry: SharedItemEntry = { - uuid: itemUuid, - id: item, + uuid, sharer: sharerUsername, permissions, }; @@ -54,7 +43,7 @@ export const shareWith = async ( } sharingData[receiverUsername] = sharingData[receiverUsername].filter( - (entry) => !(entry.id === item && entry.sharer === sharerUsername) + (entry) => entry.uuid !== uuid ); sharingData[receiverUsername].push(newEntry); @@ -69,12 +58,12 @@ export const shareWith = async ( category: "sharing", success: true, resourceType: itemType, - resourceId: item, - resourceTitle: item, + resourceId: uuid, + resourceTitle: uuid, metadata: { receiver: receiverUsername, permissions }, }); - await broadcast({ type: "sharing", action: "updated", entityId: item, username: sharerUsername }); + await broadcast({ type: "sharing", action: "updated", entityId: uuid, username: sharerUsername }); const itemTypeLabel = itemType === ItemTypes.CHECKLIST ? "checklist" : "note"; const t = await getTranslations("notifications"); @@ -82,7 +71,7 @@ export const shareWith = async ( type: "sharing", title: t("sharingTitle", { user: sharerUsername, type: itemTypeLabel }), message: t("sharingMessage", { type: itemTypeLabel }), - data: { itemId: itemUuid, itemType: itemTypeLabel }, + data: { itemId: uuid, itemType: itemTypeLabel }, }); return { success: true, data: null }; @@ -93,8 +82,8 @@ export const shareWith = async ( category: "sharing", success: false, resourceType: itemType, - resourceId: item, - resourceTitle: item, + resourceId: uuid, + resourceTitle: uuid, errorMessage: error instanceof Error ? error.message : "Failed to share item", }); @@ -104,33 +93,18 @@ export const shareWith = async ( }; export const unshareWith = async ( - item: string, - categoryPath: string, + uuid: string, sharerUsername: string, receiverUsername: string, itemType: ItemType ): Promise> => { const sharingData = await readShareFile(itemType); - const encodedCategory = encodeCategoryPath(categoryPath || "Uncategorized"); if (sharingData[receiverUsername]) { - const entryIndex = sharingData[receiverUsername].findIndex( - (entry) => entry.uuid === item + sharingData[receiverUsername] = sharingData[receiverUsername].filter( + (entry) => entry.uuid !== uuid ); - if (entryIndex !== -1) { - sharingData[receiverUsername].splice(entryIndex, 1); - } else { - sharingData[receiverUsername] = sharingData[receiverUsername].filter( - (entry) => - !( - entry.id === item && - entry.sharer === sharerUsername && - entry.category === encodedCategory - ) - ); - } - if (sharingData[receiverUsername].length === 0) { delete sharingData[receiverUsername]; } @@ -146,12 +120,12 @@ export const unshareWith = async ( category: "sharing", success: true, resourceType: itemType, - resourceId: item, - resourceTitle: item, + resourceId: uuid, + resourceTitle: uuid, metadata: { receiver: receiverUsername }, }); - await broadcast({ type: "sharing", action: "updated", entityId: item, username: sharerUsername }); + await broadcast({ type: "sharing", action: "updated", entityId: uuid, username: sharerUsername }); } catch (error) { await logAudit({ level: "ERROR", @@ -159,8 +133,8 @@ export const unshareWith = async ( category: "sharing", success: false, resourceType: itemType, - resourceId: item, - resourceTitle: item, + resourceId: uuid, + resourceTitle: uuid, errorMessage: "Failed to write unshare file", }); return { success: false, error: "Failed to write unshare file" }; @@ -170,14 +144,12 @@ export const unshareWith = async ( }; export const updateItemPermissions = async ( - item: string, - categoryPath: string, + uuid: string, itemType: ItemType, username: string, permissions: SharingPermissions ): Promise> => { const sharingData = await readShareFile(itemType); - const encodedCategory = encodeCategoryPath(categoryPath || "Uncategorized"); if (!sharingData[username]) { await logAudit({ @@ -186,23 +158,17 @@ export const updateItemPermissions = async ( category: "sharing", success: false, resourceType: itemType, - resourceId: item, + resourceId: uuid, errorMessage: "Item not shared with this user", metadata: { targetUser: username }, }); return { success: false, error: "Item not shared with this user" }; } - let entryIndex = sharingData[username].findIndex( - (entry) => entry.uuid === item + const entryIndex = sharingData[username].findIndex( + (entry) => entry.uuid === uuid ); - if (entryIndex === -1) { - entryIndex = sharingData[username].findIndex( - (entry) => entry.id === item && entry.category === encodedCategory - ); - } - if (entryIndex === -1) { await logAudit({ level: "WARNING", @@ -210,7 +176,7 @@ export const updateItemPermissions = async ( category: "sharing", success: false, resourceType: itemType, - resourceId: item, + resourceId: uuid, errorMessage: "Item not shared with this user", metadata: { targetUser: username }, }); @@ -231,7 +197,7 @@ export const updateItemPermissions = async ( category: "sharing", success: true, resourceType: itemType, - resourceId: item, + resourceId: uuid, metadata: { targetUser: username, oldPermissions, @@ -239,7 +205,7 @@ export const updateItemPermissions = async ( }, }); - await broadcast({ type: "sharing", action: "updated", entityId: item, username }); + await broadcast({ type: "sharing", action: "updated", entityId: uuid, username }); return { success: true, data: null }; } catch (error) { @@ -249,7 +215,7 @@ export const updateItemPermissions = async ( category: "sharing", success: false, resourceType: itemType, - resourceId: item, + resourceId: uuid, errorMessage: "Failed to update permissions", metadata: { targetUser: username }, }); diff --git a/app/_server/actions/sharing/types.ts b/app/_server/actions/sharing/types.ts index 16699dba..932bbe6d 100644 --- a/app/_server/actions/sharing/types.ts +++ b/app/_server/actions/sharing/types.ts @@ -2,7 +2,9 @@ import { SharingPermissions, ItemType } from "@/app/_types/core"; interface SharedItemEntry { uuid?: string; + /** @deprecated legacy on-disk field, ignored; entries are matched by uuid */ id?: string; + /** @deprecated legacy on-disk field, ignored; entries are matched by uuid */ category?: string; sharer: string; permissions: SharingPermissions; @@ -10,8 +12,6 @@ interface SharedItemEntry { interface SharingItemUpdate { uuid?: string; - id?: string; - category?: string; itemType: ItemType; sharer?: string; } diff --git a/app/_server/actions/sharing/updates.ts b/app/_server/actions/sharing/updates.ts index 574f48c7..e93da9b1 100644 --- a/app/_server/actions/sharing/updates.ts +++ b/app/_server/actions/sharing/updates.ts @@ -1,7 +1,6 @@ "use server"; import { ItemType } from "@/app/_types/core"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { readShareFile, writeShareFile } from "./io"; import { SharingItemUpdate } from "./types"; @@ -13,18 +12,10 @@ export const updateSharingData = async ( let hasChanges = false; if (newItem === null) { - const encodedCategory = encodeCategoryPath( - previousItem.category || "Uncategorized" - ); Object.keys(sharingData).forEach((username) => { const originalLength = sharingData[username].length; sharingData[username] = sharingData[username].filter( - (entry) => - !( - (previousItem.uuid && entry.uuid === previousItem.uuid) || - (entry.id === previousItem.id && - (!entry.category || entry.category === encodedCategory)) - ) + (entry) => !(previousItem.uuid && entry.uuid === previousItem.uuid) ); if (sharingData[username].length !== originalLength) { hasChanges = true; @@ -33,64 +24,15 @@ export const updateSharingData = async ( delete sharingData[username]; } }); - } else { - const prevCategory = previousItem.category - ? encodeCategoryPath(previousItem.category) - : null; - const newCategory = newItem.category - ? encodeCategoryPath(newItem.category) - : null; - + } else if (newItem.sharer && newItem.sharer !== previousItem.sharer) { Object.keys(sharingData).forEach((username) => { sharingData[username].forEach((entry) => { - let updated = false; - - if ( - !previousItem.id && - newItem.sharer && - entry.sharer === previousItem.sharer - ) { - entry.sharer = newItem.sharer; - updated = true; - } else if (previousItem.id) { - if ( - entry.id === previousItem.id && - entry.category === - (prevCategory || encodeCategoryPath("Uncategorized")) && - newItem.id !== previousItem.id - ) { - entry.id = newItem.id; - updated = true; - } - - if ( - entry.id === (newItem.id || previousItem.id) && - entry.category === - (prevCategory || encodeCategoryPath("Uncategorized")) && - newCategory && - newCategory !== - (prevCategory || encodeCategoryPath("Uncategorized")) - ) { - entry.category = newCategory; - updated = true; - } - - if ( - newItem.sharer && - entry.sharer === previousItem.sharer && - entry.id === (newItem.id || previousItem.id) && - entry.category === - (newCategory || - prevCategory || - encodeCategoryPath("Uncategorized")) && - newItem.sharer !== previousItem.sharer - ) { - entry.sharer = newItem.sharer; - updated = true; - } - } + const matches = previousItem.uuid + ? entry.uuid === previousItem.uuid + : entry.sharer === previousItem.sharer; - if (updated) { + if (matches && entry.sharer === previousItem.sharer) { + entry.sharer = newItem.sharer!; hasChanges = true; } }); diff --git a/app/_server/lib/legacy-lookup.ts b/app/_server/lib/legacy-lookup.ts new file mode 100644 index 00000000..cbfd6abe --- /dev/null +++ b/app/_server/lib/legacy-lookup.ts @@ -0,0 +1,153 @@ +import path from "path"; +import fs from "fs/promises"; +import { Modes } from "@/app/_types/enums"; +import { + ARCHIVED_DIR_NAME, + CHECKLISTS_DIR, + NOTES_DIR, + USERS_FILE, +} from "@/app/_consts/files"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { User } from "@/app/_types"; + +const _modeDir = (mode: Modes, username: string): string => + path.join( + process.cwd(), + mode === Modes.CHECKLISTS ? CHECKLISTS_DIR(username) : NOTES_DIR(username), + ); + +const _candidates = (userDir: string, category: string, id: string): string[] => { + const paths = [ + path.join(userDir, category, `${id}.md`), + path.join(userDir, ARCHIVED_DIR_NAME, category, `${id}.md`), + ]; + + if (category === UNCATEGORIZED) { + paths.push(path.join(userDir, `${id}.md`)); + paths.push(path.join(userDir, ARCHIVED_DIR_NAME, `${id}.md`)); + } + + return paths; +}; + +const _findFile = async ( + mode: Modes, + category: string, + id: string, + username: string, +): Promise => { + for (const candidate of _candidates(_modeDir(mode, username), category, id)) { + try { + await fs.access(candidate); + return candidate; + } catch { + continue; + } + } + + return null; +}; + +const _uuidFor = async (filePath: string): Promise => { + const { grepExtractField } = await import("@/app/_utils/grep-utils"); + const existing = await grepExtractField(filePath, "uuid"); + + if (existing) { + return existing; + } + + const { generateUuid, updateYamlMetadata } = + await import("@/app/_utils/yaml-metadata-utils"); + const { logAudit } = await import("@/app/_server/actions/log"); + + try { + const content = await fs.readFile(filePath, "utf-8"); + const stamped = generateUuid(); + await fs.writeFile(filePath, updateYamlMetadata(content, { uuid: stamped }), "utf-8"); + return stamped; + } catch (error) { + await logAudit({ + level: "WARNING", + action: "legacy_lookup", + category: "system", + success: false, + errorMessage: "Failed to stamp uuid during legacy lookup", + metadata: { filePath, error: String(error) }, + }); + return null; + } +}; + +async function _logDeprecated( + mode: Modes, + category: string, + id: string, + uuid: string, +): Promise { + const { logAudit } = await import("@/app/_server/actions/log"); + + await logAudit({ + level: "WARNING", + action: "legacy_lookup", + category: "system", + success: true, + errorMessage: + "Deprecated category+id lookup used; switch to uuid, support will be removed soon", + metadata: { mode, category, id, uuid }, + }); +} + +/** + * @deprecated REST API fallback: returns the param untouched when it is a + * uuid, otherwise resolves it as a legacy category+id pair (category from the + * ?category= query, defaulting to Uncategorized) and logs a deprecation + * warning. Will be removed once slug lookups are dropped. + */ +export const resolveApiId = async ( + mode: Modes, + param: string, + category?: string | null, + username?: string, +): Promise => { + const { isUuid } = await import("@/app/_consts/identity"); + + if (isUuid(param)) { + return param; + } + + return legacyResolve(mode, category || UNCATEGORIZED, param, username); +}; + +/** + * @deprecated Resolves a legacy category+id pair to the item uuid. Only the + * legacy URL redirect pages and the REST API fallback may use this; every call + * is logged as a deprecation warning. Identity is uuid everywhere else. + */ +export const legacyResolve = async ( + mode: Modes, + category: string, + id: string, + username?: string, +): Promise => { + const { readJsonFile } = await import("@/app/_server/actions/file"); + const owners: string[] = username + ? [username] + : ((await readJsonFile(USERS_FILE)) as User[]).map((u) => u.username); + + for (const owner of owners) { + const filePath = await _findFile(mode, category, id, owner); + + if (!filePath) { + continue; + } + + const uuid = await _uuidFor(filePath); + + if (uuid) { + await _logDeprecated(mode, category, id, uuid); + return uuid; + } + } + + return null; +}; diff --git a/app/_server/reminders/scanner.ts b/app/_server/reminders/scanner.ts index 10d357c1..6ed2c2d5 100644 --- a/app/_server/reminders/scanner.ts +++ b/app/_server/reminders/scanner.ts @@ -97,7 +97,14 @@ export const scanReminders = async (): Promise => { const dueItems = _collectDueItems(list.items, now); if (dueItems.length === 0) continue; - const sharees = await getUsersWithAccess(list.id, list.uuid); + if (!list.uuid) { + console.warn( + `[reminders] skipping list without uuid: ${filePath}`, + ); + continue; + } + + const sharees = await getUsersWithAccess(list.uuid); const recipients = Array.from(new Set([owner, ...sharees])); let updatedItems = list.items; @@ -114,7 +121,7 @@ export const scanReminders = async (): Promise => { board: list.title, }, data: { - itemId: list.uuid || list.id, + itemId: list.uuid, itemType: "checklist", taskId: item.id, }, @@ -140,7 +147,7 @@ export const scanReminders = async (): Promise => { await broadcast({ type: "checklist", action: "updated", - entityId: updatedList.uuid || updatedList.id, + entityId: updatedList.uuid, username: owner, }); } catch (err) { diff --git a/app/_types/sharing.ts b/app/_types/sharing.ts index 721cb19e..2d9c3d94 100644 --- a/app/_types/sharing.ts +++ b/app/_types/sharing.ts @@ -39,9 +39,7 @@ export interface GlobalSharingReturn { } export interface SharedItemSummary { - id: string; - uuid?: string; - category: string; + uuid: string; } export interface AllSharedItems { @@ -54,8 +52,10 @@ export interface AllSharedItems { } export interface UserSharedItem { - id?: string; uuid?: string; + /** @deprecated legacy on-disk field, ignored; entries are matched by uuid */ + id?: string; + /** @deprecated legacy on-disk field, ignored; entries are matched by uuid */ category?: string; sharer: string; } diff --git a/app/_utils/global-utils.ts b/app/_utils/global-utils.ts index c4234895..f2a90005 100644 --- a/app/_utils/global-utils.ts +++ b/app/_utils/global-utils.ts @@ -1,5 +1,7 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import { ItemType } from "@/app/_types/core"; +import { ItemTypes } from "@/app/_types/enums"; export const cn = (...inputs: ClassValue[]) => { return twMerge(clsx(inputs)); }; @@ -85,24 +87,11 @@ export function decodeCategoryPath(encodedPath: string): string { .join("/"); } -export function buildCategoryPath(category: string, id: string): string { - const encodedCategory = encodeCategoryPath(category); - return encodedCategory ? `${encodedCategory}/${encodeId(id)}` : encodeId(id); -} - -export function decodeId(encodedId: string): string { - if (!encodedId) { - return encodedId; - } - return decodeURIComponent(encodedId); -} +export const itemHref = (type: ItemType, uuid: string): string => + type === ItemTypes.CHECKLIST ? `/checklist/${uuid}` : `/note/${uuid}`; -export function encodeId(id: string): string { - if (id.includes("%20") || id.includes("%2F")) { - return id; - } - return encodeURIComponent(id); -} +export const publicHref = (type: ItemType, uuid: string): string => + `/public${itemHref(type, uuid)}`; export const generateWebManifest = ( appName: string, diff --git a/app/_utils/sharing-utils.ts b/app/_utils/sharing-utils.ts index 083255a4..39ca78c5 100644 --- a/app/_utils/sharing-utils.ts +++ b/app/_utils/sharing-utils.ts @@ -6,21 +6,14 @@ interface ItemDetails { sharedWith: string[]; } -export const sharingInfo = ( - data: any, - targetId: string, - targetCategory: string, -) => { +export const sharingInfo = (data: any, targetUuid: string) => { let result: ItemDetails = { exists: false, isPublic: false, sharedWith: [] as string[], }; - const isMatch = (item: { id?: string; uuid?: string; category?: string }) => - item.uuid === targetId || - (item.id === targetId && - item.category?.toLowerCase() === targetCategory?.toLowerCase()); + const isMatch = (item: { uuid?: string }) => item.uuid === targetUuid; for (const categoryKey in data) { const categoryObject = data[categoryKey]; @@ -50,16 +43,10 @@ export const sharingInfo = ( export const getPermissions = ( data: any, username: string, - targetId: string, - targetCategory: string, + targetUuid: string, itemType?: Modes, ) => { - const isMatch = (item: { id?: string; uuid?: string; category?: string }) => - item.uuid === targetId || - (item.id === targetId && - (!targetCategory || - !item.category || - item.category?.toLowerCase() === targetCategory?.toLowerCase())); + const isMatch = (item: { uuid?: string }) => item.uuid === targetUuid; const categoriesToSearch = itemType !== undefined ? [itemType] : (Object.keys(data || {}) as string[]); diff --git a/app/api/checklists/[listId]/items/reorder/route.ts b/app/api/checklists/[listId]/items/reorder/route.ts index e1731315..074279e8 100644 --- a/app/api/checklists/[listId]/items/reorder/route.ts +++ b/app/api/checklists/[listId]/items/reorder/route.ts @@ -6,9 +6,11 @@ import { listToMarkdown } from "@/app/_utils/checklist-utils"; import { serverWriteFile, ensureDir } from "@/app/_server/actions/file"; import { checkUserPermission } from "@/app/_server/actions/sharing"; import { broadcast } from "@/app/_server/ws/broadcast"; -import { ItemTypes, PermissionTypes } from "@/app/_types/enums"; +import { ItemTypes, Modes, PermissionTypes } from "@/app/_types/enums"; import path from "path"; import { CHECKLISTS_FOLDER } from "@/app/_consts/checklists"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { resolveApiId } from "@/app/_server/lib/legacy-lookup"; export const dynamic = "force-dynamic"; @@ -69,14 +71,19 @@ export async function PUT( ); } - const list = await getListById(params.listId, user.username); + const uuid = await resolveApiId( + Modes.CHECKLISTS, + params.listId, + request.nextUrl.searchParams.get("category"), + user.username, + ); + const list = uuid ? await getListById(uuid, user.username) : undefined; if (!list) { return NextResponse.json({ error: "List not found" }, { status: 404 }); } const canEdit = await checkUserPermission( - list.uuid || params.listId, - list.category || "", + list.uuid!, ItemTypes.CHECKLIST, user.username, PermissionTypes.EDIT, @@ -119,13 +126,13 @@ export async function PUT( const updatedList = { ...list, items, updatedAt: new Date().toISOString() }; const ownerDir = path.join(process.cwd(), "data", CHECKLISTS_FOLDER, list.owner!); - const categoryDir = path.join(ownerDir, list.category || "Uncategorized"); + const categoryDir = path.join(ownerDir, list.category || UNCATEGORIZED); await ensureDir(categoryDir); await serverWriteFile(path.join(categoryDir, `${list.id}.md`), listToMarkdown(updatedList as any)); try { revalidatePath("/"); - revalidatePath(`/checklist/${list.id}`); + revalidatePath(`/checklist/${list.uuid}`); } catch (error) { console.warn("Cache revalidation failed, but data was saved successfully:", error); } @@ -134,7 +141,7 @@ export async function PUT( await broadcast({ type: "checklist", action: "updated", - entityId: list.id, + entityId: list.uuid, username: user.username, }); } catch (error) { diff --git a/app/api/notes/[noteId]/route.ts b/app/api/notes/[noteId]/route.ts index 488a2cbc..5ba20bd9 100644 --- a/app/api/notes/[noteId]/route.ts +++ b/app/api/notes/[noteId]/route.ts @@ -1,15 +1,31 @@ import { NextRequest, NextResponse } from "next/server"; import { withApiAuth } from "@/app/_utils/api-utils"; import { updateNote, deleteNote } from "@/app/_server/actions/note"; +import { resolveApiId } from "@/app/_server/lib/legacy-lookup"; +import { Modes } from "@/app/_types/enums"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; export const dynamic = "force-dynamic"; +const _noteUuid = async ( + request: NextRequest, + noteId: string, + username: string, +): Promise => + resolveApiId( + Modes.NOTES, + noteId, + request.nextUrl.searchParams.get("category"), + username, + ); + export async function GET(request: NextRequest, props: { params: Promise<{ noteId: string }> }) { const params = await props.params; return withApiAuth(request, async (user) => { try { + const uuid = await _noteUuid(request, params.noteId, user.username); const { getNoteById } = await import("@/app/_server/actions/note"); - const note = await getNoteById(params.noteId, undefined, user.username); + const note = uuid ? await getNoteById(uuid, user.username) : undefined; if (!note) { return NextResponse.json({ error: "Note not found" }, { status: 404 }); @@ -18,9 +34,9 @@ export async function GET(request: NextRequest, props: { params: Promise<{ noteI return NextResponse.json({ success: true, data: { - id: note.uuid || note.id, + id: note.uuid, title: note.title, - category: note.category || "Uncategorized", + category: note.category || UNCATEGORIZED, content: note.content, createdAt: note.createdAt, updatedAt: note.updatedAt, @@ -44,6 +60,7 @@ export async function PUT(request: NextRequest, props: { params: Promise<{ noteI const body = await request.json(); const { title, content, category } = body; + const uuid = await _noteUuid(request, params.noteId, user.username); const { getUserNotes } = await import("@/app/_server/actions/note"); const notes = await getUserNotes({ username: user.username }); @@ -54,18 +71,16 @@ export async function PUT(request: NextRequest, props: { params: Promise<{ noteI ); } - const note = notes.data.find((n) => n.uuid === params.noteId); + const note = notes.data.find((n) => n.uuid === uuid); if (!note) { return NextResponse.json({ error: "Note not found" }, { status: 404 }); } const formData = new FormData(); - formData.append("id", note.id || ""); - formData.append("uuid", params.noteId); + formData.append("uuid", note.uuid!); formData.append("title", title ?? note.title); formData.append("content", content ?? note.content ?? ""); - formData.append("category", category ?? note.category ?? "Uncategorized"); - formData.append("originalCategory", note.category || "Uncategorized"); + formData.append("category", category ?? note.category ?? UNCATEGORIZED); formData.append("user", user.username); const result = await updateNote(formData); @@ -74,9 +89,9 @@ export async function PUT(request: NextRequest, props: { params: Promise<{ noteI } const transformedNote = { - id: result.data?.uuid || result.data?.id, + id: result.data?.uuid, title: result.data?.title, - category: result.data?.category || "Uncategorized", + category: result.data?.category || UNCATEGORIZED, content: result.data?.content, createdAt: result.data?.createdAt, updatedAt: result.data?.updatedAt, @@ -98,6 +113,7 @@ export async function DELETE(request: NextRequest, props: { params: Promise<{ no const params = await props.params; return withApiAuth(request, async (user) => { try { + const uuid = await _noteUuid(request, params.noteId, user.username); const { getUserNotes } = await import("@/app/_server/actions/note"); const notes = await getUserNotes({ username: user.username }); @@ -108,14 +124,13 @@ export async function DELETE(request: NextRequest, props: { params: Promise<{ no ); } - const note = notes.data.find((n) => n.uuid === params.noteId); + const note = notes.data.find((n) => n.uuid === uuid); if (!note) { return NextResponse.json({ error: "Note not found" }, { status: 404 }); } const formData = new FormData(); - formData.append("id", note.id || ""); - formData.append("category", note.category || "Uncategorized"); + formData.append("uuid", note.uuid!); const result = await deleteNote(formData, user.username); if (result.error) { diff --git a/app/public/checklist/[...categoryPath]/page.tsx b/app/public/checklist/[...categoryPath]/page.tsx index da492f6c..cd133448 100644 --- a/app/public/checklist/[...categoryPath]/page.tsx +++ b/app/public/checklist/[...categoryPath]/page.tsx @@ -1,120 +1,28 @@ -import { redirect } from "next/navigation"; -import { getAllLists, getListById } from "@/app/_server/actions/checklist"; -import { PublicChecklistView } from "@/app/_components/FeatureComponents/PublicView/PublicChecklistView"; -import { CheckForNeedsMigration } from "@/app/_server/actions/note"; -import { getCurrentUser, getUserByUsername } from "@/app/_server/actions/users"; -import type { Metadata } from "next"; +import { redirect, permanentRedirect } from "next/navigation"; import { Modes } from "@/app/_types/enums"; -import { getMedatadaTitle } from "@/app/_server/actions/config"; -import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils"; -import { sharingInfo } from "@/app/_utils/sharing-utils"; -import { isItemSharedWith } from "@/app/_server/actions/sharing"; -import { MetadataProvider } from "@/app/_providers/MetadataProvider"; -import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; -import { sanitizeUserForPublic } from "@/app/_utils/user-sanitize-utils"; -import { isEnvEnabled } from "@/app/_utils/env-utils"; +import { legacyResolve } from "@/app/_server/lib/legacy-lookup"; +import { decodeCategoryPath } from "@/app/_utils/global-utils"; -interface PublicChecklistPageProps { +interface LegacyPublicChecklistProps { params: Promise<{ categoryPath: string[]; }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; } export const dynamic = "force-dynamic"; -export async function generateMetadata( - props: PublicChecklistPageProps, -): Promise { - const params = await props.params; - const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); - - return getMedatadaTitle(Modes.CHECKLISTS, id, category); -} - -export default async function PublicChecklistPage( - props: PublicChecklistPageProps, +export default async function LegacyPublicChecklist( + props: LegacyPublicChecklistProps, ) { - const searchParams = await props.searchParams; const params = await props.params; const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); - - await CheckForNeedsMigration(); - - let checklist = await getListById(id, undefined, category); - - if (!checklist) { - const listsResult = await getAllLists(); - if (!listsResult.success || !listsResult.data) { - redirect("/"); - } - - checklist = listsResult.data.find( - (list) => list.id === id && list.category === category, - ); - - if (!checklist && categoryPath.length === 1) { - checklist = listsResult.data.find( - (list) => list.id === id && list.category === "Uncategorized", - ); - - if (!checklist) { - checklist = listsResult.data.find((list) => list.id === id); - } - } - } - - if (!checklist) { - redirect("/"); - } - - const userRecord = await getUserByUsername(checklist.owner!); - const user = sanitizeUserForPublic( - userRecord, - !!isEnvEnabled(process.env.SERVE_PUBLIC_IMAGES), - ); - - const isPubliclyShared = await isItemSharedWith( - checklist.uuid || id, - category, - "checklist", - "public", - ); - const currentUser = await getCurrentUser(); - const isOwner = currentUser?.username === checklist.owner; - const isPrintView = searchParams?.view_mode === "print"; + const id = decodeURIComponent(categoryPath[categoryPath.length - 1]); + const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/")); - if (isPubliclyShared || isOwner || (isOwner && isPrintView)) { - const metadata = { - id: checklist.id, - uuid: checklist.uuid, - title: checklist.title, - category: checklist.category || "Uncategorized", - owner: checklist.owner, - createdAt: checklist.createdAt, - updatedAt: checklist.updatedAt, - type: "checklist" as const, - }; + const uuid = await legacyResolve(Modes.CHECKLISTS, category, id); - return ( - - - - - - ); + if (uuid) { + permanentRedirect(`/public/checklist/${uuid}`); } redirect("/"); diff --git a/app/public/checklist/[uuid]/page.tsx b/app/public/checklist/[uuid]/page.tsx new file mode 100644 index 00000000..91559343 --- /dev/null +++ b/app/public/checklist/[uuid]/page.tsx @@ -0,0 +1,100 @@ +import { redirect, permanentRedirect } from "next/navigation"; +import { getListById } from "@/app/_server/actions/checklist"; +import { isUuid } from "@/app/_consts/identity"; +import { PublicChecklistView } from "@/app/_components/FeatureComponents/PublicView/PublicChecklistView"; +import { CheckForNeedsMigration } from "@/app/_server/actions/note"; +import { getCurrentUser, getUserByUsername } from "@/app/_server/actions/users"; +import type { Metadata } from "next"; +import { Modes } from "@/app/_types/enums"; +import { getMedatadaTitle } from "@/app/_server/actions/config"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { isItemSharedWith } from "@/app/_server/actions/sharing"; +import { MetadataProvider } from "@/app/_providers/MetadataProvider"; +import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; +import { sanitizeUserForPublic } from "@/app/_utils/user-sanitize-utils"; +import { isEnvEnabled } from "@/app/_utils/env-utils"; + +interface PublicChecklistPageProps { + params: Promise<{ + uuid: string; + }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export const dynamic = "force-dynamic"; + +export async function generateMetadata( + props: PublicChecklistPageProps, +): Promise { + const params = await props.params; + return getMedatadaTitle(Modes.CHECKLISTS, params.uuid); +} + +export default async function PublicChecklistPage( + props: PublicChecklistPageProps, +) { + const searchParams = await props.searchParams; + const params = await props.params; + const { uuid } = params; + + await CheckForNeedsMigration(); + + if (!isUuid(uuid)) { + const { legacyResolve } = await import("@/app/_server/lib/legacy-lookup"); + const resolved = await legacyResolve( + Modes.CHECKLISTS, + UNCATEGORIZED, + decodeURIComponent(uuid), + ); + + if (resolved) { + permanentRedirect(`/public/checklist/${resolved}`); + } + + redirect("/"); + } + + const checklist = await getListById(uuid); + + if (!checklist) { + redirect("/"); + } + + const userRecord = await getUserByUsername(checklist.owner!); + const user = sanitizeUserForPublic( + userRecord, + !!isEnvEnabled(process.env.SERVE_PUBLIC_IMAGES), + ); + + const isPubliclyShared = await isItemSharedWith( + checklist.uuid!, + "checklist", + "public", + ); + const currentUser = await getCurrentUser(); + const isOwner = currentUser?.username === checklist.owner; + const isPrintView = searchParams?.view_mode === "print"; + + if (isPubliclyShared || isOwner || (isOwner && isPrintView)) { + const metadata = { + id: checklist.id, + uuid: checklist.uuid, + title: checklist.title, + category: checklist.category || UNCATEGORIZED, + owner: checklist.owner, + createdAt: checklist.createdAt, + updatedAt: checklist.updatedAt, + type: "checklist" as const, + }; + + return ( + + + + + + ); + } + + redirect("/"); +} diff --git a/app/public/note/[...categoryPath]/page.tsx b/app/public/note/[...categoryPath]/page.tsx index 01a1a199..411996f5 100644 --- a/app/public/note/[...categoryPath]/page.tsx +++ b/app/public/note/[...categoryPath]/page.tsx @@ -1,114 +1,26 @@ -import { redirect } from "next/navigation"; -import { getAllNotes, getNoteById } from "@/app/_server/actions/note"; -import { PublicNoteView } from "@/app/_components/FeatureComponents/PublicView/PublicNoteView"; -import { getCurrentUser, getUserByUsername } from "@/app/_server/actions/users"; -import type { Metadata } from "next"; -import { getMedatadaTitle } from "@/app/_server/actions/config"; +import { redirect, permanentRedirect } from "next/navigation"; import { Modes } from "@/app/_types/enums"; -import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils"; -import { isItemSharedWith } from "@/app/_server/actions/sharing"; -import { MetadataProvider } from "@/app/_providers/MetadataProvider"; -import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; -import { sanitizeUserForPublic } from "@/app/_utils/user-sanitize-utils"; -import { isEnvEnabled } from "@/app/_utils/env-utils"; +import { legacyResolve } from "@/app/_server/lib/legacy-lookup"; +import { decodeCategoryPath } from "@/app/_utils/global-utils"; -interface PublicNotePageProps { +interface LegacyPublicNoteProps { params: Promise<{ categoryPath: string[]; }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; } export const dynamic = "force-dynamic"; -export async function generateMetadata( - props: PublicNotePageProps, -): Promise { +export default async function LegacyPublicNote(props: LegacyPublicNoteProps) { const params = await props.params; const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); + const id = decodeURIComponent(categoryPath[categoryPath.length - 1]); + const category = decodeCategoryPath(categoryPath.slice(0, -1).join("/")); - return getMedatadaTitle(Modes.NOTES, id, category); -} - -export default async function PublicNotePage(props: PublicNotePageProps) { - const searchParams = await props.searchParams; - const params = await props.params; - const { categoryPath } = params; - const id = decodeId(categoryPath[categoryPath.length - 1]); - const encodedCategoryPath = categoryPath.slice(0, -1).join("/"); - const category = - categoryPath.length === 1 - ? "Uncategorized" - : decodeCategoryPath(encodedCategoryPath); - - const isUuidOnly = categoryPath.length === 1; - let note = await getNoteById(id, isUuidOnly ? undefined : category); - - if (!note) { - const notesResult = await getAllNotes(); - if (!notesResult.success || !notesResult.data) { - redirect("/"); - } - - note = notesResult.data.find((n) => n.id === id && n.category === category); - - if (!note && isUuidOnly) { - note = notesResult.data.find( - (n) => n.id === id && n.category === "Uncategorized", - ); - - if (!note) { - note = notesResult.data.find((n) => n.id === id); - } - } - } - - if (!note) { - redirect("/"); - } - - const userRecord = await getUserByUsername(note.owner!); - const user = sanitizeUserForPublic( - userRecord, - !!isEnvEnabled(process.env.SERVE_PUBLIC_IMAGES), - ); - - const isPubliclyShared = await isItemSharedWith( - note.uuid || id, - category, - "note", - "public", - ); - const isPrintView = searchParams.view_mode === "print"; - - const currentUser = await getCurrentUser(); - const isOwner = currentUser?.username === note.owner; - - if (isPubliclyShared || isOwner || (isOwner && isPrintView)) { - const metadata = { - id: note.id, - uuid: note.uuid, - title: note.title, - category: note.category || "Uncategorized", - owner: note.owner, - createdAt: note.createdAt, - updatedAt: note.updatedAt, - type: "note" as const, - }; + const uuid = await legacyResolve(Modes.NOTES, category, id); - return ( - - - - - - ); + if (uuid) { + permanentRedirect(`/public/note/${uuid}`); } redirect("/"); diff --git a/app/public/note/[uuid]/page.tsx b/app/public/note/[uuid]/page.tsx new file mode 100644 index 00000000..d128dfc1 --- /dev/null +++ b/app/public/note/[uuid]/page.tsx @@ -0,0 +1,96 @@ +import { redirect, permanentRedirect } from "next/navigation"; +import { getNoteById } from "@/app/_server/actions/note"; +import { isUuid } from "@/app/_consts/identity"; +import { PublicNoteView } from "@/app/_components/FeatureComponents/PublicView/PublicNoteView"; +import { getCurrentUser, getUserByUsername } from "@/app/_server/actions/users"; +import type { Metadata } from "next"; +import { getMedatadaTitle } from "@/app/_server/actions/config"; +import { Modes } from "@/app/_types/enums"; +import { UNCATEGORIZED } from "@/app/_consts/notes"; +import { isItemSharedWith } from "@/app/_server/actions/sharing"; +import { MetadataProvider } from "@/app/_providers/MetadataProvider"; +import { PermissionsProvider } from "@/app/_providers/PermissionsProvider"; +import { sanitizeUserForPublic } from "@/app/_utils/user-sanitize-utils"; +import { isEnvEnabled } from "@/app/_utils/env-utils"; + +interface PublicNotePageProps { + params: Promise<{ + uuid: string; + }>; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export const dynamic = "force-dynamic"; + +export async function generateMetadata( + props: PublicNotePageProps, +): Promise { + const params = await props.params; + return getMedatadaTitle(Modes.NOTES, params.uuid); +} + +export default async function PublicNotePage(props: PublicNotePageProps) { + const searchParams = await props.searchParams; + const params = await props.params; + const { uuid } = params; + + if (!isUuid(uuid)) { + const { legacyResolve } = await import("@/app/_server/lib/legacy-lookup"); + const resolved = await legacyResolve( + Modes.NOTES, + UNCATEGORIZED, + decodeURIComponent(uuid), + ); + + if (resolved) { + permanentRedirect(`/public/note/${resolved}`); + } + + redirect("/"); + } + + const note = await getNoteById(uuid); + + if (!note) { + redirect("/"); + } + + const userRecord = await getUserByUsername(note.owner!); + const user = sanitizeUserForPublic( + userRecord, + !!isEnvEnabled(process.env.SERVE_PUBLIC_IMAGES), + ); + + const isPubliclyShared = await isItemSharedWith( + note.uuid!, + "note", + "public", + ); + const isPrintView = searchParams.view_mode === "print"; + + const currentUser = await getCurrentUser(); + const isOwner = currentUser?.username === note.owner; + + if (isPubliclyShared || isOwner || (isOwner && isPrintView)) { + const metadata = { + id: note.id, + uuid: note.uuid, + title: note.title, + category: note.category || UNCATEGORIZED, + owner: note.owner, + createdAt: note.createdAt, + updatedAt: note.updatedAt, + type: "note" as const, + }; + + return ( + + + + + + ); + } + + redirect("/"); +} From 995142461f298868b9738dd23ee969cba0eecb98 Mon Sep 17 00:00:00 2001 From: fccview Date: Sun, 12 Jul 2026 19:03:55 +0100 Subject: [PATCH 2/2] continue work on uuid changes --- .../Admin/Parts/AdminContent.tsx | 1 - .../Checklists/ChecklistsPageClient.tsx | 69 +++++----- .../Checklists/Parts/ChecklistClient.tsx | 22 ++-- .../Parts/Common/ChecklistHeader.tsx | 6 +- .../Checklists/Parts/Simple/ChecklistBody.tsx | 3 +- .../Checklists/TasksPageClient.tsx | 61 ++++----- .../FeatureComponents/Home/HomeClient.tsx | 18 +-- .../Home/Parts/ChecklistHome.tsx | 14 +-- .../Home/Parts/NotesHome.tsx | 14 +-- .../FeatureComponents/Home/Parts/TagsHome.tsx | 42 +++---- .../FeatureComponents/Kanban/Kanban.tsx | 26 +--- .../FeatureComponents/Kanban/KanbanCard.tsx | 9 +- .../Kanban/KanbanCardDetail.tsx | 48 +++---- .../FeatureComponents/Kanban/KanbanColumn.tsx | 6 - .../Kanban/KanbanPageClient.tsx | 34 ++--- .../Kanban/TimeEntriesModal.tsx | 12 +- .../FeatureComponents/Notes/NoteClient.tsx | 17 +-- .../Notes/NotesPageClient.tsx | 69 +++++----- .../Notes/Parts/NoteEditor/NoteEditor.tsx | 1 - .../Parts/NoteEditor/NoteEditorContent.tsx | 8 +- .../Parts/NoteEditor/NoteEditorHeader.tsx | 28 +---- .../Notes/Parts/ReferencedBySection.tsx | 9 +- .../Notes/Parts/SwipeNavigationWrapper.tsx | 8 +- .../InternalLinkComponent.tsx | 109 +++++----------- .../Notes/Parts/UnifiedMarkdownRenderer.tsx | 4 +- .../Parts/ConnectionsGraph/graph-data.ts | 17 +-- .../Search/Parts/SearchResults.tsx | 3 +- .../Sidebar/Parts/SidebarItem.tsx | 8 +- .../GlobalComponents/Cards/ChecklistCard.tsx | 2 +- .../Cards/ChecklistGridItem.tsx | 2 +- .../Cards/ChecklistListItem.tsx | 2 +- .../GlobalComponents/Cards/NoteCard.tsx | 2 +- .../GlobalComponents/Cards/NoteGridItem.tsx | 2 +- .../GlobalComponents/Cards/NoteListItem.tsx | 2 +- .../ChecklistModals/EditChecklistModal.tsx | 24 +--- .../Modals/NotesModal/EditNoteModal.tsx | 25 +--- .../Modals/SharingModals/ShareModal.tsx | 4 +- app/_hooks/kanban/useKanban.ts | 45 +++---- app/_hooks/kanban/useKanbanItem.tsx | 56 ++++----- app/_hooks/useAdjacentNotes.ts | 6 +- app/_hooks/useChecklist.tsx | 59 +++------ app/_hooks/useChecklistHome.tsx | 52 +++----- app/_hooks/useNoteEditor.tsx | 6 +- app/_hooks/useNotesHome.tsx | 52 +++----- app/_hooks/useSearch.ts | 2 + app/_hooks/useSharingTools.ts | 118 ++++-------------- app/_providers/ShortcutsProvider.tsx | 32 ++--- app/_server/actions/checklist-item/archive.ts | 16 +-- .../actions/checklist-item/bulk-operations.ts | 32 ++--- app/_server/actions/checklist-item/crud.ts | 4 +- app/_server/actions/checklist-item/reorder.ts | 4 +- app/_server/actions/checklist-item/status.ts | 8 +- .../actions/checklist-item/sub-items.ts | 4 +- app/_server/actions/checklist/converters.ts | 10 +- app/_server/actions/checklist/crud.ts | 2 +- app/_server/actions/checklist/queries.ts | 2 +- app/_server/actions/kanban/calendar.ts | 8 +- app/_server/actions/kanban/items.ts | 24 ++-- app/_server/actions/kanban/search.ts | 4 +- app/_server/actions/kanban/tempo.ts | 4 +- app/_server/actions/kanban/time-entries.ts | 12 +- app/_server/actions/note/crud.ts | 2 +- app/_server/actions/note/queries.ts | 12 +- app/_server/actions/users/helpers.ts | 100 +-------------- app/_server/actions/users/index.ts | 9 +- app/_server/actions/users/queries.ts | 16 +-- app/_types/checklist.ts | 3 +- app/_types/note.ts | 3 +- app/_utils/api-utils.ts | 20 +++ app/_utils/indexes-utils.ts | 9 +- app/_utils/kanban/api-transforms.ts | 2 +- app/_utils/markdown-utils.tsx | 6 +- .../[listId]/items/[itemIndex]/check/route.ts | 7 +- .../[listId]/items/[itemIndex]/route.ts | 10 +- .../items/[itemIndex]/uncheck/route.ts | 7 +- app/api/checklists/[listId]/items/route.ts | 7 +- app/api/checklists/[listId]/route.ts | 17 ++- app/api/checklists/route.ts | 4 +- app/api/kanban/[boardId]/calendar/route.ts | 5 +- .../[boardId]/items/[itemId]/assign/route.ts | 8 +- .../items/[itemId]/reminder/route.ts | 14 +-- .../kanban/[boardId]/items/[itemId]/route.ts | 13 +- .../[boardId]/items/[itemId]/status/route.ts | 8 +- app/api/kanban/[boardId]/items/route.ts | 7 +- app/api/kanban/[boardId]/route.ts | 17 +-- app/api/kanban/[boardId]/statuses/route.ts | 10 +- app/api/notes/route.ts | 4 +- .../tasks/[taskId]/items/[itemIndex]/route.ts | 8 +- .../items/[itemIndex]/status/route.ts | 8 +- app/api/tasks/[taskId]/items/route.ts | 7 +- app/api/tasks/[taskId]/route.ts | 21 ++-- .../[taskId]/statuses/[statusId]/route.ts | 8 +- app/api/tasks/[taskId]/statuses/route.ts | 8 +- app/api/tasks/route.ts | 4 +- howto/API.md | 2 + 95 files changed, 602 insertions(+), 1077 deletions(-) diff --git a/app/_components/FeatureComponents/Admin/Parts/AdminContent.tsx b/app/_components/FeatureComponents/Admin/Parts/AdminContent.tsx index 0d3ab6e0..1f800451 100644 --- a/app/_components/FeatureComponents/Admin/Parts/AdminContent.tsx +++ b/app/_components/FeatureComponents/Admin/Parts/AdminContent.tsx @@ -13,7 +13,6 @@ import { AdminContentColumn } from "./AdminContentColumn"; import { ExportContent } from "./AdminExport"; import { Accordion } from "@/app/_components/GlobalComponents/Layout/Accordion"; import { UserAvatar } from "@/app/_components/GlobalComponents/User/UserAvatar"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; import { rebuildLinkIndex } from "@/app/_server/actions/link"; import { updateTagsFromContent } from "@/app/_server/actions/tags"; import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; diff --git a/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx b/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx index dc037ddf..9fa95a79 100644 --- a/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx +++ b/app/_components/FeatureComponents/Checklists/ChecklistsPageClient.tsx @@ -25,6 +25,7 @@ import { ChecklistListItem } from "@/app/_components/GlobalComponents/Cards/Chec import { ChecklistGridItem } from "@/app/_components/GlobalComponents/Cards/ChecklistGridItem"; import { useChecklistsFilter } from "@/app/_components/FeatureComponents/Checklists/ChecklistsClient"; import { isKanbanType } from "@/app/_types/enums"; +import { itemHref } from "@/app/_utils/global-utils"; interface ChecklistsPageClientProps { initialLists: Checklist[]; @@ -54,12 +55,13 @@ export const ChecklistsPageClient = ({ let filtered = [...initialLists]; if (checklistFilter === "pinned") { - const pinnedPaths = user?.pinnedLists || []; - filtered = filtered.filter((list) => { - const uuidPath = `${list.category || "Uncategorized"}/${list.uuid || list.id}`; - const idPath = `${list.category || "Uncategorized"}/${list.id}`; - return pinnedPaths.includes(uuidPath) || pinnedPaths.includes(idPath); - }); + const pinnedEntries = user?.pinnedLists || []; + filtered = filtered.filter((list) => + pinnedEntries.some( + (entry) => + entry === list.uuid || entry.split("/").pop() === list.uuid, + ), + ); } else if (checklistFilter === "completed") { filtered = filtered.filter( (list) => @@ -135,15 +137,11 @@ export const ChecklistsPageClient = ({ ]); const handleTogglePin = async (list: Checklist) => { - if (!user || isTogglingPin === list.id) return; + if (!user || !list.uuid || isTogglingPin === list.uuid) return; - setIsTogglingPin(list.id); + setIsTogglingPin(list.uuid); try { - const result = await togglePin( - list.id, - list.category || "Uncategorized", - ItemTypes.CHECKLIST, - ); + const result = await togglePin(list.uuid, ItemTypes.CHECKLIST); if (result.success) { router.refresh(); } @@ -268,14 +266,15 @@ export const ChecklistsPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}`, + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => handleTogglePin(list)} /> @@ -287,14 +286,15 @@ export const ChecklistsPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}`, + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => handleTogglePin(list)} /> @@ -306,14 +306,15 @@ export const ChecklistsPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}`, + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => handleTogglePin(list)} /> diff --git a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx index e6e1fa05..edb8a04f 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx @@ -18,7 +18,8 @@ import { useChecklist } from "@/app/_hooks/useChecklist"; import { isKanbanType, Modes } from "@/app/_types/enums"; import { useShortcut } from "@/app/_providers/ShortcutsProvider"; import { toggleArchive } from "@/app/_server/actions/dashboard"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; +import { ItemTypes } from "@/app/_types/enums"; import { useTranslations } from "next-intl"; interface ChecklistClientProps { @@ -84,11 +85,7 @@ export const ChecklistClient = ({ const handleCloneConfirm = async (targetCategory: string) => { const formData = new FormData(); - formData.append("id", localChecklist.id); - formData.append( - "originalCategory", - localChecklist.category || "Uncategorized", - ); + formData.append("uuid", localChecklist.uuid || ""); formData.append("category", targetCategory || "Uncategorized"); if (localChecklist.owner) { formData.append("user", localChecklist.owner); @@ -97,13 +94,8 @@ export const ChecklistClient = ({ const { cloneChecklist } = await import("@/app/_server/actions/checklist"); const result = await cloneChecklist(formData); - if (result.success && result.data) { - router.push( - `/checklist/${buildCategoryPath( - result.data.category || "Uncategorized", - result.data.id, - )}`, - ); + if (result.success && result.data?.uuid) { + router.push(itemHref(ItemTypes.CHECKLIST, result.data.uuid)); router.refresh(); } }; @@ -219,8 +211,8 @@ export const ChecklistClient = ({ setShowCreateModal(false)} onCreated={(newChecklist) => { - if (newChecklist) { - router.push(`/checklist/${newChecklist.id}`); + if (newChecklist?.uuid) { + router.push(itemHref(ItemTypes.CHECKLIST, newChecklist.uuid)); } setShowCreateModal(false); router.refresh(); diff --git a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx index 69017b6c..f630f65c 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/Common/ChecklistHeader.tsx @@ -20,7 +20,6 @@ import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; import { Checklist } from "@/app/_types"; import { useChecklist } from "../../../../../_hooks/useChecklist"; import { DropdownMenu } from "@/app/_components/GlobalComponents/Dropdowns/DropdownMenu"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { sharingInfo } from "@/app/_utils/sharing-utils"; import { ChecklistsTypes } from "@/app/_types/enums"; @@ -62,8 +61,7 @@ export const ChecklistHeader = ({ const { permissions } = usePermissions(); const [showSharedWithModal, setShowSharedWithModal] = useState(false); - const encodedCategory = encodeCategoryPath(metadata.category); - const itemDetails = sharingInfo(globalSharing, metadata.uuid || metadata.id, encodedCategory); + const itemDetails = sharingInfo(globalSharing, metadata.uuid || ""); const isShared = itemDetails.exists && itemDetails.sharedWith.length > 0; const sharedWith = itemDetails.sharedWith; @@ -91,7 +89,7 @@ export const ChecklistHeader = ({ size="sm" onClick={handleCopyId} className="h-6 w-6 p-0" - title={`Copy ID: ${checklist?.uuid || checklist?.id}`} + title={`Copy ID: ${checklist?.uuid}`} > {copied ? ( diff --git a/app/_components/FeatureComponents/Checklists/Parts/Simple/ChecklistBody.tsx b/app/_components/FeatureComponents/Checklists/Parts/Simple/ChecklistBody.tsx index 2509f431..8a45d199 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/Simple/ChecklistBody.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/Simple/ChecklistBody.tsx @@ -63,12 +63,11 @@ export const ChecklistBody = ({ return getReferences( linkIndex, localList.uuid, - localList.category, ItemTypes.CHECKLIST, notes, checklists ); - }, [linkIndex, localList.uuid, localList.category, notes, checklists]); + }, [linkIndex, localList.uuid, notes, checklists]); const onDragStart = (event: DragStartEvent) => { const findItem = (items: Item[], id: string): Item | undefined => { diff --git a/app/_components/FeatureComponents/Checklists/TasksPageClient.tsx b/app/_components/FeatureComponents/Checklists/TasksPageClient.tsx index 7aa4369a..0ee6158a 100644 --- a/app/_components/FeatureComponents/Checklists/TasksPageClient.tsx +++ b/app/_components/FeatureComponents/Checklists/TasksPageClient.tsx @@ -10,7 +10,8 @@ import { PlayCircleIcon, } from "hugeicons-react"; import { Checklist, SanitisedUser } from "@/app/_types"; -import { TaskStatus } from "@/app/_types/enums"; +import { ItemTypes, TaskStatus } from "@/app/_types/enums"; +import { itemHref } from "@/app/_utils/global-utils"; import { EmptyState } from "@/app/_components/GlobalComponents/Cards/EmptyState"; import { ChecklistCard } from "@/app/_components/GlobalComponents/Cards/ChecklistCard"; import { ChecklistListItem } from "@/app/_components/GlobalComponents/Cards/ChecklistListItem"; @@ -52,12 +53,13 @@ export const TasksPageClient = ({ let filtered = [...initialLists]; if (taskFilter === "pinned") { - const pinnedPaths = user?.pinnedLists || []; - filtered = filtered.filter((list) => { - const uuidPath = `${list.category || "Uncategorized"}/${list.uuid || list.id}`; - const idPath = `${list.category || "Uncategorized"}/${list.id}`; - return pinnedPaths.includes(uuidPath) || pinnedPaths.includes(idPath); - }); + const pinnedEntries = user?.pinnedLists || []; + filtered = filtered.filter((list) => + pinnedEntries.some( + (entry) => + entry === list.uuid || entry.split("/").pop() === list.uuid, + ), + ); } else if (taskFilter === "completed") { filtered = filtered.filter( (list) => @@ -278,14 +280,15 @@ export const TasksPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}` + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => { }} /> @@ -297,14 +300,15 @@ export const TasksPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}` + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => { }} /> @@ -316,14 +320,15 @@ export const TasksPageClient = ({
{paginatedItems.map((list) => ( { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - router.push(`/checklist/${categoryPath}`); - }} - isPinned={user?.pinnedLists?.includes( - `${list.category || "Uncategorized"}/${list.id}` + onSelect={(list) => + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid || "")) + } + isPinned={user?.pinnedLists?.some( + (entry) => + entry === list.uuid || + entry.split("/").pop() === list.uuid, )} onTogglePin={() => { }} /> diff --git a/app/_components/FeatureComponents/Home/HomeClient.tsx b/app/_components/FeatureComponents/Home/HomeClient.tsx index 1d820d81..057505e8 100644 --- a/app/_components/FeatureComponents/Home/HomeClient.tsx +++ b/app/_components/FeatureComponents/Home/HomeClient.tsx @@ -8,8 +8,8 @@ import { Layout } from "@/app/_components/GlobalComponents/Layout/Layout"; import { Checklist, Category, Note, SanitisedUser } from "@/app/_types"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { useShortcut } from "@/app/_providers/ShortcutsProvider"; -import { Modes } from "@/app/_types/enums"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; +import { ItemTypes, Modes } from "@/app/_types/enums"; +import { itemHref } from "@/app/_utils/global-utils"; import { MobileHeader } from "@/app/_components/GlobalComponents/Layout/MobileHeader"; interface SharingStatus { @@ -70,11 +70,8 @@ export const HomeClient = ({ user={user} onCreateModal={handleOpenCreateModal} onSelectChecklist={(list) => { - const categoryPath = buildCategoryPath( - list.category || "Uncategorized", - list.id - ); - router.push(`/checklist/${categoryPath}`); + if (!list.uuid) return; + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid)); }} /> )} @@ -86,11 +83,8 @@ export const HomeClient = ({ user={user} onCreateModal={handleOpenCreateModal} onSelectNote={(note) => { - const categoryPath = buildCategoryPath( - note.category || "Uncategorized", - note.id - ); - router.push(`/note/${categoryPath}`); + if (!note.uuid) return; + router.push(itemHref(ItemTypes.NOTE, note.uuid)); }} /> )} diff --git a/app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx b/app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx index 6021979f..ebef1c59 100644 --- a/app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx +++ b/app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx @@ -17,7 +17,6 @@ import { } from "@dnd-kit/sortable"; import { useChecklistHome } from "@/app/_hooks/useChecklistHome"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; import { useTranslations } from "next-intl"; import { useSettings } from "@/app/_utils/settings-store"; import { ChecklistListItem } from "@/app/_components/GlobalComponents/Cards/ChecklistListItem"; @@ -135,11 +134,8 @@ export const ChecklistHome = ({ selectedCategory?.split("/").pop() || selectedCategory; const getListSharer = (list: Checklist) => { - const encodedCategory = encodeCategoryPath( - list.category || "Uncategorized", - ); const sharedItem = userSharedItems?.checklists?.find( - (item) => item.id === list.id && item.category === encodedCategory, + (item) => item.uuid === list.uuid, ); return sharedItem?.sharer; }; @@ -229,14 +225,14 @@ export const ChecklistHome = ({ onDragEnd={handleDragEnd} > list.uuid || list.id)} + items={pinned.map((list) => list.uuid)} strategy={verticalListSortingStrategy} > {viewMode === "card" && (
{pinned.map((list) => ( {pinned.map((list) => ( {pinned.map((list) => ( { - const encodedCategory = encodeCategoryPath( - note.category || "Uncategorized", - ); const sharedItem = userSharedItems?.notes?.find( - (item) => item.id === note.id && item.category === encodedCategory, + (item) => item.uuid === note.uuid, ); return sharedItem?.sharer; }; @@ -220,7 +216,7 @@ export const NotesHome = ({ onDragEnd={handleDragEnd} > note.uuid || note.id)} + items={pinned.map((note) => note.uuid)} strategy={verticalListSortingStrategy} > {viewMode === "card" && ( @@ -231,7 +227,7 @@ export const NotesHome = ({ > {pinned.map((note) => (
{pinned.map((note) => ( {pinned.map((note) => ( { - const encodedCategory = encodeCategoryPath( - note.category || "Uncategorized", - ); const sharedItem = userSharedItems?.notes?.find( - (item) => item.id === note.id && item.category === encodedCategory, + (item) => item.uuid && item.uuid === note.uuid, ); return sharedItem?.sharer; }; const getListSharer = (list: Checklist) => { - const encodedCategory = encodeCategoryPath( - list.category || "Uncategorized", - ); const sharedItem = userSharedItems?.checklists?.find( - (item) => item.id === list.id && item.category === encodedCategory, + (item) => item.uuid && item.uuid === list.uuid, ); return sharedItem?.sharer; }; const handleSelectNote = (note: Note) => { - const categoryPath = buildCategoryPath( - note.category || "Uncategorized", - note.id, - ); - router.push(`/note/${categoryPath}`); + if (!note.uuid) return; + router.push(itemHref(ItemTypes.NOTE, note.uuid)); }; const handleSelectChecklist = (list: Checklist) => { - const categoryPath = buildCategoryPath( - list.category || "Uncategorized", - list.id, - ); - router.push(`/checklist/${categoryPath}`); + if (!list.uuid) return; + router.push(itemHref(ItemTypes.CHECKLIST, list.uuid)); }; if (combinedItems.length === 0 && !selectedFilter) { @@ -178,7 +164,7 @@ export const TagsHome = ({ if (viewMode === "card") { return (
{ const handleUnarchive = async (itemId: string) => { const formData = new FormData(); - formData.append("listId", localChecklist.id); + formData.append("uuid", localChecklist.uuid || ""); formData.append("itemId", itemId); - formData.append("category", localChecklist.category || "Uncategorized"); const result = await unarchiveItem(formData); if (result.success && result.data) { @@ -152,9 +151,8 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { for (const item of items) { const formData = new FormData(); - formData.append("listId", localChecklist.id); + formData.append("uuid", localChecklist.uuid || ""); formData.append("itemId", item.id); - formData.append("category", localChecklist.category || "Uncategorized"); const result = await archiveItem(formData); if (!result.success || !result.data) { @@ -192,8 +190,7 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { } }, [ - localChecklist.id, - localChecklist.category, + localChecklist.uuid, onUpdate, refreshChecklist, showToast, @@ -280,8 +277,6 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { title={column.title} items={items} status={column.status} - checklistId={localChecklist.id} - category={localChecklist.category || "Uncategorized"} onUpdate={handleItemUpdate} onOpenDetail={(item) => setDetailItemId(item.id)} isShared={isShared} @@ -317,18 +312,11 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { return getReferences( linkIndex, localChecklist.uuid, - localChecklist.category, ItemTypes.CHECKLIST, notes, checklists, ); - }, [ - linkIndex, - localChecklist.uuid, - localChecklist.category, - checklists, - notes, - ]); + }, [linkIndex, localChecklist.uuid, checklists, notes]); return (
@@ -456,8 +444,6 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { checklist={localChecklist} item={ghostItem} isDragging - checklistId={localChecklist.id} - category={localChecklist.category || "Uncategorized"} onUpdate={() => { }} onOpenDetail={() => { }} isShared={isShared} @@ -485,8 +471,6 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { isOpen={!!calendarSelectedItem} onClose={() => setCalendarSelectedItem(null)} onUpdate={handleItemUpdate} - checklistId={localChecklist.id} - category={localChecklist.category || "Uncategorized"} /> )} @@ -497,8 +481,6 @@ export const Kanban = ({ checklist, onUpdate }: KanbanBoardProps) => { isOpen={!!detailItem} onClose={() => setDetailItemId(null)} onUpdate={handleItemUpdate} - checklistId={localChecklist.id} - category={localChecklist.category || "Uncategorized"} /> )} diff --git a/app/_components/FeatureComponents/Kanban/KanbanCard.tsx b/app/_components/FeatureComponents/Kanban/KanbanCard.tsx index c0f6cc8f..178025b8 100644 --- a/app/_components/FeatureComponents/Kanban/KanbanCard.tsx +++ b/app/_components/FeatureComponents/Kanban/KanbanCard.tsx @@ -34,8 +34,6 @@ interface KanbanCardProps { index?: number; listId?: string; isDragging?: boolean; - checklistId: string; - category: string; onUpdate: (updatedChecklist: Checklist) => void; onOpenDetail: (item: Item) => void; isShared: boolean; @@ -49,8 +47,6 @@ const KanbanCardComponent = ({ index = 0, listId, isDragging, - checklistId, - category, onUpdate, onOpenDetail, isShared, @@ -83,8 +79,6 @@ const KanbanCardComponent = ({ const kanbanItemHook = useKanbanItem({ checklist, item, - checklistId, - category, onUpdate, }); @@ -158,9 +152,8 @@ const KanbanCardComponent = ({ isOpen={showTimeEntriesModal} onClose={() => setShowTimeEntriesModal(false)} timeEntries={item.timeEntries} - checklistId={checklistId} + checklistUuid={checklist.uuid || ""} itemId={item.id} - category={category} onUpdate={onUpdate} usersPublicData={usersPublicData} /> diff --git a/app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx b/app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx index 1a6c26e3..d1bf94b8 100644 --- a/app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx +++ b/app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx @@ -36,8 +36,6 @@ interface KanbanCardDetailProps { isOpen: boolean; onClose: () => void; onUpdate: (updatedChecklist: Checklist) => void; - checklistId: string; - category: string; } const _sanitizeDescription = (text: string): string => text.replace(/\n/g, "\\n"); @@ -79,9 +77,8 @@ export const KanbanCardDetail = ({ isOpen, onClose, onUpdate, - checklistId, - category, }: KanbanCardDetailProps) => { + const checklistUuid = checklist.uuid || ""; const t = useTranslations(); const { permissions } = usePermissions(); const { usersPublicData } = useAppMode(); @@ -111,8 +108,6 @@ export const KanbanCardDetail = ({ const kanbanItemHook = useKanbanItem({ checklist, item, - checklistId, - category, onUpdate, }); @@ -133,7 +128,7 @@ export const KanbanCardDetail = ({ useEffect(() => { if (!isOpen) return; const _loadUsers = async () => { - const sharedWithUsers = await getUsersWithAccess(checklistId, checklist.uuid); + const sharedWithUsers = await getUsersWithAccess(checklistUuid); if (sharedWithUsers.length === 0) { setBoardIsShared(false); return; @@ -154,7 +149,7 @@ export const KanbanCardDetail = ({ setAvailableUsers(Array.from(userMap.values())); }; _loadUsers(); - }, [isOpen, checklistId, checklist.uuid, checklist.owner]); + }, [isOpen, checklistUuid, checklist.owner]); const descriptionHtml = useMemo(() => { const noDescText = `

${t("checklists.noDescription")}

`; @@ -165,9 +160,8 @@ export const KanbanCardDetail = ({ const _saveField = async (fields: Record) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", item.id); - formData.append("category", category); Object.entries(fields).forEach(([key, value]) => formData.append(key, value)); const result = await updateItem(checklist, formData); if (result.success && result.data) { @@ -182,12 +176,11 @@ export const KanbanCardDetail = ({ const currentUnsanitized = _unsanitizeDescription(item.description || ""); if (editText.trim() !== item.text || editDescription.trim() !== currentUnsanitized) { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", item.id); formData.append("text", editText.trim()); formData.append("description", sanitizedDescription); - formData.append("category", category); - const result = await updateItem(checklist, formData); + const result = await updateItem(checklist, formData); if (result.success && result.data) { onUpdate(result.data); const updatedItem = _findItemInChecklist(result.data, item.id); @@ -203,10 +196,9 @@ export const KanbanCardDetail = ({ const handleAddSubtask = async (text: string) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("parentId", item.id); formData.append("text", text); - formData.append("category", category); const result = await createSubItem(formData); if (result.success && result.data) { onUpdate(result.data); @@ -217,10 +209,9 @@ export const KanbanCardDetail = ({ const handleAddNestedSubtask = async (parentId: string, text: string) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("parentId", parentId); formData.append("text", text); - formData.append("category", category); const result = await createSubItem(formData); if (result.success && result.data) { onUpdate(result.data); @@ -241,10 +232,9 @@ export const KanbanCardDetail = ({ const handleToggleSubtask = async (subtaskId: string, completed: boolean) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", subtaskId); formData.append("completed", completed.toString()); - formData.append("category", category); const result = await updateItem(checklist, formData); if (result.success && result.data) { onUpdate(result.data); @@ -253,10 +243,9 @@ export const KanbanCardDetail = ({ setItem(updatedItem); if (_autoCompleteStatus && updatedItem.children?.length && _allChildrenComplete(updatedItem.children)) { const statusFormData = new FormData(); - statusFormData.append("listId", checklistId); + statusFormData.append("uuid", checklistUuid); statusFormData.append("itemId", item.id); statusFormData.append("status", _autoCompleteStatus.id); - statusFormData.append("category", category); const statusResult = await updateItemStatus(statusFormData); if (statusResult.success && statusResult.data) { onUpdate(statusResult.data as import("@/app/_types").Checklist); @@ -268,10 +257,9 @@ export const KanbanCardDetail = ({ const handleEditSubtask = async (subtaskId: string, text: string) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", subtaskId); formData.append("text", text); - formData.append("category", category); const result = await updateItem(checklist, formData); if (result.success && result.data) { onUpdate(result.data); @@ -282,9 +270,8 @@ export const KanbanCardDetail = ({ const handleDeleteSubtask = async (subtaskId: string) => { const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", subtaskId); - formData.append("category", category); const result = await deleteItem(formData); if (result.success && result.data) { onUpdate(result.data); @@ -307,10 +294,9 @@ export const KanbanCardDetail = ({ const targetItems = _findTargetItems(item.children); if (!targetItems.length) return; const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("completed", String(completed)); formData.append("itemIds", JSON.stringify(targetItems.map((t) => t.id))); - formData.append("category", category); const result = await bulkToggleItems(formData); if (result.success && result.data) { onUpdate(result.data); @@ -327,10 +313,9 @@ export const KanbanCardDetail = ({ const handleStatusChange = async (status: string) => { setStatusInput(status); const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", item.id); formData.append("status", status); - formData.append("category", category); const result = await updateItemStatus(formData); if (result.success && result.data) { onUpdate(result.data); @@ -427,9 +412,8 @@ export const KanbanCardDetail = ({ isOpen={showTimeEntriesModal} onClose={() => setShowTimeEntriesModal(false)} timeEntries={item.timeEntries} - checklistId={checklistId} + checklistUuid={checklistUuid} itemId={item.id} - category={category} onUpdate={onUpdate} usersPublicData={usersPublicData} /> @@ -579,7 +563,7 @@ export const KanbanCardDetail = ({ title: t("notifications.assignmentTitle"), message: t("notifications.assignmentMessage", { task: item.text, board: checklist.title }), data: { - itemId: checklist.uuid || checklistId, + itemId: checklistUuid, itemType: "checklist", taskId: item.id, }, diff --git a/app/_components/FeatureComponents/Kanban/KanbanColumn.tsx b/app/_components/FeatureComponents/Kanban/KanbanColumn.tsx index 4feebdad..9b333042 100644 --- a/app/_components/FeatureComponents/Kanban/KanbanColumn.tsx +++ b/app/_components/FeatureComponents/Kanban/KanbanColumn.tsx @@ -17,8 +17,6 @@ interface KanbanColumnProps { title: string; items: Item[]; status: string; - checklistId: string; - category: string; onUpdate: (updatedChecklist: Checklist) => void; onOpenDetail: (item: Item) => void; isShared: boolean; @@ -81,8 +79,6 @@ const KanbanColumnComponent = ({ title, items, status, - checklistId, - category, isShared, onUpdate, onOpenDetail, @@ -222,8 +218,6 @@ const KanbanColumnComponent = ({ item={item} index={index} listId={status} - checklistId={checklistId} - category={category} onUpdate={onUpdate} onOpenDetail={onOpenDetail} isShared={isShared} diff --git a/app/_components/FeatureComponents/Kanban/KanbanPageClient.tsx b/app/_components/FeatureComponents/Kanban/KanbanPageClient.tsx index 8b9d8dd1..6bd3d687 100644 --- a/app/_components/FeatureComponents/Kanban/KanbanPageClient.tsx +++ b/app/_components/FeatureComponents/Kanban/KanbanPageClient.tsx @@ -10,7 +10,8 @@ import { PlayCircleIcon, } from "hugeicons-react"; import { Checklist, SanitisedUser } from "@/app/_types"; -import { TaskStatus } from "@/app/_types/enums"; +import { ItemTypes, TaskStatus } from "@/app/_types/enums"; +import { itemHref } from "@/app/_utils/global-utils"; import { EmptyState } from "@/app/_components/GlobalComponents/Cards/EmptyState"; import { ChecklistCard } from "@/app/_components/GlobalComponents/Cards/ChecklistCard"; import { ChecklistListItem } from "@/app/_components/GlobalComponents/Cards/ChecklistListItem"; @@ -53,12 +54,13 @@ export const KanbanPageClient = ({ let filtered = [...initialLists]; if (taskFilter === "pinned") { - const pinnedPaths = user?.pinnedLists || []; - filtered = filtered.filter((list) => { - const uuidPath = `${list.category || "Uncategorized"}/${list.uuid || list.id}`; - const idPath = `${list.category || "Uncategorized"}/${list.id}`; - return pinnedPaths.includes(uuidPath) || pinnedPaths.includes(idPath); - }); + const pinnedEntries = user?.pinnedLists || []; + filtered = filtered.filter((list) => + pinnedEntries.some( + (entry) => + entry === list.uuid || entry.split("/").pop() === list.uuid, + ), + ); } else if (taskFilter === "completed") { filtered = filtered.filter( (list) => @@ -175,15 +177,17 @@ export const KanbanPageClient = ({ } const renderList = (list: Checklist) => { - const categoryPath = `${list.category || "Uncategorized"}/${list.id}`; - const isPinned = user?.pinnedLists?.includes(categoryPath); + const listHref = itemHref(ItemTypes.CHECKLIST, list.uuid || ""); + const isPinned = user?.pinnedLists?.some( + (entry) => entry === list.uuid || entry.split("/").pop() === list.uuid, + ); if (viewMode === 'list') { return ( router.push(`/checklist/${categoryPath}`)} + onSelect={() => router.push(listHref)} isPinned={isPinned} onTogglePin={() => {}} /> @@ -193,9 +197,9 @@ export const KanbanPageClient = ({ if (viewMode === 'grid') { return ( router.push(`/checklist/${categoryPath}`)} + onSelect={() => router.push(listHref)} isPinned={isPinned} onTogglePin={() => {}} /> @@ -204,9 +208,9 @@ export const KanbanPageClient = ({ return ( router.push(`/checklist/${categoryPath}`)} + onSelect={() => router.push(listHref)} isPinned={isPinned} onTogglePin={() => {}} /> diff --git a/app/_components/FeatureComponents/Kanban/TimeEntriesModal.tsx b/app/_components/FeatureComponents/Kanban/TimeEntriesModal.tsx index c915e3f0..c34d680c 100644 --- a/app/_components/FeatureComponents/Kanban/TimeEntriesModal.tsx +++ b/app/_components/FeatureComponents/Kanban/TimeEntriesModal.tsx @@ -15,9 +15,8 @@ interface TimeEntriesModalProps { isOpen: boolean; onClose: () => void; timeEntries: TimeEntry[]; - checklistId: string; + checklistUuid: string; itemId: string; - category: string; onUpdate: (updatedChecklist: Checklist) => void; usersPublicData?: { username?: string; avatarUrl?: string }[]; } @@ -60,9 +59,8 @@ const TimeEntriesModalComponent = ({ isOpen, onClose, timeEntries, - checklistId, + checklistUuid, itemId, - category, onUpdate, usersPublicData, }: TimeEntriesModalProps) => { @@ -88,10 +86,9 @@ const TimeEntriesModalComponent = ({ if (!editing) return; setSaving(true); const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", itemId); formData.append("entryId", editing.entryId); - formData.append("category", category); if (editing.startTime) formData.append("startTime", new Date(editing.startTime).toISOString()); if (editing.endTime) { @@ -116,10 +113,9 @@ const TimeEntriesModalComponent = ({ if (!deleteTarget) return; setSaving(true); const formData = new FormData(); - formData.append("listId", checklistId); + formData.append("uuid", checklistUuid); formData.append("itemId", itemId); formData.append("entryId", deleteTarget); - formData.append("category", category); const result = await deleteTimeEntry(formData); if (result.success && result.data) { onUpdate(result.data); diff --git a/app/_components/FeatureComponents/Notes/NoteClient.tsx b/app/_components/FeatureComponents/Notes/NoteClient.tsx index 2e90caaa..4fc2b195 100644 --- a/app/_components/FeatureComponents/Notes/NoteClient.tsx +++ b/app/_components/FeatureComponents/Notes/NoteClient.tsx @@ -10,7 +10,8 @@ import { useShortcut } from "@/app/_providers/ShortcutsProvider"; import { useShortcuts } from "@/app/_hooks/useShortcuts"; import { useNoteEditor } from "@/app/_hooks/useNoteEditor"; import { useAppMode } from "@/app/_providers/AppModeProvider"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; +import { ItemTypes } from "@/app/_types/enums"; import { CloneCategoryModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/CloneCategoryModal"; import { SwipeNavigationWrapper } from "@/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper"; @@ -57,9 +58,7 @@ export const NoteClient = ({ note, categories }: NoteClientProps) => { const handleCloneConfirm = async (targetCategory: string) => { const formData = new FormData(); - formData.append("id", localNote.id); formData.append("uuid", localNote.uuid || ""); - formData.append("originalCategory", localNote.category || "Uncategorized"); formData.append("category", targetCategory || "Uncategorized"); if (localNote.owner) { formData.append("user", localNote.owner); @@ -68,13 +67,8 @@ export const NoteClient = ({ note, categories }: NoteClientProps) => { const { cloneNote } = await import("@/app/_server/actions/note"); const result = await cloneNote(formData); - if (result.success && result.data) { - router.push( - `/note/${buildCategoryPath( - result.data.category || "Uncategorized", - result.data.id, - )}`, - ); + if (result.success && result.data?.uuid) { + router.push(itemHref(ItemTypes.NOTE, result.data.uuid)); router.refresh(); } }; @@ -117,8 +111,7 @@ export const NoteClient = ({ note, categories }: NoteClientProps) => { isEditorInEditMode={viewModel.isEditing} > { - const uuidPath = `${note.category || "Uncategorized"}/${note.uuid || note.id}`; - const idPath = `${note.category || "Uncategorized"}/${note.id}`; - return pinnedPaths.includes(uuidPath) || pinnedPaths.includes(idPath); - }); + const pinnedEntries = user?.pinnedNotes || []; + filtered = filtered.filter((note) => + pinnedEntries.some( + (entry) => + entry === note.uuid || entry.split("/").pop() === note.uuid, + ), + ); } else if (noteFilter === "recent") { filtered = filtered .sort( @@ -102,15 +104,11 @@ export const NotesPageClient = ({ }, [currentPage, totalPages, totalItems, goToPage, handleItemsPerPageChange, setPaginationInfo]); const handleTogglePin = async (note: Note) => { - if (!user || isTogglingPin === note.id) return; + if (!user || !note.uuid || isTogglingPin === note.uuid) return; - setIsTogglingPin(note.id); + setIsTogglingPin(note.uuid); try { - const result = await togglePin( - note.id, - note.category || "Uncategorized", - ItemTypes.NOTE - ); + const result = await togglePin(note.uuid, ItemTypes.NOTE); if (result.success) { router.refresh(); } @@ -164,15 +162,16 @@ export const NotesPageClient = ({ columnClassName="pl-4 bg-clip-padding" > {paginatedItems.map((note) => ( -
+
{ - const categoryPath = `${note.category || "Uncategorized"}/${note.id}`; - router.push(`/note/${categoryPath}`); - }} - isPinned={user?.pinnedNotes?.includes( - `${note.category || "Uncategorized"}/${note.id}` + onSelect={(note) => + router.push(itemHref(ItemTypes.NOTE, note.uuid || "")) + } + isPinned={user?.pinnedNotes?.some( + (entry) => + entry === note.uuid || + entry.split("/").pop() === note.uuid, )} onTogglePin={() => handleTogglePin(note)} /> @@ -185,14 +184,15 @@ export const NotesPageClient = ({
{paginatedItems.map((note) => ( { - const categoryPath = `${note.category || "Uncategorized"}/${note.id}`; - router.push(`/note/${categoryPath}`); - }} - isPinned={user?.pinnedNotes?.includes( - `${note.category || "Uncategorized"}/${note.id}` + onSelect={(note) => + router.push(itemHref(ItemTypes.NOTE, note.uuid || "")) + } + isPinned={user?.pinnedNotes?.some( + (entry) => + entry === note.uuid || + entry.split("/").pop() === note.uuid, )} onTogglePin={() => handleTogglePin(note)} /> @@ -204,14 +204,15 @@ export const NotesPageClient = ({
{paginatedItems.map((note) => ( { - const categoryPath = `${note.category || "Uncategorized"}/${note.id}`; - router.push(`/note/${categoryPath}`); - }} - isPinned={user?.pinnedNotes?.includes( - `${note.category || "Uncategorized"}/${note.id}` + onSelect={(note) => + router.push(itemHref(ItemTypes.NOTE, note.uuid || "")) + } + isPinned={user?.pinnedNotes?.some( + (entry) => + entry === note.uuid || + entry.split("/").pop() === note.uuid, )} onTogglePin={() => handleTogglePin(note)} /> diff --git a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx index 3f92718c..55cf50dd 100644 --- a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx @@ -62,7 +62,6 @@ export const NoteEditor = ({ editorContent={viewModel.editorContent} onEditorContentChange={viewModel.handleEditorContentChange} noteId={note.uuid} - noteCategory={note.category} encrypted={note.encrypted} onOpenDecryptModal={() => decryptModalRef.current?.()} onOpenViewModal={() => viewModalRef.current?.()} diff --git a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorContent.tsx b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorContent.tsx index 76e0cb6b..6584cbe4 100644 --- a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorContent.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorContent.tsx @@ -19,6 +19,7 @@ import { isEncrypted, } from "@/app/_utils/encryption-utils"; import { useTranslations } from "next-intl"; +import { ItemTypes } from "@/app/_types/enums"; interface NoteEditorContentProps { isEditing: boolean; @@ -30,7 +31,6 @@ interface NoteEditorContentProps { isDirty: boolean, ) => void; noteId?: string; - noteCategory?: string; encrypted?: boolean; onOpenDecryptModal?: () => void; onOpenViewModal?: () => void; @@ -43,7 +43,6 @@ export const NoteEditorContent = ({ editorContent, onEditorContentChange, noteId, - noteCategory, encrypted, onOpenDecryptModal, onOpenViewModal, @@ -64,12 +63,11 @@ export const NoteEditorContent = ({ return getReferences( linkIndex, noteId, - noteCategory, - "note", + ItemTypes.NOTE, notes, checklists, ); - }, [linkIndex, noteId, noteCategory, notes, checklists]); + }, [linkIndex, noteId, notes, checklists]); useEffect(() => { if ( diff --git a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx index a341bc7d..e0424051 100644 --- a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader.tsx @@ -38,11 +38,7 @@ import { useRouter } from "next/navigation"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { toggleArchive } from "@/app/_server/actions/dashboard"; import { Modes } from "@/app/_types/enums"; -import { - copyTextToClipboard, - encodeCategoryPath, - buildCategoryPath, -} from "@/app/_utils/global-utils"; +import { copyTextToClipboard } from "@/app/_utils/global-utils"; import { sharingInfo } from "@/app/_utils/sharing-utils"; import { usePermissions } from "@/app/_providers/PermissionsProvider"; import { SharedWithModal } from "@/app/_components/GlobalComponents/Modals/SharingModals/SharedWithModal"; @@ -163,13 +159,7 @@ export const NoteEditorHeader = ({ }; const handleCopyId = async () => { - const success = await copyTextToClipboard( - `${note?.uuid - ? note?.uuid - : `${encodeCategoryPath(note?.category || "Uncategorized")}/${note?.id - }` - }` - ); + const success = await copyTextToClipboard(note?.uuid || ""); if (success) { setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -243,12 +233,7 @@ export const NoteEditorHeader = ({ }; const { globalSharing } = useAppMode(); - const encodedCategory = encodeCategoryPath(metadata.category); - const itemDetails = sharingInfo( - globalSharing, - metadata.uuid || metadata.id, - encodedCategory - ); + const itemDetails = sharingInfo(globalSharing, metadata.uuid || ""); const isShared = itemDetails.exists && itemDetails.sharedWith.length > 0; const sharedWith = itemDetails.sharedWith; const isPubliclyShared = itemDetails.isPublic; @@ -297,12 +282,7 @@ export const NoteEditorHeader = ({ handleCopyId(); }} className="h-6 w-6 p-0" - title={`Copy ID: ${note?.uuid - ? note?.uuid - : `${encodeCategoryPath( - note?.category || t("notes.uncategorized") - )}/${note?.id}` - }`} + title={`Copy ID: ${note?.uuid || ""}`} > {copied ? ( diff --git a/app/_components/FeatureComponents/Notes/Parts/ReferencedBySection.tsx b/app/_components/FeatureComponents/Notes/Parts/ReferencedBySection.tsx index 3979f9aa..094a7309 100644 --- a/app/_components/FeatureComponents/Notes/Parts/ReferencedBySection.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/ReferencedBySection.tsx @@ -1,7 +1,7 @@ "use client"; import { File02Icon, CheckmarkSquare04Icon } from "hugeicons-react"; -import { encodeCategoryPath } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; import { useRouter } from "next/navigation"; import { ItemType } from "@/app/_types"; import { ItemTypes } from "@/app/_types/enums"; @@ -9,7 +9,7 @@ import { useTranslations } from "next-intl"; interface ReferencingItem { type: ItemType; - path: string; + uuid: string; title: string; category: string; } @@ -26,8 +26,7 @@ export const ReferencedBySection = ({ if (referencingItems.length === 0) return null; const handleItemClick = (item: ReferencingItem) => { - const url = `/${item.type}/${encodeCategoryPath(item.path)}`; - router.push(url); + router.push(itemHref(item.type as ItemTypes, item.uuid)); }; return ( @@ -42,7 +41,7 @@ export const ReferencedBySection = ({
{referencingItems.map((item) => (
handleItemClick(item)} className="bg-muted/50 border border-border rounded-jotty p-3 cursor-pointer hover:shadow-sm hover:border-primary/30 transition-all duration-200 group" > diff --git a/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx b/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx index 1b40f66d..a50c2e1b 100644 --- a/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/SwipeNavigationWrapper.tsx @@ -11,8 +11,7 @@ import { ItemTypes } from "@/app/_types/enums"; interface SwipeNavigationWrapperProps { children: ReactNode; - noteId: string; - noteCategory?: string; + noteUuid: string; enabled: boolean; } @@ -24,8 +23,7 @@ const getNoteUrl = (note: Partial | null, embed = false): string | null => export const SwipeNavigationWrapper = ({ children, - noteId, - noteCategory, + noteUuid, enabled, }: SwipeNavigationWrapperProps) => { const router = useRouter(); @@ -36,7 +34,7 @@ export const SwipeNavigationWrapper = ({ const nextRef = useRef(null); const [isMobile, setIsMobile] = useState(false); - const { prev, next } = useAdjacentNotes(noteId); + const { prev, next } = useAdjacentNotes(noteUuid); const prevUrl = getNoteUrl(prev, true); const nextUrl = getNoteUrl(next, true); diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/InternalLinkComponent.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/InternalLinkComponent.tsx index 79bf16a9..c479d26d 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/InternalLinkComponent.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/InternalLinkComponent.tsx @@ -12,18 +12,13 @@ import { import { useRouter } from "next/navigation"; import { getNoteById } from "@/app/_server/actions/note"; import { getListById } from "@/app/_server/actions/checklist"; -import { - buildCategoryPath, - decodeCategoryPath, - encodeCategoryPath, -} from "@/app/_utils/global-utils"; +import { decodeCategoryPath, itemHref } from "@/app/_utils/global-utils"; import { capitalize } from "lodash"; import { useAppMode } from "@/app/_providers/AppModeProvider"; import { NoteCard } from "@/app/_components/GlobalComponents/Cards/NoteCard"; import { ChecklistCard } from "@/app/_components/GlobalComponents/Cards/ChecklistCard"; import { Checklist, Note } from "@/app/_types"; import { isKanbanType, ItemTypes } from "@/app/_types/enums"; -import { encodeId } from "@/app/_utils/global-utils"; import { useTranslations } from "next-intl"; interface InternalLinkComponentProps { @@ -45,13 +40,8 @@ interface InternalLinkComponentProps { const _returnNote = async (uuid: string, router: any, note?: Note) => { const finalNote = note || (await getNoteById(uuid)); - if (finalNote) { - router.push( - `/note/${buildCategoryPath( - finalNote.category || "Uncategorized", - finalNote.id, - )}`, - ); + if (finalNote?.uuid) { + router.push(itemHref(ItemTypes.NOTE, finalNote.uuid)); return; } @@ -65,13 +55,8 @@ const _returnChecklist = async ( ) => { const finalChecklist = checklist || (await getListById(uuid)); - if (finalChecklist) { - router.push( - `/checklist/${buildCategoryPath( - finalChecklist.category || "Uncategorized", - finalChecklist.id, - )}`, - ); + if (finalChecklist?.uuid) { + router.push(itemHref(ItemTypes.CHECKLIST, finalChecklist.uuid)); return; } return undefined; @@ -147,16 +132,14 @@ export const InternalLinkComponent = ({ if (href.startsWith("/jotty/")) { const uuidFromPath = href.replace("/jotty/", ""); - if (fullItem && fullItem.id) { + if (fullItem?.uuid) { router.push( - `/${ - fullItem && "type" in fullItem && fullItem.type + itemHref( + "type" in fullItem && fullItem.type ? ItemTypes.CHECKLIST - : ItemTypes.NOTE - }/${buildCategoryPath( - fullItem.category || "Uncategorized", - fullItem.id, - )}`, + : ItemTypes.NOTE, + fullItem.uuid, + ), ); return; } @@ -184,67 +167,37 @@ export const InternalLinkComponent = ({ e.stopPropagation(); if (isJottyLink) { - if (fullItem && fullItem.id) { - const pathPrefix = - fullItem && "type" in fullItem && fullItem.type - ? "/checklist/" - : "/note/"; - const newHref = `${pathPrefix}${buildCategoryPath( - fullItem.category || "Uncategorized", - fullItem.id, - )}`; - updateAttributes({ - href: newHref, - type: - fullItem && "type" in fullItem && fullItem.type - ? "checklist" - : "note", - category: fullItem.category || "Uncategorized", - itemId: fullItem.id, - convertToBidirectional: false, - }); - } else if (itemId && category) { - const pathPrefix = type === "checklist" ? "/checklist/" : "/note/"; - const newHref = `${pathPrefix}${buildCategoryPath(category, itemId)}`; + const isChecklist = fullItem && "type" in fullItem && fullItem.type; + const targetUuid = fullItem?.uuid || uuid || href.replace("/jotty/", ""); + + if (targetUuid) { updateAttributes({ - href: newHref, + href: itemHref( + isChecklist ? ItemTypes.CHECKLIST : ItemTypes.NOTE, + targetUuid, + ), + type: isChecklist ? "checklist" : "note", + category: fullItem?.category || category || "Uncategorized", convertToBidirectional: false, }); } else { console.warn("Cannot convert jotty to path - missing data"); } } else if (isPathBasedLink) { - if (uuid) { + const resolvedUuid = + uuid || + fullItem?.uuid || + notes.find((n) => n.id === itemId && (n.category || "") === (category || ""))?.uuid || + checklists.find((c) => c.id === itemId && (c.category || "") === (category || ""))?.uuid; + + if (resolvedUuid) { updateAttributes({ - href: `/jotty/${uuid}`, + href: `/jotty/${resolvedUuid}`, + uuid: resolvedUuid, convertToBidirectional: false, }); - } else if (itemId && category) { - const foundItem = - notes.find( - (n) => - encodeId(n.id || "") === encodeId(itemId) && - encodeCategoryPath(n?.category || "") === - encodeCategoryPath(category), - ) || - checklists.find( - (c) => - encodeId(c.id || "") === encodeId(itemId) && - encodeCategoryPath(c?.category || "") === - encodeCategoryPath(category), - ); - - if (foundItem?.uuid) { - updateAttributes({ - href: `/jotty/${foundItem.uuid}`, - uuid: foundItem.uuid, - convertToBidirectional: false, - }); - } else { - console.log("Could not find item to convert"); - } } else { - console.log("Missing itemId or category"); + console.log("Could not find item to convert"); } } }; diff --git a/app/_components/FeatureComponents/Notes/Parts/UnifiedMarkdownRenderer.tsx b/app/_components/FeatureComponents/Notes/Parts/UnifiedMarkdownRenderer.tsx index c440fee6..356b8c8a 100644 --- a/app/_components/FeatureComponents/Notes/Parts/UnifiedMarkdownRenderer.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/UnifiedMarkdownRenderer.tsx @@ -27,7 +27,7 @@ import { InternalLinkComponent } from "./TipTap/CustomExtensions/InternalLinkCom import { TagLinkViewComponent } from "@/app/_components/FeatureComponents/Tags/TagLinkComponent"; import { ItemTypes } from "@/app/_types/enums"; import { extractYamlMetadata } from "@/app/_utils/yaml-metadata-utils"; -import { decodeCategoryPath, decodeId } from "@/app/_utils/global-utils"; +import { decodeCategoryPath } from "@/app/_utils/global-utils"; import { NoteFooterStats } from "@/app/_components/GlobalComponents/Statistics/NoteFooterStats"; import { useTranslations } from "next-intl"; import { @@ -227,7 +227,7 @@ export const UnifiedMarkdownRenderer = ({ ?.replace("/checklist/", "") .replace("/note/", "") .split("/"); - linkItemId = decodeId(pathParts?.[pathParts.length - 1] || ""); + linkItemId = decodeURIComponent(pathParts?.[pathParts.length - 1] || ""); linkCategory = decodeCategoryPath( pathParts?.slice(0, -1).join("/") || "" ); diff --git a/app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts b/app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts index 12d01dad..1399d390 100644 --- a/app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts +++ b/app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts @@ -1,6 +1,7 @@ import { Checklist, ItemType, LinkIndex, Note } from "@/app/_types"; import { ItemTypes } from "@/app/_types/enums"; -import { buildCategoryPath } from "@/app/_utils/global-utils"; +import { itemHref } from "@/app/_utils/global-utils"; +import { isUuid } from "@/app/_consts/identity"; export const MAX_GRAPH_NODES = 600; export const MAX_TAG_LINK_FANOUT = 8; @@ -50,14 +51,6 @@ export interface ConnectionGraphFilters { type ItemLike = Partial | Partial; -const isUuidLike = (value: string) => - /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( - value, - ); - -const itemTypePath = (type: ItemType) => - type === ItemTypes.CHECKLIST ? "checklist" : "note"; - const fallbackTitle = (type: ItemType, id: string) => `${type === ItemTypes.CHECKLIST ? "Checklist" : "Note"} ${id.slice(0, 8)}`; @@ -83,7 +76,7 @@ export const resolveGraphId = ( rawId: string, uuidByLegacyKey: Map, ) => { - if (isUuidLike(rawId)) return rawId; + if (isUuid(rawId)) return rawId; return uuidByLegacyKey.get(rawId) || rawId; }; @@ -232,7 +225,7 @@ export const buildConnectionGraphData = ( const connectionCount = inboundCount + outboundCount + tagConnectionCount; const category = item.category || "Uncategorized"; const itemId = item.id || uuid; - const path = buildCategoryPath(category, itemId); + const path = `${category}/${itemId}`; return { id: uuid, @@ -242,7 +235,7 @@ export const buildConnectionGraphData = ( category, itemId, path, - url: `/${itemTypePath(type)}/${path}`, + url: itemHref(type, uuid), tags: item.tags || [], createdAt: item.createdAt, updatedAt: item.updatedAt, diff --git a/app/_components/FeatureComponents/Search/Parts/SearchResults.tsx b/app/_components/FeatureComponents/Search/Parts/SearchResults.tsx index 8d80716d..f6544088 100644 --- a/app/_components/FeatureComponents/Search/Parts/SearchResults.tsx +++ b/app/_components/FeatureComponents/Search/Parts/SearchResults.tsx @@ -8,6 +8,7 @@ import { useTranslations } from "next-intl"; interface SearchResult { id: string; + uuid?: string; title: string; type: ItemType; content?: string; @@ -73,7 +74,7 @@ export const SearchResults = ({
{results.map((result, index) => (