aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorkamtschatka <sschatka@gmail.com>2024-05-19 13:13:54 +0200
committerGitHub <noreply@github.com>2024-05-19 12:13:54 +0100
commitd1ad84be48bb3b6914c0d478d13f92861889c466 (patch)
tree2f3f4f748b85b101d8f5b56473cf2d36944b1a1d /apps/web
parentcb62db781235fbc4b1fe92a3f8b26dadb5181963 (diff)
downloadkarakeep-d1ad84be48bb3b6914c0d478d13f92861889c466.tar.zst
feature(web): Allow adding multiple URLs at once #158 (#167)
Added a reusable dialog opening a dialog that allows you to decide if you want to import multiple URLs at once if you provide only that Co-authored-by: kamtschatka <simon.schatka@gmx.at>
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx79
-rw-r--r--apps/web/components/ui/multiple-choice-dialog.tsx55
2 files changed, 129 insertions, 5 deletions
diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
index 7c036c04..8425f669 100644
--- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
@@ -1,9 +1,10 @@
import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form";
-import { useEffect, useImperativeHandle, useRef } from "react";
+import React, { useEffect, useImperativeHandle, useRef } from "react";
import Link from "next/link";
import { ActionButton } from "@/components/ui/action-button";
import { Form, FormControl, FormItem } from "@/components/ui/form";
import InfoTooltip from "@/components/ui/info-tooltip";
+import MultipleChoiceDialog from "@/components/ui/multiple-choice-dialog";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
@@ -34,9 +35,17 @@ function useFocusOnKeyPress(inputRef: React.RefObject<HTMLTextAreaElement>) {
}, [inputRef]);
}
+interface MultiUrlImportState {
+ urls: URL[];
+ text: string;
+}
+
export default function EditorCard({ className }: { className?: string }) {
const inputRef = useRef<HTMLTextAreaElement>(null);
+ const [multiUrlImportState, setMultiUrlImportState] =
+ React.useState<MultiUrlImportState | null>(null);
+
const demoMode = !!useClientConfig().demoMode;
const formSchema = z.object({
text: z.string(),
@@ -76,14 +85,31 @@ export default function EditorCard({ className }: { className?: string }) {
},
});
- const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {
- const text = data.text.trim();
- try {
- const url = new URL(text);
+ function tryToImportUrls(text: string): void {
+ const lines = text.split("\n");
+ const urls: URL[] = [];
+ for (const line of lines) {
+ // parsing can also throw an exception, but will be caught outside
+ const url = new URL(line);
if (url.protocol != "http:" && url.protocol != "https:") {
throw new Error("Invalid URL");
}
+ urls.push(url);
+ }
+
+ if (urls.length === 1) {
+ // Only 1 url in the textfield --> simply import it
mutate({ type: "link", url: text });
+ return;
+ }
+ // multiple urls found --> ask the user if it should be imported as multiple URLs or as a text bookmark
+ setMultiUrlImportState({ urls, text });
+ }
+
+ const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {
+ const text = data.text.trim();
+ try {
+ tryToImportUrls(text);
} catch (e) {
// Not a URL
mutate({ type: "text", text });
@@ -150,6 +176,49 @@ export default function EditorCard({ className }: { className?: string }) {
: "Press ⌘ + Enter to Save"
: "Save"}
</ActionButton>
+
+ {multiUrlImportState && (
+ <MultipleChoiceDialog
+ open={true}
+ title={`Import URLs as separate Bookmarks?`}
+ description={`The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?`}
+ onOpenChange={(open) => {
+ if (!open) {
+ setMultiUrlImportState(null);
+ }
+ }}
+ actionButtons={[
+ () => (
+ <ActionButton
+ type="button"
+ variant="secondary"
+ loading={isPending}
+ onClick={() => {
+ mutate({ type: "text", text: multiUrlImportState.text });
+ setMultiUrlImportState(null);
+ }}
+ >
+ Import as Text Bookmark
+ </ActionButton>
+ ),
+ () => (
+ <ActionButton
+ type="button"
+ variant="destructive"
+ loading={isPending}
+ onClick={() => {
+ multiUrlImportState.urls.forEach((url) =>
+ mutate({ type: "link", url: url.toString() }),
+ );
+ setMultiUrlImportState(null);
+ }}
+ >
+ Import as separate Bookmarks
+ </ActionButton>
+ ),
+ ]}
+ ></MultipleChoiceDialog>
+ )}
</form>
</Form>
);
diff --git a/apps/web/components/ui/multiple-choice-dialog.tsx b/apps/web/components/ui/multiple-choice-dialog.tsx
new file mode 100644
index 00000000..b6f2a5c3
--- /dev/null
+++ b/apps/web/components/ui/multiple-choice-dialog.tsx
@@ -0,0 +1,55 @@
+import { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+
+export default function MultipleChoiceDialog({
+ open: userIsOpen,
+ setOpen: userSetOpen,
+ onOpenChange,
+ title,
+ description,
+ actionButtons,
+ children,
+}: {
+ open?: boolean;
+ setOpen?: (v: boolean) => void;
+ onOpenChange?: (open: boolean) => void;
+ title: React.ReactNode;
+ description: React.ReactNode;
+ actionButtons: ((
+ setDialogOpen: (open: boolean) => void,
+ ) => React.ReactNode)[];
+ children?: React.ReactNode;
+}) {
+ const [customIsOpen, setCustomIsOpen] = useState(false);
+ const [isDialogOpen, setDialogOpen] = [
+ userIsOpen ?? customIsOpen,
+ userSetOpen ?? setCustomIsOpen,
+ ];
+ return (
+ <Dialog
+ open={isDialogOpen}
+ onOpenChange={(isOpen) => {
+ onOpenChange?.(isOpen);
+ setDialogOpen(isOpen);
+ }}
+ >
+ {children && <DialogTrigger asChild>{children}</DialogTrigger>}
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ </DialogHeader>
+ {description}
+ <DialogFooter className="sm:justify-end">
+ {actionButtons.map((actionButton) => actionButton(setDialogOpen))}
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}