Use tanstackdb for view create & delete

This commit is contained in:
Hakan Shehu
2025-11-20 22:31:31 -08:00
parent c42a0c4abc
commit e06bd40e72
13 changed files with 75 additions and 317 deletions

View File

@@ -1,63 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { mapNode } from '@colanode/client/lib/mappers';
import { MutationHandler } from '@colanode/client/lib/types';
import {
ViewCreateMutationInput,
ViewCreateMutationOutput,
} from '@colanode/client/mutations/databases/view-create';
import {
generateId,
generateFractionalIndex,
IdType,
DatabaseViewAttributes,
} from '@colanode/core';
export class ViewCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewCreateMutationInput>
{
async handleMutation(
input: ViewCreateMutationInput
): Promise<ViewCreateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const id = generateId(IdType.DatabaseView);
const otherViews = await workspace.database
.selectFrom('nodes')
.selectAll()
.where('parent_id', '=', input.databaseId)
.where('type', '=', 'database_view')
.execute();
let maxIndex: string | null = null;
for (const otherView of otherViews) {
const view = mapNode(otherView);
if (view.type !== 'database_view') {
continue;
}
const index = view.index;
if (maxIndex === null || index > maxIndex) {
maxIndex = index;
}
}
const attributes: DatabaseViewAttributes = {
type: 'database_view',
name: input.name,
index: generateFractionalIndex(maxIndex, null),
layout: input.viewType,
parentId: input.databaseId,
};
await workspace.nodes.createNode({
id: id,
attributes: attributes,
parentId: input.databaseId,
});
return {
id: id,
};
}
}

View File

@@ -1,22 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import {
ViewDeleteMutationInput,
ViewDeleteMutationOutput,
} from '@colanode/client/mutations/databases/view-delete';
export class ViewDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewDeleteMutationInput>
{
async handleMutation(
input: ViewDeleteMutationInput
): Promise<ViewDeleteMutationOutput> {
const workspace = this.getWorkspace(input.userId);
await workspace.nodes.deleteNode(input.viewId);
return {
id: input.viewId,
};
}
}

View File

@@ -1,45 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import { MutationErrorCode, MutationError } from '@colanode/client/mutations';
import {
ViewNameUpdateMutationInput,
ViewNameUpdateMutationOutput,
} from '@colanode/client/mutations/databases/view-name-update';
import { DatabaseViewAttributes } from '@colanode/core';
export class ViewNameUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewNameUpdateMutationInput>
{
async handleMutation(
input: ViewNameUpdateMutationInput
): Promise<ViewNameUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseViewAttributes>(
input.viewId,
(attributes) => {
attributes.name = input.name;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.ViewUpdateForbidden,
"You don't have permission to update this view."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.ViewUpdateFailed,
'Something went wrong while updating the view.'
);
}
return {
id: input.viewId,
};
}
}

View File

@@ -24,9 +24,6 @@ import { FieldNameUpdateMutationHandler } from './databases/field-name-update';
import { SelectOptionCreateMutationHandler } from './databases/select-option-create';
import { SelectOptionDeleteMutationHandler } from './databases/select-option-delete';
import { SelectOptionUpdateMutationHandler } from './databases/select-option-update';
import { ViewCreateMutationHandler } from './databases/view-create';
import { ViewDeleteMutationHandler } from './databases/view-delete';
import { ViewNameUpdateMutationHandler } from './databases/view-name-update';
import { ViewUpdateMutationHandler } from './databases/view-update';
import { DocumentUpdateMutationHandler } from './documents/document-update';
import { FileCreateMutationHandler } from './files/file-create';
@@ -66,7 +63,6 @@ export const buildMutationHandlerMap = (
'email.register': new EmailRegisterMutationHandler(app),
'email.verify': new EmailVerifyMutationHandler(app),
'google.login': new GoogleLoginMutationHandler(app),
'view.create': new ViewCreateMutationHandler(app),
'node.delete': new NodeDeleteMutationHandler(app),
'node.create': new NodeCreateMutationHandler(app),
'node.update': new NodeUpdateMutationHandler(app),
@@ -102,8 +98,6 @@ export const buildMutationHandlerMap = (
'space.child.reorder': new SpaceChildReorderMutationHandler(app),
'account.update': new AccountUpdateMutationHandler(app),
'view.update': new ViewUpdateMutationHandler(app),
'view.delete': new ViewDeleteMutationHandler(app),
'view.name.update': new ViewNameUpdateMutationHandler(app),
'document.update': new DocumentUpdateMutationHandler(app),
'metadata.update': new MetadataUpdateMutationHandler(app),
'metadata.delete': new MetadataDeleteMutationHandler(app),

View File

@@ -1,20 +0,0 @@
export type ViewCreateMutationInput = {
type: 'view.create';
userId: string;
databaseId: string;
viewType: 'table' | 'board' | 'calendar';
name: string;
};
export type ViewCreateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'view.create': {
input: ViewCreateMutationInput;
output: ViewCreateMutationOutput;
};
}
}

View File

@@ -1,18 +0,0 @@
export type ViewDeleteMutationInput = {
type: 'view.delete';
userId: string;
viewId: string;
};
export type ViewDeleteMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'view.delete': {
input: ViewDeleteMutationInput;
output: ViewDeleteMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type ViewNameUpdateMutationInput = {
type: 'view.name.update';
userId: string;
databaseId: string;
viewId: string;
name: string;
};
export type ViewNameUpdateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'view.name.update': {
input: ViewNameUpdateMutationInput;
output: ViewNameUpdateMutationOutput;
};
}
}

View File

@@ -16,9 +16,6 @@ export * from './databases/field-name-update';
export * from './databases/select-option-create';
export * from './databases/select-option-delete';
export * from './databases/select-option-update';
export * from './databases/view-create';
export * from './databases/view-delete';
export * from './databases/view-name-update';
export * from './databases/view-update';
export * from './databases/database-name-field-update';
export * from './documents/document-update';

View File

@@ -4,9 +4,9 @@ import { Fragment, useState } from 'react';
import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewDeleteDialog } from '@colanode/ui/components/databases/view-delete-dialog';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button';
import {
Popover,
@@ -179,7 +179,9 @@ export const BoardViewSettings = () => {
/>
)}
{openDelete && (
<ViewDeleteDialog
<NodeDeleteDialog
title="Are you sure you want delete this view?"
description="This action cannot be undone. This view will no longer be accessible and all data in the view will be lost."
id={view.id}
open={openDelete}
onOpenChange={setOpenDelete}

View File

@@ -4,9 +4,9 @@ import { Fragment, useState } from 'react';
import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewDeleteDialog } from '@colanode/ui/components/databases/view-delete-dialog';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button';
import {
Popover,
@@ -179,7 +179,9 @@ export const CalendarViewSettings = () => {
/>
)}
{openDelete && (
<ViewDeleteDialog
<NodeDeleteDialog
title="Are you sure you want delete this view?"
description="This action cannot be undone. This view will no longer be accessible and all data in the view will be lost."
id={view.id}
open={openDelete}
onOpenChange={setOpenDelete}

View File

@@ -4,9 +4,9 @@ import { Fragment, useState } from 'react';
import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewDeleteDialog } from '@colanode/ui/components/databases/view-delete-dialog';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button';
import {
Popover,
@@ -179,7 +179,9 @@ export const TableViewSettings = () => {
/>
)}
{openDelete && (
<ViewDeleteDialog
<NodeDeleteDialog
title="Are you sure you want delete this view?"
description="This action cannot be undone. This view will no longer be accessible and all data in the view will be lost."
id={view.id}
open={openDelete}
onOpenChange={setOpenDelete}

View File

@@ -1,10 +1,19 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { Calendar, Columns, Table } from 'lucide-react';
import { FC } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod/v4';
import { LocalDatabaseViewNode } from '@colanode/client/types';
import {
compareString,
generateFractionalIndex,
generateId,
IdType,
} from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { Button } from '@colanode/ui/components/ui/button';
import {
Dialog,
@@ -26,7 +35,6 @@ import { Input } from '@colanode/ui/components/ui/input';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { cn } from '@colanode/ui/lib/utils';
const formSchema = z.object({
@@ -34,6 +42,8 @@ const formSchema = z.object({
type: z.enum(['table', 'board', 'calendar']),
});
type ViewCreateFormValues = z.infer<typeof formSchema>;
interface ViewTypeOption {
name: string;
icon: FC;
@@ -69,7 +79,6 @@ export const ViewCreateDialog = ({
}: ViewCreateDialogProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const { mutate, isPending } = useMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -79,44 +88,62 @@ export const ViewCreateDialog = ({
},
});
const { mutate, isPending } = useMutation({
mutationFn: async (values: ViewCreateFormValues) => {
const type = viewTypes.find((viewType) => viewType.type === values.type);
if (!type) {
return;
}
let name = values.name;
if (name === '') {
name = type.name;
}
const nodes = collections.workspace(workspace.userId).nodes;
let maxIndex: string | null = null;
nodes.forEach((node) => {
if (node.type === 'database_view' && node.parentId === database.id) {
const index = node.index;
if (maxIndex === null || compareString(index, maxIndex) > 0) {
maxIndex = index;
}
}
});
const viewId = generateId(IdType.DatabaseView);
const view: LocalDatabaseViewNode = {
id: viewId,
type: 'database_view',
name: name,
parentId: database.id,
layout: type.type,
index: generateFractionalIndex(maxIndex, null),
rootId: database.id,
createdAt: new Date().toISOString(),
createdBy: workspace.userId,
updatedAt: null,
updatedBy: null,
localRevision: '0',
serverRevision: '0',
};
nodes.insert(view);
return viewId;
},
onSuccess: () => {
form.reset();
onOpenChange(false);
},
onError: (error) => {
toast.error(error.message);
},
});
const handleCancel = () => {
form.reset();
onOpenChange(false);
};
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
if (isPending) {
return;
}
const type = viewTypes.find((viewType) => viewType.type === values.type);
if (!type) {
return;
}
let name = values.name;
if (name === '') {
name = type.name;
}
mutate({
input: {
type: 'view.create',
viewType: type.type,
databaseId: database.id,
name: name,
userId: workspace.userId,
},
onSuccess() {
form.reset();
onOpenChange(false);
},
onError(error) {
toast.error(error.message);
},
});
};
if (!database.canEdit) {
return null;
}
@@ -133,7 +160,7 @@ export const ViewCreateDialog = ({
<Form {...form}>
<form
className="flex flex-col"
onSubmit={form.handleSubmit(handleSubmit)}
onSubmit={form.handleSubmit((values) => mutate(values))}
>
<div className="grow space-y-4 py-2 pb-4">
<FormField

View File

@@ -1,78 +0,0 @@
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 { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface ViewDeleteDialogProps {
id: string;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const ViewDeleteDialog = ({
id,
open,
onOpenChange,
}: ViewDeleteDialogProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const { mutate, isPending } = useMutation();
if (!database.canEdit) {
return null;
}
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want delete this view?
</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This view will no longer be accessible
and all data in the view will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button
variant="destructive"
disabled={isPending}
onClick={() => {
mutate({
input: {
type: 'view.delete',
viewId: id,
databaseId: database.id,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
},
onError(error) {
toast.error(error.message);
},
});
}}
>
{isPending && <Spinner className="mr-1" />}
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};