From 3afe1e21df6dcc0483e74e0db02d9d82af32ecea Mon Sep 17 00:00:00 2001 From: xuatz Date: Mon, 2 Jun 2025 04:01:26 +0900 Subject: feat: add user customisable default archive display behaviour (#1505) * fix typo * implementation * bug fix and refactoring * Use nuqs for searchParam management * remove the todo about the tests * fix tests --------- Co-authored-by: Mohamed Bassem --- apps/web/app/dashboard/lists/[listId]/page.tsx | 15 ++++++- apps/web/app/dashboard/tags/[tagId]/page.tsx | 15 ++++++- apps/web/app/layout.tsx | 21 +++++---- .../web/components/dashboard/lists/ListOptions.tsx | 20 ++++++++- apps/web/components/dashboard/tags/TagOptions.tsx | 14 +++++- apps/web/components/settings/UserOptions.tsx | 52 ++++++++++++++++++++-- apps/web/components/utils/useShowArchived.tsx | 24 ++++++++++ apps/web/lib/i18n/locales/en/translation.json | 14 ++++-- apps/web/lib/userSettings.tsx | 1 + apps/web/package.json | 1 + 10 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 apps/web/components/utils/useShowArchived.tsx (limited to 'apps/web') diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index 6c0bc36c..de0f5054 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -8,9 +8,14 @@ import { BookmarkListContextProvider } from "@karakeep/shared-react/hooks/bookma export default async function ListPage({ params, + searchParams, }: { params: { listId: string }; + searchParams?: { + includeArchived?: string; + }; }) { + const userSettings = await api.users.settings(); let list; try { list = await api.lists.get({ listId: params.listId }); @@ -23,10 +28,18 @@ export default async function ListPage({ throw e; } + const includeArchived = + searchParams?.includeArchived !== undefined + ? searchParams.includeArchived === "true" + : userSettings.archiveDisplayBehaviour === "show"; + return ( } diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/tags/[tagId]/page.tsx index f6e02a65..b33a351a 100644 --- a/apps/web/app/dashboard/tags/[tagId]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagId]/page.tsx @@ -9,8 +9,12 @@ import { MoreHorizontal } from "lucide-react"; export default async function TagPage({ params, + searchParams, }: { params: { tagId: string }; + searchParams?: { + includeArchived?: string; + }; }) { let tag; try { @@ -23,6 +27,12 @@ export default async function TagPage({ } throw e; } + const userSettings = await api.users.settings(); + + const includeArchived = + searchParams?.includeArchived !== undefined + ? searchParams.includeArchived === "true" + : userSettings.archiveDisplayBehaviour === "show"; return ( } - query={{ tagId: tag.id }} + query={{ + tagId: tag.id, + archived: !includeArchived ? false : undefined, + }} showEditorCard={true} /> ); diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index beeecc2b..d5af9e35 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { NuqsAdapter } from "nuqs/adapters/next/app"; import "@karakeep/tailwind-config/globals.css"; @@ -55,15 +56,17 @@ export default async function RootLayout({ dir={isRTL ? "rtl" : "ltr"} > - - {children} - - - + + + {children} + + + + ); diff --git a/apps/web/components/dashboard/lists/ListOptions.tsx b/apps/web/components/dashboard/lists/ListOptions.tsx index d0dedfd5..7e020374 100644 --- a/apps/web/components/dashboard/lists/ListOptions.tsx +++ b/apps/web/components/dashboard/lists/ListOptions.tsx @@ -5,8 +5,17 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { useShowArchived } from "@/components/utils/useShowArchived"; import { useTranslation } from "@/lib/i18n/client"; -import { FolderInput, Pencil, Plus, Share, Trash2 } from "lucide-react"; +import { + FolderInput, + Pencil, + Plus, + Share, + Square, + SquareCheck, + Trash2, +} from "lucide-react"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; @@ -27,6 +36,7 @@ export function ListOptions({ children?: React.ReactNode; }) { const { t } = useTranslation(); + const { showArchived, onClickShowArchived } = useShowArchived(); const [deleteListDialogOpen, setDeleteListDialogOpen] = useState(false); const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false); @@ -93,6 +103,14 @@ export function ListOptions({ {t("lists.merge_list")} + + {showArchived ? ( + + ) : ( + + )} + {t("actions.toggle_show_archived")} + setDeleteListDialogOpen(true)} diff --git a/apps/web/components/dashboard/tags/TagOptions.tsx b/apps/web/components/dashboard/tags/TagOptions.tsx index 8d8cc9db..1419e6c3 100644 --- a/apps/web/components/dashboard/tags/TagOptions.tsx +++ b/apps/web/components/dashboard/tags/TagOptions.tsx @@ -7,8 +7,9 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { useShowArchived } from "@/components/utils/useShowArchived"; import { useTranslation } from "@/lib/i18n/client"; -import { Combine, Trash2 } from "lucide-react"; +import { Combine, Square, SquareCheck, Trash2 } from "lucide-react"; import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog"; import { MergeTagModal } from "./MergeTagModal"; @@ -21,6 +22,8 @@ export function TagOptions({ children?: React.ReactNode; }) { const { t } = useTranslation(); + const { showArchived, onClickShowArchived } = useShowArchived(); + const [deleteTagDialogOpen, setDeleteTagDialogOpen] = useState(false); const [mergeTagDialogOpen, setMergeTagDialogOpen] = useState(false); @@ -45,7 +48,14 @@ export function TagOptions({ {t("actions.merge")} - + + {showArchived ? ( + + ) : ( + + )} + {t("actions.toggle_show_archived")} + setDeleteTagDialogOpen(true)} diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx index c8aa5e86..3918ceed 100644 --- a/apps/web/components/settings/UserOptions.tsx +++ b/apps/web/components/settings/UserOptions.tsx @@ -73,12 +73,22 @@ export default function UserSettings() { ZUserSettings["bookmarkClickAction"], string > = { - open_original_link: t("settings.info.user_settings.open_external_url"), + open_original_link: t( + "settings.info.user_settings.bookmark_click_action.open_external_url", + ), expand_bookmark_preview: t( - "settings.info.user_settings.open_bookmark_details", + "settings.info.user_settings.bookmark_click_action.open_bookmark_details", ), }; + const archiveDisplayBehaviourTranslation: Record< + ZUserSettings["archiveDisplayBehaviour"], + string + > = { + show: t("settings.info.user_settings.archive_display_behaviour.show"), + hide: t("settings.info.user_settings.archive_display_behaviour.hide"), + }; + const form = useForm>({ resolver: zodResolver(zUserSettingsSchema), defaultValues: data, @@ -97,7 +107,7 @@ export default function UserSettings() { render={({ field }) => (
{ + mutate({ + archiveDisplayBehaviour: + value as ZUserSettings["archiveDisplayBehaviour"], + }); + }} + > + + + {archiveDisplayBehaviourTranslation[field.value]} + + + + {Object.entries(archiveDisplayBehaviourTranslation).map( + ([value, label]) => ( + + {label} + + ), + )} + + +
+ )} + /> ); } diff --git a/apps/web/components/utils/useShowArchived.tsx b/apps/web/components/utils/useShowArchived.tsx new file mode 100644 index 00000000..3fc66e91 --- /dev/null +++ b/apps/web/components/utils/useShowArchived.tsx @@ -0,0 +1,24 @@ +import { useCallback } from "react"; +import { useUserSettings } from "@/lib/userSettings"; +import { parseAsBoolean, useQueryState } from "nuqs"; + +export function useShowArchived() { + const userSettings = useUserSettings(); + const [showArchived, setShowArchived] = useQueryState( + "includeArchived", + parseAsBoolean + .withOptions({ + shallow: false, + }) + .withDefault(userSettings.archiveDisplayBehaviour === "show"), + ); + + const onClickShowArchived = useCallback(() => { + setShowArchived((prev) => !prev); + }, [setShowArchived]); + + return { + showArchived, + onClickShowArchived, + }; +} diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index d7ee54a3..39be43f3 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -51,6 +51,7 @@ "favorite": "Favorite", "unfavorite": "Unfavorite", "delete": "Delete", + "toggle_show_archived": "Show Archived", "refresh": "Refresh", "recrawl": "Recrawl", "download_full_page_archive": "Download Full Page Archive", @@ -101,9 +102,16 @@ "interface_lang": "Interface Language", "user_settings": { "user_settings_updated": "User settings have been updated!", - "boomark_click_action": "Bookmark Click Action", - "open_external_url": "Open Original URL", - "open_bookmark_details": "Open Bookmark Details" + "bookmark_click_action": { + "title": "Bookmark Click Action", + "open_external_url": "Open Original URL", + "open_bookmark_details": "Open Bookmark Details" + }, + "archive_display_behaviour": { + "title": "Archived Bookmarks", + "show": "Show archived bookmarks in tags and lists", + "hide": "Hide archived bookmarks in tags and lists" + } } }, "ai": { diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx index 727c823e..1590f727 100644 --- a/apps/web/lib/userSettings.tsx +++ b/apps/web/lib/userSettings.tsx @@ -8,6 +8,7 @@ import { api } from "./trpc"; export const UserSettingsContext = createContext({ bookmarkClickAction: "open_original_link", + archiveDisplayBehaviour: "show", }); export function UserSettingsContextProvider({ diff --git a/apps/web/package.json b/apps/web/package.json index 062565d8..7efb1830 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -71,6 +71,7 @@ "next-i18next": "^15.3.1", "next-pwa": "^5.6.0", "next-themes": "^0.3.0", + "nuqs": "^2.4.3", "prettier": "^3.4.2", "react": "^18.3.1", "react-day-picker": "8.10.1", -- cgit v1.2.3-70-g09d2