Use tanstackdb for space creation

This commit is contained in:
Hakan Shehu
2025-11-20 19:01:52 -08:00
parent a1dab1cb27
commit a0b4ca3eac
7 changed files with 80 additions and 159 deletions

View File

@@ -57,7 +57,6 @@ import { ServerCreateMutationHandler } from './servers/server-create';
import { ServerDeleteMutationHandler } from './servers/server-delete';
import { ServerSyncMutationHandler } from './servers/server-sync';
import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder';
import { SpaceCreateMutationHandler } from './spaces/space-create';
import { SpaceUpdateMutationHandler } from './spaces/space-update';
import { UserRoleUpdateMutationHandler } from './users/user-role-update';
import { UserStorageUpdateMutationHandler } from './users/user-storage-update';
@@ -109,7 +108,6 @@ export const buildMutationHandlerMap = (
'server.create': new ServerCreateMutationHandler(app),
'server.delete': new ServerDeleteMutationHandler(app),
'server.sync': new ServerSyncMutationHandler(app),
'space.create': new SpaceCreateMutationHandler(app),
'user.role.update': new UserRoleUpdateMutationHandler(app),
'users.create': new UsersCreateMutationHandler(app),
'workspace.create': new WorkspaceCreateMutationHandler(app),

View File

@@ -1,87 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
SpaceCreateMutationInput,
SpaceCreateMutationOutput,
} from '@colanode/client/mutations/spaces/space-create';
import {
ChannelAttributes,
generateId,
IdType,
PageAttributes,
SpaceAttributes,
} from '@colanode/core';
export class SpaceCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SpaceCreateMutationInput>
{
async handleMutation(
input: SpaceCreateMutationInput
): Promise<SpaceCreateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
if (!workspace) {
throw new MutationError(
MutationErrorCode.WorkspaceNotFound,
'Workspace was not found or has been deleted.'
);
}
if (workspace.role === 'guest' || workspace.role === 'none') {
throw new MutationError(
MutationErrorCode.SpaceCreateForbidden,
"You don't have permission to create spaces in this workspace."
);
}
const spaceId = generateId(IdType.Space);
const spaceAttributes: SpaceAttributes = {
type: 'space',
name: input.name,
visibility: 'private',
collaborators: {
[workspace.userId]: 'admin',
},
description: input.description,
avatar: input.avatar,
};
await workspace.nodes.createNode({
id: spaceId,
attributes: spaceAttributes,
parentId: null,
});
const pageId = generateId(IdType.Page);
const pageAttributes: PageAttributes = {
type: 'page',
name: 'Home',
parentId: spaceId,
};
await workspace.nodes.createNode({
id: pageId,
attributes: pageAttributes,
parentId: spaceId,
});
const channelId = generateId(IdType.Channel);
const channelAttributes: ChannelAttributes = {
type: 'channel',
name: 'Discussions',
parentId: spaceId,
};
await workspace.nodes.createNode({
id: channelId,
attributes: channelAttributes,
parentId: spaceId,
});
return {
id: spaceId,
};
}
}

View File

@@ -45,7 +45,6 @@ export * from './records/record-field-value-set';
export * from './records/record-name-update';
export * from './servers/server-create';
export * from './servers/server-delete';
export * from './spaces/space-create';
export * from './spaces/space-update';
export * from './spaces/space-child-reorder';
export * from './workspaces/workspace-create';

View File

@@ -1,20 +0,0 @@
export type SpaceCreateMutationInput = {
type: 'space.create';
userId: string;
name: string;
description: string;
avatar?: string | null;
};
export type SpaceCreateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'space.create': {
input: SpaceCreateMutationInput;
output: SpaceCreateMutationOutput;
};
}
}

View File

@@ -16,7 +16,7 @@ interface SpaceContainerProps {
export const SpaceContainer = ({ space, role }: SpaceContainerProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const { mutate } = useMutation();
const canEdit = hasNodeRole(role, 'admin');
const canDelete = hasNodeRole(role, 'admin');
@@ -53,8 +53,7 @@ export const SpaceContainer = ({ space, role }: SpaceContainerProps) => {
},
});
}}
isSaving={isPending}
saveText="Update"
submitText="Update"
/>
</div>

View File

@@ -1,6 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';
import { SpaceForm } from '@colanode/ui/components/spaces/space-form';
import { LocalPageNode, LocalSpaceNode } from '@colanode/client/types';
import { generateId, IdType } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import {
SpaceForm,
SpaceFormValues,
} from '@colanode/ui/components/spaces/space-form';
import {
Dialog,
DialogContent,
@@ -9,7 +16,6 @@ import {
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface SpaceCreateDialogProps {
open: boolean;
@@ -21,7 +27,65 @@ export const SpaceCreateDialog = ({
onOpenChange,
}: SpaceCreateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const { mutate } = useMutation({
mutationFn: async (values: SpaceFormValues) => {
const nodes = collections.workspace(workspace.userId).nodes;
const spaceId = generateId(IdType.Space);
const space: LocalSpaceNode = {
id: spaceId,
type: 'space',
attributes: {
type: 'space',
name: values.name,
description: values.description,
avatar: values.avatar,
collaborators: {
[workspace.userId]: 'admin',
},
visibility: 'private',
},
parentId: '',
rootId: spaceId,
createdAt: new Date().toISOString(),
createdBy: workspace.userId,
updatedAt: null,
updatedBy: null,
localRevision: '0',
serverRevision: '0',
};
console.log('space', JSON.stringify(space, null, 2));
const pageId = generateId(IdType.Page);
const page: LocalPageNode = {
id: pageId,
type: 'page',
attributes: {
type: 'page',
name: 'Home',
parentId: spaceId,
},
parentId: spaceId,
rootId: spaceId,
createdAt: new Date().toISOString(),
createdBy: workspace.userId,
updatedAt: null,
updatedBy: null,
localRevision: '0',
serverRevision: '0',
};
nodes.insert([space, page]);
return space;
},
onSuccess: () => {
onOpenChange(false);
},
onError: (error) => {
toast.error(error.message);
},
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -33,36 +97,9 @@ export const SpaceCreateDialog = ({
</DialogDescription>
</DialogHeader>
<SpaceForm
onSubmit={(values) => {
if (isPending) {
return;
}
if (values.name.length < 3) {
return;
}
mutate({
input: {
type: 'space.create',
name: values.name,
description: values.description,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
},
onError(error) {
toast.error(error.message);
},
});
}}
isSaving={isPending}
onCancel={() => {
onOpenChange(false);
}}
saveText="Create"
onSubmit={(values) => mutate(values)}
submitText="Create"
onCancel={() => onOpenChange(false)}
/>
</DialogContent>
</Dialog>

View File

@@ -17,7 +17,6 @@ import {
FormMessage,
} from '@colanode/ui/components/ui/form';
import { Input } from '@colanode/ui/components/ui/input';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { Textarea } from '@colanode/ui/components/ui/textarea';
import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
import { cn } from '@colanode/ui/lib/utils';
@@ -28,29 +27,27 @@ const formSchema = z.object({
avatar: z.string().optional().nullable(),
});
type formSchemaType = z.infer<typeof formSchema>;
export type SpaceFormValues = z.infer<typeof formSchema>;
interface SpaceFormProps {
values?: formSchemaType;
onSubmit: (values: formSchemaType) => void;
isSaving: boolean;
values?: SpaceFormValues;
onSubmit: (values: SpaceFormValues) => void;
onCancel?: () => void;
saveText: string;
submitText: string;
readOnly?: boolean;
}
export const SpaceForm = ({
values,
onSubmit,
isSaving,
onCancel,
saveText,
submitText,
readOnly = false,
}: SpaceFormProps) => {
const id = useRef(generateId(IdType.Space));
const isMobile = useIsMobile();
const form = useForm<formSchemaType>({
const form = useForm<SpaceFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: values?.name ?? '',
@@ -136,7 +133,6 @@ export const SpaceForm = ({
{onCancel && (
<Button
type="button"
disabled={isSaving}
variant="outline"
onClick={() => {
onCancel();
@@ -146,9 +142,8 @@ export const SpaceForm = ({
</Button>
)}
<Button type="submit" disabled={isSaving} className="w-20">
{isSaving && <Spinner className="mr-1" />}
{saveText}
<Button type="submit" disabled={readOnly} className="w-20">
{submitText}
</Button>
</div>
)}