UBERF-4127 (#4137)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-12-05 11:38:12 +06:00 committed by GitHub
parent e50bdd461b
commit d2b1e64b65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 395 additions and 116 deletions

View File

@ -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

View File

@ -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<ReccuringEvent>
declare originalStartTime: number
isCancelled?: boolean
virtual?: boolean
}

View File

@ -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<void> {
)
}
async function migrateTimezone (client: MigrationClient): Promise<void> {
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<void> {
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<void> {
const tx = new TxOperations(client, core.account.System)

View File

@ -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": {

View File

@ -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}
<button class="menu-item no-focus items-center" on:click={() => dispatch('close', item.id)}>
<span class="overflow-label label flex-grow">{item.city}</span>
<div class="tool ml-2">
<Button
icon={IconAdd}
size={'small'}
kind={'ghost'}
disabled={count > 4}
on:click={() => {
count++
dispatch('update', item.id)
}}
/>
</div>
{#if withAdd}
<div class="tool ml-2">
<Button
icon={IconAdd}
size={'small'}
kind={'ghost'}
disabled={count > 4}
on:click={() => {
count++
dispatch('update', item.id)
}}
/>
</div>
{/if}
</button>
{/each}
</div>

View File

@ -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()
}
}

View File

@ -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 @@
<MonthSquare
bind:currentDate
{viewDate}
{timeZone}
{mondayStart}
viewUpdate={false}
hideNavigator={'all'}
@ -140,6 +144,7 @@
bind:currentDate
viewDate={viewDateSec}
{mondayStart}
{timeZone}
viewUpdate={false}
noPadding
on:update={(result) => {
@ -158,6 +163,7 @@
bind:currentDate
{viewDate}
{mondayStart}
{timeZone}
viewUpdate={false}
hideNavigator={'all'}
noPadding
@ -169,6 +175,7 @@
<div class="space" />
<MonthSquare
bind:currentDate
{timeZone}
viewDate={viewDateSec}
{mondayStart}
viewUpdate={false}

View File

@ -14,23 +14,26 @@
-->
<script lang="ts">
import { afterUpdate, createEventDispatcher } from 'svelte'
import IconNavPrev from '../icons/NavPrev.svelte'
import IconNavNext from '../icons/NavNext.svelte'
import Icon from '../Icon.svelte'
import {
firstDay,
day,
getWeekDayName,
areDatesEqual,
getMonthName,
daysInMonth,
TCellStyle,
ICell
} from './internal/DateUtils'
import { capitalizeFirstLetter } from '../../utils'
import Icon from '../Icon.svelte'
import IconNavNext from '../icons/NavNext.svelte'
import IconNavPrev from '../icons/NavPrev.svelte'
import {
ICell,
TCellStyle,
areDatesEqual,
day,
daysInMonth,
firstDay,
getMonthName,
getUserTimezone,
getWeekDayName
} from './internal/DateUtils'
import moment from 'moment-timezone'
export let currentDate: Date | null
export let mondayStart: boolean = true
export let timeZone: string = getUserTimezone()
export let hideNavigator: boolean = false
const dispatch = createEventDispatcher()
@ -46,7 +49,16 @@
let days: ICell[] = []
const getDateStyle = (date: Date): TCellStyle => {
if (currentDate != null && areDatesEqual(currentDate, date)) return 'selected'
if (currentDate != null) {
const zonedTime = moment(currentDate).tz(timeZone)
if (
zonedTime.date() === date.getDate() &&
zonedTime.year() === date.getFullYear() &&
zonedTime.month() === date.getMonth()
) {
return 'selected'
}
}
return 'not-selected'
}
const renderCellStyles = (): void => {

View File

@ -17,8 +17,18 @@
import IconArrowLeft from '../icons/ArrowLeft.svelte'
import IconArrowRight from '../icons/ArrowRight.svelte'
import Button from '../Button.svelte'
import { firstDay, day, getWeekDayName, areDatesEqual, getMonthName, weekday, isWeekend } from './internal/DateUtils'
import {
firstDay,
day,
getWeekDayName,
areDatesEqual,
getMonthName,
weekday,
isWeekend,
getUserTimezone
} from './internal/DateUtils'
import { capitalizeFirstLetter } from '../../utils'
import moment from 'moment-timezone'
export let currentDate: Date | null
export let viewDate: Date
@ -27,6 +37,7 @@
export let viewUpdate: boolean = true
export let noPadding: boolean = false
export let displayedWeeksCount = 6
export let timeZone: string = getUserTimezone()
export let selectedTo: Date | null | undefined = undefined
const dispatch = createEventDispatcher()
@ -49,8 +60,26 @@
}
function isSelected (currentDate: Date | null, selectedTo: Date | null | undefined, target: Date): boolean {
if (currentDate != null && areDatesEqual(currentDate, target)) return true
if (selectedTo != null && areDatesEqual(selectedTo, target)) return true
if (currentDate != null) {
const zonedTime = moment(currentDate).tz(timeZone)
if (
zonedTime.date() === target.getDate() &&
zonedTime.year() === target.getFullYear() &&
zonedTime.month() === target.getMonth()
) {
return true
}
}
if (selectedTo != null) {
const zonedTime = moment(selectedTo).tz(timeZone)
if (
zonedTime.date() === target.getDate() &&
zonedTime.year() === target.getFullYear() &&
zonedTime.month() === target.getMonth()
) {
return true
}
}
return false
}

View File

@ -15,8 +15,10 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import Month from './Month.svelte'
import { getUserTimezone } from './internal/DateUtils'
export let currentDate: Date | null
export let timeZone: string = getUserTimezone()
export let mondayStart: boolean = true
const dispatch = createEventDispatcher()
@ -25,6 +27,7 @@
<div class="antiPopup popup">
<Month
bind:currentDate
{timeZone}
{mondayStart}
on:update={(result) => {
if (result.detail !== undefined) {

View File

@ -16,11 +16,13 @@
import { afterUpdate, createEventDispatcher } from 'svelte'
import ui from '../../plugin'
import Label from '../Label.svelte'
import moment from 'moment-timezone'
export let currentDate: Date
export let size: 'small' | 'medium' = 'medium'
export let noBorder: boolean = false
export let disabled: boolean = false
export let timeZone: string | undefined = undefined
type TEdits = 'hour' | 'min'
interface IEdits {
@ -38,14 +40,16 @@
const dispatch = createEventDispatcher()
const setValue = (val: number, date: Date | null, id: TEdits): Date => {
const setValue = (val: number, date: Date | null, id: TEdits, timeZone: string | undefined): Date => {
if (date == null) date = new Date()
switch (id) {
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
@ -61,23 +65,23 @@
}
}
const getValue = (date: Date, id: TEdits): number => {
const getValue = (date: Date, id: TEdits, timeZone: string | undefined): number => {
switch (id) {
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()
}
}
const dateToEdits = (currentDate: Date | null): void => {
const dateToEdits = (currentDate: Date | null, timeZone: string | undefined): void => {
if (currentDate == null) {
edits.forEach((edit) => {
edit.value = -1
})
} else {
for (const edit of edits) {
edit.value = getValue(currentDate, edit.id)
edit.value = getValue(currentDate, edit.id, timeZone)
}
}
edits = edits
@ -85,7 +89,7 @@
export const isNull = (currentDate?: Date): boolean => {
if (currentDate !== undefined) {
dateToEdits(currentDate)
dateToEdits(currentDate, timeZone)
}
let result: boolean = false
edits.forEach((edit, i) => {
@ -113,8 +117,8 @@
}
if (!isNull() && !startTyping) {
fixEdits()
currentDate = setValue(edits[index].value, currentDate, ed)
dateToEdits(currentDate)
currentDate = setValue(edits[index].value, currentDate, ed, timeZone)
dateToEdits(currentDate, timeZone)
}
edits = edits
dispatch('update', currentDate)
@ -132,8 +136,8 @@
if (edits[index].value !== -1) {
const val = ev.code === 'ArrowUp' ? edits[index].value + 1 : edits[index].value - 1
if (currentDate) {
currentDate = setValue(val, currentDate, ed)
dateToEdits(currentDate)
currentDate = setValue(val, currentDate, ed, timeZone)
dateToEdits(currentDate, timeZone)
dispatch('update', currentDate)
}
}
@ -160,7 +164,7 @@
dispatch('save')
}
$: dateToEdits(currentDate)
$: dateToEdits(currentDate, timeZone)
$: if (selected && edits[getIndex(selected)].el) edits[getIndex(selected)].el?.focus()
afterUpdate(() => {

View File

@ -11,6 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { type TimeZone } from '../../../types'
export const DAYS_IN_WEEK = 7
export const MILLISECONDS_IN_MINUTE = 60000
@ -127,6 +129,26 @@ export function getFormattedDate (value: number | null): string {
return value === null ? '' : new Date(value).toLocaleString('default', { month: 'short', day: 'numeric' })
}
export const getTimeZoneName = (): string => {
return Intl.DateTimeFormat().resolvedOptions().timeZone?.split('/')[1] ?? ''
export const getTimeZoneName = (val: string = Intl.DateTimeFormat().resolvedOptions().timeZone): string => {
return val.split('/')[1] ?? ''
}
export const convertTimeZone = (tz: string): TimeZone => {
const tzSpace = tz.replace(/_/gi, ' ')
const parts = tzSpace.split('/')
if (tz === '' || parts.length === 1) return { id: tz, continent: tzSpace, city: tzSpace, short: tzSpace }
return {
id: tz,
continent: parts[0],
city: parts.length > 2 ? `${parts[1]} - ${parts[2]}` : parts[1],
short: parts.length > 2 ? parts[2] : parts[1]
}
}
export function getUserTimezone (): string {
if (window.Intl !== undefined) {
return Intl.DateTimeFormat().resolvedOptions().timeZone
} else {
return 'Etc/GMT'
}
}

View File

@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { showPopup, TimeZone } from '../..'
import { convertTimeZone, showPopup, TimeZone } from '../..'
import ClockFace from './ClockFace.svelte'
import TimeZonesPopup from './TimeZonesPopup.svelte'
import TimeZonesPopup from '../TimeZonesPopup.svelte'
const clockSize: string = '80px'
const tzs: string[] = []
@ -30,17 +30,6 @@
if (!Intl.supportedValuesOf) console.log('Your browser does not support Intl.supportedValuesOf().')
else for (const timeZone of Intl.supportedValuesOf('timeZone')) tzs.push(timeZone)
const convertTimeZone = (tz: string): TimeZone => {
const tzSpace = tz.replaceAll('_', ' ')
const parts = tzSpace.split('/')
if (tz === '' || parts.length === 1) return { id: tz, continent: tzSpace, city: tzSpace, short: tzSpace }
return {
id: tz,
continent: parts[0],
city: parts.length > 2 ? `${parts[1]} - ${parts[2]}` : parts[1],
short: parts.length > 2 ? parts[2] : parts[1]
}
}
if (tzs.length > 0) tzs.forEach((tz) => timeZones.push(convertTimeZone(tz)))
const saveTZ = (): void => {

View File

@ -209,6 +209,7 @@ export { default as ModeSelector } from './components/ModeSelector.svelte'
export { default as SimpleTimePopup } from './components/calendar/SimpleTimePopup.svelte'
export { default as NumberInput } from './components/NumberInput.svelte'
export { default as Lazy } from './components/Lazy.svelte'
export { default as TimeZonesPopup } from './components/TimeZonesPopup.svelte'
export * from './types'
export * from './location'

View File

@ -51,6 +51,7 @@
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9",
"fast-equals": "^2.0.3"
"fast-equals": "^2.0.3",
"moment-timezone": "^0.5.43"
}
}

View File

@ -18,19 +18,28 @@
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Button, EditBox, Icon, IconClose, IconMoreH, createFocusManager, showPopup } from '@hcengineering/ui'
import {
Button,
EditBox,
FocusHandler,
Icon,
IconClose,
IconMoreH,
createFocusManager,
getUserTimezone,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { saveUTC } 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 attachedTo: Ref<Doc> = calendar.ids.NoAttached
export let attachedToClass: Ref<Class<Doc>> = calendar.class.Event
@ -41,7 +50,6 @@
const now = new Date()
const defaultDuration = 60 * 60 * 1000
const allDayDuration = 24 * 60 * 60 * 1000 - 1
// const offsetTZ = new Date().getTimezoneOffset() * 60 * 1000
let startDate =
date === undefined ? now.getTime() : withTime ? date.getTime() : date.setHours(now.getHours(), now.getMinutes())
@ -49,6 +57,7 @@
let dueDate = startDate + duration
let allDay = false
let location = ''
let timeZone: string = getUserTimezone()
let reminders = [30 * 60 * 1000]
@ -99,7 +108,8 @@
location,
allDay,
access: 'owner',
originalStartTime: allDay ? saveUTC(date) : date
originalStartTime: allDay ? saveUTC(date) : date,
timeZone
})
} else {
await client.addCollection(calendar.class.Event, space, attachedTo, attachedToClass, 'events', {
@ -114,6 +124,7 @@
title,
location,
allDay,
timeZone,
access: 'owner'
})
}
@ -168,8 +179,14 @@
</div>
</div>
<div class="block first flex-no-shrink">
<EventTimeEditor {allDay} bind:startDate bind:dueDate focusIndex={10004} />
<EventTimeExtraButton bind:allDay bind:rules on:repeat={setRecurrance} on:allday={allDayChangeHandler} />
<EventTimeEditor {allDay} bind:startDate bind:dueDate {timeZone} focusIndex={10004} />
<EventTimeExtraButton
bind:allDay
bind:timeZone
bind:rules
on:repeat={setRecurrance}
on:allday={allDayChangeHandler}
/>
</div>
<div class="block rightCropPadding">
<LocationEditor focusIndex={10010} bind:value={location} />

View File

@ -19,10 +19,11 @@
ButtonSize,
DatePopup,
SimpleDatePopup,
eventToHTMLElement,
showPopup,
TimeInputBox,
TimeShiftPresenter
TimeShiftPresenter,
eventToHTMLElement,
getUserTimezone,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import DateLocalePresenter from './DateLocalePresenter.svelte'
@ -36,6 +37,7 @@
export let size: ButtonSize = 'medium'
export let disabled: boolean = false
export let focusIndex = -1
export let timeZone: string = getUserTimezone()
const dispatch = createEventDispatcher()
@ -45,7 +47,7 @@
if (!showDate) {
showPopup(
DatePopup,
{ currentDate, withTime: !withoutTime, label: ui.string.SelectDate, noShift: true },
{ currentDate, withTime: !withoutTime, timeZone, label: ui.string.SelectDate, noShift: true },
undefined,
(res) => {
if (res) {
@ -58,7 +60,7 @@
}
function dateClick (e: MouseEvent) {
showPopup(SimpleDatePopup, { currentDate }, eventToHTMLElement(e), (res) => {
showPopup(SimpleDatePopup, { currentDate, timeZone }, eventToHTMLElement(e), (res) => {
if (res) {
date = res.getTime()
dispatch('update', date)
@ -80,10 +82,11 @@
{#if showDate || withoutTime}
<Button {kind} {size} padding={'0 .5rem'} {focusIndex} on:click={dateClick} {disabled}>
<svelte:fragment slot="content">
<DateLocalePresenter date={currentDate.getTime()} />
<DateLocalePresenter date={currentDate.getTime()} {timeZone} />
</svelte:fragment>
</Button>
{/if}
{#if !withoutTime}
<Button
{kind}
@ -96,6 +99,7 @@
<svelte:fragment slot="content">
<TimeInputBox
bind:currentDate
{timeZone}
noBorder
size={'small'}
on:update={(date) => {

View File

@ -13,9 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { getUserTimezone } from '@hcengineering/ui'
export let date: number
export let timeZone: string = getUserTimezone()
const current = new Date()
let options: Intl.DateTimeFormatOptions = {
timeZone,
day: 'numeric',
weekday: 'short',
month: 'short'
@ -26,6 +31,8 @@
year: '2-digit'
}
}
$: options.timeZone = timeZone
</script>
{new Date(date).toLocaleDateString('default', options)}

View File

@ -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 @@
</div>
</div>
<div class="block first flex-no-shrink">
<EventTimeEditor {allDay} bind:startDate bind:dueDate disabled={readOnly} focusIndex={10004} />
<EventTimeExtraButton bind:allDay bind:rules on:repeat={setRecurrance} on:allday={allDayChangeHandler} noRepeat />
<EventTimeEditor {allDay} bind:startDate {timeZone} bind:dueDate disabled={readOnly} focusIndex={10004} />
<EventTimeExtraButton
bind:allDay
bind:timeZone
bind:rules
on:repeat={setRecurrance}
{readOnly}
on:allday={allDayChangeHandler}
noRepeat
/>
</div>
<div class="block rightCropPadding">
<LocationEditor bind:value={location} focusIndex={10005} />

View File

@ -13,18 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Icon, areDatesEqual, IconArrowRight } from '@hcengineering/ui'
import { Icon, IconArrowRight, getUserTimezone } from '@hcengineering/ui'
import moment from 'moment-timezone'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import DateEditor from './DateEditor.svelte'
import { createEventDispatcher } from 'svelte'
export let startDate: number
export let dueDate: number
export let allDay: boolean
export let disabled: boolean = false
export let timeZone: string = getUserTimezone()
export let focusIndex = -1
$: sameDate = areDatesEqual(new Date(startDate), new Date(dueDate))
$: sameDate = moment(startDate).tz(timeZone).isSame(moment(dueDate).tz(timeZone), 'date')
let diff = dueDate - startDate
const allDayDuration = 24 * 60 * 60 * 1000 - 1
@ -56,6 +58,7 @@
bind:date={startDate}
direction={sameDate ? 'horizontal' : 'vertical'}
withoutTime={allDay}
{timeZone}
on:update={dateChange}
{disabled}
{focusIndex}
@ -70,6 +73,7 @@
showDate={!sameDate}
difference={startDate}
{disabled}
{timeZone}
focusIndex={focusIndex !== -1 ? focusIndex + 1 : focusIndex}
on:update={dueChange}
/>

View File

@ -14,28 +14,67 @@
-->
<script lang="ts">
import { RecurringRule } from '@hcengineering/calendar'
import { Button, CheckBox, Icon, Label, MILLISECONDS_IN_MINUTE } from '@hcengineering/ui'
import {
Button,
CheckBox,
Icon,
Label,
TimeZone,
TimeZonesPopup,
convertTimeZone,
eventToHTMLElement,
getUserTimezone,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import RRulePresenter from './RRulePresenter.svelte'
import TimeZoneSelector from './TimeZoneSelector.svelte'
export let allDay: boolean
export let rules: RecurringRule[] = []
export let noRepeat: boolean = false
export let timeZone: string
export let readOnly: boolean = false
const myTimezone = getUserTimezone()
const dispatch = createEventDispatcher()
const offsetTZ = new Date().getTimezoneOffset() * 60 * 1000
function showTimezoneSelector (e: MouseEvent) {
const timeZones: TimeZone[] = []
const tzs: string[] = []
if (!Intl.supportedValuesOf) console.log('Your browser does not support Intl.supportedValuesOf().')
else for (const timeZone of Intl.supportedValuesOf('timeZone')) tzs.push(timeZone)
if (tzs.length > 0) tzs.forEach((tz) => timeZones.push(convertTimeZone(tz)))
showPopup(
TimeZonesPopup,
{
timeZones,
withAdd: false,
selected: timeZone,
count: 1,
reset: null
},
eventToHTMLElement(e),
(result) => {
if (result !== undefined) {
timeZone = result
}
}
)
}
</script>
{#if !allDay && rules.length === 0}
{#if !allDay && rules.length === 0 && myTimezone === timeZone}
<div class="antiButton ghost x-small sh-no-shape text-11px pl-2 pr-2 pt-1 pb-1 mt-1 ml-5-5 gap-3 w-min">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="overflow-label cursor-pointer" on:click={() => (allDay = true)}>
<Label label={calendar.string.AllDay} />
</div>
<div class="overflow-label cursor-default">
<div class="overflow-label cursor-default" on:click={showTimezoneSelector}>
<Label label={calendar.string.TimeZone} />
</div>
{#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 @@
</div>
<div class="flex-row-center gap-1-5 mt-1">
<Icon icon={calendar.icon.Globe} size={'small'} fill={'var(--theme-dark-color)'} />
<Button label={calendar.string.TimeZone} kind={'ghost'} padding={'0 .5rem'} justify={'left'}>
<svelte:fragment slot="content">
<span class="ml-2 content-darker-color">
GMT{(offsetTZ > 0 ? '-' : '+') + Math.abs(offsetTZ / MILLISECONDS_IN_MINUTE / 60)}
</span>
</svelte:fragment>
</Button>
<TimeZoneSelector bind:timeZone disabled={readOnly} />
</div>
{#if !noRepeat}
<div class="flex-row-center gap-1-5 mt-1">
@ -78,6 +112,7 @@
kind={'ghost'}
padding={'0 .5rem'}
justify={'left'}
disabled={readOnly}
on:click={() => dispatch('repeat')}
>
<svelte:fragment slot="content">

View File

@ -0,0 +1,62 @@
<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import {
Button,
TimeZone,
TimeZonesPopup,
convertTimeZone,
eventToHTMLElement,
getTimeZoneName,
showPopup
} from '@hcengineering/ui'
import calendar from '../plugin'
export let timeZone: string
export let disabled: boolean = false
function open (e: MouseEvent) {
const timeZones: TimeZone[] = []
const tzs: string[] = []
if (!Intl.supportedValuesOf) console.log('Your browser does not support Intl.supportedValuesOf().')
else for (const timeZone of Intl.supportedValuesOf('timeZone')) tzs.push(timeZone)
if (tzs.length > 0) tzs.forEach((tz) => timeZones.push(convertTimeZone(tz)))
showPopup(
TimeZonesPopup,
{
timeZones,
withAdd: false,
selected: timeZone,
count: 1,
reset: null
},
eventToHTMLElement(e),
(result) => {
if (result !== undefined) {
timeZone = result
}
}
)
}
</script>
<Button {disabled} label={calendar.string.TimeZone} kind={'ghost'} padding={'0 .5rem'} justify={'left'} on:click={open}>
<svelte:fragment slot="content">
<span class="ml-2 content-darker-color">
{getTimeZoneName(timeZone)}
</span>
</svelte:fragment>
</Button>

View File

@ -102,7 +102,8 @@ async function deleteRecHandler (res: any, object: ReccuringInstance): Promise<v
rules: object.rules,
exdate: object.exdate,
visibility: object.visibility,
access: object.access
access: object.access,
timeZone: object.timeZone
},
object._id
)

View File

@ -162,6 +162,7 @@ export async function updateReccuringInstance (
rules: object.rules,
exdate: object.exdate,
rdate: object.rdate,
timeZone: object.timeZone,
...ops
},
object._id

View File

@ -69,6 +69,7 @@ export interface ReccuringEvent extends Event {
exdate: Timestamp[]
rdate: Timestamp[]
originalStartTime: Timestamp
timeZone: string
}
/**
@ -101,6 +102,8 @@ export interface Event extends AttachedDoc {
visibility?: Visibility
access: 'freeBusyReader' | 'reader' | 'writer' | 'owner'
timeZone?: string
}
/**