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 MeetingMinutes,
type MeetingStatus,
type MeetingSchedule,
type Office,
type ParticipantInfo,
type RequestStatus,
@ -61,7 +62,7 @@ import {
UX,
TypeBoolean
} 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 preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
@ -252,6 +253,11 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes, Tod
todos?: CollectionSize<ToDo>
}
@Mixin(love.mixin.MeetingSchedule, calendar.class.Schedule)
export class TMeetingSchedule extends TSchedule implements MeetingSchedule {
room!: Ref<Room>
}
export default love
export function createModel (builder: Builder): void {
@ -265,7 +271,8 @@ export function createModel (builder: Builder): void {
TRoomInfo,
TInvite,
TMeeting,
TMeetingMinutes
TMeetingMinutes,
TMeetingSchedule
)
builder.createDoc(
@ -325,6 +332,19 @@ export function createModel (builder: Builder): void {
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, {
extension: media.extension.StateContext,
component: love.component.MediaPopupItemExt

View File

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

View File

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

View File

@ -26,6 +26,7 @@
export let timeZone: string
export let disabled: boolean = false
export let flex: string | undefined = undefined
function open (e: MouseEvent) {
if (disabled) {
@ -56,7 +57,15 @@
}
</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">
<span class="ml-2 content-darker-color">
{getTimeZoneName(timeZone)}

View File

@ -274,7 +274,8 @@ const calendarPlugin = plugin(calendarId, {
CalDavServerURL: '' as Metadata<string>
},
extensions: {
EditEventExtensions: '' as ComponentExtensionId
EditEventExtensions: '' as ComponentExtensionId,
EditScheduleExtensions: '' as ComponentExtensionId
},
ids: {
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 MediaPopupItemExt from './components/MediaPopupItemExt.svelte'
import SharingStateIndicator from './components/SharingStateIndicator.svelte'
import MeetingScheduleData from './components/MeetingScheduleData.svelte'
import EditMeetingScheduleData from './components/EditMeetingScheduleData.svelte'
import {
copyGuestLink,
createMeeting,
createMeetingSchedule,
showRoomSettings,
startTranscription,
stopTranscription,
@ -66,10 +69,13 @@ export default async (): Promise<Resources> => ({
MeetingMinutesStatusPresenter,
RoomLanguageEditor,
MediaPopupItemExt,
SharingStateIndicator
SharingStateIndicator,
MeetingScheduleData,
EditMeetingScheduleData
},
function: {
CreateMeeting: createMeeting,
CreateMeetingSchedule: createMeetingSchedule,
CanShowRoomSettings: () => {
if (!hasAccountRole(getCurrentAccount(), AccountRole.User)) {
return

View File

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

View File

@ -1,7 +1,7 @@
import aiBot from '@hcengineering/ai-bot'
import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resources'
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 contact, { type Employee, getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
@ -32,6 +32,7 @@ import {
loveId,
type Meeting,
type MeetingMinutes,
type MeetingSchedule,
MeetingStatus,
type Office,
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 {
const endpoint = getMetadata(love.metadata.ServiceEnpdoint)
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 { AttachedDoc, Class, MarkupBlobRef, Doc, Mixin, Ref, Timestamp } from '@hcengineering/core'
import { Drive } from '@hcengineering/drive'
@ -134,6 +134,10 @@ export interface Meeting extends Event {
room: Ref<Room>
}
export interface MeetingSchedule extends Schedule {
room: Ref<Room>
}
export enum RequestStatus {
Pending,
Approved,
@ -192,7 +196,8 @@ const love = plugin(loveId, {
MeetingMinutes: '' as Ref<Class<MeetingMinutes>>
},
mixin: {
Meeting: '' as Ref<Mixin<Meeting>>
Meeting: '' as Ref<Mixin<Meeting>>,
MeetingSchedule: '' as Ref<Mixin<MeetingSchedule>>
},
action: {
ToggleMic: '' as Ref<Action>,