Use tanstackdb for node updates

This commit is contained in:
Hakan Shehu
2025-10-24 16:40:12 +02:00
parent 0e1b658b7d
commit 59ea93e136
71 changed files with 966 additions and 2463 deletions

View File

@@ -1,61 +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 {
ChannelUpdateMutationInput,
ChannelUpdateMutationOutput,
} from '@colanode/client/mutations/channels/channel-update';
import { ChannelAttributes } from '@colanode/core';
export class ChannelUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ChannelUpdateMutationInput>
{
async handleMutation(
input: ChannelUpdateMutationInput
): Promise<ChannelUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<ChannelAttributes>(
input.channelId,
(attributes: ChannelAttributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
}
);
if (result === 'not_found') {
throw new MutationError(
MutationErrorCode.ChannelNotFound,
'Channel not found or has been deleted.'
);
}
if (result === 'invalid_attributes') {
throw new MutationError(
MutationErrorCode.ChannelUpdateFailed,
'Something went wrong while updating the channel.'
);
}
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.ChannelUpdateForbidden,
"You don't have permission to update this channel."
);
}
if (result === 'success') {
return {
success: true,
};
}
throw new MutationError(
MutationErrorCode.Unknown,
'Something went wrong while updating the channel.'
);
}
}

View File

@@ -1,47 +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 {
DatabaseNameFieldUpdateMutationInput,
DatabaseNameFieldUpdateMutationOutput,
} from '@colanode/client/mutations/databases/database-name-field-update';
import { DatabaseAttributes } from '@colanode/core';
export class DatabaseNameFieldUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseNameFieldUpdateMutationInput>
{
async handleMutation(
input: DatabaseNameFieldUpdateMutationInput
): Promise<DatabaseNameFieldUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
attributes.nameField = {
name: input.name,
};
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.DatabaseUpdateForbidden,
"You don't have permission to update this database."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.DatabaseUpdateFailed,
'Something went wrong while updating the database.'
);
}
return {
success: true,
};
}
}

View File

@@ -1,46 +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 {
DatabaseUpdateMutationInput,
DatabaseUpdateMutationOutput,
} from '@colanode/client/mutations/databases/database-update';
import { DatabaseAttributes } from '@colanode/core';
export class DatabaseUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<DatabaseUpdateMutationInput>
{
async handleMutation(
input: DatabaseUpdateMutationInput
): Promise<DatabaseUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.DatabaseUpdateForbidden,
"You don't have permission to update this database."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.DatabaseUpdateFailed,
'Something went wrong while updating the database.'
);
}
return {
success: true,
};
}
}

View File

@@ -1,94 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import { fetchNode } from '@colanode/client/lib/utils';
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
FieldCreateMutationInput,
FieldCreateMutationOutput,
} from '@colanode/client/mutations/databases/field-create';
import {
compareString,
DatabaseAttributes,
FieldAttributes,
FieldType,
generateId,
generateFractionalIndex,
IdType,
} from '@colanode/core';
export class FieldCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldCreateMutationInput>
{
async handleMutation(
input: FieldCreateMutationInput
): Promise<FieldCreateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
if (input.fieldType === 'relation') {
if (!input.relationDatabaseId) {
throw new MutationError(
MutationErrorCode.RelationDatabaseNotFound,
'Relation database not found.'
);
}
const relationDatabase = await fetchNode(
workspace.database,
input.relationDatabaseId
);
if (!relationDatabase || relationDatabase.type !== 'database') {
throw new MutationError(
MutationErrorCode.RelationDatabaseNotFound,
'Relation database not found.'
);
}
}
const fieldId = generateId(IdType.Field);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
const maxIndex = Object.values(attributes.fields)
.map((field) => field.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, null);
const newField: FieldAttributes = {
id: fieldId,
type: input.fieldType as FieldType,
name: input.name,
index,
};
if (newField.type === 'relation') {
newField.databaseId = input.relationDatabaseId;
}
attributes.fields[fieldId] = newField;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.FieldCreateForbidden,
"You don't have permission to create a field in this database."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.FieldCreateFailed,
'Something went wrong while creating the field.'
);
}
return {
id: fieldId,
};
}
}

View File

@@ -1,46 +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 {
FieldDeleteMutationInput,
FieldDeleteMutationOutput,
} from '@colanode/client/mutations/databases/field-delete';
import { DatabaseAttributes } from '@colanode/core';
export class FieldDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldDeleteMutationInput>
{
async handleMutation(
input: FieldDeleteMutationInput
): Promise<FieldDeleteMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
delete attributes.fields[input.fieldId];
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.FieldDeleteForbidden,
"You don't have permission to delete this field."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.FieldDeleteFailed,
'Something went wrong while deleting the field.'
);
}
return {
id: input.fieldId,
};
}
}

View File

@@ -1,53 +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 {
FieldNameUpdateMutationInput,
FieldNameUpdateMutationOutput,
} from '@colanode/client/mutations/databases/field-name-update';
import { DatabaseAttributes } from '@colanode/core';
export class FieldNameUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FieldNameUpdateMutationInput>
{
async handleMutation(
input: FieldNameUpdateMutationInput
): Promise<FieldNameUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
const field = attributes.fields[input.fieldId];
if (!field) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to update does not exist.'
);
}
field.name = input.name;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.FieldUpdateForbidden,
"You don't have permission to update this field."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.FieldUpdateFailed,
'Something went wrong while updating the field.'
);
}
return {
id: input.fieldId,
};
}
}

View File

@@ -1,83 +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 {
SelectOptionCreateMutationInput,
SelectOptionCreateMutationOutput,
} from '@colanode/client/mutations/databases/select-option-create';
import {
compareString,
DatabaseAttributes,
generateId,
generateFractionalIndex,
IdType,
} from '@colanode/core';
export class SelectOptionCreateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionCreateMutationInput>
{
async handleMutation(
input: SelectOptionCreateMutationInput
): Promise<SelectOptionCreateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const id = generateId(IdType.SelectOption);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
const field = attributes.fields[input.fieldId];
if (!field) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to create a select option in does not exist.'
);
}
if (field.type !== 'multi_select' && field.type !== 'select') {
throw new MutationError(
MutationErrorCode.FieldTypeInvalid,
'The field you are trying to create a select option in is not a "Select" or "Multi-Select" field.'
);
}
if (!field.options) {
field.options = {};
}
const maxIndex = Object.values(field.options)
.map((selectOption) => selectOption.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, null);
field.options[id] = {
name: input.name,
id: id,
color: input.color,
index: index,
};
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.SelectOptionCreateForbidden,
"You don't have permission to create a select option in this field."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.SelectOptionCreateFailed,
'Something went wrong while creating the select option.'
);
}
return {
id: id,
};
}
}

View File

@@ -1,75 +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 {
SelectOptionDeleteMutationInput,
SelectOptionDeleteMutationOutput,
} from '@colanode/client/mutations/databases/select-option-delete';
import { DatabaseAttributes } from '@colanode/core';
export class SelectOptionDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionDeleteMutationInput>
{
async handleMutation(
input: SelectOptionDeleteMutationInput
): Promise<SelectOptionDeleteMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
const field = attributes.fields[input.fieldId];
if (!field) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to delete a select option from does not exist.'
);
}
if (field.type !== 'multi_select' && field.type !== 'select') {
throw new MutationError(
MutationErrorCode.FieldTypeInvalid,
'The field you are trying to delete a select option from is not a "Select" or "Multi-Select" field.'
);
}
if (!field.options) {
throw new MutationError(
MutationErrorCode.SelectOptionNotFound,
'The field you are trying to delete a select option from does not have any select options.'
);
}
if (!field.options[input.optionId]) {
throw new MutationError(
MutationErrorCode.SelectOptionNotFound,
'The select option you are trying to delete does not exist.'
);
}
delete field.options[input.optionId];
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.SelectOptionDeleteForbidden,
"You don't have permission to delete this select option."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.SelectOptionDeleteFailed,
'Something went wrong while deleting the select option.'
);
}
return {
id: input.optionId,
};
}
}

View File

@@ -1,73 +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 {
SelectOptionUpdateMutationInput,
SelectOptionUpdateMutationOutput,
} from '@colanode/client/mutations/databases/select-option-update';
import { DatabaseAttributes } from '@colanode/core';
export class SelectOptionUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SelectOptionUpdateMutationInput>
{
async handleMutation(
input: SelectOptionUpdateMutationInput
): Promise<SelectOptionUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseAttributes>(
input.databaseId,
(attributes) => {
const field = attributes.fields[input.fieldId];
if (!field) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to update a select option in does not exist.'
);
}
if (field.type !== 'multi_select' && field.type !== 'select') {
throw new MutationError(
MutationErrorCode.FieldTypeInvalid,
'The field you are trying to update a select option in is not a "Select" or "Multi-Select" field.'
);
}
if (!field.options) {
field.options = {};
}
const option = field.options[input.optionId];
if (!option) {
throw new MutationError(
MutationErrorCode.SelectOptionNotFound,
'The select option you are trying to update does not exist.'
);
}
option.name = input.name;
option.color = input.color;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.SelectOptionUpdateForbidden,
"You don't have permission to update this select option."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.SelectOptionUpdateFailed,
'Something went wrong while updating the select option.'
);
}
return {
id: input.optionId,
};
}
}

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

@@ -1,44 +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 {
ViewUpdateMutationInput,
ViewUpdateMutationOutput,
} from '@colanode/client/mutations/databases/view-update';
import { DatabaseViewAttributes } from '@colanode/core';
export class ViewUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<ViewUpdateMutationInput>
{
async handleMutation(
input: ViewUpdateMutationInput
): Promise<ViewUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<DatabaseViewAttributes>(
input.viewId,
() => {
return input.view;
}
);
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

@@ -1,48 +0,0 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import {
MutationError,
MutationErrorCode,
FolderUpdateMutationInput,
FolderUpdateMutationOutput,
} from '@colanode/client/mutations';
import { FolderAttributes } from '@colanode/core';
export class FolderUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<FolderUpdateMutationInput>
{
async handleMutation(
input: FolderUpdateMutationInput
): Promise<FolderUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<FolderAttributes>(
input.folderId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.FolderUpdateForbidden,
"You don't have permission to update this folder."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.FolderUpdateFailed,
'There was an error while updating the folder. Please try again.'
);
}
return {
success: true,
};
}
}

View File

@@ -16,23 +16,11 @@ import { TabCreateMutationHandler } from './apps/tab-create';
import { TabDeleteMutationHandler } from './apps/tab-delete';
import { TabUpdateMutationHandler } from './apps/tab-update';
import { AvatarUploadMutationHandler } from './avatars/avatar-upload';
import { ChannelUpdateMutationHandler } from './channels/channel-update';
import { DatabaseNameFieldUpdateMutationHandler } from './databases/database-name-field-update';
import { DatabaseUpdateMutationHandler } from './databases/database-update';
import { FieldCreateMutationHandler } from './databases/field-create';
import { FieldDeleteMutationHandler } from './databases/field-delete';
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 { ViewNameUpdateMutationHandler } from './databases/view-name-update';
import { ViewUpdateMutationHandler } from './databases/view-update';
import { DocumentUpdateMutationHandler } from './documents/document-update';
import { FileCreateMutationHandler } from './files/file-create';
import { FileDeleteMutationHandler } from './files/file-delete';
import { FileDownloadMutationHandler } from './files/file-download';
import { TempFileCreateMutationHandler } from './files/temp-file-create';
import { FolderUpdateMutationHandler } from './folders/folder-update';
import { NodeCollaboratorCreateMutationHandler } from './nodes/node-collaborator-create';
import { NodeCollaboratorDeleteMutationHandler } from './nodes/node-collaborator-delete';
import { NodeCollaboratorUpdateMutationHandler } from './nodes/node-collaborator-update';
@@ -42,15 +30,9 @@ import { NodeInteractionOpenedMutationHandler } from './nodes/node-interaction-o
import { NodeInteractionSeenMutationHandler } from './nodes/node-interaction-seen';
import { NodeReactionCreateMutationHandler } from './nodes/node-reaction-create';
import { NodeReactionDeleteMutationHandler } from './nodes/node-reaction-delete';
import { PageUpdateMutationHandler } from './pages/page-update';
import { RecordAvatarUpdateMutationHandler } from './records/record-avatar-update';
import { RecordFieldValueDeleteMutationHandler } from './records/record-field-value-delete';
import { RecordFieldValueSetMutationHandler } from './records/record-field-value-set';
import { RecordNameUpdateMutationHandler } from './records/record-name-update';
import { NodeUpdateMutationHandler } from './nodes/node-update';
import { ServerCreateMutationHandler } from './servers/server-create';
import { ServerDeleteMutationHandler } from './servers/server-delete';
import { SpaceChildReorderMutationHandler } from './spaces/space-child-reorder';
import { SpaceUpdateMutationHandler } from './spaces/space-update';
import { UserRoleUpdateMutationHandler } from './users/user-role-update';
import { UserStorageUpdateMutationHandler } from './users/user-storage-update';
import { UsersCreateMutationHandler } from './users/users-create';
@@ -71,12 +53,6 @@ export const buildMutationHandlerMap = (
'email.register': new EmailRegisterMutationHandler(app),
'email.verify': new EmailVerifyMutationHandler(app),
'google.login': new GoogleLoginMutationHandler(app),
'database.name.field.update': new DatabaseNameFieldUpdateMutationHandler(
app
),
'field.create': new FieldCreateMutationHandler(app),
'field.delete': new FieldDeleteMutationHandler(app),
'field.name.update': new FieldNameUpdateMutationHandler(app),
'file.delete': new FileDeleteMutationHandler(app),
'node.collaborator.create': new NodeCollaboratorCreateMutationHandler(app),
'node.collaborator.delete': new NodeCollaboratorDeleteMutationHandler(app),
@@ -85,13 +61,6 @@ export const buildMutationHandlerMap = (
'node.interaction.seen': new NodeInteractionSeenMutationHandler(app),
'node.reaction.create': new NodeReactionCreateMutationHandler(app),
'node.reaction.delete': new NodeReactionDeleteMutationHandler(app),
'record.avatar.update': new RecordAvatarUpdateMutationHandler(app),
'record.name.update': new RecordNameUpdateMutationHandler(app),
'record.field.value.delete': new RecordFieldValueDeleteMutationHandler(app),
'record.field.value.set': new RecordFieldValueSetMutationHandler(app),
'select.option.create': new SelectOptionCreateMutationHandler(app),
'select.option.delete': new SelectOptionDeleteMutationHandler(app),
'select.option.update': new SelectOptionUpdateMutationHandler(app),
'server.create': new ServerCreateMutationHandler(app),
'server.delete': new ServerDeleteMutationHandler(app),
'user.role.update': new UserRoleUpdateMutationHandler(app),
@@ -102,15 +71,7 @@ export const buildMutationHandlerMap = (
'account.logout': new AccountLogoutMutationHandler(app),
'file.create': new FileCreateMutationHandler(app),
'file.download': new FileDownloadMutationHandler(app),
'space.update': new SpaceUpdateMutationHandler(app),
'space.child.reorder': new SpaceChildReorderMutationHandler(app),
'account.update': new AccountUpdateMutationHandler(app),
'view.update': new ViewUpdateMutationHandler(app),
'view.name.update': new ViewNameUpdateMutationHandler(app),
'channel.update': new ChannelUpdateMutationHandler(app),
'page.update': new PageUpdateMutationHandler(app),
'folder.update': new FolderUpdateMutationHandler(app),
'database.update': new DatabaseUpdateMutationHandler(app),
'document.update': new DocumentUpdateMutationHandler(app),
'metadata.update': new MetadataUpdateMutationHandler(app),
'metadata.delete': new MetadataDeleteMutationHandler(app),
@@ -124,5 +85,6 @@ export const buildMutationHandlerMap = (
'tab.update': new TabUpdateMutationHandler(app),
'tab.delete': new TabDeleteMutationHandler(app),
'node.delete': new NodeDeleteMutationHandler(app),
'node.update': new NodeUpdateMutationHandler(app),
};
};

View File

@@ -0,0 +1,26 @@
import { merge } from 'lodash-es';
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import {
NodeUpdateMutationInput,
NodeUpdateMutationOutput,
} from '@colanode/client/mutations/nodes/node-update';
export class NodeUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<NodeUpdateMutationInput>
{
async handleMutation(
input: NodeUpdateMutationInput
): Promise<NodeUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
await workspace.nodes.updateNode(input.nodeId, (attributes) => {
return merge(attributes, input.attributes);
});
return {
success: true,
};
}
}

View File

@@ -1,47 +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 {
PageUpdateMutationInput,
PageUpdateMutationOutput,
} from '@colanode/client/mutations/pages/page-update';
import { PageAttributes } from '@colanode/core';
export class PageUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<PageUpdateMutationInput>
{
async handleMutation(
input: PageUpdateMutationInput
): Promise<PageUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<PageAttributes>(
input.pageId,
(attributes) => {
attributes.name = input.name;
attributes.avatar = input.avatar;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.PageUpdateForbidden,
"You don't have permission to update this page."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.PageUpdateFailed,
'Something went wrong while updating the page. Please try again later.'
);
}
return {
success: true,
};
}
}

View File

@@ -1,38 +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 {
RecordAvatarUpdateMutationInput,
RecordAvatarUpdateMutationOutput,
} from '@colanode/client/mutations/records/record-avatar-update';
import { RecordAttributes } from '@colanode/core';
export class RecordAvatarUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordAvatarUpdateMutationInput>
{
async handleMutation(
input: RecordAvatarUpdateMutationInput
): Promise<RecordAvatarUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<RecordAttributes>(
input.recordId,
(attributes) => {
attributes.avatar = input.avatar;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.RecordUpdateForbidden,
"You don't have permission to update this record."
);
}
return {
success: true,
};
}
}

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 { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
RecordFieldValueDeleteMutationInput,
RecordFieldValueDeleteMutationOutput,
} from '@colanode/client/mutations/records/record-field-value-delete';
import { RecordAttributes } from '@colanode/core';
export class RecordFieldValueDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordFieldValueDeleteMutationInput>
{
async handleMutation(
input: RecordFieldValueDeleteMutationInput
): Promise<RecordFieldValueDeleteMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<RecordAttributes>(
input.recordId,
(attributes) => {
delete attributes.fields[input.fieldId];
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.RecordUpdateForbidden,
"You don't have permission to delete this field value."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.RecordUpdateFailed,
'Something went wrong while deleting the field value. Please try again later.'
);
}
return {
success: true,
};
}
}

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 { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
RecordFieldValueSetMutationInput,
RecordFieldValueSetMutationOutput,
} from '@colanode/client/mutations/records/record-field-value-set';
import { RecordAttributes } from '@colanode/core';
export class RecordFieldValueSetMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordFieldValueSetMutationInput>
{
async handleMutation(
input: RecordFieldValueSetMutationInput
): Promise<RecordFieldValueSetMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<RecordAttributes>(
input.recordId,
(attributes) => {
attributes.fields[input.fieldId] = input.value;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.RecordUpdateForbidden,
"You don't have permission to set this field value."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.RecordUpdateFailed,
'Something went wrong while setting the field value.'
);
}
return {
success: true,
};
}
}

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 { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
RecordNameUpdateMutationInput,
RecordNameUpdateMutationOutput,
} from '@colanode/client/mutations/records/record-name-update';
import { RecordAttributes } from '@colanode/core';
export class RecordNameUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<RecordNameUpdateMutationInput>
{
async handleMutation(
input: RecordNameUpdateMutationInput
): Promise<RecordNameUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<RecordAttributes>(
input.recordId,
(attributes) => {
attributes.name = input.name;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.RecordUpdateForbidden,
"You don't have permission to update this record."
);
}
if (result !== 'success') {
throw new MutationError(
MutationErrorCode.RecordUpdateFailed,
'Something went wrong while updating the record name. Please try again later.'
);
}
return {
success: true,
};
}
}

View File

@@ -1,168 +0,0 @@
import { SelectNode } from '@colanode/client/databases';
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { MutationHandler } from '@colanode/client/lib/types';
import {
MutationError,
MutationErrorCode,
SpaceChildReorderMutationInput,
SpaceChildReorderMutationOutput,
} from '@colanode/client/mutations';
import {
compareString,
generateFractionalIndex,
SpaceAttributes,
} from '@colanode/core';
interface NodeFractionalIndex {
id: string;
defaultIndex: string;
customIndex: string | null;
}
export class SpaceChildReorderMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SpaceChildReorderMutationInput>
{
async handleMutation(
input: SpaceChildReorderMutationInput
): Promise<SpaceChildReorderMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const children = await workspace.database
.selectFrom('nodes')
.where('parent_id', '=', input.spaceId)
.orderBy('id')
.selectAll()
.execute();
if (children.length === 0) {
throw new MutationError(
MutationErrorCode.SpaceUpdateFailed,
'Space has no children.'
);
}
const result = await workspace.nodes.updateNode<SpaceAttributes>(
input.spaceId,
(attributes) => {
const newIndex = this.generateSpaceChildIndex(
attributes,
children,
input.childId,
input.after
);
if (!newIndex) {
throw new MutationError(
MutationErrorCode.SpaceUpdateFailed,
'Failed to generate new index.'
);
}
const childrenSettings = attributes.children ?? {};
childrenSettings[input.childId] = {
...(childrenSettings[input.childId] ?? {}),
id: input.childId,
index: newIndex,
};
attributes.children = childrenSettings;
return attributes;
}
);
if (result === 'unauthorized') {
throw new MutationError(
MutationErrorCode.SpaceUpdateForbidden,
"You don't have permission to update this space."
);
}
return {
success: true,
};
}
private generateSpaceChildIndex(
attributes: SpaceAttributes,
children: SelectNode[],
childId: string,
after: string | null
): string | null {
const child = children.find((c) => c.id === childId);
if (!child) {
return null;
}
const sortedById = children.toSorted((a, b) => compareString(a.id, b.id));
const indexes: NodeFractionalIndex[] = [];
const childrenSettings = attributes.children ?? {};
let lastIndex: string | null = null;
for (const child of sortedById) {
lastIndex = generateFractionalIndex(lastIndex, null);
indexes.push({
id: child.id,
defaultIndex: lastIndex,
customIndex: childrenSettings[child.id]?.index ?? null,
});
}
const sortedIndexes = indexes.sort((a, b) =>
compareString(
a.customIndex ?? a.defaultIndex,
b.customIndex ?? b.defaultIndex
)
);
if (after === null) {
const firstIndex = sortedIndexes[0];
if (!firstIndex) {
return generateFractionalIndex(null, null);
}
const nextIndex = firstIndex.customIndex ?? firstIndex.defaultIndex;
return generateFractionalIndex(null, nextIndex);
}
const afterNodeIndex = sortedIndexes.findIndex((node) => node.id === after);
if (afterNodeIndex === -1) {
return null;
}
const afterNode = sortedIndexes[afterNodeIndex];
if (!afterNode) {
return null;
}
const previousIndex = afterNode.customIndex ?? afterNode.defaultIndex;
let nextIndex: string | null = null;
if (afterNodeIndex < sortedIndexes.length - 1) {
const nextNode = sortedIndexes[afterNodeIndex + 1];
if (!nextNode) {
return null;
}
nextIndex = nextNode.customIndex ?? nextNode.defaultIndex;
}
let newIndex = generateFractionalIndex(previousIndex, nextIndex);
const maxDefaultIndex = sortedIndexes
.map((index) => index.defaultIndex)
.sort((a, b) => -compareString(a, b))[0]!;
const newPotentialDefaultIndex = generateFractionalIndex(
maxDefaultIndex,
null
);
if (newPotentialDefaultIndex === newIndex) {
newIndex = generateFractionalIndex(
previousIndex,
newPotentialDefaultIndex
);
}
return newIndex;
}
}

View File

@@ -1,40 +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 {
SpaceUpdateMutationInput,
SpaceUpdateMutationOutput,
} from '@colanode/client/mutations/spaces/space-update';
import { SpaceAttributes } from '@colanode/core';
export class SpaceUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<SpaceUpdateMutationInput>
{
async handleMutation(
input: SpaceUpdateMutationInput
): Promise<SpaceUpdateMutationOutput> {
const workspace = this.getWorkspace(input.userId);
const result = await workspace.nodes.updateNode<SpaceAttributes>(
input.spaceId,
(attributes) => {
attributes.name = input.name;
attributes.description = input.description;
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,
};
}
}

View File

@@ -25,7 +25,6 @@ import { IconListQueryHandler } from './icons/icon-list';
import { IconSearchQueryHandler } from './icons/icon-search';
import { IconSvgGetQueryHandler } from './icons/icon-svg-get';
import { RadarDataGetQueryHandler } from './interactions/radar-data-get';
import { NodeChildrenGetQueryHandler } from './nodes/node-children-get';
import { NodeGetQueryHandler } from './nodes/node-get';
import { NodeListQueryHandler } from './nodes/node-list';
import { NodeReactionsListQueryHandler } from './nodes/node-reaction-list';
@@ -68,7 +67,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'icon.list': new IconListQueryHandler(app),
'icon.search': new IconSearchQueryHandler(app),
'icon.category.list': new IconCategoryListQueryHandler(app),
'node.children.get': new NodeChildrenGetQueryHandler(app),
'radar.data.get': new RadarDataGetQueryHandler(app),
'record.search': new RecordSearchQueryHandler(app),
'user.storage.get': new UserStorageGetQueryHandler(app),

View File

@@ -1,102 +0,0 @@
import { SelectNode } from '@colanode/client/databases/workspace';
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import { mapNode } from '@colanode/client/lib';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { NodeChildrenGetQueryInput } from '@colanode/client/queries/nodes/node-children-get';
import { Event } from '@colanode/client/types/events';
import { LocalNode } from '@colanode/client/types/nodes';
export class NodeChildrenGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<NodeChildrenGetQueryInput>
{
public async handleQuery(
input: NodeChildrenGetQueryInput
): Promise<LocalNode[]> {
const rows = await this.fetchChildren(input);
return rows.map(mapNode) as LocalNode[];
}
public async checkForChanges(
event: Event,
input: NodeChildrenGetQueryInput,
output: LocalNode[]
): Promise<ChangeCheckResult<NodeChildrenGetQueryInput>> {
if (
event.type === 'workspace.deleted' &&
event.workspace.userId === input.userId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'node.created' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const newChildren = [...output, event.node];
return {
hasChanges: true,
result: newChildren,
};
}
if (
event.type === 'node.updated' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const node = output.find((n) => n.id === event.node.id);
if (node) {
const newChildren = output.map((node) =>
node.id === event.node.id ? event.node : node
);
return {
hasChanges: true,
result: newChildren,
};
}
}
if (
event.type === 'node.deleted' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const node = output.find((n) => n.id === event.node.id);
if (node) {
const newChildren = output.filter((n) => n.id !== event.node.id);
return {
hasChanges: true,
result: newChildren,
};
}
}
return {
hasChanges: false,
};
}
private async fetchChildren(
input: NodeChildrenGetQueryInput
): Promise<SelectNode[]> {
const workspace = this.getWorkspace(input.userId);
const rows = await workspace.database
.selectFrom('nodes')
.selectAll()
.where('parent_id', '=', input.nodeId)
.where('type', 'in', input.types ?? [])
.execute();
return rows;
}
}

View File

@@ -1,20 +0,0 @@
export type ChannelUpdateMutationInput = {
type: 'channel.update';
userId: string;
channelId: string;
name: string;
avatar?: string | null;
};
export type ChannelUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'channel.update': {
input: ChannelUpdateMutationInput;
output: ChannelUpdateMutationOutput;
};
}
}

View File

@@ -1,19 +0,0 @@
export type DatabaseNameFieldUpdateMutationInput = {
type: 'database.name.field.update';
userId: string;
databaseId: string;
name: string;
};
export type DatabaseNameFieldUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'database.name.field.update': {
input: DatabaseNameFieldUpdateMutationInput;
output: DatabaseNameFieldUpdateMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type DatabaseUpdateMutationInput = {
type: 'database.update';
userId: string;
databaseId: string;
name: string;
avatar?: string | null;
};
export type DatabaseUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'database.update': {
input: DatabaseUpdateMutationInput;
output: DatabaseUpdateMutationOutput;
};
}
}

View File

@@ -1,23 +0,0 @@
import { FieldType } from '@colanode/core';
export type FieldCreateMutationInput = {
type: 'field.create';
userId: string;
databaseId: string;
name: string;
fieldType: FieldType;
relationDatabaseId?: string | null;
};
export type FieldCreateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'field.create': {
input: FieldCreateMutationInput;
output: FieldCreateMutationOutput;
};
}
}

View File

@@ -1,19 +0,0 @@
export type FieldDeleteMutationInput = {
type: 'field.delete';
userId: string;
databaseId: string;
fieldId: string;
};
export type FieldDeleteMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'field.delete': {
input: FieldDeleteMutationInput;
output: FieldDeleteMutationOutput;
};
}
}

View File

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

View File

@@ -1,21 +0,0 @@
export type SelectOptionCreateMutationInput = {
type: 'select.option.create';
userId: string;
databaseId: string;
fieldId: string;
name: string;
color: string;
};
export type SelectOptionCreateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'select.option.create': {
input: SelectOptionCreateMutationInput;
output: SelectOptionCreateMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type SelectOptionDeleteMutationInput = {
type: 'select.option.delete';
userId: string;
databaseId: string;
fieldId: string;
optionId: string;
};
export type SelectOptionDeleteMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'select.option.delete': {
input: SelectOptionDeleteMutationInput;
output: SelectOptionDeleteMutationOutput;
};
}
}

View File

@@ -1,22 +0,0 @@
export type SelectOptionUpdateMutationInput = {
type: 'select.option.update';
userId: string;
databaseId: string;
fieldId: string;
optionId: string;
name: string;
color: string;
};
export type SelectOptionUpdateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'select.option.update': {
input: SelectOptionUpdateMutationInput;
output: SelectOptionUpdateMutationOutput;
};
}
}

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

@@ -1,21 +0,0 @@
import { DatabaseViewAttributes } from '@colanode/core';
export type ViewUpdateMutationInput = {
type: 'view.update';
userId: string;
viewId: string;
view: DatabaseViewAttributes;
};
export type ViewUpdateMutationOutput = {
id: string;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'view.update': {
input: ViewUpdateMutationInput;
output: ViewUpdateMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type FolderUpdateMutationInput = {
type: 'folder.update';
userId: string;
folderId: string;
name: string;
avatar?: string | null;
};
export type FolderUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'folder.update': {
input: FolderUpdateMutationInput;
output: FolderUpdateMutationOutput;
};
}
}

View File

@@ -9,22 +9,10 @@ export * from './accounts/google-login';
export * from './apps/metadata-delete';
export * from './apps/metadata-update';
export * from './avatars/avatar-upload';
export * from './channels/channel-update';
export * from './databases/database-update';
export * from './databases/field-create';
export * from './databases/field-delete';
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-name-update';
export * from './databases/view-update';
export * from './databases/database-name-field-update';
export * from './documents/document-update';
export * from './files/file-create';
export * from './files/file-delete';
export * from './files/file-download';
export * from './folders/folder-update';
export * from './nodes/node-collaborator-create';
export * from './nodes/node-collaborator-delete';
export * from './nodes/node-collaborator-update';
@@ -32,15 +20,8 @@ export * from './nodes/node-interaction-opened';
export * from './nodes/node-interaction-seen';
export * from './nodes/node-reaction-create';
export * from './nodes/node-reaction-delete';
export * from './pages/page-update';
export * from './records/record-avatar-update';
export * from './records/record-field-value-delete';
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-update';
export * from './spaces/space-child-reorder';
export * from './workspaces/workspace-create';
export * from './workspaces/workspace-delete';
export * from './workspaces/workspace-update';
@@ -53,6 +34,7 @@ export * from './apps/tab-update';
export * from './apps/tab-delete';
export * from './nodes/node-create';
export * from './nodes/node-delete';
export * from './nodes/node-update';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface MutationMap {}

View File

@@ -0,0 +1,21 @@
import { NodeAttributes } from '@colanode/core';
export type NodeUpdateMutationInput = {
type: 'node.update';
userId: string;
nodeId: string;
attributes: Partial<NodeAttributes>;
};
export type NodeUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'node.update': {
input: NodeUpdateMutationInput;
output: NodeUpdateMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type PageUpdateMutationInput = {
type: 'page.update';
userId: string;
pageId: string;
avatar?: string | null;
name: string;
};
export type PageUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'page.update': {
input: PageUpdateMutationInput;
output: PageUpdateMutationOutput;
};
}
}

View File

@@ -1,19 +0,0 @@
export type RecordAvatarUpdateMutationInput = {
type: 'record.avatar.update';
userId: string;
recordId: string;
avatar: string;
};
export type RecordAvatarUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'record.avatar.update': {
input: RecordAvatarUpdateMutationInput;
output: RecordAvatarUpdateMutationOutput;
};
}
}

View File

@@ -1,19 +0,0 @@
export type RecordFieldValueDeleteMutationInput = {
type: 'record.field.value.delete';
userId: string;
recordId: string;
fieldId: string;
};
export type RecordFieldValueDeleteMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'record.field.value.delete': {
input: RecordFieldValueDeleteMutationInput;
output: RecordFieldValueDeleteMutationOutput;
};
}
}

View File

@@ -1,22 +0,0 @@
import { FieldValue } from '@colanode/core';
export type RecordFieldValueSetMutationInput = {
type: 'record.field.value.set';
userId: string;
recordId: string;
fieldId: string;
value: FieldValue;
};
export type RecordFieldValueSetMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'record.field.value.set': {
input: RecordFieldValueSetMutationInput;
output: RecordFieldValueSetMutationOutput;
};
}
}

View File

@@ -1,19 +0,0 @@
export type RecordNameUpdateMutationInput = {
type: 'record.name.update';
userId: string;
recordId: string;
name: string;
};
export type RecordNameUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'record.name.update': {
input: RecordNameUpdateMutationInput;
output: RecordNameUpdateMutationOutput;
};
}
}

View File

@@ -1,20 +0,0 @@
export type SpaceChildReorderMutationInput = {
type: 'space.child.reorder';
userId: string;
spaceId: string;
childId: string;
after: string | null;
};
export type SpaceChildReorderMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'space.child.reorder': {
input: SpaceChildReorderMutationInput;
output: SpaceChildReorderMutationOutput;
};
}
}

View File

@@ -1,21 +0,0 @@
export type SpaceUpdateMutationInput = {
type: 'space.update';
userId: string;
spaceId: string;
name: string;
description: string;
avatar?: string | null;
};
export type SpaceUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'space.update': {
input: SpaceUpdateMutationInput;
output: SpaceUpdateMutationOutput;
};
}
}

View File

@@ -16,7 +16,6 @@ export * from './icons/icon-category-list';
export * from './icons/icon-list';
export * from './icons/icon-search';
export * from './interactions/radar-data-get';
export * from './nodes/node-children-get';
export * from './nodes/node-get';
export * from './nodes/node-reaction-list';
export * from './nodes/node-reactions-aggregate';

View File

@@ -1,18 +0,0 @@
import { LocalNode } from '@colanode/client/types/nodes';
import { NodeType } from '@colanode/core';
export type NodeChildrenGetQueryInput = {
type: 'node.children.get';
nodeId: string;
userId: string;
types?: NodeType[];
};
declare module '@colanode/client/queries' {
interface QueryMap {
'node.children.get': {
input: NodeChildrenGetQueryInput;
output: LocalNode[];
};
}
}

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalChannelNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { ChannelForm } from '@colanode/ui/components/channels/channel-form';
@@ -11,7 +9,7 @@ import {
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database } from '@colanode/ui/data';
interface ChannelUpdateDialogProps {
channel: LocalChannelNode;
@@ -27,7 +25,6 @@ export const ChannelUpdateDialog = ({
onOpenChange,
}: ChannelUpdateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'editor');
return (
@@ -45,33 +42,28 @@ export const ChannelUpdateDialog = ({
name: channel.attributes.name,
avatar: channel.attributes.avatar,
}}
isPending={isPending}
isPending={false}
submitText="Update"
readOnly={!canEdit}
onCancel={() => {
onOpenChange(false);
}}
onSubmit={(values) => {
if (isPending) {
const nodes = database.workspace(workspace.userId).nodes;
if (!nodes.has(channel.id)) {
return;
}
mutate({
input: {
type: 'channel.update',
channelId: channel.id,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
toast.success('Channel updated');
},
onError(error) {
toast.error(error.message);
},
nodes.update(channel.id, (draft) => {
if (draft.attributes.type !== 'channel') {
return;
}
draft.attributes.name = values.name;
draft.attributes.avatar = values.avatar;
});
onOpenChange(false);
}}
/>
</DialogContent>

View File

@@ -1,6 +1,5 @@
import { eq, useLiveQuery as useLiveQueryTanstack } from '@tanstack/react-db';
import { CircleAlert, CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import {
CollaboratorFieldAttributes,
@@ -13,7 +12,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database } from '@colanode/ui/data';
import { database as appDatabase } from '@colanode/ui/data';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface BoardViewColumnsCollaboratorProps {
@@ -81,17 +80,19 @@ export const BoardViewColumnsCollaborator = ({
),
canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => {
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
if (value.type !== 'string_array') {
return;
@@ -114,17 +115,13 @@ export const BoardViewColumnsCollaborator = ({
};
}
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value: newValue,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = newValue;
});
}
},
}}
@@ -150,29 +147,27 @@ export const BoardViewColumnsCollaborator = ({
),
canDrag: () => true,
onDragEnd: async (record, value) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = value;
});
}
},
}}
@@ -198,7 +193,7 @@ const BoardViewColumnCollaboratorHeader = ({
const userQuery = useLiveQueryTanstack((q) =>
q
.from({ users: database.workspace(workspace.userId).users })
.from({ users: appDatabase.workspace(workspace.userId).users })
.where(({ users }) => eq(users.id, collaborator))
.select(({ users }) => ({
id: users.id,

View File

@@ -1,5 +1,4 @@
import { CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import {
DatabaseViewFilterAttributes,
@@ -13,6 +12,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
@@ -90,17 +90,19 @@ export const BoardViewColumnsMultiSelect = ({
),
canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => {
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
if (value.type !== 'string_array') {
return;
@@ -122,17 +124,13 @@ export const BoardViewColumnsMultiSelect = ({
};
}
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value: newValue,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = newValue;
});
}
},
}}
@@ -159,29 +157,27 @@ export const BoardViewColumnsMultiSelect = ({
dragOverClass: noValueDraggingClass,
canDrag: () => true,
onDragEnd: async (record, value) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = value;
});
}
},
}}

View File

@@ -1,5 +1,4 @@
import { CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import {
DatabaseViewFilterAttributes,
@@ -12,6 +11,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
@@ -89,29 +89,27 @@ export const BoardViewColumnsSelect = ({
),
canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = value;
});
}
},
}}
@@ -138,29 +136,27 @@ export const BoardViewColumnsSelect = ({
dragOverClass: noValueDraggingClass,
canDrag: () => true,
onDragEnd: async (record, value) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
if (!value) {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
} else {
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = value;
});
}
},
}}

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalDatabaseNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { DatabaseForm } from '@colanode/ui/components/databases/database-form';
@@ -11,7 +9,7 @@ import {
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database as appDatabase } from '@colanode/ui/data';
interface DatabaseUpdateDialogProps {
database: LocalDatabaseNode;
@@ -27,7 +25,7 @@ export const DatabaseUpdateDialog = ({
onOpenChange,
}: DatabaseUpdateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'editor');
return (
@@ -45,33 +43,28 @@ export const DatabaseUpdateDialog = ({
name: database.attributes.name,
avatar: database.attributes.avatar,
}}
isPending={isPending}
isPending={false}
submitText="Update"
readOnly={!canEdit}
onCancel={() => {
onOpenChange(false);
}}
onSubmit={(values) => {
if (isPending) {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return;
}
mutate({
input: {
type: 'database.update',
databaseId: database.id,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
toast.success('Database updated');
},
onError(error) {
toast.error(error.message);
},
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
draft.attributes.name = values.name;
draft.attributes.avatar = values.avatar;
});
onOpenChange(false);
}}
/>
</DialogContent>

View File

@@ -1,10 +1,8 @@
import { ReactNode } from 'react';
import { toast } from 'sonner';
import { LocalDatabaseNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { DatabaseContext } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
interface DatabaseProps {
database: LocalDatabaseNode;
@@ -13,7 +11,6 @@ interface DatabaseProps {
}
export const Database = ({ database, role, children }: DatabaseProps) => {
const workspace = useWorkspace();
const canEdit = hasNodeRole(role, 'editor');
const canCreateRecord = hasNodeRole(role, 'editor');
@@ -27,112 +24,6 @@ export const Database = ({ database, role, children }: DatabaseProps) => {
fields: Object.values(database.attributes.fields),
canEdit,
canCreateRecord,
createField: async (type, name) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'field.create',
databaseId: database.id,
name,
fieldType: type,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
renameField: async (id, name) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'field.name.update',
databaseId: database.id,
fieldId: id,
name,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
updateNameField: async (name) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'database.name.field.update',
databaseId: database.id,
name,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
deleteField: async (id) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'field.delete',
databaseId: database.id,
fieldId: id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
createSelectOption: async (fieldId, name, color) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'select.option.create',
databaseId: database.id,
fieldId,
name,
color,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
updateSelectOption: async (fieldId, attributes) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'select.option.update',
databaseId: database.id,
fieldId,
optionId: attributes.id,
name: attributes.name,
color: attributes.color,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
deleteSelectOption: async (fieldId, optionId) => {
if (!canEdit) return;
const result = await window.colanode.executeMutation({
type: 'select.option.delete',
databaseId: database.id,
fieldId,
optionId,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
}
},
}}
>
{children}

View File

@@ -1,10 +1,19 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod/v4';
import { FieldType } from '@colanode/core';
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
compareString,
FieldAttributes,
FieldType,
generateFractionalIndex,
generateId,
IdType,
} from '@colanode/core';
import { DatabaseSelect } from '@colanode/ui/components/databases/database-select';
import { FieldTypeSelect } from '@colanode/ui/components/databases/fields/field-type-select';
import { Button } from '@colanode/ui/components/ui/button';
@@ -22,10 +31,9 @@ import {
PopoverContent,
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
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 { database as appDatabase } from '@colanode/ui/data';
const formSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
@@ -50,6 +58,8 @@ const formSchema = z.object({
relationDatabaseId: z.string().optional().nullable(),
});
type FieldCreateFormValues = z.infer<typeof formSchema>;
interface FieldCreatePopoverProps {
button: React.ReactNode;
onSuccess?: (fieldId: string) => void;
@@ -65,8 +75,6 @@ export const FieldCreatePopover = ({
const workspace = useWorkspace();
const database = useDatabase();
const { mutate, isPending } = useMutation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -75,6 +83,72 @@ export const FieldCreatePopover = ({
},
});
const { mutate, isPending } = useMutation({
mutationFn: async (values: FieldCreateFormValues) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (values.type === 'relation') {
if (!values.relationDatabaseId) {
throw new MutationError(
MutationErrorCode.RelationDatabaseNotFound,
'Relation database not found.'
);
}
const relationDatabase = nodes.get(values.relationDatabaseId);
if (!relationDatabase || relationDatabase.type !== 'database') {
throw new MutationError(
MutationErrorCode.RelationDatabaseNotFound,
'Relation database not found.'
);
}
}
if (!nodes.has(database.id)) {
return null;
}
const fieldId = generateId(IdType.Field);
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const maxIndex = Object.values(draft.attributes.fields)
.map((field) => field.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, null);
const newField: FieldAttributes = {
id: fieldId,
type: values.type as FieldType,
name: values.name,
index,
};
if (newField.type === 'relation') {
newField.databaseId = values.relationDatabaseId;
}
draft.attributes.fields[fieldId] = newField;
});
return fieldId;
},
onSuccess: (fieldId) => {
form.reset();
setOpen(false);
if (fieldId) {
onSuccess?.(fieldId);
}
},
onError: (error) => {
toast.error(error.message as string);
},
});
const type = form.watch('type');
const handleCancelClick = () => {
@@ -82,27 +156,6 @@ export const FieldCreatePopover = ({
form.reset();
};
const handleSubmit = (values: z.infer<typeof formSchema>) => {
mutate({
input: {
type: 'field.create',
databaseId: database.id,
name: values.name,
fieldType: values.type,
userId: workspace.userId,
relationDatabaseId: values.relationDatabaseId,
},
onSuccess: (output) => {
setOpen(false);
form.reset();
onSuccess?.(output.id);
},
onError(error) {
toast.error(error.message);
},
});
};
if (!database.canEdit) {
return null;
}
@@ -114,7 +167,7 @@ export const FieldCreatePopover = ({
<Form {...form}>
<form
className="flex flex-col gap-2"
onSubmit={form.handleSubmit(handleSubmit)}
onSubmit={form.handleSubmit((values) => mutate(values))}
>
<div className="flex-grow space-y-4 py-2 pb-4">
<FormField
@@ -170,11 +223,11 @@ export const FieldCreatePopover = ({
variant="outline"
size="sm"
onClick={handleCancelClick}
disabled={isPending}
>
Cancel
</Button>
<Button type="submit" size="sm" disabled={isPending}>
{isPending && <Spinner className="mr-1" />}
Create
</Button>
</div>

View File

@@ -9,6 +9,8 @@ import {
} from '@colanode/ui/components/ui/alert-dialog';
import { Button } from '@colanode/ui/components/ui/button';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
interface FieldDeleteDialogProps {
id: string;
@@ -21,6 +23,7 @@ export const FieldDeleteDialog = ({
open,
onOpenChange,
}: FieldDeleteDialogProps) => {
const workspace = useWorkspace();
const database = useDatabase();
return (
@@ -39,8 +42,22 @@ export const FieldDeleteDialog = ({
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button
variant="destructive"
onClick={async () => {
database.deleteField(id);
onClick={() => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
console.error('Database not found');
return;
}
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
console.error('Database not found');
return;
}
delete draft.attributes.fields[id];
});
onOpenChange(false);
}}
>

View File

@@ -1,12 +1,15 @@
import { FieldAttributes } from '@colanode/core';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
interface FieldRenameInputProps {
field: FieldAttributes;
}
export const FieldRenameInput = ({ field }: FieldRenameInputProps) => {
const workspace = useWorkspace();
const database = useDatabase();
return (
@@ -16,7 +19,23 @@ export const FieldRenameInput = ({ field }: FieldRenameInputProps) => {
readOnly={!database.canEdit}
onChange={(newName) => {
if (newName === field.name) return;
database.renameField(field.id, newName);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return;
}
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const fieldDraft = draft.attributes.fields[field.id];
if (!fieldDraft) {
return;
}
fieldDraft.name = newName;
});
}}
/>
</div>

View File

@@ -1,8 +1,14 @@
import { useMutation } from '@tanstack/react-query';
import { Check, Plus, X } from 'lucide-react';
import { Fragment, useState } from 'react';
import { toast } from 'sonner';
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
compareString,
generateFractionalIndex,
generateId,
IdType,
MultiSelectFieldAttributes,
SelectFieldAttributes,
} from '@colanode/core';
@@ -18,7 +24,7 @@ import {
} from '@colanode/ui/components/ui/command';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database as appDatabase } from '@colanode/ui/data';
import { getRandomSelectOptionColor } from '@colanode/ui/lib/databases';
interface SelectFieldOptionsProps {
@@ -36,7 +42,6 @@ export const SelectFieldOptions = ({
}: SelectFieldOptionsProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const { mutate, isPending } = useMutation();
const selectOptions = Object.values(field.options ?? {});
@@ -47,6 +52,71 @@ export const SelectFieldOptions = ({
allowAdd &&
!selectOptions.some((option) => option.name === inputValue.trim());
const { mutate, isPending } = useMutation({
mutationFn: async ({ name, color }: { name: string; color: string }) => {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return null;
}
const selectOptionId = generateId(IdType.SelectOption);
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const fieldDraft = draft.attributes.fields[field.id];
if (!fieldDraft) {
throw new MutationError(
MutationErrorCode.FieldNotFound,
'The field you are trying to create a select option in does not exist.'
);
}
if (
fieldDraft.type !== 'multi_select' &&
fieldDraft.type !== 'select'
) {
throw new MutationError(
MutationErrorCode.FieldTypeInvalid,
'The field you are trying to create a select option in is not a "Select" or "Multi-Select" field.'
);
}
if (!fieldDraft.options) {
fieldDraft.options = {};
}
const maxIndex = Object.values(fieldDraft.options)
.map((selectOption) => selectOption.index)
.sort((a, b) => -compareString(a, b))[0];
const index = generateFractionalIndex(maxIndex, null);
fieldDraft.options[selectOptionId] = {
name: name,
id: selectOptionId,
color: color,
index: index,
};
});
return selectOptionId;
},
onSuccess: (selectOptionId) => {
if (!selectOptionId) {
return;
}
setInputValue('');
setColor(getRandomSelectOptionColor());
onSelect(selectOptionId);
},
onError: (error) => {
toast.error(error.message as string);
},
});
return (
<Command className="min-h-min">
<CommandInput
@@ -104,33 +174,9 @@ export const SelectFieldOptions = ({
key={inputValue.trim()}
value={inputValue.trim()}
onSelect={() => {
if (isPending) {
return;
}
if (inputValue.trim().length === 0) {
return;
}
mutate({
input: {
type: 'select.option.create',
databaseId: database.id,
fieldId: field.id,
name: inputValue.trim(),
color,
userId: workspace.userId,
},
onSuccess(output) {
setInputValue('');
setColor(getRandomSelectOptionColor());
onSelect(output.id);
},
onError(error) {
toast.error(error.message);
},
});
mutate({ name: inputValue.trim(), color });
}}
disabled={isPending}
className="flex flex-row items-center gap-2"
>
<span className="text-xs text-muted-foreground">Create</span>

View File

@@ -9,6 +9,8 @@ import {
} from '@colanode/ui/components/ui/alert-dialog';
import { Button } from '@colanode/ui/components/ui/button';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
interface SelectOptionDeleteDialogProps {
fieldId: string;
@@ -23,6 +25,7 @@ export const SelectOptionDeleteDialog = ({
open,
onOpenChange,
}: SelectOptionDeleteDialogProps) => {
const workspace = useWorkspace();
const database = useDatabase();
return (
@@ -42,7 +45,36 @@ export const SelectOptionDeleteDialog = ({
<Button
variant="destructive"
onClick={() => {
database.deleteSelectOption(fieldId, optionId);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return;
}
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const fieldDraft = draft.attributes.fields[fieldId];
if (!fieldDraft) {
return;
}
if (
fieldDraft.type !== 'select' &&
fieldDraft.type !== 'multi_select'
) {
return;
}
if (!fieldDraft.options) {
return;
}
delete fieldDraft.options[optionId];
});
onOpenChange(false);
}}
>
Delete

View File

@@ -12,6 +12,8 @@ import {
import { Separator } from '@colanode/ui/components/ui/separator';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
import { selectOptionColors } from '@colanode/ui/lib/databases';
import { cn } from '@colanode/ui/lib/utils';
@@ -25,6 +27,7 @@ export const SelectOptionSettingsPopover = ({
option,
}: SelectOptionSettingsPopoverProps) => {
const database = useDatabase();
const workspace = useWorkspace();
const [openSetttingsPopover, setOpenSetttingsPopover] = useState(false);
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
@@ -46,9 +49,38 @@ export const SelectOptionSettingsPopover = ({
onChange={(newName) => {
if (newName === option.name) return;
database.updateSelectOption(fieldId, {
...option,
name: newName,
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return;
}
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const fieldDraft = draft.attributes.fields[fieldId];
if (!fieldDraft) {
return;
}
if (
fieldDraft.type !== 'select' &&
fieldDraft.type !== 'multi_select'
) {
return;
}
if (!fieldDraft.options) {
return;
}
const optionDraft = fieldDraft.options[option.id];
if (!optionDraft) {
return;
}
optionDraft.name = newName;
});
}}
/>
@@ -61,9 +93,38 @@ export const SelectOptionSettingsPopover = ({
key={color.value}
className="flex cursor-pointer flex-row items-center gap-2 rounded-md p-1 hover:bg-accent"
onClick={() => {
database.updateSelectOption(fieldId, {
...option,
color: color.value,
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(database.id)) {
return;
}
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
const fieldDraft = draft.attributes.fields[fieldId];
if (!fieldDraft) {
return;
}
if (
fieldDraft.type !== 'select' &&
fieldDraft.type !== 'multi_select'
) {
return;
}
if (!fieldDraft.options) {
return;
}
const optionDraft = fieldDraft.options[option.id];
if (!optionDraft) {
return;
}
optionDraft.color = color.value;
});
}}
>

View File

@@ -1,13 +1,11 @@
import isHotkey from 'is-hotkey';
import { SquareArrowOutUpRight } from 'lucide-react';
import React, { Fragment } from 'react';
import { toast } from 'sonner';
import { RecordNode } from '@colanode/core';
import { Link } from '@colanode/ui/components/ui/link';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database as appDatabase } from '@colanode/ui/data';
interface NameEditorProps {
initialValue: string;
@@ -61,26 +59,23 @@ export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
const workspace = useWorkspace();
const [isEditing, setIsEditing] = React.useState(false);
const { mutate, isPending } = useMutation();
const canEdit = true;
const hasName = record.attributes.name && record.attributes.name.length > 0;
const handleSave = (newName: string) => {
if (newName === record.attributes.name) return;
mutate({
input: {
type: 'record.name.update',
name: newName,
recordId: record.id,
userId: workspace.userId,
},
onSuccess() {
setIsEditing(false);
},
onError(error) {
toast.error(error.message);
},
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.name = newName;
});
};
@@ -112,11 +107,6 @@ export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
>
<SquareArrowOutUpRight className="mr-1 size-4" /> <p>Open</p>
</Link>
{isPending && (
<span className="absolute right-2 text-muted-foreground">
<Spinner size="small" />
</span>
)}
</Fragment>
)}
</div>

View File

@@ -13,9 +13,12 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
import { cn } from '@colanode/ui/lib/utils';
export const TableViewNameHeader = () => {
const workspace = useWorkspace();
const database = useDatabase();
const view = useDatabaseView();
@@ -90,7 +93,18 @@ export const TableViewNameHeader = () => {
readOnly={!database.canEdit}
onChange={(newName) => {
if (newName === database.nameField?.name) return;
database.updateNameField(newName);
const nodes = appDatabase.workspace(workspace.userId).nodes;
nodes.update(database.id, (draft) => {
if (draft.attributes.type !== 'database') {
return;
}
draft.attributes.nameField = {
...draft.attributes.nameField,
name: newName,
};
});
}}
/>
</div>

View File

@@ -1,6 +1,5 @@
import { useNavigate } from '@tanstack/react-router';
import { useState } from 'react';
import { toast } from 'sonner';
import { match } from 'ts-pattern';
import {
@@ -80,36 +79,34 @@ export const View = ({ view }: ViewProps) => {
rename: async (name: string) => {
if (!database.canEdit) return;
const viewAttributes = { ...view.attributes };
viewAttributes.name = name;
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.name = name;
});
},
updateAvatar: async (avatar: string) => {
if (!database.canEdit) return;
const viewAttributes = { ...view.attributes };
viewAttributes.avatar = avatar;
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.avatar = avatar;
});
},
setFieldDisplay: async (id: string, display: boolean) => {
if (!database.canEdit) return;
@@ -117,27 +114,26 @@ export const View = ({ view }: ViewProps) => {
const viewField = view.attributes.fields?.[id];
if (viewField && viewField.display === display) return;
const viewAttributes = { ...view.attributes };
viewAttributes.fields = viewAttributes.fields ?? {};
if (!viewAttributes.fields[id]) {
viewAttributes.fields[id] = {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.fields = draft.attributes.fields ?? {};
if (!draft.attributes.fields[id]) {
draft.attributes.fields[id] = {
id: id,
display: display,
};
} else {
viewAttributes.fields[id].display = display;
draft.attributes.fields[id].display = display;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
}
},
resizeField: async (id: string, width: number) => {
if (!database.canEdit) {
@@ -149,27 +145,26 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.fields = viewAttributes.fields ?? {};
if (!viewAttributes.fields[id]) {
viewAttributes.fields[id] = {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.fields = draft.attributes.fields ?? {};
if (!draft.attributes.fields[id]) {
draft.attributes.fields[id] = {
id: id,
width: width,
};
} else {
viewAttributes.fields[id].width = width;
draft.attributes.fields[id].width = width;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
}
},
resizeName: async (width: number) => {
if (!database.canEdit) {
@@ -180,38 +175,36 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.nameWidth = width;
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.nameWidth = width;
});
},
setGroupBy: async (fieldId: string | null) => {
if (!database.canEdit) {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.groupBy = fieldId;
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.groupBy = fieldId;
});
},
moveField: async (id: string, after: string) => {
if (!database.canEdit) {
@@ -224,31 +217,31 @@ export const View = ({ view }: ViewProps) => {
id,
after
);
if (newIndex === null) {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.fields = viewAttributes.fields ?? {};
if (!viewAttributes.fields[id]) {
viewAttributes.fields[id] = {
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.fields = draft.attributes.fields ?? {};
if (!draft.attributes.fields[id]) {
draft.attributes.fields[id] = {
id: id,
index: newIndex,
};
} else {
viewAttributes.fields[id].index = newIndex;
draft.attributes.fields[id].index = newIndex;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
}
},
isFieldFilterOpened: (fieldId: string) =>
openedFieldFilters.includes(fieldId),
@@ -262,19 +255,28 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.filters = viewAttributes.filters ?? {};
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
const filterId = generateId(IdType.ViewFilter);
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.filters = draft.attributes.filters ?? {};
if (fieldId === SpecialId.Name) {
const operators = getFieldFilterOperators('text');
const filter: DatabaseViewFieldFilterAttributes = {
type: 'field',
id: fieldId,
id: filterId,
fieldId,
operator: operators[0]?.value ?? 'contains',
};
viewAttributes.filters[fieldId] = filter;
draft.attributes.filters[filterId] = filter;
} else {
const field = database.fields.find((f) => f.id === fieldId);
if (!field) {
@@ -284,26 +286,16 @@ export const View = ({ view }: ViewProps) => {
const operators = getFieldFilterOperators(field.type);
const filter: DatabaseViewFieldFilterAttributes = {
type: 'field',
id: fieldId,
id: filterId,
fieldId,
operator: operators[0]?.value ?? '',
};
viewAttributes.filters[fieldId] = filter;
draft.attributes.filters[filterId] = filter;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
} else {
setOpenedFieldFilters((prev) => [...prev, fieldId]);
}
setOpenedFieldFilters((prev) => [...prev, filterId]);
},
updateFilter: async (
id: string,
@@ -317,22 +309,22 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.filters = viewAttributes.filters ?? {};
viewAttributes.filters[id] = filter;
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
const filters = draft.attributes.filters ?? {};
filters[id] = filter;
draft.attributes.filters = filters;
});
if (!result.success) {
toast.error(result.error.message);
} else {
setIsSearchBarOpened(true);
}
},
removeFilter: async (id: string) => {
if (!database.canEdit) {
@@ -343,22 +335,28 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.filters = viewAttributes.filters ?? {};
delete viewAttributes.filters[id];
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
if (!draft.attributes) {
return;
}
if (!draft.attributes.filters?.[id]) {
return;
}
delete draft.attributes.filters[id];
});
if (!result.success) {
toast.error(result.error.message);
} else {
setIsSearchBarOpened(true);
}
},
initFieldSort: async (fieldId: string, direction: SortDirection) => {
if (!database.canEdit) {
@@ -370,8 +368,17 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.sorts = viewAttributes.sorts ?? {};
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.sorts = draft.attributes.sorts ?? {};
if (fieldId === SpecialId.Name) {
const sort: DatabaseViewSortAttributes = {
@@ -379,36 +386,23 @@ export const View = ({ view }: ViewProps) => {
fieldId,
direction,
};
viewAttributes.sorts[fieldId] = sort;
draft.attributes.sorts[fieldId] = sort;
} else {
const field = database.fields.find((f) => f.id === fieldId);
if (!field) {
return;
}
const sort: DatabaseViewSortAttributes = {
id: fieldId,
fieldId,
direction,
};
viewAttributes.sorts[fieldId] = sort;
draft.attributes.sorts[fieldId] = sort;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
});
if (!result.success) {
toast.error(result.error.message);
} else {
setIsSearchBarOpened(true);
setIsSortsOpened(true);
}
},
updateSort: async (id: string, sort: DatabaseViewSortAttributes) => {
if (!database.canEdit) {
@@ -419,23 +413,21 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.sorts = viewAttributes.sorts ?? {};
viewAttributes.sorts[id] = sort;
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.sorts = draft.attributes.sorts ?? {};
draft.attributes.sorts[id] = sort;
});
if (!result.success) {
toast.error(result.error.message);
} else {
setIsSearchBarOpened(true);
setIsSortsOpened(true);
}
},
removeSort: async (id: string) => {
if (!database.canEdit) {
@@ -446,23 +438,21 @@ export const View = ({ view }: ViewProps) => {
return;
}
const viewAttributes = { ...view.attributes };
viewAttributes.sorts = viewAttributes.sorts ?? {};
delete viewAttributes.sorts[id];
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(view.id)) {
return;
}
const result = await window.colanode.executeMutation({
type: 'view.update',
userId: workspace.userId,
viewId: view.id,
view: viewAttributes,
nodes.update(view.id, (draft) => {
if (draft.attributes.type !== 'database_view') {
return;
}
draft.attributes.sorts = draft.attributes.sorts ?? {};
delete draft.attributes.sorts[id];
});
if (!result.success) {
toast.error(result.error.message);
} else {
setIsSearchBarOpened(true);
setIsSortsOpened(true);
}
},
openSearchBar: () => {
setIsSearchBarOpened(true);

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalFolderNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { FolderForm } from '@colanode/ui/components/folders/folder-form';
@@ -11,7 +9,7 @@ import {
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database } from '@colanode/ui/data';
interface FolderUpdateDialogProps {
folder: LocalFolderNode;
@@ -27,7 +25,6 @@ export const FolderUpdateDialog = ({
onOpenChange,
}: FolderUpdateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'editor');
return (
@@ -43,33 +40,28 @@ export const FolderUpdateDialog = ({
name: folder.attributes.name,
avatar: folder.attributes.avatar,
}}
isPending={isPending}
isPending={false}
submitText="Update"
readOnly={!canEdit}
onCancel={() => {
onOpenChange(false);
}}
onSubmit={(values) => {
if (isPending) {
const nodes = database.workspace(workspace.userId).nodes;
if (!nodes.has(folder.id)) {
return;
}
mutate({
input: {
type: 'folder.update',
folderId: folder.id,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
toast.success('Folder was updated successfully');
},
onError(error) {
toast.error(error.message);
},
nodes.update(folder.id, (draft) => {
if (draft.attributes.type !== 'folder') {
return;
}
draft.attributes.name = values.name;
draft.attributes.avatar = values.avatar;
});
onOpenChange(false);
}}
/>
</DialogContent>

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalPageNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { PageForm } from '@colanode/ui/components/pages/page-form';
@@ -11,7 +9,7 @@ import {
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database } from '@colanode/ui/data';
interface PageUpdateDialogProps {
page: LocalPageNode;
@@ -27,7 +25,6 @@ export const PageUpdateDialog = ({
onOpenChange,
}: PageUpdateDialogProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'editor');
return (
@@ -43,31 +40,26 @@ export const PageUpdateDialog = ({
name: page.attributes.name,
avatar: page.attributes.avatar,
}}
isPending={isPending}
isPending={false}
submitText="Update"
readOnly={!canEdit}
onCancel={() => onOpenChange(false)}
onSubmit={(values) => {
if (isPending) {
const nodes = database.workspace(workspace.userId).nodes;
if (!nodes.has(page.id)) {
return;
}
mutate({
input: {
type: 'page.update',
pageId: page.id,
name: values.name,
avatar: values.avatar,
userId: workspace.userId,
},
onSuccess() {
onOpenChange(false);
toast.success('Page was updated successfully');
},
onError(error) {
toast.error(error.message);
},
nodes.update(page.id, (draft) => {
if (draft.attributes.type !== 'page') {
return;
}
draft.attributes.name = values.name;
draft.attributes.avatar = values.avatar;
});
onOpenChange(false);
}}
/>
</DialogContent>

View File

@@ -1,18 +1,14 @@
import { toast } from 'sonner';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { Button } from '@colanode/ui/components/ui/button';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database as appDatabase } from '@colanode/ui/data';
export const RecordAvatar = () => {
const workspace = useWorkspace();
const record = useRecord();
const { mutate, isPending } = useMutation();
if (!record.canEdit) {
return (
<Button type="button" variant="outline" size="icon">
@@ -29,19 +25,19 @@ export const RecordAvatar = () => {
return (
<AvatarPopover
onPick={(avatar) => {
if (isPending) return;
if (avatar === record.avatar) return;
mutate({
input: {
type: 'record.avatar.update',
recordId: record.id,
avatar,
userId: workspace.userId,
},
onError(error) {
toast.error(error.message);
},
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.avatar = avatar;
});
}}
>

View File

@@ -1,15 +1,13 @@
import { useEffect, useRef } from 'react';
import { toast } from 'sonner';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database as appDatabase } from '@colanode/ui/data';
export const RecordName = () => {
const workspace = useWorkspace();
const record = useRecord();
const { mutate, isPending } = useMutation();
const inputRef = useRef<HTMLInputElement>(null);
@@ -29,24 +27,21 @@ export const RecordName = () => {
readOnly={!record.canEdit}
ref={inputRef}
onChange={(value) => {
if (isPending) {
return;
}
if (value === record.name) {
return;
}
mutate({
input: {
type: 'record.name.update',
recordId: record.id,
name: value,
userId: workspace.userId,
},
onError(error) {
toast.error(error.message);
},
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.name = value;
});
}}
className="font-heading border-b border-none pl-1 text-4xl font-bold shadow-none focus-visible:ring-0"

View File

@@ -1,9 +1,8 @@
import { toast } from 'sonner';
import { LocalRecordNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { RecordContext } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { database as appDatabase } from '@colanode/ui/data';
export const RecordProvider = ({
record,
@@ -34,29 +33,32 @@ export const RecordProvider = ({
localRevision: record.localRevision,
canEdit,
updateFieldValue: async (field, value) => {
const result = await window.colanode.executeMutation({
type: 'record.field.value.set',
recordId: record.id,
fieldId: field.id,
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
draft.attributes.fields[field.id] = value;
});
},
removeFieldValue: async (field) => {
const result = await window.colanode.executeMutation({
type: 'record.field.value.delete',
recordId: record.id,
fieldId: field.id,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
const nodes = appDatabase.workspace(workspace.userId).nodes;
if (!nodes.has(record.id)) {
return;
}
nodes.update(record.id, (draft) => {
if (draft.attributes.type !== 'record') {
return;
}
delete draft.attributes.fields[field.id];
});
},
getBooleanValue: (field) => {
const fieldValue = record.attributes.fields[field.id];

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { NodeCollaborators } from '@colanode/ui/components/collaborators/node-collaborators';
@@ -7,7 +5,7 @@ import { SpaceDelete } from '@colanode/ui/components/spaces/space-delete';
import { SpaceForm } from '@colanode/ui/components/spaces/space-form';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { database } from '@colanode/ui/data';
interface SpaceBodyProps {
space: LocalSpaceNode;
@@ -16,7 +14,6 @@ interface SpaceBodyProps {
export const SpaceBody = ({ space, role }: SpaceBodyProps) => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'admin');
const canDelete = hasNodeRole(role, 'admin');
@@ -36,24 +33,22 @@ export const SpaceBody = ({ space, role }: SpaceBodyProps) => {
}}
readOnly={!canEdit}
onSubmit={(values) => {
mutate({
input: {
type: 'space.update',
userId: workspace.userId,
spaceId: space.id,
name: values.name,
description: values.description,
avatar: values.avatar,
},
onSuccess() {
toast.success('Space updated');
},
onError(error) {
toast.error(error.message);
},
const nodes = database.workspace(workspace.userId).nodes;
if (!nodes.has(space.id)) {
return;
}
nodes.update(space.id, (draft) => {
if (draft.attributes.type !== 'space') {
return;
}
draft.attributes.name = values.name;
draft.attributes.description = values.description;
draft.attributes.avatar = values.avatar;
});
}}
isPending={isPending}
isPending={false}
saveText="Update"
/>
</div>

View File

@@ -1,9 +1,9 @@
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
import { ChevronRight } from 'lucide-react';
import { RefAttributes, useRef } from 'react';
import { useDrop } from 'react-dnd';
import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { LocalNode, LocalSpaceNode } from '@colanode/client/types';
import { extractNodeRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { SpaceSidebarDropdown } from '@colanode/ui/components/spaces/space-sidebar-dropdown';
@@ -15,9 +15,11 @@ import {
import { Link } from '@colanode/ui/components/ui/link';
import { WorkspaceSidebarItem } from '@colanode/ui/components/workspaces/sidebars/sidebar-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { sortSpaceChildren } from '@colanode/ui/lib/spaces';
import { database } from '@colanode/ui/data';
import {
generateSpaceChildIndex,
sortSpaceChildren,
} from '@colanode/ui/lib/spaces';
import { cn } from '@colanode/ui/lib/utils';
interface SpaceSidebarItemProps {
@@ -26,17 +28,18 @@ interface SpaceSidebarItemProps {
export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
const workspace = useWorkspace();
const mutation = useMutation();
const role = extractNodeRole(space, workspace.userId);
const canEdit = role === 'admin';
const nodeChildrenGetQuery = useLiveQuery({
type: 'node.children.get',
nodeId: space.id,
userId: workspace.userId,
types: ['page', 'channel', 'database', 'folder'],
});
const nodeChildrenGetQuery = useLiveQuery((q) =>
q
.from({ nodes: database.workspace(workspace.userId).nodes })
.where(({ nodes }) => eq(nodes.parentId, space.id))
.where(({ nodes }) =>
inArray(nodes.type, ['page', 'channel', 'database', 'folder'])
)
);
const [dropMonitor, dropRef] = useDrop({
accept: 'sidebar-item',
@@ -55,17 +58,33 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
const children = sortSpaceChildren(space, nodeChildrenGetQuery.data ?? []);
const handleDragEnd = (childId: string, after: string | null) => {
mutation.mutate({
input: {
type: 'space.child.reorder',
userId: workspace.userId,
spaceId: space.id,
childId,
after,
},
onError(error) {
toast.error(error.message);
},
const nodes = database.workspace(workspace.userId).nodes;
if (!nodes.has(space.id)) {
return;
}
const children: LocalNode[] = [];
for (const [, node] of nodes.entries()) {
if (node.parentId === space.id) {
children.push(node);
}
}
const newIndex = generateSpaceChildIndex(space, children, childId, after);
nodes.update(space.id, (draft) => {
if (draft.attributes.type !== 'space') {
return;
}
const childrenSettings = draft.attributes.children ?? {};
childrenSettings[childId] = {
...(childrenSettings[childId] ?? {}),
id: childId,
index: newIndex,
};
draft.attributes.children = childrenSettings;
});
};

View File

@@ -3,9 +3,7 @@ import { createContext, useContext } from 'react';
import {
DatabaseNameFieldAttributes,
FieldAttributes,
FieldType,
NodeRole,
SelectOptionAttributes,
} from '@colanode/core';
interface DatabaseContext {
@@ -16,16 +14,6 @@ interface DatabaseContext {
canEdit: boolean;
canCreateRecord: boolean;
role: NodeRole;
createField: (type: FieldType, name: string) => void;
renameField: (id: string, name: string) => void;
updateNameField: (name: string) => void;
deleteField: (id: string) => void;
createSelectOption: (fieldId: string, name: string, color: string) => void;
updateSelectOption: (
fieldId: string,
attributes: SelectOptionAttributes
) => void;
deleteSelectOption: (fieldId: string, optionId: string) => void;
}
export const DatabaseContext = createContext<DatabaseContext>(

View File

@@ -49,26 +49,38 @@ export const createNodesCollection = (userId: string) => {
},
},
onInsert: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map(async (mutation) => {
return await window.colanode.executeMutation({
transaction.mutations.forEach(async (mutation) => {
await window.colanode.executeMutation({
type: 'node.create',
userId,
node: mutation.modified,
});
})
);
});
},
onUpdate: async ({ transaction }) => {
transaction.mutations.forEach(async (mutation) => {
console.log('onUpdate', mutation);
const attributes = mutation.changes.attributes;
if (!attributes) {
return;
}
await window.colanode.executeMutation({
type: 'node.update',
userId,
nodeId: mutation.key,
attributes,
});
});
},
onDelete: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map(async (mutation) => {
return await window.colanode.executeMutation({
transaction.mutations.forEach(async (mutation) => {
await window.colanode.executeMutation({
type: 'node.delete',
userId,
nodeId: mutation.key,
});
})
);
});
},
});
};

View File

@@ -1,6 +1,12 @@
import { LocalNode, LocalSpaceNode } from '@colanode/client/types';
import { compareString, generateFractionalIndex } from '@colanode/core';
interface NodeFractionalIndex {
id: string;
defaultIndex: string;
customIndex: string | null;
}
export const sortSpaceChildren = (
space: LocalSpaceNode,
children: LocalNode[]
@@ -22,3 +28,84 @@ export const sortSpaceChildren = (
return compareString(aIndex, bIndex);
});
};
export const generateSpaceChildIndex = (
space: LocalSpaceNode,
children: LocalNode[],
childId: string,
after: string | null
): string | null => {
const child = children.find((c) => c.id === childId);
if (!child) {
return null;
}
const sortedById = children.toSorted((a, b) => compareString(a.id, b.id));
const indexes: NodeFractionalIndex[] = [];
const childrenSettings = space.attributes.children ?? {};
let lastIndex: string | null = null;
for (const child of sortedById) {
lastIndex = generateFractionalIndex(lastIndex, null);
indexes.push({
id: child.id,
defaultIndex: lastIndex,
customIndex: childrenSettings[child.id]?.index ?? null,
});
}
const sortedIndexes = indexes.sort((a, b) =>
compareString(
a.customIndex ?? a.defaultIndex,
b.customIndex ?? b.defaultIndex
)
);
if (after === null) {
const firstIndex = sortedIndexes[0];
if (!firstIndex) {
return generateFractionalIndex(null, null);
}
const nextIndex = firstIndex.customIndex ?? firstIndex.defaultIndex;
return generateFractionalIndex(null, nextIndex);
}
const afterNodeIndex = sortedIndexes.findIndex((node) => node.id === after);
if (afterNodeIndex === -1) {
return null;
}
const afterNode = sortedIndexes[afterNodeIndex];
if (!afterNode) {
return null;
}
const previousIndex = afterNode.customIndex ?? afterNode.defaultIndex;
let nextIndex: string | null = null;
if (afterNodeIndex < sortedIndexes.length - 1) {
const nextNode = sortedIndexes[afterNodeIndex + 1];
if (!nextNode) {
return null;
}
nextIndex = nextNode.customIndex ?? nextNode.defaultIndex;
}
let newIndex = generateFractionalIndex(previousIndex, nextIndex);
const maxDefaultIndex = sortedIndexes
.map((index) => index.defaultIndex)
.sort((a, b) => -compareString(a, b))[0]!;
const newPotentialDefaultIndex = generateFractionalIndex(
maxDefaultIndex,
null
);
if (newPotentialDefaultIndex === newIndex) {
newIndex = generateFractionalIndex(previousIndex, newPotentialDefaultIndex);
}
return newIndex;
};