mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Use paced mutations for record updates
This commit is contained in:
@@ -24,7 +24,7 @@ export const RecordAttributes = () => {
|
|||||||
<div className="w-60 max-w-60">
|
<div className="w-60 max-w-60">
|
||||||
<RecordField field={field} />
|
<RecordField field={field} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 max-w-128 p-1">
|
<div className="flex-1 max-w-lg p-1">
|
||||||
<RecordFieldValue field={field} />
|
<RecordFieldValue field={field} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { debounceStrategy, usePacedMutations } from '@tanstack/react-db';
|
||||||
import { useEffect, useRef } from 'react';
|
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 { useRecord } from '@colanode/ui/contexts/record';
|
||||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||||
|
import { applyNodeTransaction } from '@colanode/ui/lib/nodes';
|
||||||
|
|
||||||
export const RecordName = () => {
|
export const RecordName = () => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
@@ -20,26 +23,32 @@ export const RecordName = () => {
|
|||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
}, [record.canEdit, inputRef]);
|
}, [record.canEdit, inputRef]);
|
||||||
|
|
||||||
return (
|
const mutate = usePacedMutations<string, LocalNode>({
|
||||||
<SmartTextInput
|
onMutate: (value) => {
|
||||||
value={record.name}
|
workspace.collections.nodes.update(record.id, (draft) => {
|
||||||
readOnly={!record.canEdit}
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (value === record.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = workspace.collections.nodes;
|
|
||||||
nodes.update(record.id, (draft) => {
|
|
||||||
if (draft.type !== 'record') {
|
if (draft.type !== 'record') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
draft.name = value;
|
draft.name = value;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
mutationFn: async ({ transaction }) => {
|
||||||
|
await applyNodeTransaction(workspace.userId, transaction);
|
||||||
|
},
|
||||||
|
strategy: debounceStrategy({ wait: 500 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
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"
|
placeholder="Unnamed"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
import { LocalRecordNode } from '@colanode/client/types';
|
import { LocalRecordNode } from '@colanode/client/types';
|
||||||
import { NodeRole, hasNodeRole } from '@colanode/core';
|
import { NodeRole, hasNodeRole } from '@colanode/core';
|
||||||
import { RecordContext } from '@colanode/ui/contexts/record';
|
import { RecordContext } from '@colanode/ui/contexts/record';
|
||||||
@@ -33,141 +31,6 @@ export const RecordProvider = ({
|
|||||||
databaseId: record.databaseId,
|
databaseId: record.databaseId,
|
||||||
localRevision: record.localRevision,
|
localRevision: record.localRevision,
|
||||||
canEdit,
|
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}
|
{children}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { BooleanFieldAttributes, BooleanFieldValue } from '@colanode/core';
|
||||||
|
|
||||||
import { BooleanFieldAttributes } from '@colanode/core';
|
|
||||||
import { Checkbox } from '@colanode/ui/components/ui/checkbox';
|
import { Checkbox } from '@colanode/ui/components/ui/checkbox';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordBooleanValueProps {
|
interface RecordBooleanValueProps {
|
||||||
field: BooleanFieldAttributes;
|
field: BooleanFieldAttributes;
|
||||||
@@ -14,31 +13,27 @@ export const RecordBooleanValue = ({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: RecordBooleanValueProps) => {
|
}: RecordBooleanValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<BooleanFieldValue>({
|
||||||
const [input, setInput] = useState<boolean>(record.getBooleanValue(field));
|
field,
|
||||||
|
});
|
||||||
useEffect(() => {
|
|
||||||
setInput(record.getBooleanValue(field));
|
|
||||||
}, [record.localRevision]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-row items-center justify-start p-0">
|
<div className="flex h-full w-full flex-row items-center justify-start p-0">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={input}
|
checked={value?.value ?? false}
|
||||||
disabled={!record.canEdit || readOnly}
|
disabled={!record.canEdit || readOnly}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (typeof e === 'boolean') {
|
if (typeof e === 'boolean') {
|
||||||
setInput(e.valueOf());
|
|
||||||
const checked = e.valueOf();
|
const checked = e.valueOf();
|
||||||
if (checked) {
|
if (checked) {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
value: checked,
|
value: checked,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { inArray, useLiveQuery } from '@tanstack/react-db';
|
import { inArray, useLiveQuery } from '@tanstack/react-db';
|
||||||
import { X } from 'lucide-react';
|
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 { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||||
import { Badge } from '@colanode/ui/components/ui/badge';
|
import { Badge } from '@colanode/ui/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +17,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
|
|||||||
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface CollaboratorBadgeProps {
|
interface CollaboratorBadgeProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -41,10 +45,15 @@ export const RecordCollaboratorValue = ({
|
|||||||
}: RecordCollaboratorValueProps) => {
|
}: RecordCollaboratorValueProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
|
||||||
|
{
|
||||||
|
field,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const collaboratorIds = record.getCollaboratorValue(field) ?? [];
|
const collaboratorIds = useMemo(() => value?.value ?? [], [value]);
|
||||||
const collaboratorsQuery = useLiveQuery(
|
const collaboratorsQuery = useLiveQuery(
|
||||||
(q) =>
|
(q) =>
|
||||||
q
|
q
|
||||||
@@ -115,9 +124,9 @@ export const RecordCollaboratorValue = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (newCollaborators.length === 0) {
|
if (newCollaborators.length === 0) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string_array',
|
type: 'string_array',
|
||||||
value: newCollaborators,
|
value: newCollaborators,
|
||||||
});
|
});
|
||||||
@@ -141,9 +150,9 @@ export const RecordCollaboratorValue = ({
|
|||||||
: [...collaboratorIds, user.id];
|
: [...collaboratorIds, user.id];
|
||||||
|
|
||||||
if (newCollaborators.length === 0) {
|
if (newCollaborators.length === 0) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string_array',
|
type: 'string_array',
|
||||||
value: newCollaborators,
|
value: newCollaborators,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { DatePicker } from '@colanode/ui/components/ui/date-picker';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordDateValueProps {
|
interface RecordDateValueProps {
|
||||||
field: DateFieldAttributes;
|
field: DateFieldAttributes;
|
||||||
@@ -9,18 +10,21 @@ interface RecordDateValueProps {
|
|||||||
|
|
||||||
export const RecordDateValue = ({ field, readOnly }: RecordDateValueProps) => {
|
export const RecordDateValue = ({ field, readOnly }: RecordDateValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
value={record.getDateValue(field)}
|
value={value ? new Date(value.value) : null}
|
||||||
readonly={!record.canEdit || readOnly}
|
readonly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (newValue === null || newValue === undefined) {
|
if (newValue === null || newValue === undefined) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: newValue.toISOString(),
|
value: newValue.toISOString(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EmailFieldAttributes } from '@colanode/core';
|
import { EmailFieldAttributes, StringFieldValue } from '@colanode/core';
|
||||||
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 { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordEmailValueProps {
|
interface RecordEmailValueProps {
|
||||||
field: EmailFieldAttributes;
|
field: EmailFieldAttributes;
|
||||||
@@ -12,18 +13,22 @@ export const RecordEmailValue = ({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: RecordEmailValueProps) => {
|
}: RecordEmailValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartTextInput
|
<Input
|
||||||
value={record.getEmailValue(field)}
|
value={value?.value ?? ''}
|
||||||
readOnly={!record.canEdit || readOnly}
|
readOnly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
if (!record.canEdit) return;
|
if (!record.canEdit) return;
|
||||||
|
|
||||||
if (newValue === null || newValue === '') {
|
if (newValue === null || newValue === '') {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { SelectFieldOptions } from '@colanode/ui/components/databases/fields/select-field-options';
|
||||||
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
|
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +12,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@colanode/ui/components/ui/popover';
|
} from '@colanode/ui/components/ui/popover';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordMultiSelectValueProps {
|
interface RecordMultiSelectValueProps {
|
||||||
field: MultiSelectFieldAttributes;
|
field: MultiSelectFieldAttributes;
|
||||||
@@ -22,17 +26,16 @@ export const RecordMultiSelectValue = ({
|
|||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedValues, setSelectedValues] = useState(
|
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
|
||||||
record.getMultiSelectValue(field)
|
{
|
||||||
|
field,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedValues(record.getMultiSelectValue(field));
|
|
||||||
}, [record.localRevision]);
|
|
||||||
|
|
||||||
const selectOptions = Object.values(field.options ?? {});
|
const selectOptions = Object.values(field.options ?? {});
|
||||||
|
const selectedOptionIds = value?.value ?? [];
|
||||||
const selectedOptions = selectOptions.filter((option) =>
|
const selectedOptions = selectOptions.filter((option) =>
|
||||||
selectedValues.includes(option.id)
|
selectedOptionIds.includes(option.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!record.canEdit || readOnly) {
|
if (!record.canEdit || readOnly) {
|
||||||
@@ -67,20 +70,18 @@ export const RecordMultiSelectValue = ({
|
|||||||
<PopoverContent className="w-80 p-1">
|
<PopoverContent className="w-80 p-1">
|
||||||
<SelectFieldOptions
|
<SelectFieldOptions
|
||||||
field={field}
|
field={field}
|
||||||
values={selectedValues}
|
values={selectedOptionIds}
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
const newValues = selectedValues.includes(id)
|
const newValues = selectedOptionIds.includes(id)
|
||||||
? selectedValues.filter((v) => v !== id)
|
? selectedOptionIds.filter((v) => v !== id)
|
||||||
: [...selectedValues, id];
|
: [...selectedOptionIds, id];
|
||||||
|
|
||||||
setSelectedValues(newValues);
|
|
||||||
|
|
||||||
if (newValues.length === 0) {
|
if (newValues.length === 0) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string_array',
|
type: 'string_array',
|
||||||
value: newValues,
|
value: newValues,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type NumberFieldAttributes } from '@colanode/core';
|
import { NumberFieldValue, type NumberFieldAttributes } from '@colanode/core';
|
||||||
import { SmartNumberInput } from '@colanode/ui/components/ui/smart-number-input';
|
import { Input } from '@colanode/ui/components/ui/input';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordNumberValueProps {
|
interface RecordNumberValueProps {
|
||||||
field: NumberFieldAttributes;
|
field: NumberFieldAttributes;
|
||||||
@@ -12,26 +13,37 @@ export const RecordNumberValue = ({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: RecordNumberValueProps) => {
|
}: RecordNumberValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<NumberFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartNumberInput
|
<Input
|
||||||
value={record.getNumberValue(field)}
|
value={value?.value ?? undefined}
|
||||||
readOnly={!record.canEdit || readOnly}
|
readOnly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(e) => {
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (newValue === record.getNumberValue(field)) {
|
const newStringValue = e.target.value;
|
||||||
|
if (newStringValue === null || newStringValue === '') {
|
||||||
|
clearValue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === null) {
|
const newValue = parseFloat(newStringValue);
|
||||||
record.removeFieldValue(field);
|
console.log('newValue', newValue, newStringValue);
|
||||||
} else {
|
if (isNaN(newValue)) {
|
||||||
record.updateFieldValue(field, {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue === value?.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue({
|
||||||
type: 'number',
|
type: 'number',
|
||||||
value: newValue,
|
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"
|
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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PhoneFieldAttributes } from '@colanode/core';
|
import { PhoneFieldAttributes, StringFieldValue } from '@colanode/core';
|
||||||
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 { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordPhoneValueProps {
|
interface RecordPhoneValueProps {
|
||||||
field: PhoneFieldAttributes;
|
field: PhoneFieldAttributes;
|
||||||
@@ -12,22 +13,26 @@ export const RecordPhoneValue = ({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: RecordPhoneValueProps) => {
|
}: RecordPhoneValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartTextInput
|
<Input
|
||||||
value={record.getPhoneValue(field)}
|
value={value?.value ?? ''}
|
||||||
readOnly={!record.canEdit || readOnly}
|
readOnly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (newValue === record.getPhoneValue(field)) {
|
if (newValue === value?.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === null || newValue === '') {
|
if (newValue === null || newValue === '') {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
|
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { LocalRecordNode } from '@colanode/client/types';
|
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 { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||||
import { RecordSearch } from '@colanode/ui/components/records/record-search';
|
import { RecordSearch } from '@colanode/ui/components/records/record-search';
|
||||||
import { Badge } from '@colanode/ui/components/ui/badge';
|
import { Badge } from '@colanode/ui/components/ui/badge';
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordRelationValueProps {
|
interface RecordRelationValueProps {
|
||||||
field: RelationFieldAttributes;
|
field: RelationFieldAttributes;
|
||||||
@@ -37,10 +38,15 @@ export const RecordRelationValue = ({
|
|||||||
}: RecordRelationValueProps) => {
|
}: RecordRelationValueProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringArrayFieldValue>(
|
||||||
|
{
|
||||||
|
field,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const relationIds = record.getRelationValue(field) ?? [];
|
const relationIds = useMemo(() => value?.value ?? [], [value]);
|
||||||
const relationsQuery = useLiveQuery(
|
const relationsQuery = useLiveQuery(
|
||||||
(q) => {
|
(q) => {
|
||||||
if (relationIds.length === 0 || !field.databaseId) {
|
if (relationIds.length === 0 || !field.databaseId) {
|
||||||
@@ -100,9 +106,9 @@ export const RecordRelationValue = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (newRelations.length === 0) {
|
if (newRelations.length === 0) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string_array',
|
type: 'string_array',
|
||||||
value: newRelations,
|
value: newRelations,
|
||||||
});
|
});
|
||||||
@@ -130,9 +136,9 @@ export const RecordRelationValue = ({
|
|||||||
: [...relationIds, selectedRecord.id];
|
: [...relationIds, selectedRecord.id];
|
||||||
|
|
||||||
if (newRelations.length === 0) {
|
if (newRelations.length === 0) {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string_array',
|
type: 'string_array',
|
||||||
value: newRelations,
|
value: newRelations,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { SelectFieldOptions } from '@colanode/ui/components/databases/fields/select-field-options';
|
||||||
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
|
import { SelectOptionBadge } from '@colanode/ui/components/databases/fields/select-option-badge';
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@colanode/ui/components/ui/popover';
|
} from '@colanode/ui/components/ui/popover';
|
||||||
import { useRecord } from '@colanode/ui/contexts/record';
|
import { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordSelectValueProps {
|
interface RecordSelectValueProps {
|
||||||
field: SelectFieldAttributes;
|
field: SelectFieldAttributes;
|
||||||
@@ -20,17 +21,12 @@ export const RecordSelectValue = ({
|
|||||||
readOnly,
|
readOnly,
|
||||||
}: RecordSelectValueProps) => {
|
}: RecordSelectValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedValue, setSelectedValue] = useState(
|
const selectedOption = field.options?.[value?.value ?? ''];
|
||||||
record.getSelectValue(field)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedValue(record.getSelectValue(field));
|
|
||||||
}, [record.localRevision]);
|
|
||||||
|
|
||||||
const selectedOption = field.options?.[selectedValue ?? ''];
|
|
||||||
|
|
||||||
if (!record.canEdit || readOnly) {
|
if (!record.canEdit || readOnly) {
|
||||||
return (
|
return (
|
||||||
@@ -64,17 +60,14 @@ export const RecordSelectValue = ({
|
|||||||
<PopoverContent className="w-80 p-1">
|
<PopoverContent className="w-80 p-1">
|
||||||
<SelectFieldOptions
|
<SelectFieldOptions
|
||||||
field={field}
|
field={field}
|
||||||
values={[selectedValue ?? '']}
|
values={[value?.value ?? '']}
|
||||||
onSelect={(id) => {
|
onSelect={(id) => {
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
setSelectedValue(id);
|
if (value?.value === id) {
|
||||||
setOpen(false);
|
clearValue();
|
||||||
|
|
||||||
if (selectedValue === id) {
|
|
||||||
record.removeFieldValue(field);
|
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: id,
|
value: id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TextFieldAttributes } from '@colanode/core';
|
import { TextFieldAttributes, TextFieldValue } from '@colanode/core';
|
||||||
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 { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
|
|
||||||
interface RecordTextValueProps {
|
interface RecordTextValueProps {
|
||||||
field: TextFieldAttributes;
|
field: TextFieldAttributes;
|
||||||
@@ -9,22 +10,26 @@ interface RecordTextValueProps {
|
|||||||
|
|
||||||
export const RecordTextValue = ({ field, readOnly }: RecordTextValueProps) => {
|
export const RecordTextValue = ({ field, readOnly }: RecordTextValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<TextFieldValue>({
|
||||||
|
field,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SmartTextInput
|
<Input
|
||||||
value={record.getTextValue(field)}
|
value={value?.value ?? ''}
|
||||||
readOnly={!record.canEdit || readOnly}
|
readOnly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (newValue === record.getTextValue(field)) {
|
if (newValue === value?.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === null || newValue === '') {
|
if (newValue === null || newValue === '') {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
import { isValidUrl, UrlFieldAttributes } from '@colanode/core';
|
import {
|
||||||
|
isValidUrl,
|
||||||
|
StringFieldValue,
|
||||||
|
UrlFieldAttributes,
|
||||||
|
} from '@colanode/core';
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '@colanode/ui/components/ui/hover-card';
|
} 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 { useRecord } from '@colanode/ui/contexts/record';
|
||||||
|
import { useRecordField } from '@colanode/ui/hooks/use-record-field';
|
||||||
import { cn } from '@colanode/ui/lib/utils';
|
import { cn } from '@colanode/ui/lib/utils';
|
||||||
|
|
||||||
interface RecordUrlValueProps {
|
interface RecordUrlValueProps {
|
||||||
@@ -17,17 +22,20 @@ interface RecordUrlValueProps {
|
|||||||
|
|
||||||
export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
|
export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
const { value, setValue, clearValue } = useRecordField<StringFieldValue>({
|
||||||
const url = record.getUrlValue(field);
|
field,
|
||||||
|
});
|
||||||
|
const url = value?.value ?? '';
|
||||||
const canOpen = url && isValidUrl(url);
|
const canOpen = url && isValidUrl(url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard openDelay={300}>
|
<HoverCard openDelay={300}>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
<SmartTextInput
|
<Input
|
||||||
value={url}
|
value={url}
|
||||||
readOnly={!record.canEdit || readOnly}
|
readOnly={!record.canEdit || readOnly}
|
||||||
onChange={(newValue) => {
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
if (!record.canEdit || readOnly) return;
|
if (!record.canEdit || readOnly) return;
|
||||||
|
|
||||||
if (newValue === url) {
|
if (newValue === url) {
|
||||||
@@ -35,9 +43,9 @@ export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === null || newValue === '') {
|
if (newValue === null || newValue === '') {
|
||||||
record.removeFieldValue(field);
|
clearValue();
|
||||||
} else {
|
} else {
|
||||||
record.updateFieldValue(field, {
|
setValue({
|
||||||
type: 'string',
|
type: 'string',
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
@@ -48,7 +56,7 @@ export const RecordUrlValue = ({ field, readOnly }: RecordUrlValueProps) => {
|
|||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent
|
<HoverCardContent
|
||||||
className={cn(
|
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'
|
!canOpen && 'hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
import {
|
import { FieldValue } from '@colanode/core';
|
||||||
BooleanFieldAttributes,
|
|
||||||
CollaboratorFieldAttributes,
|
|
||||||
DateFieldAttributes,
|
|
||||||
EmailFieldAttributes,
|
|
||||||
FieldAttributes,
|
|
||||||
FieldValue,
|
|
||||||
FileFieldAttributes,
|
|
||||||
MultiSelectFieldAttributes,
|
|
||||||
NumberFieldAttributes,
|
|
||||||
PhoneFieldAttributes,
|
|
||||||
RelationFieldAttributes,
|
|
||||||
RollupFieldAttributes,
|
|
||||||
SelectFieldAttributes,
|
|
||||||
TextFieldAttributes,
|
|
||||||
UrlFieldAttributes,
|
|
||||||
} from '@colanode/core';
|
|
||||||
|
|
||||||
interface RecordContext {
|
interface RecordContext {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,21 +14,6 @@ interface RecordContext {
|
|||||||
databaseId: string;
|
databaseId: string;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
localRevision: string;
|
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);
|
export const RecordContext = createContext<RecordContext>({} as RecordContext);
|
||||||
|
|||||||
44
packages/ui/src/hooks/use-record-field.tsx
Normal file
44
packages/ui/src/hooks/use-record-field.tsx
Normal 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 };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user