Use paced mutations for some rename inputs

This commit is contained in:
Hakan Shehu
2025-11-21 09:25:24 -08:00
parent 019b16f428
commit b784397d30
7 changed files with 142 additions and 73 deletions

View File

@@ -1,8 +1,7 @@
import { createCollection } from '@tanstack/react-db'; import { createCollection } from '@tanstack/react-db';
import { cloneDeep } from 'lodash-es';
import { mapNodeAttributes } from '@colanode/client/lib';
import { LocalNode } from '@colanode/client/types'; import { LocalNode } from '@colanode/client/types';
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
export const createNodesCollection = (userId: string) => { export const createNodesCollection = (userId: string) => {
return createCollection<LocalNode, string>({ return createCollection<LocalNode, string>({
@@ -62,37 +61,14 @@ export const createNodesCollection = (userId: string) => {
}, },
}, },
onInsert: async ({ transaction }) => { onInsert: async ({ transaction }) => {
for (const mutation of transaction.mutations) { await applyNodeTransaction(userId, transaction);
const node = mutation.modified;
const attributes = mapNodeAttributes(node);
await window.colanode.executeMutation({
type: 'node.create',
userId,
nodeId: node.id,
attributes,
});
}
}, },
onUpdate: async ({ transaction }) => { onUpdate: async ({ transaction }) => {
for (const mutation of transaction.mutations) { console.log('onUpdate', transaction);
const node = cloneDeep(mutation.modified); await applyNodeTransaction(userId, transaction);
const attributes = mapNodeAttributes(node);
await window.colanode.executeMutation({
type: 'node.update',
userId,
nodeId: mutation.key,
attributes,
});
}
}, },
onDelete: async ({ transaction }) => { onDelete: async ({ transaction }) => {
for (const mutation of transaction.mutations) { await applyNodeTransaction(userId, transaction);
await window.colanode.executeMutation({
type: 'node.delete',
userId,
nodeId: mutation.key,
});
}
}, },
}); });
}; };

View File

@@ -5,6 +5,7 @@ import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog'; import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon'; import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon'; import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewRenameInput } from '@colanode/ui/components/databases/view-rename-input';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button'; import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog'; import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button'; import { Button } from '@colanode/ui/components/ui/button';
@@ -14,7 +15,6 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@colanode/ui/components/ui/popover'; } from '@colanode/ui/components/ui/popover';
import { Separator } from '@colanode/ui/components/ui/separator'; import { Separator } from '@colanode/ui/components/ui/separator';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -67,14 +67,10 @@ export const BoardViewSettings = () => {
/> />
</Button> </Button>
)} )}
<SmartTextInput <ViewRenameInput
value={view.name} id={view.id}
name={view.name}
readOnly={!database.canEdit} readOnly={!database.canEdit}
onChange={(newName) => {
if (newName === view.name) return;
view.rename(newName);
}}
/> />
</div> </div>
<Separator /> <Separator />

View File

@@ -5,6 +5,7 @@ import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog'; import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon'; import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon'; import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewRenameInput } from '@colanode/ui/components/databases/view-rename-input';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button'; import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog'; import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button'; import { Button } from '@colanode/ui/components/ui/button';
@@ -14,7 +15,6 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@colanode/ui/components/ui/popover'; } from '@colanode/ui/components/ui/popover';
import { Separator } from '@colanode/ui/components/ui/separator'; import { Separator } from '@colanode/ui/components/ui/separator';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -67,14 +67,10 @@ export const CalendarViewSettings = () => {
/> />
</Button> </Button>
)} )}
<SmartTextInput <ViewRenameInput
value={view.name} id={view.id}
name={view.name}
readOnly={!database.canEdit} readOnly={!database.canEdit}
onChange={(newName) => {
if (newName === view.name) return;
view.rename(newName);
}}
/> />
</div> </div>
<Separator /> <Separator />

View File

@@ -1,7 +1,12 @@
import { debounceStrategy, usePacedMutations } from '@tanstack/react-db';
import { useEffect, useState } from 'react';
import { LocalNode } from '@colanode/client/types';
import { FieldAttributes } from '@colanode/core'; import { FieldAttributes } from '@colanode/core';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input'; import { Input } from '@colanode/ui/components/ui/input';
import { useDatabase } from '@colanode/ui/contexts/database'; import { useDatabase } from '@colanode/ui/contexts/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace'; import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
interface FieldRenameInputProps { interface FieldRenameInputProps {
field: FieldAttributes; field: FieldAttributes;
@@ -10,28 +15,42 @@ interface FieldRenameInputProps {
export const FieldRenameInput = ({ field }: FieldRenameInputProps) => { export const FieldRenameInput = ({ field }: FieldRenameInputProps) => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const database = useDatabase(); const database = useDatabase();
const [name, setName] = useState(field.name);
useEffect(() => {
setName(field.name);
}, [field.name]);
const mutate = usePacedMutations<string, LocalNode>({
onMutate: (value) => {
workspace.collections.nodes.update(database.id, (draft) => {
if (draft.type !== 'database') {
return;
}
const fieldAttributes = draft.fields[field.id];
if (!fieldAttributes) {
return;
}
fieldAttributes.name = value;
});
},
mutationFn: async ({ transaction }) => {
await applyNodeTransaction(workspace.userId, transaction);
},
strategy: debounceStrategy({ wait: 500 }),
});
return ( return (
<div className="w-full p-1"> <div className="w-full p-1">
<SmartTextInput <Input
value={field.name} value={name}
readOnly={!database.canEdit} readOnly={!database.canEdit}
onChange={(newName) => { onChange={(event) => {
if (newName === field.name) return; const newValue = event.target.value;
setName(newValue);
const nodes = workspace.collections.nodes; mutate(newValue);
nodes.update(database.id, (draft) => {
if (draft.type !== 'database') {
return;
}
const fieldAttributes = draft.fields[field.id];
if (!fieldAttributes) {
return;
}
fieldAttributes.name = newName;
});
}} }}
/> />
</div> </div>

View File

@@ -5,6 +5,7 @@ import { AvatarPopover } from '@colanode/ui/components/avatars/avatar-popover';
import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog'; import { FieldDeleteDialog } from '@colanode/ui/components/databases/fields/field-delete-dialog';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon'; import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { ViewIcon } from '@colanode/ui/components/databases/view-icon'; import { ViewIcon } from '@colanode/ui/components/databases/view-icon';
import { ViewRenameInput } from '@colanode/ui/components/databases/view-rename-input';
import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button'; import { ViewSettingsButton } from '@colanode/ui/components/databases/view-settings-button';
import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog'; import { NodeDeleteDialog } from '@colanode/ui/components/nodes/node-delete-dialog';
import { Button } from '@colanode/ui/components/ui/button'; import { Button } from '@colanode/ui/components/ui/button';
@@ -14,7 +15,6 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@colanode/ui/components/ui/popover'; } from '@colanode/ui/components/ui/popover';
import { Separator } from '@colanode/ui/components/ui/separator'; import { Separator } from '@colanode/ui/components/ui/separator';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -67,14 +67,10 @@ export const TableViewSettings = () => {
/> />
</Button> </Button>
)} )}
<SmartTextInput <ViewRenameInput
value={view.name} id={view.id}
name={view.name}
readOnly={!database.canEdit} readOnly={!database.canEdit}
onChange={(newName) => {
if (newName === view.name) return;
view.rename(newName);
}}
/> />
</div> </div>
<Separator /> <Separator />

View File

@@ -0,0 +1,49 @@
import { debounceStrategy, usePacedMutations } from '@tanstack/react-db';
import { LocalNode } from '@colanode/client/types';
import { Input } from '@colanode/ui/components/ui/input';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
interface ViewRenameInputProps {
id: string;
name: string;
readOnly?: boolean;
}
export const ViewRenameInput = ({
id,
name,
readOnly,
}: ViewRenameInputProps) => {
const workspace = useWorkspace();
const mutate = usePacedMutations<string, LocalNode>({
onMutate: (value) => {
workspace.collections.nodes.update(id, (draft) => {
if (draft.type !== 'database_view') {
return;
}
draft.name = value;
});
},
mutationFn: async ({ transaction }) => {
await applyNodeTransaction(workspace.userId, transaction);
},
strategy: debounceStrategy({ wait: 500 }),
});
return (
<div className="w-full p-1">
<Input
value={name}
readOnly={readOnly}
onChange={(event) => {
const newValue = event.target.value;
mutate(newValue);
}}
/>
</div>
);
};

View File

@@ -1,4 +1,8 @@
import { NodeCollaborator } from '@colanode/client/types'; import { OperationType, TransactionWithMutations } from '@tanstack/react-db';
import { cloneDeep } from 'lodash-es';
import { mapNodeAttributes } from '@colanode/client/lib';
import { LocalNode, NodeCollaborator } from '@colanode/client/types';
import { extractNodeCollaborators, Node } from '@colanode/core'; import { extractNodeCollaborators, Node } from '@colanode/core';
export const buildNodeCollaborators = (nodes: Node[]): NodeCollaborator[] => { export const buildNodeCollaborators = (nodes: Node[]): NodeCollaborator[] => {
@@ -18,3 +22,36 @@ export const buildNodeCollaborators = (nodes: Node[]): NodeCollaborator[] => {
return Object.values(collaborators); return Object.values(collaborators);
}; };
export const applyNodeTransaction = async (
userId: string,
transaction: TransactionWithMutations<LocalNode, OperationType>
) => {
for (const mutation of transaction.mutations) {
if (mutation.type === 'insert') {
const node = mutation.modified;
const attributes = mapNodeAttributes(node);
await window.colanode.executeMutation({
type: 'node.create',
userId,
nodeId: node.id,
attributes,
});
} else if (mutation.type === 'update') {
const node = cloneDeep(mutation.modified);
const attributes = mapNodeAttributes(node);
await window.colanode.executeMutation({
type: 'node.update',
userId,
nodeId: mutation.key,
attributes,
});
} else if (mutation.type === 'delete') {
await window.colanode.executeMutation({
type: 'node.delete',
userId,
nodeId: mutation.key,
});
}
}
};