Compare commits

..

93 Commits
dev ... i18n

Author SHA1 Message Date
LinkwardenBot
e9e389d09b New translations common.json (Italian) 2026-02-25 03:20:39 +00:00
LinkwardenBot
00bfa73eec New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:39 +00:00
LinkwardenBot
43a69970c4 New translations common.json (English) 2026-02-25 03:20:39 +00:00
LinkwardenBot
9e7ba90a44 New translations common.json (Chinese Traditional) 2026-02-25 03:20:39 +00:00
LinkwardenBot
bf54ef0ff0 New translations common.json (Chinese Simplified) 2026-02-25 03:20:39 +00:00
LinkwardenBot
777129a622 New translations common.json (Ukrainian) 2026-02-25 03:20:39 +00:00
LinkwardenBot
f1cc320f3a New translations common.json (Turkish) 2026-02-25 03:20:39 +00:00
LinkwardenBot
a7a7b795ab New translations common.json (Russian) 2026-02-25 03:20:39 +00:00
LinkwardenBot
1fac208267 New translations common.json (Polish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
a1507dd28a New translations common.json (Dutch) 2026-02-25 03:20:38 +00:00
LinkwardenBot
002626f352 New translations common.json (Japanese) 2026-02-25 03:20:38 +00:00
LinkwardenBot
e708b1e431 New translations common.json (Italian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
9a24e36cb1 New translations common.json (German) 2026-02-25 03:20:38 +00:00
LinkwardenBot
fae0fc6690 New translations common.json (Spanish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
28204ab351 New translations common.json (French) 2026-02-25 03:20:38 +00:00
LinkwardenBot
5514272b64 New translations common.json (Romanian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
c1b4a8af1f New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
156ecc715c New translations common.json (English) 2026-02-25 03:20:38 +00:00
LinkwardenBot
3741e1eab4 New translations common.json (Chinese Traditional) 2026-02-25 03:20:38 +00:00
LinkwardenBot
032459b08b New translations common.json (Chinese Simplified) 2026-02-25 03:20:38 +00:00
LinkwardenBot
9c70814fb8 New translations common.json (Ukrainian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
1b38ffe24c New translations common.json (Turkish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
15385de8c5 New translations common.json (Russian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
27b676cb6f New translations common.json (Polish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
3457dafed2 New translations common.json (Dutch) 2026-02-25 03:20:38 +00:00
LinkwardenBot
a462bedaaa New translations common.json (Japanese) 2026-02-25 03:20:38 +00:00
LinkwardenBot
b5072c0a8a New translations common.json (Italian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
c1d089b97c New translations common.json (German) 2026-02-25 03:20:38 +00:00
LinkwardenBot
2bd7a7a253 New translations common.json (Spanish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
dd201f843a New translations common.json (French) 2026-02-25 03:20:38 +00:00
LinkwardenBot
f81b92aba2 New translations common.json (Romanian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
b641e6da37 New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
6ace6583d7 New translations common.json (English) 2026-02-25 03:20:38 +00:00
LinkwardenBot
3e988e1bf9 New translations common.json (Chinese Traditional) 2026-02-25 03:20:38 +00:00
LinkwardenBot
5b51b9e7bb New translations common.json (Chinese Simplified) 2026-02-25 03:20:38 +00:00
LinkwardenBot
ff5f56311e New translations common.json (Ukrainian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
3330bdca8f New translations common.json (Turkish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
dc27c08666 New translations common.json (Russian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
ab9d9eb69e New translations common.json (Polish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
299306ec61 New translations common.json (Dutch) 2026-02-25 03:20:38 +00:00
LinkwardenBot
f7c7c63827 New translations common.json (Japanese) 2026-02-25 03:20:38 +00:00
LinkwardenBot
185cd5b5ba New translations common.json (Italian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
b2325319ba New translations common.json (German) 2026-02-25 03:20:38 +00:00
LinkwardenBot
26b6fc6060 New translations common.json (Spanish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
269301f916 New translations common.json (French) 2026-02-25 03:20:38 +00:00
LinkwardenBot
864e58e186 New translations common.json (Romanian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
598e434ca8 New translations common.json (Ukrainian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
6958851d4f New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
d09cdf814e New translations common.json (English) 2026-02-25 03:20:38 +00:00
LinkwardenBot
4980e17d95 New translations common.json (Chinese Traditional) 2026-02-25 03:20:38 +00:00
LinkwardenBot
622d498e2d New translations common.json (Chinese Simplified) 2026-02-25 03:20:38 +00:00
LinkwardenBot
3be519d5dd New translations common.json (Ukrainian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
f115eb4649 New translations common.json (Turkish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
9489b99a61 New translations common.json (Russian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
44f7821f01 New translations common.json (Polish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
76aaa92a8a New translations common.json (Dutch) 2026-02-25 03:20:38 +00:00
LinkwardenBot
af5fbcaa4c New translations common.json (Japanese) 2026-02-25 03:20:38 +00:00
LinkwardenBot
b9d39e595b New translations common.json (Italian) 2026-02-25 03:20:38 +00:00
LinkwardenBot
cc01986186 New translations common.json (German) 2026-02-25 03:20:38 +00:00
LinkwardenBot
9801c4b6d3 New translations common.json (Spanish) 2026-02-25 03:20:38 +00:00
LinkwardenBot
53b31616cd New translations common.json (French) 2026-02-25 03:20:38 +00:00
LinkwardenBot
41b54c92c4 New translations common.json (Romanian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
1e6c405e02 New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
8e841bcd46 New translations common.json (English) 2026-02-25 03:20:37 +00:00
LinkwardenBot
a10fcfbb59 New translations common.json (Chinese Traditional) 2026-02-25 03:20:37 +00:00
LinkwardenBot
cdef808a8f New translations common.json (Chinese Simplified) 2026-02-25 03:20:37 +00:00
LinkwardenBot
201891ccdc New translations common.json (Ukrainian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
aee129a893 New translations common.json (Turkish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
dfb15f45bf New translations common.json (Russian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
55c100cb74 New translations common.json (Polish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
18c0d53f18 New translations common.json (Dutch) 2026-02-25 03:20:37 +00:00
LinkwardenBot
285958c0b7 New translations common.json (Japanese) 2026-02-25 03:20:37 +00:00
LinkwardenBot
981360dc6f New translations common.json (Italian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
45adcd128f New translations common.json (German) 2026-02-25 03:20:37 +00:00
LinkwardenBot
d39d0da642 New translations common.json (Spanish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
deeabb335f New translations common.json (French) 2026-02-25 03:20:37 +00:00
LinkwardenBot
ae6479c838 New translations common.json (Romanian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
fd2eaabe2c New translations common.json (French) 2026-02-25 03:20:37 +00:00
LinkwardenBot
6852e0d7d4 New translations common.json (Portuguese, Brazilian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
f28856ceb4 New translations common.json (English) 2026-02-25 03:20:37 +00:00
LinkwardenBot
1435a65694 New translations common.json (Chinese Traditional) 2026-02-25 03:20:37 +00:00
LinkwardenBot
adebd10963 New translations common.json (Chinese Simplified) 2026-02-25 03:20:37 +00:00
LinkwardenBot
e1d00bb7b5 New translations common.json (Ukrainian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
4d7780ae61 New translations common.json (Turkish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
ecc37bbdbe New translations common.json (Russian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
075985eb74 New translations common.json (Polish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
1f3e4738f9 New translations common.json (Dutch) 2026-02-25 03:20:37 +00:00
LinkwardenBot
abaf84702e New translations common.json (Japanese) 2026-02-25 03:20:37 +00:00
LinkwardenBot
4d450a9934 New translations common.json (Italian) 2026-02-25 03:20:37 +00:00
LinkwardenBot
f22cafd171 New translations common.json (German) 2026-02-25 03:20:37 +00:00
LinkwardenBot
10253f2cc3 New translations common.json (Spanish) 2026-02-25 03:20:37 +00:00
LinkwardenBot
419a7cdfaf New translations common.json (French) 2026-02-25 03:20:37 +00:00
LinkwardenBot
71a58f4394 New translations common.json (Romanian) 2026-02-25 03:20:37 +00:00
41 changed files with 716 additions and 844 deletions

View File

@@ -120,8 +120,8 @@ const RootComponent = ({
auth: MobileAuth;
}) => {
const { colorScheme } = useColorScheme();
const updateLink = useUpdateLink({ auth, Alert });
const deleteLink = useDeleteLink({ auth, Alert });
const updateLink = useUpdateLink(auth);
const deleteLink = useDeleteLink(auth);
const { tmp } = useTmpStore();
@@ -229,12 +229,12 @@ const RootComponent = ({
{tmp.link && tmp.user && (
<DropdownMenu.Item
key="pin-link"
onSelect={() => {
onSelect={async () => {
const isAlreadyPinned =
tmp.link?.pinnedBy && tmp.link.pinnedBy[0]
? true
: false;
updateLink.mutateAsync({
await updateLink.mutateAsync({
...(tmp.link as LinkIncludingShortenedCollectionAndTags),
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
@@ -282,15 +282,18 @@ const RootComponent = ({
{
text: "Delete",
style: "destructive",
onPress: async () => {
onPress: () => {
deleteLink.mutate(
tmp.link?.id as number
tmp.link?.id as number,
{
onSuccess: async () => {
await deleteLinkCache(
tmp.link?.id as number
);
},
}
);
await deleteLinkCache(
tmp.link?.id as number
);
// go back
router.back();
},
},

View File

@@ -21,7 +21,7 @@ export default function IncomingScreen() {
const { auth } = useAuthStore();
const router = useRouter();
const { data, updateData } = useDataStore();
const addLink = useAddLink({ auth });
const addLink = useAddLink(auth);
const { colorScheme } = useColorScheme();
const [showSuccess, setShowSuccess] = useState(false);
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();

View File

@@ -12,7 +12,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AddLinkSheet() {
const actionSheetRef = useRef<ActionSheetRef>(null);
const { auth } = useAuthStore();
const addLink = useAddLink({ auth, Alert });
const addLink = useAddLink(auth);
const [link, setLink] = useState("");
const { colorScheme } = useColorScheme();
@@ -43,12 +43,21 @@ export default function AddLinkSheet() {
/>
<Button
onPress={() => {
addLink.mutate({ url: link });
actionSheetRef.current?.hide();
setLink("");
}}
onPress={() =>
addLink.mutate(
{ url: link },
{
onSuccess: () => {
actionSheetRef.current?.hide();
setLink("");
},
onError: (error) => {
Alert.alert("Error", "There was an error adding the link.");
console.error("Error adding link:", error);
},
}
)
}
isLoading={addLink.isPending}
variant="accent"
className="mb-2"

View File

@@ -33,7 +33,7 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
const [link, setLink] = useState<
LinkIncludingShortenedCollectionAndTags | undefined
>(props.payload?.link);
const updateLink = useUpdateLink({ auth, Alert });
const editLink = useUpdateLink(auth);
const router = useSheetRouter("edit-link-sheet");
const { colorScheme } = useColorScheme();
@@ -124,15 +124,23 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
/>
<Button
onPress={() => {
updateLink.mutate(link as LinkIncludingShortenedCollectionAndTags);
if (link && tmp.link)
updateTmp({
link,
});
SheetManager.hide("edit-link-sheet");
}}
isLoading={updateLink.isPending}
onPress={() =>
editLink.mutate(link as LinkIncludingShortenedCollectionAndTags, {
onSuccess: () => {
if (link && tmp.link)
updateTmp({
link,
});
SheetManager.hide("edit-link-sheet");
},
onError: (error) => {
Alert.alert("Error", "There was an error editing the link.");
console.error("Error editing link:", error);
},
})
}
isLoading={editLink.isPending}
variant="accent"
className="mb-2"
>
@@ -154,7 +162,7 @@ const Main = (props: SheetProps<"edit-link-sheet">) => {
const Collections = () => {
const { auth } = useAuthStore();
const addLink = useAddLink({ auth });
const addLink = useAddLink(auth);
const [searchQuery, setSearchQuery] = useState("");
const router = useSheetRouter("edit-link-sheet");
const { link: currentLink } = useSheetRouteParams<
@@ -262,7 +270,7 @@ const Collections = () => {
const Tags = () => {
const { auth } = useAuthStore();
const addLink = useAddLink({ auth });
const addLink = useAddLink(auth);
const [searchQuery, setSearchQuery] = useState("");
const router = useSheetRouter("edit-link-sheet");
const params = useSheetRouteParams("edit-link-sheet", "tags");

View File

@@ -19,7 +19,7 @@ const CollectionListing = ({ collection }: Props) => {
const router = useRouter();
const { colorScheme } = useColorScheme();
const deleteCollection = useDeleteCollection({ auth, Alert });
const deleteCollection = useDeleteCollection(auth);
return (
<ContextMenu.Root>

View File

@@ -40,12 +40,12 @@ type Props = {
const LinkListing = ({ link, dashboard }: Props) => {
const { auth } = useAuthStore();
const router = useRouter();
const updateLink = useUpdateLink({ auth, Alert });
const updateLink = useUpdateLink(auth);
const { data: user } = useUser(auth);
const { colorScheme } = useColorScheme();
const { data } = useDataStore();
const deleteLink = useDeleteLink({ auth, Alert });
const deleteLink = useDeleteLink(auth);
const [url, setUrl] = useState("");
@@ -57,7 +57,7 @@ const LinkListing = ({ link, dashboard }: Props) => {
} catch (error) {
console.log(error);
}
}, [link.url]);
}, [link]);
return (
<ContextMenu.Root>
@@ -122,8 +122,8 @@ const LinkListing = ({ link, dashboard }: Props) => {
<View className="flex flex-row gap-1 items-center mt-1.5 pr-1.5 self-start rounded-md">
<Folder
size={16}
fill={link.collection.color || "#0ea5e9"}
color={link.collection.color || "#0ea5e9"}
fill={link.collection.color || ""}
color={link.collection.color || ""}
/>
<Text
numberOfLines={1}
@@ -215,11 +215,11 @@ const LinkListing = ({ link, dashboard }: Props) => {
<ContextMenu.Item
key="pin-link"
onSelect={() => {
onSelect={async () => {
const isAlreadyPinned =
link?.pinnedBy && link.pinnedBy[0] ? true : false;
updateLink.mutateAsync({
await updateLink.mutateAsync({
...link,
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
@@ -319,10 +319,12 @@ const LinkListing = ({ link, dashboard }: Props) => {
{
text: "Delete",
style: "destructive",
onPress: async () => {
deleteLink.mutate(link.id as number);
await deleteLinkCache(link.id as number);
onPress: () => {
deleteLink.mutate(link.id as number, {
onSuccess: async () => {
await deleteLinkCache(link.id as number);
},
});
},
},
]

View File

@@ -17,7 +17,7 @@ import {
import useOnScreen from "@/hooks/useOnScreen";
import { useCollections } from "@linkwarden/router/collections";
import { useUser } from "@linkwarden/router/user";
import { useGetLink } from "@linkwarden/router/links";
import { useGetLink, useLinks } from "@linkwarden/router/links";
import { useRouter } from "next/router";
import openLink from "@/lib/client/openLink";
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
@@ -82,6 +82,8 @@ export function Card({ link, editMode, dashboardType }: Props) {
settings: { show },
} = useLocalSettingsStore();
const { links } = useLinks();
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
@@ -100,7 +102,7 @@ export function Card({ link, editMode, dashboardType }: Props) {
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
}, [collections, link]);
}, [collections, links]);
const ref = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(ref);

View File

@@ -53,7 +53,7 @@ export default function DragNDrop({
onDragEnd: onDragEndProp,
}: DragNDropProps) {
const { t } = useTranslation();
const updateLink = useUpdateLink({ toast, t });
const updateLink = useUpdateLink();
const pinLink = usePinLink();
const { data: user } = useUser();
const queryClient = useQueryClient();
@@ -104,7 +104,25 @@ export default function DragNDrop({
updatedLink: LinkIncludingShortenedCollectionAndTags,
opts?: { invalidateDashboardOnError?: boolean }
) => {
updateLink.mutateAsync(updatedLink);
const load = toast.loading(t("updating"));
await updateLink.mutateAsync(updatedLink, {
onSettled: async (_, error) => {
toast.dismiss(load);
if (error) {
if (
opts?.invalidateDashboardOnError &&
typeof queryClient !== "undefined"
) {
await queryClient.invalidateQueries({
queryKey: ["dashboardData"],
});
}
toast.error(error.message);
} else {
toast.success(t("updated"));
}
},
});
};
// DROP ON TAG

View File

@@ -113,7 +113,7 @@ export default function LinkDetails({
);
};
const updateLink = useUpdateLink({ toast, t });
const updateLink = useUpdateLink();
const updateFile = useUpdateFile();
const submit = async (e?: any) => {
@@ -126,9 +126,21 @@ export default function LinkDetails({
return;
}
updateLink.mutateAsync(link);
const load = toast.loading(t("updating"));
setMode && setMode("view");
await updateLink.mutateAsync(link, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("updated"));
setMode && setMode("view");
setLink(data);
}
},
});
};
const setCollection = (e: any) => {

View File

@@ -54,7 +54,7 @@ export default function LinkActions({
const [refreshPreservationsModal, setRefreshPreservationsModal] =
useState(false);
const deleteLink = useDeleteLink({ toast, t });
const deleteLink = useDeleteLink();
const updateArchive = async () => {
const load = toast.loading(t("sending_request"));
@@ -131,7 +131,13 @@ export default function LinkActions({
onClick={async (e) => {
if (e.shiftKey) {
const load = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id as number);
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) toast.error(error.message);
else toast.success(t("deleted"));
},
});
} else {
setDeleteLinkModal(true);
}

View File

@@ -1,7 +1,7 @@
import { LinkIncludingShortenedCollectionAndTags } from "@linkwarden/types/global";
import Image from "next/image";
import isValidUrl from "@/lib/shared/isValidUrl";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import Icon from "@/components/Icon";
import { IconWeight } from "@phosphor-icons/react";
import clsx from "clsx";
@@ -30,10 +30,6 @@ function LinkIcon({
const [faviconLoaded, setFaviconLoaded] = useState(false);
useEffect(() => {
setFaviconLoaded(false);
}, [link.url]);
return (
<div onClick={() => onClick && onClick()}>
{link.icon ? (

View File

@@ -66,13 +66,7 @@ export default function MobileNavigation({}: Props) {
<MobileNavigationButton href={`/collections`} icon={"bi-folder"} />
</div>
</div>
{newLinkModal && (
<NewLinkModal
onClose={() => {
setNewLinkModal(false);
}}
/>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
{newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
)}

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput";
import { CollectionIncludingMembersAndLinkCount } from "@linkwarden/types/global";
import { useRouter } from "next/router";
import usePermissions from "@/hooks/usePermissions";
@@ -21,6 +22,7 @@ export default function DeleteCollectionModal({
const { t } = useTranslation();
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
const [submitLoader, setSubmitLoader] = useState(false);
const router = useRouter();
const permissions = usePermissions(collection.id as number);
@@ -28,15 +30,32 @@ export default function DeleteCollectionModal({
setCollection(activeCollection);
}, []);
const deleteCollection = useDeleteCollection({ toast, t });
const deleteCollection = useDeleteCollection();
const submit = async () => {
if (!collection) return null;
if (!submitLoader) {
setSubmitLoader(true);
if (!collection) return null;
deleteCollection.mutateAsync(collection.id as number);
setSubmitLoader(true);
onClose();
router.push("/collections");
const load = toast.loading(t("deleting_collection"));
deleteCollection.mutateAsync(collection.id as number, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
onClose();
toast.success(t("deleted"));
router.push("/collections");
}
},
});
}
};
return (

View File

@@ -18,7 +18,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const deleteLink = useDeleteLink({ toast, t });
const deleteLink = useDeleteLink();
const router = useRouter();
useEffect(() => {
@@ -26,15 +26,26 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
}, []);
const submit = async () => {
deleteLink.mutateAsync(link.id as number);
const load = toast.loading(t("deleting"));
if (
router.pathname.startsWith("/links/[id]") ||
router.pathname.startsWith("/preserved/[id]")
) {
router.push("/dashboard");
}
onClose();
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
if (
router.pathname.startsWith("/links/[id]") ||
router.pathname.startsWith("/preserved/[id]")
) {
router.push("/dashboard");
}
toast.success(t("deleted"));
onClose();
}
},
});
};
return (

View File

@@ -43,7 +43,7 @@ export default function LinkModal({
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const deleteLink = useDeleteLink({ toast, t });
const deleteLink = useDeleteLink();
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
@@ -51,8 +51,13 @@ export default function LinkModal({
setTimeout(() => (document.body.style.pointerEvents = ""), 0);
if (e.shiftKey && link.id) {
deleteLink.mutateAsync(link.id);
const loading = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id, {
onSettled: (data, error) => {
toast.dismiss(loading);
error ? toast.error(error.message) : toast.success(t("deleted"));
},
});
onClose();
} else {
onDelete();

View File

@@ -7,17 +7,14 @@ import { useRouter } from "next/router";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@linkwarden/router/collections";
import { useAddLink } from "@linkwarden/router/links";
import toast from "react-hot-toast";
import {
PostLinkSchema,
PostLinkSchemaType,
} from "@linkwarden/lib/schemaValidation";
import { PostLinkSchemaType } from "@linkwarden/lib/schemaValidation";
import { Button } from "@/components/ui/button";
import { Separator } from "../ui/separator";
import { useAddLink } from "@linkwarden/router/links";
type Props = {
onClose: () => void;
onClose: Function;
};
export default function NewLinkModal({ onClose }: Props) {
@@ -34,13 +31,10 @@ export default function NewLinkModal({ onClose }: Props) {
},
} as PostLinkSchemaType;
const addLink = useAddLink({
toast,
t,
});
const inputRef = useRef<HTMLInputElement>(null);
const [link, setLink] = useState<PostLinkSchemaType>(initial);
const addLink = useAddLink();
const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
const { data: collections = [] } = useCollections();
@@ -86,17 +80,22 @@ export default function NewLinkModal({ onClose }: Props) {
}, []);
const submit = async () => {
const dataValidation = PostLinkSchema.safeParse(link);
if (!dataValidation.success)
return toast.error(
`Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`
);
addLink.mutateAsync(link);
onClose();
if (!submitLoader) {
setSubmitLoader(true);
const load = toast.loading(t("creating_link"));
await addLink.mutateAsync(link, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
toast.error(t(error.message));
} else {
onClose();
toast.success(t("link_created"));
}
},
});
}
};
return (

View File

@@ -182,11 +182,7 @@ export default function Navbar({
</div>
)}
{newLinkModal && (
<NewLinkModal
onClose={() => {
setNewLinkModal(false);
}}
/>
<NewLinkModal onClose={() => setNewLinkModal(false)} />
)}
{newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />

View File

@@ -40,13 +40,7 @@ export default function NoLinksFound({ text }: Props) {
</span>
</Button>
</div>
{newLinkModal && (
<NewLinkModal
onClose={() => {
setNewLinkModal(false);
}}
/>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
</div>
);
}

View File

@@ -186,7 +186,7 @@ export default function SearchBar({ placeholder }: Props) {
</Button>
</div>
{/* {user?.hasUnIndexedLinks && !dismissSearchNote ? (
{user?.hasUnIndexedLinks && !dismissSearchNote ? (
<div
role="alert"
className="border border-neutral p-2 my-1 rounded flex flex-col gap-2"
@@ -204,7 +204,7 @@ export default function SearchBar({ placeholder }: Props) {
Dismiss
</Button>
</div>
) : undefined} */}
) : undefined}
</div>
</div>
)}

View File

@@ -6,21 +6,39 @@ import { useUser } from "@linkwarden/router/user";
const usePinLink = () => {
const { t } = useTranslation();
const updateLink = useUpdateLink({ toast, t });
const updateLink = useUpdateLink();
const { data: user } = useUser();
// Return a function that can be used to pin/unpin the link
const pinLink = async (link: LinkIncludingShortenedCollectionAndTags) => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false;
const load = toast.loading(t("updating"));
try {
updateLink.mutateAsync({
...link,
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
: [{ id: user?.id }]) as any,
});
await updateLink.mutateAsync(
{
...link,
pinnedBy: (isAlreadyPinned
? [{ id: undefined }]
: [{ id: user?.id }]) as any,
},
{
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(
isAlreadyPinned ? t("link_unpinned") : t("link_pinned")
);
}
},
}
);
} catch (e) {
toast.dismiss(load);
console.error(e);
}
};

View File

@@ -158,7 +158,7 @@ const Page: NextPageWithLayout = () => {
<ul className="px-5 list-disc">
<Trans
i18nKey="regenerate_broken_preserved_content_desc"
components={[<li key={0} />]}
components={[<li />]}
values={{
count: workerStats.data?.link.failed,
}}
@@ -181,7 +181,7 @@ const Page: NextPageWithLayout = () => {
<ul className="px-5 list-disc">
<Trans
i18nKey="delete_all_preserved_content_and_regenerate_desc"
components={[<li key={0} />]}
components={[<li />]}
/>
</ul>
<div role="alert" className="alert alert-warning mt-3">

View File

@@ -26,6 +26,7 @@ import ViewDropdown from "@/components/ViewDropdown";
import clsx from "clsx";
import Icon from "@/components/Icon";
import Droppable from "@/components/Droppable";
import { useUpdateLink } from "@linkwarden/router/links";
import { NextPageWithLayout } from "./_app";
const Page: NextPageWithLayout = () => {
@@ -94,6 +95,7 @@ const Page: NextPageWithLayout = () => {
const [showSurveyModal, setShowsSurveyModal] = useState(false);
const updateUser = useUpdateUser();
const updateLink = useUpdateLink();
const [submitLoader, setSubmitLoader] = useState(false);
@@ -190,13 +192,7 @@ const Page: NextPageWithLayout = () => {
}}
/>
)}
{newLinkModal && (
<NewLinkModal
onClose={() => {
setNewLinkModal(false);
}}
/>
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
</>
);
};

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Benutzerverwaltung",
"user_administration": "User Administration",
"search_users": "Suche nach Benutzern",
"no_users_found": "Keine Benutzer gefunden.",
"no_user_found_in_search": "Keine Benutzer mit den angegebenen Suchparametern gefunden.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Diese Sammlung wird öffentlich geteilt.",
"search_for_links": "Nach Links suchen",
"search_query_invalid_symbol": "Die Suchabfrage sollte nicht '%' enthalten.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Modal in einem neuen Tab öffnen",
"file": "Datei",
"tag_preservation_rule_label": "Regeln zur Erhaltung nach Tag (globale Einstellungen überschreiben):",
"ai_tagging": "AI-Tagging",
"worker": "Worker",
"regenerate_broken_preservations": "Alle defekten oder fehlenden Speicherungen für alle Benutzer neu erzeugen.",
"delete_all_preservations": "Löschen der Archivierung für alle Webseiten, gilt für alle Benutzer.",
"delete_all_preservations_and_regenerate": "Löschen und Neuarchivierung aller Webseiten, gilt für alle Benutzer.",
"delete_all_preservation_warning": "Diese Aktion löscht alle bestehenden Archivierungen auf dem Server.",
"no_broken_preservations": "Keine beschädigten Archivierungen.",
"links_are_being_represerved": "Links werden wiederhergestellt...",
"cancel": "Abbrechen",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Administración de usuarios",
"user_administration": "User Administration",
"search_users": "Buscar usuarios",
"no_users_found": "No se ha encontrado el usuario.",
"no_user_found_in_search": "No se ha encontrado el usuario con esos parámetros de búsqueda.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Esta colección está siendo compartida públicamente.",
"search_for_links": "Buscar Enlaces",
"search_query_invalid_symbol": "La consulta de búsqueda no debe contener '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Abrir este modal en una nueva pestaña",
"file": "Archivo",
"tag_preservation_rule_label": "Reglas de conservación por etiqueta (sobreescribe la configuración global):",
"ai_tagging": "Etiquetado con AI",
"worker": "Trabajador",
"regenerate_broken_preservations": "Regenerar todas las conservaciones dañadas o perdidas para todos los usuarios.",
"delete_all_preservations": "Eliminar la conservación para todas las páginas web, se aplica a todos los usuarios.",
"delete_all_preservations_and_regenerate": "Eliminar y reconservar todas las páginas web, se aplica a todos los usuarios.",
"delete_all_preservation_warning": "Esta acción eliminará todas las conservaciones existentes en el servidor.",
"no_broken_preservations": "No hay conservaciones rotas.",
"links_are_being_represerved": "Los enlaces están siendo reconservados...",
"cancel": "Cancelar",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expandir barra lateral",
"shrink_sidebar": "Contraer barra lateral",
"trial_left_plural": "La prueba termina en {{count}} días. Suscríbete.",
"trial_left_singular": "La prueba termina en 1 día. Suscríbete."
"trial_left_singular": "La prueba termina en 1 día. Suscríbete.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Administration des utilisateurs",
"user_administration": "User Administration",
"search_users": "Rechercher des utilisateurs",
"no_users_found": "Aucun utilisateur trouvé.",
"no_user_found_in_search": "Aucun utilisateur trouvé avec la requête de recherche donnée.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Cette collection est partagée publiquement.",
"search_for_links": "Rechercher des liens",
"search_query_invalid_symbol": "La recherche ne doit pas contenir '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Ouvrir cette modale dans un nouvel onglet",
"file": "Ficher",
"tag_preservation_rule_label": "Règles de préservation par étiquette (outrepasser les paramètres globaux) :",
"ai_tagging": "Étiquetage par IA",
"worker": "Worker",
"regenerate_broken_preservations": "Régénérer toutes les conservations cassées ou manquantes pour tous les utilisateurs.",
"delete_all_preservations": "Supprimer la conservation pour toutes les pages web, s'applique à tous les utilisateurs.",
"delete_all_preservations_and_regenerate": "Supprimer et conserver de nouveau toutes les pages web, s'applique à tous les utilisateurs.",
"delete_all_preservation_warning": "Cette action va supprimer toutes les conservations existantes sur le serveur.",
"no_broken_preservations": "Aucune conservation cassée.",
"links_are_being_represerved": "La conservation des liens est en cours de restauration...",
"cancel": "Annuler",
@@ -525,5 +533,22 @@
"expand_sidebar": "Agrandir le panneau latéral",
"shrink_sidebar": "Réduire le panneau latéral",
"trial_left_plural": "La période d'essai se termine dans {{count}} jours. Abonnez-vous.",
"trial_left_singular": "La période d'essai se termine dans 1 jour."
"trial_left_singular": "La période d'essai se termine dans 1 jour.",
"apply_members_roles_to_subcollections": "Appliquer les membres et les rôles aux sous-collections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Amministrazione Utenti",
"user_administration": "User Administration",
"search_users": "Cerca Utenti",
"no_users_found": "Nessun utente trovato.",
"no_user_found_in_search": "Nessun utente trovato con la query di ricerca specificata.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Questa collezione è condivisa pubblicamente.",
"search_for_links": "Cerca per Link",
"search_query_invalid_symbol": "The search query should not contain '%'.",
"search_operators": "Operatori di ricerca",
"search_operator_name": "Titolo",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collezione",
"search_operator_before": "Precedente",
"search_operator_after": "Successivo",
"search_operator_public": "Pubblico",
"search_operator_description": "Descrizione",
"search_operator_type": "Type",
"search_operator_pinned": "Fissato",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Open this modal in a new tab",
"file": "File",
"tag_preservation_rule_label": "Preservation rules by tag (override global settings):",
"ai_tagging": "AI Tagging",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerate all broken or missing preservations for all users.",
"delete_all_preservations": "Delete preservation for all webpages, applies to all users.",
"delete_all_preservations_and_regenerate": "Delete and re-preserve all webpages, applies to all users.",
"delete_all_preservation_warning": "This action will delete all existing preservations on the server.",
"no_broken_preservations": "No broken preservations.",
"links_are_being_represerved": "Links are being re-preserved...",
"cancel": "Annulla",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "ユーザー管理",
"user_administration": "User Administration",
"search_users": "ユーザーを検索",
"no_users_found": "ユーザーが見つかりません",
"no_user_found_in_search": "指定された検索クエリでユーザーが見つかりませんでした。",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "このコレクションは全体に公開されています。",
"search_for_links": "リンクを検索",
"search_query_invalid_symbol": "検索クエリに'%'を含めることはできません",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "新しいタブでこのモーダルを開く",
"file": "ファイル",
"tag_preservation_rule_label": "タグごとの保存ルール(グローバル設定を上書き):",
"ai_tagging": "AIタグ付け",
"worker": "ワーカー",
"regenerate_broken_preservations": "すべてのユーザーの壊れた保存または欠落している保存を再生成します。",
"delete_all_preservations": "すべての Web ページの保存を削除し、すべてのユーザーに適用されます。",
"delete_all_preservations_and_regenerate": "すべての Web ページを削除して再保存し、すべてのユーザーに適用されます。",
"delete_all_preservation_warning": "この操作は、サーバー上の既存のすべての保存を削除します。",
"no_broken_preservations": "壊れた保存はありません。",
"links_are_being_represerved": "リンクを再保存しています…",
"cancel": "キャンセル",
@@ -525,5 +533,22 @@
"expand_sidebar": "サイドバーを展開",
"shrink_sidebar": "サイドバーを縮小",
"trial_left_plural": "お試し期間は {{count}} 日で終了します。登録してください。",
"trial_left_singular": "試用期間はあと 1 日で終了します。登録してください。"
"trial_left_singular": "試用期間はあと 1 日で終了します。登録してください。",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Gebruikersbeheer",
"user_administration": "User Administration",
"search_users": "Zoeken naar Gebruikers",
"no_users_found": "Geen gebruikers gevonden.",
"no_user_found_in_search": "Geen gebruikers gevonden met de gegeven zoekopdracht.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "This collection is being shared publicly.",
"search_for_links": "Search for Links",
"search_query_invalid_symbol": "The search query should not contain '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Open this modal in a new tab",
"file": "Bestand",
"tag_preservation_rule_label": "Preservation rules by tag (override global settings):",
"ai_tagging": "AI Tagging",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerate all broken or missing preservations for all users.",
"delete_all_preservations": "Delete preservation for all webpages, applies to all users.",
"delete_all_preservations_and_regenerate": "Delete and re-preserve all webpages, applies to all users.",
"delete_all_preservation_warning": "This action will delete all existing preservations on the server.",
"no_broken_preservations": "No broken preservations.",
"links_are_being_represerved": "Links are being re-preserved...",
"cancel": "Cancel",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Zarządzanie użytkownikami",
"user_administration": "User Administration",
"search_users": "Wyszukaj użytkowników",
"no_users_found": "Nie znaleziono użytkowników.",
"no_user_found_in_search": "Nie znaleziono użytkowników z podanym zapytaniem.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "This collection is being shared publicly.",
"search_for_links": "Search for Links",
"search_query_invalid_symbol": "The search query should not contain '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Open this modal in a new tab",
"file": "Plik",
"tag_preservation_rule_label": "Preservation rules by tag (override global settings):",
"ai_tagging": "AI Tagging",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerate all broken or missing preservations for all users.",
"delete_all_preservations": "Delete preservation for all webpages, applies to all users.",
"delete_all_preservations_and_regenerate": "Delete and re-preserve all webpages, applies to all users.",
"delete_all_preservation_warning": "This action will delete all existing preservations on the server.",
"no_broken_preservations": "No broken preservations.",
"links_are_being_represerved": "Links are being re-preserved...",
"cancel": "Cancel",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Administração de Usuários",
"user_administration": "User Administration",
"search_users": "Pesquisar por Usuários",
"no_users_found": "Nenhum usuário encontrado.",
"no_user_found_in_search": "Nenhum usuário encontrado com a consulta de pesquisa fornecida.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Esta coleção está a ser compartilhada publicamente.",
"search_for_links": "Pesquisar por Links",
"search_query_invalid_symbol": "A consulta de pesquisa não deve conter '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Abrir este modal em uma nova aba",
"file": "Arquivo",
"tag_preservation_rule_label": "Regras de reserva por tag (sobrescrever configurações globais):",
"ai_tagging": "Marcação por AI",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerar todas as conservações danificadas ou faltando para todos os usuários.",
"delete_all_preservations": "Excluir preservação para todas as páginas da Web, aplica-se a todos os usuários.",
"delete_all_preservations_and_regenerate": "Excluir e re-preservar todas as páginas da Web, aplica-se a todos os usuários.",
"delete_all_preservation_warning": "Esta ação irá apagar todas as conservações existentes no servidor.",
"no_broken_preservations": "Nenhuma conservação quebrada.",
"links_are_being_represerved": "Links estão sendo preservados",
"cancel": "Cancelar",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expandir barra lateral",
"shrink_sidebar": "Encolher barra lateral",
"trial_left_plural": "O período de teste termina em {{count}} dias. Inscreva-se.",
"trial_left_singular": "O período de teste termina em 1 dia. Inscreva-se."
"trial_left_singular": "O período de teste termina em 1 dia. Inscreva-se.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Administrare Utilizatori",
"user_administration": "User Administration",
"search_users": "Caută utilizatori",
"no_users_found": "Nu s-a găsit niciun utilizator.",
"no_user_found_in_search": "Niciun utilizator găsit care să îndeplinească cerințele menționate.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Această colecție este partajată public.",
"search_for_links": "Caută linkuri",
"search_query_invalid_symbol": "Căutarea nu trebuie să conțină '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Deschide această fereastră într-o filă nouă",
"file": "Fișier",
"tag_preservation_rule_label": "Reguli de prezervare după etichetă (suprascrie setările globale):",
"ai_tagging": "Etichetare AI",
"worker": "Worker",
"regenerate_broken_preservations": "Regenerează toate prezervările stricate sau lipsite pentru toți utilizatorii.",
"delete_all_preservations": "Ștergeți prezervarea pentru toate paginile web, se aplică tuturor utilizatorilor.",
"delete_all_preservations_and_regenerate": "Ștergeți și re-prezervați toate paginile web, se aplică tuturor utilizatorilor.",
"delete_all_preservation_warning": "Această acțiune va șterge toate prezervările existente de pe server.",
"no_broken_preservations": "Nu există prezervări stricate.",
"links_are_being_represerved": "Linkurile sunt în curs de re-prezervare...",
"cancel": "Anulează",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Управление пользователями",
"user_administration": "User Administration",
"search_users": "Поиск пользователей",
"no_users_found": "Пользователи не найдены.",
"no_user_found_in_search": "По данному запросу пользователей не нашлось.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Это общая (доступная другим пользователям) коллекция.",
"search_for_links": "Найти ссылки",
"search_query_invalid_symbol": "Поисковый запрос не должен содержать '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Открыть это модальное окно в новой вкладке",
"file": "Файл",
"tag_preservation_rule_label": "Правила сохранения по тегу (переопределяет глобальные настройки):",
"ai_tagging": "ИИ-метки",
"worker": "Обслуживание",
"regenerate_broken_preservations": "Пересоздать все поврежденные или отсутствующие сохранения для всех пользователей.",
"delete_all_preservations": "Удалить сохранения для всех веб-страниц, действие применится ко всем пользователям.",
"delete_all_preservations_and_regenerate": "Удалить и сохранить заново все веб-страницы, действие применится ко всем пользователям.",
"delete_all_preservation_warning": "Это действие удалит все существующие сохранения на сервере.",
"no_broken_preservations": "Нет поврежденных сохранений.",
"links_are_being_represerved": "Ссылки сохраняются заново...",
"cancel": "Отменить",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Kullanıcı Yönetimi",
"user_administration": "User Administration",
"search_users": "Kullanıcı Ara",
"no_users_found": "Kullanıcı bulunamadı.",
"no_user_found_in_search": "Verilen arama sorgusuyla kullanıcı bulunamadı.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Bu koleksiyon herkese paylaşılacak.",
"search_for_links": "Linkleri Arama",
"search_query_invalid_symbol": "Aramada '%' işareti olamaz.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Bu sekmenin yeni pencereden açın",
"file": "Dosya",
"tag_preservation_rule_label": "Etiket bazında koruma kuralları (genel ayarları geçersiz kıl):",
"ai_tagging": "Yapay Zeka Etiketleri",
"worker": "Çalışan",
"regenerate_broken_preservations": "Tüm kullanıcılar için bozuk veya eksik kayıtları yeniden oluşturun.",
"delete_all_preservations": "Kullanıcıların kaydettiği tüm web sayfalarını silin.",
"delete_all_preservations_and_regenerate": "Tüm web sayfalarını silip tekrar kaydedin, bu tüm kullanıcılar için geçerli olacaktır.",
"delete_all_preservation_warning": "Bu işlem sunucudaki mevcut tüm kayıtları silecektir.",
"no_broken_preservations": "Bozuk kayıt dosyası bulunamadı.",
"links_are_being_represerved": "Bağlantıyı yeniden kaydediyor...",
"cancel": "İptal",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "Управління користувачами",
"user_administration": "User Administration",
"search_users": "Пошук користувачів",
"no_users_found": "Користувачів не знайдено.",
"no_user_found_in_search": "За вказаним пошуковим запитом не знайдено користувачів.",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "Цю колекцію поширено публічно.",
"search_for_links": "Шукати посилання",
"search_query_invalid_symbol": "Пошуковий запит не може містити '%'.",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "Відкрити спливне вікно у новій вкладці",
"file": "Файл",
"tag_preservation_rule_label": "Правила збереження за міткою (пріоритетніше загальних налаштувань):",
"ai_tagging": "Мітки ШІ",
"worker": "Працівник",
"regenerate_broken_preservations": "Повторно створити усі пошкоджені та відсутні збереження для усіх користувачів.",
"delete_all_preservations": "Видалити збереження усіх вебсторінок, застосовується до усіх користувачів.",
"delete_all_preservations_and_regenerate": "Видалити та зберегти усі вебсторінки заново, застосовується до усіх користувачів.",
"delete_all_preservation_warning": "Ця дія видалить усі наявні збереження на сервері.",
"no_broken_preservations": "Немає пошкоджених збережень.",
"links_are_being_represerved": "Посилання перезберігаються...",
"cancel": "Скасувати",
@@ -525,5 +533,22 @@
"expand_sidebar": "Розгорнути бічну панель",
"shrink_sidebar": "Згорнути бічну панель",
"trial_left_plural": "Лишилося {{count}} днів пробного доступу. Підпишіться.",
"trial_left_singular": "Лишився 1 день пробного доступу. Підпишіться."
"trial_left_singular": "Лишився 1 день пробного доступу. Підпишіться.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Назад на основну панель",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "使用者管理",
"user_administration": "User Administration",
"search_users": "搜尋使用者",
"no_users_found": "找不到使用者。",
"no_user_found_in_search": "在指定的搜尋條件下找不到使用者。",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "此收藏集已公開分享。",
"search_for_links": "搜尋連結",
"search_query_invalid_symbol": "搜尋查詢不應包含「%」符號。",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "在新分頁中開啟此對話框",
"file": "檔案",
"tag_preservation_rule_label": "依標籤設定保存規則(將覆寫全域設定):",
"ai_tagging": "AI 標記",
"worker": "工作程序",
"regenerate_broken_preservations": "為所有使用者重新產生所有損壞或遺失的保存內容。",
"delete_all_preservations": "刪除所有使用者的網頁保存內容。",
"delete_all_preservations_and_regenerate": "刪除並重新保存所有使用者的網頁內容。",
"delete_all_preservation_warning": "此動作將刪除伺服器上所有現有的保存內容。",
"no_broken_preservations": "目前沒有損壞的保存內容。",
"links_are_being_represerved": "連結正在重新保存中...",
"cancel": "取消",
@@ -525,5 +533,22 @@
"expand_sidebar": "Expand Sidebar",
"shrink_sidebar": "Shrink Sidebar",
"trial_left_plural": "Trial ends in {{count}} days. Subscribe.",
"trial_left_singular": "Trial ends in 1 day. Subscribe."
"trial_left_singular": "Trial ends in 1 day. Subscribe.",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -1,5 +1,5 @@
{
"user_administration": "用户管理",
"user_administration": "User Administration",
"search_users": "搜索用户",
"no_users_found": "未找到用户。",
"no_user_found_in_search": "在给定的搜索查询中未找到用户。",
@@ -463,15 +463,23 @@
"collection_publicly_shared": "此收藏集当前为公开分享状态。",
"search_for_links": "搜索链接",
"search_query_invalid_symbol": "搜索关键字不应该包含 '%'。",
"search_operators": "Search Operators",
"search_operator_name": "Title",
"search_operator_url": "URL",
"search_operator_tag": "Tag",
"search_operator_collection": "Collection",
"search_operator_before": "Before",
"search_operator_after": "After",
"search_operator_public": "Public",
"search_operator_description": "Description",
"search_operator_type": "Type",
"search_operator_pinned": "Pinned",
"search_operator_exclude": "Exclude",
"open_modal_new_tab": "在新标签页中打开",
"file": "文件",
"tag_preservation_rule_label": "基于标签的网页存档规则 (覆盖全局设置)",
"ai_tagging": "AI 标签",
"worker": "Worker",
"regenerate_broken_preservations": "为所有用户重新生成损坏或缺失的网页存档。",
"delete_all_preservations": "删除所有的网页存档,此操作将影响所有用户。",
"delete_all_preservations_and_regenerate": "删除所有用户的网页存档,并重新获取存档。",
"delete_all_preservation_warning": "此操作将删除服务器上的所有已存档内容。",
"no_broken_preservations": "未发现损坏的存档。",
"links_are_being_represerved": "链接正在被重新保存……",
"cancel": "取消",
@@ -525,5 +533,22 @@
"expand_sidebar": "展开侧边栏",
"shrink_sidebar": "收缩侧边栏",
"trial_left_plural": "试用期将于 {{count}} 天后结束,请订阅。",
"trial_left_singular": "试用期将于 1 天后结束,请订阅。"
"trial_left_singular": "试用期将于 1 天后结束,请订阅。",
"apply_members_roles_to_subcollections": "Apply members and roles to subcollections",
"apply_members_roles_to_subcollections_desc": "This will apply the members and their roles to all existing subcollections.",
"back_to_dashboard": "Back to Dashboard",
"background_jobs": "Background Jobs",
"background_jobs_desc": "Background jobs run 24/7 behind the scenes to process all queued links and tasks.",
"link_preservation_job": "Link Preservation Job",
"link_preservation_job_desc": "Crawls, preserves, and extracts text from your links.",
"search_indexing_job": "Search Indexing Job",
"search_indexing_job_desc": "Prepares links and documents for full-text search.",
"done": "Done",
"worker_pending_indexing_desc": "Pending items first need to be preserved before they can be indexed if they're a link.",
"regenerate_broken_links": "Regenerate Broken Links",
"regenerate_all_links": "Regenerate All Links",
"regenerate_broken_preserved_content_desc": "<0>Delete and re-preserve broken links with at least one missing preserved format.</0><0>This action will immediately delete any existing preserved format for {{count}} links.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"delete_all_preserved_content_and_regenerate_desc": "<0>This action will immediately delete all your existing preserved formats.</0><0>Affected links will be scheduled to have their preserved formats rebuilt.</0>",
"note": "Note",
"search_unindexed_links_in_bg_info": "Search may be less accurate because some links are still being processed."
}

View File

@@ -50,17 +50,14 @@ export default async function archiveHandler(
process.env.PERPLEXITY_API_KEY)
? true
: undefined,
indexVersion: null,
},
});
return;
}
const abortController = new AbortController();
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
setTimeout(() => {
abortController.abort();
reject(
new Error(
@@ -211,10 +208,6 @@ export default async function archiveHandler(
console.log("Reason:", err);
throw err;
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
const finalLink = await prisma.link.findUnique({
where: { id: link.id },
});
@@ -234,7 +227,6 @@ export default async function archiveHandler(
!finalLink.aiTagged
? true
: undefined,
indexVersion: null,
},
});
} else {

View File

@@ -25,10 +25,17 @@ export default async function handleMonolith(
const child = spawn("monolith", args, {
stdio: ["pipe", "pipe", "inherit"],
signal,
killSignal: "SIGKILL",
detached: true,
});
const abortListener = () => {
try {
if (child.pid) killProcess(child.pid);
} catch {}
reject(new Error("Monolith aborted"));
};
signal?.addEventListener("abort", abortListener);
child.stdin.write(htmlFromPage);
child.stdin.end();
@@ -36,11 +43,14 @@ export default async function handleMonolith(
child.stdout.on("data", (c) => chunks.push(c));
child.on("error", (err) => {
cleanup();
reject(err);
});
child.on("close", async (code) => {
if (code !== 0 && code !== null) {
cleanup();
if (code !== 0) {
return reject(new Error(`Monolith exited with code ${code}`));
}
@@ -54,21 +64,29 @@ export default async function handleMonolith(
return reject(new Error("Monolith output exceeded buffer limit"));
}
try {
await createFile({
data: html,
filePath: `archives/${link.collectionId}/${link.id}.html`,
});
await createFile({
data: html,
filePath: `archives/${link.collectionId}/${link.id}.html`,
});
await prisma.link.update({
where: { id: link.id },
data: { monolith: `archives/${link.collectionId}/${link.id}.html` },
});
await prisma.link.update({
where: { id: link.id },
data: { monolith: `archives/${link.collectionId}/${link.id}.html` },
});
resolve();
} catch (err) {
reject(err);
}
resolve();
});
function cleanup() {
signal?.removeEventListener("abort", abortListener);
}
});
}
const killProcess = async (PID: number) => {
if (process.platform === "win32") {
process.kill(PID, "SIGKILL");
} else {
process.kill(-PID, "SIGKILL");
}
};

View File

@@ -80,6 +80,14 @@ export async function startIndexing(interval = 10) {
{ indexVersion: null },
],
},
{
OR: [
{ lastPreserved: { not: null } },
{
url: null,
},
],
},
{
...(process.env.STRIPE_SECRET_KEY
? {
@@ -164,6 +172,7 @@ export async function startIndexing(interval = 10) {
{ indexVersion: { not: MEILI_INDEX_VERSION } },
{ indexVersion: null },
],
lastPreserved: { not: null },
},
});

View File

@@ -4,9 +4,6 @@ import {
MobileAuth,
} from "@linkwarden/types/global";
import { useSession } from "next-auth/react";
import type toaster from "react-hot-toast";
import { TFunction } from "next-i18next";
import type { Alert as Alert_ } from "react-native";
const useCollections = (auth?: MobileAuth) => {
let status: "loading" | "authenticated" | "unauthenticated";
@@ -112,17 +109,7 @@ const useUpdateCollection = () => {
});
};
const useDeleteCollection = ({
auth,
Alert,
toast,
t,
}: {
auth?: MobileAuth;
Alert?: typeof Alert_;
toast?: typeof toaster;
t?: TFunction;
}) => {
const useDeleteCollection = (auth?: MobileAuth) => {
const queryClient = useQueryClient();
return useMutation({
@@ -146,44 +133,6 @@ const useDeleteCollection = ({
return data.response;
},
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ["collections"] });
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
const previousCollections = queryClient.getQueryData(["collections"]);
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
queryClient.setQueryData(["collections"], (oldData: any) => {
return oldData?.filter((collection: any) => collection.id !== id);
});
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return oldData;
return {
...oldData,
links:
oldData.links?.filter((link: any) => link.collectionId !== id) ||
[],
numberOfPinnedLinks:
oldData.links?.filter(
(link: any) => link.collectionId !== id && link.isPinned
).length || 0,
};
});
return { previousCollections, previousDashboard };
},
onError: (error, _variables, context) => {
if (toast && t) toast.error(t(error.message));
else if (Alert)
Alert.alert("Error", "There was an error deleting the collection.");
if (!context) return;
queryClient.setQueryData(["collections"], context.previousCollections);
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
},
onSuccess: (data, id) => {
queryClient.setQueryData(["collections"], (oldData: any) => {
return oldData.filter((collection: any) => collection.id !== id);
@@ -204,9 +153,6 @@ const useDeleteCollection = ({
).length || 0,
};
});
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["links"] });
},
});
};

View File

@@ -3,7 +3,6 @@ import {
useQueryClient,
useMutation,
useQuery,
QueryKey,
} from "@tanstack/react-query";
import { useMemo } from "react";
import {
@@ -19,9 +18,6 @@ import {
} from "@linkwarden/lib/schemaValidation";
import getFormatFromContentType from "@linkwarden/lib/getFormatFromContentType";
import getLinkTypeFromFormat from "@linkwarden/lib/getLinkTypeFromFormat";
import type toaster from "react-hot-toast";
import { TFunction } from "next-i18next";
import type { Alert as Alert_ } from "react-native";
const useLinks = (params: LinkRequestQuery = {}, auth?: MobileAuth) => {
const sort =
@@ -115,254 +111,7 @@ const buildQueryString = (params: LinkRequestQuery) => {
.join("&");
};
const upsertLinkInList = (
links: LinkIncludingShortenedCollectionAndTags[] = [],
link: LinkIncludingShortenedCollectionAndTags,
optimisticId?: number
) => {
const existingIndex = links.findIndex(
(item) => item.id === optimisticId || item.id === link.id
);
if (existingIndex === -1) return [link, ...links];
const nextLinks = [...links];
nextLinks[existingIndex] = link;
return nextLinks;
};
const upsertLinkInInfiniteData = (
oldData: any,
link: LinkIncludingShortenedCollectionAndTags,
optimisticId?: number
) => {
if (!oldData?.pages?.length) return oldData;
let replaced = false;
const pages = oldData.pages.map((page: any) => {
const links = page?.links?.map((item: any) => {
if (item.id === optimisticId || item.id === link.id) {
replaced = true;
return link;
}
return item;
});
return { ...page, links };
});
if (!replaced) {
const firstPage = pages[0];
pages[0] = {
...firstPage,
links: upsertLinkInList(firstPage?.links ?? [], link, optimisticId),
};
}
return { ...oldData, pages };
};
const upsertLinkInDashboardData = (
oldData: any,
link: LinkIncludingShortenedCollectionAndTags,
optimisticId?: number
) => {
if (!oldData) return oldData;
const updatedLinks = upsertLinkInList(
oldData.links ?? [],
link,
optimisticId
).slice(0, 16);
const collectionLinks = { ...(oldData.collectionLinks ?? {}) };
const collectionId = link.collection?.id;
if (collectionId != null && collectionLinks[collectionId]) {
collectionLinks[collectionId] = upsertLinkInList(
collectionLinks[collectionId],
link,
optimisticId
).slice(0, 16);
}
return {
...oldData,
links: updatedLinks,
collectionLinks,
};
};
const removeLinkFromInfiniteData = (oldData: any, linkId: number) => {
if (!oldData?.pages?.length) return oldData;
return {
...oldData,
pages: oldData.pages.map((page: any) => ({
...page,
links: (page.links ?? []).filter((item: any) => item.id !== linkId),
})),
};
};
const removeLinkFromDashboardData = (oldData: any, linkId: number) => {
if (!oldData) return oldData;
const removedLink = (oldData.links ?? []).find(
(item: any) => item.id === linkId
);
const numberOfPinnedLinks = removedLink?.pinnedBy?.length
? Math.max(0, (oldData.numberOfPinnedLinks ?? 0) - 1)
: oldData.numberOfPinnedLinks;
const hasCollectionLinks = oldData.collectionLinks != null;
const collectionLinks = hasCollectionLinks
? { ...oldData.collectionLinks }
: oldData.collectionLinks;
if (hasCollectionLinks) {
for (const [collectionId, links] of Object.entries(collectionLinks)) {
collectionLinks[Number(collectionId)] = (links as any[]).filter(
(item: any) => item.id !== linkId
);
}
}
return {
...oldData,
links: (oldData.links ?? []).filter((item: any) => item.id !== linkId),
collectionLinks,
numberOfPinnedLinks,
};
};
const isLinkPinned = (link?: LinkIncludingShortenedCollectionAndTags) => {
return Boolean(link?.pinnedBy && link.pinnedBy.length > 0);
};
const findLinkInInfiniteData = (data: any, linkId: number) => {
if (!data?.pages?.length) return undefined;
for (const page of data.pages) {
const match = page?.links?.find((item: any) => item.id === linkId);
if (match) return match;
}
return undefined;
};
const findLinkInQueriesData = (
queries: [QueryKey, unknown][],
linkId: number
) => {
for (const [, data] of queries ?? []) {
const match = findLinkInInfiniteData(data as any, linkId);
if (match) return match;
}
return undefined;
};
const findLinkInDashboardData = (data: any, linkId: number) => {
const match = data?.links?.find((item: any) => item.id === linkId);
if (match) return match;
const collectionLinks = data?.collectionLinks;
if (!collectionLinks) return undefined;
for (const links of Object.values(collectionLinks)) {
const collectionMatch = (links as any[])?.find(
(item: any) => item.id === linkId
);
if (collectionMatch) return collectionMatch;
}
return undefined;
};
const replaceLinkInInfiniteData = (
oldData: any,
link: LinkIncludingShortenedCollectionAndTags
) => {
if (!oldData?.pages?.length) return oldData;
let updated = false;
const pages = oldData.pages.map((page: any) => {
const links = (page.links ?? []).map((item: any) => {
if (item.id === link.id) {
updated = true;
return link;
}
return item;
});
return { ...page, links };
});
if (!updated) return oldData;
return { ...oldData, pages };
};
const replaceLinkInDashboardData = (
oldData: any,
link: LinkIncludingShortenedCollectionAndTags
) => {
if (!oldData) return oldData;
let updated = false;
const links = (oldData.links ?? []).map((item: any) => {
if (item.id === link.id) {
updated = true;
return link;
}
return item;
});
let collectionLinks = oldData.collectionLinks;
if (oldData.collectionLinks != null) {
const nextCollectionLinks = { ...oldData.collectionLinks };
for (const [collectionId, linksForCollection] of Object.entries(
nextCollectionLinks
)) {
const linkList = linksForCollection as any[];
if (!Array.isArray(linkList)) continue;
if (!linkList.some((item) => item.id === link.id)) continue;
updated = true;
nextCollectionLinks[Number(collectionId)] = linkList.map((item) =>
item.id === link.id ? link : item
);
}
collectionLinks = nextCollectionLinks;
}
if (!updated) return oldData;
return {
...oldData,
links,
collectionLinks,
};
};
const applyPinnedDelta = (oldData: any, delta: number) => {
if (!oldData || !delta) return oldData;
return {
...oldData,
numberOfPinnedLinks: Math.max(
0,
(oldData.numberOfPinnedLinks ?? 0) + delta
),
};
};
const useAddLink = ({
auth,
Alert,
toast,
t,
}: {
auth?: MobileAuth;
Alert?: typeof Alert_;
toast?: typeof toaster;
t?: TFunction;
}) => {
const useAddLink = (auth?: MobileAuth) => {
const queryClient = useQueryClient();
return useMutation({
@@ -395,118 +144,20 @@ const useAddLink = ({
return data.response;
},
onMutate: async (link) => {
await queryClient.cancelQueries({ queryKey: ["links"] });
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
const previousLinks = queryClient.getQueriesData({
queryKey: ["links"],
onSuccess: (data: LinkIncludingShortenedCollectionAndTags[]) => {
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
{
links: [data, ...oldData?.pages?.[0]?.links],
nextCursor: oldData?.pages?.[0]?.nextCursor,
},
...oldData?.pages?.slice(1),
],
pageParams: oldData?.pageParams,
};
});
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
const collections =
(queryClient.getQueryData(["collections"]) as any[]) ?? [];
const tags = (queryClient.getQueryData(["tags"]) as any[]) ?? [];
const user = queryClient.getQueryData(["user"]) as any;
const collectionFromId =
link.collection?.id != null
? collections.find(
(collection) => collection.id === link.collection?.id
)
: undefined;
const collectionFromName =
!collectionFromId && link.collection?.name
? collections.find(
(collection) => collection.name === link.collection?.name
)
: undefined;
const resolvedCollection = collectionFromId ?? collectionFromName;
const tempId = -Date.now();
const tempCollectionId = tempId - 1;
const collectionId =
resolvedCollection?.id ?? link.collection?.id ?? tempCollectionId;
const collectionName =
resolvedCollection?.name ?? link.collection?.name ?? "Unorganized";
const resolvedTags =
link.tags?.map((tag, index) => {
if (tag.id != null) {
return (
tags.find((existing) => existing.id === tag.id) ?? {
id: tag.id,
name: tag.name,
}
);
}
const existingTag = tags.find(
(existing) => existing.name === tag.name
);
return (
existingTag ?? {
id: tempId - 2 - index,
name: tag.name,
}
);
}) ?? [];
const optimisticLink = {
id: tempId,
name: link.name?.trim() || link.url || "",
url: link.url || "",
description: link.description || "",
type: link.type || "url",
preview: "",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
collectionId,
collection:
resolvedCollection ??
({
id: collectionId,
name: collectionName,
ownerId: user?.id ?? 0,
} as any),
tags: resolvedTags,
} as LinkIncludingShortenedCollectionAndTags;
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
upsertLinkInInfiniteData(oldData, optimisticLink, tempId)
);
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
upsertLinkInDashboardData(oldData, optimisticLink, tempId)
);
return {
previousLinks,
previousDashboard,
optimisticId: tempId,
};
},
onError: (error, _variables, context) => {
if (toast && t) toast.error(t(error.message));
else if (Alert)
Alert.alert("Error", "There was an error adding the link.");
if (!context) return;
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
});
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
},
onSuccess: (
data: LinkIncludingShortenedCollectionAndTags,
_link,
context
) => {
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
upsertLinkInInfiniteData(oldData, data, context?.optimisticId)
);
queryClient.invalidateQueries({ queryKey: ["dashboardData"] });
queryClient.invalidateQueries({ queryKey: ["collections"] });
@@ -516,17 +167,7 @@ const useAddLink = ({
});
};
const useUpdateLink = ({
auth,
Alert,
toast,
t,
}: {
auth?: MobileAuth;
Alert?: typeof Alert_;
toast?: typeof toaster;
t?: TFunction;
}) => {
const useUpdateLink = (auth?: MobileAuth) => {
const queryClient = useQueryClient();
return useMutation({
@@ -551,117 +192,6 @@ const useUpdateLink = ({
return data.response;
},
onMutate: async (link) => {
const linkId = link.id;
if (linkId == null) {
return {};
}
await queryClient.cancelQueries({ queryKey: ["links"] });
await queryClient.cancelQueries({ queryKey: ["publicLinks"] });
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
const previousLinks = queryClient.getQueriesData({
queryKey: ["links"],
});
const previousPublicLinks = queryClient.getQueriesData({
queryKey: ["publicLinks"],
});
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
const previousLinkQueries = queryClient.getQueriesData({
queryKey: ["link", linkId],
});
const cachedLink =
findLinkInQueriesData(previousLinks, linkId) ??
findLinkInDashboardData(previousDashboard, linkId);
const collections =
(queryClient.getQueryData(["collections"]) as any[]) ?? [];
const nextCollectionId =
link.collection?.id ?? cachedLink?.collection?.id;
const resolvedCollection =
nextCollectionId != null
? collections.find((collection) => collection.id === nextCollectionId)
: undefined;
const optimisticCollection =
resolvedCollection ??
(link.collection?.id &&
cachedLink?.collection?.id === link.collection.id
? { ...cachedLink.collection, ...link.collection }
: link.collection ?? cachedLink?.collection);
const previousPinned = isLinkPinned(cachedLink);
const nextPinned =
typeof link.pinnedBy === "undefined"
? previousPinned
: isLinkPinned(link);
const pinnedDelta =
nextPinned === previousPinned ? 0 : nextPinned ? 1 : -1;
const optimisticLink = {
...(cachedLink ?? {}),
...link,
collection: optimisticCollection,
collectionId:
optimisticCollection?.id ??
link.collection?.id ??
cachedLink?.collectionId,
updatedAt: new Date().toISOString(),
} as LinkIncludingShortenedCollectionAndTags;
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
replaceLinkInInfiniteData(oldData, optimisticLink)
);
queryClient.setQueriesData(
{ queryKey: ["publicLinks"] },
(oldData: any) => replaceLinkInInfiniteData(oldData, optimisticLink)
);
queryClient.setQueriesData(
{ queryKey: ["link", linkId] },
() => optimisticLink
);
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
applyPinnedDelta(
replaceLinkInDashboardData(oldData, optimisticLink),
pinnedDelta
)
);
return {
previousLinks,
previousPublicLinks,
previousDashboard,
previousLinkQueries,
};
},
onError: (error, _variables, context) => {
if (toast && t) toast.error(t(error.message));
else if (Alert)
Alert.alert("Error", "There was an error updating the link.");
if (!context) return;
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
});
context.previousPublicLinks?.forEach(
([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
}
);
context.previousLinkQueries?.forEach(
([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
}
);
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["links"] });
queryClient.invalidateQueries({ queryKey: ["link", data.id] });
@@ -673,17 +203,7 @@ const useUpdateLink = ({
});
};
const useDeleteLink = ({
auth,
Alert,
toast,
t,
}: {
auth?: MobileAuth;
Alert?: typeof Alert_;
toast?: typeof toaster;
t?: TFunction;
}) => {
const useDeleteLink = (auth?: MobileAuth) => {
const queryClient = useQueryClient();
return useMutation({
@@ -706,57 +226,6 @@ const useDeleteLink = ({
return data.response;
},
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ["links"] });
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
await queryClient.cancelQueries({ queryKey: ["publicLinks"] });
const previousLinks = queryClient.getQueriesData({
queryKey: ["links"],
});
const previousPublicLinks = queryClient.getQueriesData({
queryKey: ["publicLinks"],
});
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
removeLinkFromInfiniteData(oldData, id)
);
queryClient.setQueriesData(
{ queryKey: ["publicLinks"] },
(oldData: any) => removeLinkFromInfiniteData(oldData, id)
);
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
removeLinkFromDashboardData(oldData, id)
);
return {
previousLinks,
previousPublicLinks,
previousDashboard,
};
},
onError: (error, _variables, context) => {
if (toast && t) toast.error(t(error.message));
else if (Alert)
Alert.alert("Error", "There was an error deleting the link.");
if (!context) return;
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
});
context.previousPublicLinks?.forEach(
([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
}
);
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
},
onSuccess: (data) => {
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => {
if (!oldData?.pages?.[0]) return undefined;