Skip to content

Commit

Permalink
fix: incorrect filter conditions when viewing more events in the cale…
Browse files Browse the repository at this point in the history
…ndar (#1115)

* fix: calendar using default formatting when creating date fields

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

* fix: time zone in the calendar

* fix: grid editing exit when pressing enter with IME

* fix: time zone when adding events

* fix: time zone when expanding all events
  • Loading branch information
Sky-FE authored Nov 28, 2024
1 parent b2e278e commit 5e5f368
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 45 deletions.
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

0 comments on commit 5e5f368

Please sign in to comment.