mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Implement calendar view
This commit is contained in:
27
desktop/package-lock.json
generated
27
desktop/package-lock.json
generated
@@ -69,6 +69,7 @@
|
||||
"lowlight": "^3.1.0",
|
||||
"re-resizable": "^6.9.18",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.0.9",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -5583,6 +5584,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||
@@ -11261,6 +11272,22 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.0.9.tgz",
|
||||
"integrity": "sha512-Sj+1UuuUmdxSKPmJlQFb6h1wKw9N7gG0K4db9mQtHm/qzd94t6LT9X8cGwDHaW9Pdb9dua4/sLhUSW06A1Xqkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^3.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/gpbl"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"lowlight": "^3.1.0",
|
||||
"re-resizable": "^6.9.18",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.0.9",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
import { RecordNode } from '@/types/databases';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CalendarViewCardProps {
|
||||
record: RecordNode;
|
||||
}
|
||||
|
||||
export const CalendarViewCard = ({ record }: CalendarViewCardProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const name = record.name;
|
||||
const hasName = name !== null && name !== '';
|
||||
|
||||
return (
|
||||
<button
|
||||
role="presentation"
|
||||
key={record.id}
|
||||
className={cn(
|
||||
'animate-fade-in flex cursor-pointer flex-col gap-1 rounded-md border p-2 hover:bg-gray-50',
|
||||
hasName ? '' : 'text-muted-foreground',
|
||||
)}
|
||||
onClick={() => workspace.openModal(record.id)}
|
||||
>
|
||||
{record.name ?? 'Unnamed'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { cn, isSameDay } from '@/lib/utils';
|
||||
import { RecordNode } from '@/types/databases';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { CalendarViewCard } from '@/components/databases/calendars/calendar-view-card';
|
||||
|
||||
interface CalendarViewDayProps {
|
||||
date: Date;
|
||||
month: Date;
|
||||
outside: boolean;
|
||||
records: RecordNode[];
|
||||
}
|
||||
|
||||
export const CalendarViewDay = ({
|
||||
date,
|
||||
month,
|
||||
outside,
|
||||
records,
|
||||
}: CalendarViewDayProps) => {
|
||||
const isToday = isSameDay(date, new Date());
|
||||
const canCreateRecord = false;
|
||||
|
||||
return (
|
||||
<td
|
||||
className={cn(
|
||||
'relative h-40 flex-1 p-2 text-right text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent',
|
||||
'[&:has([aria-selected])]:rounded-md',
|
||||
'overflow-auto border-r border-gray-100 first:border-l',
|
||||
'group flex flex-col gap-1',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between text-sm',
|
||||
outside ? 'text-muted-foreground' : '',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name="add-line"
|
||||
className={cn(
|
||||
'cursor-pointer opacity-0',
|
||||
canCreateRecord ? 'group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<p
|
||||
className={
|
||||
isToday ? 'rounder-md rounded bg-red-500 p-0.5 text-white' : ''
|
||||
}
|
||||
>
|
||||
{date.getDate()}
|
||||
</p>
|
||||
</div>
|
||||
{records.map((record) => {
|
||||
return <CalendarViewCard key={record.id} record={record} />;
|
||||
})}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Icon } from '@/components/ui/icon';
|
||||
import { cn, getDisplayedDates } from '@/lib/utils';
|
||||
import { DayPicker, DayProps } from 'react-day-picker';
|
||||
import { CalendarViewDay } from '@/components/databases/calendars/calendar-view-day';
|
||||
import { CalendarViewNode, FieldNode, ViewFilterNode } from '@/types/databases';
|
||||
import { useRecordsQuery } from '@/queries/use-records-query';
|
||||
import { useDatabase } from '@/contexts/database';
|
||||
import { filterRecords } from '@/lib/databases';
|
||||
import { useWorkspace } from '@/contexts/workspace';
|
||||
|
||||
interface CalendarViewGridProps {
|
||||
view: CalendarViewNode;
|
||||
field: FieldNode;
|
||||
}
|
||||
|
||||
export const CalendarViewGrid = ({ view, field }: CalendarViewGridProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const database = useDatabase();
|
||||
|
||||
const [month, setMonth] = React.useState(new Date());
|
||||
const { first, last } = getDisplayedDates(month);
|
||||
|
||||
const filters = [
|
||||
...view.filters,
|
||||
{
|
||||
id: 'start_date',
|
||||
fieldId: field.id,
|
||||
operator: 'is_on_or_after',
|
||||
values: [
|
||||
{
|
||||
textValue: first.toISOString(),
|
||||
numberValue: null,
|
||||
foreignNodeId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'end_date',
|
||||
fieldId: field.id,
|
||||
operator: 'is_on_or_before',
|
||||
values: [
|
||||
{
|
||||
textValue: last.toISOString(),
|
||||
numberValue: null,
|
||||
foreignNodeId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const { data, isPending, isFetchingNextPage, fetchNextPage, hasNextPage } =
|
||||
useRecordsQuery(database.id, filters);
|
||||
|
||||
if (isPending) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const records = data ?? [];
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays
|
||||
className="p-3"
|
||||
month={month}
|
||||
onMonthChange={(month) => {
|
||||
setMonth(month);
|
||||
}}
|
||||
classNames={{
|
||||
months:
|
||||
'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 w-full',
|
||||
month: 'space-y-4 w-full',
|
||||
month_caption: 'flex justify-center pt-1 relative items-center',
|
||||
caption_label: 'text-sm font-medium',
|
||||
nav: 'space-x-1 flex items-center',
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
'absolute left-1',
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
'absolute right-1',
|
||||
),
|
||||
month_grid: 'w-full border-collapse space-y-1',
|
||||
weekdays: 'flex flex-row mb-2',
|
||||
weekday:
|
||||
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem]',
|
||||
week: 'flex flex-row w-full border-b first:border-t',
|
||||
}}
|
||||
components={{
|
||||
Chevron: (props) => {
|
||||
if (props.orientation === 'left') {
|
||||
return (
|
||||
<Icon name="arrow-left-s-line" className="h-4 w-4" {...props} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Icon name="arrow-right-s-line" className="h-4 w-4" {...props} />
|
||||
);
|
||||
},
|
||||
Day: (props: DayProps) => {
|
||||
const filter: ViewFilterNode = {
|
||||
id: 'calendar_filter',
|
||||
fieldId: field.id,
|
||||
operator: 'is_equal_to',
|
||||
values: [
|
||||
{
|
||||
textValue: props.day.date.toISOString(),
|
||||
numberValue: null,
|
||||
foreignNodeId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const dayRecords = filterRecords(
|
||||
records,
|
||||
filter,
|
||||
field,
|
||||
workspace.userId,
|
||||
);
|
||||
|
||||
return (
|
||||
<CalendarViewDay
|
||||
date={props.day.date}
|
||||
month={props.day.displayMonth}
|
||||
outside={props.day.outside}
|
||||
records={dayRecords}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,47 @@
|
||||
import React from 'react';
|
||||
import { CalendarViewNode } from '@/types/databases';
|
||||
import { ViewTabs } from '@/components/databases/view-tabs';
|
||||
import { useDatabase } from '@/contexts/database';
|
||||
import { ViewActionButton } from '@/components/databases/view-action-button';
|
||||
import { ViewFilters } from '@/components/databases/filters/view-filters';
|
||||
import { CalendarViewGrid } from './calendar-view-grid';
|
||||
|
||||
interface CalendarViewProps {
|
||||
node: CalendarViewNode;
|
||||
}
|
||||
|
||||
export const CalendarView = ({ node }: CalendarViewProps) => {
|
||||
const database = useDatabase();
|
||||
|
||||
const [openFilters, setOpenFilters] = React.useState(true);
|
||||
const [openSort, setOpenSort] = React.useState(false);
|
||||
|
||||
const groupByField = database.fields.find(
|
||||
(field) => field.id === node.groupBy,
|
||||
);
|
||||
|
||||
if (!groupByField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="mt-2 flex flex-row justify-between border-b">
|
||||
<ViewTabs />
|
||||
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
||||
<ViewActionButton
|
||||
icon="sort-desc"
|
||||
onClick={() => setOpenSort((prev) => !prev)}
|
||||
/>
|
||||
<ViewActionButton
|
||||
icon="filter-line"
|
||||
onClick={() => setOpenFilters((prev) => !prev)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{openFilters && <ViewFilters viewId={node.id} filters={node.filters} />}
|
||||
<div className="mt-2 w-full min-w-full max-w-full overflow-auto pr-5">
|
||||
calendar view {node.id}
|
||||
<CalendarViewGrid view={node} field={groupByField} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { FieldDataType } from '@/types/databases';
|
||||
import {
|
||||
FieldDataType,
|
||||
FieldNode,
|
||||
RecordNode,
|
||||
ViewFilterNode,
|
||||
} from '@/types/databases';
|
||||
|
||||
export const getFieldIcon = (type?: FieldDataType): string => {
|
||||
if (!type) return '';
|
||||
@@ -214,6 +219,22 @@ export const createdAtFieldFilterOperators: FieldFilterOperator[] = [
|
||||
label: 'Is Not Equal To',
|
||||
value: 'is_not_equal_to',
|
||||
},
|
||||
{
|
||||
label: 'Is on or after',
|
||||
value: 'is_on_or_after',
|
||||
},
|
||||
{
|
||||
label: 'Is on or before',
|
||||
value: 'is_on_or_before',
|
||||
},
|
||||
{
|
||||
label: 'Is After',
|
||||
value: 'is_after',
|
||||
},
|
||||
{
|
||||
label: 'Is Before',
|
||||
value: 'is_before',
|
||||
},
|
||||
];
|
||||
|
||||
export const createdByFieldFilterOperators: FieldFilterOperator[] = [
|
||||
@@ -491,3 +512,493 @@ export const getFieldFilterOperators = (
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const filterRecords = (
|
||||
records: RecordNode[],
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
currentUserId: string,
|
||||
): RecordNode[] => {
|
||||
return records.filter((record) =>
|
||||
recordMatchesFilter(record, filter, field, currentUserId),
|
||||
);
|
||||
};
|
||||
|
||||
const recordMatchesFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
currentUserId: string,
|
||||
) => {
|
||||
switch (field.dataType) {
|
||||
case 'boolean':
|
||||
return recordMatchesBooleanFilter(record, filter, field);
|
||||
case 'collaborator':
|
||||
return recordMatchesCollaboratorFilter(record, filter, field);
|
||||
case 'created_at':
|
||||
return recordMatchesCreatedAtFilter(record, filter);
|
||||
case 'created_by':
|
||||
return recordMatchesCreatedByFilter(record, filter, currentUserId);
|
||||
case 'date':
|
||||
return recordMatchesDateFilter(record, filter, field);
|
||||
case 'email':
|
||||
return recordMatchesEmailFilter(record, filter, field);
|
||||
case 'file':
|
||||
return recordMatchesFileFilter(record, filter, field);
|
||||
case 'multi_select':
|
||||
return recordMatchesMultiSelectFilter(record, filter, field);
|
||||
case 'number':
|
||||
return recordMatchesNumberFilter(record, filter, field);
|
||||
case 'phone':
|
||||
return recordMatchesPhoneFilter(record, filter, field);
|
||||
case 'select':
|
||||
return recordMatchesSelectFilter(record, filter, field);
|
||||
case 'text':
|
||||
return recordMatchesTextFilter(record, filter, field);
|
||||
case 'url':
|
||||
return recordMatchesUrlFilter(record, filter, field);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const recordMatchesBooleanFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.numberValue;
|
||||
|
||||
if (filter.operator === 'is_true') {
|
||||
return fieldValue === 1;
|
||||
}
|
||||
if (filter.operator === 'is_false') {
|
||||
return !fieldValue || fieldValue === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesCollaboratorFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesCreatedAtFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
) => {
|
||||
if (filter.values.length === 0) return false;
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
const filterDate = new Date(filterValue);
|
||||
filterDate.setHours(0, 0, 0, 0); // Set time to midnight
|
||||
|
||||
const recordDate = new Date(record.createdAt);
|
||||
recordDate.setHours(0, 0, 0, 0); // Set time to midnight
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return recordDate.getTime() === filterDate.getTime();
|
||||
case 'is_not_equal_to':
|
||||
return recordDate.getTime() !== filterDate.getTime();
|
||||
case 'is_on_or_after':
|
||||
return recordDate.getTime() >= filterDate.getTime();
|
||||
case 'is_on_or_before':
|
||||
return recordDate.getTime() <= filterDate.getTime();
|
||||
case 'is_after':
|
||||
return recordDate.getTime() > filterDate.getTime();
|
||||
case 'is_before':
|
||||
return recordDate.getTime() < filterDate.getTime();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesCreatedByFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
currentUserId: string,
|
||||
) => {
|
||||
const createdBy = record.createdBy?.id;
|
||||
if (!createdBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterValues = filter.values.map((value) => value.foreignNodeId);
|
||||
if (filterValues.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_me':
|
||||
return createdBy === currentUserId;
|
||||
case 'is_not_me':
|
||||
return createdBy !== currentUserId;
|
||||
case 'is_in':
|
||||
return filterValues.includes(createdBy);
|
||||
case 'is_not_in':
|
||||
return !filterValues.includes(createdBy);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const recordMatchesDateFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.textValue;
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const recordDate = new Date(fieldValue);
|
||||
recordDate.setHours(0, 0, 0, 0); // Set time to midnight
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterDate = new Date(filterValue);
|
||||
filterDate.setHours(0, 0, 0, 0); // Set time to midnight
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return recordDate.getTime() === filterDate.getTime();
|
||||
case 'is_not_equal_to':
|
||||
return recordDate.getTime() !== filterDate.getTime();
|
||||
case 'is_on_or_after':
|
||||
return recordDate.getTime() >= filterDate.getTime();
|
||||
case 'is_on_or_before':
|
||||
return recordDate.getTime() <= filterDate.getTime();
|
||||
case 'is_after':
|
||||
return recordDate.getTime() > filterDate.getTime();
|
||||
case 'is_before':
|
||||
return recordDate.getTime() < filterDate.getTime();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesEmailFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.textValue;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return fieldValue === filterValue;
|
||||
case 'is_not_equal_to':
|
||||
return fieldValue !== filterValue;
|
||||
case 'contains':
|
||||
return fieldValue.includes(filterValue);
|
||||
case 'does_not_contain':
|
||||
return !fieldValue.includes(filterValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesFileFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesMultiSelectFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValues = record.attributes
|
||||
.filter((attribute) => attribute.type === field.id)
|
||||
?.map((attribute) => attribute.foreignNodeId);
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return fieldValues?.length === 0;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return fieldValues?.length > 0;
|
||||
}
|
||||
|
||||
if (!fieldValues) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectOptionIds = filter.values.map((value) => value.foreignNodeId);
|
||||
if (selectOptionIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_in':
|
||||
return fieldValues.some((value) => selectOptionIds.includes(value));
|
||||
case 'is_not_in':
|
||||
return !fieldValues.some((value) => selectOptionIds.includes(value));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesNumberFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.numberValue;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].numberValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return fieldValue === filterValue;
|
||||
case 'is_not_equal_to':
|
||||
return fieldValue !== filterValue;
|
||||
case 'is_greater_than':
|
||||
return fieldValue > filterValue;
|
||||
case 'is_less_than':
|
||||
return fieldValue < filterValue;
|
||||
case 'is_greater_than_or_equal_to':
|
||||
return fieldValue >= filterValue;
|
||||
case 'is_less_than_or_equal_to':
|
||||
return fieldValue <= filterValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesPhoneFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.textValue;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return fieldValue === filterValue;
|
||||
case 'is_not_equal_to':
|
||||
return fieldValue !== filterValue;
|
||||
case 'contains':
|
||||
return fieldValue.includes(filterValue);
|
||||
case 'does_not_contain':
|
||||
return !fieldValue.includes(filterValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesSelectFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.foreignNodeId;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectOptionIds = filter.values.map((value) => value.foreignNodeId);
|
||||
if (selectOptionIds.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_in':
|
||||
return selectOptionIds.includes(fieldValue);
|
||||
case 'is_not_in':
|
||||
return !selectOptionIds.includes(fieldValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesTextFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.textValue;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return fieldValue === filterValue;
|
||||
case 'is_not_equal_to':
|
||||
return fieldValue !== filterValue;
|
||||
case 'contains':
|
||||
return fieldValue.includes(filterValue);
|
||||
case 'does_not_contain':
|
||||
return !fieldValue.includes(filterValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordMatchesUrlFilter = (
|
||||
record: RecordNode,
|
||||
filter: ViewFilterNode,
|
||||
field: FieldNode,
|
||||
) => {
|
||||
const fieldValue = record.attributes.find(
|
||||
(attribute) => attribute.type === field.id,
|
||||
)?.textValue;
|
||||
|
||||
if (filter.operator === 'is_empty') {
|
||||
return !fieldValue;
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_not_empty') {
|
||||
return !!fieldValue;
|
||||
}
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.values.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const filterValue = filter.values[0].textValue;
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return fieldValue === filterValue;
|
||||
case 'is_not_equal_to':
|
||||
return fieldValue !== filterValue;
|
||||
case 'contains':
|
||||
return fieldValue.includes(filterValue);
|
||||
case 'does_not_contain':
|
||||
return !fieldValue.includes(filterValue);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -134,3 +134,49 @@ export const isValidUrl = (url: string) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDisplayedDates = (
|
||||
month: Date,
|
||||
): {
|
||||
first: Date;
|
||||
last: Date;
|
||||
} => {
|
||||
const firstDayOfMonth = new Date(month.getFullYear(), month.getMonth(), 1);
|
||||
const lastDayOfMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0);
|
||||
|
||||
// Find the first day of the visible grid (Sunday of the week containing the 1st of the month)
|
||||
const firstDayOfWeek = firstDayOfMonth.getDay();
|
||||
const firstDayDisplayed = new Date(
|
||||
firstDayOfMonth.getFullYear(),
|
||||
firstDayOfMonth.getMonth(),
|
||||
firstDayOfMonth.getDate() - firstDayOfWeek,
|
||||
);
|
||||
|
||||
// Find the last day of the visible grid (Saturday of the week containing the last day of the month)
|
||||
const lastDayOfWeek = lastDayOfMonth.getDay();
|
||||
const lastDayDisplayed = new Date(
|
||||
lastDayOfMonth.getFullYear(),
|
||||
lastDayOfMonth.getMonth(),
|
||||
lastDayOfMonth.getDate() + (6 - lastDayOfWeek),
|
||||
);
|
||||
|
||||
return { first: firstDayDisplayed, last: lastDayDisplayed };
|
||||
};
|
||||
|
||||
export const isSameDay = (
|
||||
date1: Date | string | null,
|
||||
date2: Date | string | null,
|
||||
) => {
|
||||
if (date1 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (date2 == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const d1 = typeof date1 === 'string' ? new Date(date1) : date1;
|
||||
const d2 = typeof date2 === 'string' ? new Date(date2) : date2;
|
||||
|
||||
return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth();
|
||||
};
|
||||
|
||||
@@ -205,6 +205,10 @@ const buildCalendarViewNode = (
|
||||
(attribute) => attribute.type === AttributeTypes.Name,
|
||||
)?.textValue;
|
||||
|
||||
const groupBy = node.attributes.find(
|
||||
(attribute) => attribute.type === AttributeTypes.GroupBy,
|
||||
)?.foreignNodeId;
|
||||
|
||||
const viewFilters = filters.map(buildViewFilterNode);
|
||||
|
||||
return {
|
||||
@@ -212,6 +216,7 @@ const buildCalendarViewNode = (
|
||||
name: name ?? 'Unnamed',
|
||||
type: 'calendar_view',
|
||||
filters: viewFilters,
|
||||
groupBy,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mapNodeWithAttributes } from '@/lib/nodes';
|
||||
import { compareString } from '@/lib/utils';
|
||||
import {
|
||||
BooleanFieldNode,
|
||||
CreatedAtFieldNode,
|
||||
EmailFieldNode,
|
||||
FieldNode,
|
||||
MultiSelectFieldNode,
|
||||
@@ -222,7 +223,7 @@ const buildFilterQuery = (
|
||||
case 'collaborator':
|
||||
return null;
|
||||
case 'created_at':
|
||||
return null;
|
||||
return buildCreatedAtFilterQuery(filter, field);
|
||||
case 'created_by':
|
||||
return null;
|
||||
case 'date':
|
||||
@@ -254,14 +255,14 @@ const buildBooleanFilterQuery = (
|
||||
): FilterQuery | null => {
|
||||
if (filter.operator === 'is_true') {
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value = 1`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = 1`,
|
||||
whereQuery: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (filter.operator === 'is_false') {
|
||||
return {
|
||||
joinQuery: buildLeftJoinQuery(filter.id, field.id),
|
||||
joinQuery: buildLeftJoinNodeAttributesQuery(filter.id, field.id),
|
||||
whereQuery: `na_${filter.id}.node_id IS NULL OR na_${filter.id}.number_value = 0`,
|
||||
};
|
||||
}
|
||||
@@ -293,32 +294,32 @@ const buildNumberFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value = ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value = ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value != ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value != ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_greater_than':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value > ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value > ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_less_than':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value < ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value < ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_greater_than_or_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value >= ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value >= ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_less_than_or_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.number_value <= ${value}`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.number_value <= ${value}`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -350,32 +351,32 @@ const buildTextFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'contains':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'does_not_contain':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'starts_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'ends_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -407,32 +408,32 @@ const buildEmailFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'contains':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'does_not_contain':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'starts_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'ends_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -464,32 +465,32 @@ const buildPhoneFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'contains':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'does_not_contain':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'starts_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'ends_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -521,32 +522,32 @@ const buildUrlFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value = '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value != '${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'contains':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'does_not_contain':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value NOT LIKE '%${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'starts_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '${value}%'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'ends_with':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.text_value LIKE '%${value}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -578,12 +579,12 @@ const buildSelectFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_in':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_in':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id NOT IN (${joinIds(ids)})`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id NOT IN (${joinIds(ids)})`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
@@ -615,12 +616,12 @@ const buildMultiSelectFilterQuery = (
|
||||
switch (filter.operator) {
|
||||
case 'is_in':
|
||||
return {
|
||||
joinQuery: `${buildJoinQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
joinQuery: `${buildJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_in':
|
||||
return {
|
||||
joinQuery: `${buildLeftJoinQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
joinQuery: `${buildLeftJoinNodeAttributesQuery(filter.id, field.id)} AND na_${filter.id}.foreign_node_id IN (${joinIds(ids)})`,
|
||||
whereQuery: `na_${filter.id}.node_id IS NULL`,
|
||||
};
|
||||
default:
|
||||
@@ -628,12 +629,67 @@ const buildMultiSelectFilterQuery = (
|
||||
}
|
||||
};
|
||||
|
||||
const buildCreatedAtFilterQuery = (
|
||||
filter: ViewFilterNode,
|
||||
field: CreatedAtFieldNode,
|
||||
): FilterQuery | null => {
|
||||
if (filter.values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = filter.values[0].textValue;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
switch (filter.operator) {
|
||||
case 'is_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) = '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_not_equal_to':
|
||||
return {
|
||||
joinQuery: `${buildLeftJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) != '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_on_or_after':
|
||||
return {
|
||||
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) >= '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_on_or_before':
|
||||
return {
|
||||
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) <= '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_after':
|
||||
return {
|
||||
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) > '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
case 'is_before':
|
||||
return {
|
||||
joinQuery: `${buildJoinNodesQuery(filter.id)} AND DATE(n_${filter.id}.created_at) < '${dateString}'`,
|
||||
whereQuery: null,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildIsEmptyFilterQuery = (
|
||||
filterId: string,
|
||||
fieldId: string,
|
||||
): FilterQuery => {
|
||||
return {
|
||||
joinQuery: buildLeftJoinQuery(filterId, fieldId),
|
||||
joinQuery: buildLeftJoinNodeAttributesQuery(filterId, fieldId),
|
||||
whereQuery: `na_${filterId}.node_id IS NULL`,
|
||||
};
|
||||
};
|
||||
@@ -643,16 +699,30 @@ const buildIsNotEmptyFilterQuery = (
|
||||
fieldId: string,
|
||||
): FilterQuery => {
|
||||
return {
|
||||
joinQuery: buildJoinQuery(filterId, fieldId),
|
||||
joinQuery: buildJoinNodeAttributesQuery(filterId, fieldId),
|
||||
whereQuery: null,
|
||||
};
|
||||
};
|
||||
|
||||
const buildJoinQuery = (filterId: string, fieldId: string): string => {
|
||||
const buildJoinNodesQuery = (filterId: string): string => {
|
||||
return `JOIN nodes n_${filterId} ON n_${filterId}.id = na.node_id`;
|
||||
};
|
||||
|
||||
const buildLeftJoinNodesQuery = (filterId: string): string => {
|
||||
return `LEFT JOIN nodes n_${filterId} ON n_${filterId}.id = na.node_id`;
|
||||
};
|
||||
|
||||
const buildJoinNodeAttributesQuery = (
|
||||
filterId: string,
|
||||
fieldId: string,
|
||||
): string => {
|
||||
return `JOIN node_attributes na_${filterId} ON na_${filterId}.node_id = na.node_id AND na_${filterId}.type = '${fieldId}'`;
|
||||
};
|
||||
|
||||
const buildLeftJoinQuery = (filterId: string, fieldId: string): string => {
|
||||
const buildLeftJoinNodeAttributesQuery = (
|
||||
filterId: string,
|
||||
fieldId: string,
|
||||
): string => {
|
||||
return `LEFT JOIN node_attributes na_${filterId} ON na_${filterId}.node_id = na.node_id AND na_${filterId}.type = '${fieldId}'`;
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export type CalendarViewNode = {
|
||||
name: string;
|
||||
type: 'calendar_view';
|
||||
filters: ViewFilterNode[];
|
||||
groupBy: string | null;
|
||||
};
|
||||
|
||||
export type FieldDataType =
|
||||
|
||||
Reference in New Issue
Block a user