diff --git a/packages/client/src/lib/records.ts b/packages/client/src/lib/records.ts
index 998be22e..86694104 100644
--- a/packages/client/src/lib/records.ts
+++ b/packages/client/src/lib/records.ts
@@ -19,6 +19,7 @@ import {
CreatedByFieldAttributes,
UpdatedAtFieldAttributes,
UpdatedByFieldAttributes,
+ RelationFieldAttributes,
} from '@colanode/core';
type SqliteOperator =
@@ -94,6 +95,8 @@ const buildFilterQuery = (
return buildNumberFilterQuery(filter, field);
case 'phone':
return buildPhoneFilterQuery(filter, field);
+ case 'relation':
+ return buildRelationFilterQuery(filter, field);
case 'select':
return buildSelectFilterQuery(filter, field);
case 'text':
@@ -377,6 +380,36 @@ const buildPhoneFilterQuery = (
}
};
+const buildRelationFilterQuery = (
+ filter: DatabaseViewFieldFilterAttributes,
+ field: RelationFieldAttributes
+): string | null => {
+ if (filter.operator === 'is_empty') {
+ return buildFieldArrayIsEmptyFilterQuery(field.id);
+ }
+
+ if (filter.operator === 'is_not_empty') {
+ return buildFieldArrayIsNotEmptyFilterQuery(field.id);
+ }
+
+ if (!isStringArray(filter.value)) {
+ return null;
+ }
+
+ if (filter.value.length === 0) {
+ return null;
+ }
+
+ switch (filter.operator) {
+ case 'is_in':
+ return buildArrayFieldContainsFilterQuery(field.id, filter.value);
+ case 'is_not_in':
+ return buildArrayFieldDoesNotContainFilterQuery(field.id, filter.value);
+ default:
+ return null;
+ }
+};
+
const buildUrlFilterQuery = (
filter: DatabaseViewFieldFilterAttributes,
field: UrlFieldAttributes
diff --git a/packages/ui/src/components/databases/search/view-filters.tsx b/packages/ui/src/components/databases/search/view-filters.tsx
index 28d86b19..a3a057e1 100644
--- a/packages/ui/src/components/databases/search/view-filters.tsx
+++ b/packages/ui/src/components/databases/search/view-filters.tsx
@@ -12,6 +12,7 @@ import { ViewMultiSelectFieldFilter } from '@colanode/ui/components/databases/se
import { ViewNameFieldFilter } from '@colanode/ui/components/databases/search/view-name-field-filter';
import { ViewNumberFieldFilter } from '@colanode/ui/components/databases/search/view-number-field-filter';
import { ViewPhoneFieldFilter } from '@colanode/ui/components/databases/search/view-phone-field-filter';
+import { ViewRelationFieldFilter } from '@colanode/ui/components/databases/search/view-relation-field-filter';
import { ViewSelectFieldFilter } from '@colanode/ui/components/databases/search/view-select-field-filter';
import { ViewTextFieldFilter } from '@colanode/ui/components/databases/search/view-text-field-filter';
import { ViewUpdatedAtFieldFilter } from '@colanode/ui/components/databases/search/view-updated-at-field-filter';
@@ -160,6 +161,14 @@ export const ViewFilters = () => {
filter={filter}
/>
);
+ case 'relation':
+ return (
+
+ );
default:
return null;
diff --git a/packages/ui/src/components/databases/search/view-relation-field-filter.tsx b/packages/ui/src/components/databases/search/view-relation-field-filter.tsx
new file mode 100644
index 00000000..a0370721
--- /dev/null
+++ b/packages/ui/src/components/databases/search/view-relation-field-filter.tsx
@@ -0,0 +1,223 @@
+import { ChevronDown, Trash2, X } from 'lucide-react';
+
+import { LocalRecordNode } from '@colanode/client/types';
+import {
+ DatabaseViewFieldFilterAttributes,
+ RelationFieldAttributes,
+} from '@colanode/core';
+import { Avatar } from '@colanode/ui/components/avatars/avatar';
+import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
+import { RecordSearch } from '@colanode/ui/components/records/record-search';
+import { Badge } from '@colanode/ui/components/ui/badge';
+import { Button } from '@colanode/ui/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@colanode/ui/components/ui/dropdown-menu';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@colanode/ui/components/ui/popover';
+import { Separator } from '@colanode/ui/components/ui/separator';
+import { useDatabaseView } from '@colanode/ui/contexts/database-view';
+import { useWorkspace } from '@colanode/ui/contexts/workspace';
+import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
+import { relationFieldFilterOperators } from '@colanode/ui/lib/databases';
+
+interface ViewRelationFieldFilterProps {
+ field: RelationFieldAttributes;
+ filter: DatabaseViewFieldFilterAttributes;
+}
+
+const RelationBadge = ({ record }: { record: LocalRecordNode }) => {
+ const name = record.attributes.name ?? 'Unnamed';
+ return (
+
+ );
+};
+
+const isOperatorWithoutValue = (operator: string) => {
+ return operator === 'is_empty' || operator === 'is_not_empty';
+};
+
+export const ViewRelationFieldFilter = ({
+ field,
+ filter,
+}: ViewRelationFieldFilterProps) => {
+ const workspace = useWorkspace();
+ const view = useDatabaseView();
+
+ const operator =
+ relationFieldFilterOperators.find(
+ (operator) => operator.value === filter.operator
+ ) ?? relationFieldFilterOperators[0]!;
+
+ const relationIds = (filter.value as string[]) ?? [];
+ const results = useLiveQueries(
+ relationIds.map((id) => ({
+ type: 'node.get',
+ nodeId: id,
+ accountId: workspace.accountId,
+ workspaceId: workspace.id,
+ }))
+ );
+
+ const relations: LocalRecordNode[] = [];
+ for (const result of results) {
+ if (result.data && result.data.type === 'record') {
+ relations.push(result.data);
+ }
+ }
+
+ const hideInput = isOperatorWithoutValue(operator.value);
+
+ if (!field.databaseId) {
+ return null;
+ }
+
+ return (
+ {
+ if (view.isFieldFilterOpened(filter.id)) {
+ view.closeFieldFilter(filter.id);
+ } else {
+ view.openFieldFilter(filter.id);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {relationFieldFilterOperators.map((operator) => (
+ {
+ const value = isOperatorWithoutValue(operator.value)
+ ? []
+ : relationIds;
+
+ view.updateFilter(filter.id, {
+ ...filter,
+ operator: operator.value,
+ value: value,
+ });
+ }}
+ >
+ {operator.label}
+
+ ))}
+
+
+
+
+ {!hideInput && (
+
+
+
+ {relations.slice(0, 1).map((relation) => (
+
+ ))}
+ {relations.length === 0 && (
+
+ No records selected
+
+ )}
+ {relations.length > 1 && (
+
+ +{relations.length - 1}
+
+ )}
+
+
+
+ {relations.length > 0 && (
+
+ {relations.map((relation) => (
+
+
+ {
+ const newRelations = relationIds.filter(
+ (id) => id !== relation.id
+ );
+
+ view.updateFilter(filter.id, {
+ ...filter,
+ value: newRelations,
+ });
+ }}
+ />
+
+ ))}
+
+
+ )}
+ {
+ const newRelations = relationIds.includes(record.id)
+ ? relationIds.filter((id) => id !== record.id)
+ : [...relationIds, record.id];
+
+ view.updateFilter(filter.id, {
+ ...filter,
+ value: newRelations,
+ });
+ }}
+ />
+
+
+ )}
+
+
+ );
+};
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 c6c525ee..9069c5dd 100644
--- a/packages/ui/src/components/records/values/record-relation-value.tsx
+++ b/packages/ui/src/components/records/values/record-relation-value.tsx
@@ -1,7 +1,8 @@
import { X } from 'lucide-react';
import { Fragment, useState } from 'react';
-import { RecordNode, RelationFieldAttributes } from '@colanode/core';
+import { LocalRecordNode } from '@colanode/client/types';
+import { RelationFieldAttributes } 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';
@@ -20,7 +21,7 @@ interface RecordRelationValueProps {
readOnly?: boolean;
}
-const RelationBadge = ({ record }: { record: RecordNode }) => {
+const RelationBadge = ({ record }: { record: LocalRecordNode }) => {
const name = record.attributes.name ?? 'Unnamed';
return (
@@ -54,7 +55,7 @@ export const RecordRelationValue = ({
}))
);
- const relations: RecordNode[] = [];
+ const relations: LocalRecordNode[] = [];
for (const result of results) {
if (result.data && result.data.type === 'record') {
relations.push(result.data);
diff --git a/packages/ui/src/lib/databases.ts b/packages/ui/src/lib/databases.ts
index de912597..df741b46 100644
--- a/packages/ui/src/lib/databases.ts
+++ b/packages/ui/src/lib/databases.ts
@@ -426,6 +426,25 @@ export const phoneFieldFilterOperators: FieldFilterOperator[] = [
},
];
+export const relationFieldFilterOperators: FieldFilterOperator[] = [
+ {
+ label: 'Is In',
+ value: 'is_in',
+ },
+ {
+ label: 'Is Not In',
+ value: 'is_not_in',
+ },
+ {
+ label: 'Is Empty',
+ value: 'is_empty',
+ },
+ {
+ label: 'Is Not Empty',
+ value: 'is_not_empty',
+ },
+];
+
export const selectFieldFilterOperators: FieldFilterOperator[] = [
{
label: 'Is In',
@@ -588,6 +607,8 @@ export const getFieldFilterOperators = (
return numberFieldFilterOperators;
case 'phone':
return phoneFieldFilterOperators;
+ case 'relation':
+ return relationFieldFilterOperators;
case 'select':
return selectFieldFilterOperators;
case 'text':