aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorxuatz <xzlow10@gmail.com>2025-06-02 04:01:26 +0900
committerGitHub <noreply@github.com>2025-06-01 20:01:26 +0100
commit3afe1e21df6dcc0483e74e0db02d9d82af32ecea (patch)
tree32208bf2de6fedae00aaeb4f7ca0d74a816b4764 /apps/web
parent8784c73c112b5b87bcf48670401e3956464ea436 (diff)
downloadkarakeep-3afe1e21df6dcc0483e74e0db02d9d82af32ecea.tar.zst
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 <me@mbassem.com>
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/dashboard/lists/[listId]/page.tsx15
-rw-r--r--apps/web/app/dashboard/tags/[tagId]/page.tsx15
-rw-r--r--apps/web/app/layout.tsx21
-rw-r--r--apps/web/components/dashboard/lists/ListOptions.tsx20
-rw-r--r--apps/web/components/dashboard/tags/TagOptions.tsx14
-rw-r--r--apps/web/components/settings/UserOptions.tsx52
-rw-r--r--apps/web/components/utils/useShowArchived.tsx24
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json14
-rw-r--r--apps/web/lib/userSettings.tsx1
-rw-r--r--apps/web/package.json1
10 files changed, 157 insertions, 20 deletions
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 (
<BookmarkListContextProvider list={list}>
<Bookmarks
- query={{ listId: list.id }}
+ query={{
+ listId: list.id,
+ archived: !includeArchived ? false : undefined,
+ }}
showDivider={true}
showEditorCard={list.type === "manual"}
header={<ListHeader initialData={list} />}
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 (
<Bookmarks
@@ -40,7 +50,10 @@ export default async function TagPage({
</TagOptions>
</div>
}
- 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"}
>
<body className={inter.className}>
- <Providers
- session={session}
- clientConfig={clientConfig}
- userLocalSettings={await getUserLocalSettings()}
- >
- {children}
- <ReactQueryDevtools initialIsOpen={false} />
- </Providers>
- <Toaster />
+ <NuqsAdapter>
+ <Providers
+ session={session}
+ clientConfig={clientConfig}
+ userLocalSettings={await getUserLocalSettings()}
+ >
+ {children}
+ <ReactQueryDevtools initialIsOpen={false} />
+ </Providers>
+ <Toaster />
+ </NuqsAdapter>
</body>
</html>
);
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({
<FolderInput className="size-4" />
<span>{t("lists.merge_list")}</span>
</DropdownMenuItem>
+ <DropdownMenuItem className="flex gap-2" onClick={onClickShowArchived}>
+ {showArchived ? (
+ <SquareCheck className="size-4" />
+ ) : (
+ <Square className="size-4" />
+ )}
+ <span>{t("actions.toggle_show_archived")}</span>
+ </DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => 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({
<Combine className="size-4" />
<span>{t("actions.merge")}</span>
</DropdownMenuItem>
-
+ <DropdownMenuItem className="flex gap-2" onClick={onClickShowArchived}>
+ {showArchived ? (
+ <SquareCheck className="size-4" />
+ ) : (
+ <Square className="size-4" />
+ )}
+ <span>{t("actions.toggle_show_archived")}</span>
+ </DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => 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<z.infer<typeof zUserSettingsSchema>>({
resolver: zodResolver(zUserSettingsSchema),
defaultValues: data,
@@ -97,7 +107,7 @@ export default function UserSettings() {
render={({ field }) => (
<div className="flex w-full flex-col gap-2">
<Label>
- {t("settings.info.user_settings.boomark_click_action")}
+ {t("settings.info.user_settings.bookmark_click_action.title")}
</Label>
<Select
disabled={!!clientConfig.demoMode}
@@ -127,6 +137,42 @@ export default function UserSettings() {
</div>
)}
/>
+ <FormField
+ control={form.control}
+ name="archiveDisplayBehaviour"
+ render={({ field }) => (
+ <div className="flex w-full flex-col gap-2">
+ <Label>
+ {t("settings.info.user_settings.archive_display_behaviour.title")}
+ </Label>
+ <Select
+ disabled={!!clientConfig.demoMode}
+ value={field.value}
+ onValueChange={(value) => {
+ mutate({
+ archiveDisplayBehaviour:
+ value as ZUserSettings["archiveDisplayBehaviour"],
+ });
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue>
+ {archiveDisplayBehaviourTranslation[field.value]}
+ </SelectValue>
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(archiveDisplayBehaviourTranslation).map(
+ ([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ),
+ )}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+ />
</Form>
);
}
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<ZUserSettings>({
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",