From 88e2df5885040e7c7f14ac58bc33e4763b1acb59 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 31 Jul 2024 20:33:57 +0500 Subject: [PATCH] Scheduled meetings (#6206) Signed-off-by: Denis Bykhov --- models/love/package.json | 3 +- models/love/src/index.ts | 50 +++++++-- .../extensions/DocCreateExtComponent.svelte | 2 +- .../src/components/CreateEvent.svelte | 76 +++++++++---- .../src/components/EditEvent.svelte | 3 +- .../src/components/EventReminders.svelte | 2 + plugins/calendar/src/index.ts | 5 +- plugins/login-resources/src/utils.ts | 2 +- plugins/love-assets/lang/en.json | 3 +- plugins/love-assets/lang/es.json | 3 +- plugins/love-assets/lang/fr.json | 3 +- plugins/love-assets/lang/pt.json | 3 +- plugins/love-assets/lang/ru.json | 3 +- plugins/love-assets/lang/zh.json | 3 +- plugins/love-resources/package.json | 1 + .../src/components/ControlBar.svelte | 5 +- .../src/components/EditMeetingData.svelte | 37 +++++++ .../love-resources/src/components/Hall.svelte | 44 ++++---- .../src/components/MeetingData.svelte | 56 ++++++++++ .../src/components/RoomSelector.svelte | 64 +++++++++++ plugins/love-resources/src/index.ts | 13 ++- plugins/love-resources/src/plugin.ts | 13 ++- plugins/love-resources/src/utils.ts | 69 +++++++++++- plugins/love/package.json | 1 + plugins/love/src/index.ts | 10 +- server/account/src/operations.ts | 34 +++--- .../calendar/pod-calendar/src/calendar.ts | 101 +++++++++++++++++- 27 files changed, 512 insertions(+), 97 deletions(-) create mode 100644 plugins/love-resources/src/components/EditMeetingData.svelte create mode 100644 plugins/love-resources/src/components/MeetingData.svelte create mode 100644 plugins/love-resources/src/components/RoomSelector.svelte diff --git a/models/love/package.json b/models/love/package.json index b7109a59bb..ac9a360544 100644 --- a/models/love/package.json +++ b/models/love/package.json @@ -46,6 +46,7 @@ "@hcengineering/love": "^0.6.0", "@hcengineering/love-resources": "^0.6.0", "@hcengineering/notification": "^0.6.23", - "@hcengineering/model-notification": "^0.6.0" + "@hcengineering/model-notification": "^0.6.0", + "@hcengineering/model-calendar": "^0.6.0" } } diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 4732183984..b71ee1ef3e 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -15,21 +15,13 @@ import contact, { type Employee, type Person } from '@hcengineering/contact' import { AccountRole, DOMAIN_TRANSIENT, IndexKind, type Domain, type Ref } from '@hcengineering/core' -import { Index, Model, Prop, TypeRef, type Builder } from '@hcengineering/model' -import core, { TDoc } from '@hcengineering/model-core' -import preference, { TPreference } from '@hcengineering/model-preference' -import presentation from '@hcengineering/model-presentation' -import view, { createAction } from '@hcengineering/model-view' -import notification from '@hcengineering/notification' -import { getEmbeddedLabel } from '@hcengineering/platform' -import setting from '@hcengineering/setting' -import workbench from '@hcengineering/workbench' import { loveId, type DevicesPreference, type Floor, type Invite, type JoinRequest, + type Meeting, type Office, type ParticipantInfo, type RequestStatus, @@ -38,6 +30,16 @@ import { type RoomInfo, type RoomType } from '@hcengineering/love' +import { Index, Mixin, Model, Prop, TypeRef, type Builder } from '@hcengineering/model' +import calendar, { TEvent } from '@hcengineering/model-calendar' +import core, { TDoc } from '@hcengineering/model-core' +import preference, { TPreference } from '@hcengineering/model-preference' +import presentation from '@hcengineering/model-presentation' +import view, { createAction } from '@hcengineering/model-view' +import notification from '@hcengineering/notification' +import { getEmbeddedLabel } from '@hcengineering/platform' +import setting from '@hcengineering/setting' +import workbench from '@hcengineering/workbench' import love from './plugin' export { loveId } from '@hcengineering/love' @@ -127,10 +129,25 @@ export class TRoomInfo extends TDoc implements RoomInfo { isOffice!: boolean } +@Mixin(love.mixin.Meeting, calendar.class.Event) +export class TMeeting extends TEvent implements Meeting { + room!: Ref +} + export default love export function createModel (builder: Builder): void { - builder.createModel(TRoom, TFloor, TOffice, TParticipantInfo, TJoinRequest, TDevicesPreference, TRoomInfo, TInvite) + builder.createModel( + TRoom, + TFloor, + TOffice, + TParticipantInfo, + TJoinRequest, + TDevicesPreference, + TRoomInfo, + TInvite, + TMeeting + ) builder.createDoc( workbench.class.Application, @@ -151,6 +168,19 @@ export function createModel (builder: Builder): void { component: love.component.WorkbenchExtension }) + builder.createDoc(presentation.class.DocCreateExtension, core.space.Model, { + ofClass: calendar.class.Event, + apply: love.function.CreateMeeting, + components: { + body: love.component.MeetingData + } + }) + + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { + extension: calendar.extensions.EditEventExtensions, + component: love.component.EditMeetingData + }) + builder.createDoc( setting.class.SettingsCategory, core.space.Model, diff --git a/packages/presentation/src/components/extensions/DocCreateExtComponent.svelte b/packages/presentation/src/components/extensions/DocCreateExtComponent.svelte index 94d3e6e9e8..bfca1781d7 100644 --- a/packages/presentation/src/components/extensions/DocCreateExtComponent.svelte +++ b/packages/presentation/src/components/extensions/DocCreateExtComponent.svelte @@ -7,7 +7,7 @@ export let manager: DocCreateExtensionManager export let kind: CreateExtensionKind export let props: Record = {} - export let space: Space | undefined + export let space: Space | undefined = undefined $: extensions = manager.extensions diff --git a/plugins/calendar-resources/src/components/CreateEvent.svelte b/plugins/calendar-resources/src/components/CreateEvent.svelte index 421cf68333..a5d0975fdd 100644 --- a/plugins/calendar-resources/src/components/CreateEvent.svelte +++ b/plugins/calendar-resources/src/components/CreateEvent.svelte @@ -13,10 +13,15 @@ // limitations under the License. --> + +{#if isMeeting && meeting} + changeRoom(ev.detail)} /> +{/if} diff --git a/plugins/love-resources/src/components/Hall.svelte b/plugins/love-resources/src/components/Hall.svelte index 027af91b3a..b1a8b48fc4 100644 --- a/plugins/love-resources/src/components/Hall.svelte +++ b/plugins/love-resources/src/components/Hall.svelte @@ -16,12 +16,12 @@ import { Contact, Person } from '@hcengineering/contact' import { personByIdStore } from '@hcengineering/contact-resources' import { Ref } from '@hcengineering/core' - import love, { Floor as FloorType, Office, Room, RoomInfo, isOffice } from '@hcengineering/love' + import love, { Floor as FloorType, Meeting, Office, Room, RoomInfo, isOffice } from '@hcengineering/love' import { getClient } from '@hcengineering/presentation' import { deviceOptionsStore as deviceInfo, getCurrentLocation, navigate } from '@hcengineering/ui' import { onMount, onDestroy } from 'svelte' import { activeFloor, floors, infos, invites, myInfo, myRequests, rooms } from '../stores' - import { tryConnect } from '../utils' + import { connectToMeeting, tryConnect } from '../utils' import Floor from './Floor.svelte' import FloorConfigure from './FloorConfigure.svelte' import Floors from './Floors.svelte' @@ -42,25 +42,31 @@ $: $deviceInfo.replacedPanel = replacedPanel onDestroy(() => ($deviceInfo.replacedPanel = undefined)) + async function connectToSession (sessionId: string): Promise { + const client = getClient() + const info = await client.findOne(love.class.RoomInfo, { _id: sessionId as Ref }) + if (info === undefined) return + const room = $rooms.find((p) => p._id === info.room) + if (room === undefined) return + tryConnect( + $personByIdStore, + $myInfo, + room, + $infos.filter((p) => p.room === room._id), + $myRequests, + $invites + ) + } + onMount(async () => { const loc = getCurrentLocation() - const { meetId, ...query } = loc.query ?? {} - if (meetId != null) { - loc.query = Object.keys(query).length === 0 ? undefined : query - navigate(loc, true) - const client = getClient() - const info = await client.findOne(love.class.RoomInfo, { _id: meetId as Ref }) - if (info === undefined) return - const room = $rooms.find((p) => p._id === info.room) - if (room === undefined) return - tryConnect( - $personByIdStore, - $myInfo, - room, - $infos.filter((p) => p.room === room._id), - $myRequests, - $invites - ) + const { sessionId, meetId, ...query } = loc.query ?? {} + loc.query = Object.keys(query).length === 0 ? undefined : query + navigate(loc, true) + if (sessionId != null) { + await connectToSession(sessionId) + } else if (meetId != null) { + await connectToMeeting($personByIdStore, $myInfo, $infos, $myRequests, $invites, $rooms, meetId) } }) diff --git a/plugins/love-resources/src/components/MeetingData.svelte b/plugins/love-resources/src/components/MeetingData.svelte new file mode 100644 index 0000000000..cd16c10e17 --- /dev/null +++ b/plugins/love-resources/src/components/MeetingData.svelte @@ -0,0 +1,56 @@ + + + +
+ +
+{#if isMeeting} + { + changeRoom(ev.detail) + }} + /> +{/if} diff --git a/plugins/love-resources/src/components/RoomSelector.svelte b/plugins/love-resources/src/components/RoomSelector.svelte new file mode 100644 index 0000000000..570b4f96c6 --- /dev/null +++ b/plugins/love-resources/src/components/RoomSelector.svelte @@ -0,0 +1,64 @@ + + + +{#if items.length > 0} +
+ + { + change(e.detail._id) + }} + /> +
+{/if} diff --git a/plugins/love-resources/src/index.ts b/plugins/love-resources/src/index.ts index 37018fb045..3f57a19ab3 100644 --- a/plugins/love-resources/src/index.ts +++ b/plugins/love-resources/src/index.ts @@ -1,10 +1,12 @@ import { type Resources } from '@hcengineering/platform' import ControlExt from './components/ControlExt.svelte' +import EditMeetingData from './components/EditMeetingData.svelte' import Main from './components/Main.svelte' +import MeetingData from './components/MeetingData.svelte' +import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte' import Settings from './components/Settings.svelte' import WorkbenchExtension from './components/WorkbenchExtension.svelte' -import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte' -import { toggleMic, toggleVideo } from './utils' +import { createMeeting, toggleMic, toggleVideo } from './utils' export { setCustomCreateScreenTracks } from './utils' @@ -14,7 +16,12 @@ export default async (): Promise => ({ ControlExt, Settings, WorkbenchExtension, - SelectScreenSourcePopup + SelectScreenSourcePopup, + MeetingData, + EditMeetingData + }, + function: { + CreateMeeting: createMeeting }, actionImpl: { ToggleMic: toggleMic, diff --git a/plugins/love-resources/src/plugin.ts b/plugins/love-resources/src/plugin.ts index 91a66f57d6..1ae5211df8 100644 --- a/plugins/love-resources/src/plugin.ts +++ b/plugins/love-resources/src/plugin.ts @@ -13,15 +13,22 @@ // limitations under the License. // -import { mergeIds, type IntlString } from '@hcengineering/platform' -import { type AnyComponent } from '@hcengineering/ui' import love, { loveId } from '@hcengineering/love' +import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' +import { type DocCreateFunction } from '@hcengineering/presentation' +import { type AnyComponent } from '@hcengineering/ui' export default mergeIds(loveId, love, { component: { - ControlExt: '' as AnyComponent + ControlExt: '' as AnyComponent, + MeetingData: '' as AnyComponent, + EditMeetingData: '' as AnyComponent + }, + function: { + CreateMeeting: '' as Resource }, string: { + CreateMeeting: '' as IntlString, LeaveRoom: '' as IntlString, LeaveRoomConfirmation: '' as IntlString, Mute: '' as IntlString, diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index d2dce4af16..14ae6de8a6 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -1,6 +1,17 @@ import { Analytics } from '@hcengineering/analytics' +import calendar, { type Event } from '@hcengineering/calendar' import contact, { getName, type Person, type PersonAccount } from '@hcengineering/contact' -import core, { concatLink, getCurrentAccount, type IdMap, type Ref, type Space } from '@hcengineering/core' +import core, { + AccountRole, + concatLink, + getCurrentAccount, + type Data, + type IdMap, + type Ref, + type Space, + type TxOperations +} from '@hcengineering/core' +import login from '@hcengineering/login' import { RequestStatus, RoomAccess, @@ -9,12 +20,13 @@ import { loveId, type Invite, type JoinRequest, + type Meeting, type Office, type ParticipantInfo, type Room } from '@hcengineering/love' -import { getEmbeddedLabel, getMetadata, type IntlString } from '@hcengineering/platform' -import presentation, { createQuery, getClient } from '@hcengineering/presentation' +import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform' +import presentation, { createQuery, getClient, type DocCreatePhase } from '@hcengineering/presentation' import { getCurrentLocation, navigate, type DropdownTextItem } from '@hcengineering/ui' import { KrispNoiseFilter, isKrispNoiseFilterSupported } from '@livekit/krisp-noise-filter' import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors' @@ -555,6 +567,33 @@ function checkPlace (room: Room, info: ParticipantInfo[], x: number, y: number): return !isOffice(room) && info.find((p) => p.x === x && p.y === y) === undefined } +export async function connectToMeeting ( + personByIdStore: IdMap, + currentInfo: ParticipantInfo | undefined, + info: ParticipantInfo[], + currentRequests: JoinRequest[], + currentInvites: Invite[], + rooms: Room[], + meetId: string +): Promise { + const client = getClient() + const meeting = await client.findOne(love.mixin.Meeting, { _id: meetId as Ref }) + if (meeting === undefined) return + const room = rooms.find((p) => p._id === meeting.room) + if (room === undefined) return + + // check time (it should be 10 minutes before the meeting or active in roomInfo) + + await tryConnect( + personByIdStore, + currentInfo, + room, + info.filter((p) => p.room === room._id), + currentRequests, + currentInvites + ) +} + export async function tryConnect ( personByIdStore: IdMap, currentInfo: ParticipantInfo | undefined, @@ -720,3 +759,27 @@ async function checkRecordAvailable (): Promise { } void checkRecordAvailable() + +export async function createMeeting ( + client: TxOperations, + _id: Ref, + space: Space, + data: Data, + store: Record, + phase: DocCreatePhase +): Promise { + if (phase === 'post' && store.room != null && store.isMeeting === true) { + await client.createMixin(_id, calendar.class.Event, space._id, love.mixin.Meeting, { + room: store.room as Ref + }) + const event = await client.findOne(calendar.class.Event, { _id }) + if (event === undefined) return + const navigateUrl = getCurrentLocation() + navigateUrl.query = { + meetId: _id + } + const func = await getResource(login.function.GetInviteLink) + const link = await func(-1, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl))) + await client.update(event, { location: link }) + } +} diff --git a/plugins/love/package.json b/plugins/love/package.json index 8b214191fb..d3e111ac8c 100644 --- a/plugins/love/package.json +++ b/plugins/love/package.json @@ -43,6 +43,7 @@ "@hcengineering/preference": "^0.6.13", "@hcengineering/notification": "^0.6.23", "@hcengineering/ui": "^0.6.15", + "@hcengineering/calendar": "^0.6.24", "@hcengineering/drive": "^0.6.0", "@hcengineering/core": "^0.6.32", "@hcengineering/view": "^0.6.13" diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts index 960018c38c..95816ec231 100644 --- a/plugins/love/src/index.ts +++ b/plugins/love/src/index.ts @@ -1,5 +1,6 @@ +import { Event } from '@hcengineering/calendar' import { Person } from '@hcengineering/contact' -import { Class, Doc, Ref } from '@hcengineering/core' +import { Class, Doc, Mixin, Ref } from '@hcengineering/core' import { Drive } from '@hcengineering/drive' import { NotificationType } from '@hcengineering/notification' import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform' @@ -58,6 +59,10 @@ export interface RoomInfo extends Doc { isOffice: boolean } +export interface Meeting extends Event { + room: Ref +} + export enum RequestStatus { Pending, Approved, @@ -97,6 +102,9 @@ const love = plugin(loveId, { RoomInfo: '' as Ref>, Invite: '' as Ref> }, + mixin: { + Meeting: '' as Ref> + }, action: { ToggleMic: '' as Ref, ToggleVideo: '' as Ref diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index d883f64ae8..863208e1ac 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -24,7 +24,6 @@ import contact, { Person, PersonAccount } from '@hcengineering/contact' -import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' import core, { AccountRole, BaseWorkspaceInfo, @@ -40,30 +39,17 @@ import core, { Ref, roleOrder, systemAccountEmail, + Timestamp, Tx, TxOperations, Version, versionToString, WorkspaceId, - Timestamp, WorkspaceIdWithUrl, type Branding } from '@hcengineering/core' import { consoleModelLogger, MigrateOperation, ModelLogger } from '@hcengineering/model' import platform, { getMetadata, PlatformError, Severity, Status, translate } from '@hcengineering/platform' -import { decodeToken, generateToken } from '@hcengineering/server-token' -import toolPlugin, { - connect, - initializeWorkspace, - initModel, - updateModel, - prepareTools, - upgradeModel -} from '@hcengineering/server-tool' -import { pbkdf2Sync, randomBytes } from 'crypto' -import { Binary, Db, Filter, ObjectId, type MongoClient } from 'mongodb' -import fetch from 'node-fetch' -import otpGenerator from 'otp-generator' import { DummyFullTextAdapter, Pipeline, @@ -78,6 +64,20 @@ import { registerServerPlugins, registerStringLoaders } from '@hcengineering/server-pipeline' +import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' +import { decodeToken, generateToken } from '@hcengineering/server-token' +import toolPlugin, { + connect, + initializeWorkspace, + initModel, + prepareTools, + updateModel, + upgradeModel +} from '@hcengineering/server-tool' +import { pbkdf2Sync, randomBytes } from 'crypto' +import { Binary, Db, Filter, ObjectId, type MongoClient } from 'mongodb' +import fetch from 'node-fetch' +import otpGenerator from 'otp-generator' import { accountPlugin } from './plugin' @@ -692,7 +692,7 @@ export async function checkInvite (ctx: MeasureContext, invite: Invite | null, e Analytics.handleError(new Error(`no invite or invite limit exceed ${email}`)) throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {})) } - if (invite.exp < Date.now()) { + if (invite.exp !== -1 && invite.exp < Date.now()) { ctx.error('invite', { email, state: 'link expired' }) Analytics.handleError(new Error(`invite link expired ${invite._id.toString()} ${email}`)) throw new PlatformError(new Status(Severity.ERROR, platform.status.ExpiredLink, {})) @@ -1577,7 +1577,7 @@ export async function getInviteLink ( ctx.info('Getting invite link', { workspace: workspace.name, emailMask, limit }) const data: Omit = { workspace, - exp: Date.now() + exp, + exp: exp < 0 ? -1 : Date.now() + exp, emailMask, limit, role: role ?? AccountRole.User diff --git a/services/calendar/pod-calendar/src/calendar.ts b/services/calendar/pod-calendar/src/calendar.ts index 03daf71223..05b0672ae3 100644 --- a/services/calendar/pod-calendar/src/calendar.ts +++ b/services/calendar/pod-calendar/src/calendar.ts @@ -27,7 +27,10 @@ import core, { AttachedData, Client, Data, + Doc, + DocData, DocumentUpdate, + Mixin, Ref, TxOperations, TxUpdateDoc, @@ -42,10 +45,10 @@ import type { Collection, Db } from 'mongodb' import { encode64 } from './base64' import { CalendarController } from './calendarController' import config from './config' +import { RateLimiter } from './rateLimiter' import type { CalendarHistory, EventHistory, EventWatch, ProjectCredentials, State, Token, User, Watch } from './types' import { encodeReccuring, isToken, parseRecurrenceStrings } from './utils' import type { WorkspaceClient } from './workspaceClient' -import { RateLimiter } from './rateLimiter' const SCOPES = [ 'https://www.googleapis.com/auth/calendar.calendars.readonly', @@ -703,7 +706,7 @@ export class CalendarClient { } const data: Partial> = await this.parseUpdateData(event) if (event.recurringEventId != null) { - const diff = this.getEventDiff( + const diff = this.getDiff( { ...data, recurringEventId: event.recurringEventId as Ref, @@ -719,7 +722,7 @@ export class CalendarClient { } else { if (event.recurrence != null) { const parseRule = parseRecurrenceStrings(event.recurrence) - const diff = this.getEventDiff( + const diff = this.getDiff( { ...data, rules: parseRule.rules, @@ -733,13 +736,70 @@ export class CalendarClient { await this.client.update(current, diff) } } else { - const diff = this.getEventDiff(data, current) + const diff = this.getDiff(data, current) if (Object.keys(diff).length > 0) { console.log('UPDATE EVENT DIFF', JSON.stringify(diff), JSON.stringify(current)) await this.client.update(current, diff) } } } + await this.updateMixins(event, current) + } + + private async updateMixins (event: calendar_v3.Schema$Event, current: Event): Promise { + const mixins = this.parseMixins(event) + if (mixins !== undefined) { + for (const mixin in mixins) { + const attr = mixins[mixin] + if (typeof attr === 'object' && Object.keys(attr).length > 0) { + if (this.client.getHierarchy().hasMixin(current, mixin as Ref>)) { + const diff = this.getDiff(attr, this.client.getHierarchy().as(current, mixin as Ref>)) + if (Object.keys(diff).length > 0) { + await this.client.updateMixin( + current._id, + current._class, + calendar.space.Calendar, + mixin as Ref>, + diff + ) + } + } else { + await this.client.createMixin( + current._id, + current._class, + calendar.space.Calendar, + mixin as Ref>, + attr + ) + } + } + } + } + } + + private parseMixins (event: calendar_v3.Schema$Event): Record | undefined { + if (event.extendedProperties?.shared?.mixins !== undefined) { + const mixins = JSON.parse(event.extendedProperties.shared.mixins) + return mixins + } + } + + private async saveMixins (event: calendar_v3.Schema$Event, _id: Ref): Promise { + const mixins = this.parseMixins(event) + if (mixins !== undefined) { + for (const mixin in mixins) { + const attr = mixins[mixin] + if (typeof attr === 'object' && Object.keys(attr).length > 0) { + await this.client.createMixin( + _id, + calendar.class.Event, + calendar.space.Calendar, + mixin as Ref>, + attr + ) + } + } + } } private async saveExtEvent ( @@ -767,6 +827,7 @@ export class CalendarClient { timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT' } ) + await this.saveMixins(event, id) console.log('SAVE INSTANCE', id, JSON.stringify(event)) } else if (event.status !== 'cancelled') { if (event.recurrence != null) { @@ -786,6 +847,7 @@ export class CalendarClient { timeZone: event.start?.timeZone ?? event.end?.timeZone ?? 'Etc/GMT' } ) + await this.saveMixins(event, id) console.log('SAVE REC EVENT', id, JSON.stringify(event)) } else { const id = await this.client.addCollection( @@ -796,12 +858,13 @@ export class CalendarClient { 'events', data ) + await this.saveMixins(event, id) console.log('SAVE EVENT', id, JSON.stringify(event)) } } } - private getEventDiff(data: Partial>, current: T): Partial> { + private getDiff(data: Partial>, current: T): Partial> { const res = {} for (const key in data) { if (!deepEqual((data as any)[key], (current as any)[key])) { @@ -1120,6 +1183,24 @@ export class CalendarClient { return false } + private getMixinFields (event: Event): Record { + const res = {} + const h = this.client.getHierarchy() + for (const [k, v] of Object.entries(event)) { + if (typeof v === 'object' && h.isMixin(k as Ref>)) { + for (const [key, value] of Object.entries(v)) { + if (value !== undefined) { + const obj = (res as any)[k] ?? {} + obj[key] = value + ;(res as any)[k] = obj + } + } + } + } + + return res + } + private convertBody (event: Event): calendar_v3.Schema$Event { const res: calendar_v3.Schema$Event = { start: convertDate(event.date, event.allDay, getTimezone(event)), @@ -1141,6 +1222,16 @@ export class CalendarClient { } } } + const mixin = this.getMixinFields(event) + if (Object.keys(mixin).length > 0) { + res.extendedProperties = { + ...res.extendedProperties, + shared: { + ...res.extendedProperties?.shared, + mixin: JSON.stringify(mixin) + } + } + } if (event.reminders !== undefined) { res.reminders = { useDefault: false,