Select default meeting room in public schedule (#8732)

Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
This commit is contained in:
Chunosov 2025-04-28 20:01:36 +07:00 committed by GitHub
parent 3d8c358845
commit c31815e19f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 207 additions and 31 deletions

View File

@ -35,6 +35,7 @@ import {
type Meeting, type Meeting,
type MeetingMinutes, type MeetingMinutes,
type MeetingStatus, type MeetingStatus,
type MeetingSchedule,
type Office, type Office,
type ParticipantInfo, type ParticipantInfo,
type RequestStatus, type RequestStatus,
@ -61,7 +62,7 @@ import {
UX, UX,
TypeBoolean TypeBoolean
} from '@hcengineering/model' } from '@hcengineering/model'
import calendar, { TEvent } from '@hcengineering/model-calendar' import calendar, { TEvent, TSchedule } from '@hcengineering/model-calendar'
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation' import presentation from '@hcengineering/model-presentation'
@ -252,6 +253,11 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes, Tod
todos?: CollectionSize<ToDo> todos?: CollectionSize<ToDo>
} }
@Mixin(love.mixin.MeetingSchedule, calendar.class.Schedule)
export class TMeetingSchedule extends TSchedule implements MeetingSchedule {
room!: Ref<Room>
}
export default love export default love
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
@ -265,7 +271,8 @@ export function createModel (builder: Builder): void {
TRoomInfo, TRoomInfo,
TInvite, TInvite,
TMeeting, TMeeting,
TMeetingMinutes TMeetingMinutes,
TMeetingSchedule
) )
builder.createDoc( builder.createDoc(
@ -325,6 +332,19 @@ export function createModel (builder: Builder): void {
component: love.component.EditMeetingData component: love.component.EditMeetingData
}) })
builder.createDoc(presentation.class.DocCreateExtension, core.space.Model, {
ofClass: calendar.class.Schedule,
apply: love.function.CreateMeetingSchedule,
components: {
body: love.component.MeetingScheduleData
}
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: calendar.extensions.EditScheduleExtensions,
component: love.component.EditMeetingScheduleData
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: media.extension.StateContext, extension: media.extension.StateContext,
component: love.component.MediaPopupItemExt component: love.component.MediaPopupItemExt

View File

@ -8,6 +8,8 @@
import { Label, Modal, CheckBox, Spinner, Button, IconCopy, EditBox } from '@hcengineering/ui' import { Label, Modal, CheckBox, Spinner, Button, IconCopy, EditBox } from '@hcengineering/ui'
import calendar from '@hcengineering/calendar' import calendar from '@hcengineering/calendar'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { slide } from 'svelte/transition'
import { quintOut } from 'svelte/easing'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { getCurrentAccount, pickPrimarySocialId, SocialId, SocialIdType } from '@hcengineering/core' import { getCurrentAccount, pickPrimarySocialId, SocialId, SocialIdType } from '@hcengineering/core'
import { getAccountClient } from '../utils' import { getAccountClient } from '../utils'
@ -221,8 +223,12 @@
</div> </div>
</div> </div>
{#if !wasAccessEnabled} {#if !wasAccessEnabled && !loading}
<div class="flex-col-stretch flex-gap-1-5" class:accessDisabled={!accessEnabled}> <div
class="flex-col-stretch flex-gap-1-5"
class:accessDisabled={!accessEnabled}
transition:slide={{ duration: 300, easing: quintOut }}
>
<Label label={calendar.string.CalDavAccessPassword} /> <Label label={calendar.string.CalDavAccessPassword} />
<div class="flex-row-center flex-gap-1"> <div class="flex-row-center flex-gap-1">
<EditBox bind:value={password} kind="ghost" format="password" fullSize focusable disabled={true} /> <EditBox bind:value={password} kind="ghost" format="password" fullSize focusable disabled={true} />
@ -236,7 +242,9 @@
/> />
</div> </div>
{#if canSave} {#if canSave}
<Label label={calendar.string.CalDavAccessPasswordWarning} /> <div class:accessDisabled={!accessEnabled} transition:slide={{ duration: 300, easing: quintOut }}>
<Label label={calendar.string.CalDavAccessPasswordWarning} />
</div>
{/if} {/if}
</div> </div>
{/if} {/if}

View File

@ -15,8 +15,14 @@
// //
--> -->
<script lang="ts"> <script lang="ts">
import { Data, generateUuid, Ref } from '@hcengineering/core' import core, { Data, generateId, generateUuid, Ref, Space } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import {
ComponentExtensions,
createQuery,
DocCreateExtComponent,
DocCreateExtensionManager,
getClient
} from '@hcengineering/presentation'
import type { Schedule, ScheduleAvailability } from '@hcengineering/calendar' import type { Schedule, ScheduleAvailability } from '@hcengineering/calendar'
import ui, { import ui, {
Button, Button,
@ -51,12 +57,20 @@
export let schedule: Schedule | undefined export let schedule: Schedule | undefined
const docCreateManager = DocCreateExtensionManager.create(calendar.class.Schedule)
type EditableAvailability = Record<number, { start: Date, end: Date }[]> type EditableAvailability = Record<number, { start: Date, end: Date }[]>
const manager = createFocusManager() const manager = createFocusManager()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
const spaceQ = createQuery()
let space: Space | undefined = undefined
spaceQ.query(core.class.Space, { _id: calendar.space.Calendar }, (res) => {
space = res[0]
})
let title = schedule?.title ?? '' let title = schedule?.title ?? ''
let description = schedule?.description ?? '' let description = schedule?.description ?? ''
let meetingDuration = schedule?.meetingDuration ?? 30 * 60_000 let meetingDuration = schedule?.meetingDuration ?? 30 * 60_000
@ -175,6 +189,7 @@
async function saveSchedule (): Promise<void> { async function saveSchedule (): Promise<void> {
if (schedule === undefined) { if (schedule === undefined) {
const _id = generateId<Schedule>()
const currentUser = getCurrentEmployee() const currentUser = getCurrentEmployee()
const data: Data<Schedule> = { const data: Data<Schedule> = {
owner: currentUser, owner: currentUser,
@ -185,8 +200,10 @@
availability: getStorableAvailability(), availability: getStorableAvailability(),
timeZone timeZone
} }
const id = generateUuid() as Ref<Schedule> await client.createDoc(calendar.class.Schedule, calendar.space.Calendar, data, _id)
await client.createDoc(calendar.class.Schedule, calendar.space.Calendar, data, id) if (space !== undefined) {
await docCreateManager.commit(client, _id, space, {}, 'post')
}
} else { } else {
await client.update(schedule, { await client.update(schedule, {
title, title,
@ -299,7 +316,7 @@
bind:content={description} bind:content={description}
/> />
</div> </div>
<div class="block rightCropPadding"> <div class="block">
<div class="flex-row-top flex-gap-1"> <div class="flex-row-top flex-gap-1">
<Icon icon={calendar.icon.Duration} size={'small'} /> <Icon icon={calendar.icon.Duration} size={'small'} />
<div class="prop"> <div class="prop">
@ -312,7 +329,7 @@
on:click={showDurationVariants} on:click={showDurationVariants}
/> />
</div> </div>
<div class="prop"> <div class="prop" style="margin-left: 2rem; margin-right: 1rem">
<Label label={calendar.string.MeetingInterval} /> <Label label={calendar.string.MeetingInterval} />
<Button <Button
focusIndex={10005} focusIndex={10005}
@ -324,18 +341,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="block rightCropPadding"> <div class="block">
<div class="flex-row-center flex-gap-1-5"> <div class="flex-row-center flex-gap-1-5">
<Icon icon={calendar.icon.Globe} size={'small'} /> <Icon icon={calendar.icon.Globe} size={'small'} />
<TimeZoneSelector bind:timeZone /> <TimeZoneSelector bind:timeZone flex="1" />
</div> </div>
</div> </div>
<div class="block rightCropPadding"> <div class="block">
<div class="flex-row-top flex-gap-1"> <div class="flex-row-top flex-gap-1">
<Icon icon={calendar.icon.Timer} size={'small'} /> <Icon icon={calendar.icon.Timer} size={'small'} />
<div class="prop"> <div class="prop">
<Label label={calendar.string.ScheduleAvailability} /> <Label label={calendar.string.ScheduleAvailability} />
{#each getWeekDayNames() as { weekDay, dayName }, i} {#each getWeekDayNames() as { weekDay, dayName }}
<div class="flex-row-center flex-gap-1 availability"> <div class="flex-row-center flex-gap-1 availability">
<span class="weekDay"> <span class="weekDay">
{dayName} {dayName}
@ -375,6 +392,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="block">
{#if schedule === undefined}
<DocCreateExtComponent manager={docCreateManager} kind={'body'} />
{:else}
<ComponentExtensions extension={calendar.extensions.EditScheduleExtensions} props={{ value: schedule }} />
{/if}
</div>
</Scroller> </Scroller>
<div class="antiDivider noMargin" /> <div class="antiDivider noMargin" />
<div class="flex-between p-5 flex-no-shrink"> <div class="flex-between p-5 flex-no-shrink">
@ -410,18 +434,15 @@
flex-shrink: 0; flex-shrink: 0;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
flex-direction: column;
padding: 0.75rem 1rem 0.75rem 1.25rem;
&:not(:last-child) { &:not(:last-child) {
border-bottom: 1px solid var(--theme-divider-color); border-bottom: 1px solid var(--theme-divider-color);
} }
&:not(.rightCropPadding) {
padding: 0.75rem 1.25rem;
}
&.rightCropPadding {
padding: 0.75rem 1rem 0.75rem 1.25rem;
}
&.row { &.row {
padding: 0 1.25rem 0.5rem; padding: 0 1.25rem 0.5rem;
flex-direction: row;
} }
} }
@ -429,10 +450,14 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
padding: 0rem 0.75rem 0rem 0.75rem; flex: 1;
padding: 0rem 0rem 0rem 0.75rem;
gap: 0.5rem; gap: 0.5rem;
.availability { .availability {
width: 100%;
justify-content: space-between;
&:first-child { &:first-child {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@ -441,7 +466,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
width: 11rem;
} }
} }
} }
@ -453,7 +477,7 @@
} }
.weekDay { .weekDay {
width: 3rem; width: 2.25rem;
} }
} }
</style> </style>

View File

@ -26,6 +26,7 @@
export let timeZone: string export let timeZone: string
export let disabled: boolean = false export let disabled: boolean = false
export let flex: string | undefined = undefined
function open (e: MouseEvent) { function open (e: MouseEvent) {
if (disabled) { if (disabled) {
@ -56,7 +57,15 @@
} }
</script> </script>
<Button {disabled} label={calendar.string.TimeZone} kind={'ghost'} padding={'0 .5rem'} justify={'left'} on:click={open}> <Button
{disabled}
label={calendar.string.TimeZone}
kind={'ghost'}
padding={'0 .5rem'}
justify={'left'}
{flex}
on:click={open}
>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<span class="ml-2 content-darker-color"> <span class="ml-2 content-darker-color">
{getTimeZoneName(timeZone)} {getTimeZoneName(timeZone)}

View File

@ -274,7 +274,8 @@ const calendarPlugin = plugin(calendarId, {
CalDavServerURL: '' as Metadata<string> CalDavServerURL: '' as Metadata<string>
}, },
extensions: { extensions: {
EditEventExtensions: '' as ComponentExtensionId EditEventExtensions: '' as ComponentExtensionId,
EditScheduleExtensions: '' as ComponentExtensionId
}, },
ids: { ids: {
ReminderNotification: '' as Ref<NotificationType>, ReminderNotification: '' as Ref<NotificationType>,

View File

@ -0,0 +1,40 @@
<!--
// Copyright © 2025 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 { Schedule } 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: Schedule
const client = getClient()
$: isMeetingSchedule = client.getHierarchy().hasMixin(value, love.mixin.MeetingSchedule)
$: meetingSchedule = isMeetingSchedule ? client.getHierarchy().as(value, love.mixin.MeetingSchedule) : null
async function changeRoom (val: Ref<Room>): Promise<void> {
const schedules = await client.findAll(value._class, { _id: value._id }, { projection: { _id: 1 } })
for (const schedule of schedules) {
await client.updateMixin(schedule._id, schedule._class, schedule.space, love.mixin.MeetingSchedule, { room: val })
}
}
</script>
{#if isMeetingSchedule && meetingSchedule}
<RoomSelector value={meetingSchedule?.room} on:change={(ev) => changeRoom(ev.detail)} />
{/if}

View File

@ -0,0 +1,33 @@
<!--
// Copyright © 2025 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 { Writable } from 'svelte/store'
import RoomSelector from './RoomSelector.svelte'
export let state: Writable<Record<string, any>>
function changeRoom (val: Ref<Room>): void {
$state.room = val
}
</script>
<RoomSelector
value={$state.room}
on:change={(ev) => {
changeRoom(ev.detail)
}}
/>

View File

@ -26,10 +26,13 @@ import MeetingMinutesStatusPresenter from './components/MeetingMinutesStatusPres
import RoomLanguageEditor from './components/RoomLanguageEditor.svelte' import RoomLanguageEditor from './components/RoomLanguageEditor.svelte'
import MediaPopupItemExt from './components/MediaPopupItemExt.svelte' import MediaPopupItemExt from './components/MediaPopupItemExt.svelte'
import SharingStateIndicator from './components/SharingStateIndicator.svelte' import SharingStateIndicator from './components/SharingStateIndicator.svelte'
import MeetingScheduleData from './components/MeetingScheduleData.svelte'
import EditMeetingScheduleData from './components/EditMeetingScheduleData.svelte'
import { import {
copyGuestLink, copyGuestLink,
createMeeting, createMeeting,
createMeetingSchedule,
showRoomSettings, showRoomSettings,
startTranscription, startTranscription,
stopTranscription, stopTranscription,
@ -66,10 +69,13 @@ export default async (): Promise<Resources> => ({
MeetingMinutesStatusPresenter, MeetingMinutesStatusPresenter,
RoomLanguageEditor, RoomLanguageEditor,
MediaPopupItemExt, MediaPopupItemExt,
SharingStateIndicator SharingStateIndicator,
MeetingScheduleData,
EditMeetingScheduleData
}, },
function: { function: {
CreateMeeting: createMeeting, CreateMeeting: createMeeting,
CreateMeetingSchedule: createMeetingSchedule,
CanShowRoomSettings: () => { CanShowRoomSettings: () => {
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) { if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
return return

View File

@ -34,10 +34,13 @@ export default mergeIds(loveId, love, {
FloorView: '' as AnyComponent, FloorView: '' as AnyComponent,
PanelControlBar: '' as AnyComponent, PanelControlBar: '' as AnyComponent,
MeetingMinutesDocEditor: '' as AnyComponent, MeetingMinutesDocEditor: '' as AnyComponent,
MeetingMinutesStatusPresenter: '' as AnyComponent MeetingMinutesStatusPresenter: '' as AnyComponent,
MeetingScheduleData: '' as AnyComponent,
EditMeetingScheduleData: '' as AnyComponent
}, },
function: { function: {
CreateMeeting: '' as Resource<DocCreateFunction>, CreateMeeting: '' as Resource<DocCreateFunction>,
CreateMeetingSchedule: '' as Resource<DocCreateFunction>,
CanShowRoomSettings: '' as Resource<ViewActionAvailabilityFunction>, CanShowRoomSettings: '' as Resource<ViewActionAvailabilityFunction>,
CanCopyGuestLink: '' as Resource<ViewActionAvailabilityFunction> CanCopyGuestLink: '' as Resource<ViewActionAvailabilityFunction>
}, },

View File

@ -1,7 +1,7 @@
import aiBot from '@hcengineering/ai-bot' import aiBot from '@hcengineering/ai-bot'
import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resources' import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resources'
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import calendar, { type Event, getAllEvents } from '@hcengineering/calendar' import calendar, { type Event, type Schedule, getAllEvents } from '@hcengineering/calendar'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import contact, { type Employee, getCurrentEmployee, getName, type Person } from '@hcengineering/contact' import contact, { type Employee, getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources' import { personByIdStore } from '@hcengineering/contact-resources'
@ -32,6 +32,7 @@ import {
loveId, loveId,
type Meeting, type Meeting,
type MeetingMinutes, type MeetingMinutes,
type MeetingSchedule,
MeetingStatus, MeetingStatus,
type Office, type Office,
type ParticipantInfo, type ParticipantInfo,
@ -1112,6 +1113,32 @@ export async function createMeeting (
} }
} }
export async function createMeetingSchedule (
client: TxOperations,
_id: Ref<Schedule>,
space: Space,
data: Data<Schedule>,
store: Record<string, any>,
phase: DocCreatePhase
): Promise<void> {
console.log('createMeetingSchedule-0', _id)
if (phase === 'post') {
console.log('createMeetingSchedule-1', _id)
const schedule = await client.findOne(calendar.class.Schedule, { _id })
console.log('createMeetingSchedule-2', schedule)
if (schedule === undefined) return
await client.createMixin<Schedule, MeetingSchedule>(
schedule._id,
calendar.class.Schedule,
space._id,
love.mixin.MeetingSchedule,
{
room: store.room as Ref<Room>
}
)
}
}
export function getLoveEndpoint (): string { export function getLoveEndpoint (): string {
const endpoint = getMetadata(love.metadata.ServiceEnpdoint) const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
if (endpoint === undefined) { if (endpoint === undefined) {

View File

@ -1,4 +1,4 @@
import { Event } from '@hcengineering/calendar' import { Event, Schedule } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { AttachedDoc, Class, MarkupBlobRef, Doc, Mixin, Ref, Timestamp } from '@hcengineering/core' import { AttachedDoc, Class, MarkupBlobRef, Doc, Mixin, Ref, Timestamp } from '@hcengineering/core'
import { Drive } from '@hcengineering/drive' import { Drive } from '@hcengineering/drive'
@ -134,6 +134,10 @@ export interface Meeting extends Event {
room: Ref<Room> room: Ref<Room>
} }
export interface MeetingSchedule extends Schedule {
room: Ref<Room>
}
export enum RequestStatus { export enum RequestStatus {
Pending, Pending,
Approved, Approved,
@ -192,7 +196,8 @@ const love = plugin(loveId, {
MeetingMinutes: '' as Ref<Class<MeetingMinutes>> MeetingMinutes: '' as Ref<Class<MeetingMinutes>>
}, },
mixin: { mixin: {
Meeting: '' as Ref<Mixin<Meeting>> Meeting: '' as Ref<Mixin<Meeting>>,
MeetingSchedule: '' as Ref<Mixin<MeetingSchedule>>
}, },
action: { action: {
ToggleMic: '' as Ref<Action>, ToggleMic: '' as Ref<Action>,