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 { ServerDeleteMutationHandler } from './servers/server-delete';
import { ServerSyncMutationHandler } from './servers/server-sync'; import { ServerSyncMutationHandler } from './servers/server-sync';
import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder'; import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder';
import { SpaceCreateMutationHandler } from './spaces/space-create';
import { SpaceUpdateMutationHandler } from './spaces/space-update'; import { SpaceUpdateMutationHandler } from './spaces/space-update';
import { UserRoleUpdateMutationHandler } from './users/user-role-update'; import { UserRoleUpdateMutationHandler } from './users/user-role-update';
import { UserStorageUpdateMutationHandler } from './users/user-storage-update'; import { UserStorageUpdateMutationHandler } from './users/user-storage-update';
@@ -109,7 +108,6 @@ export const buildMutationHandlerMap = (
'server.create': new ServerCreateMutationHandler(app), 'server.create': new ServerCreateMutationHandler(app),
'server.delete': new ServerDeleteMutationHandler(app), 'server.delete': new ServerDeleteMutationHandler(app),
'server.sync': new ServerSyncMutationHandler(app), 'server.sync': new ServerSyncMutationHandler(app),
'space.create': new SpaceCreateMutationHandler(app),
'user.role.update': new UserRoleUpdateMutationHandler(app), 'user.role.update': new UserRoleUpdateMutationHandler(app),
'users.create': new UsersCreateMutationHandler(app), 'users.create': new UsersCreateMutationHandler(app),
'workspace.create': new WorkspaceCreateMutationHandler(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 './records/record-name-update';
export * from './servers/server-create'; export * from './servers/server-create';
export * from './servers/server-delete'; export * from './servers/server-delete';
export * from './spaces/space-create';
export * from './spaces/space-update'; export * from './spaces/space-update';
export * from './spaces/space-child-reorder'; export * from './spaces/space-child-reorder';
export * from './workspaces/workspace-create'; 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) => { export const SpaceContainer = ({ space, role }: SpaceContainerProps) => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const { mutate, isPending } = useMutation(); const { mutate } = useMutation();
const canEdit = hasNodeRole(role, 'admin'); const canEdit = hasNodeRole(role, 'admin');
const canDelete = hasNodeRole(role, 'admin'); const canDelete = hasNodeRole(role, 'admin');
@@ -53,8 +53,7 @@ export const SpaceContainer = ({ space, role }: SpaceContainerProps) => {
}, },
}); });
}} }}
isSaving={isPending} submitText="Update"
saveText="Update"
/> />
</div> </div>

View File

@@ -1,6 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner'; 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 { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -9,7 +16,6 @@ import {
DialogTitle, DialogTitle,
} from '@colanode/ui/components/ui/dialog'; } from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace'; import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface SpaceCreateDialogProps { interface SpaceCreateDialogProps {
open: boolean; open: boolean;
@@ -21,7 +27,65 @@ export const SpaceCreateDialog = ({
onOpenChange, onOpenChange,
}: SpaceCreateDialogProps) => { }: SpaceCreateDialogProps) => {
const workspace = useWorkspace(); 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 ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -33,36 +97,9 @@ export const SpaceCreateDialog = ({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<SpaceForm <SpaceForm
onSubmit={(values) => { onSubmit={(values) => mutate(values)}
if (isPending) { submitText="Create"
return; onCancel={() => onOpenChange(false)}
}
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"
/> />
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

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