diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 8774d9a2b5..cedb0a1272 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -983,6 +983,9 @@ dependencies: minio: specifier: ^7.0.26 version: 7.1.3 + moment-timezone: + specifier: ^0.5.43 + version: 0.5.43 mongodb: specifier: ^4.11.0 version: 4.17.1 @@ -13221,6 +13224,16 @@ packages: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} dev: false + /moment-timezone@0.5.43: + resolution: {integrity: sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==} + dependencies: + moment: 2.29.4 + dev: false + + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + dev: false + /mongodb-connection-string-url@2.6.0: resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==} dependencies: @@ -17768,7 +17781,7 @@ packages: dev: false file:projects/calendar-resources.tgz(@types/node@16.11.68)(esbuild@0.16.17)(postcss-load-config@4.0.1)(postcss@8.4.31)(ts-node@10.9.1): - resolution: {integrity: sha512-uhWVaA9LfyRGkkRYOipr4pcykZVqdl5EHUGCF146vAHuv01YV+njwZCeSRzbDYEB9VzQdPMdhh6/Wol8Zm3ENg==, tarball: file:projects/calendar-resources.tgz} + resolution: {integrity: sha512-GKA5RNUEh4K+pp2mAf479tKTKebnRqldRDNln/RgNekUVHs/vqOuzHbu/4uHlIpC1QJXxyi7H+m6lidp5bqv7A==, tarball: file:projects/calendar-resources.tgz} id: file:projects/calendar-resources.tgz name: '@rush-temp/calendar-resources' version: 0.0.0 @@ -17785,6 +17798,7 @@ packages: eslint-plugin-svelte3: 4.0.0(eslint@8.51.0)(svelte@3.55.1) fast-equals: 2.0.4 jest: 29.7.0(@types/node@16.11.68)(ts-node@10.9.1) + moment-timezone: 0.5.43 prettier: 3.1.0 prettier-plugin-svelte: 3.1.0(prettier@3.1.0)(svelte@4.2.5) sass: 1.69.0 @@ -23711,7 +23725,7 @@ packages: dev: false file:projects/ui.tgz(@types/node@16.11.68)(esbuild@0.16.17)(postcss-load-config@4.0.1)(postcss@8.4.31)(ts-node@10.9.1): - resolution: {integrity: sha512-g5wJSxU0oHaWmOknjGHp3d+ML2V2bnveZV51xeHaq5GfbEwCdHat3DOsEt6zC3BMVRZsyJz8EmwNnd6A+KZYcA==, tarball: file:projects/ui.tgz} + resolution: {integrity: sha512-N7xpf37pbt76Fi7r6f4ioMRsUhBC6xMQmAliUP5wX0OLlzS/sU7SHQFqY5BYcXBV9HhyTcRqQ3cIG2yGgQivDQ==, tarball: file:projects/ui.tgz} id: file:projects/ui.tgz name: '@rush-temp/ui' version: 0.0.0 @@ -23731,6 +23745,7 @@ packages: fast-equals: 2.0.4 jest: 29.7.0(@types/node@16.11.68)(ts-node@10.9.1) just-clone: 6.2.0 + moment-timezone: 0.5.43 prettier: 3.1.0 prettier-plugin-svelte: 3.1.0(prettier@3.1.0)(svelte@4.2.5) sass: 1.69.0 diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index 2d5809e048..ff790a1231 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -117,11 +117,14 @@ export class TEvent extends TAttachedDoc implements Event { access!: 'freeBusyReader' | 'reader' | 'writer' | 'owner' visibility?: Visibility + + timeZone?: string } @Model(calendar.class.ReccuringEvent, calendar.class.Event) @UX(calendar.string.ReccuringEvent, calendar.icon.Calendar) export class TReccuringEvent extends TEvent implements ReccuringEvent { + declare timeZone: string rules!: RecurringRule[] exdate!: Timestamp[] rdate!: Timestamp[] @@ -132,7 +135,6 @@ export class TReccuringEvent extends TEvent implements ReccuringEvent { @UX(calendar.string.Event, calendar.icon.Calendar) export class TReccuringInstance extends TReccuringEvent implements ReccuringInstance { recurringEventId!: Ref - declare originalStartTime: number isCancelled?: boolean virtual?: boolean } diff --git a/models/calendar/src/migration.ts b/models/calendar/src/migration.ts index 1667c5f062..948faa9e09 100644 --- a/models/calendar/src/migration.ts +++ b/models/calendar/src/migration.ts @@ -13,10 +13,15 @@ // limitations under the License. // -import { type Calendar, type Event, type ReccuringEvent } from '@hcengineering/calendar' +import { calendarId, type Calendar, type Event, type ReccuringEvent } from '@hcengineering/calendar' import contact from '@hcengineering/contact' -import core, { type Ref, TxOperations } from '@hcengineering/core' -import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' +import core, { TxOperations, type Ref } from '@hcengineering/core' +import { + tryMigrate, + type MigrateOperation, + type MigrationClient, + type MigrationUpgradeClient +} from '@hcengineering/model' import { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_SETTING } from '@hcengineering/model-setting' import { type Integration } from '@hcengineering/setting' @@ -121,6 +126,17 @@ async function migrateSync (client: MigrationClient): Promise { ) } +async function migrateTimezone (client: MigrationClient): Promise { + await client.update( + DOMAIN_CALENDAR, + { + _class: { $in: [calendar.class.ReccuringEvent, calendar.class.ReccuringInstance] }, + timeZone: { $exists: false } + }, + { timeZone: 'Etc/GMT' } + ) +} + export const calendarOperation: MigrateOperation = { async migrate (client: MigrationClient): Promise { await fixEventDueDate(client) @@ -128,6 +144,12 @@ export const calendarOperation: MigrateOperation = { await fillOriginalStartTime(client) await migrateSync(client) await migrateExternalCalendars(client) + await tryMigrate(client, calendarId, [ + { + state: 'timezone', + func: migrateTimezone + } + ]) }, async upgrade (client: MigrationUpgradeClient): Promise { const tx = new TxOperations(client, core.account.System) diff --git a/packages/ui/package.json b/packages/ui/package.json index 81163c9439..924de03b7a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -43,7 +43,8 @@ "svelte": "^4.2.5", "fast-equals": "^2.0.3", "autolinker": "4.0.0", - "emoji-regex": "^10.1.0" + "emoji-regex": "^10.1.0", + "moment-timezone": "^0.5.43" }, "repository": "https://github.com/hcenginneing/anticrm", "publishConfig": { diff --git a/packages/ui/src/components/internal/TimeZonesPopup.svelte b/packages/ui/src/components/TimeZonesPopup.svelte similarity index 89% rename from packages/ui/src/components/internal/TimeZonesPopup.svelte rename to packages/ui/src/components/TimeZonesPopup.svelte index 5b98380972..bb86106b3a 100644 --- a/packages/ui/src/components/internal/TimeZonesPopup.svelte +++ b/packages/ui/src/components/TimeZonesPopup.svelte @@ -27,7 +27,7 @@ IconChevronDown, ActionIcon, IconUndo - } from '../..' + } from '..' interface TimeZoneGroup { continent: string @@ -38,6 +38,7 @@ export let timeZones: TimeZone[] = [] export let count: number export let reset: string | null + export let withAdd: boolean = true const dispatch = createEventDispatcher() @@ -142,18 +143,20 @@ {#each items as item} {/each} diff --git a/packages/ui/src/components/calendar/DateInputBox.svelte b/packages/ui/src/components/calendar/DateInputBox.svelte index 95bbae4694..63c49b4528 100644 --- a/packages/ui/src/components/calendar/DateInputBox.svelte +++ b/packages/ui/src/components/calendar/DateInputBox.svelte @@ -18,11 +18,13 @@ import Icon from '../Icon.svelte' import Label from '../Label.svelte' import IconClose from '../icons/Close.svelte' - import { daysInMonth } from './internal/DateUtils' + import { daysInMonth, getUserTimezone } from './internal/DateUtils' + import moment from 'moment-timezone' export let currentDate: Date | null export let withTime: boolean = false export let kind: 'default' | 'plain' = 'default' + export let timeZone: string = getUserTimezone() type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min' interface IEdits { @@ -44,19 +46,31 @@ if (date == null) date = new Date() switch (id) { case 'day': + date = new Date(timeZone ? moment(date).tz(timeZone).date(val).valueOf() : moment(date).date(val).valueOf()) date.setDate(val) break case 'month': - date.setMonth(val - 1) + date = new Date( + timeZone + ? moment(date) + .tz(timeZone) + .month(val + 1) + .valueOf() + : moment(date) + .month(val + 1) + .valueOf() + ) break case 'year': - date.setFullYear(val) + date = new Date(timeZone ? moment(date).tz(timeZone).year(val).valueOf() : moment(date).year(val).valueOf()) break case 'hour': - date.setHours(val) + date = new Date(timeZone ? moment(date).tz(timeZone).hours(val).valueOf() : moment(date).hours(val).valueOf()) break case 'min': - date.setMinutes(val) + date = new Date( + timeZone ? moment(date).tz(timeZone).minutes(val).valueOf() : moment(date).minutes(val).valueOf() + ) break } return date @@ -81,15 +95,15 @@ const getValue = (date: Date, id: TEdits): number => { switch (id) { case 'day': - return date.getDate() + return timeZone ? moment(date).tz(timeZone).date() : moment(date).date() case 'month': - return date.getMonth() + 1 + return timeZone ? moment(date).tz(timeZone).month() + 1 : moment(date).month() + 1 case 'year': - return date.getFullYear() + return timeZone ? moment(date).tz(timeZone).year() : moment(date).year() case 'hour': - return date.getHours() + return timeZone ? moment(date).tz(timeZone).hours() : moment(date).hours() case 'min': - return date.getMinutes() + return timeZone ? moment(date).tz(timeZone).minutes() : moment(date).minutes() } } diff --git a/packages/ui/src/components/calendar/DatePopup.svelte b/packages/ui/src/components/calendar/DatePopup.svelte index c64dae97b4..d0a3254752 100644 --- a/packages/ui/src/components/calendar/DatePopup.svelte +++ b/packages/ui/src/components/calendar/DatePopup.svelte @@ -19,11 +19,12 @@ import { ActionIcon, Button, - Scroller, - deviceOptionsStore as deviceInfo, - checkAdaptiveMatching, + IconClose, Label, - IconClose + Scroller, + checkAdaptiveMatching, + deviceOptionsStore as deviceInfo, + getUserTimezone } from '../..' import ui from '../../plugin' import DateInputBox from './DateInputBox.svelte' @@ -36,6 +37,7 @@ export let label = currentDate != null ? ui.string.EditDueDate : ui.string.AddDueDate export let detail: IntlString | undefined = undefined export let noShift: boolean = false + export let timeZone: string = getUserTimezone() const dispatch = createEventDispatcher() @@ -110,6 +112,7 @@ bind:this={dateInput} bind:currentDate {withTime} + {timeZone} kind={'plain'} on:close={() => { closeDP(withTime) @@ -126,6 +129,7 @@ { @@ -158,6 +163,7 @@ bind:currentDate {viewDate} {mondayStart} + {timeZone} viewUpdate={false} hideNavigator={'all'} noPadding @@ -169,6 +175,7 @@
{new Date(date).toLocaleDateString('default', options)} diff --git a/plugins/calendar-resources/src/components/EditEvent.svelte b/plugins/calendar-resources/src/components/EditEvent.svelte index cb85cb6d74..7fbb4fc64f 100644 --- a/plugins/calendar-resources/src/components/EditEvent.svelte +++ b/plugins/calendar-resources/src/components/EditEvent.svelte @@ -18,20 +18,28 @@ import { DocumentUpdate, Ref } from '@hcengineering/core' import presentation, { getClient } from '@hcengineering/presentation' import { StyledTextBox } from '@hcengineering/text-editor' - import { Button, EditBox, Icon, IconClose, createFocusManager, showPopup } from '@hcengineering/ui' + import { + Button, + EditBox, + FocusHandler, + Icon, + IconClose, + createFocusManager, + getUserTimezone, + showPopup + } from '@hcengineering/ui' import { deepEqual } from 'fast-equals' import { createEventDispatcher } from 'svelte' import calendar from '../plugin' import { isReadOnly, saveUTC, updateReccuringInstance } from '../utils' + import CalendarSelector from './CalendarSelector.svelte' import EventParticipants from './EventParticipants.svelte' import EventReminders from './EventReminders.svelte' import EventTimeEditor from './EventTimeEditor.svelte' import EventTimeExtraButton from './EventTimeExtraButton.svelte' + import LocationEditor from './LocationEditor.svelte' import ReccurancePopup from './ReccurancePopup.svelte' import VisibilityEditor from './VisibilityEditor.svelte' - import CalendarSelector from './CalendarSelector.svelte' - import LocationEditor from './LocationEditor.svelte' - import FocusHandler from '@hcengineering/ui/src/components/FocusHandler.svelte' export let object: Event $: readOnly = isReadOnly(object) @@ -48,6 +56,7 @@ let visibility = object.visibility ?? 'public' let reminders = [...(object.reminders ?? [])] let space = object.space + let timeZone: string = object.timeZone ?? getUserTimezone() let description = object.description let location = object.location @@ -84,6 +93,9 @@ if (object.location !== location) { update.location = location } + if (object.timeZone !== timeZone) { + update.timeZone = timeZone + } if (allDay !== object.allDay) { update.date = allDay ? saveUTC(startDate) : startDate update.dueDate = allDay ? saveUTC(dueDate) : dueDate @@ -170,8 +182,16 @@
- - + +
diff --git a/plugins/calendar-resources/src/components/EventTimeEditor.svelte b/plugins/calendar-resources/src/components/EventTimeEditor.svelte index 9634c714af..45b6bc51a3 100644 --- a/plugins/calendar-resources/src/components/EventTimeEditor.svelte +++ b/plugins/calendar-resources/src/components/EventTimeEditor.svelte @@ -13,18 +13,20 @@ // limitations under the License. --> -{#if !allDay && rules.length === 0} +{#if !allDay && rules.length === 0 && myTimezone === timeZone}
(allDay = true)}>
-
+
{#if !noRepeat} @@ -54,6 +93,7 @@ kind={'ghost'} padding={'0 .5rem'} justify={'left'} + disabled={readOnly} on:click={() => { allDay = !allDay dispatch('allday') @@ -62,13 +102,7 @@
- +
{#if !noRepeat}
@@ -78,6 +112,7 @@ kind={'ghost'} padding={'0 .5rem'} justify={'left'} + disabled={readOnly} on:click={() => dispatch('repeat')} > diff --git a/plugins/calendar-resources/src/components/TimeZoneSelector.svelte b/plugins/calendar-resources/src/components/TimeZoneSelector.svelte new file mode 100644 index 0000000000..b34a202367 --- /dev/null +++ b/plugins/calendar-resources/src/components/TimeZoneSelector.svelte @@ -0,0 +1,62 @@ + + + + diff --git a/plugins/calendar-resources/src/index.ts b/plugins/calendar-resources/src/index.ts index 5c57d3bb83..d35bd19e25 100644 --- a/plugins/calendar-resources/src/index.ts +++ b/plugins/calendar-resources/src/index.ts @@ -102,7 +102,8 @@ async function deleteRecHandler (res: any, object: ReccuringInstance): Promise