mirror of
https://github.com/colanode/colanode.git
synced 2026-07-09 20:08:46 +02:00
Filter and sort by name (#117)
This commit is contained in:
@@ -24,8 +24,23 @@ import {
|
||||
DatabaseViewFilterAttributes,
|
||||
DatabaseViewSortAttributes,
|
||||
MultiSelectFieldAttributes,
|
||||
SpecialId,
|
||||
} from '@colanode/core';
|
||||
|
||||
type SqliteOperator =
|
||||
| '='
|
||||
| '!='
|
||||
| '>'
|
||||
| '<'
|
||||
| '>='
|
||||
| '<='
|
||||
| 'LIKE'
|
||||
| 'NOT LIKE'
|
||||
| 'IS'
|
||||
| 'IS NOT'
|
||||
| 'IN'
|
||||
| 'NOT IN';
|
||||
|
||||
export class RecordListQueryHandler
|
||||
extends WorkspaceQueryHandlerBase
|
||||
implements QueryHandler<RecordListQueryInput>
|
||||
@@ -214,6 +229,10 @@ export class RecordListQueryHandler
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter.fieldId === SpecialId.Name) {
|
||||
return this.buildNameFilterQuery(filter);
|
||||
}
|
||||
|
||||
const field = fields[filter.fieldId];
|
||||
if (!field) {
|
||||
return null;
|
||||
@@ -251,16 +270,62 @@ export class RecordListQueryHandler
|
||||
}
|
||||
};
|
||||
|
||||
private buildNameFilterQuery = (
|
||||
filter: DatabaseViewFieldFilterAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildAttributeFilterQuery('name', 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildAttributeFilterQuery('name', 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof filter.value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = filter.value as string;
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildAttributeFilterQuery('name', '=', `'${value}'`);
|
||||
case 'is_not_equal_to':
|
||||
return this.buildAttributeFilterQuery('name', '!=', `'${value}'`);
|
||||
case 'contains':
|
||||
return this.buildAttributeFilterQuery('name', 'LIKE', `'%${value}%'`);
|
||||
case 'does_not_contain':
|
||||
return this.buildAttributeFilterQuery(
|
||||
'name',
|
||||
'NOT LIKE',
|
||||
`'%${value}%'`
|
||||
);
|
||||
case 'starts_with':
|
||||
return this.buildAttributeFilterQuery('name', 'LIKE', `'${value}%'`);
|
||||
case 'ends_with':
|
||||
return this.buildAttributeFilterQuery('name', 'LIKE', `'%${value}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildBooleanFilterQuery = (
|
||||
filter: DatabaseViewFieldFilterAttributes,
|
||||
field: BooleanFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_true') {
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') = true`;
|
||||
return this.buildFieldFilterQuery(field.id, '=', 'true');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_false') {
|
||||
return `(json_extract(n.attributes, '$.fields.${field.id}.value') = false OR json_extract(n.attributes, '$.fields.${field.id}.value') IS NULL)`;
|
||||
return `(${this.buildFieldFilterQuery(field.id, '=', 'false')} OR ${this.buildFieldFilterQuery(field.id, 'IS', 'NULL')})`;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -271,11 +336,11 @@ export class RecordListQueryHandler
|
||||
field: NumberFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -287,36 +352,26 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const value = filter.value as number;
|
||||
let operator: string | null;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
case 'is_greater_than':
|
||||
operator = '>';
|
||||
break;
|
||||
case 'is_less_than':
|
||||
operator = '<';
|
||||
break;
|
||||
case 'is_greater_than_or_equal_to':
|
||||
operator = '>=';
|
||||
break;
|
||||
case 'is_less_than_or_equal_to':
|
||||
operator = '<=';
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
if (isNaN(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') ${operator} ${value}`;
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '=', value.toString());
|
||||
case 'is_not_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '!=', value.toString());
|
||||
case 'is_greater_than':
|
||||
return this.buildFieldFilterQuery(field.id, '>', value.toString());
|
||||
case 'is_less_than':
|
||||
return this.buildFieldFilterQuery(field.id, '<', value.toString());
|
||||
case 'is_greater_than_or_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '>=', value.toString());
|
||||
case 'is_less_than_or_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '<=', value.toString());
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildTextFilterQuery = (
|
||||
@@ -324,11 +379,11 @@ export class RecordListQueryHandler
|
||||
field: TextFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -340,41 +395,26 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const value = filter.value as string;
|
||||
let operator: string | null;
|
||||
let formattedValue = value;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
case 'contains':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'does_not_contain':
|
||||
operator = 'NOT LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'starts_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `${value}%`;
|
||||
break;
|
||||
case 'ends_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}`;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') ${operator} '${formattedValue}'`;
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '=', `'${value}'`);
|
||||
case 'is_not_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '!=', `'${value}'`);
|
||||
case 'contains':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}%'`);
|
||||
case 'does_not_contain':
|
||||
return this.buildFieldFilterQuery(field.id, 'NOT LIKE', `'%${value}%'`);
|
||||
case 'starts_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'${value}%'`);
|
||||
case 'ends_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildEmailFilterQuery = (
|
||||
@@ -382,11 +422,11 @@ export class RecordListQueryHandler
|
||||
field: EmailFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -398,41 +438,26 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const value = filter.value as string;
|
||||
let operator: string | null;
|
||||
let formattedValue = value;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
case 'contains':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'does_not_contain':
|
||||
operator = 'NOT LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'starts_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `${value}%`;
|
||||
break;
|
||||
case 'ends_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}`;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') ${operator} '${formattedValue}'`;
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '=', `'${value}'`);
|
||||
case 'is_not_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '!=', `'${value}'`);
|
||||
case 'contains':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}%'`);
|
||||
case 'does_not_contain':
|
||||
return this.buildFieldFilterQuery(field.id, 'NOT LIKE', `'%${value}%'`);
|
||||
case 'starts_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'${value}%'`);
|
||||
case 'ends_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildPhoneFilterQuery = (
|
||||
@@ -440,11 +465,11 @@ export class RecordListQueryHandler
|
||||
field: PhoneFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -456,41 +481,26 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const value = filter.value as string;
|
||||
let operator: string | null;
|
||||
let formattedValue = value;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
case 'contains':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'does_not_contain':
|
||||
operator = 'NOT LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'starts_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `${value}%`;
|
||||
break;
|
||||
case 'ends_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}`;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') ${operator} '${formattedValue}'`;
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '=', `'${value}'`);
|
||||
case 'is_not_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '!=', `'${value}'`);
|
||||
case 'contains':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}%'`);
|
||||
case 'does_not_contain':
|
||||
return this.buildFieldFilterQuery(field.id, 'NOT LIKE', `'%${value}%'`);
|
||||
case 'starts_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'${value}%'`);
|
||||
case 'ends_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildUrlFilterQuery = (
|
||||
@@ -498,11 +508,11 @@ export class RecordListQueryHandler
|
||||
field: UrlFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -514,41 +524,26 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const value = filter.value as string;
|
||||
let operator: string | null;
|
||||
let formattedValue = value;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
case 'contains':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'does_not_contain':
|
||||
operator = 'NOT LIKE';
|
||||
formattedValue = `%${value}%`;
|
||||
break;
|
||||
case 'starts_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `${value}%`;
|
||||
break;
|
||||
case 'ends_with':
|
||||
operator = 'LIKE';
|
||||
formattedValue = `%${value}`;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
if (!value || value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') ${operator} '${formattedValue}'`;
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '=', `'${value}'`);
|
||||
case 'is_not_equal_to':
|
||||
return this.buildFieldFilterQuery(field.id, '!=', `'${value}'`);
|
||||
case 'contains':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}%'`);
|
||||
case 'does_not_contain':
|
||||
return this.buildFieldFilterQuery(field.id, 'NOT LIKE', `'%${value}%'`);
|
||||
case 'starts_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'${value}%'`);
|
||||
case 'ends_with':
|
||||
return this.buildFieldFilterQuery(field.id, 'LIKE', `'%${value}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private buildSelectFilterQuery = (
|
||||
@@ -556,11 +551,11 @@ export class RecordListQueryHandler
|
||||
field: SelectFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (!isStringArray(filter.value)) {
|
||||
@@ -574,9 +569,9 @@ export class RecordListQueryHandler
|
||||
const values = this.joinIds(filter.value);
|
||||
switch (filter.operator) {
|
||||
case 'is_in':
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') IN (${values})`;
|
||||
return this.buildFieldFilterQuery(field.id, 'IN', `(${values})`);
|
||||
case 'is_not_in':
|
||||
return `json_extract(n.attributes, '$.fields.${field.id}.value') NOT IN (${values})`;
|
||||
return this.buildFieldFilterQuery(field.id, 'NOT IN', `(${values})`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -618,11 +613,11 @@ export class RecordListQueryHandler
|
||||
field: DateFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return this.buildIsEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return this.buildIsNotEmptyFilterQuery(field.id);
|
||||
return this.buildFieldFilterQuery(field.id, 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -639,36 +634,23 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
let operator: string | null;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '=', `'${dateString}'`);
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '!=', `'${dateString}'`);
|
||||
case 'is_on_or_after':
|
||||
operator = '>=';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '>=', `'${dateString}'`);
|
||||
case 'is_on_or_before':
|
||||
operator = '<=';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '<=', `'${dateString}'`);
|
||||
case 'is_after':
|
||||
operator = '>';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '>', `'${dateString}'`);
|
||||
case 'is_before':
|
||||
operator = '<';
|
||||
break;
|
||||
return this.buildFieldFilterQuery(field.id, '<', `'${dateString}'`);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `DATE(json_extract(n.attributes, '$.fields.${field.id}.value')) ${operator} '${dateString}'`;
|
||||
};
|
||||
|
||||
private buildCreatedAtFilterQuery = (
|
||||
@@ -676,11 +658,11 @@ export class RecordListQueryHandler
|
||||
_: CreatedAtFieldAttributes
|
||||
): string | null => {
|
||||
if (filter.operator === 'is_empty') {
|
||||
return `n.created_at IS NULL`;
|
||||
return this.buildAttributeFilterQuery('created_at', 'IS', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return `n.created_at IS NOT NULL`;
|
||||
return this.buildAttributeFilterQuery('created_at', 'IS NOT', 'NULL');
|
||||
}
|
||||
|
||||
if (filter.value === null) {
|
||||
@@ -697,44 +679,67 @@ export class RecordListQueryHandler
|
||||
}
|
||||
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
let operator: string | null;
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
operator = '=';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'=',
|
||||
`'${dateString}'`
|
||||
);
|
||||
case 'is_not_equal_to':
|
||||
operator = '!=';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'!=',
|
||||
`'${dateString}'`
|
||||
);
|
||||
case 'is_on_or_after':
|
||||
operator = '>=';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'>=',
|
||||
`'${dateString}'`
|
||||
);
|
||||
case 'is_on_or_before':
|
||||
operator = '<=';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'<=',
|
||||
`'${dateString}'`
|
||||
);
|
||||
case 'is_after':
|
||||
operator = '>';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'>',
|
||||
`'${dateString}'`
|
||||
);
|
||||
case 'is_before':
|
||||
operator = '<';
|
||||
break;
|
||||
return this.buildAttributeFilterQuery(
|
||||
'created_at',
|
||||
'<',
|
||||
`'${dateString}'`
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (operator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `DATE(n.created_at) ${operator} '${dateString}'`;
|
||||
};
|
||||
|
||||
private buildIsEmptyFilterQuery = (fieldId: string): string => {
|
||||
return `json_extract(n.attributes, '$.fields.${fieldId}.value') IS NULL`;
|
||||
private buildFieldFilterQuery = (
|
||||
fieldId: string,
|
||||
operator: SqliteOperator,
|
||||
value: string
|
||||
): string => {
|
||||
return this.buildAttributeFilterQuery(
|
||||
`fields.${fieldId}.value`,
|
||||
operator,
|
||||
value
|
||||
);
|
||||
};
|
||||
|
||||
private buildIsNotEmptyFilterQuery = (fieldId: string): string => {
|
||||
return `json_extract(n.attributes, '$.fields.${fieldId}.value') IS NOT NULL`;
|
||||
private buildAttributeFilterQuery = (
|
||||
name: string,
|
||||
operator: SqliteOperator,
|
||||
value: string
|
||||
): string => {
|
||||
return `json_extract(n.attributes, '$.${name}') ${operator} ${value}`;
|
||||
};
|
||||
|
||||
private joinIds = (ids: string[]): string => {
|
||||
@@ -755,6 +760,10 @@ export class RecordListQueryHandler
|
||||
sort: DatabaseViewSortAttributes,
|
||||
fields: Record<string, FieldAttributes>
|
||||
): string | null => {
|
||||
if (sort.fieldId === SpecialId.Name) {
|
||||
return `json_extract(n.attributes, '$.name') ${sort.direction}`;
|
||||
}
|
||||
|
||||
const field = fields[sort.fieldId];
|
||||
if (!field) {
|
||||
return null;
|
||||
|
||||
@@ -43,6 +43,10 @@ export enum IdType {
|
||||
Save = 'sv',
|
||||
}
|
||||
|
||||
export const SpecialId = {
|
||||
Name: 'name',
|
||||
};
|
||||
|
||||
export const generateId = (type: IdType): string => {
|
||||
return ulid().toLowerCase() + type;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Type } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SpecialId } from '@colanode/core';
|
||||
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
|
||||
import {
|
||||
Command,
|
||||
@@ -35,6 +37,10 @@ export const ViewFilterAddPopover = ({
|
||||
)
|
||||
);
|
||||
|
||||
const isFilteredByName = view.filters.some(
|
||||
(filter) => filter.type === 'field' && filter.fieldId === SpecialId.Name
|
||||
);
|
||||
|
||||
if (fieldsWithoutFilters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -48,6 +54,20 @@ export const ViewFilterAddPopover = ({
|
||||
<CommandEmpty>No field found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup className="h-min">
|
||||
{!isFilteredByName && (
|
||||
<CommandItem
|
||||
key={SpecialId.Name}
|
||||
onSelect={() => {
|
||||
view.initFieldFilter(SpecialId.Name);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<Type className="size-4" />
|
||||
<p>{database.nameField?.name ?? 'Name'}</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
)}
|
||||
{fieldsWithoutFilters.map((field) => (
|
||||
<CommandItem
|
||||
key={field.id}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { SpecialId } from '@colanode/core';
|
||||
import { ViewBooleanFieldFilter } from '@colanode/ui/components/databases/search/view-boolean-field-filter';
|
||||
import { ViewCreatedAtFieldFilter } from '@colanode/ui/components/databases/search/view-created-at-field-fitler';
|
||||
import { ViewDateFieldFilter } from '@colanode/ui/components/databases/search/view-date-field-filter';
|
||||
import { ViewEmailFieldFilter } from '@colanode/ui/components/databases/search/view-email-field-filter';
|
||||
import { ViewFilterAddPopover } from '@colanode/ui/components/databases/search/view-filter-add-popover';
|
||||
import { ViewMultiSelectFieldFilter } from '@colanode/ui/components/databases/search/view-multi-select-field-filter';
|
||||
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 { ViewSelectFieldFilter } from '@colanode/ui/components/databases/search/view-select-field-filter';
|
||||
@@ -17,6 +19,7 @@ import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
export const ViewFilters = () => {
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
console.log('view.filters', view.filters);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
@@ -26,6 +29,10 @@ export const ViewFilters = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter.fieldId === SpecialId.Name) {
|
||||
return <ViewNameFieldFilter key={filter.id} filter={filter} />;
|
||||
}
|
||||
|
||||
const field = database.fields.find(
|
||||
(field) => field.id === filter.fieldId
|
||||
);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { ChevronDown, Trash2, Type } from 'lucide-react';
|
||||
|
||||
import { DatabaseViewFieldFilterAttributes } from '@colanode/core';
|
||||
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 { SmartTextInput } from '@colanode/ui/components/ui/smart-text-input';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { nameFieldFilterOperators } from '@colanode/ui/lib/databases';
|
||||
|
||||
interface ViewNameFieldFilterProps {
|
||||
filter: DatabaseViewFieldFilterAttributes;
|
||||
}
|
||||
|
||||
export const ViewNameFieldFilter = ({ filter }: ViewNameFieldFilterProps) => {
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const operator =
|
||||
nameFieldFilterOperators.find(
|
||||
(operator) => operator.value === filter.operator
|
||||
) ?? nameFieldFilterOperators[0];
|
||||
|
||||
if (!operator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const textValue = filter.value as string | null;
|
||||
|
||||
const hideInput =
|
||||
operator.value === 'is_empty' || operator.value === 'is_not_empty';
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={view.isFieldFilterOpened(filter.id)}
|
||||
onOpenChange={() => {
|
||||
if (view.isFieldFilterOpened(filter.id)) {
|
||||
view.closeFieldFilter(filter.id);
|
||||
} else {
|
||||
view.openFieldFilter(filter.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-muted-foregroundc border-dashed text-xs"
|
||||
>
|
||||
{database.nameField?.name ?? 'Name'}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex w-96 flex-col gap-2 p-2">
|
||||
<div className="flex flex-row items-center gap-3 text-sm">
|
||||
<div className="flex flex-row items-center gap-0.5 p-1">
|
||||
<Type className="size-4" />
|
||||
<p>{database.nameField?.name ?? 'Name'}</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex flex-grow flex-row items-center gap-1 rounded-md p-1 font-semibold cursor-pointer hover:bg-gray-100">
|
||||
<p>{operator.label}</p>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{nameFieldFilterOperators.map((operator) => (
|
||||
<DropdownMenuItem
|
||||
key={operator.value}
|
||||
onSelect={() => {
|
||||
const value =
|
||||
operator.value === 'is_empty' ||
|
||||
operator.value === 'is_not_empty'
|
||||
? null
|
||||
: textValue;
|
||||
|
||||
view.updateFilter(filter.id, {
|
||||
...filter,
|
||||
operator: operator.value,
|
||||
value: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{operator.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
view.removeFilter(filter.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{!hideInput && (
|
||||
<SmartTextInput
|
||||
value={textValue}
|
||||
onChange={(value) => {
|
||||
view.updateFilter(filter.id, {
|
||||
...filter,
|
||||
value: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Type } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SpecialId } from '@colanode/core';
|
||||
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
|
||||
import {
|
||||
Command,
|
||||
@@ -33,6 +35,10 @@ export const ViewSortAddPopover = ({ children }: ViewSortAddPopoverProps) => {
|
||||
!view.sorts.some((sort) => sort.fieldId === field.id)
|
||||
);
|
||||
|
||||
const isSortedByName = view.sorts.some(
|
||||
(sort) => sort.fieldId === SpecialId.Name
|
||||
);
|
||||
|
||||
if (sortableFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -46,6 +52,20 @@ export const ViewSortAddPopover = ({ children }: ViewSortAddPopoverProps) => {
|
||||
<CommandEmpty>No sortable field found.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup className="h-min">
|
||||
{!isSortedByName && (
|
||||
<CommandItem
|
||||
key={SpecialId.Name}
|
||||
onSelect={() => {
|
||||
view.initFieldSort(SpecialId.Name, 'asc');
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center gap-2">
|
||||
<Type className="size-4" />
|
||||
<p>{database.nameField?.name ?? 'Name'}</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
)}
|
||||
{sortableFields.map((field) => (
|
||||
<CommandItem
|
||||
key={field.id}
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
} from '@colanode/ui/components/ui/dropdown-menu';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
|
||||
interface ViewSortProps {
|
||||
interface ViewSortFieldRowProps {
|
||||
sort: DatabaseViewSortAttributes;
|
||||
field: FieldAttributes;
|
||||
}
|
||||
|
||||
export const ViewSortRow = ({ sort, field }: ViewSortProps) => {
|
||||
export const ViewSortFieldRow = ({ sort, field }: ViewSortFieldRowProps) => {
|
||||
const view = useDatabaseView();
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,71 @@
|
||||
import { ChevronDown, Trash2, Type } from 'lucide-react';
|
||||
|
||||
import { DatabaseViewSortAttributes } from '@colanode/core';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@colanode/ui/components/ui/dropdown-menu';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
|
||||
interface ViewSortNameRowProps {
|
||||
sort: DatabaseViewSortAttributes;
|
||||
}
|
||||
|
||||
export const ViewSortNameRow = ({ sort }: ViewSortNameRowProps) => {
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-3 text-sm">
|
||||
<div className="flex flex-row items-center gap-0.5 p-1">
|
||||
<Type className="size-4" />
|
||||
<p>{database.nameField?.name ?? 'Name'}</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex flex-grow flex-row items-center gap-1 rounded-md p-1 font-semibold cursor-pointer hover:bg-gray-100">
|
||||
<p>{sort.direction === 'asc' ? 'Ascending' : 'Descending'}</p>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
view.updateSort(sort.id, {
|
||||
...sort,
|
||||
direction: 'asc',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p>Ascending</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
view.updateSort(sort.id, {
|
||||
...sort,
|
||||
direction: 'desc',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<p>Descending</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
view.removeSort(sort.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
import { SpecialId } from '@colanode/core';
|
||||
import { ViewSortAddPopover } from '@colanode/ui/components/databases/search/view-sort-add-popover';
|
||||
import { ViewSortRow } from '@colanode/ui/components/databases/search/view-sort-row';
|
||||
import { ViewSortFieldRow } from '@colanode/ui/components/databases/search/view-sort-field-row';
|
||||
import { ViewSortNameRow } from '@colanode/ui/components/databases/search/view-sort-name-row';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
@@ -38,12 +40,16 @@ export const ViewSorts = () => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col gap-2 p-2">
|
||||
{view.sorts.map((sort) => {
|
||||
if (sort.fieldId === SpecialId.Name) {
|
||||
return <ViewSortNameRow key={sort.id} sort={sort} />;
|
||||
}
|
||||
|
||||
const field = database.fields.find(
|
||||
(field) => field.id === sort.fieldId
|
||||
);
|
||||
|
||||
if (!field) return null;
|
||||
return <ViewSortRow key={sort.id} sort={sort} field={field} />;
|
||||
return <ViewSortFieldRow key={sort.id} sort={sort} field={field} />;
|
||||
})}
|
||||
<ViewSortAddPopover>
|
||||
<button className="flex cursor-pointer flex-row items-center gap-1 rounded-lg p-1 text-sm text-muted-foreground hover:bg-gray-50">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ArrowDownAz, ArrowDownZa, Filter, Type } from 'lucide-react';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { Fragment, useRef } from 'react';
|
||||
import { Fragment, useRef, useState } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
|
||||
import { SpecialId } from '@colanode/core';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -18,6 +19,8 @@ export const TableViewNameHeader = () => {
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const [openPopover, setOpenPopover] = useState(false);
|
||||
|
||||
const [dropMonitor, dropRef] = useDrop({
|
||||
accept: 'table-field-header',
|
||||
drop: () => ({
|
||||
@@ -65,7 +68,7 @@ export const TableViewNameHeader = () => {
|
||||
view.resizeName(newWidth);
|
||||
}}
|
||||
>
|
||||
<Popover modal={true}>
|
||||
<Popover modal={true} open={openPopover} onOpenChange={setOpenPopover}>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -97,12 +100,8 @@ export const TableViewNameHeader = () => {
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
// eventBus.publish({
|
||||
// __typename: 'AddSortEvent',
|
||||
// field: viewField.field,
|
||||
// viewId: view.id,
|
||||
// direction: 'ASC',
|
||||
// });
|
||||
view.initFieldSort(SpecialId.Name, 'asc');
|
||||
setOpenPopover(false);
|
||||
}}
|
||||
>
|
||||
<ArrowDownAz className="size-4" />
|
||||
@@ -112,12 +111,8 @@ export const TableViewNameHeader = () => {
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
// eventBus.publish({
|
||||
// __typename: 'AddSortEvent',
|
||||
// field: viewField.field,
|
||||
// viewId: view.id,
|
||||
// direction: 'DESC',
|
||||
// });
|
||||
view.initFieldSort(SpecialId.Name, 'desc');
|
||||
setOpenPopover(false);
|
||||
}}
|
||||
>
|
||||
<ArrowDownZa className="size-4" />
|
||||
@@ -125,7 +120,13 @@ export const TableViewNameHeader = () => {
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-gray-100">
|
||||
<div
|
||||
className="flex cursor-pointer flex-row items-center gap-2 p-1 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
view.initFieldFilter(SpecialId.Name);
|
||||
setOpenPopover(false);
|
||||
}}
|
||||
>
|
||||
<Filter className="size-4" />
|
||||
<span>Filter</span>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DatabaseViewFieldFilterAttributes,
|
||||
DatabaseViewFilterAttributes,
|
||||
DatabaseViewSortAttributes,
|
||||
SpecialId,
|
||||
} from '@colanode/core';
|
||||
import { BoardView } from '@colanode/ui/components/databases/boards/board-view';
|
||||
import { CalendarView } from '@colanode/ui/components/databases/calendars/calendar-view';
|
||||
@@ -67,7 +68,7 @@ export const View = ({ view }: ViewProps) => {
|
||||
sorts: Object.values(view.attributes.sorts ?? {}),
|
||||
groupBy: view.attributes.groupBy,
|
||||
nameWidth: view.attributes.nameWidth ?? getDefaultNameWidth(),
|
||||
isSearchBarOpened,
|
||||
isSearchBarOpened: isSearchBarOpened || openedFieldFilters.length > 0,
|
||||
isSortsOpened,
|
||||
rename: async (name: string) => {
|
||||
if (!database.canEdit) return;
|
||||
@@ -241,22 +242,35 @@ export const View = ({ view }: ViewProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = database.fields.find((f) => f.id === fieldId);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const operators = getFieldFilterOperators(field.type);
|
||||
const filter: DatabaseViewFieldFilterAttributes = {
|
||||
type: 'field',
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
operator: operators[0]?.value ?? '',
|
||||
};
|
||||
|
||||
const viewAttributes = { ...view.attributes };
|
||||
viewAttributes.filters = viewAttributes.filters ?? {};
|
||||
viewAttributes.filters[fieldId] = filter;
|
||||
|
||||
if (fieldId === SpecialId.Name) {
|
||||
const operators = getFieldFilterOperators('text');
|
||||
const filter: DatabaseViewFieldFilterAttributes = {
|
||||
type: 'field',
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
operator: operators[0]?.value ?? 'contains',
|
||||
};
|
||||
|
||||
viewAttributes.filters[fieldId] = filter;
|
||||
} else {
|
||||
const field = database.fields.find((f) => f.id === fieldId);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const operators = getFieldFilterOperators(field.type);
|
||||
const filter: DatabaseViewFieldFilterAttributes = {
|
||||
type: 'field',
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
operator: operators[0]?.value ?? '',
|
||||
};
|
||||
|
||||
viewAttributes.filters[fieldId] = filter;
|
||||
}
|
||||
|
||||
const result = await window.colanode.executeMutation({
|
||||
type: 'view.update',
|
||||
@@ -339,20 +353,31 @@ export const View = ({ view }: ViewProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = database.fields.find((f) => f.id === fieldId);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sort: DatabaseViewSortAttributes = {
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
direction,
|
||||
};
|
||||
|
||||
const viewAttributes = { ...view.attributes };
|
||||
viewAttributes.sorts = viewAttributes.sorts ?? {};
|
||||
viewAttributes.sorts[fieldId] = sort;
|
||||
|
||||
if (fieldId === SpecialId.Name) {
|
||||
const sort: DatabaseViewSortAttributes = {
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
direction,
|
||||
};
|
||||
|
||||
viewAttributes.sorts[fieldId] = sort;
|
||||
} else {
|
||||
const field = database.fields.find((f) => f.id === fieldId);
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sort: DatabaseViewSortAttributes = {
|
||||
id: fieldId,
|
||||
fieldId,
|
||||
direction,
|
||||
};
|
||||
|
||||
viewAttributes.sorts[fieldId] = sort;
|
||||
}
|
||||
|
||||
const result = await window.colanode.executeMutation({
|
||||
type: 'view.update',
|
||||
|
||||
@@ -153,6 +153,33 @@ export interface FieldFilterOperator {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const nameFieldFilterOperators: FieldFilterOperator[] = [
|
||||
{
|
||||
label: 'Is Equal To',
|
||||
value: 'is_equal_to',
|
||||
},
|
||||
{
|
||||
label: 'Is Not Equal To',
|
||||
value: 'is_not_equal_to',
|
||||
},
|
||||
{
|
||||
label: 'Contains',
|
||||
value: 'contains',
|
||||
},
|
||||
{
|
||||
label: 'Does Not Contain',
|
||||
value: 'does_not_contain',
|
||||
},
|
||||
{
|
||||
label: 'Is Empty',
|
||||
value: 'is_empty',
|
||||
},
|
||||
{
|
||||
label: 'Is Not Empty',
|
||||
value: 'is_not_empty',
|
||||
},
|
||||
];
|
||||
|
||||
export const booleanFieldFilterOperators: FieldFilterOperator[] = [
|
||||
{
|
||||
label: 'Is True',
|
||||
|
||||
Reference in New Issue
Block a user