Merge branch 'dev' into tags-in-public-collection

This commit is contained in:
Daniel
2024-11-02 17:56:43 -04:00
committed by GitHub
73 changed files with 2271 additions and 417 deletions
@@ -46,6 +46,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
},
{
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -58,8 +59,6 @@ export default function BulkEditLinksModal({ onClose }: Props) {
},
}
);
setSubmitLoader(false);
}
};
@@ -44,6 +44,7 @@ export default function DeleteCollectionModal({
deleteCollection.mutateAsync(collection.id as number, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -55,8 +56,6 @@ export default function DeleteCollectionModal({
}
},
});
setSubmitLoader(false);
}
};
+20 -10
View File
@@ -3,6 +3,7 @@ import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { useDeleteUser } from "@/hooks/store/admin/users";
import { useState } from "react";
import { useSession } from "next-auth/react";
type Props = {
onClose: Function;
@@ -23,31 +24,40 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
onSuccess: () => {
onClose();
},
onSettled: (data, error) => {
setSubmitLoader(false);
},
});
setSubmitLoader(false);
}
};
const { data } = useSession();
const isAdmin = data?.user?.id === Number(process.env.NEXT_PUBLIC_ADMIN);
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">{t("delete_user")}</p>
<p className="text-xl font-thin text-red-500">
{isAdmin ? t("delete_user") : t("remove_user")}
</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-3">
<p>{t("confirm_user_deletion")}</p>
<p>{t("confirm_user_removal_desc")}</p>
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>{t("warning")}:</b> {t("irreversible_action_warning")}
</span>
</div>
{isAdmin && (
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>{t("warning")}:</b> {t("irreversible_action_warning")}
</span>
</div>
)}
<Button className="ml-auto" intent="destructive" onClick={submit}>
<i className="bi-trash text-xl" />
{t("delete_confirmation")}
{isAdmin ? t("delete_confirmation") : t("confirm")}
</Button>
</div>
</Modal>
@@ -35,6 +35,7 @@ export default function EditCollectionModal({
await updateCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -45,8 +46,6 @@ export default function EditCollectionModal({
}
},
});
setSubmitLoader(false);
}
};
@@ -45,6 +45,7 @@ export default function EditCollectionSharingModal({
await updateCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -55,8 +56,6 @@ export default function EditCollectionSharingModal({
}
},
});
setSubmitLoader(false);
}
};
@@ -67,7 +66,7 @@ export default function EditCollectionSharingModal({
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
const [memberUsername, setMemberUsername] = useState("");
const [memberIdentifier, setMemberIdentifier] = useState("");
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
@@ -92,7 +91,7 @@ export default function EditCollectionSharingModal({
members: [...collection.members, newMember],
});
setMemberUsername("");
setMemberIdentifier("");
};
return (
@@ -174,15 +173,15 @@ export default function EditCollectionSharingModal({
<div className="flex items-center gap-2">
<TextInput
value={memberUsername || ""}
value={memberIdentifier || ""}
className="bg-base-200"
placeholder={t("members_username_placeholder")}
onChange={(e) => setMemberUsername(e.target.value)}
placeholder={t("add_member_placeholder")}
onChange={(e) => setMemberIdentifier(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" &&
addMemberToCollection(
user.username as string,
memberUsername || "",
user,
memberIdentifier.replace(/^@/, "") || "",
collection,
setMemberState,
t
@@ -193,8 +192,8 @@ export default function EditCollectionSharingModal({
<div
onClick={() =>
addMemberToCollection(
user.username as string,
memberUsername || "",
user,
memberIdentifier.replace(/^@/, "") || "",
collection,
setMemberState,
t
+125
View File
@@ -0,0 +1,125 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import TextInput from "../TextInput";
import { FormEvent, useState } from "react";
import { useTranslation, Trans } from "next-i18next";
import { useAddUser } from "@/hooks/store/admin/users";
import Link from "next/link";
import { signIn } from "next-auth/react";
type Props = {
onClose: Function;
};
type FormData = {
username?: string;
email?: string;
invite: boolean;
};
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
export default function InviteModal({ onClose }: Props) {
const { t } = useTranslation();
const addUser = useAddUser();
const [form, setForm] = useState<FormData>({
username: emailEnabled ? undefined : "",
email: emailEnabled ? "" : undefined,
invite: true,
});
const [submitLoader, setSubmitLoader] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!submitLoader) {
const checkFields = () => {
if (emailEnabled) {
return form.email !== "";
} else {
return form.username !== "";
}
};
if (checkFields()) {
setSubmitLoader(true);
await addUser.mutateAsync(form, {
onSettled: () => {
setSubmitLoader(false);
},
onSuccess: async () => {
await signIn("invite", {
email: form.email,
callbackUrl: "/member-onboarding",
redirect: false,
});
onClose();
},
});
} else {
toast.error(t("fill_all_fields_error"));
}
}
}
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("invite_user")}</p>
<div className="divider mb-3 mt-1"></div>
<p className="mb-3">{t("invite_user_desc")}</p>
<form onSubmit={submit}>
{emailEnabled ? (
<div>
<TextInput
placeholder={t("placeholder_email")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, email: e.target.value })}
value={form.email}
/>
</div>
) : (
<div>
<p className="mb-2">
{t("username")}{" "}
{emailEnabled && (
<span className="text-xs text-neutral">{t("optional")}</span>
)}
</p>
<TextInput
placeholder={t("placeholder_john")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, username: e.target.value })}
value={form.username}
/>
</div>
)}
<div role="note" className="alert alert-note mt-5">
<i className="bi-exclamation-triangle text-xl" />
<span>
<p className="mb-1">{t("invite_user_note")}</p>
<Link
href=""
className="font-semibold whitespace-nowrap hover:opacity-80 duration-100"
target="_blank"
>
{t("learn_more")} <i className="bi-box-arrow-up-right"></i>
</Link>
</span>
</div>
<div className="flex justify-between items-center mt-5">
<button
className="btn btn-accent dark:border-violet-400 text-white ml-auto"
type="submit"
>
{t("send_invitation")}
</button>
</div>
</form>
</Modal>
);
}
@@ -43,6 +43,7 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
await createCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -53,8 +54,6 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
}
},
});
setSubmitLoader(false);
};
return (
+1 -2
View File
@@ -80,6 +80,7 @@ export default function NewLinkModal({ onClose }: Props) {
await addLink.mutateAsync(link, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -90,8 +91,6 @@ export default function NewLinkModal({ onClose }: Props) {
}
},
});
setSubmitLoader(false);
}
};
+1 -2
View File
@@ -34,6 +34,7 @@ export default function NewTokenModal({ onClose }: Props) {
await addToken.mutateAsync(token, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -43,8 +44,6 @@ export default function NewTokenModal({ onClose }: Props) {
}
},
});
setSubmitLoader(false);
}
};
+6 -2
View File
@@ -35,6 +35,9 @@ export default function NewUserModal({ onClose }: Props) {
event.preventDefault();
if (!submitLoader) {
if (form.password.length < 8)
return toast.error(t("password_length_error"));
const checkFields = () => {
if (emailEnabled) {
return form.name !== "" && form.email !== "" && form.password !== "";
@@ -52,9 +55,10 @@ export default function NewUserModal({ onClose }: Props) {
onSuccess: () => {
onClose();
},
onSettled: () => {
setSubmitLoader(false);
},
});
setSubmitLoader(false);
} else {
toast.error(t("fill_all_fields_error"));
}
+1 -2
View File
@@ -115,6 +115,7 @@ export default function UploadFileModal({ onClose }: Props) {
{ link, file },
{
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -126,8 +127,6 @@ export default function UploadFileModal({ onClose }: Props) {
},
}
);
setSubmitLoader(false);
}
};
+15
View File
@@ -6,6 +6,8 @@ import { signOut } from "next-auth/react";
import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
export default function ProfileDropdown() {
const { t } = useTranslation();
const { settings, updateSettings } = useLocalSettingsStore();
@@ -73,6 +75,19 @@ export default function ProfileDropdown() {
</Link>
</li>
)}
{!user.parentSubscriptionId && stripeEnabled && (
<li>
<Link
href="/settings/billing"
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
tabIndex={0}
role="button"
className="whitespace-nowrap"
>
{t("invite_users")}
</Link>
</li>
)}
<li>
<div
onClick={() => {
+1 -1
View File
@@ -5,7 +5,7 @@ type Props = {
src?: string;
className?: string;
priority?: boolean;
name?: string;
name?: string | null;
large?: boolean;
};
+4 -1
View File
@@ -2,11 +2,14 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
export default function SettingsSidebar({ className }: { className?: string }) {
const { t } = useTranslation();
const LINKWARDEN_VERSION = process.env.version;
const { data: user } = useUser();
const router = useRouter();
const [active, setActive] = useState("");
@@ -73,7 +76,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
</div>
</Link>
{process.env.NEXT_PUBLIC_STRIPE && (
{process.env.NEXT_PUBLIC_STRIPE && !user.parentSubscriptionId && (
<Link href="/settings/billing">
<div
className={`${
+12
View File
@@ -0,0 +1,12 @@
import clsx from "clsx";
import React from "react";
type Props = {
className?: string;
};
function Divider({ className }: Props) {
return <hr className={clsx("border-neutral-content border-t", className)} />;
}
export default Divider;