mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Add widgets sidebar (#6578)
This commit is contained in:
parent
d38df9c600
commit
a1cee24473
@ -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
|
||||
})
|
||||
|
@ -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<Action>,
|
||||
@ -66,6 +68,7 @@ export default mergeIds(calendarId, calendar, {
|
||||
},
|
||||
ids: {
|
||||
UpdateRemainderActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
CalendarNotificationGroup: '' as Ref<NotificationGroup>
|
||||
CalendarNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
CalendarWidget: '' as Ref<Widget>
|
||||
}
|
||||
})
|
||||
|
@ -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' }
|
||||
})
|
||||
}
|
||||
|
@ -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<SocialChannelProvider>
|
||||
|
||||
@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<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
declare attachedToClass: Ref<Class<ActivityMessage>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectId!: Ref<Doc>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@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<Class<Doc>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
messageClass!: Ref<Class<Doc>>
|
||||
|
||||
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<Person>
|
||||
hidden!: Ref<DocNotifyContext>[]
|
||||
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<InlineButtonAction>
|
||||
}
|
||||
|
||||
@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TTypingInfo extends TDoc implements TypingInfo {
|
||||
objectId!: Ref<Doc>
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
person!: Ref<Person>
|
||||
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<ActivityMessageControl<ChunterSpace>>(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)
|
||||
}
|
||||
|
@ -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<ActionCategory>
|
||||
@ -98,9 +103,10 @@ export default mergeIds(chunterId, chunter, {
|
||||
CanCopyMessageLink: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
GetChunterSpaceLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
ReplyToThread: '' as Resource<(doc: ActivityMessage) => Promise<void>>,
|
||||
ReplyToThread: '' as Resource<(doc: ActivityMessage, event: MouseEvent) => Promise<void>>,
|
||||
CanReplyToThread: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
||||
GetMessageLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
|
||||
GetMessageLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
|
||||
CloseChatWidgetTab: '' as Resource<(tab: WidgetTab) => Promise<void>>
|
||||
},
|
||||
filter: {
|
||||
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
|
162
models/chunter/src/types.ts
Normal file
162
models/chunter/src/types.ts
Normal file
@ -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<SocialChannelProvider>
|
||||
|
||||
@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<ActivityMessage>
|
||||
|
||||
@Prop(TypeRef(activity.class.ActivityMessage), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
declare attachedToClass: Ref<Class<ActivityMessage>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectId!: Ref<Doc>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@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<Class<Doc>>
|
||||
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
messageClass!: Ref<Class<Doc>>
|
||||
|
||||
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<Person>
|
||||
hidden!: Ref<DocNotifyContext>[]
|
||||
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<InlineButtonAction>
|
||||
}
|
||||
|
||||
@Model(chunter.class.TypingInfo, core.class.Doc, DOMAIN_TRANSIENT)
|
||||
export class TTypingInfo extends TDoc implements TypingInfo {
|
||||
objectId!: Ref<Doc>
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
person!: Ref<Person>
|
||||
lastTyping!: Timestamp
|
||||
}
|
@ -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
|
||||
|
@ -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<Doc>
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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<void>>
|
||||
}
|
||||
|
||||
@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<Widget>
|
||||
|
||||
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
|
||||
})
|
||||
|
@ -19,5 +19,5 @@
|
||||
</script>
|
||||
|
||||
{#each extensions as extension}
|
||||
<Component is={extension.component} props={{ ...extension.props, ...props }} on:open />
|
||||
<Component is={extension.component} showLoading={false} props={{ ...extension.props, ...props }} on:open />
|
||||
{/each}
|
||||
|
@ -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 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
use:resizeObserver={(element) => {
|
||||
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}
|
||||
|
163
packages/ui/src/components/ModernTab.svelte
Normal file
163
packages/ui/src/components/ModernTab.svelte
Normal file
@ -0,0 +1,163 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { AnySvelteComponent, ButtonIcon, Icon, IconClose, Label, tooltip } from '../index'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let labelIntl: IntlString | undefined = undefined
|
||||
export let boldLabel: string | undefined = undefined
|
||||
export let boldLabelIntl: IntlString | undefined = undefined
|
||||
export let highlighted: boolean = false
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let iconProps: Record<string, any> | undefined = undefined
|
||||
export let maxSize: string | undefined = undefined
|
||||
export let orientation: 'horizontal' | 'vertical' = 'horizontal'
|
||||
export let kind: 'primary' | 'secondary' = 'primary'
|
||||
export let canClose = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="container main flex-row-center flex-gap-2 {orientation} {kind}"
|
||||
style:max-width={orientation === 'horizontal' ? maxSize : 'auto'}
|
||||
style:max-height={orientation === 'vertical' ? maxSize : 'auto'}
|
||||
class:active={highlighted}
|
||||
use:tooltip={{ label: label ? getEmbeddedLabel(label) : labelIntl }}
|
||||
on:click
|
||||
on:contextmenu
|
||||
>
|
||||
<slot name="prefix" />
|
||||
|
||||
{#if icon}
|
||||
<div class="icon {orientation}">
|
||||
<Icon {icon} size={'x-small'} {iconProps} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="overflow-label">
|
||||
{#if label}
|
||||
{label}
|
||||
{:else if labelIntl}
|
||||
<Label label={labelIntl} />
|
||||
{/if}
|
||||
{#if boldLabel}
|
||||
<span class="label">
|
||||
{boldLabel}
|
||||
</span>
|
||||
{:else if boldLabelIntl}
|
||||
<span class="label">
|
||||
<Label label={boldLabelIntl} />
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if canClose}
|
||||
<div class="close-button {orientation}">
|
||||
<ButtonIcon icon={IconClose} size="min" on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&.primary {
|
||||
background-color: var(--theme-button-pressed);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
border-color: var(--highlight-select-border);
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
padding: 0.125rem 0.125rem 0.125rem 0.5rem;
|
||||
height: 1.625rem;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
padding: 0.5rem 0.125rem 0.125rem 0.125rem;
|
||||
width: 1.625rem;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: sideways;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
|
||||
&.main {
|
||||
&.horizontal {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.primary {
|
||||
background-color: var(--theme-button-hovered);
|
||||
border-color: var(--theme-navpanel-divider);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
border-color: var(--highlight-select-border);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
background-color: var(--highlight-select);
|
||||
border-color: var(--highlight-select-border);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--highlight-select);
|
||||
border-color: var(--highlight-select-border);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
&.vertical {
|
||||
transform: rotate(90deg);
|
||||
text-orientation: upright;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
&.vertical {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -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'
|
||||
|
@ -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<string[]>([])
|
||||
|
@ -84,7 +84,7 @@
|
||||
|
||||
if (replyProvider) {
|
||||
const fn = await getResource(replyProvider.function)
|
||||
await fn(object)
|
||||
await fn(object, e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -228,7 +228,7 @@ export interface SavedMessage extends Preference {
|
||||
}
|
||||
|
||||
export interface ReplyProvider extends Doc {
|
||||
function: Resource<(message: ActivityMessage) => Promise<void>>
|
||||
function: Resource<(message: ActivityMessage, event: MouseEvent) => Promise<void>>
|
||||
}
|
||||
|
||||
export interface UserMentionInfo extends AttachedDoc {
|
||||
|
@ -0,0 +1,65 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import { CalendarMode } from '../index'
|
||||
import CalendarNavigation from './CalendarNavigation.svelte'
|
||||
|
||||
export let currentDate: Date
|
||||
export let monthName: string
|
||||
export let ddItems: {
|
||||
id: string | number
|
||||
label: IntlString
|
||||
mode: CalendarMode
|
||||
params?: Record<string, any>
|
||||
}[] = []
|
||||
export let mode: CalendarMode
|
||||
export let onToday: () => void
|
||||
export let onBack: () => void
|
||||
export let onForward: () => void
|
||||
</script>
|
||||
|
||||
<div class="calendar-header">
|
||||
<div class="title">
|
||||
{monthName}
|
||||
<span>{currentDate.getFullYear()}</span>
|
||||
</div>
|
||||
<CalendarNavigation {ddItems} {mode} {onToday} {onBack} {onForward} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.75rem 1.75rem 0.75rem 2.25rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
&::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
span {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Button, DropdownLabelsIntl, IconBack, IconForward } from '@hcengineering/ui'
|
||||
|
||||
import { CalendarMode } from '../index'
|
||||
import calendar from '../plugin'
|
||||
|
||||
export let ddItems: {
|
||||
id: string | number
|
||||
label: IntlString
|
||||
mode: CalendarMode
|
||||
params?: Record<string, any>
|
||||
}[] = []
|
||||
export let mode: CalendarMode
|
||||
export let onToday: () => void
|
||||
export let onBack: () => void
|
||||
export let onForward: () => void
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if ddItems.length > 1}
|
||||
<DropdownLabelsIntl
|
||||
items={ddItems.map((it) => {
|
||||
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}
|
||||
<Button label={calendar.string.Today} on:click={onToday} />
|
||||
<Button icon={IconBack} kind={'ghost'} on:click={onBack} />
|
||||
<Button icon={IconForward} kind={'ghost'} on:click={onForward} />
|
||||
</div>
|
@ -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<Class<Doc>> = calendar.class.Event
|
||||
export let query: DocumentQuery<Event> | 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<Event>
|
||||
|
||||
function dragEnter (e: CustomEvent<any>) {
|
||||
function dragEnter (e: CustomEvent<any>): 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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="calendar-header">
|
||||
<div class="title">
|
||||
{getMonthName(currentDate)}
|
||||
<span>{currentDate.getFullYear()}</span>
|
||||
</div>
|
||||
<div class="flex-row-center gap-2">
|
||||
{#if ddItems.length > 1}
|
||||
<DropdownLabelsIntl
|
||||
items={ddItems.map((it) => {
|
||||
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}
|
||||
<Button
|
||||
label={calendar.string.Today}
|
||||
on:click={() => {
|
||||
inc(0)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconBack}
|
||||
kind={'ghost'}
|
||||
on:click={() => {
|
||||
inc(-1)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconForward}
|
||||
kind={'ghost'}
|
||||
on:click={() => {
|
||||
inc(1)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if headerComponent}
|
||||
<svelte:component
|
||||
this={headerComponent}
|
||||
{mode}
|
||||
{currentDate}
|
||||
{ddItems}
|
||||
monthName={getMonthName(currentDate)}
|
||||
onToday={handleToday}
|
||||
onBack={handleBack}
|
||||
onForward={handleForward}
|
||||
on:close
|
||||
/>
|
||||
{:else}
|
||||
<CalendarHeader
|
||||
{mode}
|
||||
{currentDate}
|
||||
{ddItems}
|
||||
monthName={getMonthName(currentDate)}
|
||||
onToday={handleToday}
|
||||
onBack={handleBack}
|
||||
onForward={handleForward}
|
||||
/>
|
||||
{/if}
|
||||
{#if mode === CalendarMode.Year}
|
||||
<YearCalendar
|
||||
{mondayStart}
|
||||
@ -412,27 +408,3 @@
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<!-- <div class="min-h-4 max-h-4 h-4 flex-no-shrink" /> -->
|
||||
<style lang="scss">
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1.75rem 0.75rem 2.25rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
|
||||
&::first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
span {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,20 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CalendarMode, CalendarView } from '../index'
|
||||
import CalendarWidgetHeader from './CalendarWidgetHeader.svelte'
|
||||
</script>
|
||||
|
||||
<CalendarView allowedModes={[CalendarMode.Day]} headerComponent={CalendarWidgetHeader} on:close />
|
@ -0,0 +1,61 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs, Header } from '@hcengineering/ui'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import { CalendarMode } from '../index'
|
||||
import CalendarNavigation from './CalendarNavigation.svelte'
|
||||
|
||||
export let currentDate: Date
|
||||
export let ddItems: {
|
||||
id: string | number
|
||||
label: IntlString
|
||||
mode: CalendarMode
|
||||
params?: Record<string, any>
|
||||
}[] = []
|
||||
export let mode: CalendarMode
|
||||
export let monthName: string
|
||||
export let onToday: () => void
|
||||
export let onBack: () => void
|
||||
export let onForward: () => void
|
||||
</script>
|
||||
|
||||
<Header
|
||||
allowFullsize={false}
|
||||
type="type-aside"
|
||||
hideBefore={true}
|
||||
hideActions={false}
|
||||
hideDescription={true}
|
||||
hideExtra={false}
|
||||
adaptive="autoExtra"
|
||||
doubleRowWidth={350}
|
||||
on:close
|
||||
>
|
||||
<div class="title">
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{
|
||||
title: `${monthName} ${currentDate.getFullYear()}`
|
||||
}
|
||||
]}
|
||||
currentOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="extra">
|
||||
<CalendarNavigation {ddItems} {mode} {onToday} {onBack} {onForward} />
|
||||
</svelte:fragment>
|
||||
</Header>
|
@ -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<Resources> => ({
|
||||
CalendarIntegrationIcon,
|
||||
CalendarEventPresenter,
|
||||
IntegrationConfigure,
|
||||
ConnectApp
|
||||
ConnectApp,
|
||||
CalendarWidget
|
||||
},
|
||||
actionImpl: {
|
||||
SaveEventReminder: saveEventReminder,
|
||||
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
@ -121,6 +121,7 @@
|
||||
"JoinChannel": "Приссоединение к каналу",
|
||||
"YouJoinedChannel": "Вы присоединились к каналу",
|
||||
"AndMore": "и еще {count}",
|
||||
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}..."
|
||||
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}...",
|
||||
"ThreadIn": "Обсуждение в {name}"
|
||||
}
|
||||
}
|
@ -121,6 +121,7 @@
|
||||
"JoinChannel": "加入频道",
|
||||
"YouJoinedChannel": "你已加入频道",
|
||||
"AndMore": "和 {count} 人",
|
||||
"IsTyping": "正在输入..."
|
||||
"IsTyping": "正在输入...",
|
||||
"ThreadIn": "线程在 {name}"
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
private readonly tailQuery = createQuery(true)
|
||||
private readonly refsQuery = createQuery(true)
|
||||
|
||||
private chatId: Ref<Doc> | undefined = undefined
|
||||
chatId: Ref<Doc> | undefined = undefined
|
||||
private readonly msgClass: Ref<Class<ActivityMessage>>
|
||||
private selectedMsgId: Ref<ActivityMessage> | undefined = undefined
|
||||
private tailStart: Timestamp | undefined = undefined
|
||||
|
@ -29,6 +29,7 @@
|
||||
export let context: DocNotifyContext | undefined
|
||||
export let filters: Ref<ActivityMessagesFilter>[] = []
|
||||
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)
|
||||
|
@ -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<ActivityMessagesFilter>[] = []
|
||||
|
||||
@ -47,12 +49,13 @@
|
||||
})
|
||||
|
||||
async function updateDescription (_id: Ref<Doc>, _class: Ref<Class<Doc>>, object?: Doc): Promise<void> {
|
||||
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
|
||||
>
|
||||
|
@ -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}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="flex-col h-full relative">
|
||||
<div class="flex-col relative" class:h-full={fullHeight}>
|
||||
{#if startFromBottom}
|
||||
<div class="grower" />
|
||||
{/if}
|
||||
|
@ -0,0 +1,113 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { Widget } from '@hcengineering/workbench'
|
||||
import { ChatWidgetTab } from '@hcengineering/chunter'
|
||||
|
||||
import Channel from './Channel.svelte'
|
||||
import { closeThreadInSidebarChannel } from '../navigation'
|
||||
import { ThreadView } from '../index'
|
||||
import ChannelHeader from './ChannelHeader.svelte'
|
||||
|
||||
export let widget: Widget
|
||||
export let tab: ChatWidgetTab
|
||||
export let height: string
|
||||
export let width: string
|
||||
|
||||
const notificationsClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextByDocStore = notificationsClient.contextByDoc
|
||||
const objectQuery = createQuery()
|
||||
|
||||
let object: Doc | undefined = undefined
|
||||
let context: DocNotifyContext | undefined = undefined
|
||||
|
||||
$: context = object ? $contextByDocStore.get(object._id) : undefined
|
||||
$: void loadObject(tab.data._id, tab.data._class)
|
||||
|
||||
$: threadId = tab.data.thread
|
||||
|
||||
async function loadObject (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): Promise<void> {
|
||||
if (_id === undefined || _class === undefined) {
|
||||
object = undefined
|
||||
objectQuery.unsubscribe()
|
||||
return
|
||||
}
|
||||
|
||||
objectQuery.query(
|
||||
_class,
|
||||
{ _id },
|
||||
(res) => {
|
||||
object = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
{#key object._id}
|
||||
<div class="channel" class:invisible={threadId !== undefined} style:height style:width>
|
||||
<ChannelHeader
|
||||
_id={object._id}
|
||||
_class={object._class}
|
||||
{object}
|
||||
withAside={false}
|
||||
withSearch={false}
|
||||
canOpen={true}
|
||||
allowClose={true}
|
||||
on:close
|
||||
/>
|
||||
<Channel {object} {context} syncLocation={false} />
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if threadId}
|
||||
<div class="thread" style:height style:width>
|
||||
<ThreadView _id={threadId} on:close={() => closeThreadInSidebarChannel(widget, tab)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.channel {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.thread {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--theme-panel-color);
|
||||
}
|
||||
</style>
|
58
plugins/chunter-resources/src/components/ChatWidget.svelte
Normal file
58
plugins/chunter-resources/src/components/ChatWidget.svelte
Normal file
@ -0,0 +1,58 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { closeWidget, closeWidgetTab } from '@hcengineering/workbench-resources'
|
||||
import { Widget } from '@hcengineering/workbench'
|
||||
import { ChatWidgetTab } from '@hcengineering/chunter'
|
||||
|
||||
import ChannelSidebarView from './ChannelSidebarView.svelte'
|
||||
import chunter from '../plugin'
|
||||
import ThreadSidebarView from './threads/ThreadSidebarView.svelte'
|
||||
|
||||
export let widget: Widget | undefined
|
||||
export let tab: ChatWidgetTab | undefined
|
||||
export let height: string
|
||||
export let width: string
|
||||
|
||||
function handleClose (tabId?: string): void {
|
||||
if (widget === undefined || tabId === undefined) return
|
||||
void closeWidgetTab(widget, tabId)
|
||||
}
|
||||
|
||||
$: if (widget === undefined || tab === undefined) {
|
||||
closeWidget(chunter.ids.ChatWidget)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if widget && tab && tab.type === 'channel'}
|
||||
<ChannelSidebarView
|
||||
{widget}
|
||||
{tab}
|
||||
{height}
|
||||
{width}
|
||||
on:close={() => {
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{:else if widget && tab && tab.type === 'thread'}
|
||||
<ThreadSidebarView
|
||||
{tab}
|
||||
{height}
|
||||
{width}
|
||||
on:close={() => {
|
||||
handleClose(tab?.id)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
107
plugins/chunter-resources/src/components/ChatWidgetTab.svelte
Normal file
107
plugins/chunter-resources/src/components/ChatWidgetTab.svelte
Normal file
@ -0,0 +1,107 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Action, Menu, ModernTab, showPopup } from '@hcengineering/ui'
|
||||
import { Widget } from '@hcengineering/workbench'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { ChatWidgetTab } from '@hcengineering/chunter'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
import {
|
||||
getNotificationsCount,
|
||||
InboxNotificationsClientImpl,
|
||||
isActivityNotification,
|
||||
isMentionNotification,
|
||||
NotifyMarker
|
||||
} from '@hcengineering/notification-resources'
|
||||
import chunter from '../plugin'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
export let tab: ChatWidgetTab
|
||||
export let widget: Widget
|
||||
export let selected = false
|
||||
export let actions: Action[] = []
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextByDocStore = notificationClient.contextByDoc
|
||||
|
||||
$: icon = tab.icon ?? widget.icon
|
||||
|
||||
$: if (tab.iconComponent) {
|
||||
void getResource(tab.iconComponent).then((res) => {
|
||||
icon = res
|
||||
})
|
||||
}
|
||||
let notifications: InboxNotification[] = []
|
||||
|
||||
let count: number = 0
|
||||
|
||||
$: objectId = tab.type === 'thread' ? tab.data.thread : tab.data._id
|
||||
$: context = objectId ? $contextByDocStore.get(objectId) : undefined
|
||||
|
||||
const unsubscribe = notificationClient.inboxNotificationsByContext.subscribe((res) => {
|
||||
if (context === undefined) {
|
||||
count = 0
|
||||
return
|
||||
}
|
||||
|
||||
notifications = (res.get(context._id) ?? []).filter((n) => {
|
||||
if (isActivityNotification(n)) return true
|
||||
|
||||
return isMentionNotification(n) && hierarchy.isDerived(n.mentionedInClass, chunter.class.ChatMessage)
|
||||
})
|
||||
})
|
||||
|
||||
$: void getNotificationsCount(context, notifications).then((res) => {
|
||||
count = res
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
function handleMenu (event: MouseEvent): void {
|
||||
if (actions.length === 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
showPopup(Menu, { actions }, event.target as HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModernTab
|
||||
label={tab.name}
|
||||
labelIntl={tab.nameIntl ?? widget.label}
|
||||
highlighted={selected}
|
||||
orientation="vertical"
|
||||
kind={tab.isPinned ? 'secondary' : 'primary'}
|
||||
{icon}
|
||||
iconProps={tab.iconProps}
|
||||
canClose={!tab.isPinned}
|
||||
maxSize="13.5rem"
|
||||
on:close
|
||||
on:click
|
||||
on:contextmenu={handleMenu}
|
||||
>
|
||||
<svelte:fragment slot="prefix">
|
||||
{#if count > 0}
|
||||
<NotifyMarker kind="simple" size="xx-small" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ModernTab>
|
@ -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<DirectMessage> | 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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if persons.length === 0 && value}
|
||||
<Icon icon={classIcon(client, value._class) ?? chunter.icon.Chunter} {size} />
|
||||
{#if persons.length === 0}
|
||||
<Icon icon={classIcon(client, chunter.class.DirectMessage) ?? chunter.icon.Chunter} {size} />
|
||||
{/if}
|
||||
|
||||
{#if persons.length === 1}
|
||||
@ -83,12 +93,18 @@
|
||||
{/if}
|
||||
|
||||
{#if persons.length > 1 && size !== 'medium'}
|
||||
<CombineAvatars
|
||||
_class={contact.class.Person}
|
||||
items={persons.map(({ _id }) => _id)}
|
||||
size={avatarSize}
|
||||
limit={visiblePersons}
|
||||
/>
|
||||
{#if compact}
|
||||
<div class="hulyAvatar-container hulyAvatarSize-{size} group-ava">
|
||||
{persons.length}
|
||||
</div>
|
||||
{:else}
|
||||
<CombineAvatars
|
||||
_class={contact.class.Person}
|
||||
items={persons.map(({ _id }) => _id)}
|
||||
size={avatarSize}
|
||||
limit={visiblePersons}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@ -117,4 +133,19 @@
|
||||
font-size: 0.688rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.group-ava {
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
border-radius: 0.25rem;
|
||||
opacity: 0.9;
|
||||
|
||||
&.inline,
|
||||
&.tiny,
|
||||
&.card,
|
||||
&.x-small {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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<ActivityMessagesFilter>[] = []
|
||||
export let adaptive: HeaderAdaptive = 'default'
|
||||
export let hideActions: boolean = false
|
||||
@ -110,21 +111,23 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="search" let:doubleRow>
|
||||
<SearchInput
|
||||
collapsed
|
||||
bind:value={searchValue}
|
||||
on:change={(ev) => {
|
||||
userSearch.set(ev.detail)
|
||||
{#if withSearch}
|
||||
<SearchInput
|
||||
collapsed
|
||||
bind:value={searchValue}
|
||||
on:change={(ev) => {
|
||||
userSearch.set(ev.detail)
|
||||
|
||||
if (ev.detail !== '') {
|
||||
navigateToSpecial('chunterBrowser')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if withFilters}
|
||||
<ChannelMessagesFilter bind:selectedFilters={filters} />
|
||||
if (ev.detail !== '') {
|
||||
navigateToSpecial('chunterBrowser')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if withFilters}
|
||||
<ChannelMessagesFilter bind:selectedFilters={filters} />
|
||||
{/if}
|
||||
<slot name="search" {doubleRow} />
|
||||
{/if}
|
||||
<slot name="search" {doubleRow} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions" let:doubleRow>
|
||||
<slot name="actions" {doubleRow} />
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref, Class, Space } from '@hcengineering/core'
|
||||
import { Doc, Ref, Class } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
Component,
|
||||
@ -24,8 +24,7 @@
|
||||
Separator,
|
||||
Location,
|
||||
restoreLocation,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
type AnyComponent
|
||||
deviceOptionsStore as deviceInfo
|
||||
} from '@hcengineering/ui'
|
||||
import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
@ -33,16 +32,13 @@
|
||||
import { chunterId } from '@hcengineering/chunter'
|
||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||
import { parseLinkId, getObjectLinkId } from '@hcengineering/view-resources'
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import ChatNavigator from './navigator/ChatNavigator.svelte'
|
||||
import ChannelView from '../ChannelView.svelte'
|
||||
import { chatSpecials, loadSavedAttachments } from './utils'
|
||||
import { SelectChannelEvent } from './types'
|
||||
import { openChannel } from '../../navigation'
|
||||
|
||||
export let currentSpace: Ref<Space> | undefined = undefined
|
||||
export let asideComponent: AnyComponent | undefined = undefined
|
||||
export let asideId: string | undefined = undefined
|
||||
import { openChannel, openThreadInSidebar } from '../../navigation'
|
||||
|
||||
const notificationsClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextByDocStore = notificationsClient.contextByDoc
|
||||
@ -63,9 +59,12 @@
|
||||
let object: Doc | undefined = undefined
|
||||
let replacedPanel: HTMLElement
|
||||
|
||||
location.subscribe((loc) => {
|
||||
const unsubcribe = location.subscribe((loc) => {
|
||||
syncLocation(loc)
|
||||
})
|
||||
onDestroy(() => {
|
||||
unsubcribe()
|
||||
})
|
||||
|
||||
$: void loadObject(selectedData?.id, selectedData?._class)
|
||||
|
||||
@ -118,6 +117,12 @@
|
||||
const [id, _class] = decodeObjectURI(loc.path[3])
|
||||
selectedData = { id, _class }
|
||||
}
|
||||
|
||||
const thread = loc.path[4] as Ref<ActivityMessage> | undefined
|
||||
|
||||
if (thread !== undefined) {
|
||||
void openThreadInSidebar(thread)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChannelSelected (event: CustomEvent): Promise<void> {
|
||||
@ -175,8 +180,7 @@
|
||||
short
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div bind:this={replacedPanel} class="hulyComponent" class:beforeAside={asideComponent !== undefined && asideId}>
|
||||
<div bind:this={replacedPanel} class="hulyComponent">
|
||||
{#if currentSpecial}
|
||||
<Component
|
||||
is={currentSpecial.component}
|
||||
@ -197,10 +201,4 @@
|
||||
<ChannelView {object} {context} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if asideComponent !== undefined && asideId}
|
||||
<Separator name={'chat'} index={1} color={'var(--theme-divider-color)'} separatorSize={1} />
|
||||
<div class="hulyComponent aside">
|
||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -80,6 +80,7 @@
|
||||
result.push({
|
||||
icon: view.icon.Open,
|
||||
label: view.string.Open,
|
||||
group: 'edit',
|
||||
action: async () => {
|
||||
const id = await getObjectLinkId(linkProviders, object._id, object._class, object)
|
||||
openChannel(id, object._class)
|
||||
@ -107,9 +108,10 @@
|
||||
result.push({
|
||||
icon: action.icon ?? IconEdit,
|
||||
label: action.label,
|
||||
group: action.context.group,
|
||||
action: async (_: any, evt: Event) => {
|
||||
const impl = await getResource(action.action)
|
||||
await impl(context, evt, action.actionProps)
|
||||
await impl(context, evt, { ...action.actionProps, object })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ChatWidgetTab } from '@hcengineering/chunter'
|
||||
|
||||
import ThreadView from './ThreadView.svelte'
|
||||
|
||||
export let tab: ChatWidgetTab
|
||||
export let height: string
|
||||
export let width: string
|
||||
</script>
|
||||
|
||||
{#if tab.data.thread}
|
||||
{#key tab.data.thread}
|
||||
<div class="root" style:height style:width>
|
||||
<ThreadView _id={tab.data.thread} on:close />
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
@ -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 @@
|
||||
|
||||
<div class="hulyComponent-content hulyComponent-content__container noShrink">
|
||||
{#if message && dataProvider !== undefined}
|
||||
<ChannelScrollView bind:selectedMessageId embedded skipLabels object={message} provider={dataProvider}>
|
||||
<ChannelScrollView
|
||||
bind:selectedMessageId
|
||||
embedded
|
||||
skipLabels
|
||||
object={message}
|
||||
provider={dataProvider}
|
||||
initialScrollBottom={false}
|
||||
fullHeight={false}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="mt-3">
|
||||
<ThreadParentMessage {message} />
|
||||
</div>
|
||||
<div class="separator">
|
||||
{#if message.replies && message.replies > 0}
|
||||
|
||||
{#if message.replies && message.replies > 0}
|
||||
<div class="separator">
|
||||
<div class="label lower">
|
||||
<Label label={activity.string.RepliesCount} params={{ replies: message.replies }} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="line" />
|
||||
</div>
|
||||
<div class="line" />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ChannelScrollView>
|
||||
{/if}
|
||||
|
@ -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<Resources> => ({
|
||||
ChannelIcon,
|
||||
ChatMessageNotificationLabel,
|
||||
ThreadNotificationPresenter,
|
||||
ChatAside,
|
||||
ThreadMessagePreview,
|
||||
ChatMessagePreview,
|
||||
JoinChannelNotificationPresenter
|
||||
JoinChannelNotificationPresenter,
|
||||
ChatWidget,
|
||||
ChatWidgetTab
|
||||
},
|
||||
activity: {
|
||||
ChannelCreatedMessage,
|
||||
@ -188,7 +194,9 @@ export default async (): Promise<Resources> => ({
|
||||
GetThreadLink: getThreadLink,
|
||||
ReplyToThread: replyToThread,
|
||||
CanReplyToThread: canReplyToThread,
|
||||
GetMessageLink: getMessageLocation
|
||||
GetMessageLink: getMessageLocation,
|
||||
CloseChatWidgetTab: closeChatWidgetTab,
|
||||
OpenChannelInSidebar: openChannelInSidebar
|
||||
},
|
||||
actionImpl: {
|
||||
ArchiveChannel,
|
||||
@ -197,6 +205,8 @@ export default async (): Promise<Resources> => ({
|
||||
DeleteChatMessage: deleteChatMessage,
|
||||
LeaveChannel: leaveChannelAction,
|
||||
RemoveChannel: removeChannelAction,
|
||||
ReplyToThread: replyToThread
|
||||
ReplyToThread: replyToThread,
|
||||
OpenInSidebar: openChannelInSidebarAction,
|
||||
OpenInSidebarTab: openChannelInSidebarTabAction
|
||||
}
|
||||
})
|
||||
|
@ -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<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
|
||||
const loc = getCurrentLocation()
|
||||
@ -152,11 +163,23 @@ export async function getThreadLink (doc: ThreadMessage): Promise<Location> {
|
||||
return await buildThreadLink(loc, doc.objectId, doc.objectClass, doc.attachedTo, doc)
|
||||
}
|
||||
|
||||
export async function replyToThread (message: ActivityMessage): Promise<void> {
|
||||
export async function replyToThread (message: ActivityMessage, e: Event): Promise<void> {
|
||||
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<Doc>, _class: Ref<Class<D
|
||||
closePanel()
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
function getChannelClassIcon (object: Doc): Asset | undefined {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
if (hierarchy.isDerived(object._class, chunter.class.Channel)) {
|
||||
return (object as Channel).private ? chunter.icon.Lock : chunter.icon.Hashtag
|
||||
}
|
||||
|
||||
return classIcon(client, object._class)
|
||||
}
|
||||
|
||||
export async function openChannelInSidebar (
|
||||
_id: Ref<Doc>,
|
||||
_class: Ref<Class<Doc>>,
|
||||
doc?: Doc,
|
||||
thread?: Ref<ActivityMessage>,
|
||||
newTab = true
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
await openChannelInSidebar(context.objectId, context.objectClass, props?.object, undefined, props?.newTab ?? false)
|
||||
}
|
||||
|
||||
export async function openChannelInSidebarTabAction (
|
||||
context: DocNotifyContext,
|
||||
event: Event,
|
||||
props?: { object?: Doc }
|
||||
): Promise<void> {
|
||||
await openChannelInSidebarAction(context, event, { newTab: true, object: props?.object })
|
||||
}
|
||||
|
||||
export async function openThreadInSidebarChannel (
|
||||
widget: Widget,
|
||||
tab: ChatWidgetTab,
|
||||
message: ActivityMessage
|
||||
): Promise<void> {
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
data: { ...tab.data, thread: message._id }
|
||||
}
|
||||
createWidgetTab(widget, newTab)
|
||||
}
|
||||
|
||||
export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidgetTab): Promise<void> {
|
||||
const newTab: ChatWidgetTab = {
|
||||
...tab,
|
||||
data: { ...tab.data, thread: undefined }
|
||||
}
|
||||
|
||||
createWidgetTab(widget, newTab)
|
||||
}
|
||||
|
||||
export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc): Promise<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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<InlineButtonAction>
|
||||
}
|
||||
|
||||
export interface ChatWidgetTab extends WidgetTab {
|
||||
type: 'channel' | 'thread'
|
||||
data: { _id?: Ref<Doc>, _class?: Ref<Class<Doc>>, thread?: Ref<ActivityMessage> }
|
||||
}
|
||||
|
||||
/**
|
||||
* @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<NotificationType>,
|
||||
ThreadNotification: '' as Ref<NotificationType>,
|
||||
ChannelNotification: '' as Ref<NotificationType>,
|
||||
JoinChannelNotification: '' as Ref<NotificationType>,
|
||||
ThreadMessageViewlet: '' as Ref<ChatMessageViewlet>
|
||||
ThreadMessageViewlet: '' as Ref<ChatMessageViewlet>,
|
||||
ChatWidget: '' as Ref<Widget>
|
||||
},
|
||||
app: {
|
||||
Chunter: '' as Ref<Doc>
|
||||
@ -216,5 +223,10 @@ export default plugin(chunterId, {
|
||||
LeaveChannel: '' as Ref<Action>,
|
||||
RemoveChannel: '' as Ref<Action>,
|
||||
CloseConversation: '' as Ref<Action>
|
||||
},
|
||||
function: {
|
||||
OpenChannelInSidebar: '' as Resource<
|
||||
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
|
||||
>
|
||||
}
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { PersonAccount, formatName } from '@hcengineering/contact'
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { IdMap, Ref, getCurrentAccount, toIdMap } from '@hcengineering/core'
|
||||
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
|
||||
import {
|
||||
Floor,
|
||||
Invite,
|
||||
@ -31,8 +31,6 @@
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
ActionIcon,
|
||||
Label,
|
||||
Location,
|
||||
PopupResult,
|
||||
closePopup,
|
||||
@ -43,6 +41,9 @@
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { onDestroy } from 'svelte'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import { closeWidget, openWidget, sidebarStore } from '@hcengineering/workbench-resources'
|
||||
|
||||
import love from '../plugin'
|
||||
import {
|
||||
activeFloor,
|
||||
@ -79,7 +80,6 @@
|
||||
import RequestPopup from './RequestPopup.svelte'
|
||||
import RequestingPopup from './RequestingPopup.svelte'
|
||||
import RoomPopup from './RoomPopup.svelte'
|
||||
import VideoPopup from './VideoPopup.svelte'
|
||||
|
||||
let allowCam: boolean = false
|
||||
let allowLeave: boolean = false
|
||||
@ -112,8 +112,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
const me = (getCurrentAccount() as PersonAccount).person
|
||||
|
||||
interface ActiveRoom extends Room {
|
||||
participants: ParticipantInfo[]
|
||||
}
|
||||
@ -285,31 +283,24 @@
|
||||
showPopup(CamSettingPopup, {}, eventToHTMLElement(e))
|
||||
}
|
||||
|
||||
const videoPopupCategory = 'videoPopup'
|
||||
let videoPopup: PopupResult | undefined = undefined
|
||||
|
||||
function checkActiveVideo (loc: Location, video: boolean, room: Ref<Room> | undefined): void {
|
||||
if (room === undefined) return
|
||||
const isOpened = $sidebarStore.widgetsState.get(love.ids.VideoWidget)
|
||||
if (room === undefined) {
|
||||
if (isOpened) {
|
||||
closeWidget(love.ids.VideoWidget)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (loc.path[2] !== loveId && video) {
|
||||
if (videoPopup !== undefined) return
|
||||
videoPopup = showPopup(
|
||||
VideoPopup,
|
||||
{
|
||||
room
|
||||
},
|
||||
'movable',
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
category: videoPopupCategory,
|
||||
overlay: false,
|
||||
fixed: true,
|
||||
refId: videoPopupCategory
|
||||
}
|
||||
)
|
||||
} else if (videoPopup) {
|
||||
videoPopup.close()
|
||||
videoPopup = undefined
|
||||
if (isOpened) return
|
||||
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.VideoWidget })[0]
|
||||
if (widget === undefined) return
|
||||
openWidget(widget, {
|
||||
room
|
||||
})
|
||||
} else {
|
||||
closeWidget(love.ids.VideoWidget)
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,7 +317,7 @@
|
||||
closePopup(inviteCategory)
|
||||
closePopup(joinRequestCategory)
|
||||
closePopup(myJoinRequestCategory)
|
||||
closePopup(videoPopupCategory)
|
||||
closeWidget(love.ids.VideoWidget)
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
@ -362,56 +353,56 @@
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="container main flex-row-center flex-gap-2" bind:this={myOfficeElement} on:click={selectFloor}>
|
||||
{#if selectedFloor}
|
||||
<Label label={love.string.Floor} />
|
||||
<span class="label overflow-label">
|
||||
{selectedFloor?.name}
|
||||
</span>
|
||||
{/if}
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
size={'small'}
|
||||
action={changeMute}
|
||||
on:contextmenu={micSettings}
|
||||
disabled={!$isConnected}
|
||||
keys={micKeys}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={!$isConnected || !allowCam
|
||||
? love.icon.Cam
|
||||
: $isCameraEnabled
|
||||
? love.icon.CamEnabled
|
||||
: love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
size={'small'}
|
||||
action={changeCam}
|
||||
on:contextmenu={camSettings}
|
||||
disabled={!$isConnected || !allowCam}
|
||||
keys={camKeys}
|
||||
/>
|
||||
{#if $isConnected}
|
||||
<ActionIcon
|
||||
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
|
||||
label={$isSharingEnabled ? love.string.StopShare : love.string.Share}
|
||||
disabled={$screenSharing && !$isSharingEnabled}
|
||||
size={'small'}
|
||||
action={changeShare}
|
||||
/>
|
||||
{/if}
|
||||
{#if allowLeave}
|
||||
<ActionIcon
|
||||
icon={love.icon.LeaveRoom}
|
||||
iconProps={{ color: '#FF6711' }}
|
||||
label={love.string.LeaveRoom}
|
||||
size={'small'}
|
||||
action={leave}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- <div class="container main flex-row-center flex-gap-2" bind:this={myOfficeElement} on:click={selectFloor}>-->
|
||||
<!-- {#if selectedFloor}-->
|
||||
<!-- <Label label={love.string.Floor} />-->
|
||||
<!-- <span class="label overflow-label">-->
|
||||
<!-- {selectedFloor?.name}-->
|
||||
<!-- </span>-->
|
||||
<!-- {/if}-->
|
||||
<!-- <ActionIcon-->
|
||||
<!-- icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}-->
|
||||
<!-- label={$isMicEnabled ? love.string.Mute : love.string.UnMute}-->
|
||||
<!-- size={'small'}-->
|
||||
<!-- action={changeMute}-->
|
||||
<!-- on:contextmenu={micSettings}-->
|
||||
<!-- disabled={!$isConnected}-->
|
||||
<!-- keys={micKeys}-->
|
||||
<!-- />-->
|
||||
<!-- <ActionIcon-->
|
||||
<!-- icon={!$isConnected || !allowCam-->
|
||||
<!-- ? love.icon.Cam-->
|
||||
<!-- : $isCameraEnabled-->
|
||||
<!-- ? love.icon.CamEnabled-->
|
||||
<!-- : love.icon.CamDisabled}-->
|
||||
<!-- label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}-->
|
||||
<!-- size={'small'}-->
|
||||
<!-- action={changeCam}-->
|
||||
<!-- on:contextmenu={camSettings}-->
|
||||
<!-- disabled={!$isConnected || !allowCam}-->
|
||||
<!-- keys={camKeys}-->
|
||||
<!-- />-->
|
||||
<!-- {#if $isConnected}-->
|
||||
<!-- <ActionIcon-->
|
||||
<!-- icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}-->
|
||||
<!-- label={$isSharingEnabled ? love.string.StopShare : love.string.Share}-->
|
||||
<!-- disabled={$screenSharing && !$isSharingEnabled}-->
|
||||
<!-- size={'small'}-->
|
||||
<!-- action={changeShare}-->
|
||||
<!-- />-->
|
||||
<!-- {/if}-->
|
||||
<!-- {#if allowLeave}-->
|
||||
<!-- <ActionIcon-->
|
||||
<!-- icon={love.icon.LeaveRoom}-->
|
||||
<!-- iconProps={{ color: '#FF6711' }}-->
|
||||
<!-- label={love.string.LeaveRoom}-->
|
||||
<!-- size={'small'}-->
|
||||
<!-- action={leave}-->
|
||||
<!-- />-->
|
||||
<!-- {/if}-->
|
||||
<!-- </div>-->
|
||||
{#if activeRooms.length > 0}
|
||||
<div class="divider" />
|
||||
<!-- <div class="divider" />-->
|
||||
{#each activeRooms as active}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@ -434,7 +425,9 @@
|
||||
{/each}
|
||||
{/if}
|
||||
{#if reception && receptionParticipants.length > 0}
|
||||
<div class="divider" />
|
||||
{#if activeRooms.length > 0}
|
||||
<div class="divider" />
|
||||
{/if}
|
||||
<div class="container flex-row-center flex-gap-2">
|
||||
<div>{getRoomName(reception, $personByIdStore)}</div>
|
||||
<div class="flex-row-center avatars">
|
||||
|
98
plugins/love-resources/src/components/LoveWidget.svelte
Normal file
98
plugins/love-resources/src/components/LoveWidget.svelte
Normal file
@ -0,0 +1,98 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Floor, Room } from '@hcengineering/love'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import ui, { Button, IconChevronLeft, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
|
||||
import FloorPreview from './FloorPreview.svelte'
|
||||
import { floors, rooms } from '../stores'
|
||||
import IconLayers from './icons/Layers.svelte'
|
||||
import love from '../plugin'
|
||||
|
||||
let selectedFloor: Floor | undefined
|
||||
let floorsSelector: boolean = false
|
||||
|
||||
$: if (selectedFloor === undefined && $floors.length > 0) {
|
||||
selectedFloor = $floors[0]
|
||||
}
|
||||
|
||||
function getRooms (rooms: Room[], floor: Ref<Floor>): Room[] {
|
||||
return rooms.filter((p) => p.floor === floor)
|
||||
}
|
||||
|
||||
function changeMode (): void {
|
||||
floorsSelector = !floorsSelector
|
||||
}
|
||||
|
||||
function selectFloor (_id: Ref<Floor>): void {
|
||||
selectedFloor = $floors.find((p) => p._id === _id)
|
||||
floorsSelector = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
{#if floorsSelector}
|
||||
{#each $floors as floor, i}
|
||||
<Button
|
||||
kind={'ghost'}
|
||||
size={'large'}
|
||||
on:click={() => {
|
||||
selectFloor(floor._id)
|
||||
}}
|
||||
justify={'left'}
|
||||
label={getEmbeddedLabel(floor.name)}
|
||||
/>
|
||||
{#if i !== $floors.length - 1}<div class="divider" />{/if}
|
||||
{/each}
|
||||
<div class="flex-row-center flex-reverse mt-4 w-full">
|
||||
<ModernButton on:click={changeMode} icon={IconChevronLeft} label={ui.string.Back} />
|
||||
</div>
|
||||
{:else}
|
||||
{#if selectedFloor}
|
||||
<Scroller>
|
||||
<FloorPreview
|
||||
floor={selectedFloor}
|
||||
showRoomName
|
||||
rooms={getRooms($rooms, selectedFloor._id)}
|
||||
selected
|
||||
isOpen
|
||||
disabled
|
||||
cropped
|
||||
size={'small'}
|
||||
kind={'no-border'}
|
||||
background={'var(--theme-popup-color)'}
|
||||
/>
|
||||
</Scroller>
|
||||
{/if}
|
||||
{#if $floors.length > 1}
|
||||
<div class="flex-row-center flex-reverse flex-no-shrink w-full mt-4">
|
||||
<ModernButton on:click={changeMode} icon={IconLayers} label={love.string.ChangeFloor} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding-right: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
@ -49,6 +49,7 @@
|
||||
|
||||
export let isDock: boolean = false
|
||||
export let room: Ref<TypeRoom>
|
||||
export let canUnpin: boolean = true
|
||||
|
||||
interface ParticipantData {
|
||||
_id: string
|
||||
@ -320,14 +321,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ActionIcon
|
||||
icon={view.icon.Pin}
|
||||
label={isDock ? view.string.Unpin : view.string.Pin}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
dispatch('dock')
|
||||
}}
|
||||
/>
|
||||
{#if canUnpin}
|
||||
<ActionIcon
|
||||
icon={view.icon.Pin}
|
||||
label={isDock ? view.string.Unpin : view.string.Pin}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
dispatch('dock')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if allowLeave}
|
||||
<ActionIcon
|
||||
icon={love.icon.LeaveRoom}
|
||||
|
47
plugins/love-resources/src/components/VideoWidget.svelte
Normal file
47
plugins/love-resources/src/components/VideoWidget.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Room as TypeRoom } from '@hcengineering/love'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { closeWidget, WidgetState } from '@hcengineering/workbench-resources'
|
||||
|
||||
import love from '../plugin'
|
||||
import VideoPopup from './VideoPopup.svelte'
|
||||
|
||||
export let widgetState: WidgetState
|
||||
|
||||
let room: Ref<TypeRoom> | undefined = undefined
|
||||
$: room = widgetState.data?.room
|
||||
|
||||
$: if (widgetState.data?.room === undefined) {
|
||||
closeWidget(love.ids.VideoWidget)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if room}
|
||||
<div class="root">
|
||||
<VideoPopup {room} isDock canUnpin={false} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
background-color: var(--theme-statusbar-color);
|
||||
}
|
||||
</style>
|
@ -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<Resources> => ({
|
||||
WorkbenchExtension,
|
||||
SelectScreenSourcePopup,
|
||||
MeetingData,
|
||||
EditMeetingData
|
||||
EditMeetingData,
|
||||
LoveWidget,
|
||||
VideoWidget
|
||||
},
|
||||
function: {
|
||||
CreateMeeting: createMeeting
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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<Floor>,
|
||||
Reception: '' as Ref<Room>,
|
||||
InviteNotification: '' as Ref<NotificationType>,
|
||||
KnockNotification: '' as Ref<NotificationType>
|
||||
KnockNotification: '' as Ref<NotificationType>,
|
||||
LoveWidget: '' as Ref<Widget>,
|
||||
VideoWidget: '' as Ref<Widget>
|
||||
},
|
||||
icon: {
|
||||
Love: '' as Asset,
|
||||
|
@ -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",
|
||||
|
@ -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<PersonAccount>) : undefined
|
||||
$: sender = senderAccount !== undefined ? $personByIdStore.get(senderAccount.person) : undefined
|
||||
|
||||
async function openChannelInSidebar (): Promise<void> {
|
||||
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<ActivityMessage> | undefined
|
||||
const fn = await getResource(chunter.function.OpenChannelInSidebar)
|
||||
await fn(_id, _class, undefined, thread)
|
||||
}
|
||||
</script>
|
||||
|
||||
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
|
||||
@ -35,10 +56,7 @@
|
||||
<Button
|
||||
label={view.string.Open}
|
||||
on:click={() => {
|
||||
if (value.onClickLocation) {
|
||||
onRemove()
|
||||
navigate(value.onClickLocation)
|
||||
}
|
||||
openChannelInSidebar()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -14,13 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let count: number = 0
|
||||
export let kind: 'primary' | 'secondary' = 'primary'
|
||||
export let size: 'x-small' | 'small' | 'medium' = 'small'
|
||||
export let kind: 'primary' | 'secondary' | 'simple' = 'primary'
|
||||
export let size: 'xx-small' | 'x-small' | 'small' | 'medium' = 'small'
|
||||
|
||||
const maxNumber = 9
|
||||
</script>
|
||||
|
||||
{#if count > 0}
|
||||
{#if kind === 'primary' && count > 0}
|
||||
<div class="notifyMarker {size} {kind}">
|
||||
{#if count > maxNumber}
|
||||
{maxNumber}+
|
||||
@ -30,7 +30,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if count === 0 && kind === 'secondary'}
|
||||
{#if kind === 'secondary'}
|
||||
<div class="notifyMarker {size} {kind}" />
|
||||
{/if}
|
||||
|
||||
{#if kind === 'simple'}
|
||||
<div class="notifyMarker {size} {kind}" />
|
||||
{/if}
|
||||
|
||||
@ -43,6 +47,7 @@
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
|
||||
&.simple,
|
||||
&.primary {
|
||||
background-color: var(--global-higlight-Color);
|
||||
color: var(--global-on-accent-TextColor);
|
||||
@ -52,6 +57,11 @@
|
||||
background-color: var(--global-subtle-BackgroundColor);
|
||||
}
|
||||
|
||||
&.xx-small {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
&.x-small {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { Doc, getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder, Space } from '@hcengineering/core'
|
||||
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
@ -47,10 +47,6 @@
|
||||
import { onDestroy } from 'svelte'
|
||||
import SettingsButton from './SettingsButton.svelte'
|
||||
|
||||
export let currentSpace: Ref<Space> | undefined = undefined
|
||||
export let asideComponent: AnyComponent | undefined = undefined
|
||||
export let asideId: string | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const me = getCurrentAccount()
|
||||
@ -145,7 +141,7 @@
|
||||
$: filteredData = filterData(filter, selectedTabId, inboxData, $contextByIdStore)
|
||||
|
||||
const unsubscribeLoc = locationStore.subscribe((newLocation) => {
|
||||
void syncLocation(newLocation, $contextByDocStore)
|
||||
void syncLocation(newLocation)
|
||||
})
|
||||
|
||||
let isContextsLoaded = false
|
||||
@ -156,11 +152,11 @@
|
||||
}
|
||||
|
||||
const loc = getCurrentLocation()
|
||||
void syncLocation(loc, docs)
|
||||
void syncLocation(loc)
|
||||
isContextsLoaded = true
|
||||
})
|
||||
|
||||
async function syncLocation (newLocation: Location, contextByDoc: Map<Ref<Doc>, DocNotifyContext>): Promise<void> {
|
||||
async function syncLocation (newLocation: Location): Promise<void> {
|
||||
const loc = await resolveLocation(newLocation)
|
||||
if (loc?.loc.path[2] !== notificationId) {
|
||||
return
|
||||
@ -174,7 +170,7 @@
|
||||
|
||||
const [id, _class] = decodeObjectURI(loc?.loc.path[3] ?? '')
|
||||
const _id = await parseLinkId(linkProviders, id, _class)
|
||||
const context = _id ? contextByDoc.get(_id) : undefined
|
||||
const context = _id ? $contextByDocStore.get(_id) : undefined
|
||||
|
||||
selectedContextId = context?._id
|
||||
|
||||
@ -423,7 +419,7 @@
|
||||
short
|
||||
/>
|
||||
{/if}
|
||||
<div bind:this={replacedPanel} class="hulyComponent" class:beforeAside={asideComponent !== undefined && asideId}>
|
||||
<div bind:this={replacedPanel} class="hulyComponent">
|
||||
{#if selectedContext && selectedComponent}
|
||||
<Component
|
||||
is={selectedComponent}
|
||||
@ -438,12 +434,6 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if asideComponent !== undefined && asideId}
|
||||
<Separator name={'inbox'} index={1} color={'var(--theme-divider-color)'} separatorSize={1} />
|
||||
<div class="hulyComponent aside">
|
||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -180,4 +180,7 @@
|
||||
<rect width="18" height="18" x="3" y="3" rx="2"/>
|
||||
<path d="m9 12 2 2 4-4"/>
|
||||
</symbol>
|
||||
<symbol id="details-filled" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8.00049C2 5.79135 3.79086 4.00049 6 4.00049H26C28.2091 4.00049 30 5.79135 30 8.00049V24.0005C30 26.2096 28.2091 28.0005 26 28.0005H6C3.79086 28.0005 2 26.2096 2 24.0005V8.00049ZM20 6.00049H6C4.89543 6.00049 4 6.89592 4 8.00049V24.0005C4 25.1051 4.89543 26.0005 6 26.0005H20V6.00049Z"/>
|
||||
</symbol>
|
||||
</svg>
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
@ -55,5 +55,6 @@ loadMetadata(view.icon, {
|
||||
Circle: `${icons}#circle`,
|
||||
Join: `${icons}#join`,
|
||||
Leave: `${icons}#leave`,
|
||||
Copy: `${icons}#copy`
|
||||
Copy: `${icons}#copy`,
|
||||
DetailsFilled: `${icons}#details-filled`
|
||||
})
|
||||
|
@ -251,7 +251,8 @@ const view = plugin(viewId, {
|
||||
Join: '' as Asset,
|
||||
Leave: '' as Asset,
|
||||
Copy: '' as Asset,
|
||||
TodoList: '' as Asset
|
||||
TodoList: '' as Asset,
|
||||
DetailsFilled: '' as Asset
|
||||
},
|
||||
category: {
|
||||
General: '' as Ref<ActionCategory>,
|
||||
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "Log in anyway",
|
||||
"WorkspaceCreating": "Creation in progress...",
|
||||
"AccessDenied": "Object doesn't exist or you are not permitted to access it.",
|
||||
"UpgradeDownloadProgress": "Downloading upgrade: {percent}%"
|
||||
"UpgradeDownloadProgress": "Downloading upgrade: {percent}%",
|
||||
"Widget": "Widget",
|
||||
"WidgetPreferences": "Widget preferences",
|
||||
"OpenInSidebar": "Open in sidebar",
|
||||
"OpenInSidebarNewTab": "Open in sidebar new tab",
|
||||
"ConfigureWidgets": "Configure widgets"
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "Iniciar sesión de todas formas",
|
||||
"WorkspaceCreating": "Creation in progress...",
|
||||
"AccessDenied": "El objeto no existe o no tienes permiso para acceder a él.",
|
||||
"UpgradeDownloadProgress": "Descargando actualización: {percent}%"
|
||||
"UpgradeDownloadProgress": "Descargando actualización: {percent}%",
|
||||
"Widget": "Widget",
|
||||
"WidgetPreferences": "Preferencias del widget",
|
||||
"OpenInSidebar": "Abrir en la barra lateral",
|
||||
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
|
||||
"ConfigureWidgets": "Configurar widgets"
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "Se connecter quand même",
|
||||
"WorkspaceCreating": "Création en cours...",
|
||||
"AccessDenied": "L'objet n'existe pas ou vous n'êtes pas autorisé à y accéder.",
|
||||
"UpgradeDownloadProgress": "Téléchargement de la mise à jour: {percent}%"
|
||||
"UpgradeDownloadProgress": "Téléchargement de la mise à jour: {percent}%",
|
||||
"Widget": "Widget",
|
||||
"WidgetPreferences": "Préférences du widget",
|
||||
"OpenInSidebar": "Ouvrir dans la barre latérale",
|
||||
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
|
||||
"ConfigureWidgets": "Configurer les widgets"
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "Entrar de qualquer maneira",
|
||||
"WorkspaceCreating": "Creation in progress...",
|
||||
"AccessDenied": "O objeto não existe ou você não tem permissão para acessá-lo.",
|
||||
"UpgradeDownloadProgress": "Baixando atualização: {percent}%"
|
||||
"UpgradeDownloadProgress": "Baixando atualização: {percent}%",
|
||||
"Widget": "Widget",
|
||||
"WidgetPreferences": "Preferências do widget",
|
||||
"OpenInSidebar": "Abrir na barra lateral",
|
||||
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
|
||||
"ConfigureWidgets": "Configurar widgets"
|
||||
}
|
||||
}
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "Все равно войти",
|
||||
"WorkspaceCreating": "Пространство создается...",
|
||||
"AccessDenied": "Объект не существует или у вас нет прав доступа.",
|
||||
"UpgradeDownloadProgress": "Загрузка обновления: {percent}%"
|
||||
"UpgradeDownloadProgress": "Загрузка обновления: {percent}%",
|
||||
"Widget": "Виджет",
|
||||
"WidgetPreferences": "Настройки виджета",
|
||||
"OpenInSidebar": "Открыть в боковой панели",
|
||||
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
|
||||
"ConfigureWidgets": "Настроить виджеты"
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,11 @@
|
||||
"LogInAnyway": "仍然登录",
|
||||
"WorkspaceCreating": "创建进行中...",
|
||||
"AccessDenied": "对象不存在或您无权访问",
|
||||
"UpgradeDownloadProgress": "正在下载更新:{percent}%"
|
||||
"UpgradeDownloadProgress": "正在下载更新:{percent}%",
|
||||
"Widget": "小部件",
|
||||
"WidgetPreferences": "小部件首选项",
|
||||
"OpenInSidebar": "在侧边栏中打开",
|
||||
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
|
||||
"ConfigureWidgets": "配置小部件"
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,8 @@
|
||||
rootBarExtensions,
|
||||
setResolvedLocation,
|
||||
showPopup,
|
||||
workbenchSeparators
|
||||
workbenchSeparators,
|
||||
mainSeparators
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import {
|
||||
@ -93,6 +94,8 @@
|
||||
import SelectWorkspaceMenu from './SelectWorkspaceMenu.svelte'
|
||||
import SpaceView from './SpaceView.svelte'
|
||||
import TopMenu from './icons/TopMenu.svelte'
|
||||
import WidgetsBar from './sidebar/Sidebar.svelte'
|
||||
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
|
||||
|
||||
let contentPanel: HTMLElement
|
||||
|
||||
@ -150,6 +153,7 @@
|
||||
$workspacesStore = await getWorkspaceFn()
|
||||
await updateWindowTitle(getLocation())
|
||||
})
|
||||
syncSidebarState()
|
||||
})
|
||||
|
||||
const account = getCurrentAccount() as PersonAccount
|
||||
@ -607,6 +611,7 @@
|
||||
let lastLoc: Location | undefined = undefined
|
||||
|
||||
defineSeparators('workbench', workbenchSeparators)
|
||||
defineSeparators('main', mainSeparators)
|
||||
|
||||
$: mainNavigator = currentApplication && navigatorModel && navigator && $deviceInfo.navigator.visible
|
||||
$: elementPanel = $deviceInfo.replacedPanel ?? contentPanel
|
||||
@ -615,6 +620,14 @@
|
||||
person && client.getHierarchy().hasMixin(person, contact.mixin.Employee)
|
||||
? !client.getHierarchy().as(person, contact.mixin.Employee).active
|
||||
: false
|
||||
|
||||
let asideComponent: AnyComponent | undefined
|
||||
|
||||
$: if (asideId !== undefined && navigatorModel !== undefined) {
|
||||
asideComponent = navigatorModel?.aside ?? currentApplication?.aside
|
||||
} else {
|
||||
asideComponent = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if person && deactivated && !isAdminUser()}
|
||||
@ -852,17 +865,18 @@
|
||||
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if asideId && navigatorModel !== undefined}
|
||||
{@const asideComponent = navigatorModel?.aside ?? currentApplication?.aside}
|
||||
{#if asideComponent !== undefined}
|
||||
<Separator name={'workbench'} index={1} color={'transparent'} separatorSize={0} short />
|
||||
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
|
||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if asideComponent !== undefined}
|
||||
<Separator name={'workbench'} index={1} color={'transparent'} separatorSize={0} short />
|
||||
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
|
||||
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $sidebarStore.variant === SidebarVariant.EXPANDED}
|
||||
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short />
|
||||
{/if}
|
||||
<WidgetsBar />
|
||||
<Dock />
|
||||
<div bind:this={cover} class="cover" />
|
||||
<TooltipInstance />
|
||||
|
@ -0,0 +1,70 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { WidgetPreference } from '@hcengineering/workbench'
|
||||
|
||||
import workbench from '../../plugin'
|
||||
import { sidebarStore, SidebarVariant } from '../../sidebar'
|
||||
import WidgetsBarMini from './SidebarMini.svelte'
|
||||
import WidgetsBarExpanded from './SidebarExpanded.svelte'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const widgets = client.getModel().findAllSync(workbench.class.Widget, {})
|
||||
const preferencesQuery = createQuery()
|
||||
|
||||
let preferences: WidgetPreference[] = []
|
||||
$: preferencesQuery.query(workbench.class.WidgetPreference, {}, (res) => {
|
||||
preferences = res
|
||||
})
|
||||
|
||||
$: widgetId = $sidebarStore.widget
|
||||
$: widget = widgets.find((it) => it._id === widgetId)
|
||||
$: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : widget?.size
|
||||
</script>
|
||||
|
||||
<div class="antiPanel-component antiComponent root size-{size}" id="sidebar">
|
||||
{#if $sidebarStore.variant === SidebarVariant.MINI}
|
||||
<WidgetsBarMini {widgets} {preferences} />
|
||||
{:else if $sidebarStore.variant === SidebarVariant.EXPANDED}
|
||||
<WidgetsBarExpanded {widgets} {preferences} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
position: relative;
|
||||
background-color: var(--theme-panel-color);
|
||||
|
||||
&.size-mini {
|
||||
width: 3.5rem !important;
|
||||
min-width: 3.5rem !important;
|
||||
max-width: 3.5rem !important;
|
||||
}
|
||||
|
||||
&.size-small {
|
||||
width: 10rem !important;
|
||||
min-width: 10rem !important;
|
||||
max-width: 10rem !important;
|
||||
}
|
||||
|
||||
&.size-medium {
|
||||
width: 25rem !important;
|
||||
min-width: 25rem !important;
|
||||
max-width: 25rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,172 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget, WidgetPreference, WidgetTab } from '@hcengineering/workbench'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import {
|
||||
Component,
|
||||
resizeObserver,
|
||||
location as locationStore,
|
||||
Location,
|
||||
Header,
|
||||
Breadcrumbs
|
||||
} from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import { closeWidgetTab, sidebarStore, SidebarVariant, WidgetState, openWidgetTab, closeWidget } from '../../sidebar'
|
||||
import WidgetsBar from './widgets/WidgetsBar.svelte'
|
||||
import SidebarTabs from './SidebarTabs.svelte'
|
||||
|
||||
export let widgets: Widget[] = []
|
||||
export let preferences: WidgetPreference[] = []
|
||||
|
||||
let widgetId: Ref<Widget> | undefined = undefined
|
||||
let widget: Widget | undefined = undefined
|
||||
let widgetState: WidgetState | undefined = undefined
|
||||
|
||||
let tabId: string | undefined = undefined
|
||||
let tab: WidgetTab | undefined = undefined
|
||||
let tabs: WidgetTab[] = []
|
||||
|
||||
$: widgetId = $sidebarStore.widget
|
||||
$: widget = widgetId !== undefined ? widgets.find((it) => it._id === widgetId) : undefined
|
||||
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
|
||||
|
||||
$: tabId = widgetState?.tab
|
||||
$: tabs = widgetState?.tabs ?? []
|
||||
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
|
||||
|
||||
$: if ($sidebarStore.widget === undefined) {
|
||||
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
|
||||
}
|
||||
|
||||
const unsubscribe = locationStore.subscribe((loc: Location) => {
|
||||
if (widget === undefined) return
|
||||
|
||||
for (const tab of tabs) {
|
||||
if (tab.allowedPath !== undefined && !tab.isPinned) {
|
||||
const path = loc.path.join('/')
|
||||
if (!path.startsWith(tab.allowedPath)) {
|
||||
void handleTabClose(tab.id, widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
async function handleTabClose (tabId: string, widget?: Widget): Promise<void> {
|
||||
if (widget === undefined) return
|
||||
await closeWidgetTab(widget, tabId)
|
||||
}
|
||||
|
||||
function handleTabOpen (tabId: string, widget?: Widget): void {
|
||||
if (widget === undefined) return
|
||||
openWidgetTab(widget._id, tabId)
|
||||
}
|
||||
|
||||
let componentHeight = '0px'
|
||||
let componentWidth = '0px'
|
||||
|
||||
function resize (element: Element): void {
|
||||
componentHeight = `${element.clientHeight}px`
|
||||
componentWidth = `${element.clientWidth}px`
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="content">
|
||||
{#if widget?.component}
|
||||
<div class="component" use:resizeObserver={resize}>
|
||||
{#if widget.headerLabel}
|
||||
<Header
|
||||
allowFullsize={false}
|
||||
type="type-aside"
|
||||
hideBefore={true}
|
||||
hideActions={false}
|
||||
hideDescription={true}
|
||||
adaptive="disabled"
|
||||
on:close={() => {
|
||||
if (widget !== undefined) {
|
||||
closeWidget(widget._id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs items={[{ label: widget.headerLabel }]} currentOnly />
|
||||
</Header>
|
||||
{/if}
|
||||
<Component
|
||||
is={widget?.component}
|
||||
props={{ tab, widgetState, height: componentHeight, width: componentWidth, widget }}
|
||||
on:close={() => {
|
||||
if (widget !== undefined) {
|
||||
closeWidget(widget._id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if widget !== undefined && tabs.length > 0}
|
||||
<SidebarTabs
|
||||
{tabs}
|
||||
selected={tab?.id}
|
||||
{widget}
|
||||
on:close={(e) => {
|
||||
void handleTabClose(e.detail, widget)
|
||||
}}
|
||||
on:open={(e) => {
|
||||
handleTabOpen(e.detail, widget)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<WidgetsBar {widgets} {preferences} selected={widgetId} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
overflow: auto;
|
||||
|
||||
flex: 1;
|
||||
width: calc(100% - 3.5rem);
|
||||
height: 100%;
|
||||
background: var(--theme-panel-color);
|
||||
|
||||
border-right: 1px solid var(--global-ui-BorderColor);
|
||||
border-top-left-radius: var(--small-focus-BorderRadius);
|
||||
border-bottom-left-radius: var(--small-focus-BorderRadius);
|
||||
}
|
||||
|
||||
.component {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget, WidgetPreference } from '@hcengineering/workbench'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
import WidgetsBar from './widgets/WidgetsBar.svelte'
|
||||
|
||||
export let widgets: Widget[] = []
|
||||
export let preferences: WidgetPreference[] = []
|
||||
export let selected: Ref<Widget> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<WidgetsBar {widgets} {preferences} {selected} />
|
@ -0,0 +1,57 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Action, Menu, ModernTab, showPopup } from '@hcengineering/ui'
|
||||
import { Widget, WidgetTab } from '@hcengineering/workbench'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
export let tab: WidgetTab
|
||||
export let widget: Widget
|
||||
export let selected = false
|
||||
export let actions: Action[] = []
|
||||
|
||||
$: icon = tab.icon ?? widget.icon
|
||||
|
||||
$: if (tab.iconComponent) {
|
||||
void getResource(tab.iconComponent).then((res) => {
|
||||
icon = res
|
||||
})
|
||||
}
|
||||
|
||||
function handleMenu (event: MouseEvent): void {
|
||||
if (actions.length === 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
showPopup(Menu, { actions }, event.target as HTMLElement)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModernTab
|
||||
label={tab.name}
|
||||
labelIntl={tab.nameIntl ?? widget.label}
|
||||
highlighted={selected}
|
||||
orientation="vertical"
|
||||
kind={tab.isPinned ? 'secondary' : 'primary'}
|
||||
{icon}
|
||||
iconProps={tab.iconProps}
|
||||
canClose={!tab.isPinned}
|
||||
maxSize="13.5rem"
|
||||
on:close
|
||||
on:click
|
||||
on:contextmenu={handleMenu}
|
||||
/>
|
@ -0,0 +1,99 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget, WidgetTab } from '@hcengineering/workbench'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { Action, Component } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import SidebarTab from './SidebarTab.svelte'
|
||||
import { closeWidgetTab, pinWidgetTab, unpinWidgetTab } from '../../sidebar'
|
||||
|
||||
export let tabs: WidgetTab[] = []
|
||||
export let widget: Widget
|
||||
export let selected: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getActions (tab: WidgetTab): Action[] {
|
||||
const pinAction: Action = {
|
||||
label: view.string.Pin,
|
||||
icon: view.icon.Pin,
|
||||
action: async () => {
|
||||
pinWidgetTab(widget, tab.id)
|
||||
}
|
||||
}
|
||||
const unpinAction: Action = {
|
||||
label: view.string.Unpin,
|
||||
icon: view.icon.Pin,
|
||||
action: async () => {
|
||||
unpinWidgetTab(widget, tab.id)
|
||||
}
|
||||
}
|
||||
const closeAction: Action = {
|
||||
label: presentation.string.Close,
|
||||
icon: view.icon.Delete,
|
||||
action: async () => {
|
||||
void closeWidgetTab(widget, tab.id)
|
||||
}
|
||||
}
|
||||
return [tab.isPinned ? unpinAction : pinAction, closeAction]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tabs">
|
||||
{#each tabs as tab}
|
||||
{#if widget.tabComponent}
|
||||
<Component
|
||||
is={widget.tabComponent}
|
||||
props={{ tab, widget, selected: tab.id === selected, actions: getActions(tab) }}
|
||||
on:close={() => dispatch('close', tab.id)}
|
||||
on:click={() => {
|
||||
dispatch('open', tab.id)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<SidebarTab
|
||||
{tab}
|
||||
{widget}
|
||||
actions={getActions(tab)}
|
||||
selected={tab.id === selected}
|
||||
on:close={() => dispatch('close', tab.id)}
|
||||
on:click={() => {
|
||||
dispatch('open', tab.id)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
max-width: 30px;
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--theme-divider-color);
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,104 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench'
|
||||
import { CheckBox, Grid, Modal } from '@hcengineering/ui'
|
||||
import core, { Ref } from '@hcengineering/core'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import WidgetPresenter from './WidgetPresenter.svelte'
|
||||
import workbench from '../../../plugin'
|
||||
|
||||
export let widgets: Widget[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const preferencesQuery = createQuery()
|
||||
|
||||
const widgetsUpdates = new Map<Ref<Widget>, boolean>()
|
||||
|
||||
let preferences: WidgetPreference[] = []
|
||||
$: preferencesQuery.query(workbench.class.WidgetPreference, {}, (res) => {
|
||||
preferences = res
|
||||
})
|
||||
|
||||
function handleClose (): void {
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function isEnabled (
|
||||
widget: Widget,
|
||||
widgetsUpdates: Map<Ref<Widget>, boolean>,
|
||||
preference?: WidgetPreference
|
||||
): boolean {
|
||||
return widgetsUpdates.get(widget._id) ?? preference?.enabled ?? false
|
||||
}
|
||||
|
||||
function handleCheck (widget: Widget, preference?: WidgetPreference): void {
|
||||
const newValue = !isEnabled(widget, widgetsUpdates, preference)
|
||||
|
||||
widgetsUpdates.set(widget._id, newValue)
|
||||
}
|
||||
|
||||
async function handleApply (): Promise<void> {
|
||||
for (const [widget, enabled] of widgetsUpdates) {
|
||||
const preference = preferences.find((it) => it.attachedTo === widget)
|
||||
if (preference !== undefined) {
|
||||
await client.diffUpdate(preference, { enabled })
|
||||
} else {
|
||||
await client.createDoc(workbench.class.WidgetPreference, core.space.Workspace, { attachedTo: widget, enabled })
|
||||
}
|
||||
}
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
label={workbench.string.ConfigureWidgets}
|
||||
type="type-popup"
|
||||
okLabel={presentation.string.Save}
|
||||
okAction={handleApply}
|
||||
canSave={true}
|
||||
onCancel={handleClose}
|
||||
on:close
|
||||
>
|
||||
<Grid column={1} rowGap={0.5}>
|
||||
{#each widgets as widget}
|
||||
{#if widget.type === WidgetType.Configurable}
|
||||
{@const preference = preferences.find((it) => it.attachedTo === widget._id)}
|
||||
<div class="item">
|
||||
<CheckBox
|
||||
size="small"
|
||||
checked={isEnabled(widget, widgetsUpdates, preference)}
|
||||
on:value={() => {
|
||||
handleCheck(widget, preference)
|
||||
}}
|
||||
/>
|
||||
<WidgetPresenter {widget} withLabel />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Grid>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget } from '@hcengineering/workbench'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
import AppItem from '../../AppItem.svelte'
|
||||
|
||||
export let widget: Widget
|
||||
export let withLabel = false
|
||||
export let highlighted = false
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<AppItem label={widget.label} icon={widget.icon} selected={highlighted} size="small" on:click />
|
||||
{#if withLabel}
|
||||
<Label label={widget.label} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,120 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench'
|
||||
import { IconSettings, ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
import WidgetPresenter from './/WidgetPresenter.svelte'
|
||||
import AddWidgetsPopup from './AddWidgetsPopup.svelte'
|
||||
import { openWidget, sidebarStore, SidebarVariant } from '../../../sidebar'
|
||||
|
||||
export let widgets: Widget[] = []
|
||||
export let preferences: WidgetPreference[] = []
|
||||
export let selected: Ref<Widget> | undefined = undefined
|
||||
|
||||
function handleAddWidget (): void {
|
||||
showPopup(AddWidgetsPopup, { widgets })
|
||||
}
|
||||
|
||||
function handleSelectWidget (widget: Widget): void {
|
||||
if (selected === widget._id) {
|
||||
sidebarStore.update((state) => ({ ...state, widget: undefined, variant: SidebarVariant.MINI }))
|
||||
} else {
|
||||
openWidget(widget)
|
||||
}
|
||||
}
|
||||
|
||||
$: fixedWidgets = widgets.filter((widget) => widget.type === WidgetType.Fixed)
|
||||
$: flexibleWidgets = widgets.filter(
|
||||
(widget) => widget.type === WidgetType.Flexible && $sidebarStore.widgetsState.has(widget._id)
|
||||
)
|
||||
$: configurableWidgets = preferences
|
||||
.filter((it) => it.enabled)
|
||||
.sort((a, b) => a.modifiedOn - b.modifiedOn)
|
||||
.map((it) => widgets.find((widget) => widget._id === it.attachedTo))
|
||||
.filter((widget): widget is Widget => widget !== undefined && widget.type === WidgetType.Configurable)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="block">
|
||||
{#each fixedWidgets as widget}
|
||||
<WidgetPresenter
|
||||
{widget}
|
||||
highlighted={widget._id === selected}
|
||||
on:click={() => {
|
||||
handleSelectWidget(widget)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{#if configurableWidgets.length > 0}
|
||||
<div class="separator" />
|
||||
{#each configurableWidgets as widget}
|
||||
<WidgetPresenter
|
||||
{widget}
|
||||
highlighted={widget._id === selected}
|
||||
on:click={() => {
|
||||
handleSelectWidget(widget)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if flexibleWidgets.length > 0}
|
||||
<div class="separator" />
|
||||
{#each flexibleWidgets as widget}
|
||||
<WidgetPresenter
|
||||
{widget}
|
||||
highlighted={widget._id === selected}
|
||||
on:click={() => {
|
||||
handleSelectWidget(widget)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if widgets.some((widget) => widget.type === WidgetType.Configurable)}
|
||||
<div class="separator" />
|
||||
<ModernButton icon={IconSettings} size="small" on:click={handleAddWidget} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 0.5rem 0;
|
||||
width: 3.5rem;
|
||||
min-width: 3.5rem;
|
||||
max-width: 3.5rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 2rem;
|
||||
height: 1px;
|
||||
background-color: var(--global-ui-BorderColor);
|
||||
}
|
||||
</style>
|
@ -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<Resources> => ({
|
||||
component: {
|
||||
WorkbenchApp,
|
||||
|
@ -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<boolean>
|
||||
|
294
plugins/workbench-resources/src/sidebar.ts
Normal file
294
plugins/workbench-resources/src/sidebar.ts
Normal file
@ -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<Widget>
|
||||
data?: Record<string, any>
|
||||
tabs: WidgetTab[]
|
||||
tab?: string
|
||||
}
|
||||
|
||||
export interface SidebarState {
|
||||
variant: SidebarVariant
|
||||
widgetsState: Map<Ref<Widget>, WidgetState>
|
||||
widget?: Ref<Widget>
|
||||
}
|
||||
|
||||
export const defaultSidebarState: SidebarState = {
|
||||
variant: SidebarVariant.MINI,
|
||||
widgetsState: new Map()
|
||||
}
|
||||
|
||||
export const sidebarStore = writable<SidebarState>(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<string, any>): 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<Widget>): 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<void> {
|
||||
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<Widget>, 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)
|
||||
}
|
@ -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<number | undefined>(undefined)
|
||||
|
||||
@ -155,6 +157,7 @@ export async function showApplication (app: Application): Promise<void> {
|
||||
}
|
||||
|
||||
export const workspacesStore = writable<Workspace[]>([])
|
||||
export const workspaceStore = derived(location, (loc: Location) => loc.path[1])
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -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<void>>
|
||||
}
|
||||
|
||||
export interface WidgetPreference extends Preference {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface WidgetTab {
|
||||
id: string
|
||||
name?: string
|
||||
nameIntl?: IntlString
|
||||
icon?: Asset | AnySvelteComponent
|
||||
iconComponent?: AnyComponent
|
||||
iconProps?: Record<string, any>
|
||||
widget?: Ref<Widget>
|
||||
isPinned?: boolean
|
||||
allowedPath?: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -141,7 +184,9 @@ export default plugin(workbenchId, {
|
||||
class: {
|
||||
Application: '' as Ref<Class<Application>>,
|
||||
ApplicationNavModel: '' as Ref<Class<ApplicationNavModel>>,
|
||||
HiddenApplication: '' as Ref<Class<HiddenApplication>>
|
||||
HiddenApplication: '' as Ref<Class<HiddenApplication>>,
|
||||
Widget: '' as Ref<Class<Widget>>,
|
||||
WidgetPreference: '' as Ref<Class<WidgetPreference>>
|
||||
},
|
||||
mixin: {
|
||||
SpaceView: '' as Ref<Mixin<SpaceView>>
|
||||
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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<void> {
|
||||
if (messageExists) {
|
||||
await expect(this.textMessageInSidebar(messageText)).toBeVisible()
|
||||
} else {
|
||||
await expect(this.textMessageInSidebar(messageText)).toBeHidden()
|
||||
}
|
||||
}
|
||||
|
||||
async checkIfEmojiIsAdded (emoji: string): Promise<void> {
|
||||
await expect(this.selectEmoji(emoji + ' 1')).toBeVisible()
|
||||
}
|
||||
|
@ -232,19 +232,15 @@ export class PlanningPage extends CalendarPage {
|
||||
}
|
||||
|
||||
async openToDoByName (toDoName: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
Loading…
Reference in New Issue
Block a user