Use paced mutations for record updates

This commit is contained in:
Hakan Shehu
2025-11-21 11:40:35 -08:00
parent fc8a6a0626
commit 2f59d19d72
16 changed files with 225 additions and 297 deletions

View File

@@ -24,7 +24,7 @@ export const RecordAttributes = () => {
<div className="w-60 max-w-60">
<RecordField field={field} />
</div>
<div className="flex-1 max-w-128 p-1">
<div className="flex-1 max-w-lg p-1">
<RecordFieldValue field={field} />
</div>
</div>

View File

@@ -1,8 +1,11 @@
import { debounceStrategy, usePacedMutations } from '@tanstack/react-db';
import { useEffect, useRef } from 'react';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { LocalNode } from '@colanode/client/types';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
export const RecordName = () => {
const workspace = useWorkspace();
@@ -20,26 +23,32 @@ export const RecordName = () => {
return () => clearTimeout(timeoutId);
}, [record.canEdit, inputRef]);
return (
<SmartTextInput
value={record.name}
readOnly={!record.canEdit}
ref={inputRef}
onChange={(value) => {
if (value === record.name) {
const mutate = usePacedMutations<string, LocalNode>({
onMutate: (value) => {
workspace.collections.nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
const nodes = workspace.collections.nodes;
nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
draft.name = value;
});
},
mutationFn: async ({ transaction }) => {
await applyNodeTransaction(workspace.userId, transaction);
},
strategy: debounceStrategy({ wait: 500 }),
});
draft.name = value;
});
return (
<Input
value={record.name}
readOnly={!record.canEdit}
ref={inputRef}
onChange={(event) => {
const newValue = event.target.value;
mutate(newValue);
}}
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 md:text-4xl text-2xl font-bold shadow-none focus-visible:ring-0"
placeholder="Unnamed"
/>
);

View File

@@ -1,5 +1,3 @@
import { toast } from 'sonner';
import { LocalRecordNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { RecordContext } from '@colanode/ui/contexts/record';
@@ -33,141 +31,6 @@ export const RecordProvider = ({
databaseId: record.databaseId,
localRevision: record.localRevision,
canEdit,
updateFieldValue: async (field, value) => {
const nodes = workspace.collections.nodes;
if (!nodes.has(record.id)) {
toast.error('Record not found');
return;
}
nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
draft.fields[field.id] = value;
});
},
removeFieldValue: async (field) => {
const nodes = workspace.collections.nodes;
if (!nodes.has(record.id)) {
toast.error('Record not found');
return;
}
nodes.update(record.id, (draft) => {
if (draft.type !== 'record') {
return;
}
const { [field.id]: _removed, ...rest } = draft.fields;
draft.fields = rest;
});
},
getBooleanValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'boolean') {
return fieldValue.value;
}
return false;
},
getCollaboratorValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string_array') {
return fieldValue.value;
}
return null;
},
getDateValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return new Date(fieldValue.value);
}
return null;
},
getEmailValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return fieldValue.value;
}
return null;
},
getFileValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string_array') {
return fieldValue.value;
}
return null;
},
getMultiSelectValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string_array') {
return fieldValue.value;
}
return [];
},
getNumberValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'number') {
return fieldValue.value;
}
return null;
},
getPhoneValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return fieldValue.value;
}
return null;
},
getRelationValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string_array') {
return fieldValue.value;
}
return null;
},
getRollupValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return fieldValue.value;
}
return null;
},
getSelectValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return fieldValue.value;
}
return null;
},
getTextValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'text') {
return fieldValue.value;
}
return null;
},
getUrlValue: (field) => {
const fieldValue = record.fields[field.id];
if (fieldValue?.type === 'string') {
return fieldValue.value;
}
return null;
},
}}
>
{children}

View File

@@ -1,8 +1,7 @@
import { useEffect, useState } from 'react';
import { BooleanFieldAttributes } from '@colanode/core';
import { BooleanFieldAttributes, BooleanFieldValue } from '@colanode/core';
import { Checkbox } from '@colanode/ui/components/ui/checkbox';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordBooleanValueProps {
field: BooleanFieldAttributes;
@@ -14,31 +13,27 @@ export const RecordBooleanValue = ({
readOnly,
}: RecordBooleanValueProps) => {
const record = useRecord();
const [input, setInput] = useState<boolean>(record.getBooleanValue(field));
useEffect(() => {
setInput(record.getBooleanValue(field));
}, [record.localRevision]);
const { value, setValue, clearValue } = useRecordField<BooleanFieldValue>({
field,
});
return (
<div className="flex h-full w-full flex-row items-center justify-start p-0">
<Checkbox
checked={input}
checked={value?.value ?? false}
disabled={!record.canEdit || readOnly}
onCheckedChange={(e) => {
if (!record.canEdit || readOnly) return;
if (typeof e === 'boolean') {
setInput(e.valueOf());
const checked = e.valueOf();
if (checked) {
record.updateFieldValue(field, {
setValue({
type: 'boolean',
value: checked,
});
} else {
record.removeFieldValue(field);
clearValue();
}
}
}}

View File

@@ -1,8 +1,11 @@
import { inArray, useLiveQuery } from '@tanstack/react-db';
import { X } from 'lucide-react';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { CollaboratorFieldAttributes } from '@colanode/core';
import {
CollaboratorFieldAttributes,
StringArrayFieldValue,
} from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { Badge } from '@colanode/ui/components/ui/badge';
import {
@@ -14,6 +17,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { UserSearch } from '@colanode/ui/components/users/user-search';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface CollaboratorBadgeProps {
id: string;
@@ -41,10 +45,15 @@ export const RecordCollaboratorValue = ({
}: RecordCollaboratorValueProps) => {
const workspace = useWorkspace();
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
{
field,
}
);
const [open, setOpen] = useState(false);
const collaboratorIds = record.getCollaboratorValue(field) ?? [];
const collaboratorIds = useMemo(() => value?.value ?? [], [value]);
const collaboratorsQuery = useLiveQuery(
(q) =>
q
@@ -115,9 +124,9 @@ export const RecordCollaboratorValue = ({
);
if (newCollaborators.length === 0) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string_array',
value: newCollaborators,
});
@@ -141,9 +150,9 @@ export const RecordCollaboratorValue = ({
: [...collaboratorIds, user.id];
if (newCollaborators.length === 0) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string_array',
value: newCollaborators,
});

View File

@@ -1,6 +1,7 @@
import { DateFieldAttributes } from '@colanode/core';
import { DateFieldAttributes, StringFieldValue } from '@colanode/core';
import { DatePicker } from '@colanode/ui/components/ui/date-picker';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordDateValueProps {
field: DateFieldAttributes;
@@ -9,18 +10,21 @@ interface RecordDateValueProps {
export const RecordDateValue = ({ field, readOnly }: RecordDateValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
field,
});
return (
<DatePicker
value={record.getDateValue(field)}
value={value ? new Date(value.value) : null}
readonly={!record.canEdit || readOnly}
onChange={(newValue) => {
if (!record.canEdit || readOnly) return;
if (newValue === null || newValue === undefined) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string',
value: newValue.toISOString(),
});

View File

@@ -1,6 +1,7 @@
import { EmailFieldAttributes } from '@colanode/core';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { EmailFieldAttributes, StringFieldValue } from '@colanode/core';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordEmailValueProps {
field: EmailFieldAttributes;
@@ -12,18 +13,22 @@ export const RecordEmailValue = ({
readOnly,
}: RecordEmailValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
field,
});
return (
<SmartTextInput
value={record.getEmailValue(field)}
<Input
value={value?.value ?? ''}
readOnly={!record.canEdit || readOnly}
onChange={(newValue) => {
onChange={(e) => {
const newValue = e.target.value;
if (!record.canEdit) return;
if (newValue === null || newValue === '') {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string',
value: newValue,
});

View File

@@ -1,6 +1,9 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { MultiSelectFieldAttributes } from '@colanode/core';
import {
MultiSelectFieldAttributes,
StringArrayFieldValue,
} from '@colanode/core';
import { SelectFieldOptions } from '@colanode/ui/components/databases/fields/select-field-options';
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
import {
@@ -9,6 +12,7 @@ import {
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordMultiSelectValueProps {
field: MultiSelectFieldAttributes;
@@ -22,17 +26,16 @@ export const RecordMultiSelectValue = ({
const record = useRecord();
const [open, setOpen] = useState(false);
const [selectedValues, setSelectedValues] = useState(
record.getMultiSelectValue(field)
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
{
field,
}
);
useEffect(() => {
setSelectedValues(record.getMultiSelectValue(field));
}, [record.localRevision]);
const selectOptions = Object.values(field.options ?? {});
const selectedOptionIds = value?.value ?? [];
const selectedOptions = selectOptions.filter((option) =>
selectedValues.includes(option.id)
selectedOptionIds.includes(option.id)
);
if (!record.canEdit || readOnly) {
@@ -67,20 +70,18 @@ export const RecordMultiSelectValue = ({
<PopoverContent className="w-80 p-1">
<SelectFieldOptions
field={field}
values={selectedValues}
values={selectedOptionIds}
onSelect={(id) => {
if (!record.canEdit || readOnly) return;
const newValues = selectedValues.includes(id)
? selectedValues.filter((v) => v !== id)
: [...selectedValues, id];
setSelectedValues(newValues);
const newValues = selectedOptionIds.includes(id)
? selectedOptionIds.filter((v) => v !== id)
: [...selectedOptionIds, id];
if (newValues.length === 0) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string_array',
value: newValues,
});

View File

@@ -1,6 +1,7 @@
import { type NumberFieldAttributes } from '@colanode/core';
import { SmartNumberInput } from '@colanode/ui/components/ui/smart-number-input';
import { NumberFieldValue, type NumberFieldAttributes } from '@colanode/core';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordNumberValueProps {
field: NumberFieldAttributes;
@@ -12,26 +13,37 @@ export const RecordNumberValue = ({
readOnly,
}: RecordNumberValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<NumberFieldValue>({
field,
});
return (
<SmartNumberInput
value={record.getNumberValue(field)}
<Input
value={value?.value ?? undefined}
readOnly={!record.canEdit || readOnly}
onChange={(newValue) => {
onChange={(e) => {
if (!record.canEdit || readOnly) return;
if (newValue === record.getNumberValue(field)) {
const newStringValue = e.target.value;
if (newStringValue === null || newStringValue === '') {
clearValue();
return;
}
if (newValue === null) {
record.removeFieldValue(field);
} else {
record.updateFieldValue(field, {
type: 'number',
value: newValue,
});
const newValue = parseFloat(newStringValue);
console.log('newValue', newValue, newStringValue);
if (isNaN(newValue)) {
return;
}
if (newValue === value?.value) {
return;
}
setValue({
type: 'number',
value: newValue,
});
}}
className="flex h-full w-full cursor-pointer flex-row items-center gap-1 border-none p-0 text-sm focus-visible:cursor-text shadow-none"
/>

View File

@@ -1,6 +1,7 @@
import { PhoneFieldAttributes } from '@colanode/core';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { PhoneFieldAttributes, StringFieldValue } from '@colanode/core';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordPhoneValueProps {
field: PhoneFieldAttributes;
@@ -12,22 +13,26 @@ export const RecordPhoneValue = ({
readOnly,
}: RecordPhoneValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
field,
});
return (
<SmartTextInput
value={record.getPhoneValue(field)}
<Input
value={value?.value ?? ''}
readOnly={!record.canEdit || readOnly}
onChange={(newValue) => {
onChange={(e) => {
const newValue = e.target.value;
if (!record.canEdit || readOnly) return;
if (newValue === record.getPhoneValue(field)) {
if (newValue === value?.value) {
return;
}
if (newValue === null || newValue === '') {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string',
value: newValue,
});

View File

@@ -1,9 +1,9 @@
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
import { X } from 'lucide-react';
import { Fragment, useState } from 'react';
import { Fragment, useMemo, useState } from 'react';
import { LocalRecordNode } from '@colanode/client/types';
import { RelationFieldAttributes } from '@colanode/core';
import { RelationFieldAttributes, StringArrayFieldValue } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { RecordSearch } from '@colanode/ui/components/records/record-search';
import { Badge } from '@colanode/ui/components/ui/badge';
@@ -15,6 +15,7 @@ import {
import { Separator } from '@colanode/ui/components/ui/separator';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordRelationValueProps {
field: RelationFieldAttributes;
@@ -37,10 +38,15 @@ export const RecordRelationValue = ({
}: RecordRelationValueProps) => {
const workspace = useWorkspace();
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
{
field,
}
);
const [open, setOpen] = useState(false);
const relationIds = record.getRelationValue(field) ?? [];
const relationIds = useMemo(() => value?.value ?? [], [value]);
const relationsQuery = useLiveQuery(
(q) => {
if (relationIds.length === 0 || !field.databaseId) {
@@ -100,9 +106,9 @@ export const RecordRelationValue = ({
);
if (newRelations.length === 0) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string_array',
value: newRelations,
});
@@ -130,9 +136,9 @@ export const RecordRelationValue = ({
: [...relationIds, selectedRecord.id];
if (newRelations.length === 0) {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string_array',
value: newRelations,
});

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { SelectFieldAttributes } from '@colanode/core';
import { SelectFieldAttributes, StringFieldValue } from '@colanode/core';
import { SelectFieldOptions } from '@colanode/ui/components/databases/fields/select-field-options';
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
import {
@@ -9,6 +9,7 @@ import {
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordSelectValueProps {
field: SelectFieldAttributes;
@@ -20,17 +21,12 @@ export const RecordSelectValue = ({
readOnly,
}: RecordSelectValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
field,
});
const [open, setOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(
record.getSelectValue(field)
);
useEffect(() => {
setSelectedValue(record.getSelectValue(field));
}, [record.localRevision]);
const selectedOption = field.options?.[selectedValue ?? ''];
const selectedOption = field.options?.[value?.value ?? ''];
if (!record.canEdit || readOnly) {
return (
@@ -64,17 +60,14 @@ export const RecordSelectValue = ({
<PopoverContent className="w-80 p-1">
<SelectFieldOptions
field={field}
values={[selectedValue ?? '']}
values={[value?.value ?? '']}
onSelect={(id) => {
if (!record.canEdit || readOnly) return;
setSelectedValue(id);
setOpen(false);
if (selectedValue === id) {
record.removeFieldValue(field);
if (value?.value === id) {
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string',
value: id,
});

View File

@@ -1,6 +1,7 @@
import { TextFieldAttributes } from '@colanode/core';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { TextFieldAttributes, TextFieldValue } from '@colanode/core';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
interface RecordTextValueProps {
field: TextFieldAttributes;
@@ -9,22 +10,26 @@ interface RecordTextValueProps {
export const RecordTextValue = ({ field, readOnly }: RecordTextValueProps) => {
const record = useRecord();
const { value, setValue, clearValue } = useRecordField<TextFieldValue>({
field,
});
return (
<SmartTextInput
value={record.getTextValue(field)}
<Input
value={value?.value ?? ''}
readOnly={!record.canEdit || readOnly}
onChange={(newValue) => {
onChange={(e) => {
const newValue = e.target.value;
if (!record.canEdit || readOnly) return;
if (newValue === record.getTextValue(field)) {
if (newValue === value?.value) {
return;
}
if (newValue === null || newValue === '') {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'text',
value: newValue,
});

View File

@@ -1,13 +1,18 @@
import { ExternalLink } from 'lucide-react';
import { isValidUrl, UrlFieldAttributes } from '@colanode/core';
import {
isValidUrl,
StringFieldValue,
UrlFieldAttributes,
} from '@colanode/core';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@colanode/ui/components/ui/hover-card';
import { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
import { Input } from '@colanode/ui/components/ui/input';
import { useRecord } from '@colanode/ui/contexts/record';
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
import { cn } from '@colanode/ui/lib/utils';
interface RecordUrlValueProps {
@@ -17,17 +22,20 @@ interface RecordUrlValueProps {
export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
const record = useRecord();
const url = record.getUrlValue(field);
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
field,
});
const url = value?.value ?? '';
const canOpen = url && isValidUrl(url);
return (
<HoverCard openDelay={300}>
<HoverCardTrigger>
<SmartTextInput
<Input
value={url}
readOnly={!record.canEdit || readOnly}
onChange={(newValue) => {
onChange={(e) => {
const newValue = e.target.value;
if (!record.canEdit || readOnly) return;
if (newValue === url) {
@@ -35,9 +43,9 @@ export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
}
if (newValue === null || newValue === '') {
record.removeFieldValue(field);
clearValue();
} else {
record.updateFieldValue(field, {
setValue({
type: 'string',
value: newValue,
});
@@ -48,7 +56,7 @@ export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
</HoverCardTrigger>
<HoverCardContent
className={cn(
'flex w-full min-w-80 max-w-128 flex-row items-center justify-between gap-2 overflow-hidden',
'flex w-full min-w-80 max-w-lg flex-row items-center justify-between gap-2 overflow-hidden',
!canOpen && 'hidden'
)}
>

View File

@@ -1,22 +1,6 @@
import { createContext, useContext } from 'react';
import {
BooleanFieldAttributes,
CollaboratorFieldAttributes,
DateFieldAttributes,
EmailFieldAttributes,
FieldAttributes,
FieldValue,
FileFieldAttributes,
MultiSelectFieldAttributes,
NumberFieldAttributes,
PhoneFieldAttributes,
RelationFieldAttributes,
RollupFieldAttributes,
SelectFieldAttributes,
TextFieldAttributes,
UrlFieldAttributes,
} from '@colanode/core';
import { FieldValue } from '@colanode/core';
interface RecordContext {
id: string;
@@ -30,21 +14,6 @@ interface RecordContext {
databaseId: string;
canEdit: boolean;
localRevision: string;
updateFieldValue: (field: FieldAttributes, value: FieldValue) => void;
removeFieldValue: (field: FieldAttributes) => void;
getBooleanValue: (field: BooleanFieldAttributes) => boolean;
getCollaboratorValue: (field: CollaboratorFieldAttributes) => string[] | null;
getDateValue: (field: DateFieldAttributes) => Date | null;
getEmailValue: (field: EmailFieldAttributes) => string | null;
getFileValue: (field: FileFieldAttributes) => string[] | null;
getMultiSelectValue: (field: MultiSelectFieldAttributes) => string[];
getNumberValue: (field: NumberFieldAttributes) => number | null;
getPhoneValue: (field: PhoneFieldAttributes) => string | null;
getRelationValue: (field: RelationFieldAttributes) => string[] | null;
getRollupValue: (field: RollupFieldAttributes) => string | null;
getSelectValue: (field: SelectFieldAttributes) => string | null;
getTextValue: (field: TextFieldAttributes) => string | null;
getUrlValue: (field: UrlFieldAttributes) => string | null;
}
export const RecordContext = createContext<RecordContext>({} as RecordContext);

View File

@@ -0,0 +1,44 @@
import { debounceStrategy, usePacedMutations } from '@tanstack/react-db';
import { useCallback, useMemo } from 'react';
import { LocalNode } from '@colanode/client/types';
import { FieldAttributes, FieldValue } from '@colanode/core';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
interface Options {
field: FieldAttributes;
}
export const useRecordField = <T extends FieldValue>({ field }: Options) => {
const record = useRecord();
const workspace = useWorkspace();
const mutate = usePacedMutations<T | null, LocalNode>({
onMutate: (nextValue) => {
workspace.collections.nodes.update(record.id, (draft) => {
if (draft.type !== 'record') return;
if (nextValue === null) {
const { [field.id]: _removed, ...rest } = draft.fields;
draft.fields = rest;
} else {
draft.fields[field.id] = nextValue;
}
});
},
mutationFn: async ({ transaction }) => {
await applyNodeTransaction(workspace.userId, transaction);
},
strategy: debounceStrategy({ wait: 500 }),
});
const value = useMemo(() => {
return (record.fields[field.id] as T | undefined) ?? null;
}, [record.fields, field.id]) as T | null;
const setValue = useCallback((next: T) => mutate(next), [mutate]);
const clearValue = useCallback(() => mutate(null), [mutate]);
return { value, setValue, clearValue };
};