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