Implement folder update

This commit is contained in:
Hakan Shehu
2024-11-19 16:11:00 +01:00
parent c3801688d7
commit 597bf2ff49
12 changed files with 361 additions and 90 deletions

View File

@@ -18,6 +18,7 @@ export class FolderCreateMutationHandler
type: 'folder',
parentId: input.parentId,
name: input.name,
avatar: input.avatar,
collaborators: {},
};

View File

@@ -0,0 +1,29 @@
import { MutationHandler } from '@/main/types';
import {
FolderUpdateMutationInput,
FolderUpdateMutationOutput,
} from '@/shared/mutations/folder-update';
import { nodeService } from '@/main/services/node-service';
export class FolderUpdateMutationHandler
implements MutationHandler<FolderUpdateMutationInput>
{
async handleMutation(
input: FolderUpdateMutationInput
): Promise<FolderUpdateMutationOutput> {
await nodeService.updateNode(input.folderId, input.userId, (attributes) => {
if (attributes.type !== 'folder') {
throw new Error('Node is not a folder');
}
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
});
return {
success: true,
};
}
}

View File

@@ -41,6 +41,8 @@ import { ViewUpdateMutationHandler } from '@/main/mutations/view-update';
import { ViewDeleteMutationHandler } from '@/main/mutations/view-delete';
import { ChannelUpdateMutationHandler } from '@/main/mutations/channel-update';
import { PageUpdateMutationHandler } from '@/main/mutations/page-update';
import { FolderUpdateMutationHandler } from '@/main/mutations/folder-update';
type MutationHandlerMap = {
[K in keyof MutationMap]: MutationHandler<MutationMap[K]['input']>;
};
@@ -87,4 +89,5 @@ export const mutationHandlerMap: MutationHandlerMap = {
view_delete: new ViewDeleteMutationHandler(),
channel_update: new ChannelUpdateMutationHandler(),
page_update: new PageUpdateMutationHandler(),
folder_update: new FolderUpdateMutationHandler(),
};

View File

@@ -56,6 +56,7 @@ export const ChannelCreateDialog = ({
type: 'channel_create',
spaceId: spaceId,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess(output) {

View File

@@ -3,29 +3,13 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/renderer/components/ui/dialog';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/renderer/components/ui/form';
import { Input } from '@/renderer/components/ui/input';
import { Button } from '@/renderer/components/ui/button';
import { Spinner } from '@/renderer/components/ui/spinner';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@/renderer/hooks/use-mutation';
const formSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters long.'),
});
import { IdType } from '@colanode/core';
import { generateId } from '@colanode/core';
import { FolderForm } from './folder-form';
interface FolderCreateDialogProps {
spaceId: string;
@@ -41,75 +25,48 @@ export const FolderCreateDialog = ({
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
},
});
const handleCancel = () => {
form.reset();
onOpenChange(false);
};
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
mutate({
input: {
type: 'folder_create',
parentId: spaceId,
name: values.name,
generateIndex: true,
userId: workspace.userId,
},
onSuccess(output) {
onOpenChange(false);
form.reset();
workspace.openInMain(output.id);
},
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create folder</DialogTitle>
<DialogDescription>
Create a new folder to organize your files
Create a new folder to organize your pages
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
className="flex flex-col"
onSubmit={form.handleSubmit(handleSubmit)}
>
<div className="flex-grow space-y-4 py-2 pb-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Name *</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isPending}>
{isPending && <Spinner className="mr-1" />}
Create
</Button>
</DialogFooter>
</form>
</Form>
<FolderForm
id={generateId(IdType.Folder)}
values={{
name: '',
avatar: null,
}}
isPending={isPending}
submitText="Create"
handleCancel={() => {
onOpenChange(false);
}}
handleSubmit={(values) => {
console.log('submit', values);
if (isPending) {
return;
}
mutate({
input: {
type: 'folder_create',
parentId: spaceId,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
generateIndex: true,
},
onSuccess(output) {
onOpenChange(false);
workspace.openInMain(output.id);
},
});
}}
/>
</DialogContent>
</Dialog>
);

View File

@@ -0,0 +1,109 @@
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/renderer/components/ui/form';
import { Button } from '@/renderer/components/ui/button';
import { Spinner } from '@/renderer/components/ui/spinner';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Input } from '@/renderer/components/ui/input';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { AvatarPopover } from '@/renderer/components/avatars/avatar-popover';
const formSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters long.'),
avatar: z.string().optional().nullable(),
});
interface FolderFormProps {
id: string;
values: z.infer<typeof formSchema>;
isPending: boolean;
submitText: string;
handleCancel: () => void;
handleSubmit: (values: z.infer<typeof formSchema>) => void;
readOnly?: boolean;
}
export const FolderForm = ({
id,
values,
isPending,
submitText,
handleCancel,
handleSubmit,
readOnly = false,
}: FolderFormProps) => {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: values,
});
const name = form.watch('name');
const avatar = form.watch('avatar');
return (
<Form {...form}>
<form
className="flex flex-col"
onSubmit={form.handleSubmit(handleSubmit)}
>
<div className="flex-grow flex flex-row items-end gap-2 py-2 pb-4">
{readOnly ? (
<Button type="button" variant="outline" size="icon">
<Avatar id={id} name={name} avatar={avatar} className="h-6 w-6" />
</Button>
) : (
<AvatarPopover
onPick={(avatar) => {
if (isPending) return;
if (avatar === values.avatar) return;
form.setValue('avatar', avatar);
}}
>
<Button type="button" variant="outline" size="icon">
<Avatar
id={id}
name={name}
avatar={avatar}
className="h-6 w-6"
/>
</Button>
</AvatarPopover>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input readOnly={readOnly} placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
disabled={isPending}
onClick={handleCancel}
>
Cancel
</Button>
<Button type="submit" disabled={isPending || readOnly}>
{isPending && <Spinner className="mr-1" />}
{submitText}
</Button>
</div>
</form>
</Form>
);
};

View File

@@ -30,7 +30,7 @@ export const FolderHeader = ({ nodes, folder, role }: FolderHeaderProps) => {
nodes={nodes}
role={role}
/>
<FolderSettings nodeId={folder.id} />
<FolderSettings folder={folder} role={role} />
</div>
</div>
</Header>

View File

@@ -3,24 +3,65 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/renderer/components/ui/dropdown-menu';
import { Copy, Settings, Trash2 } from 'lucide-react';
import { PageDeleteDialog } from '@/renderer/components/pages/page-delete-dialog';
import { Copy, Image, LetterText, Settings, Trash2 } from 'lucide-react';
import { FolderNode, hasEditorAccess, NodeRole } from '@colanode/core';
import { NodeCollaboratorAudit } from '@/renderer/components/collaborators/node-collaborator-audit';
import { FolderDeleteDialog } from '@/renderer/components/folders/folder-delete-dialog';
import { FolderUpdateDialog } from '@/renderer/components/folders/folder-update-dialog';
interface FolderSettingsProps {
nodeId: string;
folder: FolderNode;
role: NodeRole;
}
export const FolderSettings = ({ nodeId }: FolderSettingsProps) => {
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
export const FolderSettings = ({ folder, role }: FolderSettingsProps) => {
const [showUpdateDialog, setShowUpdateDialog] = React.useState(false);
const [showDeleteDialog, setShowDeleteModal] = React.useState(false);
const canEdit = hasEditorAccess(role);
const canDelete = hasEditorAccess(role);
return (
<React.Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-56">
<DropdownMenuContent side="bottom" className="mr-2 w-80">
<DropdownMenuLabel>{folder.attributes.name}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
if (!canEdit) {
return;
}
setShowUpdateDialog(true);
}}
disabled={!canEdit}
>
<LetterText className="size-4" />
Rename
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={!canEdit}
onClick={() => {
if (!canEdit) {
return;
}
setShowUpdateDialog(true);
}}
>
<Image className="size-4" />
Update icon
</DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2" disabled>
<Copy className="size-4" />
Duplicate
@@ -28,19 +69,50 @@ export const FolderSettings = ({ nodeId }: FolderSettingsProps) => {
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
if (!canDelete) {
return;
}
setShowDeleteModal(true);
}}
disabled={!canDelete}
>
<Trash2 className="size-4" />
Delete
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>Created by</DropdownMenuLabel>
<DropdownMenuItem>
<NodeCollaboratorAudit
collaboratorId={folder.createdBy}
date={folder.createdAt}
/>
</DropdownMenuItem>
{folder.updatedBy && (
<React.Fragment>
<DropdownMenuSeparator />
<DropdownMenuLabel>Last updated by</DropdownMenuLabel>
<DropdownMenuItem>
<NodeCollaboratorAudit
collaboratorId={folder.updatedBy}
date={folder.updatedAt}
/>
</DropdownMenuItem>
</React.Fragment>
)}
</DropdownMenuContent>
</DropdownMenu>
<PageDeleteDialog
nodeId={nodeId}
open={showDeleteModal}
<FolderDeleteDialog
nodeId={folder.id}
open={showDeleteDialog}
onOpenChange={setShowDeleteModal}
/>
<FolderUpdateDialog
folder={folder}
role={role}
open={showUpdateDialog}
onOpenChange={setShowUpdateDialog}
/>
</React.Fragment>
);
};

View File

@@ -0,0 +1,77 @@
import { useWorkspace } from '@/renderer/contexts/workspace';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/renderer/components/ui/dialog';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { FolderNode, hasEditorAccess, NodeRole } from '@colanode/core';
import { toast } from '@/renderer/hooks/use-toast';
import { FolderForm } from '@/renderer/components/folders/folder-form';
interface FolderUpdateDialogProps {
folder: FolderNode;
role: NodeRole;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const FolderUpdateDialog = ({
folder,
role,
open,
onOpenChange,
}: FolderUpdateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasEditorAccess(role);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Update folder</DialogTitle>
<DialogDescription>Update the folder name and icon</DialogDescription>
</DialogHeader>
<FolderForm
id={folder.id}
values={{
name: folder.attributes.name,
avatar: folder.attributes.avatar,
}}
isPending={isPending}
submitText="Update"
readOnly={!canEdit}
handleCancel={() => {
onOpenChange(false);
}}
handleSubmit={(values) => {
if (isPending) {
return;
}
mutate({
input: {
type: 'folder_update',
folderId: folder.id,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
toast({
title: 'Folder updated',
description: 'Folder was updated successfully',
variant: 'default',
});
},
});
}}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -18,6 +18,7 @@ export const FolderCommand: EditorCommand = {
const output = await window.colanode.executeMutation({
type: 'folder_create',
name: 'Untitled',
avatar: null,
userId,
parentId: documentId,
generateIndex: false,

View File

@@ -3,6 +3,7 @@ export type FolderCreateMutationInput = {
userId: string;
parentId: string;
name: string;
avatar: string | null;
generateIndex: boolean;
};

View File

@@ -0,0 +1,20 @@
export type FolderUpdateMutationInput = {
type: 'folder_update';
userId: string;
folderId: string;
name: string;
avatar: string | null;
};
export type FolderUpdateMutationOutput = {
success: boolean;
};
declare module '@/shared/mutations' {
interface MutationMap {
folder_update: {
input: FolderUpdateMutationInput;
output: FolderUpdateMutationOutput;
};
}
}