Use tanstackdb for record updates

This commit is contained in:
Hakan Shehu
2025-11-20 21:46:36 -08:00
parent 8ae7f007d8
commit 29bf4fbb27
20 changed files with 197 additions and 467 deletions

View File

@@ -46,11 +46,8 @@ import { NodeInteractionOpenedMutationHandler } from './nodes/node-interaction-o
import { NodeInteractionSeenMutationHandler } from './nodes/node-interaction-seen'; import { NodeInteractionSeenMutationHandler } from './nodes/node-interaction-seen';
import { NodeReactionCreateMutationHandler } from './nodes/node-reaction-create'; import { NodeReactionCreateMutationHandler } from './nodes/node-reaction-create';
import { NodeReactionDeleteMutationHandler } from './nodes/node-reaction-delete'; import { NodeReactionDeleteMutationHandler } from './nodes/node-reaction-delete';
import { NodeUpdateMutationHandler } from './nodes/node-update';
import { PageUpdateMutationHandler } from './pages/page-update'; 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 { ServerCreateMutationHandler } from './servers/server-create'; import { ServerCreateMutationHandler } from './servers/server-create';
import { ServerDeleteMutationHandler } from './servers/server-delete'; import { ServerDeleteMutationHandler } from './servers/server-delete';
import { ServerSyncMutationHandler } from './servers/server-sync'; import { ServerSyncMutationHandler } from './servers/server-sync';
@@ -78,6 +75,7 @@ export const buildMutationHandlerMap = (
'view.create': new ViewCreateMutationHandler(app), 'view.create': new ViewCreateMutationHandler(app),
'node.delete': new NodeDeleteMutationHandler(app), 'node.delete': new NodeDeleteMutationHandler(app),
'node.create': new NodeCreateMutationHandler(app), 'node.create': new NodeCreateMutationHandler(app),
'node.update': new NodeUpdateMutationHandler(app),
'chat.create': new ChatCreateMutationHandler(app), 'chat.create': new ChatCreateMutationHandler(app),
'database.create': new DatabaseCreateMutationHandler(app), 'database.create': new DatabaseCreateMutationHandler(app),
'database.name.field.update': new DatabaseNameFieldUpdateMutationHandler( 'database.name.field.update': new DatabaseNameFieldUpdateMutationHandler(
@@ -94,10 +92,6 @@ export const buildMutationHandlerMap = (
'node.interaction.seen': new NodeInteractionSeenMutationHandler(app), 'node.interaction.seen': new NodeInteractionSeenMutationHandler(app),
'node.reaction.create': new NodeReactionCreateMutationHandler(app), 'node.reaction.create': new NodeReactionCreateMutationHandler(app),
'node.reaction.delete': new NodeReactionDeleteMutationHandler(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.create': new SelectOptionCreateMutationHandler(app),
'select.option.delete': new SelectOptionDeleteMutationHandler(app), 'select.option.delete': new SelectOptionDeleteMutationHandler(app),
'select.option.update': new SelectOptionUpdateMutationHandler(app), 'select.option.update': new SelectOptionUpdateMutationHandler(app),

View File

@@ -0,0 +1,24 @@
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, () => {
return input.attributes;
});
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

@@ -37,10 +37,6 @@ export * from './nodes/node-interaction-seen';
export * from './nodes/node-reaction-create'; export * from './nodes/node-reaction-create';
export * from './nodes/node-reaction-delete'; export * from './nodes/node-reaction-delete';
export * from './pages/page-update'; 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-create';
export * from './servers/server-delete'; export * from './servers/server-delete';
export * from './spaces/space-update'; export * from './spaces/space-update';
@@ -58,6 +54,7 @@ export * from './servers/server-sync';
export * from './apps/tab-delete'; export * from './apps/tab-delete';
export * from './nodes/node-delete'; export * from './nodes/node-delete';
export * from './nodes/node-create'; export * from './nodes/node-create';
export * from './nodes/node-update';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type // eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface MutationMap {} 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: NodeAttributes;
};
export type NodeUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'node.update': {
input: NodeUpdateMutationInput;
output: NodeUpdateMutationOutput;
};
}
}

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,5 +1,5 @@
import { createCollection } from '@tanstack/react-db'; import { createCollection } from '@tanstack/react-db';
// import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { mapNodeAttributes } from '@colanode/client/lib'; import { mapNodeAttributes } from '@colanode/client/lib';
import { LocalNode } from '@colanode/client/types'; import { LocalNode } from '@colanode/client/types';
@@ -73,17 +73,18 @@ export const createNodesCollection = (userId: string) => {
}); });
} }
}, },
// onUpdate: async ({ transaction }) => { onUpdate: async ({ transaction }) => {
// for (const mutation of transaction.mutations) { for (const mutation of transaction.mutations) {
// const attributes = cloneDeep(mutation.modified.attributes); const node = cloneDeep(mutation.modified);
// await window.colanode.executeMutation({ const attributes = mapNodeAttributes(node);
// type: 'node.update', await window.colanode.executeMutation({
// userId, type: 'node.update',
// nodeId: mutation.key, userId,
// attributes, nodeId: mutation.key,
// }); attributes,
// } });
// }, }
},
onDelete: async ({ transaction }) => { onDelete: async ({ transaction }) => {
for (const mutation of transaction.mutations) { for (const mutation of transaction.mutations) {
await window.colanode.executeMutation({ await window.colanode.executeMutation({

View File

@@ -1,6 +1,5 @@
import { eq, useLiveQuery as useLiveQueryTanstack } from '@tanstack/react-db'; import { eq, useLiveQuery as useLiveQueryTanstack } from '@tanstack/react-db';
import { CircleAlert, CircleDashed } from 'lucide-react'; import { CircleAlert, CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import { import {
CollaboratorFieldAttributes, CollaboratorFieldAttributes,
@@ -81,17 +80,16 @@ export const BoardViewColumnsCollaborator = ({
), ),
canDrag: (record) => record.canEdit, canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
if (value.type !== 'string_array') { if (value.type !== 'string_array') {
return; return;
@@ -114,17 +112,13 @@ export const BoardViewColumnsCollaborator = ({
}; };
} }
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value: newValue,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = newValue;
toast.error(result.error.message); });
}
} }
}, },
}} }}
@@ -150,29 +144,24 @@ export const BoardViewColumnsCollaborator = ({
), ),
canDrag: () => true, canDrag: () => true,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = value;
toast.error(result.error.message); });
}
} }
}, },
}} }}

View File

@@ -1,5 +1,4 @@
import { CircleDashed } from 'lucide-react'; import { CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import { import {
DatabaseViewFilterAttributes, DatabaseViewFilterAttributes,
@@ -7,6 +6,7 @@ import {
MultiSelectFieldAttributes, MultiSelectFieldAttributes,
SelectOptionAttributes, SelectOptionAttributes,
} from '@colanode/core'; } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column'; import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column';
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge'; import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
import { BoardViewContext } from '@colanode/ui/contexts/board-view'; import { BoardViewContext } from '@colanode/ui/contexts/board-view';
@@ -90,17 +90,16 @@ export const BoardViewColumnsMultiSelect = ({
), ),
canDrag: (record) => record.canEdit, canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
if (value.type !== 'string_array') { if (value.type !== 'string_array') {
return; return;
@@ -122,17 +121,13 @@ export const BoardViewColumnsMultiSelect = ({
}; };
} }
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value: newValue,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = newValue;
toast.error(result.error.message); });
}
} }
}, },
}} }}
@@ -159,29 +154,24 @@ export const BoardViewColumnsMultiSelect = ({
dragOverClass: noValueDraggingClass, dragOverClass: noValueDraggingClass,
canDrag: () => true, canDrag: () => true,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = value;
toast.error(result.error.message); });
}
} }
}, },
}} }}

View File

@@ -1,11 +1,11 @@
import { CircleDashed } from 'lucide-react'; import { CircleDashed } from 'lucide-react';
import { toast } from 'sonner';
import { import {
DatabaseViewFilterAttributes, DatabaseViewFilterAttributes,
SelectFieldAttributes, SelectFieldAttributes,
SelectOptionAttributes, SelectOptionAttributes,
} from '@colanode/core'; } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column'; import { BoardViewColumn } from '@colanode/ui/components/databases/boards/board-view-column';
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge'; import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
import { BoardViewContext } from '@colanode/ui/contexts/board-view'; import { BoardViewContext } from '@colanode/ui/contexts/board-view';
@@ -89,29 +89,24 @@ export const BoardViewColumnsSelect = ({
), ),
canDrag: (record) => record.canEdit, canDrag: (record) => record.canEdit,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = value;
toast.error(result.error.message); });
}
} }
}, },
}} }}
@@ -138,29 +133,24 @@ export const BoardViewColumnsSelect = ({
dragOverClass: noValueDraggingClass, dragOverClass: noValueDraggingClass,
canDrag: () => true, canDrag: () => true,
onDragEnd: async (record, value) => { onDragEnd: async (record, value) => {
const nodes = collections.workspace(workspace.userId).nodes;
if (!value) { if (!value) {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.delete', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
userId: workspace.userId,
});
if (!result.success) { const { [field.id]: _removed, ...rest } = draft.fields;
toast.error(result.error.message); draft.fields = rest;
} });
} else { } else {
const result = await window.colanode.executeMutation({ nodes.update(record.id, (draft) => {
type: 'record.field.value.set', if (draft.type !== 'record') {
recordId: record.id, return;
fieldId: field.id, }
value,
userId: workspace.userId,
});
if (!result.success) { draft.fields[field.id] = value;
toast.error(result.error.message); });
}
} }
}, },
}} }}

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { toast } from 'sonner';
import { LocalRecordNode } from '@colanode/client/types'; import { LocalRecordNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core'; import { NodeRole, hasNodeRole } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { RecordContext } from '@colanode/ui/contexts/record'; import { RecordContext } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace'; import { useWorkspace } from '@colanode/ui/contexts/workspace';
@@ -34,29 +35,35 @@ export const RecordProvider = ({
localRevision: record.localRevision, localRevision: record.localRevision,
canEdit, canEdit,
updateFieldValue: async (field, value) => { updateFieldValue: async (field, value) => {
const result = await window.colanode.executeMutation({ const nodes = collections.workspace(workspace.userId).nodes;
type: 'record.field.value.set', if (!nodes.has(record.id)) {
recordId: record.id, toast.error('Record not found');
fieldId: field.id, return;
value,
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
} }
nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
draft.fields[field.id] = value;
});
}, },
removeFieldValue: async (field) => { removeFieldValue: async (field) => {
const result = await window.colanode.executeMutation({ const nodes = collections.workspace(workspace.userId).nodes;
type: 'record.field.value.delete', if (!nodes.has(record.id)) {
recordId: record.id, toast.error('Record not found');
fieldId: field.id, return;
userId: workspace.userId,
});
if (!result.success) {
toast.error(result.error.message);
} }
nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
const { [field.id]: _removed, ...rest } = draft.fields;
draft.fields = rest;
});
}, },
getBooleanValue: (field) => { getBooleanValue: (field) => {
const fieldValue = record.fields[field.id]; const fieldValue = record.fields[field.id];