mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Allow users to delete custom servers (#48)
This commit is contained in:
@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { LoginOutput } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -24,7 +23,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
interface EmailLoginProps {
|
||||
server: Server;
|
||||
server: string;
|
||||
onSuccess: (output: LoginOutput) => void;
|
||||
onForgotPassword: () => void;
|
||||
}
|
||||
@@ -49,7 +48,7 @@ export const EmailLogin = ({
|
||||
type: 'email.login',
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
server: server.domain,
|
||||
server,
|
||||
},
|
||||
onSuccess(output) {
|
||||
onSuccess(output);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -39,7 +38,7 @@ const formSchema = z
|
||||
});
|
||||
|
||||
interface EmailPasswordResetCompleteProps {
|
||||
server: Server;
|
||||
server: string;
|
||||
id: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
@@ -73,7 +72,7 @@ export const EmailPasswordResetComplete = ({
|
||||
type: 'email.password.reset.complete',
|
||||
otp: values.otp,
|
||||
password: values.password,
|
||||
server: server.domain,
|
||||
server,
|
||||
id: id,
|
||||
},
|
||||
onSuccess() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { EmailPasswordResetInitOutput } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -23,7 +22,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
interface EmailPasswordResetInitProps {
|
||||
server: Server;
|
||||
server: string;
|
||||
onSuccess: (output: EmailPasswordResetInitOutput) => void;
|
||||
}
|
||||
|
||||
@@ -44,7 +43,7 @@ export const EmailPasswordResetInit = ({
|
||||
input: {
|
||||
type: 'email.password.reset.init',
|
||||
email: values.email,
|
||||
server: server.domain,
|
||||
server,
|
||||
},
|
||||
onSuccess(output) {
|
||||
onSuccess(output);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { LoginOutput } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -39,7 +38,7 @@ const formSchema = z
|
||||
});
|
||||
|
||||
interface EmailRegisterProps {
|
||||
server: Server;
|
||||
server: string;
|
||||
onSuccess: (output: LoginOutput) => void;
|
||||
}
|
||||
|
||||
@@ -62,7 +61,7 @@ export const EmailRegister = ({ server, onSuccess }: EmailRegisterProps) => {
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
server: server.domain,
|
||||
server,
|
||||
},
|
||||
onSuccess(output) {
|
||||
onSuccess(output);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { LoginOutput } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
@@ -24,7 +23,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
interface EmailVerifyProps {
|
||||
server: Server;
|
||||
server: string;
|
||||
id: string;
|
||||
expiresAt: Date;
|
||||
onSuccess: (output: LoginOutput) => void;
|
||||
@@ -56,7 +55,7 @@ export const EmailVerify = ({
|
||||
input: {
|
||||
type: 'email.verify',
|
||||
otp: values.otp,
|
||||
server: server.domain,
|
||||
server,
|
||||
id,
|
||||
},
|
||||
onSuccess(output) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, Fragment } from 'react';
|
||||
import { useState, Fragment, useEffect } from 'react';
|
||||
|
||||
import { Account, Server } from '@colanode/client/types';
|
||||
import { EmailLogin } from '@colanode/ui/components/accounts/email-login';
|
||||
@@ -48,11 +48,21 @@ type PanelState =
|
||||
|
||||
export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
const app = useApp();
|
||||
const [server, setServer] = useState<Server>(servers[0]!);
|
||||
const [server, setServer] = useState<string | null>(
|
||||
servers[0]?.domain ?? null
|
||||
);
|
||||
const [panel, setPanel] = useState<PanelState>({
|
||||
type: 'login',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const serverExists =
|
||||
server !== null && servers.some((s) => s.domain === server);
|
||||
if (!serverExists && servers.length > 0) {
|
||||
setServer(servers[0]!.domain);
|
||||
}
|
||||
}, [server, servers]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ServerDropdown
|
||||
@@ -61,7 +71,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
servers={servers}
|
||||
readonly={panel.type === 'verify'}
|
||||
/>
|
||||
{panel.type === 'login' && (
|
||||
{server && panel.type === 'login' && (
|
||||
<Fragment>
|
||||
<EmailLogin
|
||||
server={server}
|
||||
@@ -94,7 +104,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
</p>
|
||||
</Fragment>
|
||||
)}
|
||||
{panel.type === 'register' && (
|
||||
{server && panel.type === 'register' && (
|
||||
<Fragment>
|
||||
<EmailRegister
|
||||
server={server}
|
||||
@@ -123,7 +133,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{panel.type === 'verify' && (
|
||||
{server && panel.type === 'verify' && (
|
||||
<Fragment>
|
||||
<EmailVerify
|
||||
server={server}
|
||||
@@ -148,7 +158,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{panel.type === 'password_reset_init' && (
|
||||
{server && panel.type === 'password_reset_init' && (
|
||||
<Fragment>
|
||||
<EmailPasswordResetInit
|
||||
server={server}
|
||||
@@ -173,7 +183,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{panel.type === 'password_reset_complete' && (
|
||||
{server && panel.type === 'password_reset_complete' && (
|
||||
<Fragment>
|
||||
<EmailPasswordResetComplete
|
||||
server={server}
|
||||
|
||||
@@ -65,6 +65,7 @@ export const ServerCreateDialog = ({
|
||||
},
|
||||
onSuccess(output) {
|
||||
onCreate(output.server);
|
||||
toast.success('Server added successfully');
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
|
||||
72
packages/ui/src/components/servers/server-delete-dialog.tsx
Normal file
72
packages/ui/src/components/servers/server-delete-dialog.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@colanode/ui/components/ui/alert-dialog';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
interface ServerDeleteDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export const ServerDeleteDialog = ({
|
||||
domain,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ServerDeleteDialogProps) => {
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want delete the server{' '}
|
||||
<span className="font-bold">"{domain}"</span>?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Deleting the server will remove all accounts connected to it. You
|
||||
can re-add it later.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'server.delete',
|
||||
domain,
|
||||
},
|
||||
onSuccess() {
|
||||
onOpenChange(false);
|
||||
toast.success(
|
||||
'Server and all associated accounts have been deleted'
|
||||
);
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, PlusIcon, ServerOffIcon, TrashIcon } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
import { Server } from '@colanode/client/types';
|
||||
import { isColanodeServer } from '@colanode/core';
|
||||
import { ServerAvatar } from '@colanode/ui/components/servers/server-avatar';
|
||||
import { ServerCreateDialog } from '@colanode/ui/components/servers/server-create-dialog';
|
||||
import { ServerDeleteDialog } from '@colanode/ui/components/servers/server-delete-dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,8 +15,8 @@ import {
|
||||
} from '@colanode/ui/components/ui/dropdown-menu';
|
||||
|
||||
interface ServerDropdownProps {
|
||||
value: Server;
|
||||
onChange: (server: Server) => void;
|
||||
value: string | null;
|
||||
onChange: (server: string) => void;
|
||||
servers: Server[];
|
||||
readonly?: boolean;
|
||||
}
|
||||
@@ -27,6 +29,9 @@ export const ServerDropdown = ({
|
||||
}: ServerDropdownProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openCreate, setOpenCreate] = useState(false);
|
||||
const [deleteDomain, setDeleteDomain] = useState<string | null>(null);
|
||||
|
||||
const server = servers.find((server) => server.domain === value);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -40,40 +45,73 @@ export const ServerDropdown = ({
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex w-full flex-grow flex-row items-center gap-3 rounded-md border border-input p-2 cursor-pointer hover:bg-gray-100">
|
||||
<ServerAvatar
|
||||
url={value.avatar}
|
||||
name={value.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{value.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{value.domain}</p>
|
||||
</div>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-96">
|
||||
{servers.map((server) => (
|
||||
<DropdownMenuItem
|
||||
key={server.domain}
|
||||
onSelect={() => {
|
||||
if (value.domain !== server.domain) {
|
||||
onChange(server);
|
||||
}
|
||||
}}
|
||||
className="flex w-full flex-grow flex-row items-center gap-3 rounded-md border-b border-input p-2 cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
{server ? (
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{server.domain}</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
) : (
|
||||
<ServerOffIcon className="size-8 text-muted-foreground rounded-md" />
|
||||
)}
|
||||
<div className="flex-grow">
|
||||
{server ? (
|
||||
<Fragment>
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.domain}
|
||||
</p>
|
||||
</Fragment>
|
||||
) : (
|
||||
<p className="flex-grow text-muted-foreground">
|
||||
Select a server
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-96">
|
||||
{servers.map((server) => {
|
||||
const canDelete = !isColanodeServer(server.domain);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={server.domain}
|
||||
onSelect={() => {
|
||||
if (value !== server.domain) {
|
||||
onChange(server.domain);
|
||||
}
|
||||
}}
|
||||
className="group/server flex w-full flex-grow flex-row items-center gap-3 rounded-md border-b border-input p-2 cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex flex-grow items-center gap-3">
|
||||
<ServerAvatar
|
||||
url={server.avatar}
|
||||
name={server.name}
|
||||
className="size-8 rounded-md"
|
||||
/>
|
||||
<div className="flex-grow">
|
||||
<p className="flex-grow font-semibold">{server.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{server.domain}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{canDelete && (
|
||||
<button
|
||||
className="text-muted-foreground opacity-0 group-hover/server:opacity-100 hover:bg-gray-200 size-8 flex items-center justify-center rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDeleteDomain(server.domain);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
@@ -81,6 +119,7 @@ export const ServerDropdown = ({
|
||||
}}
|
||||
className="py-2"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Add new server
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -88,9 +127,19 @@ export const ServerDropdown = ({
|
||||
{openCreate && (
|
||||
<ServerCreateDialog
|
||||
onCancel={() => setOpenCreate(false)}
|
||||
onCreate={(server) => {
|
||||
onCreate={() => {
|
||||
setOpenCreate(false);
|
||||
onChange(server);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteDomain && (
|
||||
<ServerDeleteDialog
|
||||
domain={deleteDomain}
|
||||
open={!!deleteDomain}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleteDomain(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user