From 2f59d19d72b7101a47b187950e77eda152b752f1 Mon Sep 17 00:00:00 2001 From: Hakan Shehu Date: Fri, 21 Nov 2025 11:40:35 -0800 Subject: [PATCH] Use paced mutations for record updates --- .../components/records/record-attributes.tsx | 2 +- .../ui/src/components/records/record-name.tsx | 41 ++++-- .../components/records/record-provider.tsx | 137 ------------------ .../records/values/record-boolean-value.tsx | 21 +-- .../values/record-collaborator-value.tsx | 23 ++- .../records/values/record-date-value.tsx | 12 +- .../records/values/record-email-value.tsx | 19 ++- .../values/record-multi-select-value.tsx | 35 ++--- .../records/values/record-number-value.tsx | 38 +++-- .../records/values/record-phone-value.tsx | 21 ++- .../records/values/record-relation-value.tsx | 20 ++- .../records/values/record-select-value.tsx | 29 ++-- .../records/values/record-text-value.tsx | 21 ++- .../records/values/record-url-value.tsx | 26 ++-- packages/ui/src/contexts/record.ts | 33 +---- packages/ui/src/hooks/use-record-field.tsx | 44 ++++++ 16 files changed, 225 insertions(+), 297 deletions(-) create mode 100644 packages/ui/src/hooks/use-record-field.tsx diff --git a/packages/ui/src/components/records/record-attributes.tsx b/packages/ui/src/components/records/record-attributes.tsx index 065c483c..ce16819c 100644 --- a/packages/ui/src/components/records/record-attributes.tsx +++ b/packages/ui/src/components/records/record-attributes.tsx @@ -24,7 +24,7 @@ export const RecordAttributes = () => {
-
+
diff --git a/packages/ui/src/components/records/record-name.tsx b/packages/ui/src/components/records/record-name.tsx index 1f28b632..60fc58d1 100644 --- a/packages/ui/src/components/records/record-name.tsx +++ b/packages/ui/src/components/records/record-name.tsx @@ -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 ( - { - if (value === record.name) { + const mutate = usePacedMutations({ + 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 ( + { + 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" /> ); diff --git a/packages/ui/src/components/records/record-provider.tsx b/packages/ui/src/components/records/record-provider.tsx index d463bfec..89aaa796 100644 --- a/packages/ui/src/components/records/record-provider.tsx +++ b/packages/ui/src/components/records/record-provider.tsx @@ -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} diff --git a/packages/ui/src/components/records/values/record-boolean-value.tsx b/packages/ui/src/components/records/values/record-boolean-value.tsx index f8bf6d0c..042d4fda 100644 --- a/packages/ui/src/components/records/values/record-boolean-value.tsx +++ b/packages/ui/src/components/records/values/record-boolean-value.tsx @@ -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(record.getBooleanValue(field)); - - useEffect(() => { - setInput(record.getBooleanValue(field)); - }, [record.localRevision]); + const { value, setValue, clearValue } = useRecordField({ + field, + }); return (
{ 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(); } } }} diff --git a/packages/ui/src/components/records/values/record-collaborator-value.tsx b/packages/ui/src/components/records/values/record-collaborator-value.tsx index 9e70007e..eae512da 100644 --- a/packages/ui/src/components/records/values/record-collaborator-value.tsx +++ b/packages/ui/src/components/records/values/record-collaborator-value.tsx @@ -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( + { + 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, }); diff --git a/packages/ui/src/components/records/values/record-date-value.tsx b/packages/ui/src/components/records/values/record-date-value.tsx index bd27d84a..12658022 100644 --- a/packages/ui/src/components/records/values/record-date-value.tsx +++ b/packages/ui/src/components/records/values/record-date-value.tsx @@ -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({ + field, + }); return ( { if (!record.canEdit || readOnly) return; if (newValue === null || newValue === undefined) { - record.removeFieldValue(field); + clearValue(); } else { - record.updateFieldValue(field, { + setValue({ type: 'string', value: newValue.toISOString(), }); diff --git a/packages/ui/src/components/records/values/record-email-value.tsx b/packages/ui/src/components/records/values/record-email-value.tsx index b3829f38..5ab6f1bf 100644 --- a/packages/ui/src/components/records/values/record-email-value.tsx +++ b/packages/ui/src/components/records/values/record-email-value.tsx @@ -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({ + field, + }); return ( - { + 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, }); diff --git a/packages/ui/src/components/records/values/record-multi-select-value.tsx b/packages/ui/src/components/records/values/record-multi-select-value.tsx index 3c94fca6..f4f5bbb0 100644 --- a/packages/ui/src/components/records/values/record-multi-select-value.tsx +++ b/packages/ui/src/components/records/values/record-multi-select-value.tsx @@ -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( + { + 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 = ({ { 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, }); diff --git a/packages/ui/src/components/records/values/record-number-value.tsx b/packages/ui/src/components/records/values/record-number-value.tsx index 3f366b6c..f6562e6c 100644 --- a/packages/ui/src/components/records/values/record-number-value.tsx +++ b/packages/ui/src/components/records/values/record-number-value.tsx @@ -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({ + field, + }); return ( - { + 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" /> diff --git a/packages/ui/src/components/records/values/record-phone-value.tsx b/packages/ui/src/components/records/values/record-phone-value.tsx index ef59f729..b8d2685c 100644 --- a/packages/ui/src/components/records/values/record-phone-value.tsx +++ b/packages/ui/src/components/records/values/record-phone-value.tsx @@ -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({ + field, + }); return ( - { + 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, }); diff --git a/packages/ui/src/components/records/values/record-relation-value.tsx b/packages/ui/src/components/records/values/record-relation-value.tsx index 52bb1843..e9742a89 100644 --- a/packages/ui/src/components/records/values/record-relation-value.tsx +++ b/packages/ui/src/components/records/values/record-relation-value.tsx @@ -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( + { + 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, }); diff --git a/packages/ui/src/components/records/values/record-select-value.tsx b/packages/ui/src/components/records/values/record-select-value.tsx index 47d6918a..f5427b2b 100644 --- a/packages/ui/src/components/records/values/record-select-value.tsx +++ b/packages/ui/src/components/records/values/record-select-value.tsx @@ -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({ + 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 = ({ { 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, }); diff --git a/packages/ui/src/components/records/values/record-text-value.tsx b/packages/ui/src/components/records/values/record-text-value.tsx index 984f66d7..3b9d8576 100644 --- a/packages/ui/src/components/records/values/record-text-value.tsx +++ b/packages/ui/src/components/records/values/record-text-value.tsx @@ -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({ + field, + }); return ( - { + 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, }); diff --git a/packages/ui/src/components/records/values/record-url-value.tsx b/packages/ui/src/components/records/values/record-url-value.tsx index dcf7d0f5..e06d38fa 100644 --- a/packages/ui/src/components/records/values/record-url-value.tsx +++ b/packages/ui/src/components/records/values/record-url-value.tsx @@ -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({ + field, + }); + const url = value?.value ?? ''; const canOpen = url && isValidUrl(url); return ( - { + 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) => { diff --git a/packages/ui/src/contexts/record.ts b/packages/ui/src/contexts/record.ts index e0878ec5..87b5b94a 100644 --- a/packages/ui/src/contexts/record.ts +++ b/packages/ui/src/contexts/record.ts @@ -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({} as RecordContext); diff --git a/packages/ui/src/hooks/use-record-field.tsx b/packages/ui/src/hooks/use-record-field.tsx new file mode 100644 index 00000000..8ff74f00 --- /dev/null +++ b/packages/ui/src/hooks/use-record-field.tsx @@ -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 = ({ field }: Options) => { + const record = useRecord(); + const workspace = useWorkspace(); + + const mutate = usePacedMutations({ + 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 }; +};