Fix meeting minutes (#7181)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-11-16 20:30:36 +04:00 committed by GitHub
parent 430be8c3ba
commit bcdfed6871
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 744 additions and 254 deletions

View File

@ -201,11 +201,8 @@ export class TDocUpdateMessageViewlet extends TDoc implements DocUpdateMessageVi
@Model(activity.class.ActivityExtension, core.class.Doc, DOMAIN_MODEL) @Model(activity.class.ActivityExtension, core.class.Doc, DOMAIN_MODEL)
export class TActivityExtension extends TDoc implements ActivityExtension { export class TActivityExtension extends TDoc implements ActivityExtension {
@Prop(TypeRef(core.class.Class), core.string.Class) ofClass!: Ref<Class<Doc>>
@Index(IndexKind.Indexed) components!: Record<ActivityExtensionKind, { component: AnyComponent, props?: Record<string, any> }>
ofClass!: Ref<Class<Doc>>
components!: Record<ActivityExtensionKind, AnyComponent>
} }
@Model(activity.class.ActivityMessagesFilter, core.class.Doc, DOMAIN_MODEL) @Model(activity.class.ActivityMessagesFilter, core.class.Doc, DOMAIN_MODEL)

View File

@ -85,7 +85,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: analyticsCollector.class.OnboardingChannel, ofClass: analyticsCollector.class.OnboardingChannel,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc<ActivityMessageControl<OnboardingChannel>>( builder.createDoc<ActivityMessageControl<OnboardingChannel>>(

View File

@ -273,27 +273,27 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: chunter.class.Channel, ofClass: chunter.class.Channel,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: chunter.class.DirectMessage, ofClass: chunter.class.DirectMessage,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: activity.class.DocUpdateMessage, ofClass: activity.class.DocUpdateMessage,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: chunter.class.ChatMessage, ofClass: chunter.class.ChatMessage,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: activity.class.ActivityReference, ofClass: activity.class.ActivityReference,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
// Indexing // Indexing

View File

@ -263,22 +263,22 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Contact, ofClass: contact.class.Contact,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Person, ofClass: contact.class.Person,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Organization, ofClass: contact.class.Organization,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: contact.class.Member, ofClass: contact.class.Member,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectFactory, { builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectFactory, {

View File

@ -514,7 +514,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: documents.class.DocumentCategory, ofClass: documents.class.DocumentCategory,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(documents.class.DocumentCategory, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(documents.class.DocumentCategory, core.class.Class, view.mixin.ObjectPresenter, {
@ -768,7 +768,7 @@ export function defineNotifications (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: documents.class.DocumentComment, ofClass: documents.class.DocumentComment,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(documents.class.ControlledDocument, core.class.Class, notification.mixin.ClassCollaborators, { builder.mixin(documents.class.ControlledDocument, core.class.Class, notification.mixin.ClassCollaborators, {

View File

@ -492,7 +492,7 @@ function defineDocument (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: document.class.Document, ofClass: document.class.Document,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
// Search // Search

View File

@ -595,7 +595,7 @@ function defineFile (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: drive.class.File, ofClass: drive.class.File,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
// Search // Search

View File

@ -85,17 +85,17 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: inventory.class.Product, ofClass: inventory.class.Product,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: inventory.class.Category, ofClass: inventory.class.Category,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: inventory.class.Variant, ofClass: inventory.class.Variant,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(inventory.class.Category, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(inventory.class.Category, core.class.Class, view.mixin.ObjectPresenter, {

View File

@ -51,12 +51,12 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: lead.class.Lead, ofClass: lead.class.Lead,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: lead.class.Funnel, ofClass: lead.class.Funnel,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, { builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, {

View File

@ -28,8 +28,8 @@
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"dependencies": { "dependencies": {
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/activity": "^0.6.0", "@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/chunter": "^0.6.20", "@hcengineering/chunter": "^0.6.20",
"@hcengineering/contact": "^0.6.24", "@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
@ -46,6 +46,7 @@
"@hcengineering/notification": "^0.6.23", "@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/time": "^0.6.0",
"@hcengineering/ui": "^0.6.15", "@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13", "@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16" "@hcengineering/workbench": "^0.6.16"

View File

@ -20,7 +20,10 @@ import {
DOMAIN_TRANSIENT, DOMAIN_TRANSIENT,
IndexKind, IndexKind,
type Ref, type Ref,
type CollaborativeDoc type CollaborativeDoc,
type Doc,
type Timestamp,
type CollectionSize
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
type DevicesPreference, type DevicesPreference,
@ -37,7 +40,8 @@ import {
type RoomInfo, type RoomInfo,
type RoomType, type RoomType,
type RoomLanguage, type RoomLanguage,
type MeetingMinutes type MeetingMinutes,
type MeetingStatus
} from '@hcengineering/love' } from '@hcengineering/love'
import { import {
type Builder, type Builder,
@ -52,7 +56,9 @@ import {
TypeCollaborativeDoc, TypeCollaborativeDoc,
TypeRef, TypeRef,
TypeString, TypeString,
UX TypeTimestamp,
UX,
TypeAny
} from '@hcengineering/model' } from '@hcengineering/model'
import calendar, { TEvent } from '@hcengineering/model-calendar' import calendar, { TEvent } from '@hcengineering/model-calendar'
import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core'
@ -66,6 +72,7 @@ import workbench, { WidgetType } from '@hcengineering/workbench'
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import time, { type ToDo, type Todoable } from '@hcengineering/time'
import love from './plugin' import love from './plugin'
@ -107,6 +114,9 @@ export class TRoom extends TDoc implements Room {
@Prop(PropCollection(love.class.MeetingMinutes), love.string.MeetingMinutes) @Prop(PropCollection(love.class.MeetingMinutes), love.string.MeetingMinutes)
meetings?: number meetings?: number
@Prop(PropCollection(chunter.class.ChatMessage), activity.string.Messages)
messages?: number
} }
@Model(love.class.Office, love.class.Room) @Model(love.class.Office, love.class.Room)
@ -184,8 +194,13 @@ export class TMeeting extends TEvent implements Meeting {
} }
@Model(love.class.MeetingMinutes, core.class.Doc, DOMAIN_MEETING_MINUTES) @Model(love.class.MeetingMinutes, core.class.Doc, DOMAIN_MEETING_MINUTES)
@UX(love.string.MeetingMinutes, love.icon.Cam) @UX(love.string.MeetingMinutes, love.icon.Cam, undefined, 'createdOn', undefined, love.string.MeetingsMinutes)
export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes { export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes, Todoable {
@Prop(TypeRef(core.class.Doc), love.string.Room, { editor: love.component.MeetingMinutesDocEditor })
@Index(IndexKind.Indexed)
@ReadOnly()
declare attachedTo: Ref<Doc>
@Hidden() @Hidden()
sid!: string sid!: string
@ -197,6 +212,12 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: CollaborativeDoc description!: CollaborativeDoc
@Prop(TypeAny(love.component.MeetingMinutesStatusPresenter, love.string.Status), love.string.Status, {
editor: love.component.MeetingMinutesStatusPresenter
})
@ReadOnly()
status!: MeetingStatus
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number attachments?: number
@ -205,6 +226,18 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes {
@Prop(PropCollection(chunter.class.ChatMessage), activity.string.Messages) @Prop(PropCollection(chunter.class.ChatMessage), activity.string.Messages)
messages?: number messages?: number
@Prop(TypeTimestamp(), love.string.MeetingStart, { editor: view.component.TimestampPresenter })
@ReadOnly()
@Index(IndexKind.IndexedDsc)
declare createdOn: Timestamp
@Prop(TypeTimestamp(), love.string.MeetingEnd)
@ReadOnly()
meetingEnd?: Timestamp
@Prop(Collection(time.class.ToDo), getEmbeddedLabel('Action Items'))
todos?: CollectionSize<ToDo>
} }
export default love export default love
@ -418,17 +451,17 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: love.class.Room, ofClass: love.class.Room,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput, props: { collection: 'messages' } } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: love.class.Office, ofClass: love.class.Office,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput, props: { collection: 'messages' } } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: love.class.MeetingMinutes, ofClass: love.class.MeetingMinutes,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput, props: { collection: 'messages' } } }
}) })
builder.mixin(love.class.MeetingMinutes, core.class.Class, activity.mixin.ActivityDoc, {}) builder.mixin(love.class.MeetingMinutes, core.class.Class, activity.mixin.ActivityDoc, {})
@ -439,6 +472,10 @@ export function createModel (builder: Builder): void {
presenter: love.component.MeetingMinutesPresenter presenter: love.component.MeetingMinutesPresenter
}) })
builder.mixin(love.class.Room, core.class.Class, view.mixin.ObjectPresenter, {
presenter: love.component.RoomPresenter
})
builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.CollectionEditor, { builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.CollectionEditor, {
editor: love.component.MeetingMinutesSection editor: love.component.MeetingMinutesSection
}) })
@ -467,10 +504,11 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: [ config: [
'', '',
{ key: 'status', presenter: love.component.MeetingMinutesStatusPresenter, label: love.string.Status },
'createdOn',
'meetingEnd',
{ key: 'messages', displayProps: { key: 'messages', suffix: true } }, { key: 'messages', displayProps: { key: 'messages', suffix: true } },
{ key: 'transcription', displayProps: { key: 'transcription', suffix: true } }, { key: 'transcription', displayProps: { key: 'transcription', suffix: true } }
'modifiedOn',
'modifiedBy'
], ],
configOptions: { configOptions: {
hiddenKeys: ['description'], hiddenKeys: ['description'],
@ -481,6 +519,26 @@ export function createModel (builder: Builder): void {
love.viewlet.TableMeetingMinutes love.viewlet.TableMeetingMinutes
) )
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: love.class.MeetingMinutes,
descriptor: view.viewlet.Table,
config: [
'',
{ key: 'status', presenter: love.component.MeetingMinutesStatusPresenter, label: love.string.Status },
'createdOn',
'meetingEnd'
],
configOptions: {
sortable: true
},
variant: 'embedded'
},
love.viewlet.TableMeetingMinutesEmbedded
)
builder.createDoc( builder.createDoc(
view.class.ViewletDescriptor, view.class.ViewletDescriptor,
core.space.Model, core.space.Model,
@ -574,4 +632,8 @@ export function createModel (builder: Builder): void {
indexes: [], indexes: [],
searchDisabled: true searchDisabled: true
}) })
builder.mixin(love.class.MeetingMinutes, core.class.Class, view.mixin.ObjectPanelFooter, {
editor: love.component.PanelControlBar
})
} }

View File

@ -16,7 +16,16 @@
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { type Space, TxOperations, type Ref, makeCollaborativeDoc } from '@hcengineering/core' import { type Space, TxOperations, type Ref, makeCollaborativeDoc } from '@hcengineering/core'
import drive from '@hcengineering/drive' import drive from '@hcengineering/drive'
import { RoomAccess, RoomType, createDefaultRooms, isOffice, loveId, type Floor, type Room } from '@hcengineering/love' import {
MeetingStatus,
RoomAccess,
RoomType,
createDefaultRooms,
isOffice,
loveId,
type Floor,
type Room
} from '@hcengineering/love'
import { import {
createDefaultSpace, createDefaultSpace,
migrateSpace, migrateSpace,
@ -142,6 +151,16 @@ export const loveOperation: MigrateOperation = {
} }
} }
} }
},
{
state: 'default-meeting-minutes-status',
func: async (client) => {
await client.update(
DOMAIN_MEETING_MINUTES,
{ status: { $exists: false } },
{ status: MeetingStatus.Finished }
)
}
} }
]) ])
}, },

View File

@ -159,7 +159,7 @@ function defineProduct (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: products.class.Product, ofClass: products.class.Product,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(products.class.Product, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(products.class.Product, core.class.Class, view.mixin.ObjectEditor, {
@ -299,7 +299,7 @@ function defineProductVersion (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: products.class.ProductVersion, ofClass: products.class.ProductVersion,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(products.class.ProductVersion, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(products.class.ProductVersion, core.class.Class, view.mixin.ObjectEditor, {

View File

@ -56,17 +56,17 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: recruit.class.Vacancy, ofClass: recruit.class.Vacancy,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: recruit.class.Applicant, ofClass: recruit.class.Applicant,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: recruit.class.Review, ofClass: recruit.class.Review,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, { builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {

View File

@ -99,7 +99,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: survey.class.Survey, ofClass: survey.class.Survey,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc<Viewlet>( builder.createDoc<Viewlet>(
@ -137,7 +137,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: survey.class.Poll, ofClass: survey.class.Poll,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(survey.class.Poll, core.class.Class, view.mixin.CollectionEditor, { builder.mixin(survey.class.Poll, core.class.Class, view.mixin.CollectionEditor, {

View File

@ -143,7 +143,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestProject, ofClass: testManagement.class.TestProject,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
defineTestSuite(builder) defineTestSuite(builder)
@ -218,7 +218,7 @@ function defineTestSuite (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestSuite, ofClass: testManagement.class.TestSuite,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectEditor, {
@ -283,7 +283,7 @@ function defineTestCase (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestCase, ofClass: testManagement.class.TestCase,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectEditor, {
@ -388,7 +388,7 @@ function defineTestRun (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestRun, ofClass: testManagement.class.TestRun,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectEditor, {

View File

@ -461,22 +461,22 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: tracker.class.Issue, ofClass: tracker.class.Issue,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: tracker.class.Milestone, ofClass: tracker.class.Milestone,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: tracker.class.Component, ofClass: tracker.class.Component,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: tracker.class.IssueTemplate, ofClass: tracker.class.IssueTemplate,
components: { input: chunter.component.ChatMessageInput } components: { input: { component: chunter.component.ChatMessageInput } }
}) })
defineViewlets(builder) defineViewlets(builder)

View File

@ -67,6 +67,7 @@ import {
type ListItemPresenter, type ListItemPresenter,
type ObjectEditor, type ObjectEditor,
type ObjectEditorFooter, type ObjectEditorFooter,
type ObjectPanelFooter,
type ObjectEditorHeader, type ObjectEditorHeader,
type ObjectFactory, type ObjectFactory,
type ObjectPanel, type ObjectPanel,
@ -216,6 +217,11 @@ export class TObjectEditorFooter extends TClass implements ObjectEditorFooter {
editor!: AnyComponent editor!: AnyComponent
} }
@Mixin(view.mixin.ObjectPanelFooter, core.class.Class)
export class TObjectPanelFooter extends TClass implements ObjectPanelFooter {
editor!: AnyComponent
}
@Mixin(view.mixin.SpaceHeader, core.class.Class) @Mixin(view.mixin.SpaceHeader, core.class.Class)
export class TSpaceHeader extends TClass implements SpaceHeader { export class TSpaceHeader extends TClass implements SpaceHeader {
header!: AnyComponent header!: AnyComponent
@ -445,6 +451,7 @@ export function createModel (builder: Builder): void {
TObjectTitle, TObjectTitle,
TObjectEditorHeader, TObjectEditorHeader,
TObjectEditorFooter, TObjectEditorFooter,
TObjectPanelFooter,
TSpaceHeader, TSpaceHeader,
TSpaceName, TSpaceName,
TSpacePresenter, TSpacePresenter,

View File

@ -158,6 +158,11 @@
<svelte:fragment slot="pre-utils"> <svelte:fragment slot="pre-utils">
<slot name="pre-utils" /> <slot name="pre-utils" />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="panel-footer">
<slot name="panel-footer" />
</svelte:fragment>
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
{#if isUtils && $$slots.utils} {#if isUtils && $$slots.utils}
<slot name="utils" /> <slot name="utils" />

View File

@ -626,7 +626,7 @@ export async function getAttributeEditor (
function filterKeys (hierarchy: Hierarchy, keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] { function filterKeys (hierarchy: Hierarchy, keys: KeyedAttribute[], ignoreKeys: string[]): KeyedAttribute[] {
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys()) const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
keys = keys.filter((k) => !docKeys.has(k.key)) keys = keys.filter((k) => !docKeys.has(k.key) || k.attr.editor !== undefined)
keys = keys.filter((k) => !ignoreKeys.includes(k.key)) keys = keys.filter((k) => !ignoreKeys.includes(k.key))
return keys return keys
} }

View File

@ -309,6 +309,11 @@
</div> </div>
{/if} {/if}
</div> </div>
{#if $$slots['panel-footer']}
<div class="popupPanel-footer">
<slot name="panel-footer" />
</div>
{/if}
<div class="popupPanel-pageHeader only-print" id="page-header"> <div class="popupPanel-pageHeader only-print" id="page-header">
<slot name="page-header" /> <slot name="page-header" />
</div> </div>

View File

@ -372,7 +372,8 @@ export function fitPopupElement (
newProps.right = '1px' newProps.right = '1px'
show = true show = true
} else if (element === 'full-centered') { } else if (element === 'full-centered') {
newProps.top = '20px' const rect = contentPanel !== undefined ? contentPanel.getBoundingClientRect() : { top: 0 }
newProps.top = `${Math.max(20, rect.top + 1)}px`
newProps.bottom = '20px' newProps.bottom = '20px'
newProps.left = '20px' newProps.left = '20px'
newProps.right = '20px' newProps.right = '20px'

View File

@ -24,5 +24,11 @@
</script> </script>
{#if extension} {#if extension}
<Component is={extension.components[kind]} {props} on:close on:open on:submit /> <Component
is={extension.components[kind].component}
props={{ ...extension.components[kind].props, ...props }}
on:close
on:open
on:submit
/>
{/if} {/if}

View File

@ -30,7 +30,7 @@
$: attrViewletConfig = viewlet?.config?.[attributeModel.key] $: attrViewletConfig = viewlet?.config?.[attributeModel.key]
$: attributeIcon = attrViewletConfig?.icon ?? attributeModel.icon ?? IconEdit $: attributeIcon = attrViewletConfig?.icon ?? attributeModel.icon ?? IconEdit
$: isUnset = values.length > 0 && !values.some((value) => value !== null && value !== '') $: isUnset = values.length > 0 && !values.some((value) => value != null && value !== '')
$: isTextType = getIsTextType(attributeModel) $: isTextType = getIsTextType(attributeModel)
@ -42,7 +42,7 @@
</script> </script>
{#if isUnset} {#if isUnset}
<div class="row overflow-label"> <div class="unset row overflow-label">
<span class="mr-1"><Icon icon={attributeIcon} size="small" /></span> <span class="mr-1"><Icon icon={attributeIcon} size="small" /></span>
<Label label={activity.string.Unset} /> <Label label={activity.string.Unset} />
<span class="lower"><Label label={attributeModel.label} /></span> <span class="lower"><Label label={attributeModel.label} /></span>
@ -89,6 +89,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.25rem;
color: var(--global-primary-TextColor);
} }
.showMore { .showMore {

View File

@ -210,7 +210,7 @@ export type ActivityExtensionKind = 'input'
*/ */
export interface ActivityExtension extends Doc { export interface ActivityExtension extends Doc {
ofClass: Ref<Class<Doc>> ofClass: Ref<Class<Doc>>
components: Record<ActivityExtensionKind, AnyComponent> components: Record<ActivityExtensionKind, { component: AnyComponent, props?: Record<string, any> }>
} }
/** /**

View File

@ -72,9 +72,6 @@
async function updateDataProvider (attachedTo: Ref<Doc>, selectedMessageId?: Ref<ActivityMessage>): Promise<void> { async function updateDataProvider (attachedTo: Ref<Doc>, selectedMessageId?: Ref<ActivityMessage>): Promise<void> {
if (dataProvider === undefined) { if (dataProvider === undefined) {
// For now loading all messages for documents with activity. Need to correct handle aggregation with pagination.
// Perhaps we should load all activity messages once, and keep loading in chunks only for ChatMessages then merge them correctly with activity messages
const loadAll = isDocChannel
const ctx = const ctx =
context ?? context ??
(await client.findOne(notification.class.DocNotifyContext, { (await client.findOne(notification.class.DocNotifyContext, {
@ -90,7 +87,7 @@
attachedTo, attachedTo,
activity.class.ActivityMessage, activity.class.ActivityMessage,
selectedMessageId, selectedMessageId,
loadAll, false,
hasRefs, hasRefs,
collection collection
) )
@ -112,7 +109,7 @@
provider={dataProvider} provider={dataProvider}
{freeze} {freeze}
{autofocus} {autofocus}
loadMoreAllowed={!isDocChannel} loadMoreAllowed
{withInput} {withInput}
{readonly} {readonly}
{onReply} {onReply}

View File

@ -68,9 +68,15 @@
"Transcription": "Transcription", "Transcription": "Transcription",
"StartWithTranscription": "Start with transcription", "StartWithTranscription": "Start with transcription",
"MeetingMinutes": "Meeting minutes", "MeetingMinutes": "Meeting minutes",
"MeetingsMinutes": "Meetings minutes",
"StartMeeting": "Start meeting", "StartMeeting": "Start meeting",
"Video": "Video", "Video": "Video",
"NoMeetingMinutes": "No meeting minutes", "NoMeetingMinutes": "No meeting minutes",
"JoinMeeting": "Join meeting" "JoinMeeting": "Join meeting",
"MeetingStart": "Meeting start",
"MeetingEnd": "Meeting end",
"Status": "Status",
"Active": "Active",
"Finished": "Finished"
} }
} }

View File

@ -68,9 +68,15 @@
"Transcription": "Transcripción", "Transcription": "Transcripción",
"StartWithTranscription": "Iniciar con transcripción", "StartWithTranscription": "Iniciar con transcripción",
"MeetingMinutes": "Minutos de la reunión", "MeetingMinutes": "Minutos de la reunión",
"MeetingsMinutes": "Minutos de las reuniones",
"StartMeeting": "Iniciar reunión", "StartMeeting": "Iniciar reunión",
"Video": "Video", "Video": "Video",
"NoMeetingMinutes": "Sin minutos de reunión", "NoMeetingMinutes": "Sin minutos de reunión",
"JoinMeeting": "Unirse a la reunión" "JoinMeeting": "Unirse a la reunión",
"MeetingStart": "Inicio de la reunión",
"MeetingEnd": "Fin de la reunión",
"Status": "Estado",
"Active": "Activo",
"Finished": "Terminado"
} }
} }

View File

@ -68,9 +68,15 @@
"Transcription": "Transcription", "Transcription": "Transcription",
"StartWithTranscription": "Démarrer avec la transcription", "StartWithTranscription": "Démarrer avec la transcription",
"MeetingMinutes": "Minutes de la réunion", "MeetingMinutes": "Minutes de la réunion",
"MeetingsMinutes": "Minutes des réunions",
"StartMeeting": "Démarrer la réunion", "StartMeeting": "Démarrer la réunion",
"Video": "Vidéo", "Video": "Vidéo",
"NoMeetingMinutes": "Pas de minutes de réunion", "NoMeetingMinutes": "Pas de minutes de réunion",
"JoinMeeting": "Rejoindre la réunion" "JoinMeeting": "Rejoindre la réunion",
"MeetingStart": "Début de la réunion",
"MeetingEnd": "Fin de la réunion",
"Status": "Statut",
"Active": "Actif",
"Finished": "Terminé"
} }
} }

View File

@ -68,9 +68,15 @@
"Transcription": "Trascrizione", "Transcription": "Trascrizione",
"StartWithTranscription": "Inizia con la trascrizione", "StartWithTranscription": "Inizia con la trascrizione",
"MeetingMinutes": "Verbale della riunione", "MeetingMinutes": "Verbale della riunione",
"MeetingsMinutes": "Verbali delle riunioni",
"StartMeeting": "Inizia riunione", "StartMeeting": "Inizia riunione",
"Video": "Video", "Video": "Video",
"NoMeetingMinutes": "Nessun verbale della riunione", "NoMeetingMinutes": "Nessun verbale della riunione",
"JoinMeeting": "Unisciti alla riunione" "JoinMeeting": "Unisciti alla riunione",
"MeetingStart": "Inizio riunione",
"MeetingEnd": "Fine riunione",
"Status": "Stato",
"Active": "Attivo",
"Finished": "Finito"
} }
} }

View File

@ -68,9 +68,15 @@
"Transcription": "Transcrição", "Transcription": "Transcrição",
"StartWithTranscription": "Começar com transcrição", "StartWithTranscription": "Começar com transcrição",
"MeetingMinutes": "Minutos da reunião", "MeetingMinutes": "Minutos da reunião",
"MeetingsMinutes": "Minutos das reuniões",
"StartMeeting": "Iniciar reunião", "StartMeeting": "Iniciar reunião",
"Video": "Vídeo", "Video": "Vídeo",
"NoMeetingMinutes": "Sem minutos de reunião", "NoMeetingMinutes": "Sem minutos de reunião",
"JoinMeeting": "Participar na reunião" "JoinMeeting": "Participar na reunião",
"MeetingStart": "Início da reunião",
"MeetingEnd": "Fim da reunião",
"Status": "Estado",
"Active": "Ativo",
"Finished": "Finalizado"
} }
} }

View File

@ -67,10 +67,16 @@
"Meeting": "Встреча", "Meeting": "Встреча",
"Transcription": "Транскрипция", "Transcription": "Транскрипция",
"StartWithTranscription": "Начинать с транскрипцией", "StartWithTranscription": "Начинать с транскрипцией",
"MeetingMinutes": "Результаты встреч", "MeetingMinutes": "Протокол встречи",
"MeetingsMinutes": "Протоколы встреч",
"StartMeeting": "Начать встречу", "StartMeeting": "Начать встречу",
"Video": "Видео", "Video": "Видео",
"NoMeetingMinutes": "Нет результатов встреч", "NoMeetingMinutes": "Нет результатов встреч",
"JoinMeeting": "Присоединиться к встрече" "JoinMeeting": "Присоединиться к встрече",
"MeetingStart": "Начало встречи",
"MeetingEnd": "Конец встречи",
"Status": "Статус",
"Active": "Активно",
"Finished": "Завершено"
} }
} }

View File

@ -68,9 +68,15 @@
"Transcription": "转录", "Transcription": "转录",
"StartWithTranscription": "开始转录", "StartWithTranscription": "开始转录",
"MeetingMinutes": "会议记录", "MeetingMinutes": "会议记录",
"MeetingsMinutes": "会议记录",
"StartMeeting": "开始会议", "StartMeeting": "开始会议",
"Video": "视频", "Video": "视频",
"NoMeetingMinutes": "无会议记录", "NoMeetingMinutes": "无会议记录",
"JoinMeeting": "加入会议" "JoinMeeting": "加入会议",
"MeetingStart": "会议开始",
"MeetingEnd": "会议结束",
"Status": "状态",
"Active": "活动",
"Finished": "已完成"
} }
} }

View File

@ -1,90 +0,0 @@
<!--
// 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 { personByIdStore } from '@hcengineering/contact-resources'
import { Room as TypeRoom } from '@hcengineering/love'
import { getMetadata } from '@hcengineering/platform'
import { Label, Loading, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import presentation, { getClient } from '@hcengineering/presentation'
import { EditDoc } from '@hcengineering/view-resources'
import { subscribeDoc } from '@hcengineering/notification-resources'
import love from '../plugin'
import { storePromise, currentRoom, infos, invites, myInfo, myRequests, meetingMinutesStore } from '../stores'
import { awaitConnect, isConnected, isCurrentInstanceConnected, isFullScreen, tryConnect } from '../utils'
import ControlBar from './ControlBar.svelte'
export let room: TypeRoom
let loading: boolean = false
let configured: boolean = false
onMount(async () => {
loading = true
const wsURL = getMetadata(love.metadata.WebSocketURL)
if (wsURL === undefined) {
return
}
configured = true
await $storePromise
if (
!$isConnected &&
!$isCurrentInstanceConnected &&
$myInfo?.sessionId === getMetadata(presentation.metadata.SessionId)
) {
const info = $infos.filter((p) => p.room === room._id)
await tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites)
}
await awaitConnect()
loading = false
})
let replacedPanel: HTMLElement
$: $deviceInfo.replacedPanel = replacedPanel
onDestroy(() => ($deviceInfo.replacedPanel = undefined))
$: if ($meetingMinutesStore) {
void subscribeDoc(getClient(), $meetingMinutesStore._class, $meetingMinutesStore._id, 'add', $meetingMinutesStore)
}
</script>
<div class="antiPanel-component filledNav" bind:this={replacedPanel}>
<div class="hulyComponent">
{#if $isConnected && !$isCurrentInstanceConnected}
<div class="flex-center justify-center error h-full w-full clear-mins">
<Label label={love.string.AnotherWindowError} />
</div>
{:else if !configured}
<div class="flex-center justify-center error h-full w-full clear-mins">
<Label label={love.string.ServiceNotConfigured} />
</div>
{:else if loading || !$currentRoom || !$meetingMinutesStore}
<Loading />
{:else}
<EditDoc _id={$meetingMinutesStore._id} _class={$meetingMinutesStore._class} embedded selectedAside={false} />
{/if}
{#if $currentRoom}
<div class="flex-grow flex-shrink" />
<ControlBar room={$currentRoom} fullScreen={$isFullScreen} />
{/if}
</div>
</div>

View File

@ -29,7 +29,8 @@
type CompAndProps, type CompAndProps,
IconMoreV, IconMoreV,
ButtonMenu, ButtonMenu,
DropdownIntlItem DropdownIntlItem,
IconMaximize
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view, { Action } from '@hcengineering/view' import view, { Action } from '@hcengineering/view'
import { getActions } from '@hcengineering/view-resources' import { getActions } from '@hcengineering/view-resources'
@ -60,8 +61,10 @@
import RoomAccessPopup from './RoomAccessPopup.svelte' import RoomAccessPopup from './RoomAccessPopup.svelte'
import RoomLanguageSelector from './RoomLanguageSelector.svelte' import RoomLanguageSelector from './RoomLanguageSelector.svelte'
import ControlBarContainer from './ControlBarContainer.svelte' import ControlBarContainer from './ControlBarContainer.svelte'
import RoomModal from './RoomModal.svelte'
export let room: Room export let room: Room
export let canMaximize: boolean = true
export let fullScreen: boolean = false export let fullScreen: boolean = false
export let onFullScreen: (() => void) | undefined = undefined export let onFullScreen: (() => void) | undefined = undefined
@ -159,6 +162,10 @@
await fn(room) await fn(room)
} }
$: withVideo = $screenSharing || room.type === RoomType.Video $: withVideo = $screenSharing || room.type === RoomType.Video
function maximize (): void {
showPopup(RoomModal, { room }, 'full-centered')
}
</script> </script>
<ControlBarContainer bind:noLabel> <ControlBarContainer bind:noLabel>
@ -253,6 +260,20 @@
}} }}
/> />
{/if} {/if}
{#if ($screenSharing || room.type === RoomType.Video) && $isConnected && canMaximize}
<ModernButton
icon={IconMaximize}
tooltip={{
label: love.string.FullscreenMode,
direction: 'top'
}}
kind={'secondary'}
iconSize="medium"
size={'large'}
on:click={maximize}
/>
{/if}
{#if $isConnected && moreItems.length > 0} {#if $isConnected && moreItems.length > 0}
<ButtonMenu <ButtonMenu
items={moreItems} items={moreItems}

View File

@ -16,6 +16,7 @@
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { EditBox } from '@hcengineering/ui' import { EditBox } from '@hcengineering/ui'
import { MeetingMinutes } from '@hcengineering/love' import { MeetingMinutes } from '@hcengineering/love'
import { createEventDispatcher, onMount } from 'svelte'
import love from '../plugin' import love from '../plugin'
@ -23,10 +24,15 @@
export let readonly: boolean = false export let readonly: boolean = false
const client = getClient() const client = getClient()
const dispatch = createEventDispatcher()
async function changeTitle (): Promise<void> { async function changeTitle (): Promise<void> {
await client.diffUpdate(object, { title: object.title }) await client.update(object, { title: object.title })
} }
onMount(() => {
dispatch('open', { ignoreKeys: ['title'] })
})
</script> </script>
<div class="flex-row-stretch"> <div class="flex-row-stretch">

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { EditBox, ModernButton, closePanel } from '@hcengineering/ui' import { EditBox, ModernButton } from '@hcengineering/ui'
import { Room, isOffice } from '@hcengineering/love' import { Room, isOffice } from '@hcengineering/love'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import { personByIdStore } from '@hcengineering/contact-resources' import { personByIdStore } from '@hcengineering/contact-resources'
@ -22,7 +22,7 @@
import love from '../plugin' import love from '../plugin'
import { getRoomName, tryConnect } from '../utils' import { getRoomName, tryConnect } from '../utils'
import { infos, invites, myInfo, myRequests, selectedRoomPlace } from '../stores' import { infos, invites, myInfo, myRequests, selectedRoomPlace, myOffice, currentRoom } from '../stores'
export let object: Room export let object: Room
export let readonly: boolean = false export let readonly: boolean = false
@ -59,8 +59,6 @@
) )
connecting = false connecting = false
selectedRoomPlace.set(undefined) selectedRoomPlace.set(undefined)
closePanel()
dispatch('close')
} }
let connectLabel: IntlString = love.string.StartMeeting let connectLabel: IntlString = love.string.StartMeeting
@ -83,7 +81,9 @@
focusIndex={1} focusIndex={1}
/> />
</div> </div>
<ModernButton label={connectLabel} size="large" kind={'primary'} on:click={connect} loading={connecting} /> {#if object._id !== $myOffice?._id && ($currentRoom?._id !== object._id || connecting)}
<ModernButton label={connectLabel} size="large" kind={'primary'} on:click={connect} loading={connecting} />
{/if}
</div> </div>
</div> </div>

View File

@ -14,12 +14,15 @@
--> -->
<script lang="ts"> <script lang="ts">
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui' import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import { onDestroy } from 'svelte' import { onDestroy, onMount } from 'svelte'
import presentation from '@hcengineering/presentation'
import { personByIdStore } from '@hcengineering/contact-resources'
import Hall from './Hall.svelte' import Hall from './Hall.svelte'
import { currentRoom } from '../stores' import { getMetadata } from '@hcengineering/platform'
import { isConnected } from '../utils' import love from '../plugin'
import ActiveMeeting from './ActiveMeeting.svelte' import { tryConnect, isConnected, isCurrentInstanceConnected } from '../utils'
import { infos, invites, myInfo, myRequests, storePromise, currentRoom } from '../stores'
const localNav: boolean = $deviceInfo.navigator.visible const localNav: boolean = $deviceInfo.navigator.visible
const savedNav = localStorage.getItem('love-visibleNav') const savedNav = localStorage.getItem('love-visibleNav')
@ -29,12 +32,30 @@
onDestroy(() => { onDestroy(() => {
$deviceInfo.navigator.visible = localNav $deviceInfo.navigator.visible = localNav
}) })
onMount(async () => {
const wsURL = getMetadata(love.metadata.WebSocketURL)
if (wsURL === undefined) {
return
}
await $storePromise
const room = $currentRoom
if (room === undefined) return
if (
!$isConnected &&
!$isCurrentInstanceConnected &&
$myInfo?.sessionId === getMetadata(presentation.metadata.SessionId)
) {
const info = $infos.filter((p) => p.room === room._id)
await tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites)
}
})
</script> </script>
<div class="hulyPanels-container"> <div class="hulyPanels-container">
{#if $currentRoom && $isConnected} <Hall />
<ActiveMeeting room={$currentRoom} />
{:else}
<Hall />
{/if}
</div> </div>

View File

@ -0,0 +1,54 @@
<!--
// 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 { MeetingMinutes } from '@hcengineering/love'
import { ObjectPresenter, openDoc } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
import { ActionIcon } from '@hcengineering/ui'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { Doc } from '@hcengineering/core'
export let object: MeetingMinutes
const client = getClient()
const docQuery = createQuery()
let doc: Doc | undefined
$: docQuery.query(object.attachedToClass, { _id: object.attachedTo }, (r) => {
doc = r.shift()
})
</script>
{#if doc}
<span class="label flex-row-center flex-gap-4 ml-3 no-word-wrap">
<ObjectPresenter
objectId={doc._id}
_class={doc._class}
value={doc}
shouldShowAvatar={false}
disabled
props={{ type: 'text' }}
/>
<ActionIcon
icon={view.icon.Open}
size={'small'}
action={async () => {
if (doc === undefined) return
await openDoc(client.getHierarchy(), doc)
}}
/>
</span>
{/if}

View File

@ -15,24 +15,43 @@
<script lang="ts"> <script lang="ts">
import type { Class, Doc, Ref, Space } from '@hcengineering/core' import type { Class, Doc, Ref, Space } from '@hcengineering/core'
import { Label, Section } from '@hcengineering/ui' import { Label, Section } from '@hcengineering/ui'
import { Table } from '@hcengineering/view-resources' import { Table, ViewletsSettingButton } from '@hcengineering/view-resources'
import love from '@hcengineering/love' import { Viewlet, ViewletPreference } from '@hcengineering/view'
import love from '../plugin'
export let objectId: Ref<Doc> export let objectId: Ref<Doc>
export let space: Ref<Space> export let space: Ref<Space>
export let _class: Ref<Class<Doc>> export let _class: Ref<Class<Doc>>
export let readonly: boolean = false export let readonly: boolean = false
export let meetings: number export let meetings: number
let viewlet: Viewlet | undefined
let preference: ViewletPreference | undefined
let loading = true
</script> </script>
<Section label={love.string.MeetingMinutes} icon={love.icon.Cam}> <Section label={love.string.MeetingMinutes} icon={love.icon.Cam}>
<svelte:fragment slot="header">
<div class="flex-row-center gap-2 reverse">
<ViewletsSettingButton
viewletQuery={{ _id: love.viewlet.TableMeetingMinutesEmbedded }}
kind={'tertiary'}
bind:viewlet
bind:loading
bind:preference
/>
</div>
</svelte:fragment>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
{#if meetings > 0} {#if meetings > 0 && viewlet}
<Table <Table
_class={love.class.MeetingMinutes} _class={love.class.MeetingMinutes}
config={['', 'transcription', 'messages']} config={preference?.config ?? viewlet.config}
query={{ attachedTo: objectId }} query={{ attachedTo: objectId }}
loadingProps={{ length: meetings }} loadingProps={{ length: meetings }}
prefferedSorting="createdOn"
{readonly} {readonly}
/> />
{:else} {:else}

View File

@ -0,0 +1,46 @@
<!--
// 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 { MeetingMinutes, MeetingStatus } from '@hcengineering/love'
import { StateType, StateTag } from '@hcengineering/ui'
import love from '../plugin'
export let object: MeetingMinutes | undefined
export let value: MeetingStatus | undefined
export let attributeKey: string | undefined
const displayData = {
[MeetingStatus.Active]: {
label: love.string.Active,
type: StateType.Positive
},
[MeetingStatus.Finished]: {
label: love.string.Finished,
type: StateType.Regular
}
}
let status: MeetingStatus | undefined
$: status = value ?? object?.status
$: data = status !== undefined ? displayData[status] : undefined
</script>
{#if data}
<span class="flex-row-center" class:ml-3={attributeKey !== undefined}>
<StateTag type={data.type} label={data.label} />
</span>
{/if}

View File

@ -51,7 +51,8 @@
viewlet, viewlet,
viewOptions, viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other, viewOptionsConfig: viewlet.viewOptions?.other,
enableChecking: false enableChecking: false,
prefferedSorting: 'createdOn'
}} }}
/> />
{/if} {/if}

View File

@ -0,0 +1,29 @@
<!--
// 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 { MeetingMinutes } from '@hcengineering/love'
import ControlBar from './ControlBar.svelte'
import { currentRoom, currentMeetingMinutes } from '../stores'
export let object: MeetingMinutes
</script>
{#if $currentRoom && $currentMeetingMinutes?._id === object._id}
<div class="flex-grow flex-shrink">
<ControlBar room={$currentRoom} />
</div>
{/if}

View File

@ -50,6 +50,7 @@
import ParticipantView from './ParticipantView.svelte' import ParticipantView from './ParticipantView.svelte'
export let withVideo: boolean export let withVideo: boolean
export let canMaximize: boolean = true
export let room: TypeRoom export let room: TypeRoom
interface ParticipantData { interface ParticipantData {
@ -432,7 +433,7 @@
{/if} {/if}
</div> </div>
{#if $currentRoom} {#if $currentRoom}
<ControlBar room={$currentRoom} fullScreen={$isFullScreen} {onFullScreen} /> <ControlBar room={$currentRoom} fullScreen={$isFullScreen} {onFullScreen} {canMaximize} />
{/if} {/if}
</div> </div>

View File

@ -38,6 +38,6 @@
padding="0" padding="0"
on:close={() => dispatch('close')} on:close={() => dispatch('close')}
> >
<RoomComponent withVideo={$currentRoom.type === RoomType.Video} room={$currentRoom} /> <RoomComponent withVideo={$currentRoom.type === RoomType.Video} room={$currentRoom} canMaximize={false} />
</Modal> </Modal>
{/if} {/if}

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Person, PersonAccount } from '@hcengineering/contact' import { Person, PersonAccount } from '@hcengineering/contact'
import { personByIdStore, UserInfo } from '@hcengineering/contact-resources' import { personByIdStore, UserInfo } from '@hcengineering/contact-resources'
import { IdMap, getCurrentAccount } from '@hcengineering/core' import { IdMap, getCurrentAccount, Ref, Class, Doc } from '@hcengineering/core'
import ui, { import ui, {
ModernButton, ModernButton,
SplitButton, SplitButton,
@ -23,12 +23,12 @@
IconUpOutline, IconUpOutline,
Label, Label,
eventToHTMLElement, eventToHTMLElement,
getCurrentLocation, Location,
location, location,
navigate, navigate,
showPopup, showPopup,
Scroller, Scroller,
closePanel panelstore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { import {
ParticipantInfo, ParticipantInfo,
@ -37,11 +37,14 @@
isOffice, isOffice,
loveId, loveId,
roomAccessIcon, roomAccessIcon,
roomAccessLabel roomAccessLabel,
MeetingMinutes
} from '@hcengineering/love' } from '@hcengineering/love'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import love from '../plugin' import love from '../plugin'
import { currentRoom, infos, invites, myInfo, myOffice, myRequests } from '../stores' import { currentRoom, infos, invites, myInfo, myOffice, myRequests, currentMeetingMinutes } from '../stores'
import { import {
getRoomName, getRoomName,
isCameraEnabled, isCameraEnabled,
@ -58,9 +61,11 @@
import CamSettingPopup from './CamSettingPopup.svelte' import CamSettingPopup from './CamSettingPopup.svelte'
import MicSettingPopup from './MicSettingPopup.svelte' import MicSettingPopup from './MicSettingPopup.svelte'
import RoomAccessPopup from './RoomAccessPopup.svelte' import RoomAccessPopup from './RoomAccessPopup.svelte'
import view from '@hcengineering/view'
export let room: Room export let room: Room
const client = getClient()
function getPerson (info: ParticipantInfo | undefined, employees: IdMap<Person>): Person | undefined { function getPerson (info: ParticipantInfo | undefined, employees: IdMap<Person>): Person | undefined {
if (info !== undefined) { if (info !== undefined) {
return employees.get(info.person) return employees.get(info.person)
@ -104,12 +109,20 @@
dispatch('close') dispatch('close')
} }
function back (): void { async function back (): Promise<void> {
closePanel() const meetingMinutes = $currentMeetingMinutes
const loc = getCurrentLocation() if (meetingMinutes !== undefined) {
loc.path[2] = loveId const hierarchy = client.getHierarchy()
loc.path.length = 3 const panelComponent = hierarchy.classHierarchyMixin(
navigate(loc) meetingMinutes._class as Ref<Class<Doc>>,
view.mixin.ObjectPanel
)
const comp = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, meetingMinutes, {}, comp)
loc.path[2] = loveId
loc.path.length = 3
navigate(loc)
}
} }
function micSettings (e: MouseEvent): void { function micSettings (e: MouseEvent): void {
@ -129,6 +142,16 @@
} }
const me = (getCurrentAccount() as PersonAccount).person const me = (getCurrentAccount() as PersonAccount).person
function canGoBack (joined: boolean, location: Location, meetingMinutes?: MeetingMinutes): boolean {
if (!joined) return false
if (location.path[2] !== loveId) return true
if (meetingMinutes === undefined) return false
const panel = $panelstore.panel
const { _id } = panel ?? {}
return _id !== meetingMinutes._id
}
</script> </script>
<div class="antiPopup room-popup"> <div class="antiPopup room-popup">
@ -215,7 +238,7 @@
on:click={connect} on:click={connect}
/> />
{/if} {/if}
{#if $location.path[2] !== loveId} {#if canGoBack(joined, $location, $currentMeetingMinutes)}
<ModernButton icon={IconArrowLeft} label={ui.string.Back} kind={'secondary'} size={'large'} on:click={back} /> <ModernButton icon={IconArrowLeft} label={ui.string.Back} kind={'secondary'} size={'large'} on:click={back} />
{/if} {/if}
</div> </div>

View File

@ -0,0 +1,53 @@
<!--
// 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 love, { Room } from '@hcengineering/love'
import { WithLookup } from '@hcengineering/core'
import { ObjectPresenterType } from '@hcengineering/view'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import { tooltip, Icon } from '@hcengineering/ui'
export let value: WithLookup<Room>
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
export let shouldShowAvatar = true
export let type: ObjectPresenterType = 'link'
</script>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else if type === 'link'}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
{#if shouldShowAvatar}
<div class="icon">
<Icon icon={love.icon.Love} size={'small'} />
</div>
{/if}
<div class="label nowrap flex flex-gap-2" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
<span>{value.name}</span>
</div>
</div>
</DocNavLink>
{:else if type === 'text'}
<span class="overflow-label" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
{value.name}
</span>
{/if}
{/if}

View File

@ -24,7 +24,7 @@
import { get } from 'svelte/store' import { get } from 'svelte/store'
import love from '../plugin' import love from '../plugin'
import { myInfo, selectedRoomPlace, currentRoom, meetingMinutesStore } from '../stores' import { myInfo, selectedRoomPlace, currentRoom, currentMeetingMinutes } from '../stores'
import { getRoomLabel, lk } from '../utils' import { getRoomLabel, lk } from '../utils'
import PersonActionPopup from './PersonActionPopup.svelte' import PersonActionPopup from './PersonActionPopup.svelte'
import RoomLanguage from './RoomLanguage.svelte' import RoomLanguage from './RoomLanguage.svelte'
@ -64,23 +64,32 @@
hovered = false hovered = false
} }
async function clickHandler (e: MouseEvent, x: number, y: number, person: Person | undefined): Promise<void> { async function openRoom (x: number, y: number): Promise<void> {
const client = getClient()
const hierarchy = client.getHierarchy()
if ($currentRoom?._id === room._id) {
const sid = await lk.getSid()
const meetingMinutes =
get(currentMeetingMinutes) ?? (await client.findOne(love.class.MeetingMinutes, { sid, attachedTo: room._id }))
if (meetingMinutes === undefined) {
await openDoc(hierarchy, room)
} else {
await openDoc(hierarchy, meetingMinutes)
}
} else {
selectedRoomPlace.set({ _id: room._id, x, y })
await openDoc(hierarchy, room)
}
}
async function placeClickHandler (e: MouseEvent, x: number, y: number, person: Person | undefined): Promise<void> {
e.stopPropagation()
e.preventDefault()
if (person !== undefined) { if (person !== undefined) {
if (room._id === $myInfo?.room || $myInfo === undefined) return if (room._id === $myInfo?.room || $myInfo === undefined) return
showPopup(PersonActionPopup, { room, person: person._id }, eventToHTMLElement(e)) showPopup(PersonActionPopup, { room, person: person._id }, eventToHTMLElement(e))
} else { } else {
const client = getClient() await openRoom(x, y)
const hierarchy = client.getHierarchy()
if ($currentRoom?._id === room._id) {
const sid = await lk.getSid()
const meetingMinutes =
get(meetingMinutesStore) ?? (await client.findOne(love.class.MeetingMinutes, { sid, attachedTo: room._id }))
if (meetingMinutes === undefined) return
await openDoc(hierarchy, meetingMinutes)
} else {
selectedRoomPlace.set({ _id: room._id, x, y })
await openDoc(hierarchy, room)
}
} }
} }
@ -121,6 +130,10 @@
} }
return init return init
} }
async function handleClick (): Promise<void> {
await openRoom(0, 0)
}
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -141,6 +154,7 @@
on:mouseover|stopPropagation on:mouseover|stopPropagation
on:mouseenter|stopPropagation={mouseEnter} on:mouseenter|stopPropagation={mouseEnter}
on:mouseleave|stopPropagation={mouseLeave} on:mouseleave|stopPropagation={mouseLeave}
on:click|stopPropagation={handleClick}
> >
{#each new Array(room.height) as _, y} {#each new Array(room.height) as _, y}
{#each new Array(room.width + extraRow) as _, x} {#each new Array(room.width + extraRow) as _, x}
@ -162,7 +176,7 @@
hoveredRoomY = undefined hoveredRoomY = undefined
}} }}
on:click={(e) => { on:click={(e) => {
clickHandler(e, x, y, person) placeClickHandler(e, x, y, person)
}} }}
> >
{#if personInfo} {#if personInfo}

View File

@ -21,7 +21,7 @@
import love from '../../plugin' import love from '../../plugin'
import VideoTab from './VideoTab.svelte' import VideoTab from './VideoTab.svelte'
import { isCurrentInstanceConnected, lk } from '../../utils' import { isCurrentInstanceConnected, lk } from '../../utils'
import { currentRoom, meetingMinutesStore } from '../../stores' import { currentRoom, currentMeetingMinutes } from '../../stores'
import ChatTab from './ChatTab.svelte' import ChatTab from './ChatTab.svelte'
import TranscriptionTab from './TranscriptionTab.svelte' import TranscriptionTab from './TranscriptionTab.svelte'
@ -60,7 +60,9 @@
$: if (sid != null && room !== undefined) { $: if (sid != null && room !== undefined) {
meetingQuery.query(love.class.MeetingMinutes, { sid, attachedTo: room._id }, async (res) => { meetingQuery.query(love.class.MeetingMinutes, { sid, attachedTo: room._id }, async (res) => {
meetingMinutes = res[0] meetingMinutes = res[0]
meetingMinutesStore.set(meetingMinutes) if (meetingMinutes) {
currentMeetingMinutes.set(meetingMinutes)
}
isMeetingMinutesLoaded = true isMeetingMinutesLoaded = true
}) })
} else { } else {

View File

@ -18,6 +18,10 @@ import EditRoom from './components/EditRoom.svelte'
import FloorAttributePresenter from './components/FloorAttributePresenter.svelte' import FloorAttributePresenter from './components/FloorAttributePresenter.svelte'
import FloorView from './components/FloorView.svelte' import FloorView from './components/FloorView.svelte'
import MeetingMinutesTable from './components/MeetingMinutesTable.svelte' import MeetingMinutesTable from './components/MeetingMinutesTable.svelte'
import PanelControlBar from './components/PanelControlBar.svelte'
import RoomPresenter from './components/RoomPresenter.svelte'
import MeetingMinutesDocEditor from './components/MeetingMinutesDocEditor.svelte'
import MeetingMinutesStatusPresenter from './components/MeetingMinutesStatusPresenter.svelte'
import { import {
copyGuestLink, copyGuestLink,
@ -49,7 +53,11 @@ export default async (): Promise<Resources> => ({
EditRoom, EditRoom,
FloorAttributePresenter, FloorAttributePresenter,
FloorView, FloorView,
MeetingMinutesTable MeetingMinutesTable,
PanelControlBar,
RoomPresenter,
MeetingMinutesDocEditor,
MeetingMinutesStatusPresenter
}, },
function: { function: {
CreateMeeting: createMeeting, CreateMeeting: createMeeting,

View File

@ -25,12 +25,16 @@ export default mergeIds(loveId, love, {
MeetingData: '' as AnyComponent, MeetingData: '' as AnyComponent,
EditMeetingData: '' as AnyComponent, EditMeetingData: '' as AnyComponent,
MeetingMinutesPresenter: '' as AnyComponent, MeetingMinutesPresenter: '' as AnyComponent,
RoomPresenter: '' as AnyComponent,
MeetingMinutesSection: '' as AnyComponent, MeetingMinutesSection: '' as AnyComponent,
EditMeetingMinutes: '' as AnyComponent, EditMeetingMinutes: '' as AnyComponent,
EditRoom: '' as AnyComponent, EditRoom: '' as AnyComponent,
FloorAttributePresenter: '' as AnyComponent, FloorAttributePresenter: '' as AnyComponent,
MeetingMinutesTable: '' as AnyComponent, MeetingMinutesTable: '' as AnyComponent,
FloorView: '' as AnyComponent FloorView: '' as AnyComponent,
PanelControlBar: '' as AnyComponent,
MeetingMinutesDocEditor: '' as AnyComponent,
MeetingMinutesStatusPresenter: '' as AnyComponent
}, },
function: { function: {
CreateMeeting: '' as Resource<DocCreateFunction>, CreateMeeting: '' as Resource<DocCreateFunction>,

View File

@ -60,7 +60,7 @@ export const activeInvites = derived(invites, (val) => {
export const myPreferences = writable<DevicesPreference | undefined>() export const myPreferences = writable<DevicesPreference | undefined>()
export let $myPreferences: DevicesPreference | undefined export let $myPreferences: DevicesPreference | undefined
export const meetingMinutesStore = writable<MeetingMinutes | undefined>(undefined) export const currentMeetingMinutes = writable<MeetingMinutes | undefined>(undefined)
export const selectedRoomPlace = writable<{ _id: Ref<Room>, x: number, y: number } | undefined>(undefined) export const selectedRoomPlace = writable<{ _id: Ref<Room>, x: number, y: number } | undefined>(undefined)
function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] { function filterParticipantInfo (value: ParticipantInfo[]): ParticipantInfo[] {

View File

@ -30,7 +30,8 @@ import {
RoomType, RoomType,
TranscriptionStatus, TranscriptionStatus,
type RoomMetadata, type RoomMetadata,
type MeetingMinutes type MeetingMinutes,
MeetingStatus
} from '@hcengineering/love' } from '@hcengineering/love'
import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform' import { getEmbeddedLabel, getMetadata, getResource, type IntlString } from '@hcengineering/platform'
import presentation, { import presentation, {
@ -39,7 +40,14 @@ import presentation, {
type DocCreatePhase, type DocCreatePhase,
getClient getClient
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { type DropdownTextItem, getCurrentLocation, navigate, showPopup } from '@hcengineering/ui' import {
type DropdownTextItem,
getCurrentLocation,
navigate,
showPopup,
panelstore,
closePanel
} from '@hcengineering/ui'
import { isKrispNoiseFilterSupported, KrispNoiseFilter } from '@livekit/krisp-noise-filter' import { isKrispNoiseFilterSupported, KrispNoiseFilter } from '@livekit/krisp-noise-filter'
import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors' import { BackgroundBlur, type BackgroundOptions, type ProcessorWrapper } from '@livekit/track-processors'
import { import {
@ -70,10 +78,11 @@ import {
import { type Widget, type WidgetTab } from '@hcengineering/workbench' import { type Widget, type WidgetTab } from '@hcengineering/workbench'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import chunter from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import { openDoc } from '@hcengineering/view-resources'
import { sendMessage } from './broadcast' import { sendMessage } from './broadcast'
import love from './plugin' import love from './plugin'
import { $myPreferences, meetingMinutesStore, currentRoom } from './stores' import { $myPreferences, currentRoom, currentMeetingMinutes, selectedRoomPlace } from './stores'
import RoomSettingsPopup from './components/RoomSettingsPopup.svelte' import RoomSettingsPopup from './components/RoomSettingsPopup.svelte'
export const selectedCamId = 'selectedDevice_cam' export const selectedCamId = 'selectedDevice_cam'
@ -444,7 +453,6 @@ export async function disconnect (): Promise<void> {
isMicEnabled.set(false) isMicEnabled.set(false)
isCameraEnabled.set(false) isCameraEnabled.set(false)
isSharingEnabled.set(false) isSharingEnabled.set(false)
meetingMinutesStore.set(undefined)
sendMessage({ type: 'mic', value: false }) sendMessage({ type: 'mic', value: false })
sendMessage({ type: 'cam', value: false }) sendMessage({ type: 'cam', value: false })
sendMessage({ type: 'share', value: false }) sendMessage({ type: 'share', value: false })
@ -463,6 +471,22 @@ export async function leaveRoom (ownInfo: ParticipantInfo | undefined, ownOffice
} }
} }
await disconnect() await disconnect()
closeMeetingMinutes()
}
function closeMeetingMinutes (): void {
const loc = getCurrentLocation()
if (loc.path[2] === loveId) {
const meetingMinutes = get(currentMeetingMinutes)
const panel = get(panelstore).panel
const { _id } = panel ?? {}
if (_id !== undefined && meetingMinutes !== undefined && _id === meetingMinutes._id) {
closePanel()
}
}
currentMeetingMinutes.set(undefined)
} }
export async function setCam (value: boolean): Promise<void> { export async function setCam (value: boolean): Promise<void> {
@ -593,7 +617,7 @@ async function connectLK (currentPerson: Person, room: Room): Promise<void> {
]) ])
} }
async function createMeetingMinutes (room: Room): Promise<void> { async function openMeetingMinutes (room: Room): Promise<void> {
const client = getClient() const client = getClient()
const sid = await lk.getSid() const sid = await lk.getSid()
@ -601,7 +625,17 @@ async function createMeetingMinutes (room: Room): Promise<void> {
const doc = await client.findOne(love.class.MeetingMinutes, { sid }) const doc = await client.findOne(love.class.MeetingMinutes, { sid })
if (doc === undefined) { if (doc === undefined) {
const dateStr = new Date().toISOString().replace('T', ' ').slice(0, 19) const date = new Date()
.toLocaleDateString('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'UTC'
})
.replace(',', ' at')
const _id = generateId<MeetingMinutes>() const _id = generateId<MeetingMinutes>()
const newDoc: MeetingMinutes = { const newDoc: MeetingMinutes = {
_id, _id,
@ -611,24 +645,35 @@ async function createMeetingMinutes (room: Room): Promise<void> {
attachedToClass: room._class, attachedToClass: room._class,
collection: 'meetings', collection: 'meetings',
space: core.space.Workspace, space: core.space.Workspace,
title: room.name + ' ' + dateStr, title: `${room.name} ${date}`,
description: makeCollaborativeDoc(_id, 'description'), description: makeCollaborativeDoc(_id, 'description'),
status: MeetingStatus.Active,
modifiedBy: getCurrentAccount()._id, modifiedBy: getCurrentAccount()._id,
modifiedOn: Date.now() modifiedOn: Date.now()
} }
await client.addCollection( await client.addCollection(
love.class.MeetingMinutes, love.class.MeetingMinutes,
core.space.Workspace, core.space.Workspace,
room._id, room._id,
room._class, room._class,
'meetings', 'meetings',
{ sid, title: newDoc.title, description: newDoc.description }, { sid, title: newDoc.title, description: newDoc.description, status: newDoc.status },
_id _id
) )
meetingMinutesStore.set(newDoc) currentMeetingMinutes.set(newDoc)
const loc = getCurrentLocation()
if (loc.path[2] === loveId) {
await openDoc(client.getHierarchy(), newDoc)
}
} else { } else {
meetingMinutesStore.set(doc) currentMeetingMinutes.set(doc)
const loc = getCurrentLocation()
if (loc.path[2] === loveId) {
await openDoc(client.getHierarchy(), doc)
}
if (doc.status !== MeetingStatus.Active) {
void client.update(doc, { status: MeetingStatus.Active, meetingEnd: undefined })
}
} }
} }
} }
@ -643,7 +688,8 @@ export async function connectRoom (
await disconnect() await disconnect()
await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null) await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null)
await connectLK(currentPerson, room) await connectLK(currentPerson, room)
await createMeetingMinutes(room) selectedRoomPlace.set(undefined)
await openMeetingMinutes(room)
} }
export const joinRequest: Ref<JoinRequest> | undefined = undefined export const joinRequest: Ref<JoinRequest> | undefined = undefined

View File

@ -1,6 +1,6 @@
import { Event } from '@hcengineering/calendar' import { Event } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { AttachedDoc, Class, CollaborativeDoc, Doc, Mixin, Ref } from '@hcengineering/core' import { AttachedDoc, Class, CollaborativeDoc, Doc, Mixin, Ref, Timestamp } from '@hcengineering/core'
import { Drive } from '@hcengineering/drive' import { Drive } from '@hcengineering/drive'
import { NotificationType } from '@hcengineering/notification' import { NotificationType } from '@hcengineering/notification'
import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform' import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/platform'
@ -105,6 +105,7 @@ export interface Room extends Doc {
description: CollaborativeDoc description: CollaborativeDoc
attachments?: number attachments?: number
meetings?: number meetings?: number
messages?: number
} }
export interface Office extends Room { export interface Office extends Room {
@ -158,12 +159,22 @@ export interface DevicesPreference extends Preference {
camEnabled: boolean camEnabled: boolean
} }
export enum MeetingStatus {
Active,
Finished
}
export interface MeetingMinutes extends AttachedDoc { export interface MeetingMinutes extends AttachedDoc {
sid: string sid: string
title: string title: string
description: CollaborativeDoc
status: MeetingStatus
meetingEnd?: Timestamp
transcription?: number transcription?: number
messages?: number messages?: number
description: CollaborativeDoc
attachments?: number attachments?: number
} }
@ -205,10 +216,16 @@ const love = plugin(loveId, {
Transcription: '' as IntlString, Transcription: '' as IntlString,
StartWithTranscription: '' as IntlString, StartWithTranscription: '' as IntlString,
MeetingMinutes: '' as IntlString, MeetingMinutes: '' as IntlString,
MeetingsMinutes: '' as IntlString,
StartMeeting: '' as IntlString, StartMeeting: '' as IntlString,
Video: '' as IntlString, Video: '' as IntlString,
NoMeetingMinutes: '' as IntlString, NoMeetingMinutes: '' as IntlString,
JoinMeeting: '' as IntlString JoinMeeting: '' as IntlString,
MeetingStart: '' as IntlString,
MeetingEnd: '' as IntlString,
Status: '' as IntlString,
Active: '' as IntlString,
Finished: '' as IntlString
}, },
ids: { ids: {
MainFloor: '' as Ref<Floor>, MainFloor: '' as Ref<Floor>,
@ -254,6 +271,7 @@ const love = plugin(loveId, {
}, },
viewlet: { viewlet: {
TableMeetingMinutes: '' as Ref<Viewlet>, TableMeetingMinutes: '' as Ref<Viewlet>,
TableMeetingMinutesEmbedded: '' as Ref<Viewlet>,
MeetingMinutesDescriptor: '' as Ref<ViewletDescriptor>, MeetingMinutesDescriptor: '' as Ref<ViewletDescriptor>,
FloorDescriptor: '' as Ref<ViewletDescriptor>, FloorDescriptor: '' as Ref<ViewletDescriptor>,
Floor: '' as Ref<Viewlet>, Floor: '' as Ref<Viewlet>,

View File

@ -20,7 +20,7 @@
import { Component, showPanel } from '@hcengineering/ui' import { Component, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import time from '../plugin' import time from '../plugin'
import { getObjectLinkId } from '@hcengineering/view-resources' import { DocReferencePresenter, getObjectLinkId } from '@hcengineering/view-resources'
export let todo: ToDo export let todo: ToDo
export let kind: 'default' | 'todo-line' = 'default' export let kind: 'default' | 'todo-line' = 'default'
@ -50,16 +50,22 @@
} }
</script> </script>
{#if presenter?.presenter && doc} {#if doc}
{#if kind === 'default'} {#if presenter?.presenter}
<!-- svelte-ignore a11y-no-static-element-interactions --> {#if kind === 'default'}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cursor-pointer clear-mins" on:click={click}> <!-- svelte-ignore a11y-click-events-have-key-events -->
<Component is={presenter.presenter} props={{ value: doc, withoutSpace }} /> <div class="cursor-pointer clear-mins" on:click={click}>
</div> <Component is={presenter.presenter} props={{ value: doc, withoutSpace }} />
</div>
{:else}
<Component is={presenter.presenter} props={{ value: doc, withoutSpace }} on:click={click}>
<slot />
</Component>
{/if}
{:else} {:else}
<Component is={presenter.presenter} props={{ value: doc, withoutSpace }} on:click={click}> <DocReferencePresenter value={doc} on:click={click}>
<slot /> <slot />
</Component> </DocReferencePresenter>
{/if} {/if}
{/if} {/if}

View File

@ -155,9 +155,24 @@
return undefined return undefined
} }
function getPanelFooter (
_class: Ref<Class<Doc>>,
object?: Doc
): { footer: AnyComponent, props?: Record<string, any> } | undefined {
if (object !== undefined) {
const footer = hierarchy.findClassOrMixinMixin(object, view.mixin.ObjectPanelFooter)
if (footer !== undefined) {
return { footer: footer.editor, props: footer.props }
}
}
return undefined
}
let mainEditor: MixinEditor | undefined let mainEditor: MixinEditor | undefined
$: editorFooter = getEditorFooter(_class, object) $: editorFooter = getEditorFooter(_class, object)
$: panelFooter = getPanelFooter(_class, object)
const getEditorOrDefault = reduceCalls(async function (_class: Ref<Class<Doc>>, _id?: Ref<Doc>): Promise<void> { const getEditorOrDefault = reduceCalls(async function (_class: Ref<Class<Doc>>, _id?: Ref<Doc>): Promise<void> {
if (objectId === undefined) return if (objectId === undefined) return
@ -341,5 +356,11 @@
<Component is={editorFooter.footer} props={{ object, _class, ...editorFooter.props, readonly }} /> <Component is={editorFooter.footer} props={{ object, _class, ...editorFooter.props, readonly }} />
</div> </div>
{/if} {/if}
<svelte:fragment slot="panel-footer">
{#if panelFooter}
<Component is={panelFooter.footer} props={{ object, _class, ...panelFooter.props, readonly }} />
{/if}
</svelte:fragment>
</Panel> </Panel>
{/if} {/if}

View File

@ -33,6 +33,7 @@
export let enableChecking = true export let enableChecking = true
export let tableId: string | undefined = undefined export let tableId: string | undefined = undefined
export let fade: FadeOptions = tableSP export let fade: FadeOptions = tableSP
export let prefferedSorting: string = 'modifiedOn'
// If defined, will show a number of dummy items before real data will appear. // If defined, will show a number of dummy items before real data will appear.
export let loadingProps: LoadingProps | undefined = undefined export let loadingProps: LoadingProps | undefined = undefined
@ -55,11 +56,9 @@
// Search config // Search config
const _config = config const _config = config
let prefferedSorting: string = 'modifiedOn'
function updateConfig (config: Array<BuildModelKey | string>, search?: string): void { function updateConfig (config: Array<BuildModelKey | string>, search?: string): void {
const useSearch = search !== '' && search != null const useSearch = search !== '' && search != null
prefferedSorting = !useSearch ? 'modifiedOn' : '#score' prefferedSorting = !useSearch ? prefferedSorting : '#score'
} }
$: updateConfig(config, query.$search) $: updateConfig(config, query.$search)

View File

@ -27,7 +27,9 @@
{#if kind === 'link'} {#if kind === 'link'}
<Button {kind} {size} {justify} {width}> <Button {kind} {size} {justify} {width}>
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<TimeSince {value} /> {#if value != null}
<TimeSince {value} />
{/if}
</svelte:fragment> </svelte:fragment>
</Button> </Button>
{:else} {:else}

View File

@ -91,6 +91,7 @@ const view = plugin(viewId, {
ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>, ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>,
ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>, ObjectEditorHeader: '' as Ref<Mixin<ObjectEditorHeader>>,
ObjectEditorFooter: '' as Ref<Mixin<ObjectEditorFooter>>, ObjectEditorFooter: '' as Ref<Mixin<ObjectEditorFooter>>,
ObjectPanelFooter: '' as Ref<Mixin<ObjectEditorFooter>>,
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>, ObjectValidator: '' as Ref<Mixin<ObjectValidator>>,
ObjectFactory: '' as Ref<Mixin<ObjectFactory>>, ObjectFactory: '' as Ref<Mixin<ObjectFactory>>,
ObjectTitle: '' as Ref<Mixin<ObjectTitle>>, ObjectTitle: '' as Ref<Mixin<ObjectTitle>>,

View File

@ -236,6 +236,11 @@ export interface ObjectEditorFooter extends Class<Doc> {
props?: Record<string, any> props?: Record<string, any>
} }
export interface ObjectPanelFooter extends Class<Doc> {
editor: AnyComponent
props?: Record<string, any>
}
/** /**
* @public * @public
*/ */

View File

@ -13,29 +13,30 @@
// limitations under the License. // limitations under the License.
// //
import contact, { Employee, Person, PersonAccount, formatName, getName } from '@hcengineering/contact' import contact, { Employee, formatName, getName, Person, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
concatLink,
Doc,
Ref, Ref,
Tx, Tx,
TxCUD,
TxCreateDoc, TxCreateDoc,
TxCUD,
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxUpdateDoc, TxUpdateDoc,
UserStatus, UserStatus
Doc,
concatLink
} from '@hcengineering/core' } from '@hcengineering/core'
import love, { import love, {
Invite, Invite,
isOffice,
JoinRequest, JoinRequest,
loveId,
MeetingMinutes, MeetingMinutes,
MeetingStatus,
ParticipantInfo, ParticipantInfo,
RequestStatus, RequestStatus,
RoomAccess, RoomAccess
isOffice,
loveId
} from '@hcengineering/love' } from '@hcengineering/love'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { getMetadata, translate } from '@hcengineering/platform' import { getMetadata, translate } from '@hcengineering/platform'
@ -240,6 +241,40 @@ async function setDefaultRoomAccess (info: ParticipantInfo, control: TriggerCont
return res return res
} }
async function finishMeetingMinutes (
info: ParticipantInfo,
control: TriggerControl,
tx: TxCUD<ParticipantInfo>
): Promise<Tx[]> {
const res: Tx[] = []
const roomInfos = await control.queryFind(control.ctx, love.class.RoomInfo, {})
const roomInfo = roomInfos.find((ri) => ri.persons.includes(info.person))
if (roomInfo === undefined) {
return res
}
const currentPersons = roomInfo.persons.filter((p) => p !== info.person)
if (currentPersons.length === 0) {
const meetingMinutes = await control.findAll(control.ctx, love.class.MeetingMinutes, {
attachedTo: roomInfo.room,
status: MeetingStatus.Active
})
for (const meeting of meetingMinutes) {
res.push(
control.txFactory.createTxUpdateDoc(meeting._class, meeting.space, meeting._id, {
status: MeetingStatus.Finished,
meetingEnd: tx.modifiedOn
})
)
}
}
return res
}
export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): Promise<Tx[]> { export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = [] const result: Tx[] = []
for (const tx of txes) { for (const tx of txes) {
@ -254,6 +289,7 @@ export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): P
continue continue
} }
result.push(...(await setDefaultRoomAccess(removedInfo, control))) result.push(...(await setDefaultRoomAccess(removedInfo, control)))
result.push(...(await finishMeetingMinutes(removedInfo, control, actualTx)))
continue continue
} }
if (actualTx._class === core.class.TxUpdateDoc) { if (actualTx._class === core.class.TxUpdateDoc) {
@ -269,6 +305,7 @@ export async function OnParticipantInfo (txes: Tx[], control: TriggerControl): P
} }
result.push(...(await rejectJoinRequests(info, control))) result.push(...(await rejectJoinRequests(info, control)))
result.push(...(await setDefaultRoomAccess(info, control))) result.push(...(await setDefaultRoomAccess(info, control)))
result.push(...(await finishMeetingMinutes(info, control, actualTx)))
result.push(...(await roomJoinHandler(info, control))) result.push(...(await roomJoinHandler(info, control)))
} }
} }

View File

@ -949,7 +949,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(activity.class.ActivityExtension, core.space.Model, { builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: github.class.GithubPullRequest, ofClass: github.class.GithubPullRequest,
components: { components: {
input: chunter.component.ChatMessageInput input: { component: chunter.component.ChatMessageInput }
} }
}) })