From a1cee24473d5499d4e511ac45d6a4ec52f29d1a4 Mon Sep 17 00:00:00 2001 From: Kristina Date: Wed, 18 Sep 2024 06:27:13 +0400 Subject: [PATCH] Add widgets sidebar (#6578) --- models/calendar/src/index.ts | 15 + models/calendar/src/plugin.ts | 7 +- models/chunter/src/actions.ts | 32 +- models/chunter/src/index.ts | 382 ++++++------------ models/chunter/src/plugin.ts | 12 +- models/chunter/src/types.ts | 162 ++++++++ models/love/src/index.ts | 34 +- models/love/src/plugin.ts | 4 +- models/notification/src/index.ts | 4 +- models/workbench/src/index.ts | 38 +- .../extensions/ComponentExtensions.svelte | 2 +- packages/ui/src/components/Header.svelte | 5 +- packages/ui/src/components/ModernTab.svelte | 163 ++++++++ packages/ui/src/index.ts | 1 + packages/ui/src/resize.ts | 5 + .../src/components/Replies.svelte | 2 +- plugins/activity/src/index.ts | 2 +- .../src/components/CalendarHeader.svelte | 65 +++ .../src/components/CalendarNavigation.svelte | 48 +++ .../src/components/CalendarView.svelte | 110 ++--- .../src/components/CalendarWidget.svelte | 20 + .../components/CalendarWidgetHeader.svelte | 61 +++ plugins/calendar-resources/src/index.ts | 4 +- plugins/chunter-assets/lang/en.json | 3 +- plugins/chunter-assets/lang/es.json | 3 +- plugins/chunter-assets/lang/fr.json | 3 +- plugins/chunter-assets/lang/pt.json | 3 +- plugins/chunter-assets/lang/ru.json | 3 +- plugins/chunter-assets/lang/zh.json | 3 +- .../src/channelDataProvider.ts | 2 +- .../src/components/Channel.svelte | 4 + .../src/components/ChannelHeader.svelte | 8 +- .../src/components/ChannelScrollView.svelte | 30 +- .../src/components/ChannelSidebarView.svelte | 113 ++++++ .../src/components/ChatWidget.svelte | 58 +++ .../src/components/ChatWidgetTab.svelte | 107 +++++ .../src/components/DirectIcon.svelte | 49 ++- .../src/components/Header.svelte | 29 +- .../src/components/chat/Chat.svelte | 32 +- .../chat/navigator/ChatNavItem.svelte | 4 +- .../threads/ThreadSidebarView.svelte | 38 ++ .../src/components/threads/ThreadView.svelte | 46 ++- plugins/chunter-resources/src/index.ts | 20 +- plugins/chunter-resources/src/navigation.ts | 179 +++++++- plugins/chunter/package.json | 1 + plugins/chunter/src/index.ts | 18 +- plugins/love-resources/package.json | 20 +- .../src/components/ControlExt.svelte | 155 ++++--- .../src/components/LoveWidget.svelte | 98 +++++ .../src/components/VideoPopup.svelte | 19 +- .../src/components/VideoWidget.svelte | 47 +++ plugins/love-resources/src/index.ts | 6 +- plugins/love/package.json | 3 +- plugins/love/src/index.ts | 5 +- plugins/notification-resources/package.json | 2 +- .../src/components/Notification.svelte | 30 +- .../src/components/NotifyMarker.svelte | 18 +- .../src/components/inbox/Inbox.svelte | 22 +- plugins/view-assets/assets/icons.svg | 3 + plugins/view-assets/src/index.ts | 3 +- plugins/view/src/index.ts | 3 +- plugins/workbench-assets/lang/en.json | 7 +- plugins/workbench-assets/lang/es.json | 7 +- plugins/workbench-assets/lang/fr.json | 7 +- plugins/workbench-assets/lang/pt.json | 7 +- plugins/workbench-assets/lang/ru.json | 7 +- plugins/workbench-assets/lang/zh.json | 7 +- .../src/components/Workbench.svelte | 32 +- .../src/components/sidebar/Sidebar.svelte | 70 ++++ .../components/sidebar/SidebarExpanded.svelte | 172 ++++++++ .../src/components/sidebar/SidebarMini.svelte | 26 ++ .../src/components/sidebar/SidebarTab.svelte | 57 +++ .../src/components/sidebar/SidebarTabs.svelte | 99 +++++ .../sidebar/widgets/AddWidgetsPopup.svelte | 104 +++++ .../sidebar/widgets/WidgetPresenter.svelte | 38 ++ .../sidebar/widgets/WidgetsBar.svelte | 120 ++++++ plugins/workbench-resources/src/index.ts | 1 + plugins/workbench-resources/src/plugin.ts | 4 +- plugins/workbench-resources/src/sidebar.ts | 294 ++++++++++++++ plugins/workbench-resources/src/utils.ts | 7 +- plugins/workbench/src/index.ts | 54 ++- tests/sanity/tests/chat/chat.spec.ts | 13 +- tests/sanity/tests/model/channel-page.ts | 11 + .../tests/model/planning/planning-page.ts | 10 +- 84 files changed, 2922 insertions(+), 600 deletions(-) create mode 100644 models/chunter/src/types.ts create mode 100644 packages/ui/src/components/ModernTab.svelte create mode 100644 plugins/calendar-resources/src/components/CalendarHeader.svelte create mode 100644 plugins/calendar-resources/src/components/CalendarNavigation.svelte create mode 100644 plugins/calendar-resources/src/components/CalendarWidget.svelte create mode 100644 plugins/calendar-resources/src/components/CalendarWidgetHeader.svelte create mode 100644 plugins/chunter-resources/src/components/ChannelSidebarView.svelte create mode 100644 plugins/chunter-resources/src/components/ChatWidget.svelte create mode 100644 plugins/chunter-resources/src/components/ChatWidgetTab.svelte create mode 100644 plugins/chunter-resources/src/components/threads/ThreadSidebarView.svelte create mode 100644 plugins/love-resources/src/components/LoveWidget.svelte create mode 100644 plugins/love-resources/src/components/VideoWidget.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/Sidebar.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/SidebarMini.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/SidebarTabs.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/widgets/AddWidgetsPopup.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/widgets/WidgetPresenter.svelte create mode 100644 plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte create mode 100644 plugins/workbench-resources/src/sidebar.ts diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index 62a03144c2..b2c9099059 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -58,6 +58,9 @@ import view, { createAction } from '@hcengineering/model-view' import notification from '@hcengineering/notification' import setting from '@hcengineering/setting' import { type AnyComponent } from '@hcengineering/ui/src/types' +import workbench from '@hcengineering/model-workbench' +import { WidgetType } from '@hcengineering/workbench' + import calendar from './plugin' export * from '@hcengineering/calendar' @@ -166,6 +169,18 @@ export function createModel (builder: Builder): void { TCalendarEventPresenter ) + builder.createDoc( + workbench.class.Widget, + core.space.Model, + { + label: calendar.string.Calendar, + type: WidgetType.Fixed, + icon: calendar.icon.Calendar, + component: calendar.component.CalendarWidget + }, + calendar.ids.CalendarWidget + ) + builder.mixin(calendar.class.Event, core.class.Class, calendar.mixin.CalendarEventPresenter, { presenter: calendar.component.CalendarEventPresenter }) diff --git a/models/calendar/src/plugin.ts b/models/calendar/src/plugin.ts index c049c20f56..3821c807fc 100644 --- a/models/calendar/src/plugin.ts +++ b/models/calendar/src/plugin.ts @@ -28,6 +28,7 @@ import { type Viewlet, type ViewletDescriptor } from '@hcengineering/view' +import { type Widget } from '@hcengineering/workbench' export default mergeIds(calendarId, calendar, { component: { @@ -36,7 +37,8 @@ export default mergeIds(calendarId, calendar, { EventPresenter: '' as AnyComponent, CalendarIntegrationIcon: '' as AnyComponent, CalendarEventPresenter: '' as AnyComponent, - IntegrationConfigure: '' as AnyComponent + IntegrationConfigure: '' as AnyComponent, + CalendarWidget: '' as AnyComponent }, action: { SaveEventReminder: '' as Ref, @@ -66,6 +68,7 @@ export default mergeIds(calendarId, calendar, { }, ids: { UpdateRemainderActivityViewlet: '' as Ref, - CalendarNotificationGroup: '' as Ref + CalendarNotificationGroup: '' as Ref, + CalendarWidget: '' as Ref } }) diff --git a/models/chunter/src/actions.ts b/models/chunter/src/actions.ts index 474ea008df..0bdd338601 100644 --- a/models/chunter/src/actions.ts +++ b/models/chunter/src/actions.ts @@ -18,6 +18,7 @@ import view, { actionTemplates as viewTemplates, createAction, template } from ' import notification, { notificationActionTemplates } from '@hcengineering/model-notification' import activity from '@hcengineering/activity' import workbench from '@hcengineering/model-workbench' +import core from '@hcengineering/model-core' import chunter from './plugin' @@ -35,6 +36,13 @@ const actionTemplates = template({ }) export function defineActions (builder: Builder): void { + builder.createDoc( + view.class.ActionCategory, + core.space.Model, + { label: chunter.string.Chat, visible: true }, + chunter.category.Chunter + ) + createAction( builder, { @@ -127,7 +135,7 @@ export function defineActions (builder: Builder): void { }, context: { mode: 'context', - group: 'tools' + group: 'remove' } }, chunter.action.ArchiveChannel @@ -138,7 +146,7 @@ export function defineActions (builder: Builder): void { target: chunter.class.Channel, context: { mode: ['browser', 'context'], - group: 'create' + group: 'edit' }, action: workbench.actionImpl.Navigate, actionProps: { @@ -246,4 +254,24 @@ export function defineActions (builder: Builder): void { objectClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] } } }) + + createAction(builder, { + action: chunter.actionImpl.OpenInSidebar, + label: workbench.string.OpenInSidebar, + icon: view.icon.DetailsFilled, + input: 'focus', + category: chunter.category.Chunter, + target: notification.class.DocNotifyContext, + context: { mode: ['context', 'browser'], group: 'tools' } + }) + + createAction(builder, { + action: chunter.actionImpl.OpenInSidebarTab, + label: workbench.string.OpenInSidebarNewTab, + icon: view.icon.DetailsFilled, + input: 'focus', + category: chunter.category.Chunter, + target: notification.class.DocNotifyContext, + context: { mode: ['context', 'browser'], group: 'tools' } + }) } diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 769a294793..d0e6303295 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -13,164 +13,35 @@ // limitations under the License. // -import activity, { type ActivityMessage, type ActivityMessageControl } from '@hcengineering/activity' -import { - type Channel, - chunterId, - type DirectMessage, - type ChatMessage, - type ChatMessageViewlet, - type ChunterSpace, - type ObjectChatPanel, - type ThreadMessage, - type ChatSyncInfo, - type InlineButton, - type TypingInfo, - type InlineButtonAction -} from '@hcengineering/chunter' +import activity, { type ActivityMessageControl } from '@hcengineering/activity' +import { chunterId, type ChunterSpace } from '@hcengineering/chunter' import presentation from '@hcengineering/model-presentation' -import contact, { type ChannelProvider as SocialChannelProvider, type Person } from '@hcengineering/contact' -import { - type Class, - type Doc, - type Domain, - DOMAIN_MODEL, - type Ref, - type Timestamp, - IndexKind, - DOMAIN_TRANSIENT -} from '@hcengineering/core' -import { - type Builder, - Collection as PropCollection, - Index, - Mixin, - Model, - Prop, - TypeMarkup, - TypeRef, - TypeString, - TypeTimestamp, - UX -} from '@hcengineering/model' -import attachment from '@hcengineering/model-attachment' -import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import { type Builder } from '@hcengineering/model' +import core from '@hcengineering/model-core' import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' -import { type IntlString, type Resource } from '@hcengineering/platform' -import { TActivityMessage } from '@hcengineering/model-activity' -import { type DocNotifyContext } from '@hcengineering/notification' +import { WidgetType } from '@hcengineering/workbench' import chunter from './plugin' import { defineActions } from './actions' import { defineNotifications } from './notifications' +import { + DOMAIN_CHUNTER, + TChannel, + TChatMessage, + TChatMessageViewlet, + TChatSyncInfo, + TChunterSpace, + TDirectMessage, + TInlineButton, + TObjectChatPanel, + TThreadMessage, + TTypingInfo +} from './types' export { chunterId } from '@hcengineering/chunter' export { chunterOperation } from './migration' - -export const DOMAIN_CHUNTER = 'chunter' as Domain - -@Model(chunter.class.ChunterSpace, core.class.Space) -export class TChunterSpace extends TSpace implements ChunterSpace { - @Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages) - messages?: number -} - -@Model(chunter.class.Channel, chunter.class.ChunterSpace) -@UX(chunter.string.Channel, chunter.icon.Hashtag, undefined, undefined, undefined, chunter.string.Channels) -export class TChannel extends TChunterSpace implements Channel { - @Prop(TypeString(), chunter.string.Topic) - @Index(IndexKind.FullText) - topic?: string -} - -@Model(chunter.class.DirectMessage, chunter.class.ChunterSpace) -@UX(chunter.string.DirectMessage, contact.icon.Person, undefined, undefined, undefined, chunter.string.DirectMessages) -export class TDirectMessage extends TChunterSpace implements DirectMessage {} - -@Model(chunter.class.ChatMessage, activity.class.ActivityMessage) -@UX(chunter.string.Message, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) -export class TChatMessage extends TActivityMessage implements ChatMessage { - @Prop(TypeMarkup(), chunter.string.Message) - @Index(IndexKind.FullText) - message!: string - - @Prop(TypeTimestamp(), chunter.string.Edit) - editedOn?: Timestamp - - @Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, { - shortLabel: attachment.string.Files - }) - attachments?: number - - @Prop(TypeRef(contact.class.ChannelProvider), core.string.Object) - provider?: Ref - - @Prop(PropCollection(chunter.class.InlineButton), core.string.Object) - inlineButtons?: number -} - -@Model(chunter.class.ThreadMessage, chunter.class.ChatMessage) -@UX(chunter.string.ThreadMessage, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) -export class TThreadMessage extends TChatMessage implements ThreadMessage { - @Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo) - @Index(IndexKind.Indexed) - declare attachedTo: Ref - - @Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass) - @Index(IndexKind.Indexed) - declare attachedToClass: Ref> - - @Prop(TypeRef(core.class.Doc), core.string.Object) - @Index(IndexKind.Indexed) - objectId!: Ref - - @Prop(TypeRef(core.class.Class), core.string.Class) - @Index(IndexKind.Indexed) - objectClass!: Ref> -} - -@Model(chunter.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL) -export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet { - @Prop(TypeRef(core.class.Doc), core.string.Class) - @Index(IndexKind.Indexed) - objectClass!: Ref> - - @Prop(TypeRef(core.class.Doc), core.string.Class) - @Index(IndexKind.Indexed) - messageClass!: Ref> - - label?: IntlString - onlyWithParent?: boolean -} - -@Mixin(chunter.mixin.ObjectChatPanel, core.class.Class) -export class TObjectChatPanel extends TClass implements ObjectChatPanel { - ignoreKeys!: string[] -} - -@Model(chunter.class.ChatSyncInfo, core.class.Doc, DOMAIN_CHUNTER) -export class TChatSyncInfo extends TDoc implements ChatSyncInfo { - user!: Ref - hidden!: Ref[] - timestamp!: Timestamp -} - -@Model(chunter.class.InlineButton, core.class.Doc, DOMAIN_CHUNTER) -export class TInlineButton extends TAttachedDoc implements InlineButton { - name!: string - titleIntl?: IntlString - title?: string - action!: Resource -} - -@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT) -export class TTypingInfo extends TDoc implements TypingInfo { - objectId!: Ref - objectClass!: Ref> - person!: Ref - lastTyping!: Timestamp -} +export * from './types' export function createModel (builder: Builder): void { builder.createModel( @@ -185,16 +56,37 @@ export function createModel (builder: Builder): void { TInlineButton, TTypingInfo ) + + builder.createDoc( + workbench.class.Application, + core.space.Model, + { + label: chunter.string.ApplicationLabelChunter, + icon: chunter.icon.Chunter, + alias: chunterId, + hidden: false, + component: chunter.component.Chat + }, + chunter.app.Chunter + ) + + builder.createDoc( + workbench.class.Widget, + core.space.Model, + { + label: chunter.string.Chat, + type: WidgetType.Flexible, + icon: chunter.icon.Chunter, + closeIfNoTabs: true, + onTabClose: chunter.function.CloseChatWidgetTab, + component: chunter.component.ChatWidget, + tabComponent: chunter.component.ChatWidgetTab + }, + chunter.ids.ChatWidget + ) + const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage] - builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIcon, { - component: chunter.component.DirectIcon - }) - - builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectIcon, { - component: chunter.component.ChannelIcon - }) - spaceClasses.forEach((spaceClass) => { builder.mixin(spaceClass, core.class.Class, activity.mixin.ActivityDoc, {}) @@ -211,6 +103,15 @@ export function createModel (builder: Builder): void { }) }) + // Presenters + builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIcon, { + component: chunter.component.DirectIcon + }) + + builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ObjectIcon, { + component: chunter.component.ChannelIcon + }) + builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectTitle, { titleProvider: chunter.function.DirectTitleProvider }) @@ -235,12 +136,21 @@ export function createModel (builder: Builder): void { header: chunter.component.ChannelHeader }) - builder.createDoc( - view.class.ActionCategory, - core.space.Model, - { label: chunter.string.Chat, visible: true }, - chunter.category.Chunter - ) + builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIdentifier, { + provider: chunter.function.DmIdentifierProvider + }) + + builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.CollectionPresenter, { + presenter: chunter.component.ChatMessagesPresenter + }) + + builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.ObjectPresenter, { + presenter: chunter.component.ChatMessagePresenter + }) + + builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, { + presenter: chunter.component.ThreadMessagePresenter + }) builder.createDoc( view.class.Viewlet, @@ -257,58 +167,6 @@ export function createModel (builder: Builder): void { chunter.viewlet.Channels ) - builder.createDoc( - workbench.class.Application, - core.space.Model, - { - label: chunter.string.ApplicationLabelChunter, - icon: chunter.icon.Chunter, - alias: chunterId, - hidden: false, - component: chunter.component.Chat, - aside: chunter.component.ChatAside - }, - chunter.app.Chunter - ) - - builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.LinkProvider, { - encode: chunter.function.GetMessageLink - }) - - builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.LinkProvider, { - encode: chunter.function.GetThreadLink - }) - - builder.mixin(chunter.class.Channel, core.class.Class, view.mixin.ClassFilters, { - filters: [] - }) - - builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { - label: chunter.string.Comments, - position: 60, - filter: chunter.filter.ChatMessagesFilter - }) - - builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectIdentifier, { - provider: chunter.function.DmIdentifierProvider - }) - - builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.CollectionPresenter, { - presenter: chunter.component.ChatMessagesPresenter - }) - - builder.mixin(chunter.class.ChatMessage, core.class.Class, view.mixin.ObjectPresenter, { - presenter: chunter.component.ChatMessagePresenter - }) - - builder.mixin(chunter.class.ChatMessage, core.class.Class, presentation.mixin.InstantTransactions, { - txClasses: [core.class.TxCreateDoc] - }) - - builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.ObjectPresenter, { - presenter: chunter.component.ThreadMessagePresenter - }) - builder.createDoc( chunter.class.ChatMessageViewlet, core.space.Model, @@ -320,31 +178,6 @@ export function createModel (builder: Builder): void { chunter.ids.ThreadMessageViewlet ) - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { - ofClass: chunter.class.Channel, - components: { input: chunter.component.ChatMessageInput } - }) - - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { - ofClass: chunter.class.DirectMessage, - components: { input: chunter.component.ChatMessageInput } - }) - - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { - ofClass: activity.class.DocUpdateMessage, - components: { input: chunter.component.ChatMessageInput } - }) - - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { - ofClass: chunter.class.ChatMessage, - components: { input: chunter.component.ChatMessageInput } - }) - - builder.createDoc(activity.class.ActivityExtension, core.space.Model, { - ofClass: activity.class.ActivityReference, - components: { input: chunter.component.ChatMessageInput } - }) - builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, { ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description', 'members', 'owners'] }) @@ -353,19 +186,6 @@ export function createModel (builder: Builder): void { ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description', 'members', 'owners'] }) - builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, { - presenter: chunter.component.ChatMessagePreview - }) - - builder.mixin(chunter.class.ThreadMessage, core.class.Class, activity.mixin.ActivityMessagePreview, { - presenter: chunter.component.ThreadMessagePreview - }) - - builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { - domain: DOMAIN_CHUNTER, - disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }] - }) - builder.createDoc(activity.class.ReplyProvider, core.space.Model, { function: chunter.function.ReplyToThread }) @@ -375,6 +195,11 @@ export function createModel (builder: Builder): void { strict: true }) + builder.mixin(chunter.class.ChatMessage, core.class.Class, presentation.mixin.InstantTransactions, { + txClasses: [core.class.TxCreateDoc] + }) + + // Activity builder.createDoc>(activity.class.ActivityMessageControl, core.space.Model, { objectClass: chunter.class.Channel, skip: [ @@ -417,6 +242,59 @@ export function createModel (builder: Builder): void { } }) + builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, { + presenter: chunter.component.ChatMessagePreview + }) + + builder.mixin(chunter.class.ThreadMessage, core.class.Class, activity.mixin.ActivityMessagePreview, { + presenter: chunter.component.ThreadMessagePreview + }) + + builder.mixin(activity.class.ActivityMessage, core.class.Class, view.mixin.LinkProvider, { + encode: chunter.function.GetMessageLink + }) + + builder.mixin(chunter.class.ThreadMessage, core.class.Class, view.mixin.LinkProvider, { + encode: chunter.function.GetThreadLink + }) + + builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, { + label: chunter.string.Comments, + position: 60, + filter: chunter.filter.ChatMessagesFilter + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: chunter.class.Channel, + components: { input: chunter.component.ChatMessageInput } + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: chunter.class.DirectMessage, + components: { input: chunter.component.ChatMessageInput } + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: activity.class.DocUpdateMessage, + components: { input: chunter.component.ChatMessageInput } + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: chunter.class.ChatMessage, + components: { input: chunter.component.ChatMessageInput } + }) + + builder.createDoc(activity.class.ActivityExtension, core.space.Model, { + ofClass: activity.class.ActivityReference, + components: { input: chunter.component.ChatMessageInput } + }) + + // Indexing + builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { + domain: DOMAIN_CHUNTER, + disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }] + }) + defineActions(builder) defineNotifications(builder) } diff --git a/models/chunter/src/plugin.ts b/models/chunter/src/plugin.ts index 4a5efef78e..9f21394ddd 100644 --- a/models/chunter/src/plugin.ts +++ b/models/chunter/src/plugin.ts @@ -22,6 +22,7 @@ import type { IntlString, Resource } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform' import type { AnyComponent, Location } from '@hcengineering/ui/src/types' import type { Action, ActionCategory, ViewAction, Viewlet, ViewletDescriptor } from '@hcengineering/view' +import { type WidgetTab } from '@hcengineering/workbench' export default mergeIds(chunterId, chunter, { component: { @@ -29,6 +30,8 @@ export default mergeIds(chunterId, chunter, { DmPresenter: '' as AnyComponent, ChannelsPanel: '' as AnyComponent, Chat: '' as AnyComponent, + ChatWidget: '' as AnyComponent, + ChatWidgetTab: '' as AnyComponent, ChatMessageNotificationLabel: '' as AnyComponent, ThreadNotificationPresenter: '' as AnyComponent, JoinChannelNotificationPresenter: '' as AnyComponent @@ -47,7 +50,9 @@ export default mergeIds(chunterId, chunter, { UnarchiveChannel: '' as ViewAction, ConvertDmToPrivateChannel: '' as ViewAction, DeleteChatMessage: '' as ViewAction, - ReplyToThread: '' as ViewAction + ReplyToThread: '' as ViewAction, + OpenInSidebar: '' as ViewAction, + OpenInSidebarTab: '' as ViewAction }, category: { Chunter: '' as Ref @@ -98,9 +103,10 @@ export default mergeIds(chunterId, chunter, { CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise>, GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record) => Promise>, GetThreadLink: '' as Resource<(doc: Doc, props: Record) => Promise>, - ReplyToThread: '' as Resource<(doc: ActivityMessage) => Promise>, + ReplyToThread: '' as Resource<(doc: ActivityMessage, event: MouseEvent) => Promise>, CanReplyToThread: '' as Resource<(doc?: Doc | Doc[]) => Promise>, - GetMessageLink: '' as Resource<(doc: Doc, props: Record) => Promise> + GetMessageLink: '' as Resource<(doc: Doc, props: Record) => Promise>, + CloseChatWidgetTab: '' as Resource<(tab: WidgetTab) => Promise> }, filter: { ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref) => boolean> diff --git a/models/chunter/src/types.ts b/models/chunter/src/types.ts new file mode 100644 index 0000000000..c24f2e086e --- /dev/null +++ b/models/chunter/src/types.ts @@ -0,0 +1,162 @@ +// +// 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. +// + +import { + Collection as PropCollection, + Index, + Mixin, + Model, + Prop, + TypeMarkup, + TypeRef, + TypeString, + TypeTimestamp, + UX +} from '@hcengineering/model' +import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core' +import type { + Channel, + ChatMessage, + ChatMessageViewlet, + ChatSyncInfo, + ChunterSpace, + DirectMessage, + InlineButton, + InlineButtonAction, + ObjectChatPanel, + ThreadMessage, + TypingInfo +} from '@hcengineering/chunter' +import { + type Class, + type Doc, + type Domain, + DOMAIN_MODEL, + DOMAIN_TRANSIENT, + IndexKind, + type Ref, + type Timestamp +} from '@hcengineering/core' +import contact, { type ChannelProvider as SocialChannelProvider, type Person } from '@hcengineering/contact' +import activity, { type ActivityMessage } from '@hcengineering/activity' +import { TActivityMessage } from '@hcengineering/model-activity' +import attachment from '@hcengineering/model-attachment' +import type { IntlString, Resource } from '@hcengineering/platform' +import type { DocNotifyContext } from '@hcengineering/notification' + +import chunter from './plugin' + +export const DOMAIN_CHUNTER = 'chunter' as Domain +@Model(chunter.class.ChunterSpace, core.class.Space) +export class TChunterSpace extends TSpace implements ChunterSpace { + @Prop(PropCollection(activity.class.ActivityMessage), chunter.string.Messages) + messages?: number +} + +@Model(chunter.class.Channel, chunter.class.ChunterSpace) +@UX(chunter.string.Channel, chunter.icon.Hashtag, undefined, undefined, undefined, chunter.string.Channels) +export class TChannel extends TChunterSpace implements Channel { + @Prop(TypeString(), chunter.string.Topic) + @Index(IndexKind.FullText) + topic?: string +} + +@Model(chunter.class.DirectMessage, chunter.class.ChunterSpace) +@UX(chunter.string.DirectMessage, contact.icon.Person, undefined, undefined, undefined, chunter.string.DirectMessages) +export class TDirectMessage extends TChunterSpace implements DirectMessage {} + +@Model(chunter.class.ChatMessage, activity.class.ActivityMessage) +@UX(chunter.string.Message, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) +export class TChatMessage extends TActivityMessage implements ChatMessage { + @Prop(TypeMarkup(), chunter.string.Message) + @Index(IndexKind.FullText) + message!: string + + @Prop(TypeTimestamp(), chunter.string.Edit) + editedOn?: Timestamp + + @Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, { + shortLabel: attachment.string.Files + }) + attachments?: number + + @Prop(TypeRef(contact.class.ChannelProvider), core.string.Object) + provider?: Ref + + @Prop(PropCollection(chunter.class.InlineButton), core.string.Object) + inlineButtons?: number +} + +@Model(chunter.class.ThreadMessage, chunter.class.ChatMessage) +@UX(chunter.string.ThreadMessage, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) +export class TThreadMessage extends TChatMessage implements ThreadMessage { + @Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedTo) + @Index(IndexKind.Indexed) + declare attachedTo: Ref + + @Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass) + @Index(IndexKind.Indexed) + declare attachedToClass: Ref> + + @Prop(TypeRef(core.class.Doc), core.string.Object) + @Index(IndexKind.Indexed) + objectId!: Ref + + @Prop(TypeRef(core.class.Class), core.string.Class) + @Index(IndexKind.Indexed) + objectClass!: Ref> +} + +@Model(chunter.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL) +export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet { + @Prop(TypeRef(core.class.Doc), core.string.Class) + @Index(IndexKind.Indexed) + objectClass!: Ref> + + @Prop(TypeRef(core.class.Doc), core.string.Class) + @Index(IndexKind.Indexed) + messageClass!: Ref> + + label?: IntlString + onlyWithParent?: boolean +} + +@Mixin(chunter.mixin.ObjectChatPanel, core.class.Class) +export class TObjectChatPanel extends TClass implements ObjectChatPanel { + ignoreKeys!: string[] +} + +@Model(chunter.class.ChatSyncInfo, core.class.Doc, DOMAIN_CHUNTER) +export class TChatSyncInfo extends TDoc implements ChatSyncInfo { + user!: Ref + hidden!: Ref[] + timestamp!: Timestamp +} + +@Model(chunter.class.InlineButton, core.class.Doc, DOMAIN_CHUNTER) +export class TInlineButton extends TAttachedDoc implements InlineButton { + name!: string + titleIntl?: IntlString + title?: string + action!: Resource +} + +@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT) +export class TTypingInfo extends TDoc implements TypingInfo { + objectId!: Ref + objectClass!: Ref> + person!: Ref + lastTyping!: Timestamp +} diff --git a/models/love/src/index.ts b/models/love/src/index.ts index e92ef5a5a9..5cacc6fca4 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -14,13 +14,13 @@ // import contact, { type Employee, type Person } from '@hcengineering/contact' -import { AccountRole, DOMAIN_TRANSIENT, IndexKind, type Domain, type Ref } from '@hcengineering/core' +import { AccountRole, type Domain, DOMAIN_TRANSIENT, IndexKind, type Ref } from '@hcengineering/core' import { - loveId, type DevicesPreference, type Floor, type Invite, type JoinRequest, + loveId, type Meeting, type Office, type ParticipantInfo, @@ -30,7 +30,7 @@ import { type RoomInfo, type RoomType } from '@hcengineering/love' -import { Index, Mixin, Model, Prop, TypeRef, type Builder } from '@hcengineering/model' +import { type Builder, Index, Mixin, Model, Prop, TypeRef } from '@hcengineering/model' import calendar, { TEvent } from '@hcengineering/model-calendar' import core, { TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' @@ -39,7 +39,7 @@ import view, { createAction } from '@hcengineering/model-view' import notification from '@hcengineering/notification' import { getEmbeddedLabel } from '@hcengineering/platform' import setting from '@hcengineering/setting' -import workbench from '@hcengineering/workbench' +import workbench, { WidgetType } from '@hcengineering/workbench' import love from './plugin' export { loveId } from '@hcengineering/love' @@ -165,6 +165,32 @@ export function createModel (builder: Builder): void { love.app.Love ) + builder.createDoc( + workbench.class.Widget, + core.space.Model, + { + label: love.string.Office, + type: WidgetType.Fixed, + icon: love.icon.Love, + component: love.component.LoveWidget, + headerLabel: love.string.Office + }, + love.ids.LoveWidget + ) + + builder.createDoc( + workbench.class.Widget, + core.space.Model, + { + label: love.string.MeetingRoom, + type: WidgetType.Flexible, + icon: love.icon.Cam, + component: love.component.VideoWidget, + size: 'medium' + }, + love.ids.VideoWidget + ) + builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, { extension: workbench.extensions.WorkbenchExtensions, component: love.component.WorkbenchExtension diff --git a/models/love/src/plugin.ts b/models/love/src/plugin.ts index 205a602b89..6ea8db1216 100644 --- a/models/love/src/plugin.ts +++ b/models/love/src/plugin.ts @@ -25,7 +25,9 @@ export default mergeIds(loveId, love, { component: { Main: '' as AnyComponent, WorkbenchExtension: '' as AnyComponent, - Settings: '' as AnyComponent + Settings: '' as AnyComponent, + LoveWidget: '' as AnyComponent, + VideoWidget: '' as AnyComponent }, app: { Love: '' as Ref diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 8ede6fbe79..cc076d5782 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -15,7 +15,6 @@ // import activity, { type ActivityMessage } from '@hcengineering/activity' -import chunter from '@hcengineering/chunter' import { type PersonSpace } from '@hcengineering/contact' import { AccountRole, @@ -393,8 +392,7 @@ export function createModel (builder: Builder): void { alias: notificationId, hidden: true, locationResolver: notification.resolver.Location, - component: notification.component.Inbox, - aside: chunter.component.ThreadView + component: notification.component.Inbox }, notification.app.Inbox ) diff --git a/models/workbench/src/index.ts b/models/workbench/src/index.ts index 253d5c0783..1c089f87ad 100644 --- a/models/workbench/src/index.ts +++ b/models/workbench/src/index.ts @@ -17,17 +17,22 @@ import { type Class, DOMAIN_MODEL, type Ref, type Space, type AccountRole } from import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model' import preference, { TPreference } from '@hcengineering/model-preference' import { createAction } from '@hcengineering/model-view' -import { getEmbeddedLabel, type Asset, type IntlString } from '@hcengineering/platform' +import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform' import view, { type KeyBinding } from '@hcengineering/view' import type { Application, ApplicationNavModel, HiddenApplication, SpaceView, - ViewConfiguration + ViewConfiguration, + Widget, + WidgetPreference, + WidgetTab, + WidgetType } from '@hcengineering/workbench' - +import { type AnyComponent } from '@hcengineering/ui' import core, { TClass, TDoc } from '@hcengineering/model-core' + import workbench from './plugin' export { workbenchId } from '@hcengineering/workbench' @@ -61,8 +66,33 @@ export class TSpaceView extends TClass implements SpaceView { view!: ViewConfiguration } +@Model(workbench.class.Widget, core.class.Doc, DOMAIN_MODEL) +@UX(workbench.string.Widget) +export class TWidget extends TDoc implements Widget { + label!: IntlString + icon!: Asset + type!: WidgetType + + component!: AnyComponent + tabComponent?: AnyComponent + headerLabel?: IntlString + + closeIfNoTabs?: boolean + onTabClose?: Resource<(tab: WidgetTab) => Promise> +} + +@Model(workbench.class.WidgetPreference, preference.class.Preference) +@UX(workbench.string.WidgetPreference) +export class TWidgetPreference extends TPreference implements WidgetPreference { + @Prop(TypeRef(workbench.class.Widget), workbench.string.WidgetPreference) + declare attachedTo: Ref + + enabled!: boolean +} + export function createModel (builder: Builder): void { - builder.createModel(TApplication, TSpaceView, THiddenApplication, TApplicationNavModel) + builder.createModel(TApplication, TSpaceView, THiddenApplication, TApplicationNavModel, TWidget, TWidgetPreference) + builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, { presenter: workbench.component.ApplicationPresenter }) diff --git a/packages/presentation/src/components/extensions/ComponentExtensions.svelte b/packages/presentation/src/components/extensions/ComponentExtensions.svelte index 9c058485d6..656fbbd8b8 100644 --- a/packages/presentation/src/components/extensions/ComponentExtensions.svelte +++ b/packages/presentation/src/components/extensions/ComponentExtensions.svelte @@ -19,5 +19,5 @@ {#each extensions as extension} - + {/each} diff --git a/packages/ui/src/components/Header.svelte b/packages/ui/src/components/Header.svelte index 27b782a80a..250b517774 100644 --- a/packages/ui/src/components/Header.svelte +++ b/packages/ui/src/components/Header.svelte @@ -38,6 +38,7 @@ export let overflowExtra: boolean = false export let noPrint: boolean = false export let freezeBefore: boolean = false + export let doubleRowWidth = 768 const dispatch = createEventDispatcher() @@ -76,8 +77,8 @@
{ - if (!doubleRow && element.clientWidth <= 768) doubleRow = true - else if (doubleRow && element.clientWidth > 768) doubleRow = false + if (!doubleRow && element.clientWidth <= doubleRowWidth) doubleRow = true + else if (doubleRow && element.clientWidth > doubleRowWidth) doubleRow = false }} class="hulyHeader-container" class:doubleRow={_doubleRow} diff --git a/packages/ui/src/components/ModernTab.svelte b/packages/ui/src/components/ModernTab.svelte new file mode 100644 index 0000000000..2be4c7f9cb --- /dev/null +++ b/packages/ui/src/components/ModernTab.svelte @@ -0,0 +1,163 @@ + + + + + +
+ + + {#if icon} +
+ +
+ {/if} + + + {#if label} + {label} + {:else if labelIntl} + + + {#if canClose} +
+ dispatch('close')} /> +
+ {:else} +
+ {/if} +
+ + diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 2a14bf1aea..4322db40f7 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -157,6 +157,7 @@ export { default as Hotkey } from './components/Hotkey.svelte' export { default as HotkeyGroup } from './components/HotkeyGroup.svelte' export { default as ModernWizardDialog } from './components/wizard/ModernWizardDialog.svelte' export { default as ModernWizardBar } from './components/wizard/ModernWizardBar.svelte' +export { default as ModernTab } from './components/ModernTab.svelte' export { default as IconAdd } from './components/icons/Add.svelte' export { default as IconCircleAdd } from './components/icons/CircleAdd.svelte' diff --git a/packages/ui/src/resize.ts b/packages/ui/src/resize.ts index 8701f8f51f..cb02763815 100644 --- a/packages/ui/src/resize.ts +++ b/packages/ui/src/resize.ts @@ -164,6 +164,11 @@ export const settingsSeparators: DefSeparators = [ { minSize: 19, size: 30, maxSize: 32, float: 'aside' } ] +export const mainSeparators: DefSeparators = [ + { minSize: 30, size: 'auto', maxSize: 'auto' }, + { minSize: 20, size: 30, maxSize: 45, float: 'sidebar' } +] + export const secondNavSeparators: DefSeparators = [{ minSize: 7, size: 7.5, maxSize: 15, float: 'navigator' }, null] export const separatorsStore = writable([]) diff --git a/plugins/activity-resources/src/components/Replies.svelte b/plugins/activity-resources/src/components/Replies.svelte index 96d5199375..71008d446d 100644 --- a/plugins/activity-resources/src/components/Replies.svelte +++ b/plugins/activity-resources/src/components/Replies.svelte @@ -84,7 +84,7 @@ if (replyProvider) { const fn = await getResource(replyProvider.function) - await fn(object) + await fn(object, e) } } diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts index a55c4047cb..bc087b7bb3 100644 --- a/plugins/activity/src/index.ts +++ b/plugins/activity/src/index.ts @@ -228,7 +228,7 @@ export interface SavedMessage extends Preference { } export interface ReplyProvider extends Doc { - function: Resource<(message: ActivityMessage) => Promise> + function: Resource<(message: ActivityMessage, event: MouseEvent) => Promise> } export interface UserMentionInfo extends AttachedDoc { diff --git a/plugins/calendar-resources/src/components/CalendarHeader.svelte b/plugins/calendar-resources/src/components/CalendarHeader.svelte new file mode 100644 index 0000000000..d3f2e94b00 --- /dev/null +++ b/plugins/calendar-resources/src/components/CalendarHeader.svelte @@ -0,0 +1,65 @@ + + + +
+
+ {monthName} + {currentDate.getFullYear()} +
+ +
+ + diff --git a/plugins/calendar-resources/src/components/CalendarNavigation.svelte b/plugins/calendar-resources/src/components/CalendarNavigation.svelte new file mode 100644 index 0000000000..03983c0b1f --- /dev/null +++ b/plugins/calendar-resources/src/components/CalendarNavigation.svelte @@ -0,0 +1,48 @@ + + + +
+ {#if ddItems.length > 1} + { + return { id: it.id, label: it.label, params: it.params } + })} + size={'medium'} + selected={ddItems.find((it) => it.mode === mode)?.id} + on:selected={(e) => (mode = ddItems.find((it) => it.id === e.detail)?.mode ?? ddItems[0].mode)} + /> + {/if} +
diff --git a/plugins/calendar-resources/src/components/CalendarView.svelte b/plugins/calendar-resources/src/components/CalendarView.svelte index 29e78bbb3f..f4c3432fab 100644 --- a/plugins/calendar-resources/src/components/CalendarView.svelte +++ b/plugins/calendar-resources/src/components/CalendarView.svelte @@ -29,19 +29,18 @@ import { createQuery } from '@hcengineering/presentation' import { AnyComponent, - Button, - DropdownLabelsIntl, - IconBack, - IconForward, MonthCalendar, YearCalendar, areDatesEqual, getMonday, - showPopup + showPopup, + AnySvelteComponent } from '@hcengineering/ui' + import { CalendarMode, DayCalendar, calendarByIdStore, hidePrivateEvents } from '../index' import calendar from '../plugin' import Day from './Day.svelte' + import CalendarHeader from './CalendarHeader.svelte' export let _class: Ref> = calendar.class.Event export let query: DocumentQuery | undefined = undefined @@ -55,6 +54,7 @@ CalendarMode.Month, CalendarMode.Year ] + export let headerComponent: AnySvelteComponent | undefined = undefined const me = getCurrentAccount() as PersonAccount @@ -236,7 +236,7 @@ const dragItemId = 'drag_item' as Ref - function dragEnter (e: CustomEvent) { + function dragEnter (e: CustomEvent): void { if (dragItem !== undefined) { const current = raw.find((p) => p._id === dragItemId) if (current !== undefined) { @@ -295,47 +295,43 @@ { id: 'month', label: calendar.string.ModeMonth, mode: CalendarMode.Month }, { id: 'year', label: calendar.string.ModeYear, mode: CalendarMode.Year } ] + + function handleToday (): void { + inc(0) + } + + function handleBack (): void { + inc(-1) + } + + function handleForward (): void { + inc(1) + } -
-
- {getMonthName(currentDate)} - {currentDate.getFullYear()} -
-
- {#if ddItems.length > 1} - { - return { id: it.id, label: it.label, params: it.params } - })} - size={'medium'} - selected={ddItems.find((it) => it.mode === mode)?.id} - on:selected={(e) => (mode = ddItems.find((it) => it.id === e.detail)?.mode ?? ddItems[0].mode)} - /> - {/if} -
-
- +{#if headerComponent} + +{:else} + +{/if} {#if mode === CalendarMode.Year} {/key} {/if} - - - diff --git a/plugins/calendar-resources/src/components/CalendarWidget.svelte b/plugins/calendar-resources/src/components/CalendarWidget.svelte new file mode 100644 index 0000000000..3e68b8e66a --- /dev/null +++ b/plugins/calendar-resources/src/components/CalendarWidget.svelte @@ -0,0 +1,20 @@ + + + + diff --git a/plugins/calendar-resources/src/components/CalendarWidgetHeader.svelte b/plugins/calendar-resources/src/components/CalendarWidgetHeader.svelte new file mode 100644 index 0000000000..a00fbb52d4 --- /dev/null +++ b/plugins/calendar-resources/src/components/CalendarWidgetHeader.svelte @@ -0,0 +1,61 @@ + + + +
+
+ +
+ + + + +
diff --git a/plugins/calendar-resources/src/index.ts b/plugins/calendar-resources/src/index.ts index ae446fad61..c43809a0b5 100644 --- a/plugins/calendar-resources/src/index.ts +++ b/plugins/calendar-resources/src/index.ts @@ -41,6 +41,7 @@ import EventReminders from './components/EventReminders.svelte' import VisibilityEditor from './components/VisibilityEditor.svelte' import CalendarSelector from './components/CalendarSelector.svelte' import ConnectApp from './components/ConnectApp.svelte' +import CalendarWidget from './components/CalendarWidget.svelte' import calendar from './plugin' import contact from '@hcengineering/contact' import { deleteObjects } from '@hcengineering/view-resources' @@ -189,7 +190,8 @@ export default async (): Promise => ({ CalendarIntegrationIcon, CalendarEventPresenter, IntegrationConfigure, - ConnectApp + ConnectApp, + CalendarWidget }, actionImpl: { SaveEventReminder: saveEventReminder, diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json index 369949aaff..93a64935d1 100644 --- a/plugins/chunter-assets/lang/en.json +++ b/plugins/chunter-assets/lang/en.json @@ -121,6 +121,7 @@ "JoinChannel": "Join channel", "YouJoinedChannel": "You have been joined to channel", "AndMore": "and {count} more", - "IsTyping": "{count, plural, =1 {is} other {are}} typing..." + "IsTyping": "{count, plural, =1 {is} other {are}} typing...", + "ThreadIn": "thread in {name}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/es.json b/plugins/chunter-assets/lang/es.json index 22dc858795..b01ab9555b 100644 --- a/plugins/chunter-assets/lang/es.json +++ b/plugins/chunter-assets/lang/es.json @@ -121,6 +121,7 @@ "JoinChannel": "Unirse", "YouJoinedChannel": "Te has unido al canal", "AndMore": "y {count} más", - "IsTyping": "está escribiendo..." + "IsTyping": "está escribiendo...", + "ThreadIn": "hilo en {name}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/fr.json b/plugins/chunter-assets/lang/fr.json index 17f41e3cd7..df3213a5f8 100644 --- a/plugins/chunter-assets/lang/fr.json +++ b/plugins/chunter-assets/lang/fr.json @@ -121,6 +121,7 @@ "JoinChannel": "Rejoindre", "YouJoinedChannel": "Vous avez rejoint le canal", "AndMore": "et {count} de plus", - "IsTyping": "est en train d'écrire..." + "IsTyping": "est en train d'écrire...", + "ThreadIn": "fil de discussion dans {name}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/pt.json b/plugins/chunter-assets/lang/pt.json index 0d658d13a2..fcc921fd47 100644 --- a/plugins/chunter-assets/lang/pt.json +++ b/plugins/chunter-assets/lang/pt.json @@ -121,6 +121,7 @@ "JoinChannel": "Participar no canal", "YouJoinedChannel": "Entrou no canal", "AndMore": "e mais {count}", - "IsTyping": "está a escrever..." + "IsTyping": "está a escrever...", + "ThreadIn": "Conversa em cadeia em {name}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json index ff275c5510..0d192d3808 100644 --- a/plugins/chunter-assets/lang/ru.json +++ b/plugins/chunter-assets/lang/ru.json @@ -121,6 +121,7 @@ "JoinChannel": "Приссоединение к каналу", "YouJoinedChannel": "Вы присоединились к каналу", "AndMore": "и еще {count}", - "IsTyping": "{count, plural, =1 {печатает} other {печатают}}..." + "IsTyping": "{count, plural, =1 {печатает} other {печатают}}...", + "ThreadIn": "Обсуждение в {name}" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/zh.json b/plugins/chunter-assets/lang/zh.json index 5365aaac63..72c31ff3df 100644 --- a/plugins/chunter-assets/lang/zh.json +++ b/plugins/chunter-assets/lang/zh.json @@ -121,6 +121,7 @@ "JoinChannel": "加入频道", "YouJoinedChannel": "你已加入频道", "AndMore": "和 {count} 人", - "IsTyping": "正在输入..." + "IsTyping": "正在输入...", + "ThreadIn": "线程在 {name}" } } diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 37dd22a669..66244cb07d 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -69,7 +69,7 @@ export class ChannelDataProvider implements IChannelDataProvider { private readonly tailQuery = createQuery(true) private readonly refsQuery = createQuery(true) - private chatId: Ref | undefined = undefined + chatId: Ref | undefined = undefined private readonly msgClass: Ref> private selectedMsgId: Ref | undefined = undefined private tailStart: Timestamp | undefined = undefined diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte index 1a3e973538..cf9f74ac77 100644 --- a/plugins/chunter-resources/src/components/Channel.svelte +++ b/plugins/chunter-resources/src/components/Channel.svelte @@ -29,6 +29,7 @@ export let context: DocNotifyContext | undefined export let filters: Ref[] = [] export let isAsideOpened = false + export let syncLocation = true const client = getClient() const hierarchy = client.getHierarchy() @@ -45,6 +46,9 @@ }) const unsubscribeLocation = locationStore.subscribe((newLocation) => { + if (!syncLocation) { + return + } const id = getMessageFromLoc(newLocation) selectedMessageId = id messageInFocus.set(id) diff --git a/plugins/chunter-resources/src/components/ChannelHeader.svelte b/plugins/chunter-resources/src/components/ChannelHeader.svelte index fde7820be1..60bb596db5 100644 --- a/plugins/chunter-resources/src/components/ChannelHeader.svelte +++ b/plugins/chunter-resources/src/components/ChannelHeader.svelte @@ -19,6 +19,7 @@ import { Channel } from '@hcengineering/chunter' import { ActivityMessagesFilter, WithReferences } from '@hcengineering/activity' import contact from '@hcengineering/contact' + import view from '@hcengineering/view' import Header from './Header.svelte' import chunter from '../plugin' @@ -31,6 +32,7 @@ export let allowClose: boolean = false export let canOpen: boolean = false export let withAside: boolean = false + export let withSearch: boolean = true export let isAsideShown: boolean = false export let filters: Ref[] = [] @@ -47,12 +49,13 @@ }) async function updateDescription (_id: Ref, _class: Ref>, object?: Doc): Promise { - if (hierarchy.isDerived(_class, chunter.class.DirectMessage)) { + if (hierarchy.isDerived(_class, chunter.class.DirectMessage) || hierarchy.isDerived(_class, contact.class.Person)) { description = undefined } else if (hierarchy.isDerived(_class, chunter.class.Channel)) { description = (object as Channel)?.topic } else { - description = await getDocTitle(client, _id, _class, object) + const hasId = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIdentifier) !== undefined + description = hasId ? await getDocTitle(client, _id, _class, object) : undefined } } @@ -74,6 +77,7 @@ {canOpen} {withAside} {isAsideShown} + {withSearch} on:aside-toggled on:close > diff --git a/plugins/chunter-resources/src/components/ChannelScrollView.svelte b/plugins/chunter-resources/src/components/ChannelScrollView.svelte index 5d5d8dbae8..c41d057f07 100644 --- a/plugins/chunter-resources/src/components/ChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ChannelScrollView.svelte @@ -61,6 +61,8 @@ export let skipLabels = false export let loadMoreAllowed = true export let isAsideOpened = false + export let initialScrollBottom = true + export let fullHeight = true const doc = object @@ -450,14 +452,22 @@ isInitialScrolling = false } else if (separatorIndex === -1) { await wait() - isScrollInitialized = true - shouldWaitAndRead = true - autoscroll = true - shouldScrollToNew = true - isInitialScrolling = false - waitLastMessageRenderAndRead(() => { + if (initialScrollBottom) { + isScrollInitialized = true + shouldWaitAndRead = true + autoscroll = true + shouldScrollToNew = true + isInitialScrolling = false + waitLastMessageRenderAndRead(() => { + autoscroll = false + }) + } else { + isScrollInitialized = true autoscroll = false - }) + updateShouldScrollToNew() + isInitialScrolling = false + readViewportMessages() + } } else if (separatorElement) { await wait() scrollToSeparator() @@ -552,7 +562,7 @@ } else if (dateToJump !== undefined) { await wait() scrollToDate(dateToJump) - } else if (newCount > messagesCount) { + } else if (messagesCount > 0 && newCount > messagesCount) { await wait() scrollToNewMessages() } @@ -566,7 +576,7 @@ return } - if (shouldScrollToNew) { + if (shouldScrollToNew && initialScrollBottom) { scrollToBottom() } @@ -703,7 +713,7 @@ {#if isLoading} {:else} -
+
{#if startFromBottom}
{/if} diff --git a/plugins/chunter-resources/src/components/ChannelSidebarView.svelte b/plugins/chunter-resources/src/components/ChannelSidebarView.svelte new file mode 100644 index 0000000000..ce725234c7 --- /dev/null +++ b/plugins/chunter-resources/src/components/ChannelSidebarView.svelte @@ -0,0 +1,113 @@ + + + +{#if object} + {#key object._id} +
+ + +
+ {/key} +{/if} +{#if threadId} +
+ closeThreadInSidebarChannel(widget, tab)} /> +
+{/if} + + diff --git a/plugins/chunter-resources/src/components/ChatWidget.svelte b/plugins/chunter-resources/src/components/ChatWidget.svelte new file mode 100644 index 0000000000..5051ce0f8a --- /dev/null +++ b/plugins/chunter-resources/src/components/ChatWidget.svelte @@ -0,0 +1,58 @@ + + + +{#if widget && tab && tab.type === 'channel'} + { + handleClose(tab?.id) + }} + /> +{:else if widget && tab && tab.type === 'thread'} + { + handleClose(tab?.id) + }} + /> +{/if} diff --git a/plugins/chunter-resources/src/components/ChatWidgetTab.svelte b/plugins/chunter-resources/src/components/ChatWidgetTab.svelte new file mode 100644 index 0000000000..ffc126d5e6 --- /dev/null +++ b/plugins/chunter-resources/src/components/ChatWidgetTab.svelte @@ -0,0 +1,107 @@ + + + + + + {#if count > 0} + + {/if} + + diff --git a/plugins/chunter-resources/src/components/DirectIcon.svelte b/plugins/chunter-resources/src/components/DirectIcon.svelte index fb09084b17..87ae03a0db 100644 --- a/plugins/chunter-resources/src/components/DirectIcon.svelte +++ b/plugins/chunter-resources/src/components/DirectIcon.svelte @@ -16,7 +16,7 @@ import { DirectMessage } from '@hcengineering/chunter' import contact, { Person, PersonAccount } from '@hcengineering/contact' import { Avatar, CombineAvatars, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' - import { Account, IdMap } from '@hcengineering/core' + import { Account, IdMap, Ref } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' import { Icon, IconSize } from '@hcengineering/ui' import { classIcon } from '@hcengineering/view-resources' @@ -25,14 +25,24 @@ import { getDmPersons } from '../utils' export let value: DirectMessage | undefined + export let _id: Ref | undefined = undefined export let size: IconSize = 'small' export let showStatus = false + export let compact = false const visiblePersons = 4 const client = getClient() let persons: Person[] = [] + if (_id !== undefined && value === undefined) { + void client.findOne(chunter.class.DirectMessage, { _id }).then((res) => { + value = res + }) + } else if (_id && value && value._id !== _id) { + value = undefined + } + $: if (value !== undefined) { void getDmPersons(client, value, $personByIdStore).then((res) => { persons = res @@ -50,8 +60,8 @@ } -{#if persons.length === 0 && value} - +{#if persons.length === 0} + {/if} {#if persons.length === 1} @@ -83,12 +93,18 @@ {/if} {#if persons.length > 1 && size !== 'medium'} - _id)} - size={avatarSize} - limit={visiblePersons} - /> + {#if compact} +
+ {persons.length} +
+ {:else} + _id)} + size={avatarSize} + limit={visiblePersons} + /> + {/if} {/if} diff --git a/plugins/chunter-resources/src/components/Header.svelte b/plugins/chunter-resources/src/components/Header.svelte index cba93cc4f1..fb25f6bbcd 100644 --- a/plugins/chunter-resources/src/components/Header.svelte +++ b/plugins/chunter-resources/src/components/Header.svelte @@ -49,6 +49,7 @@ export let isAsideShown: boolean = false export let titleKind: 'default' | 'breadcrumbs' = 'default' export let withFilters: boolean = false + export let withSearch: boolean = true export let filters: Ref[] = [] export let adaptive: HeaderAdaptive = 'default' export let hideActions: boolean = false @@ -110,21 +111,23 @@ - { - userSearch.set(ev.detail) + {#if withSearch} + { + userSearch.set(ev.detail) - if (ev.detail !== '') { - navigateToSpecial('chunterBrowser') - } - }} - /> - {#if withFilters} - + if (ev.detail !== '') { + navigateToSpecial('chunterBrowser') + } + }} + /> + {#if withFilters} + + {/if} + {/if} - diff --git a/plugins/chunter-resources/src/components/chat/Chat.svelte b/plugins/chunter-resources/src/components/chat/Chat.svelte index 48e5875328..5d846d944c 100644 --- a/plugins/chunter-resources/src/components/chat/Chat.svelte +++ b/plugins/chunter-resources/src/components/chat/Chat.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> + +{#if tab.data.thread} + {#key tab.data.thread} +
+ +
+ {/key} +{/if} + + diff --git a/plugins/chunter-resources/src/components/threads/ThreadView.svelte b/plugins/chunter-resources/src/components/threads/ThreadView.svelte index cf9b4713e1..258ec9e09c 100644 --- a/plugins/chunter-resources/src/components/threads/ThreadView.svelte +++ b/plugins/chunter-resources/src/components/threads/ThreadView.svelte @@ -16,10 +16,11 @@ import { Doc, Ref } from '@hcengineering/core' import { createQuery, getClient } from '@hcengineering/presentation' import { Breadcrumbs, Label, location as locationStore, Header } from '@hcengineering/ui' - import { onDestroy } from 'svelte' + import { createEventDispatcher, onDestroy } from 'svelte' import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity' import { getMessageFromLoc, messageInFocus } from '@hcengineering/activity-resources' import contact from '@hcengineering/contact' + import attachment from '@hcengineering/attachment' import chunter from '../../plugin' import ThreadParentMessage from './ThreadParentPresenter.svelte' @@ -33,6 +34,7 @@ const client = getClient() const hierarchy = client.getHierarchy() + const dispatch = createEventDispatcher() const messageQuery = createQuery() const channelQuery = createQuery() @@ -62,9 +64,24 @@ unsubscribeLocation() }) - $: messageQuery.query(activity.class.ActivityMessage, { _id }, (result: ActivityMessage[]) => { - message = result[0] as DisplayActivityMessage - }) + $: messageQuery.query( + activity.class.ActivityMessage, + { _id }, + (result: ActivityMessage[]) => { + message = result[0] as DisplayActivityMessage + + if (message === undefined) { + dispatch('close') + } + }, + { + lookup: { + _id: { + attachments: attachment.class.Attachment + } + } + } + ) $: message && channelQuery.query(message.attachedToClass, { _id: message.attachedTo }, (res) => { @@ -118,19 +135,28 @@
{#if message && dataProvider !== undefined} - +
-
- {#if message.replies && message.replies > 0} + + {#if message.replies && message.replies > 0} +
- {/if} -
-
+
+
+ {/if} {/if} diff --git a/plugins/chunter-resources/src/index.ts b/plugins/chunter-resources/src/index.ts index d437d398b6..a065242869 100644 --- a/plugins/chunter-resources/src/index.ts +++ b/plugins/chunter-resources/src/index.ts @@ -32,7 +32,6 @@ import ChatMessagePresenter from './components/chat-message/ChatMessagePresenter import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte' import ChatMessagesPresenter from './components/chat-message/ChatMessagesPresenter.svelte' import Chat from './components/chat/Chat.svelte' -import ChatAside from './components/chat/ChatAside.svelte' import CreateChannel from './components/chat/create/CreateChannel.svelte' import CreateDirectChat from './components/chat/create/CreateDirectChat.svelte' import ChunterBrowser from './components/chat/specials/ChunterBrowser.svelte' @@ -51,12 +50,18 @@ import ThreadParentPresenter from './components/threads/ThreadParentPresenter.sv import Threads from './components/threads/Threads.svelte' import ThreadView from './components/threads/ThreadView.svelte' import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte' +import ChatWidget from './components/ChatWidget.svelte' +import ChatWidgetTab from './components/ChatWidgetTab.svelte' import { chunterSpaceLinkFragmentProvider, + closeChatWidgetTab, getMessageLink, getMessageLocation, getThreadLink, + openChannelInSidebar, + openChannelInSidebarAction, + openChannelInSidebarTabAction, replyToThread } from './navigation' import { @@ -164,10 +169,11 @@ export default async (): Promise => ({ ChannelIcon, ChatMessageNotificationLabel, ThreadNotificationPresenter, - ChatAside, ThreadMessagePreview, ChatMessagePreview, - JoinChannelNotificationPresenter + JoinChannelNotificationPresenter, + ChatWidget, + ChatWidgetTab }, activity: { ChannelCreatedMessage, @@ -188,7 +194,9 @@ export default async (): Promise => ({ GetThreadLink: getThreadLink, ReplyToThread: replyToThread, CanReplyToThread: canReplyToThread, - GetMessageLink: getMessageLocation + GetMessageLink: getMessageLocation, + CloseChatWidgetTab: closeChatWidgetTab, + OpenChannelInSidebar: openChannelInSidebar }, actionImpl: { ArchiveChannel, @@ -197,6 +205,8 @@ export default async (): Promise => ({ DeleteChatMessage: deleteChatMessage, LeaveChannel: leaveChannelAction, RemoveChannel: removeChannelAction, - ReplyToThread: replyToThread + ReplyToThread: replyToThread, + OpenInSidebar: openChannelInSidebarAction, + OpenInSidebarTab: openChannelInSidebarTabAction } }) diff --git a/plugins/chunter-resources/src/navigation.ts b/plugins/chunter-resources/src/navigation.ts index 5dd14280c7..dcce6326ab 100644 --- a/plugins/chunter-resources/src/navigation.ts +++ b/plugins/chunter-resources/src/navigation.ts @@ -7,16 +7,27 @@ import { navigate } from '@hcengineering/ui' import { type Ref, type Doc, type Class } from '@hcengineering/core' -import type { ActivityMessage } from '@hcengineering/activity' -import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter' -import { notificationId } from '@hcengineering/notification' -import { workbenchId } from '@hcengineering/workbench' -import { getObjectLinkId } from '@hcengineering/view-resources' +import activity, { type ActivityMessage } from '@hcengineering/activity' +import { + type Channel, + type ChatWidgetTab, + chunterId, + type ChunterSpace, + type ThreadMessage +} from '@hcengineering/chunter' +import { type DocNotifyContext, notificationId } from '@hcengineering/notification' +import workbench, { type Widget, workbenchId } from '@hcengineering/workbench' +import { classIcon, getObjectLinkId } from '@hcengineering/view-resources' import { getClient } from '@hcengineering/presentation' import view, { encodeObjectURI, decodeObjectURI } from '@hcengineering/view' +import { createWidgetTab, isElementFromSidebar, sidebarStore } from '@hcengineering/workbench-resources' +import { type Asset, translate } from '@hcengineering/platform' +import contact from '@hcengineering/contact' +import { get } from 'svelte/store' import { chatSpecials } from './components/chat/utils' -import { isThreadMessage } from './utils' +import { getChannelName, isThreadMessage } from './utils' +import chunter from './plugin' export function openChannel (_id: string, _class: Ref>, thread?: Ref): void { const loc = getCurrentLocation() @@ -152,11 +163,23 @@ export async function getThreadLink (doc: ThreadMessage): Promise { return await buildThreadLink(loc, doc.objectId, doc.objectClass, doc.attachedTo, doc) } -export async function replyToThread (message: ActivityMessage): Promise { +export async function replyToThread (message: ActivityMessage, e: Event): Promise { + const fromSidebar = isElementFromSidebar(e.target as HTMLElement) const loc = getCurrentLocation() - if (loc.path[2] !== notificationId) { - loc.path[2] = chunterId + if (fromSidebar) { + const widget = getClient().getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0] + const widgetState = get(sidebarStore).widgetsState.get(widget._id) + const tab = widgetState?.tabs.find((it) => it?.data?._id === message.attachedTo) + if (tab !== undefined) { + void openThreadInSidebarChannel(widget, tab as ChatWidgetTab, message) + return + } + } + + void openThreadInSidebar(message._id, message) + if (loc.path[2] !== chunterId && loc.path[2] !== notificationId) { + return } const newLoc = await buildThreadLink(loc, message.attachedTo, message.attachedToClass, message._id) @@ -194,3 +217,141 @@ export async function resetChunterLocIfEqual (_id: Ref, _class: Ref, + _class: Ref>, + doc?: Doc, + thread?: Ref, + newTab = true +): Promise { + const client = getClient() + + const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0] + if (widget === undefined) return + + const object = doc ?? (await client.findOne(_class, { _id })) + if (object === undefined) return + + const titleIntl = client.getHierarchy().getClass(object._class).label + const hierarchy = client.getHierarchy() + const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon) + const isPerson = hierarchy.isDerived(_class, contact.class.Person) + const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage) + const isChannel = hierarchy.isDerived(_class, chunter.class.Channel) + const name = (await getChannelName(_id, _class, object)) ?? (await translate(titleIntl, {})) + + const tab: ChatWidgetTab = { + id: `chunter_${_id}`, + name, + icon: getChannelClassIcon(object), + iconComponent: isChannel ? undefined : iconMixin?.component, + iconProps: { + _id: object._id, + size: isDirect || isPerson ? 'tiny' : 'x-small', + compact: true + }, + type: 'channel', + data: { + _id, + _class, + thread + } + } + + createWidgetTab(widget, tab, newTab) +} + +export async function openChannelInSidebarAction ( + context: DocNotifyContext, + _: Event, + props?: { object?: Doc, newTab?: boolean } +): Promise { + await openChannelInSidebar(context.objectId, context.objectClass, props?.object, undefined, props?.newTab ?? false) +} + +export async function openChannelInSidebarTabAction ( + context: DocNotifyContext, + event: Event, + props?: { object?: Doc } +): Promise { + await openChannelInSidebarAction(context, event, { newTab: true, object: props?.object }) +} + +export async function openThreadInSidebarChannel ( + widget: Widget, + tab: ChatWidgetTab, + message: ActivityMessage +): Promise { + const newTab: ChatWidgetTab = { + ...tab, + data: { ...tab.data, thread: message._id } + } + createWidgetTab(widget, newTab) +} + +export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise { + const newTab: ChatWidgetTab = { + ...tab, + data: { ...tab.data, thread: undefined } + } + + createWidgetTab(widget, newTab) +} + +export async function openThreadInSidebar (_id: Ref, msg?: ActivityMessage, doc?: Doc): Promise { + const client = getClient() + + const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0] + if (widget === undefined) return + + const message = msg ?? (await client.findOne(activity.class.ActivityMessage, { _id })) + if (message === undefined) return + + const object = doc ?? (await client.findOne(message.attachedToClass, { _id: message.attachedTo })) + if (object === undefined) return + + const titleIntl = client.getHierarchy().getClass(object._class).label + const name = (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})) + const tabName = await translate(chunter.string.ThreadIn, { name }) + const loc = getCurrentLocation() + + const tab: ChatWidgetTab = { + id: 'thread_' + _id, + name: tabName, + icon: chunter.icon.Thread, + allowedPath: loc.path.join('/'), + type: 'thread', + data: { + _id: object?._id, + _class: object?._class, + thread: message._id + } + } + createWidgetTab(widget, tab, true) +} + +export function closeChatWidgetTab (tab?: ChatWidgetTab): void { + if (tab?.type === 'thread') { + const loc = getCurrentLocation() + + if (loc.path[2] === chunterId || loc.path[2] === notificationId) { + if (loc.path[4] === tab.data.thread) { + loc.path[4] = '' + loc.path.length = 4 + navigate(loc) + } + } + } +} diff --git a/plugins/chunter/package.json b/plugins/chunter/package.json index 03c1148b9d..51bcc3f0e3 100644 --- a/plugins/chunter/package.json +++ b/plugins/chunter/package.json @@ -44,6 +44,7 @@ "@hcengineering/platform": "^0.6.11", "@hcengineering/ui": "^0.6.15", "@hcengineering/view": "^0.6.13", + "@hcengineering/workbench": "^0.6.16", "fast-equals": "^5.0.1" }, "repository": "https://github.com/hcengineering/platform", diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 6f63376819..b67b9ab41c 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -21,6 +21,7 @@ import { IntlString, plugin } from '@hcengineering/platform' import { AnyComponent } from '@hcengineering/ui' import { Action } from '@hcengineering/view' import { Person, ChannelProvider as SocialChannelProvider } from '@hcengineering/contact' +import { Widget, WidgetTab } from '@hcengineering/workbench' /** * @public @@ -98,6 +99,11 @@ export interface InlineButton extends AttachedDoc { action: Resource } +export interface ChatWidgetTab extends WidgetTab { + type: 'channel' | 'thread' + data: { _id?: Ref, _class?: Ref>, thread?: Ref } +} + /** * @public */ @@ -127,7 +133,6 @@ export default plugin(chunterId, { ChatMessagesPresenter: '' as AnyComponent, ChatMessagePresenter: '' as AnyComponent, ThreadMessagePresenter: '' as AnyComponent, - ChatAside: '' as AnyComponent, ChatMessagePreview: '' as AnyComponent, ThreadMessagePreview: '' as AnyComponent }, @@ -199,14 +204,16 @@ export default plugin(chunterId, { CreatedChannelOn: '' as IntlString, YouJoinedChannel: '' as IntlString, AndMore: '' as IntlString, - IsTyping: '' as IntlString + IsTyping: '' as IntlString, + ThreadIn: '' as IntlString }, ids: { DMNotification: '' as Ref, ThreadNotification: '' as Ref, ChannelNotification: '' as Ref, JoinChannelNotification: '' as Ref, - ThreadMessageViewlet: '' as Ref + ThreadMessageViewlet: '' as Ref, + ChatWidget: '' as Ref }, app: { Chunter: '' as Ref @@ -216,5 +223,10 @@ export default plugin(chunterId, { LeaveChannel: '' as Ref, RemoveChannel: '' as Ref, CloseConversation: '' as Ref + }, + function: { + OpenChannelInSidebar: '' as Resource< + (_id: Ref, _class: Ref, doc?: Doc, thread?: Ref) => Promise + > } }) diff --git a/plugins/love-resources/package.json b/plugins/love-resources/package.json index be54cfb38b..5b24014017 100644 --- a/plugins/love-resources/package.json +++ b/plugins/love-resources/package.json @@ -37,22 +37,24 @@ "svelte-eslint-parser": "^0.33.1" }, "dependencies": { - "@hcengineering/platform": "^0.6.11", - "svelte": "^4.2.12", - "@hcengineering/ui": "^0.6.15", + "@hcengineering/analytics": "^0.6.0", "@hcengineering/calendar": "^0.6.24", "@hcengineering/contact": "^0.6.24", "@hcengineering/contact-resources": "^0.6.0", - "@hcengineering/view-resources": "^0.6.0", - "@hcengineering/analytics": "^0.6.0", - "@hcengineering/presentation": "^0.6.3", "@hcengineering/core": "^0.6.32", "@hcengineering/login": "^0.6.12", - "@hcengineering/panel": "^0.6.23", "@hcengineering/love": "^0.6.0", - "livekit-client": "^2.0.10", + "@hcengineering/panel": "^0.6.23", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/presentation": "^0.6.3", + "@hcengineering/ui": "^0.6.15", + "@hcengineering/view": "^0.6.13", + "@hcengineering/view-resources": "^0.6.0", + "@hcengineering/workbench": "^0.6.16", + "@hcengineering/workbench-resources": "^0.6.1", "@livekit/krisp-noise-filter": "~0.2.1", "@livekit/track-processors": "~0.3.1", - "@hcengineering/view": "^0.6.13" + "livekit-client": "^2.0.10", + "svelte": "^4.2.12" } } diff --git a/plugins/love-resources/src/components/ControlExt.svelte b/plugins/love-resources/src/components/ControlExt.svelte index 5cf23602a9..ae5c1d3e8a 100644 --- a/plugins/love-resources/src/components/ControlExt.svelte +++ b/plugins/love-resources/src/components/ControlExt.svelte @@ -13,9 +13,9 @@ // limitations under the License. --> + +
+ {#if floorsSelector} + {#each $floors as floor, i} +
- { - dispatch('dock') - }} - /> + {#if canUnpin} + { + dispatch('dock') + }} + /> + {/if} {#if allowLeave} + + +{#if room} +
+ +
+{/if} + + diff --git a/plugins/love-resources/src/index.ts b/plugins/love-resources/src/index.ts index 3f57a19ab3..9b7d7f6595 100644 --- a/plugins/love-resources/src/index.ts +++ b/plugins/love-resources/src/index.ts @@ -6,6 +6,8 @@ import MeetingData from './components/MeetingData.svelte' import SelectScreenSourcePopup from './components/SelectScreenSourcePopup.svelte' import Settings from './components/Settings.svelte' import WorkbenchExtension from './components/WorkbenchExtension.svelte' +import LoveWidget from './components/LoveWidget.svelte' +import VideoWidget from './components/VideoWidget.svelte' import { createMeeting, toggleMic, toggleVideo } from './utils' export { setCustomCreateScreenTracks } from './utils' @@ -18,7 +20,9 @@ export default async (): Promise => ({ WorkbenchExtension, SelectScreenSourcePopup, MeetingData, - EditMeetingData + EditMeetingData, + LoveWidget, + VideoWidget }, function: { CreateMeeting: createMeeting diff --git a/plugins/love/package.json b/plugins/love/package.json index d3e111ac8c..7285b10653 100644 --- a/plugins/love/package.json +++ b/plugins/love/package.json @@ -46,6 +46,7 @@ "@hcengineering/calendar": "^0.6.24", "@hcengineering/drive": "^0.6.0", "@hcengineering/core": "^0.6.32", - "@hcengineering/view": "^0.6.13" + "@hcengineering/view": "^0.6.13", + "@hcengineering/workbench": "^0.6.16" } } diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts index 4a05ee0707..caf6cc780c 100644 --- a/plugins/love/src/index.ts +++ b/plugins/love/src/index.ts @@ -7,6 +7,7 @@ import { Asset, IntlString, Metadata, Plugin, plugin } from '@hcengineering/plat import { Preference } from '@hcengineering/preference' import { AnyComponent } from '@hcengineering/ui/src/types' import { Action } from '@hcengineering/view' +import { Widget } from '@hcengineering/workbench' export const loveId = 'love' as Plugin export type { ScreenSource } from './utils' @@ -128,7 +129,9 @@ const love = plugin(loveId, { MainFloor: '' as Ref, Reception: '' as Ref, InviteNotification: '' as Ref, - KnockNotification: '' as Ref + KnockNotification: '' as Ref, + LoveWidget: '' as Ref, + VideoWidget: '' as Ref }, icon: { Love: '' as Asset, diff --git a/plugins/notification-resources/package.json b/plugins/notification-resources/package.json index 307b9aa804..e3c3d971fc 100644 --- a/plugins/notification-resources/package.json +++ b/plugins/notification-resources/package.json @@ -38,9 +38,9 @@ "typescript": "^5.3.3" }, "dependencies": { - "@hcengineering/analytics": "^0.6.0", "@hcengineering/activity": "^0.6.0", "@hcengineering/activity-resources": "^0.6.1", + "@hcengineering/analytics": "^0.6.0", "@hcengineering/attachment": "^0.6.14", "@hcengineering/attachment-resources": "^0.6.0", "@hcengineering/chunter": "^0.6.20", diff --git a/plugins/notification-resources/src/components/Notification.svelte b/plugins/notification-resources/src/components/Notification.svelte index 1315da8ad8..e76f6c5ae8 100644 --- a/plugins/notification-resources/src/components/Notification.svelte +++ b/plugins/notification-resources/src/components/Notification.svelte @@ -3,8 +3,12 @@ import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' import { Ref } from '@hcengineering/core' import { BrowserNotification } from '@hcengineering/notification' - import { Button, Notification as PlatformNotification, NotificationToast, navigate } from '@hcengineering/ui' - import view from '@hcengineering/view' + import { Button, navigate, Notification as PlatformNotification, NotificationToast } from '@hcengineering/ui' + import view, { decodeObjectURI } from '@hcengineering/view' + import chunter from '@hcengineering/chunter' + import { getResource } from '@hcengineering/platform' + import { ActivityMessage } from '@hcengineering/activity' + import { pushAvailable, subscribePush } from '../utils' import plugin from '../plugin' @@ -16,6 +20,23 @@ $: senderAccount = value.senderId !== undefined ? $personAccountByIdStore.get(value.senderId as Ref) : undefined $: sender = senderAccount !== undefined ? $personByIdStore.get(senderAccount.person) : undefined + + async function openChannelInSidebar (): Promise { + if (!value.onClickLocation) return + const { onClickLocation } = value + const [_id, _class] = decodeObjectURI(onClickLocation.path[3] ?? '') + + onRemove() + + if (!_id || !_class || _id === '' || _class === '') { + navigate(onClickLocation) + return + } + + const thread = onClickLocation.path[4] as Ref | undefined + const fn = await getResource(chunter.function.OpenChannelInSidebar) + await fn(_id, _class, undefined, thread) + } @@ -35,10 +56,7 @@
+ {#if $sidebarStore.variant === SidebarVariant.EXPANDED} + + {/if} +
diff --git a/plugins/workbench-resources/src/components/sidebar/Sidebar.svelte b/plugins/workbench-resources/src/components/sidebar/Sidebar.svelte new file mode 100644 index 0000000000..d3da1373ef --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/Sidebar.svelte @@ -0,0 +1,70 @@ + + + + + + diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte new file mode 100644 index 0000000000..b6d6c9deb8 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte @@ -0,0 +1,172 @@ + + + +
+
+ {#if widget?.component} +
+ {#if widget.headerLabel} +
{ + if (widget !== undefined) { + closeWidget(widget._id) + } + }} + > + +
+ {/if} + { + if (widget !== undefined) { + closeWidget(widget._id) + } + }} + /> +
+ {/if} +
+ {#if widget !== undefined && tabs.length > 0} + { + void handleTabClose(e.detail, widget) + }} + on:open={(e) => { + handleTabOpen(e.detail, widget) + }} + /> + {/if} + +
+ + diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarMini.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarMini.svelte new file mode 100644 index 0000000000..24acec8ce5 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/SidebarMini.svelte @@ -0,0 +1,26 @@ + + + + diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte new file mode 100644 index 0000000000..1c8cd358ae --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/SidebarTab.svelte @@ -0,0 +1,57 @@ + + + + diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarTabs.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarTabs.svelte new file mode 100644 index 0000000000..c01dfd1454 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/SidebarTabs.svelte @@ -0,0 +1,99 @@ + + + +
+ {#each tabs as tab} + {#if widget.tabComponent} + dispatch('close', tab.id)} + on:click={() => { + dispatch('open', tab.id) + }} + /> + {:else} + dispatch('close', tab.id)} + on:click={() => { + dispatch('open', tab.id) + }} + /> + {/if} + {/each} +
+ + diff --git a/plugins/workbench-resources/src/components/sidebar/widgets/AddWidgetsPopup.svelte b/plugins/workbench-resources/src/components/sidebar/widgets/AddWidgetsPopup.svelte new file mode 100644 index 0000000000..10be660ea9 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/widgets/AddWidgetsPopup.svelte @@ -0,0 +1,104 @@ + + + + + + {#each widgets as widget} + {#if widget.type === WidgetType.Configurable} + {@const preference = preferences.find((it) => it.attachedTo === widget._id)} +
+ { + handleCheck(widget, preference) + }} + /> + +
+ {/if} + {/each} +
+
+ + diff --git a/plugins/workbench-resources/src/components/sidebar/widgets/WidgetPresenter.svelte b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetPresenter.svelte new file mode 100644 index 0000000000..bb34f89a56 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetPresenter.svelte @@ -0,0 +1,38 @@ + + + +
+ + {#if withLabel} +
+ + diff --git a/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte new file mode 100644 index 0000000000..8635252380 --- /dev/null +++ b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte @@ -0,0 +1,120 @@ + + + +
+
+ {#each fixedWidgets as widget} + { + handleSelectWidget(widget) + }} + /> + {/each} + {#if configurableWidgets.length > 0} +
+ {#each configurableWidgets as widget} + { + handleSelectWidget(widget) + }} + /> + {/each} + {/if} + {#if flexibleWidgets.length > 0} +
+ {#each flexibleWidgets as widget} + { + handleSelectWidget(widget) + }} + /> + {/each} + {/if} + + {#if widgets.some((widget) => widget.type === WidgetType.Configurable)} +
+ + {/if} +
+
+ + diff --git a/plugins/workbench-resources/src/index.ts b/plugins/workbench-resources/src/index.ts index 39a11afdf6..7d5abf79a8 100644 --- a/plugins/workbench-resources/src/index.ts +++ b/plugins/workbench-resources/src/index.ts @@ -37,6 +37,7 @@ export { default as TreeSeparator } from './components/navigator/TreeSeparator.s export { SpecialView } export * from './utils' +export * from './sidebar' export default async (): Promise => ({ component: { WorkbenchApp, diff --git a/plugins/workbench-resources/src/plugin.ts b/plugins/workbench-resources/src/plugin.ts index 18d6910b45..8f540733d7 100644 --- a/plugins/workbench-resources/src/plugin.ts +++ b/plugins/workbench-resources/src/plugin.ts @@ -45,7 +45,9 @@ export default mergeIds(workbenchId, workbench, { MobileNotSupported: '' as IntlString, LogInAnyway: '' as IntlString, WorkspaceCreating: '' as IntlString, - AccessDenied: '' as IntlString + AccessDenied: '' as IntlString, + Widget: '' as IntlString, + WidgetPreference: '' as IntlString }, metadata: { MobileAllowed: '' as Metadata diff --git a/plugins/workbench-resources/src/sidebar.ts b/plugins/workbench-resources/src/sidebar.ts new file mode 100644 index 0000000000..067896011a --- /dev/null +++ b/plugins/workbench-resources/src/sidebar.ts @@ -0,0 +1,294 @@ +// +// 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. +// +import { type Widget, type WidgetTab } from '@hcengineering/workbench' +import { getCurrentAccount, type Ref } from '@hcengineering/core' +import { get, writable } from 'svelte/store' +import { getCurrentLocation } from '@hcengineering/ui' +import { getResource } from '@hcengineering/platform' + +import { workspaceStore } from './utils' + +export enum SidebarVariant { + MINI = 'mini', + EXPANDED = 'expanded' +} + +export interface WidgetState { + _id: Ref + data?: Record + tabs: WidgetTab[] + tab?: string +} + +export interface SidebarState { + variant: SidebarVariant + widgetsState: Map, WidgetState> + widget?: Ref +} + +export const defaultSidebarState: SidebarState = { + variant: SidebarVariant.MINI, + widgetsState: new Map() +} + +export const sidebarStore = writable(defaultSidebarState) + +workspaceStore.subscribe((workspace) => { + sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? '')) +}) + +sidebarStore.subscribe(setSidebarStateToLocalStorage) + +export function syncSidebarState (): void { + const workspace = get(workspaceStore) + sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? '')) +} +function getSideBarLocalStorageKey (workspace: string): string | undefined { + const me = getCurrentAccount() + if (me == null || workspace === '') return undefined + return `workbench.${workspace}.${me.person}.sidebar.state.` +} + +function getSidebarStateFromLocalStorage (workspace: string): SidebarState { + const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace) + if (sidebarStateLocalStorageKey === undefined) return defaultSidebarState + const state = window.localStorage.getItem(sidebarStateLocalStorageKey) + + if (state == null || state === '') return defaultSidebarState + + try { + const parsed = JSON.parse(state) + + return { + ...defaultSidebarState, + ...parsed, + widgetsState: new Map(Object.entries(parsed.widgetsState ?? {})) + } + } catch (e) { + console.error(e) + setSidebarStateToLocalStorage(defaultSidebarState) + return defaultSidebarState + } +} + +function setSidebarStateToLocalStorage (state: SidebarState): void { + const workspace = get(workspaceStore) + if (workspace == null || workspace === '') return + + const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace) + if (sidebarStateLocalStorageKey === undefined) return + window.localStorage.setItem( + sidebarStateLocalStorageKey, + JSON.stringify({ ...state, widgetsState: Object.fromEntries(state.widgetsState.entries()) }) + ) +} + +export function openWidget (widget: Widget, data?: Record): void { + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget._id) + + widgetsState.set(widget._id, { _id: widget._id, data, tab: widgetState?.tab, tabs: widgetState?.tabs ?? [] }) + + sidebarStore.set({ + ...state, + widgetsState, + variant: SidebarVariant.EXPANDED, + widget: widget._id + }) +} + +export function closeWidget (widget: Ref): void { + const state = get(sidebarStore) + const { widgetsState } = state + + widgetsState.delete(widget) + + if (state.widget === widget) { + sidebarStore.set({ + ...state, + widgetsState, + variant: SidebarVariant.MINI, + widget: undefined + }) + } else { + sidebarStore.set({ + ...state, + widgetsState + }) + } +} + +export async function closeWidgetTab (widget: Widget, tab: string): Promise { + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget._id) + + if (widgetState === undefined) return + + const tabs = widgetState.tabs + const newTabs = tabs.filter((it) => it.id !== tab) + const closedTab = tabs.find((it) => it.id === tab) + + if (widget.onTabClose !== undefined && closedTab !== undefined) { + const fn = await getResource(widget.onTabClose) + void fn(closedTab) + } + + if (newTabs.length === 0) { + if (widget.closeIfNoTabs === true) { + widgetsState.delete(widget._id) + sidebarStore.set({ ...state, widgetsState, variant: SidebarVariant.MINI }) + } else { + widgetsState.set(widget._id, { ...widgetState, tabs: [], tab: undefined }) + sidebarStore.set({ ...state, widgetsState }) + } + return + } + + const shouldReplace = widgetState.tab === tab + + if (!shouldReplace) { + widgetsState.set(widget._id, { ...widgetState, tabs: newTabs }) + } else { + const index = tabs.findIndex((it) => it.id === widgetState.tab) + const newTab = index === -1 ? newTabs[0] : tabs[index + 1] ?? tabs[index - 1] ?? newTabs[0] + + widgetsState.set(widget._id, { ...widgetState, tabs: newTabs, tab: newTab.id }) + } + + sidebarStore.set({ + ...state, + ...widgetsState + }) +} + +export function openWidgetTab (widget: Ref, tab: string): void { + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget) + + if (widgetState === undefined) return + + const newTab = widgetState.tabs.find((it) => it.id === tab) + if (newTab === undefined) return + + widgetsState.set(widget, { ...widgetState, tab }) + sidebarStore.set({ + ...state, + widgetsState + }) +} + +export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false): void { + openWidget(widget) + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget._id) + const currentTabs = widgetState?.tabs ?? [] + const opened = currentTabs.some(({ id }) => id === tab.id) ?? false + + let newTabs: WidgetTab[] + + if (opened) { + newTabs = currentTabs.map((it) => (it.id === tab.id ? { ...tab, isPinned: it.isPinned } : it)) + } else if (newTab || currentTabs.length === 0) { + newTabs = [...currentTabs, tab] + } else { + const current = + currentTabs.find(({ id }) => id === widgetState?.tab) ?? currentTabs.find(({ isPinned }) => isPinned === false) + const shouldReplace = current !== undefined && current.isPinned !== true + + newTabs = shouldReplace ? currentTabs.map((it) => (it.id === current?.id ? tab : it)) : [...currentTabs, tab] + } + + widgetsState.set(widget._id, { + _id: widget._id, + tabs: newTabs, + tab: tab.id + }) + + sidebarStore.set({ + ...state, + widgetsState + }) +} + +export function pinWidgetTab (widget: Widget, tabId: string): void { + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget._id) + + if (widgetState === undefined) return + + const tabs = widgetState.tabs + .map((it) => (it.id === tabId ? { ...it, isPinned: true } : it)) + .sort((a, b) => (a.isPinned === b.isPinned ? 0 : a.isPinned === true ? -1 : 1)) + + widgetsState.set(widget._id, { ...widgetState, tabs }) + + sidebarStore.set({ + ...state, + widgetsState + }) +} + +export function unpinWidgetTab (widget: Widget, tabId: string): void { + const state = get(sidebarStore) + const { widgetsState } = state + const widgetState = widgetsState.get(widget._id) + + if (widgetState === undefined) return + const tab = widgetState.tabs.find((it) => it.id === tabId) + + if (tab?.allowedPath !== undefined) { + const loc = getCurrentLocation() + const path = loc.path.join('/') + if (!path.startsWith(tab.allowedPath)) { + void closeWidgetTab(widget, tabId) + } + } + + const tabs = widgetState.tabs + .map((it) => (it.id === tabId ? { ...it, isPinned: false } : it)) + .sort((a, b) => (a.isPinned === b.isPinned ? 0 : a.isPinned === true ? -1 : 1)) + + widgetsState.set(widget._id, { ...widgetState, tabs }) + + sidebarStore.set({ + ...state, + widgetsState + }) +} + +function isDescendant (parent: HTMLElement, child: HTMLElement): boolean { + let node = child.parentNode + while (node != null) { + if (node === parent) { + return true + } + node = node.parentNode + } + return false +} + +export function isElementFromSidebar (element: HTMLElement): boolean { + const sidebarElement = document.getElementById('sidebar') + if (sidebarElement == null) { + return false + } + + return isDescendant(sidebarElement, element) +} diff --git a/plugins/workbench-resources/src/utils.ts b/plugins/workbench-resources/src/utils.ts index 30a676e9f1..d00a88a0d4 100644 --- a/plugins/workbench-resources/src/utils.ts +++ b/plugins/workbench-resources/src/utils.ts @@ -25,12 +25,14 @@ import { closePanel, fetchMetadataLocalStorage, getCurrentLocation, + type Location, navigate, - setMetadataLocalStorage + setMetadataLocalStorage, + location } from '@hcengineering/ui' import view from '@hcengineering/view' import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench' -import { writable } from 'svelte/store' +import { derived, writable } from 'svelte/store' export const workspaceCreating = writable(undefined) @@ -155,6 +157,7 @@ export async function showApplication (app: Application): Promise { } export const workspacesStore = writable([]) +export const workspaceStore = derived(location, (loc: Location) => loc.path[1]) /** * @public diff --git a/plugins/workbench/src/index.ts b/plugins/workbench/src/index.ts index 113f9e6809..a89c45a336 100644 --- a/plugins/workbench/src/index.ts +++ b/plugins/workbench/src/index.ts @@ -18,7 +18,13 @@ import { DocNotifyContext, InboxNotification } from '@hcengineering/notification import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import type { Preference } from '@hcengineering/preference' -import { AnyComponent, ComponentExtensionId, Location, ResolvedLocation } from '@hcengineering/ui' +import { + AnyComponent, + type AnySvelteComponent, + ComponentExtensionId, + Location, + ResolvedLocation +} from '@hcengineering/ui' import { ViewAction } from '@hcengineering/view' /** @@ -45,6 +51,43 @@ export interface Application extends Doc { navFooterComponent?: AnyComponent } +export enum WidgetType { + Fixed = 'fixed', // Fixed sidebar are always visible + Flexible = 'flexible', // Flexible sidebar are visible only in special cases (during the meeting, etc.) + Configurable = 'configurable ' // Configurable might be fixed in sidebar by user in preferences +} + +export interface Widget extends Doc { + label: IntlString + icon: Asset + type: WidgetType + size?: 'small' | 'medium' + + component: AnyComponent + tabComponent?: AnyComponent + headerLabel?: IntlString + + closeIfNoTabs?: boolean + onTabClose?: Resource<(tab: WidgetTab) => Promise> +} + +export interface WidgetPreference extends Preference { + enabled: boolean +} + +export interface WidgetTab { + id: string + name?: string + nameIntl?: IntlString + icon?: Asset | AnySvelteComponent + iconComponent?: AnyComponent + iconProps?: Record + widget?: Ref + isPinned?: boolean + allowedPath?: string + data?: Record +} + /** * @public */ @@ -141,7 +184,9 @@ export default plugin(workbenchId, { class: { Application: '' as Ref>, ApplicationNavModel: '' as Ref>, - HiddenApplication: '' as Ref> + HiddenApplication: '' as Ref>, + Widget: '' as Ref>, + WidgetPreference: '' as Ref> }, mixin: { SpaceView: '' as Ref> @@ -156,7 +201,10 @@ export default plugin(workbenchId, { Archive: '' as IntlString, View: '' as IntlString, ServerUnderMaintenance: '' as IntlString, - UpgradeDownloadProgress: '' as IntlString + UpgradeDownloadProgress: '' as IntlString, + OpenInSidebar: '' as IntlString, + OpenInSidebarNewTab: '' as IntlString, + ConfigureWidgets: '' as IntlString }, icon: { Search: '' as Asset diff --git a/tests/sanity/tests/chat/chat.spec.ts b/tests/sanity/tests/chat/chat.spec.ts index 08db15f311..006ae50416 100644 --- a/tests/sanity/tests/chat/chat.spec.ts +++ b/tests/sanity/tests/chat/chat.spec.ts @@ -257,14 +257,14 @@ test.describe('Channel tests', () => { await channelPage.checkIfMessageExist(true, 'Test message') }) - test('Check if user can pin message', async () => { + test('Check if user can reply message', async () => { await leftSideMenuPage.clickChunter() await channelPage.clickChannel('random') await channelPage.sendMessage('Test message') await channelPage.replyToMessage('Test message', 'Reply message') - await channelPage.checkIfMessageExist(true, 'Reply message') + await channelPage.checkIfMessageExistInSidebar(true, 'Reply message') await channelPage.closeAndOpenReplyMessage() - await channelPage.checkIfMessageExist(true, 'Reply message') + await channelPage.checkIfMessageExistInSidebar(true, 'Reply message') }) test('Check if user can edit message', async ({ page }) => { @@ -498,13 +498,6 @@ test.describe('Channel tests', () => { await channelPage.clickChannelTab() }) - await test.step('Search channel by first 3 char and find nothing', async () => { - await channelPage.searchChannel(data.channelName.slice(0, 3)) - await channelPage.page.keyboard.press('Enter') - await channelPage.checkIfChannelTableExist(data.channelName, false) - await channelPage.checkIfChannelTableExist('general', false) - }) - await test.step('Search channel by fillName and find channel', async () => { await channelPage.searchChannel(data.channelName) await channelPage.page.keyboard.press('Enter') diff --git a/tests/sanity/tests/model/channel-page.ts b/tests/sanity/tests/model/channel-page.ts index 36df33e231..31a52c154f 100644 --- a/tests/sanity/tests/model/channel-page.ts +++ b/tests/sanity/tests/model/channel-page.ts @@ -15,6 +15,9 @@ export class ChannelPage extends CommonPage { readonly textMessage = (messageText: string): Locator => this.page.locator('.hulyComponent .activityMessage', { hasText: messageText }) + readonly textMessageInSidebar = (messageText: string): Locator => + this.page.locator('#sidebar .activityMessage', { hasText: messageText }) + readonly channelName = (channel: string): Locator => this.page.getByText('general random').getByText(channel) readonly channelTab = (): Locator => this.page.getByRole('link', { name: 'Channels' }).getByRole('button') readonly channelTable = (): Locator => this.page.getByRole('table') @@ -280,6 +283,14 @@ export class ChannelPage extends CommonPage { } } + async checkIfMessageExistInSidebar (messageExists: boolean, messageText: string): Promise { + if (messageExists) { + await expect(this.textMessageInSidebar(messageText)).toBeVisible() + } else { + await expect(this.textMessageInSidebar(messageText)).toBeHidden() + } + } + async checkIfEmojiIsAdded (emoji: string): Promise { await expect(this.selectEmoji(emoji + ' 1')).toBeVisible() } diff --git a/tests/sanity/tests/model/planning/planning-page.ts b/tests/sanity/tests/model/planning/planning-page.ts index 4b077db24d..c7f89e2d1f 100644 --- a/tests/sanity/tests/model/planning/planning-page.ts +++ b/tests/sanity/tests/model/planning/planning-page.ts @@ -232,19 +232,15 @@ export class PlanningPage extends CalendarPage { } async openToDoByName (toDoName: string): Promise { - await this.page.locator('button.hulyToDoLine-container div[class$="overflow-label"]', { hasText: toDoName }).click() + await this.page.locator(`button.hulyToDoLine-container:has-text("${toDoName}")`).click() } async checkToDoNotExist (toDoName: string): Promise { - await expect( - this.page.locator('button.hulyToDoLine-container div[class$="overflow-label"]', { hasText: toDoName }) - ).toHaveCount(0) + await expect(this.page.locator(`button.hulyToDoLine-container:has-text("${toDoName}")`)).toHaveCount(0) } async checkToDoExist (toDoName: string): Promise { - await expect( - this.page.locator('button.hulyToDoLine-container div[class$="overflow-label"]', { hasText: toDoName }) - ).toHaveCount(1) + await expect(this.page.locator(`button.hulyToDoLine-container:has-text("${toDoName}")`)).toHaveCount(1) } async checkToDo (data: NewToDo): Promise {