Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: incorrect filter conditions when viewing more events in the calendar #1115

Merged
merged 6 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FieldType } from '@teable/core';
import { FieldType, TimeFormatting } from '@teable/core';
import { useTableId, useTablePermission, useView } from '@teable/sdk/hooks';
import { Field } from '@teable/sdk/model';
import {
Expand All @@ -12,6 +12,11 @@ import {
} from '@teable/ui-lib/shadcn';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import {
getFormatStringForLanguage,
localFormatStrings,
systemTimeZone,
} from '@/features/app/components/field-setting/formatting/DatetimeFormatting';
import { tableConfig } from '@/features/i18n/table.config';
import { useCalendar } from '../hooks';

Expand All @@ -35,13 +40,27 @@ export const AddDateFieldDialog = () => {
const onClick = async () => {
if (!tableId) return;

const localDateFormatting = getFormatStringForLanguage(navigator.language, localFormatStrings);

const defaultFormatting = {
date: localDateFormatting,
time: TimeFormatting.None,
timeZone: systemTimeZone,
};

const startDateField = await Field.createField(tableId, {
name: t('table:calendar.dialog.startDate'),
type: FieldType.Date,
options: {
formatting: defaultFormatting,
},
});
const endDateField = await Field.createField(tableId, {
name: t('table:calendar.dialog.endDate'),
type: FieldType.Date,
options: {
formatting: defaultFormatting,
},
});

if (view != null && viewUpdatable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ import {
Calendar as DatePicker,
cn,
} from '@teable/ui-lib/shadcn';
import { addDays, format } from 'date-fns';
import { addDays, subDays, format, set } from 'date-fns';
import { enUS, zhCN, ja, ru, fr } from 'date-fns/locale';
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
import { useTranslation } from 'next-i18next';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { tableConfig } from '@/features/i18n/table.config';
import { EventListContainer } from '../components/EventListContainer';
import { EventMenu } from '../components/EventMenu';
import { useCalendar, useEventMenuStore } from '../hooks';
import { getColorByConfig, getEventTitle } from '../util';
import { getColorByConfig, getDateByTimezone, getEventTitle } from '../util';

const ADD_EVENT_BUTTON_CLASS_NAME = 'calendar-add-event-button';
const MORE_LINK_TEXT_CLASS_NAME = 'calendar-custom-more-link-text';
Expand Down Expand Up @@ -130,13 +131,17 @@ export const Calendar = (props: ICalendarProps) => {

if (!tableId || !startDateField || !endDateField) return;

const { timeZone } = startDateField.options.formatting;
const newDate = set(date, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 });
const newDateStr = zonedTimeToUtc(newDate, timeZone).toISOString();

const { data } = await Record.createRecords(tableId, {
fieldKeyType: FieldKeyType.Id,
records: [
{
fields: {
[startDateField.id]: date.toISOString(),
[endDateField.id]: date.toISOString(),
[startDateField.id]: newDateStr,
[endDateField.id]: newDateStr,
},
},
],
Expand Down Expand Up @@ -193,8 +198,6 @@ export const Calendar = (props: ICalendarProps) => {
setTitle(calendarRef.current.getApi().view.title);
}

if (!startDateField || !endDateField) return;

const startStr = start.toISOString();
const endStr = end.toISOString();

Expand All @@ -204,7 +207,7 @@ export const Calendar = (props: ICalendarProps) => {
});
};

const isLoading = !calendarDailyCollection;
const isLoading = startDateField && endDateField && !calendarDailyCollection;
const { countMap, records = [] } = calendarDailyCollection ?? {};

const events = useMemo(() => {
Expand All @@ -215,12 +218,14 @@ export const Calendar = (props: ICalendarProps) => {
const title = r.fields[titleField.id];
const start = r.fields[startDateField.id];
const end = r.fields[endDateField.id];
const { timeZone } = startDateField.options.formatting;

const { color: textColor, backgroundColor } = getColorByConfig(
r as unknown as Record,
colorConfig,
colorField
);
const endDate = end ? addDays(new Date(end as string), 1).toISOString() : undefined;

return {
id: r.id,
Expand All @@ -229,8 +234,8 @@ export const Calendar = (props: ICalendarProps) => {
start as string,
startDateField
),
start,
end: end ? addDays(new Date(end as string), 1).toISOString() : undefined,
start: start ? utcToZonedTime(new Date(start as string), timeZone) : undefined,
end: endDate ? utcToZonedTime(new Date(endDate), timeZone) : undefined,
textColor,
backgroundColor,
allDay: true,
Expand Down Expand Up @@ -295,45 +300,57 @@ export const Calendar = (props: ICalendarProps) => {

if (!tableId || !startDateField || !endDateField) return;

const { timeZone } = startDateField.options.formatting;

// resize start date
if (startDelta.days !== 0) {
const start = event.extendedProps.meta.start ?? event.extendedProps.meta.end;
const newStart = addDays(new Date(start), startDelta.days).toISOString();
const newDate = getDateByTimezone(
new Date(event.startStr),
timeZone,
event.extendedProps.meta.start
);

updateRecord(tableId, event.id, {
fieldKeyType: FieldKeyType.Id,
record: {
fields: {
[startDateField.id]: newStart,
[startDateField.id]: newDate,
},
},
});
}

// resize end date
if (endDelta.days !== 0) {
const end = event.extendedProps.meta.end ?? event.extendedProps.meta.start;
const newEnd = addDays(new Date(end), endDelta.days).toISOString();
const newDate = getDateByTimezone(
subDays(new Date(event.endStr), 1),
timeZone,
event.extendedProps.meta.end
);

updateRecord(tableId, event.id, {
fieldKeyType: FieldKeyType.Id,
record: {
fields: {
[endDateField.id]: newEnd,
[endDateField.id]: newDate,
},
},
});
}
};

const onEventDrop = (info: EventDropArg) => {
const { event, delta } = info;
const { event } = info;

if (!tableId || !startDateField || !endDateField) return;

const start = event.extendedProps.meta.start;
const newStart = addDays(new Date(start), delta.days).toISOString();
const { timeZone } = startDateField.options.formatting;

const end = event.extendedProps.meta.end;
const newEnd = end ? addDays(new Date(end), delta.days).toISOString() : undefined;
const { start, end } = event.extendedProps.meta;
const newStart = getDateByTimezone(new Date(event.startStr), timeZone, start);
const newEnd = end
? getDateByTimezone(subDays(new Date(event.endStr), 1), timeZone, end)
: undefined;

updateRecord(tableId, event.id, {
fieldKeyType: FieldKeyType.Id,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { IFilter } from '@teable/core';
import { mergeFilter, and, exactDate, isOnOrBefore, isOnOrAfter } from '@teable/core';
import { mergeFilter, and, exactDate, isOnOrBefore, isOnOrAfter, or, is } from '@teable/core';
import { RowCountProvider } from '@teable/sdk/context';
import { format } from 'date-fns';
import { zonedTimeToUtc } from 'date-fns-tz';
import { useMemo } from 'react';
import { useCalendar } from '../hooks';
import { EventList } from './EventList';
Expand All @@ -16,28 +18,51 @@ export const EventListContainer = (props: IEventListContainerProps) => {
const query = useMemo(() => {
if (!startDateField || !endDateField) return;

const dateStr = date.toISOString();
const { timeZone } = startDateField.options.formatting;

const dateStr = format(date, 'yyyy-MM-dd');
const startDateUtc = zonedTimeToUtc(`${dateStr} 00:00:00`, timeZone);
const endDateUtc = zonedTimeToUtc(`${dateStr} 23:59:59.999`, timeZone);

const filter = mergeFilter(recordQuery?.filter, {
conjunction: and.value,
filterSet: [
{
fieldId: startDateField.id,
operator: isOnOrBefore.value,
value: {
exactDate: dateStr,
mode: exactDate.value,
timeZone: startDateField.options.formatting.timeZone,
},
},
{
fieldId: endDateField.id,
operator: isOnOrAfter.value,
value: {
exactDate: dateStr,
mode: exactDate.value,
timeZone: endDateField.options.formatting.timeZone,
},
conjunction: or.value,
filterSet: [
{
conjunction: and.value,
filterSet: [
{
fieldId: startDateField.id,
operator: isOnOrBefore.value,
value: {
exactDate: endDateUtc.toISOString(),
mode: exactDate.value,
timeZone,
},
},
{
fieldId: endDateField.id,
operator: isOnOrAfter.value,
value: {
exactDate: startDateUtc.toISOString(),
mode: exactDate.value,
timeZone,
},
},
],
},
{
fieldId: startDateField.id,
operator: is.value,
value: {
exactDate: startDateUtc.toISOString(),
mode: exactDate.value,
timeZone,
},
},
],
},
],
}) as IFilter;
Expand Down
18 changes: 17 additions & 1 deletion apps/nextjs-app/src/features/app/blocks/view/calendar/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { IColorConfig } from '@teable/core';
import { ColorConfigType, TimeFormatting } from '@teable/core';
import { getColorPairs } from '@teable/sdk/components';
import type { DateField, Record, SingleSelectField } from '@teable/sdk/model';
import { formatInTimeZone } from 'date-fns-tz';
import { set } from 'date-fns';
import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
import { DEFAULT_COLOR } from './components/CalendarConfig';

export const getColorByConfig = (
Expand Down Expand Up @@ -35,3 +36,18 @@ export const getEventTitle = (title: string, startDate: string | null, dateField

return `${prefixStr}${title}`;
};

export const getDateByTimezone = (date: Date, timeZone: string, originalDate?: string) => {
const originalTime = utcToZonedTime(
originalDate
? new Date(originalDate)
: set(new Date(), { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }),
timeZone
);
const newDate = set(date, {
hours: originalTime.getHours(),
minutes: originalTime.getMinutes(),
seconds: originalTime.getSeconds(),
});
return zonedTimeToUtc(newDate, timeZone).toISOString();
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { TimeZoneFormatting } from './TimeZoneFormatting';
dayjs.extend(utc);
dayjs.extend(timezone);

const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
export const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

// | Locale | Date Format | Notes |
// |--------|-------------|-------|
Expand All @@ -22,7 +22,7 @@ const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// | ja-JP | YYYY/MM/DD | Japanese (Japan), e.g., 2023/12/31 |
// | zh-CN | YYYY-MM-DD | Simplified Chinese (China), e.g., 2023-12-31 |
// | ko-KR | YYYY.MM.DD | Korean (South Korea), e.g., 2023.12.31 |
const localFormatStrings: { [key: string]: string } = {
export const localFormatStrings: { [key: string]: string } = {
en: 'M/D/YYYY',
'en-GB': 'D/M/YYYY',
fr: 'DD/MM/YYYY',
Expand All @@ -32,7 +32,7 @@ const localFormatStrings: { [key: string]: string } = {
ko: 'YYYY.MM.DD',
};

const friendlyFormatStrings: { [key: string]: string } = {
export const friendlyFormatStrings: { [key: string]: string } = {
en: 'MMMM D, YYYY', // English
'en-GB': 'D MMMM YYYY', // English GB
zh: 'YYYY 年 M 月 D 日', // Chinese
Expand All @@ -51,7 +51,7 @@ const friendlyFormatStrings: { [key: string]: string } = {
ta: 'D MMMM, YYYY', // Tamil
};

function getFormatStringForLanguage(language: string, preset: { [key: string]: string }) {
export function getFormatStringForLanguage(language: string, preset: { [key: string]: string }) {
// If the full language tag is not found, fallback to the base language
const baseLanguage = language.split('-')[0];
return preset[language] || preset[baseLanguage] || preset['en']; // Default to 'en'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@ export const useKeyboardSelection = (props: ISelectionKeyboardProps) => {

useHotkeys(
['enter'],
() => {
(keyboardEvent) => {
if (keyboardEvent.isComposing) return;

const { isColumnSelection, ranges: selectionRanges } = selection;
if (isEditing) {
let range = selectionRanges[0];
Expand Down