mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Implement space container
This commit is contained in:
@@ -50,7 +50,9 @@ import { SelectOptionUpdateMutationHandler } from '@/main/mutations/databases/se
|
||||
import { ServerCreateMutationHandler } from '@/main/mutations/servers/server-create';
|
||||
import { SpaceCreateMutationHandler } from '@/main/mutations/spaces/space-create';
|
||||
import { SpaceDeleteMutationHandler } from '@/main/mutations/spaces/space-delete';
|
||||
import { SpaceUpdateMutationHandler } from '@/main/mutations/spaces/space-update';
|
||||
import { SpaceDescriptionUpdateMutationHandler } from '@/main/mutations/spaces/space-description-update';
|
||||
import { SpaceAvatarUpdateMutationHandler } from '@/main/mutations/spaces/space-avatar-update';
|
||||
import { SpaceNameUpdateMutationHandler } from '@/main/mutations/spaces/space-name-update';
|
||||
import { ViewCreateMutationHandler } from '@/main/mutations/databases/view-create';
|
||||
import { ViewDeleteMutationHandler } from '@/main/mutations/databases/view-delete';
|
||||
import { ViewUpdateMutationHandler } from '@/main/mutations/databases/view-update';
|
||||
@@ -121,7 +123,9 @@ export const mutationHandlerMap: MutationHandlerMap = {
|
||||
file_mark_opened: new FileMarkOpenedMutationHandler(),
|
||||
file_mark_seen: new FileMarkSeenMutationHandler(),
|
||||
file_save_temp: new FileSaveTempMutationHandler(),
|
||||
space_update: new SpaceUpdateMutationHandler(),
|
||||
space_avatar_update: new SpaceAvatarUpdateMutationHandler(),
|
||||
space_description_update: new SpaceDescriptionUpdateMutationHandler(),
|
||||
space_name_update: new SpaceNameUpdateMutationHandler(),
|
||||
account_update: new AccountUpdateMutationHandler(),
|
||||
view_update: new ViewUpdateMutationHandler(),
|
||||
view_delete: new ViewDeleteMutationHandler(),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SpaceAttributes } from '@colanode/core';
|
||||
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { MutationError, MutationErrorCode } from '@/shared/mutations';
|
||||
import {
|
||||
SpaceAvatarUpdateMutationInput,
|
||||
SpaceAvatarUpdateMutationOutput,
|
||||
} from '@/shared/mutations/spaces/space-avatar-update';
|
||||
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
|
||||
|
||||
export class SpaceAvatarUpdateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
implements MutationHandler<SpaceAvatarUpdateMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: SpaceAvatarUpdateMutationInput
|
||||
): Promise<SpaceAvatarUpdateMutationOutput> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
const result = await workspace.entries.updateEntry<SpaceAttributes>(
|
||||
input.spaceId,
|
||||
(attributes) => {
|
||||
attributes.avatar = input.avatar;
|
||||
return attributes;
|
||||
}
|
||||
);
|
||||
|
||||
if (result === 'unauthorized') {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.SpaceUpdateForbidden,
|
||||
"You don't have permission to update this space."
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,24 @@ import { SpaceAttributes } from '@colanode/core';
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { MutationError, MutationErrorCode } from '@/shared/mutations';
|
||||
import {
|
||||
SpaceUpdateMutationInput,
|
||||
SpaceUpdateMutationOutput,
|
||||
} from '@/shared/mutations/spaces/space-update';
|
||||
SpaceDescriptionUpdateMutationInput,
|
||||
SpaceDescriptionUpdateMutationOutput,
|
||||
} from '@/shared/mutations/spaces/space-description-update';
|
||||
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
|
||||
|
||||
export class SpaceUpdateMutationHandler
|
||||
export class SpaceDescriptionUpdateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
implements MutationHandler<SpaceUpdateMutationInput>
|
||||
implements MutationHandler<SpaceDescriptionUpdateMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: SpaceUpdateMutationInput
|
||||
): Promise<SpaceUpdateMutationOutput> {
|
||||
input: SpaceDescriptionUpdateMutationInput
|
||||
): Promise<SpaceDescriptionUpdateMutationOutput> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
const result = await workspace.entries.updateEntry<SpaceAttributes>(
|
||||
input.id,
|
||||
input.spaceId,
|
||||
(attributes) => {
|
||||
attributes.name = input.name;
|
||||
attributes.description = input.description;
|
||||
attributes.avatar = input.avatar;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
);
|
||||
@@ -35,13 +32,6 @@ export class SpaceUpdateMutationHandler
|
||||
);
|
||||
}
|
||||
|
||||
if (result !== 'success') {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.SpaceUpdateFailed,
|
||||
'Something went wrong while updating the space. Please try again later.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
39
apps/desktop/src/main/mutations/spaces/space-name-update.ts
Normal file
39
apps/desktop/src/main/mutations/spaces/space-name-update.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { SpaceAttributes } from '@colanode/core';
|
||||
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { MutationError, MutationErrorCode } from '@/shared/mutations';
|
||||
import {
|
||||
SpaceNameUpdateMutationInput,
|
||||
SpaceNameUpdateMutationOutput,
|
||||
} from '@/shared/mutations/spaces/space-name-update';
|
||||
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
|
||||
|
||||
export class SpaceNameUpdateMutationHandler
|
||||
extends WorkspaceMutationHandlerBase
|
||||
implements MutationHandler<SpaceNameUpdateMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: SpaceNameUpdateMutationInput
|
||||
): Promise<SpaceNameUpdateMutationOutput> {
|
||||
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
|
||||
|
||||
const result = await workspace.entries.updateEntry<SpaceAttributes>(
|
||||
input.spaceId,
|
||||
(attributes) => {
|
||||
attributes.name = input.name;
|
||||
return attributes;
|
||||
}
|
||||
);
|
||||
|
||||
if (result === 'unauthorized') {
|
||||
throw new MutationError(
|
||||
MutationErrorCode.SpaceUpdateForbidden,
|
||||
"You don't have permission to update this space."
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export const Avatar = (props: AvatarProps) => {
|
||||
}
|
||||
|
||||
const avatarType = getIdType(avatar);
|
||||
if (avatarType === IdType.Emoji) {
|
||||
if (avatarType === IdType.EmojiSkin) {
|
||||
return <EmojiAvatar {...props} />;
|
||||
} else if (avatarType === IdType.Icon) {
|
||||
return <IconAvatar {...props} />;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getIdType, IdType } from '@colanode/core';
|
||||
|
||||
import { ContainerTab } from '@/shared/types/workspaces';
|
||||
import { TabsContent } from '@/renderer/components/ui/tabs';
|
||||
import { SpaceContainer } from '@/renderer/components/spaces/space-container';
|
||||
import { ChannelContainer } from '@/renderer/components/channels/channel-container';
|
||||
import { ChatContainer } from '@/renderer/components/chats/chat-container';
|
||||
import { DatabaseContainer } from '@/renderer/components/databases/database-container';
|
||||
@@ -23,6 +24,7 @@ export const ContainerTabContent = ({ tab }: ContainerTabContentProps) => {
|
||||
className="h-full min-h-full w-full min-w-full m-0 pt-2"
|
||||
>
|
||||
{match(getIdType(tab.path))
|
||||
.with(IdType.Space, () => <SpaceContainer spaceId={tab.path} />)
|
||||
.with(IdType.Channel, () => <ChannelContainer channelId={tab.path} />)
|
||||
.with(IdType.Page, () => <PageContainer pageId={tab.path} />)
|
||||
.with(IdType.Database, () => (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { TabsTrigger } from '@/renderer/components/ui/tabs';
|
||||
import { ContainerTab } from '@/shared/types/workspaces';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { SpaceContainerTab } from '@/renderer/components/spaces/space-container-tab';
|
||||
import { ChannelContainerTab } from '@/renderer/components/channels/channel-container-tab';
|
||||
import { FileContainerTab } from '@/renderer/components/files/file-container-tab';
|
||||
import { DatabaseContainerTab } from '@/renderer/components/databases/database-container-tab';
|
||||
@@ -62,7 +63,7 @@ export const ContainerTabTrigger = ({
|
||||
value={tab.path}
|
||||
key={tab.path}
|
||||
className={cn(
|
||||
'overflow-hidden rounded-b-none bg-muted py-2 data-[state=active]:z-10 data-[state=active]:shadow-none h-10 group/tab app-no-drag-region flex items-center justify-between gap-2',
|
||||
'overflow-hidden rounded-b-none bg-muted py-2 data-[state=active]:z-10 data-[state=active]:shadow-none h-10 group/tab app-no-drag-region flex items-center justify-between gap-2 max-w-60',
|
||||
tab.preview && 'italic',
|
||||
dropMonitor.isOver &&
|
||||
dropMonitor.canDrop &&
|
||||
@@ -78,6 +79,7 @@ export const ContainerTabTrigger = ({
|
||||
>
|
||||
<div className="overflow-hidden truncate">
|
||||
{match(getIdType(tab.path))
|
||||
.with(IdType.Space, () => <SpaceContainerTab spaceId={tab.path} />)
|
||||
.with(IdType.Channel, () => (
|
||||
<ChannelContainerTab channelId={tab.path} />
|
||||
))
|
||||
|
||||
49
apps/desktop/src/renderer/components/spaces/space-avatar.tsx
Normal file
49
apps/desktop/src/renderer/components/spaces/space-avatar.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { AvatarPopover } from '@/renderer/components/avatars/avatar-popover';
|
||||
import { Button } from '@/renderer/components/ui/button';
|
||||
|
||||
interface SpaceAvatarProps {
|
||||
space: SpaceEntry;
|
||||
readonly: boolean;
|
||||
onUpdate: (avatar: string) => void;
|
||||
}
|
||||
|
||||
export const SpaceAvatar = ({
|
||||
space,
|
||||
readonly,
|
||||
onUpdate,
|
||||
}: SpaceAvatarProps) => {
|
||||
if (readonly) {
|
||||
return (
|
||||
<Button type="button" variant="outline" size="icon">
|
||||
<Avatar
|
||||
id={space.id}
|
||||
name={space.attributes.name}
|
||||
avatar={space.attributes.avatar}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AvatarPopover
|
||||
onPick={(avatar) => {
|
||||
if (avatar === space.attributes.avatar) return;
|
||||
|
||||
onUpdate(avatar);
|
||||
}}
|
||||
>
|
||||
<Button type="button" variant="outline" size="icon">
|
||||
<Avatar
|
||||
id={space.id}
|
||||
name={space.attributes.name}
|
||||
avatar={space.attributes.avatar}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</Button>
|
||||
</AvatarPopover>
|
||||
);
|
||||
};
|
||||
90
apps/desktop/src/renderer/components/spaces/space-body.tsx
Normal file
90
apps/desktop/src/renderer/components/spaces/space-body.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { EntryRole, hasEntryRole, SpaceEntry } from '@colanode/core';
|
||||
import { Info, Trash2, Users } from 'lucide-react';
|
||||
|
||||
import { EntryCollaborators } from '@/renderer/components/collaborators/entry-collaborators';
|
||||
import { SpaceDeleteForm } from '@/renderer/components/spaces/space-delete-form';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/renderer/components/ui/tabs';
|
||||
import { useLayout } from '@/renderer/contexts/layout';
|
||||
import { SpaceGeneralTab } from '@/renderer/components/spaces/space-general-tab';
|
||||
|
||||
interface SpaceBodyProps {
|
||||
space: SpaceEntry;
|
||||
role: EntryRole;
|
||||
}
|
||||
|
||||
export const SpaceBody = ({ space, role }: SpaceBodyProps) => {
|
||||
const layout = useLayout();
|
||||
const canEdit = hasEntryRole(role, 'admin');
|
||||
const canDelete = hasEntryRole(role, 'admin');
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue="general"
|
||||
className="grid h-full max-h-full grid-cols-[200px_minmax(0,1fr)] overflow-hidden gap-4"
|
||||
>
|
||||
<TabsList className="flex h-full max-h-full flex-col items-start justify-start gap-1 rounded-none bg-white">
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-general`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="general"
|
||||
>
|
||||
<Info className="mr-2 size-4" />
|
||||
General
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-collaborators`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="collaborators"
|
||||
>
|
||||
<Users className="mr-2 size-4" />
|
||||
Collaborators
|
||||
</TabsTrigger>
|
||||
{canDelete && (
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-delete`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="delete"
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
Delete
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<div className="overflow-auto pl-1 max-w-[50rem]">
|
||||
<TabsContent
|
||||
key="tab-content-info"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="general"
|
||||
>
|
||||
<SpaceGeneralTab space={space} readonly={!canEdit} />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
key="tab-content-collaborators"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="collaborators"
|
||||
>
|
||||
<EntryCollaborators entry={space} entries={[space]} role={role} />
|
||||
</TabsContent>
|
||||
{canDelete && (
|
||||
<TabsContent
|
||||
key="tab-content-delete"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="delete"
|
||||
>
|
||||
<SpaceDeleteForm
|
||||
id={space.id}
|
||||
onDeleted={() => {
|
||||
layout.close(space.id);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
|
||||
interface SpaceContainerTabProps {
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export const SpaceContainerTab = ({ spaceId }: SpaceContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const { data: entry } = useQuery({
|
||||
type: 'entry_get',
|
||||
entryId: spaceId,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const space = entry as SpaceEntry;
|
||||
if (!space) {
|
||||
return <p>Not found</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
size="small"
|
||||
id={space.id}
|
||||
name={space.attributes.name}
|
||||
avatar={space.attributes.avatar}
|
||||
/>
|
||||
<span>{space.attributes.name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
ContainerSettings,
|
||||
} from '@/renderer/components/ui/container';
|
||||
import { ContainerBreadcrumb } from '@/renderer/components/layouts/containers/container-breadrumb';
|
||||
import { SpaceNotFound } from '@/renderer/components/spaces/space-not-found';
|
||||
import { EntryCollaboratorsPopover } from '@/renderer/components/collaborators/entry-collaborators-popover';
|
||||
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
|
||||
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
|
||||
import { SpaceBody } from '@/renderer/components/spaces/space-body';
|
||||
|
||||
interface SpaceContainerProps {
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
export const SpaceContainer = ({ spaceId }: SpaceContainerProps) => {
|
||||
const data = useEntryContainer<SpaceEntry>(spaceId);
|
||||
|
||||
useEntryRadar(data.entry);
|
||||
|
||||
if (data.isPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data.entry) {
|
||||
return <SpaceNotFound />;
|
||||
}
|
||||
|
||||
const { entry, role } = data;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||
<ContainerSettings>
|
||||
<EntryCollaboratorsPopover
|
||||
entry={entry}
|
||||
entries={[entry]}
|
||||
role={role}
|
||||
/>
|
||||
</ContainerSettings>
|
||||
</ContainerHeader>
|
||||
<ContainerBody>
|
||||
<SpaceBody space={entry} role={role} />
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@/renderer/components/ui/button';
|
||||
import { Spinner } from '@/renderer/components/ui/spinner';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/renderer/components/ui/alert-dialog';
|
||||
|
||||
interface SpaceDeleteFormProps {
|
||||
id: string;
|
||||
onDeleted: () => void;
|
||||
}
|
||||
|
||||
export const SpaceDeleteForm = ({ id }: SpaceDeleteFormProps) => {
|
||||
export const SpaceDeleteForm = ({ id, onDeleted }: SpaceDeleteFormProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<h3 className="font-heading mb-px mt-2 text-xl font-semibold tracking-tight">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="font-heading mb-px text-2xl font-semibold tracking-tight">
|
||||
Delete space
|
||||
</h3>
|
||||
<p>Deleting a space is permanent and cannot be undone.</p>
|
||||
@@ -26,6 +38,29 @@ export const SpaceDeleteForm = ({ id }: SpaceDeleteFormProps) => {
|
||||
<div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
Delete space
|
||||
</Button>
|
||||
</div>
|
||||
<AlertDialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want delete this space?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This space will no longer be
|
||||
accessible by you or others you've shared it with.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
mutate({
|
||||
input: {
|
||||
@@ -35,6 +70,8 @@ export const SpaceDeleteForm = ({ id }: SpaceDeleteFormProps) => {
|
||||
spaceId: id,
|
||||
},
|
||||
onSuccess() {
|
||||
setShowDeleteModal(false);
|
||||
onDeleted();
|
||||
toast({
|
||||
title: 'Space deleted',
|
||||
description: 'Space was deleted successfully',
|
||||
@@ -51,9 +88,11 @@ export const SpaceDeleteForm = ({ id }: SpaceDeleteFormProps) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}Delete space
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { SmartTextarea } from '@/renderer/components/ui/smart-textarea';
|
||||
|
||||
interface SpaceDescriptionProps {
|
||||
space: SpaceEntry;
|
||||
readonly: boolean;
|
||||
onUpdate: (description: string) => void;
|
||||
}
|
||||
|
||||
export const SpaceDescription = ({
|
||||
space,
|
||||
readonly,
|
||||
onUpdate,
|
||||
}: SpaceDescriptionProps) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (readonly) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [readonly, textareaRef]);
|
||||
|
||||
return (
|
||||
<SmartTextarea
|
||||
value={space.attributes.description ?? ''}
|
||||
readOnly={readonly}
|
||||
ref={textareaRef}
|
||||
onChange={(value) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === space.attributes.description) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdate(value);
|
||||
}}
|
||||
placeholder="No description"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
|
||||
import { SpaceAvatar } from '@/renderer/components/spaces/space-avatar';
|
||||
import { SpaceDescription } from '@/renderer/components/spaces/space-description';
|
||||
import { SpaceName } from '@/renderer/components/spaces/space-name';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
|
||||
interface SpaceGeneralTabProps {
|
||||
space: SpaceEntry;
|
||||
readonly: boolean;
|
||||
}
|
||||
|
||||
export const SpaceGeneralTab = ({ space, readonly }: SpaceGeneralTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<SpaceAvatar
|
||||
space={space}
|
||||
readonly={readonly || isPending}
|
||||
onUpdate={(avatar) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'space_avatar_update',
|
||||
spaceId: space.id,
|
||||
avatar,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to update space avatar',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SpaceName
|
||||
space={space}
|
||||
readonly={readonly || isPending}
|
||||
onUpdate={(name) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'space_name_update',
|
||||
spaceId: space.id,
|
||||
name,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to update space name',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<p className="text-sm font-medium">Description</p>
|
||||
<SpaceDescription
|
||||
space={space}
|
||||
readonly={readonly || isPending}
|
||||
onUpdate={(description) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'space_description_update',
|
||||
spaceId: space.id,
|
||||
description,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to update space description',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
45
apps/desktop/src/renderer/components/spaces/space-name.tsx
Normal file
45
apps/desktop/src/renderer/components/spaces/space-name.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { SmartTextInput } from '@/renderer/components/ui/smart-text-input';
|
||||
|
||||
interface SpaceNameProps {
|
||||
space: SpaceEntry;
|
||||
readonly: boolean;
|
||||
onUpdate: (name: string) => void;
|
||||
}
|
||||
|
||||
export const SpaceName = ({ space, readonly, onUpdate }: SpaceNameProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (readonly) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [readonly, inputRef]);
|
||||
|
||||
return (
|
||||
<SmartTextInput
|
||||
value={space.attributes.name}
|
||||
readOnly={readonly}
|
||||
ref={inputRef}
|
||||
onChange={(value) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === space.attributes.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
onUpdate(value);
|
||||
}}
|
||||
className="font-heading border-0 pl-1 text-4xl font-bold shadow-none focus-visible:ring-0"
|
||||
placeholder="Unnamed"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BadgeAlert } from 'lucide-react';
|
||||
|
||||
export const SpaceNotFound = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-6 text-center">
|
||||
<BadgeAlert className="size-12 mb-4" />
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Space not found</h1>
|
||||
<p className="mt-2 text-sm font-medium text-muted-foreground">
|
||||
The space you are looking for does not exist. It may have been deleted
|
||||
or your access has been removed.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
import { extractEntryRole, SpaceEntry, hasEntryRole } from '@colanode/core';
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { Info, Trash2, Users } from 'lucide-react';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { EntryCollaborators } from '@/renderer/components/collaborators/entry-collaborators';
|
||||
import { SpaceDeleteForm } from '@/renderer/components/spaces/space-delete-form';
|
||||
import { SpaceUpdateForm } from '@/renderer/components/spaces/space-update-form';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/renderer/components/ui/dialog';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/renderer/components/ui/tabs';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
|
||||
interface SpaceSettingsDialogProps {
|
||||
space: SpaceEntry;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
defaultTab?: string;
|
||||
}
|
||||
|
||||
export const SpaceSettingsDialog = ({
|
||||
space,
|
||||
open,
|
||||
onOpenChange,
|
||||
defaultTab,
|
||||
}: SpaceSettingsDialogProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const role = extractEntryRole(space, workspace.userId);
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canDelete = hasEntryRole(role, 'editor');
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="md:min-h-3/4 md:max-h-3/4 p-3 md:h-3/4 md:w-3/4 md:max-w-full">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Space Settings</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<Tabs
|
||||
defaultValue={defaultTab ?? 'info'}
|
||||
className="grid h-full max-h-full grid-cols-[240px_minmax(0,1fr)] overflow-hidden"
|
||||
>
|
||||
<TabsList className="flex h-full max-h-full flex-col items-start justify-start gap-1 rounded-none border-r border-r-gray-100 bg-white pr-3">
|
||||
<div className="mb-1 flex h-10 w-full items-center justify-between bg-gray-50 p-1 text-foreground/80">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
id={space.id}
|
||||
avatar={space.attributes.avatar}
|
||||
name={space.attributes.name}
|
||||
size="small"
|
||||
/>
|
||||
<span>{space.attributes.name ?? 'Error'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-info`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="info"
|
||||
>
|
||||
<Info className="mr-2 size-4" />
|
||||
Info
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-collaborators`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="collaborators"
|
||||
>
|
||||
<Users className="mr-2 size-4" />
|
||||
Collaborators
|
||||
</TabsTrigger>
|
||||
{canDelete && (
|
||||
<TabsTrigger
|
||||
key={`tab-trigger-delete`}
|
||||
className="w-full justify-start p-2 hover:bg-gray-50"
|
||||
value="delete"
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
Delete
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<div className="overflow-auto p-4">
|
||||
<TabsContent
|
||||
key="tab-content-info"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="info"
|
||||
>
|
||||
<SpaceUpdateForm space={space} />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
key="tab-content-collaborators"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="collaborators"
|
||||
>
|
||||
<EntryCollaborators entry={space} entries={[space]} role={role} />
|
||||
</TabsContent>
|
||||
{canDelete && (
|
||||
<TabsContent
|
||||
key="tab-content-delete"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="delete"
|
||||
>
|
||||
<SpaceDeleteForm
|
||||
id={space.id}
|
||||
onDeleted={() => {
|
||||
onOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,6 @@ import { DatabaseCreateDialog } from '@/renderer/components/databases/database-c
|
||||
import { FolderCreateDialog } from '@/renderer/components/folders/folder-create-dialog';
|
||||
import { SidebarItem } from '@/renderer/components/layouts/sidebars/sidebar-item';
|
||||
import { PageCreateDialog } from '@/renderer/components/pages/page-create-dialog';
|
||||
import { SpaceSettingsDialog } from '@/renderer/components/spaces/space-settings-dialog';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
@@ -36,11 +35,6 @@ import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { useLayout } from '@/renderer/contexts/layout';
|
||||
|
||||
interface SettingsState {
|
||||
open: boolean;
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
interface SpaceSidebarItemProps {
|
||||
space: SpaceEntry;
|
||||
}
|
||||
@@ -63,9 +57,6 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
|
||||
const [openCreateChannel, setOpenCreateChannel] = React.useState(false);
|
||||
const [openCreateDatabase, setOpenCreateDatabase] = React.useState(false);
|
||||
const [openCreateFolder, setOpenCreateFolder] = React.useState(false);
|
||||
const [settingsState, setSettingsState] = React.useState<SettingsState>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
@@ -125,22 +116,13 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setSettingsState({ open: true })}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => layout.previewLeft(space.id)}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Settings className="size-4" />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setSettingsState({
|
||||
open: true,
|
||||
tab: 'collaborators',
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => layout.previewLeft(space.id)}>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Plus className="size-4" />
|
||||
<span>Add collaborators</span>
|
||||
@@ -205,16 +187,6 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
|
||||
onOpenChange={setOpenCreateFolder}
|
||||
/>
|
||||
)}
|
||||
{settingsState.open && (
|
||||
<SpaceSettingsDialog
|
||||
space={space}
|
||||
open={settingsState.open}
|
||||
onOpenChange={(open) =>
|
||||
setSettingsState({ open, tab: settingsState.tab })
|
||||
}
|
||||
defaultTab={settingsState.tab}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { SpaceEntry } from '@colanode/core';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { AvatarPopover } from '@/renderer/components/avatars/avatar-popover';
|
||||
import { Button } from '@/renderer/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/renderer/components/ui/form';
|
||||
import { Input } from '@/renderer/components/ui/input';
|
||||
import { Spinner } from '@/renderer/components/ui/spinner';
|
||||
import { Textarea } from '@/renderer/components/ui/textarea';
|
||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
|
||||
interface SpaceUpdateFormProps {
|
||||
space: SpaceEntry;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const SpaceUpdateForm = ({ space }: SpaceUpdateFormProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const canEdit = true;
|
||||
|
||||
const { mutate: updateSpace, isPending: isUpdatingSpace } = useMutation();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: space.attributes.name,
|
||||
description: space.attributes.description ?? '',
|
||||
avatar: space.attributes.avatar ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
updateSpace({
|
||||
input: {
|
||||
type: 'space_update',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
id: space.id,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
avatar: values.avatar,
|
||||
},
|
||||
onSuccess() {
|
||||
toast({
|
||||
title: 'Space updated',
|
||||
description: 'Space was updated successfully',
|
||||
variant: 'default',
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: 'Failed to update space',
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const name = form.watch('name');
|
||||
const avatar = form.watch('avatar');
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<div className="space-y-4 pb-4">
|
||||
<div className="flex flex-row items-end gap-4">
|
||||
<AvatarPopover onPick={(avatar) => form.setValue('avatar', avatar)}>
|
||||
<Button type="button" variant="outline" size="icon">
|
||||
<Avatar
|
||||
id={space.id}
|
||||
name={name}
|
||||
avatar={avatar}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</Button>
|
||||
</AvatarPopover>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} disabled={!canEdit} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Write a short description about the network"
|
||||
{...field}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isUpdatingSpace}>
|
||||
{isUpdatingSpace && <Spinner className="mr-1" />}
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
84
apps/desktop/src/renderer/components/ui/smart-textarea.tsx
Normal file
84
apps/desktop/src/renderer/components/ui/smart-textarea.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
|
||||
interface SmartTextareaProps {
|
||||
value: string | null;
|
||||
onChange: (newValue: string) => void;
|
||||
className?: string;
|
||||
readOnly?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const SmartTextarea = React.forwardRef<HTMLTextAreaElement, SmartTextareaProps>(
|
||||
({ value, onChange, className, readOnly, placeholder, ...props }, ref) => {
|
||||
const [localValue, setLocalValue] = React.useState(value ?? '');
|
||||
const initialValue = React.useRef(value ?? '');
|
||||
|
||||
// Create a debounced version of onChange
|
||||
const debouncedOnChange = React.useMemo(
|
||||
() => debounce((value: string) => onChange(value), 500),
|
||||
[onChange]
|
||||
);
|
||||
|
||||
// Update localValue when value prop changes
|
||||
React.useEffect(() => {
|
||||
setLocalValue(value ?? '');
|
||||
initialValue.current = value ?? '';
|
||||
}, [value]);
|
||||
|
||||
// Cleanup debounce on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
debouncedOnChange.cancel();
|
||||
};
|
||||
}, [debouncedOnChange]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (localValue !== initialValue.current) {
|
||||
debouncedOnChange.cancel(); // Cancel any pending debounced calls
|
||||
onChange(localValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
setLocalValue(initialValue.current); // Revert to initial value
|
||||
debouncedOnChange.cancel(); // Cancel any pending debounced calls
|
||||
} else if (event.key === 'Enter') {
|
||||
if (localValue !== initialValue.current) {
|
||||
onChange(localValue); // Fire onChange immediately when Enter is pressed
|
||||
debouncedOnChange.cancel(); // Cancel any pending debounced calls
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = event.target.value;
|
||||
setLocalValue(newValue);
|
||||
debouncedOnChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SmartTextarea.displayName = 'SmartTextarea';
|
||||
|
||||
export { SmartTextarea };
|
||||
@@ -0,0 +1,20 @@
|
||||
export type SpaceAvatarUpdateMutationInput = {
|
||||
type: 'space_avatar_update';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
spaceId: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
export type SpaceAvatarUpdateMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
space_avatar_update: {
|
||||
input: SpaceAvatarUpdateMutationInput;
|
||||
output: SpaceAvatarUpdateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export type SpaceDescriptionUpdateMutationInput = {
|
||||
type: 'space_description_update';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
spaceId: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type SpaceDescriptionUpdateMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
space_description_update: {
|
||||
input: SpaceDescriptionUpdateMutationInput;
|
||||
output: SpaceDescriptionUpdateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export type SpaceNameUpdateMutationInput = {
|
||||
type: 'space_name_update';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
spaceId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SpaceNameUpdateMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
space_name_update: {
|
||||
input: SpaceNameUpdateMutationInput;
|
||||
output: SpaceNameUpdateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export type SpaceUpdateMutationInput = {
|
||||
type: 'space_update';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar?: string | null;
|
||||
};
|
||||
|
||||
export type SpaceUpdateMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
space_update: {
|
||||
input: SpaceUpdateMutationInput;
|
||||
output: SpaceUpdateMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -354,7 +354,10 @@ export class YDoc {
|
||||
const currentText = yText.toString();
|
||||
const newText = value ? value.toString() : '';
|
||||
|
||||
if (!isEqual(currentText, newText)) {
|
||||
if (isEqual(currentText, newText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diffs = diffChars(currentText, newText);
|
||||
let index = 0;
|
||||
|
||||
@@ -369,7 +372,6 @@ export class YDoc {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractType(
|
||||
schema: z.ZodType<any, any, any>,
|
||||
|
||||
@@ -46,6 +46,8 @@ const createAccount = async (
|
||||
name: account.name,
|
||||
email: account.email,
|
||||
password: account.password,
|
||||
platform: 'seed',
|
||||
version: '0.0.4',
|
||||
});
|
||||
|
||||
if (data.type !== 'success') {
|
||||
|
||||
Reference in New Issue
Block a user