mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Implement folder update
This commit is contained in:
@@ -18,6 +18,7 @@ export class FolderCreateMutationHandler
|
||||
type: 'folder',
|
||||
parentId: input.parentId,
|
||||
name: input.name,
|
||||
avatar: input.avatar,
|
||||
collaborators: {},
|
||||
};
|
||||
|
||||
|
||||
29
apps/desktop/src/main/mutations/folder-update.ts
Normal file
29
apps/desktop/src/main/mutations/folder-update.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export const ChannelCreateDialog = ({
|
||||
type: 'channel_create',
|
||||
spaceId: spaceId,
|
||||
name: values.name,
|
||||
avatar: values.avatar,
|
||||
userId: workspace.userId,
|
||||
},
|
||||
onSuccess(output) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
109
apps/desktop/src/renderer/components/folders/folder-form.tsx
Normal file
109
apps/desktop/src/renderer/components/folders/folder-form.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,7 @@ export type FolderCreateMutationInput = {
|
||||
userId: string;
|
||||
parentId: string;
|
||||
name: string;
|
||||
avatar: string | null;
|
||||
generateIndex: boolean;
|
||||
};
|
||||
|
||||
|
||||
20
apps/desktop/src/shared/mutations/folder-update.ts
Normal file
20
apps/desktop/src/shared/mutations/folder-update.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user