mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
Scheduled meetings (#6206)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
31f6fcbcf8
commit
88e2df5885
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<Room>
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -7,7 +7,7 @@
|
||||
export let manager: DocCreateExtensionManager
|
||||
export let kind: CreateExtensionKind
|
||||
export let props: Record<string, any> = {}
|
||||
export let space: Space | undefined
|
||||
export let space: Space | undefined = undefined
|
||||
|
||||
$: extensions = manager.extensions
|
||||
|
||||
|
@ -13,10 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Calendar, RecurringRule, Visibility, generateEventId } from '@hcengineering/calendar'
|
||||
import { Calendar, Event, ReccuringEvent, RecurringRule, Visibility, generateEventId } from '@hcengineering/calendar'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, Doc, Markup, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import core, { Class, Doc, Markup, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import presentation, {
|
||||
createQuery,
|
||||
DocCreateExtComponent,
|
||||
DocCreateExtensionManager,
|
||||
getClient
|
||||
} from '@hcengineering/presentation'
|
||||
import { EmptyMarkup } from '@hcengineering/text'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor-resources'
|
||||
import {
|
||||
@ -43,16 +48,21 @@
|
||||
import ReccurancePopup from './ReccurancePopup.svelte'
|
||||
import VisibilityEditor from './VisibilityEditor.svelte'
|
||||
|
||||
const currentUser = getCurrentAccount() as PersonAccount
|
||||
|
||||
export let attachedTo: Ref<Doc> = calendar.ids.NoAttached
|
||||
export let attachedToClass: Ref<Class<Doc>> = calendar.class.Event
|
||||
export let title: string = ''
|
||||
export let date: Date | undefined = undefined
|
||||
export let withTime = false
|
||||
export let participants: Ref<Person>[] = [currentUser.person]
|
||||
|
||||
const now = new Date()
|
||||
const defaultDuration = 60 * 60 * 1000
|
||||
const allDayDuration = 24 * 60 * 60 * 1000 - 1
|
||||
|
||||
const docCreateManager = DocCreateExtensionManager.create(calendar.class.Event)
|
||||
|
||||
let startDate =
|
||||
date === undefined ? now.getTime() : withTime ? date.getTime() : date.setHours(now.getHours(), now.getMinutes())
|
||||
const duration = defaultDuration
|
||||
@ -75,10 +85,14 @@
|
||||
}
|
||||
})
|
||||
|
||||
const spaceQ = createQuery()
|
||||
let space: Space | undefined = undefined
|
||||
spaceQ.query(core.class.Space, { _id: calendar.space.Calendar }, (res) => {
|
||||
space = res[0]
|
||||
})
|
||||
|
||||
let rules: RecurringRule[] = []
|
||||
|
||||
const currentUser = getCurrentAccount() as PersonAccount
|
||||
let participants: Ref<Person>[] = [currentUser.person]
|
||||
let externalParticipants: string[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -93,6 +107,7 @@
|
||||
if (startDate != null) date = startDate
|
||||
if (date === undefined) return
|
||||
if (title === '') return
|
||||
const _id = generateId<Event>()
|
||||
if (rules.length > 0) {
|
||||
await client.addCollection(
|
||||
calendar.class.ReccuringEvent,
|
||||
@ -119,25 +134,37 @@
|
||||
access: 'owner',
|
||||
originalStartTime: allDay ? saveUTC(date) : date,
|
||||
timeZone
|
||||
}
|
||||
},
|
||||
_id as Ref<ReccuringEvent>
|
||||
)
|
||||
} else {
|
||||
await client.addCollection(calendar.class.Event, calendar.space.Calendar, attachedTo, attachedToClass, 'events', {
|
||||
calendar: _calendar,
|
||||
eventId: generateEventId(),
|
||||
date: allDay ? saveUTC(date) : date,
|
||||
dueDate: allDay ? saveUTC(dueDate) : dueDate,
|
||||
externalParticipants,
|
||||
description,
|
||||
visibility,
|
||||
participants,
|
||||
reminders,
|
||||
title,
|
||||
location,
|
||||
allDay,
|
||||
timeZone,
|
||||
access: 'owner'
|
||||
})
|
||||
await client.addCollection(
|
||||
calendar.class.Event,
|
||||
calendar.space.Calendar,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
'events',
|
||||
{
|
||||
calendar: _calendar,
|
||||
eventId: generateEventId(),
|
||||
date: allDay ? saveUTC(date) : date,
|
||||
dueDate: allDay ? saveUTC(dueDate) : dueDate,
|
||||
externalParticipants,
|
||||
description,
|
||||
visibility,
|
||||
participants,
|
||||
reminders,
|
||||
title,
|
||||
location,
|
||||
allDay,
|
||||
timeZone,
|
||||
access: 'owner'
|
||||
},
|
||||
_id
|
||||
)
|
||||
}
|
||||
if (space !== undefined) {
|
||||
await docCreateManager.commit(client, _id, space, {}, 'post')
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
@ -204,6 +231,9 @@
|
||||
<LocationEditor focusIndex={10010} bind:value={location} />
|
||||
<EventParticipants focusIndex={10011} bind:participants bind:externalParticipants />
|
||||
</div>
|
||||
<div class="block">
|
||||
<DocCreateExtComponent manager={docCreateManager} kind={'body'} />
|
||||
</div>
|
||||
<div class="block row gap-1-5">
|
||||
<div class="top-icon">
|
||||
<Icon icon={calendar.icon.Description} size={'small'} />
|
||||
@ -222,7 +252,7 @@
|
||||
<CalendarSelector bind:value={_calendar} focusIndex={10101} />
|
||||
<div class="flex-row-center flex-gap-1">
|
||||
<Icon icon={calendar.icon.Hidden} size={'small'} />
|
||||
<VisibilityEditor bind:value={visibility} kind={'tertiary'} focusIndex={10102} withoutIcon />
|
||||
<VisibilityEditor bind:value={visibility} kind={'tertiary'} size={'small'} focusIndex={10102} withoutIcon />
|
||||
</div>
|
||||
<EventReminders bind:reminders focusIndex={10103} />
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { Event, ReccuringEvent, ReccuringInstance, RecurringRule } from '@hcengineering/calendar'
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { DocumentUpdate, Ref } from '@hcengineering/core'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import presentation, { ComponentExtensions, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor-resources'
|
||||
import {
|
||||
Button,
|
||||
@ -199,6 +199,7 @@
|
||||
<div class="block rightCropPadding">
|
||||
<LocationEditor bind:value={location} focusIndex={10005} />
|
||||
<EventParticipants bind:participants bind:externalParticipants disabled={readOnly} focusIndex={10006} />
|
||||
<ComponentExtensions extension={calendar.extensions.EditEventExtensions} props={{ readOnly, value: object }} />
|
||||
</div>
|
||||
<div class="block row gap-1-5">
|
||||
<div class="top-icon">
|
||||
|
@ -49,6 +49,8 @@
|
||||
<Button
|
||||
label={reminders.length > 0 ? calendar.string.AddReminder : calendar.string.Reminders}
|
||||
{disabled}
|
||||
padding={'0 .5rem'}
|
||||
justify={'left'}
|
||||
kind={'ghost'}
|
||||
{focusIndex}
|
||||
on:click={(e) => {
|
||||
|
@ -17,7 +17,7 @@ import { NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, IntlString, Metadata, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { Handler, IntegrationType } from '@hcengineering/setting'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -212,6 +212,9 @@ const calendarPlugin = plugin(calendarId, {
|
||||
metadata: {
|
||||
CalendarServiceURL: '' as Metadata<string>
|
||||
},
|
||||
extensions: {
|
||||
EditEventExtensions: '' as ComponentExtensionId
|
||||
},
|
||||
ids: {
|
||||
ReminderNotification: '' as Ref<NotificationType>,
|
||||
NoAttached: '' as Ref<Event>
|
||||
|
@ -521,7 +521,7 @@ export async function getInviteLinkId (
|
||||
): Promise<string> {
|
||||
const accountsUrl = getMetadata(login.metadata.AccountsUrl)
|
||||
|
||||
const exp = expHours * 1000 * 60 * 60
|
||||
const exp = expHours < 0 ? -1 : expHours * 1000 * 60 * 60
|
||||
|
||||
if (accountsUrl === undefined) {
|
||||
throw new Error('accounts url not specified')
|
||||
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "Full-screen mode",
|
||||
"ExitingFullscreenMode": "Exiting fullscreen mode",
|
||||
"Select": "Select",
|
||||
"ChooseShare": "Choose what to share"
|
||||
"ChooseShare": "Choose what to share",
|
||||
"CreateMeeting": "Create meeting"
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "Modo de pantalla completa",
|
||||
"ExitingFullscreenMode": "Salir del modo de pantalla completa",
|
||||
"Select": "Seleccionar",
|
||||
"ChooseShare": "Elija qué compartir"
|
||||
"ChooseShare": "Elija qué compartir",
|
||||
"CreateMeeting": "Crear reunión"
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "Mode plein écran",
|
||||
"ExitingFullscreenMode": "Quitter le mode plein écran",
|
||||
"Select": "Sélectionner",
|
||||
"ChooseShare": "Choisissez ce que vous voulez partager"
|
||||
"ChooseShare": "Choisissez ce que vous voulez partager",
|
||||
"CreateMeeting": "Créer une réunion"
|
||||
}
|
||||
}
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "Modo de ecrã inteiro",
|
||||
"ExitingFullscreenMode": "Saindo do modo de tela cheia",
|
||||
"Select": "Seleccione",
|
||||
"ChooseShare": "Escolha o que partilhar"
|
||||
"ChooseShare": "Escolha o que partilhar",
|
||||
"CreateMeeting": "Criar reunião"
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "Полноэкранный режим",
|
||||
"ExitingFullscreenMode": "Выход из полноэкранного режима",
|
||||
"Select": "Выбрать",
|
||||
"ChooseShare": "Выберите, чем вы хотите поделиться"
|
||||
"ChooseShare": "Выберите, чем вы хотите поделиться",
|
||||
"CreateMeeting": "Создать встречу"
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@
|
||||
"FullscreenMode": "全屏模式",
|
||||
"ExitingFullscreenMode": "退出全屏模式",
|
||||
"Select": "选择",
|
||||
"ChooseShare": "选择共享内容"
|
||||
"ChooseShare": "选择共享内容",
|
||||
"CreateMeeting": "创建会议"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"svelte": "^4.2.12",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/calendar": "^0.6.24",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
|
@ -122,10 +122,11 @@
|
||||
if (roomInfo !== undefined) {
|
||||
const navigateUrl = getCurrentLocation()
|
||||
navigateUrl.query = {
|
||||
meetId: roomInfo._id
|
||||
sessionId: roomInfo._id
|
||||
}
|
||||
|
||||
const func = await getResource(login.function.GetInviteLink)
|
||||
return await func(24 * 30, '', -1, AccountRole.Guest, JSON.stringify(navigateUrl))
|
||||
return await func(24, '', -1, AccountRole.Guest, encodeURIComponent(JSON.stringify(navigateUrl)))
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
37
plugins/love-resources/src/components/EditMeetingData.svelte
Normal file
37
plugins/love-resources/src/components/EditMeetingData.svelte
Normal file
@ -0,0 +1,37 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { Event } from '@hcengineering/calendar'
|
||||
import love from '../plugin'
|
||||
import RoomSelector from './RoomSelector.svelte'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Room } from '@hcengineering/love'
|
||||
|
||||
export let value: Event
|
||||
|
||||
const client = getClient()
|
||||
|
||||
$: isMeeting = client.getHierarchy().hasMixin(value, love.mixin.Meeting)
|
||||
$: meeting = isMeeting ? client.getHierarchy().as(value, love.mixin.Meeting) : null
|
||||
|
||||
async function changeRoom (val: Ref<Room>): Promise<void> {
|
||||
await client.updateMixin(value._id, value._class, value.space, love.mixin.Meeting, { room: val })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isMeeting && meeting}
|
||||
<RoomSelector value={meeting?.room} on:change={(ev) => changeRoom(ev.detail)} />
|
||||
{/if}
|
@ -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<void> {
|
||||
const client = getClient()
|
||||
const info = await client.findOne(love.class.RoomInfo, { _id: sessionId as Ref<RoomInfo> })
|
||||
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<RoomInfo> })
|
||||
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)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
56
plugins/love-resources/src/components/MeetingData.svelte
Normal file
56
plugins/love-resources/src/components/MeetingData.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { Ref } from '@hcengineering/core'
|
||||
import { Room } from '@hcengineering/love'
|
||||
import { Button, CheckBox } from '@hcengineering/ui'
|
||||
import { Writable } from 'svelte/store'
|
||||
import love from '../plugin'
|
||||
import RoomSelector from './RoomSelector.svelte'
|
||||
|
||||
export let state: Writable<Record<string, any>>
|
||||
|
||||
function changeRoom (val: Ref<Room>) {
|
||||
$state.room = val
|
||||
}
|
||||
|
||||
function changeIsMeeting () {
|
||||
$state.isMeeting = isMeeting
|
||||
}
|
||||
|
||||
let isMeeting = false
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center gap-1-5 mt-1">
|
||||
<CheckBox bind:checked={isMeeting} kind={'primary'} on:value={changeIsMeeting} />
|
||||
<Button
|
||||
label={love.string.CreateMeeting}
|
||||
kind={'ghost'}
|
||||
padding={'0 .5rem'}
|
||||
justify={'left'}
|
||||
on:click={() => {
|
||||
isMeeting = !isMeeting
|
||||
changeIsMeeting()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isMeeting}
|
||||
<RoomSelector
|
||||
value={$state.room}
|
||||
on:change={(ev) => {
|
||||
changeRoom(ev.detail)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
64
plugins/love-resources/src/components/RoomSelector.svelte
Normal file
64
plugins/love-resources/src/components/RoomSelector.svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { Ref } from '@hcengineering/core'
|
||||
import love, { isOffice, Room } from '@hcengineering/love'
|
||||
import { Dropdown, Icon } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { rooms } from '../stores'
|
||||
|
||||
export let value: Ref<Room> | undefined
|
||||
export let disabled: boolean = false
|
||||
export let focusIndex = -1
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: items = $rooms
|
||||
.filter((p) => !isOffice(p))
|
||||
.map((p) => {
|
||||
return {
|
||||
_id: p._id,
|
||||
label: p.name
|
||||
}
|
||||
})
|
||||
|
||||
$: selected = value !== undefined ? items.find((p) => p._id === value) : undefined
|
||||
|
||||
function change (id: Ref<Room>) {
|
||||
if (value !== id) {
|
||||
dispatch('change', id)
|
||||
value = id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if items.length > 0}
|
||||
<div class="flex-row-center flex-gap-1">
|
||||
<Icon icon={love.icon.Mic} size={'small'} />
|
||||
<Dropdown
|
||||
kind={'ghost'}
|
||||
size={'medium'}
|
||||
placeholder={love.string.Room}
|
||||
{items}
|
||||
withSearch={false}
|
||||
{selected}
|
||||
{disabled}
|
||||
{focusIndex}
|
||||
on:selected={(e) => {
|
||||
change(e.detail._id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
@ -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<Resources> => ({
|
||||
ControlExt,
|
||||
Settings,
|
||||
WorkbenchExtension,
|
||||
SelectScreenSourcePopup
|
||||
SelectScreenSourcePopup,
|
||||
MeetingData,
|
||||
EditMeetingData
|
||||
},
|
||||
function: {
|
||||
CreateMeeting: createMeeting
|
||||
},
|
||||
actionImpl: {
|
||||
ToggleMic: toggleMic,
|
||||
|
@ -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<DocCreateFunction>
|
||||
},
|
||||
string: {
|
||||
CreateMeeting: '' as IntlString,
|
||||
LeaveRoom: '' as IntlString,
|
||||
LeaveRoomConfirmation: '' as IntlString,
|
||||
Mute: '' as IntlString,
|
||||
|
@ -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<Person>,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
info: ParticipantInfo[],
|
||||
currentRequests: JoinRequest[],
|
||||
currentInvites: Invite[],
|
||||
rooms: Room[],
|
||||
meetId: string
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
const meeting = await client.findOne(love.mixin.Meeting, { _id: meetId as Ref<Meeting> })
|
||||
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<Person>,
|
||||
currentInfo: ParticipantInfo | undefined,
|
||||
@ -720,3 +759,27 @@ async function checkRecordAvailable (): Promise<void> {
|
||||
}
|
||||
|
||||
void checkRecordAvailable()
|
||||
|
||||
export async function createMeeting (
|
||||
client: TxOperations,
|
||||
_id: Ref<Event>,
|
||||
space: Space,
|
||||
data: Data<Event>,
|
||||
store: Record<string, any>,
|
||||
phase: DocCreatePhase
|
||||
): Promise<void> {
|
||||
if (phase === 'post' && store.room != null && store.isMeeting === true) {
|
||||
await client.createMixin<Event, Meeting>(_id, calendar.class.Event, space._id, love.mixin.Meeting, {
|
||||
room: store.room as Ref<Room>
|
||||
})
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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<Room>
|
||||
}
|
||||
|
||||
export enum RequestStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
@ -97,6 +102,9 @@ const love = plugin(loveId, {
|
||||
RoomInfo: '' as Ref<Class<RoomInfo>>,
|
||||
Invite: '' as Ref<Class<Invite>>
|
||||
},
|
||||
mixin: {
|
||||
Meeting: '' as Ref<Mixin<Meeting>>
|
||||
},
|
||||
action: {
|
||||
ToggleMic: '' as Ref<Action>,
|
||||
ToggleVideo: '' as Ref<Action>
|
||||
|
@ -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<Invite, '_id'> = {
|
||||
workspace,
|
||||
exp: Date.now() + exp,
|
||||
exp: exp < 0 ? -1 : Date.now() + exp,
|
||||
emailMask,
|
||||
limit,
|
||||
role: role ?? AccountRole.User
|
||||
|
@ -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<AttachedData<Event>> = await this.parseUpdateData(event)
|
||||
if (event.recurringEventId != null) {
|
||||
const diff = this.getEventDiff<ReccuringInstance>(
|
||||
const diff = this.getDiff<ReccuringInstance>(
|
||||
{
|
||||
...data,
|
||||
recurringEventId: event.recurringEventId as Ref<ReccuringEvent>,
|
||||
@ -719,7 +722,7 @@ export class CalendarClient {
|
||||
} else {
|
||||
if (event.recurrence != null) {
|
||||
const parseRule = parseRecurrenceStrings(event.recurrence)
|
||||
const diff = this.getEventDiff<ReccuringEvent>(
|
||||
const diff = this.getDiff<ReccuringEvent>(
|
||||
{
|
||||
...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<void> {
|
||||
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<Mixin<Doc>>)) {
|
||||
const diff = this.getDiff(attr, this.client.getHierarchy().as(current, mixin as Ref<Mixin<Doc>>))
|
||||
if (Object.keys(diff).length > 0) {
|
||||
await this.client.updateMixin(
|
||||
current._id,
|
||||
current._class,
|
||||
calendar.space.Calendar,
|
||||
mixin as Ref<Mixin<Doc>>,
|
||||
diff
|
||||
)
|
||||
}
|
||||
} else {
|
||||
await this.client.createMixin(
|
||||
current._id,
|
||||
current._class,
|
||||
calendar.space.Calendar,
|
||||
mixin as Ref<Mixin<Doc>>,
|
||||
attr
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseMixins (event: calendar_v3.Schema$Event): Record<string, any> | 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<Event>): Promise<void> {
|
||||
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<Mixin<Doc>>,
|
||||
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<T extends Event>(data: Partial<AttachedData<T>>, current: T): Partial<AttachedData<T>> {
|
||||
private getDiff<T extends Doc>(data: Partial<DocData<T>>, current: T): Partial<DocData<T>> {
|
||||
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<string, any> {
|
||||
const res = {}
|
||||
const h = this.client.getHierarchy()
|
||||
for (const [k, v] of Object.entries(event)) {
|
||||
if (typeof v === 'object' && h.isMixin(k as Ref<Mixin<Doc>>)) {
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user