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 };
+};