mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
Add workbench tabs (#6788)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
ce2621d472
commit
b5fed4879f
@ -64,6 +64,7 @@ export function createModel (builder: Builder): void {
|
|||||||
core.space.Model,
|
core.space.Model,
|
||||||
{
|
{
|
||||||
label: chunter.string.ApplicationLabelChunter,
|
label: chunter.string.ApplicationLabelChunter,
|
||||||
|
locationDataResolver: chunter.function.LocationDataResolver,
|
||||||
icon: chunter.icon.Chunter,
|
icon: chunter.icon.Chunter,
|
||||||
alias: chunterId,
|
alias: chunterId,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
@ -87,6 +88,11 @@ export function createModel (builder: Builder): void {
|
|||||||
chunter.ids.ChatWidget
|
chunter.ids.ChatWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
|
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
|
||||||
|
extension: workbench.extensions.WorkbenchTabExtensions,
|
||||||
|
component: chunter.component.WorkbenchTabExtension
|
||||||
|
})
|
||||||
|
|
||||||
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
|
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
|
||||||
|
|
||||||
spaceClasses.forEach((spaceClass) => {
|
spaceClasses.forEach((spaceClass) => {
|
||||||
|
@ -22,7 +22,7 @@ import type { IntlString, Resource } from '@hcengineering/platform'
|
|||||||
import { mergeIds } from '@hcengineering/platform'
|
import { mergeIds } from '@hcengineering/platform'
|
||||||
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
|
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
|
||||||
import type { Action, ActionCategory, ViewAction, Viewlet, ViewletDescriptor } from '@hcengineering/view'
|
import type { Action, ActionCategory, ViewAction, Viewlet, ViewletDescriptor } from '@hcengineering/view'
|
||||||
import { type WidgetTab } from '@hcengineering/workbench'
|
import { type WidgetTab, type LocationData } from '@hcengineering/workbench'
|
||||||
|
|
||||||
export default mergeIds(chunterId, chunter, {
|
export default mergeIds(chunterId, chunter, {
|
||||||
component: {
|
component: {
|
||||||
@ -34,7 +34,8 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
ChatWidgetTab: '' as AnyComponent,
|
ChatWidgetTab: '' as AnyComponent,
|
||||||
ChatMessageNotificationLabel: '' as AnyComponent,
|
ChatMessageNotificationLabel: '' as AnyComponent,
|
||||||
ThreadNotificationPresenter: '' as AnyComponent,
|
ThreadNotificationPresenter: '' as AnyComponent,
|
||||||
JoinChannelNotificationPresenter: '' as AnyComponent
|
JoinChannelNotificationPresenter: '' as AnyComponent,
|
||||||
|
WorkbenchTabExtension: '' as AnyComponent
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
MarkCommentUnread: '' as Ref<Action>,
|
MarkCommentUnread: '' as Ref<Action>,
|
||||||
@ -108,7 +109,8 @@ export default mergeIds(chunterId, chunter, {
|
|||||||
ReplyToThread: '' as Resource<(doc: ActivityMessage, event: MouseEvent) => Promise<void>>,
|
ReplyToThread: '' as Resource<(doc: ActivityMessage, event: MouseEvent) => Promise<void>>,
|
||||||
CanReplyToThread: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
|
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>>
|
CloseChatWidgetTab: '' as Resource<(tab: WidgetTab) => Promise<void>>,
|
||||||
|
LocationDataResolver: '' as Resource<(loc: Location) => Promise<LocationData>>
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"@hcengineering/model-attachment": "^0.6.0",
|
"@hcengineering/model-attachment": "^0.6.0",
|
||||||
"@hcengineering/model-chunter": "^0.6.0",
|
"@hcengineering/model-chunter": "^0.6.0",
|
||||||
"@hcengineering/model-core": "^0.6.0",
|
"@hcengineering/model-core": "^0.6.0",
|
||||||
|
"@hcengineering/model-guest": "^0.6.0",
|
||||||
"@hcengineering/model-notification": "^0.6.0",
|
"@hcengineering/model-notification": "^0.6.0",
|
||||||
"@hcengineering/model-presentation": "^0.6.0",
|
"@hcengineering/model-presentation": "^0.6.0",
|
||||||
"@hcengineering/model-view": "^0.6.0",
|
"@hcengineering/model-view": "^0.6.0",
|
||||||
@ -48,7 +49,7 @@
|
|||||||
"@hcengineering/templates": "^0.6.11",
|
"@hcengineering/templates": "^0.6.11",
|
||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
"@hcengineering/model-guest": "^0.6.0",
|
"@hcengineering/workbench": "^0.6.16",
|
||||||
"cross-fetch": "^3.1.5"
|
"cross-fetch": "^3.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,6 +324,7 @@ export function createModel (builder: Builder): void {
|
|||||||
hidden: false,
|
hidden: false,
|
||||||
// component: contact.component.ContactsTabs,
|
// component: contact.component.ContactsTabs,
|
||||||
locationResolver: contact.resolver.Location,
|
locationResolver: contact.resolver.Location,
|
||||||
|
locationDataResolver: contact.resolver.LocationData,
|
||||||
navigatorModel: {
|
navigatorModel: {
|
||||||
spaces: [],
|
spaces: [],
|
||||||
specials: [
|
specials: [
|
||||||
|
@ -21,9 +21,10 @@ import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineer
|
|||||||
import { type NotificationGroup } from '@hcengineering/notification'
|
import { type NotificationGroup } from '@hcengineering/notification'
|
||||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||||
import { type TemplateFieldFunc } from '@hcengineering/templates'
|
import { type TemplateFieldFunc } from '@hcengineering/templates'
|
||||||
import type { AnyComponent } from '@hcengineering/ui/src/types'
|
import { type AnyComponent, type Location } from '@hcengineering/ui/src/types'
|
||||||
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
|
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
|
||||||
import { type ChatMessageViewlet } from '@hcengineering/chunter'
|
import { type ChatMessageViewlet } from '@hcengineering/chunter'
|
||||||
|
import { type LocationData } from '@hcengineering/workbench'
|
||||||
|
|
||||||
export default mergeIds(contactId, contact, {
|
export default mergeIds(contactId, contact, {
|
||||||
activity: {
|
activity: {
|
||||||
@ -63,7 +64,6 @@ export default mergeIds(contactId, contact, {
|
|||||||
ChannelIcon: '' as AnyComponent
|
ChannelIcon: '' as AnyComponent
|
||||||
},
|
},
|
||||||
string: {
|
string: {
|
||||||
Persons: '' as IntlString,
|
|
||||||
SearchEmployee: '' as IntlString,
|
SearchEmployee: '' as IntlString,
|
||||||
SearchPerson: '' as IntlString,
|
SearchPerson: '' as IntlString,
|
||||||
SearchOrganization: '' as IntlString,
|
SearchOrganization: '' as IntlString,
|
||||||
@ -97,7 +97,6 @@ export default mergeIds(contactId, contact, {
|
|||||||
|
|
||||||
ConfigLabel: '' as IntlString,
|
ConfigLabel: '' as IntlString,
|
||||||
ConfigDescription: '' as IntlString,
|
ConfigDescription: '' as IntlString,
|
||||||
Employees: '' as IntlString,
|
|
||||||
People: '' as IntlString
|
People: '' as IntlString
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
@ -142,5 +141,8 @@ export default mergeIds(contactId, contact, {
|
|||||||
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||||
SetPersonStore: '' as Resource<(manager: DocManager<any>) => void>,
|
SetPersonStore: '' as Resource<(manager: DocManager<any>) => void>,
|
||||||
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
|
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
|
||||||
|
},
|
||||||
|
resolver: {
|
||||||
|
LocationData: '' as Resource<(loc: Location) => Promise<LocationData>>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -30,13 +30,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/core": "^0.6.32",
|
"@hcengineering/core": "^0.6.32",
|
||||||
"@hcengineering/model": "^0.6.11",
|
"@hcengineering/model": "^0.6.11",
|
||||||
"@hcengineering/platform": "^0.6.11",
|
|
||||||
"@hcengineering/model-core": "^0.6.0",
|
"@hcengineering/model-core": "^0.6.0",
|
||||||
"@hcengineering/workbench": "^0.6.16",
|
"@hcengineering/model-preference": "^0.6.0",
|
||||||
|
"@hcengineering/model-presentation": "^0.6.0",
|
||||||
|
"@hcengineering/model-view": "^0.6.0",
|
||||||
|
"@hcengineering/platform": "^0.6.11",
|
||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
"@hcengineering/model-view": "^0.6.0",
|
"@hcengineering/workbench": "^0.6.16",
|
||||||
"@hcengineering/workbench-resources": "^0.6.1",
|
"@hcengineering/workbench-resources": "^0.6.1"
|
||||||
"@hcengineering/model-preference": "^0.6.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,12 @@ import type {
|
|||||||
WidgetPreference,
|
WidgetPreference,
|
||||||
WidgetTab,
|
WidgetTab,
|
||||||
WidgetType,
|
WidgetType,
|
||||||
SidebarEvent
|
SidebarEvent,
|
||||||
|
WorkbenchTab
|
||||||
} from '@hcengineering/workbench'
|
} from '@hcengineering/workbench'
|
||||||
import { type AnyComponent } from '@hcengineering/ui'
|
import { type AnyComponent } from '@hcengineering/ui'
|
||||||
import core, { TClass, TDoc, TTx } from '@hcengineering/model-core'
|
import core, { TClass, TDoc, TTx } from '@hcengineering/model-core'
|
||||||
|
import presentation from '@hcengineering/model-presentation'
|
||||||
|
|
||||||
import workbench from './plugin'
|
import workbench from './plugin'
|
||||||
|
|
||||||
@ -98,6 +100,13 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent {
|
|||||||
params!: Record<string, any>
|
params!: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Model(workbench.class.WorkbenchTab, preference.class.Preference)
|
||||||
|
@UX(workbench.string.Tab)
|
||||||
|
export class TWorkbenchTab extends TPreference implements WorkbenchTab {
|
||||||
|
location!: string
|
||||||
|
isPinned!: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(
|
builder.createModel(
|
||||||
TApplication,
|
TApplication,
|
||||||
@ -106,7 +115,8 @@ export function createModel (builder: Builder): void {
|
|||||||
TApplicationNavModel,
|
TApplicationNavModel,
|
||||||
TWidget,
|
TWidget,
|
||||||
TWidgetPreference,
|
TWidgetPreference,
|
||||||
TTxSidebarEvent
|
TTxSidebarEvent,
|
||||||
|
TWorkbenchTab
|
||||||
)
|
)
|
||||||
|
|
||||||
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
|
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
@ -133,6 +143,52 @@ export function createModel (builder: Builder): void {
|
|||||||
mode: ['workbench']
|
mode: ['workbench']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: workbench.actionImpl.PinTab,
|
||||||
|
label: view.string.Pin,
|
||||||
|
icon: view.icon.Pin,
|
||||||
|
input: 'focus',
|
||||||
|
category: workbench.category.Workbench,
|
||||||
|
target: workbench.class.WorkbenchTab,
|
||||||
|
query: {
|
||||||
|
isPinned: false
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: workbench.actionImpl.UnpinTab,
|
||||||
|
label: view.string.Unpin,
|
||||||
|
icon: view.icon.Pin,
|
||||||
|
input: 'focus',
|
||||||
|
category: workbench.category.Workbench,
|
||||||
|
target: workbench.class.WorkbenchTab,
|
||||||
|
query: {
|
||||||
|
isPinned: true
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
action: workbench.actionImpl.CloseTab,
|
||||||
|
label: presentation.string.Close,
|
||||||
|
icon: view.icon.Delete,
|
||||||
|
input: 'focus',
|
||||||
|
category: workbench.category.Workbench,
|
||||||
|
target: workbench.class.WorkbenchTab,
|
||||||
|
visibilityTester: workbench.function.CanCloseTab,
|
||||||
|
context: {
|
||||||
|
mode: 'context',
|
||||||
|
group: 'edit'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default workbench
|
export default workbench
|
||||||
|
@ -13,11 +13,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Doc, type Space } from '@hcengineering/core'
|
import { type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||||
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
|
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
|
||||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||||
import { workbenchId } from '@hcengineering/workbench'
|
import { workbenchId } from '@hcengineering/workbench'
|
||||||
import workbench from '@hcengineering/workbench-resources/src/plugin'
|
import workbench from '@hcengineering/workbench-resources/src/plugin'
|
||||||
|
import type { ActionCategory, ViewActionAvailabilityFunction } from '@hcengineering/view'
|
||||||
|
|
||||||
export default mergeIds(workbenchId, workbench, {
|
export default mergeIds(workbenchId, workbench, {
|
||||||
component: {
|
component: {
|
||||||
@ -30,6 +31,15 @@ export default mergeIds(workbenchId, workbench, {
|
|||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
|
HasArchiveSpaces: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
|
||||||
IsOwner: '' as Resource<(docs: Doc[]) => Promise<boolean>>
|
IsOwner: '' as Resource<(docs: Doc[]) => Promise<boolean>>,
|
||||||
|
CanCloseTab: '' as Resource<ViewActionAvailabilityFunction<Doc>>
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
Workbench: '' as Ref<ActionCategory>
|
||||||
|
},
|
||||||
|
actionImpl: {
|
||||||
|
PinTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>,
|
||||||
|
UnpinTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>,
|
||||||
|
CloseTab: '' as Resource<(doc?: Doc | Doc[]) => Promise<void>>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -292,6 +292,8 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-radius: var(--small-focus-BorderRadius);
|
border-radius: var(--small-focus-BorderRadius);
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius:0 ;
|
||||||
|
|
||||||
&:not(.rowContent) { flex-direction: column; }
|
&:not(.rowContent) { flex-direction: column; }
|
||||||
.panel-instance & {
|
.panel-instance & {
|
||||||
|
@ -150,6 +150,7 @@
|
|||||||
.icon {
|
.icon {
|
||||||
writing-mode: initial;
|
writing-mode: initial;
|
||||||
text-orientation: initial;
|
text-orientation: initial;
|
||||||
|
margin: 0;
|
||||||
&.vertical {
|
&.vertical {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
text-orientation: upright;
|
text-orientation: upright;
|
||||||
@ -158,6 +159,10 @@
|
|||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
&.vertical {
|
&.vertical {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
@ -208,6 +208,8 @@
|
|||||||
z-index: 401;
|
z-index: 401;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
position: static;
|
position: static;
|
||||||
|
@ -161,7 +161,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-row-center" style:-webkit-app-region={'no-drag'}>
|
<div class="flex-row-center left-items" style:-webkit-app-region={'no-drag'}>
|
||||||
<RootBarExtension position="left" />
|
<RootBarExtension position="left" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -267,4 +267,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-items {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -11,9 +11,11 @@
|
|||||||
}
|
}
|
||||||
oldLoc = newLocation.path[0]
|
oldLoc = newLocation.path[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: sorted = $rootBarExtensions.sort((a, b) => a[1].order - b[1].order)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each $rootBarExtensions as ext (ext[1].id)}
|
{#each sorted as ext (ext[1].id)}
|
||||||
{#if ext[0] === position}
|
{#if ext[0] === position}
|
||||||
<div id={ext[1].id} style:margin-right={'1px'}>
|
<div id={ext[1].id} style:margin-right={'1px'}>
|
||||||
<Component
|
<Component
|
||||||
|
@ -306,6 +306,7 @@ Array<
|
|||||||
id: string
|
id: string
|
||||||
component: AnyComponent | AnySvelteComponent
|
component: AnyComponent | AnySvelteComponent
|
||||||
props?: Record<string, any>
|
props?: Record<string, any>
|
||||||
|
order: number
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
>
|
>
|
||||||
@ -331,14 +332,15 @@ export async function formatDuration (duration: number, language: string): Promi
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pushRootBarComponent (pos: 'left' | 'right', component: AnyComponent): void {
|
export function pushRootBarComponent (pos: 'left' | 'right', component: AnyComponent, order?: number): void {
|
||||||
rootBarExtensions.update((cur) => {
|
rootBarExtensions.update((cur) => {
|
||||||
if (cur.find((p) => p[1].component === component) === undefined) {
|
if (cur.find((p) => p[1].component === component) === undefined) {
|
||||||
cur.push([
|
cur.push([
|
||||||
pos,
|
pos,
|
||||||
{
|
{
|
||||||
id: component,
|
id: component,
|
||||||
component
|
component,
|
||||||
|
order: order ?? 1000
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@ -369,6 +371,7 @@ export function pushRootBarProgressComponent (
|
|||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
component: RootStatusComponent,
|
component: RootStatusComponent,
|
||||||
|
order: 10,
|
||||||
props: {
|
props: {
|
||||||
label,
|
label,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { WorkbenchTab } from '@hcengineering/workbench'
|
||||||
|
import {
|
||||||
|
getNotificationsCount,
|
||||||
|
InboxNotificationsClientImpl,
|
||||||
|
isActivityNotification,
|
||||||
|
isMentionNotification,
|
||||||
|
NotifyMarker
|
||||||
|
} from '@hcengineering/notification-resources'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { InboxNotification } from '@hcengineering/notification'
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
|
import { concatLink, Doc, Ref } from '@hcengineering/core'
|
||||||
|
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||||
|
import { chunterId } from '@hcengineering/chunter'
|
||||||
|
import { parseLinkId } from '@hcengineering/view-resources'
|
||||||
|
import { parseLocation } from '@hcengineering/ui'
|
||||||
|
|
||||||
|
import chunter from '../plugin'
|
||||||
|
|
||||||
|
export let tab: WorkbenchTab
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||||
|
const contextByDocStore = notificationClient.contextByDoc
|
||||||
|
|
||||||
|
let objectId: Ref<Doc> | undefined = undefined
|
||||||
|
let notifications: InboxNotification[] = []
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
$: context = objectId !== undefined ? $contextByDocStore.get(objectId) : undefined
|
||||||
|
|
||||||
|
$: void updateObjectId(tab)
|
||||||
|
|
||||||
|
async function updateObjectId (tab: WorkbenchTab): Promise<void> {
|
||||||
|
const base = `${window.location.protocol}//${window.location.host}`
|
||||||
|
const url = new URL(concatLink(base, tab.location))
|
||||||
|
const loc = parseLocation(url)
|
||||||
|
|
||||||
|
if (loc.path[2] !== chunterId) {
|
||||||
|
objectId = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||||
|
const [id, _class] = decodeObjectURI(loc.path[3])
|
||||||
|
objectId = await parseLinkId(providers, id, _class)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if count > 0}
|
||||||
|
<NotifyMarker kind="simple" size="xx-small" />
|
||||||
|
{/if}
|
@ -52,6 +52,7 @@ import ThreadView from './components/threads/ThreadView.svelte'
|
|||||||
import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte'
|
import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte'
|
||||||
import ChatWidget from './components/ChatWidget.svelte'
|
import ChatWidget from './components/ChatWidget.svelte'
|
||||||
import ChatWidgetTab from './components/ChatWidgetTab.svelte'
|
import ChatWidgetTab from './components/ChatWidgetTab.svelte'
|
||||||
|
import WorkbenchTabExtension from './components/WorkbenchTabExtension.svelte'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chunterSpaceLinkFragmentProvider,
|
chunterSpaceLinkFragmentProvider,
|
||||||
@ -59,6 +60,7 @@ import {
|
|||||||
getMessageLink,
|
getMessageLink,
|
||||||
getMessageLocation,
|
getMessageLocation,
|
||||||
getThreadLink,
|
getThreadLink,
|
||||||
|
locationDataResolver,
|
||||||
openChannelInSidebar,
|
openChannelInSidebar,
|
||||||
openChannelInSidebarAction,
|
openChannelInSidebarAction,
|
||||||
openThreadInSidebar,
|
openThreadInSidebar,
|
||||||
@ -178,7 +180,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
ChatMessagePreview,
|
ChatMessagePreview,
|
||||||
JoinChannelNotificationPresenter,
|
JoinChannelNotificationPresenter,
|
||||||
ChatWidget,
|
ChatWidget,
|
||||||
ChatWidgetTab
|
ChatWidgetTab,
|
||||||
|
WorkbenchTabExtension
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
ChannelCreatedMessage,
|
ChannelCreatedMessage,
|
||||||
@ -203,7 +206,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
CloseChatWidgetTab: closeChatWidgetTab,
|
CloseChatWidgetTab: closeChatWidgetTab,
|
||||||
OpenChannelInSidebar: openChannelInSidebar,
|
OpenChannelInSidebar: openChannelInSidebar,
|
||||||
CanTranslateMessage: canTranslateMessage,
|
CanTranslateMessage: canTranslateMessage,
|
||||||
OpenThreadInSidebar: openThreadInSidebar
|
OpenThreadInSidebar: openThreadInSidebar,
|
||||||
|
LocationDataResolver: locationDataResolver
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
ArchiveChannel,
|
ArchiveChannel,
|
||||||
|
@ -4,7 +4,8 @@ import {
|
|||||||
getCurrentResolvedLocation,
|
getCurrentResolvedLocation,
|
||||||
getLocation,
|
getLocation,
|
||||||
type Location,
|
type Location,
|
||||||
navigate
|
navigate,
|
||||||
|
languageStore
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
|
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
|
||||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||||
@ -16,12 +17,12 @@ import {
|
|||||||
type ThreadMessage
|
type ThreadMessage
|
||||||
} from '@hcengineering/chunter'
|
} from '@hcengineering/chunter'
|
||||||
import { type DocNotifyContext, notificationId } from '@hcengineering/notification'
|
import { type DocNotifyContext, notificationId } from '@hcengineering/notification'
|
||||||
import workbench, { type Widget, workbenchId } from '@hcengineering/workbench'
|
import workbench, { type Widget, workbenchId, type LocationData } from '@hcengineering/workbench'
|
||||||
import { classIcon, getObjectLinkId } from '@hcengineering/view-resources'
|
import { classIcon, getObjectLinkId, parseLinkId } from '@hcengineering/view-resources'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import view, { encodeObjectURI, decodeObjectURI } from '@hcengineering/view'
|
import view, { encodeObjectURI, decodeObjectURI } from '@hcengineering/view'
|
||||||
import { createWidgetTab, isElementFromSidebar, sidebarStore } from '@hcengineering/workbench-resources'
|
import { createWidgetTab, isElementFromSidebar, sidebarStore } from '@hcengineering/workbench-resources'
|
||||||
import { type Asset, translate } from '@hcengineering/platform'
|
import { type Asset, type IntlString, translate } from '@hcengineering/platform'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
@ -385,3 +386,73 @@ export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
|||||||
removeThreadFromLoc(tab.data.thread)
|
removeThreadFromLoc(tab.data.thread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function locationDataResolver (loc: Location): Promise<LocationData> {
|
||||||
|
const point = loc.path[3]
|
||||||
|
|
||||||
|
if (point == null || point === '') {
|
||||||
|
return { name: await translate(chunter.string.Chat, {}, get(languageStore)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialsData: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
label: IntlString
|
||||||
|
icon: Asset
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
threads: {
|
||||||
|
label: chunter.string.Threads,
|
||||||
|
icon: chunter.icon.Chunter
|
||||||
|
// icon: chunter.icon.Thread
|
||||||
|
},
|
||||||
|
saved: {
|
||||||
|
label: chunter.string.Saved,
|
||||||
|
icon: chunter.icon.Chunter
|
||||||
|
// icon: chunter.icon.Bookmarks
|
||||||
|
},
|
||||||
|
chunterBrowser: {
|
||||||
|
label: chunter.string.ChunterBrowser,
|
||||||
|
icon: chunter.icon.Chunter
|
||||||
|
// icon: chunter.icon.ChunterBrowser
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
label: chunter.string.Channels,
|
||||||
|
icon: chunter.icon.Chunter
|
||||||
|
// icon: chunter.icon.Hashtag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialData = specialsData[point]
|
||||||
|
|
||||||
|
if (specialData !== undefined) {
|
||||||
|
return { name: await translate(specialData.label, {}, get(languageStore)), icon: specialData.icon }
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
const [id, _class] = decodeObjectURI(loc.path[3])
|
||||||
|
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||||
|
const _id: Ref<Doc> | undefined = await parseLinkId(linkProviders, id, _class)
|
||||||
|
|
||||||
|
const object = await client.findOne(_class, { _id })
|
||||||
|
if (object === undefined) return { name: await translate(chunter.string.Chat, {}, get(languageStore)) }
|
||||||
|
|
||||||
|
const titleIntl = client.getHierarchy().getClass(object._class).label
|
||||||
|
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
||||||
|
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
|
||||||
|
const isChunterSpace = hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
||||||
|
const name = (await getChannelName(_id, _class, object)) ?? (await translate(titleIntl, {}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
icon: chunter.icon.Chunter,
|
||||||
|
iconComponent: isChunterSpace ? iconMixin?.component : undefined,
|
||||||
|
iconProps: {
|
||||||
|
_id: object._id,
|
||||||
|
value: object,
|
||||||
|
size: isDirect ? 'tiny' : 'x-small'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"@hcengineering/ui": "^0.6.15",
|
"@hcengineering/ui": "^0.6.15",
|
||||||
"@hcengineering/view": "^0.6.13",
|
"@hcengineering/view": "^0.6.13",
|
||||||
"@hcengineering/view-resources": "^0.6.0",
|
"@hcengineering/view-resources": "^0.6.0",
|
||||||
|
"@hcengineering/workbench": "^0.6.16",
|
||||||
"svelte": "^4.2.12"
|
"svelte": "^4.2.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,8 @@ import {
|
|||||||
getCurrentEmployeePosition,
|
getCurrentEmployeePosition,
|
||||||
getPersonTooltip,
|
getPersonTooltip,
|
||||||
grouppingPersonManager,
|
grouppingPersonManager,
|
||||||
resolveLocation
|
resolveLocation,
|
||||||
|
resolveLocationData
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
@ -444,7 +445,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
PersonFilterFunction: filterPerson
|
PersonFilterFunction: filterPerson
|
||||||
},
|
},
|
||||||
resolver: {
|
resolver: {
|
||||||
Location: resolveLocation
|
Location: resolveLocation,
|
||||||
|
LocationData: resolveLocationData
|
||||||
},
|
},
|
||||||
aggregation: {
|
aggregation: {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
@ -50,7 +50,7 @@ import core, {
|
|||||||
type WithLookup
|
type WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||||
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
|
import { type IntlString, getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { type TemplateDataProvider } from '@hcengineering/templates'
|
import { type TemplateDataProvider } from '@hcengineering/templates'
|
||||||
import {
|
import {
|
||||||
@ -64,6 +64,7 @@ import {
|
|||||||
import view, { type Filter, type GrouppingManager } from '@hcengineering/view'
|
import view, { type Filter, type GrouppingManager } from '@hcengineering/view'
|
||||||
import { accessDeniedStore, FilterQuery } from '@hcengineering/view-resources'
|
import { accessDeniedStore, FilterQuery } from '@hcengineering/view-resources'
|
||||||
import { derived, get, writable } from 'svelte/store'
|
import { derived, get, writable } from 'svelte/store'
|
||||||
|
import { type LocationData } from '@hcengineering/workbench'
|
||||||
|
|
||||||
import contact from './plugin'
|
import contact from './plugin'
|
||||||
import { personStore } from '.'
|
import { personStore } from '.'
|
||||||
@ -536,3 +537,30 @@ export function groupPersonAccountValuesWithEmpty (
|
|||||||
}
|
}
|
||||||
return personAccountList.map((it) => it._id)
|
return personAccountList.map((it) => it._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveLocationData (loc: Location): Promise<LocationData> {
|
||||||
|
const special = loc.path[3]
|
||||||
|
const specialsData: Record<string, IntlString> = {
|
||||||
|
companies: contact.string.Organizations,
|
||||||
|
employees: contact.string.Employees,
|
||||||
|
persons: contact.string.Persons
|
||||||
|
}
|
||||||
|
|
||||||
|
if (special == null) {
|
||||||
|
return { nameIntl: contact.string.Contacts }
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialLabel = specialsData[special]
|
||||||
|
if (specialLabel !== undefined) {
|
||||||
|
return { nameIntl: specialLabel }
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const object = await client.findOne(contact.class.Contact, { _id: special as Ref<Contact> })
|
||||||
|
|
||||||
|
if (object === undefined) {
|
||||||
|
return { nameIntl: specialLabel }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: getName(client.getHierarchy(), object) }
|
||||||
|
}
|
||||||
|
@ -299,7 +299,9 @@ export const contactPlugin = plugin(contactId, {
|
|||||||
SelectUsers: '' as IntlString,
|
SelectUsers: '' as IntlString,
|
||||||
AddGuest: '' as IntlString,
|
AddGuest: '' as IntlString,
|
||||||
Members: '' as IntlString,
|
Members: '' as IntlString,
|
||||||
Contacts: '' as IntlString
|
Contacts: '' as IntlString,
|
||||||
|
Employees: '' as IntlString,
|
||||||
|
Persons: '' as IntlString
|
||||||
},
|
},
|
||||||
viewlet: {
|
viewlet: {
|
||||||
TableMember: '' as Ref<Viewlet>,
|
TableMember: '' as Ref<Viewlet>,
|
||||||
|
@ -175,7 +175,10 @@ export function getDocumentLinkId (doc: Document): string {
|
|||||||
return `${slug}-${doc._id}`
|
return `${slug}-${doc._id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseDocumentId (shortLink: string): Ref<Document> | undefined {
|
export function parseDocumentId (shortLink?: string): Ref<Document> | undefined {
|
||||||
|
if (shortLink === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const parts = shortLink.split('-')
|
const parts = shortLink.split('-')
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
return parts[parts.length - 1] as Ref<Document>
|
return parts[parts.length - 1] as Ref<Document>
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
pushRootBarComponent('left', love.component.ControlExt)
|
pushRootBarComponent('left', love.component.ControlExt, 20)
|
||||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||||
})
|
})
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "Widget preferences",
|
"WidgetPreferences": "Widget preferences",
|
||||||
"OpenInSidebar": "Open in sidebar",
|
"OpenInSidebar": "Open in sidebar",
|
||||||
"OpenInSidebarNewTab": "Open in sidebar new tab",
|
"OpenInSidebarNewTab": "Open in sidebar new tab",
|
||||||
"ConfigureWidgets": "Configure widgets"
|
"ConfigureWidgets": "Configure widgets",
|
||||||
|
"Tab": "Tab"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "Preferencias del widget",
|
"WidgetPreferences": "Preferencias del widget",
|
||||||
"OpenInSidebar": "Abrir en la barra lateral",
|
"OpenInSidebar": "Abrir en la barra lateral",
|
||||||
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
|
"OpenInSidebarNewTab": "Abrir en una nueva pestaña de la barra lateral",
|
||||||
"ConfigureWidgets": "Configurar widgets"
|
"ConfigureWidgets": "Configurar widgets",
|
||||||
|
"Tab": "Pestaña"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "Préférences du widget",
|
"WidgetPreferences": "Préférences du widget",
|
||||||
"OpenInSidebar": "Ouvrir dans la barre latérale",
|
"OpenInSidebar": "Ouvrir dans la barre latérale",
|
||||||
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
|
"OpenInSidebarNewTab": "Ouvrir dans un nouvel onglet de la barre latérale",
|
||||||
"ConfigureWidgets": "Configurer les widgets"
|
"ConfigureWidgets": "Configurer les widgets",
|
||||||
|
"Tab": "Onglet"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "Preferências do widget",
|
"WidgetPreferences": "Preferências do widget",
|
||||||
"OpenInSidebar": "Abrir na barra lateral",
|
"OpenInSidebar": "Abrir na barra lateral",
|
||||||
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
|
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
|
||||||
"ConfigureWidgets": "Configurar widgets"
|
"ConfigureWidgets": "Configurar widgets",
|
||||||
|
"Tab": "Aba"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "Настройки виджета",
|
"WidgetPreferences": "Настройки виджета",
|
||||||
"OpenInSidebar": "Открыть в боковой панели",
|
"OpenInSidebar": "Открыть в боковой панели",
|
||||||
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
|
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
|
||||||
"ConfigureWidgets": "Настроить виджеты"
|
"ConfigureWidgets": "Настроить виджеты",
|
||||||
|
"Tab": "Вкладка"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"WidgetPreferences": "小部件首选项",
|
"WidgetPreferences": "小部件首选项",
|
||||||
"OpenInSidebar": "在侧边栏中打开",
|
"OpenInSidebar": "在侧边栏中打开",
|
||||||
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
|
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
|
||||||
"ConfigureWidgets": "配置小部件"
|
"ConfigureWidgets": "配置小部件",
|
||||||
|
"Tab": "选项卡"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,20 @@
|
|||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||||
import core, { AccountRole, Class, Doc, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
import core, {
|
||||||
|
AccountRole,
|
||||||
|
Class,
|
||||||
|
Doc,
|
||||||
|
getCurrentAccount,
|
||||||
|
hasAccountRole,
|
||||||
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
|
Space
|
||||||
|
} from '@hcengineering/core'
|
||||||
import login from '@hcengineering/login'
|
import login from '@hcengineering/login'
|
||||||
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||||
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||||
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
|
import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
ActionContext,
|
ActionContext,
|
||||||
ComponentExtensions,
|
ComponentExtensions,
|
||||||
@ -30,55 +39,62 @@
|
|||||||
reduceCalls
|
reduceCalls
|
||||||
} from '@hcengineering/presentation'
|
} from '@hcengineering/presentation'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import support, { SupportStatus, supportLink } from '@hcengineering/support'
|
import support, { supportLink, SupportStatus } from '@hcengineering/support'
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
|
areLocationsEqual,
|
||||||
Button,
|
Button,
|
||||||
|
closePanel,
|
||||||
|
closePopup,
|
||||||
|
closeTooltip,
|
||||||
CompAndProps,
|
CompAndProps,
|
||||||
Component,
|
Component,
|
||||||
|
defineSeparators,
|
||||||
|
deviceOptionsStore as deviceInfo,
|
||||||
Dock,
|
Dock,
|
||||||
|
getCurrentLocation,
|
||||||
|
getLocation,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
Label,
|
Label,
|
||||||
Location,
|
Location,
|
||||||
|
location,
|
||||||
|
locationStorageKeyId,
|
||||||
|
locationToUrl,
|
||||||
|
mainSeparators,
|
||||||
|
navigate,
|
||||||
|
openPanel,
|
||||||
PanelInstance,
|
PanelInstance,
|
||||||
Popup,
|
Popup,
|
||||||
PopupAlignment,
|
PopupAlignment,
|
||||||
PopupPosAlignment,
|
PopupPosAlignment,
|
||||||
PopupResult,
|
PopupResult,
|
||||||
ResolvedLocation,
|
|
||||||
Separator,
|
|
||||||
TooltipInstance,
|
|
||||||
areLocationsEqual,
|
|
||||||
closePanel,
|
|
||||||
closePopup,
|
|
||||||
closeTooltip,
|
|
||||||
defineSeparators,
|
|
||||||
deviceOptionsStore as deviceInfo,
|
|
||||||
getCurrentLocation,
|
|
||||||
getLocation,
|
|
||||||
location,
|
|
||||||
locationStorageKeyId,
|
|
||||||
navigate,
|
|
||||||
openPanel,
|
|
||||||
popupstore,
|
popupstore,
|
||||||
pushRootBarComponent,
|
pushRootBarComponent,
|
||||||
|
ResolvedLocation,
|
||||||
resolvedLocationStore,
|
resolvedLocationStore,
|
||||||
|
Separator,
|
||||||
setResolvedLocation,
|
setResolvedLocation,
|
||||||
showPopup,
|
showPopup,
|
||||||
workbenchSeparators,
|
TooltipInstance,
|
||||||
mainSeparators
|
workbenchSeparators
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
import {
|
import {
|
||||||
|
accessDeniedStore,
|
||||||
ActionHandler,
|
ActionHandler,
|
||||||
ListSelectionProvider,
|
ListSelectionProvider,
|
||||||
NavLink,
|
|
||||||
accessDeniedStore,
|
|
||||||
migrateViewOpttions,
|
migrateViewOpttions,
|
||||||
|
NavLink,
|
||||||
parseLinkId,
|
parseLinkId,
|
||||||
updateFocus
|
updateFocus
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
|
import type {
|
||||||
|
Application,
|
||||||
|
NavigatorModel,
|
||||||
|
SpecialNavModel,
|
||||||
|
ViewConfiguration,
|
||||||
|
WorkbenchTab
|
||||||
|
} from '@hcengineering/workbench'
|
||||||
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
||||||
import { subscribeMobile } from '../mobile'
|
import { subscribeMobile } from '../mobile'
|
||||||
import workbench from '../plugin'
|
import workbench from '../plugin'
|
||||||
@ -96,6 +112,7 @@
|
|||||||
import TopMenu from './icons/TopMenu.svelte'
|
import TopMenu from './icons/TopMenu.svelte'
|
||||||
import WidgetsBar from './sidebar/Sidebar.svelte'
|
import WidgetsBar from './sidebar/Sidebar.svelte'
|
||||||
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
|
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
|
||||||
|
import { getTabLocation, selectTab, syncWorkbenchTab, tabIdStore, tabsStore } from '../workbench'
|
||||||
|
|
||||||
let contentPanel: HTMLElement
|
let contentPanel: HTMLElement
|
||||||
|
|
||||||
@ -142,13 +159,74 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let tabs: WorkbenchTab[] = []
|
||||||
|
let areTabsLoaded = false
|
||||||
|
let prevTab: Ref<WorkbenchTab> | undefined
|
||||||
|
|
||||||
|
const query = createQuery()
|
||||||
|
$: query.query(
|
||||||
|
workbench.class.WorkbenchTab,
|
||||||
|
{},
|
||||||
|
(res) => {
|
||||||
|
tabs = res
|
||||||
|
tabsStore.set(tabs)
|
||||||
|
if (!areTabsLoaded) {
|
||||||
|
void initCurrentTab(tabs)
|
||||||
|
areTabsLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sort: {
|
||||||
|
isPinned: SortingOrder.Descending,
|
||||||
|
createdOn: SortingOrder.Ascending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async function initCurrentTab (tabs: WorkbenchTab[]): Promise<void> {
|
||||||
|
if (tabs.length === 0) {
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
|
||||||
|
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||||
|
attachedTo: account._id,
|
||||||
|
location: locationToUrl(loc),
|
||||||
|
isPinned: false
|
||||||
|
})
|
||||||
|
prevTab = _id
|
||||||
|
selectTab(_id)
|
||||||
|
} else {
|
||||||
|
const tab = tabs.find((t) => t._id === $tabIdStore)
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
const tabLoc = tab ? getTabLocation(tab) : undefined
|
||||||
|
const isLocEqual = tabLoc ? areLocationsEqual(loc, tabLoc) : false
|
||||||
|
if (!isLocEqual) {
|
||||||
|
const url = locationToUrl(loc)
|
||||||
|
const tabByUrl = tabs.find((t) => t.location === url)
|
||||||
|
if (tabByUrl !== undefined) {
|
||||||
|
prevTab = tabByUrl._id
|
||||||
|
selectTab(tabByUrl._id)
|
||||||
|
} else {
|
||||||
|
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||||
|
attachedTo: account._id,
|
||||||
|
location: url,
|
||||||
|
isPinned: false
|
||||||
|
})
|
||||||
|
prevTab = _id
|
||||||
|
selectTab(_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
pushRootBarComponent('right', view.component.SearchSelector)
|
pushRootBarComponent('right', view.component.SearchSelector)
|
||||||
|
pushRootBarComponent('left', workbench.component.WorkbenchTabs, 30)
|
||||||
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||||
$workspacesStore = await getWorkspaceFn()
|
$workspacesStore = await getWorkspaceFn()
|
||||||
await updateWindowTitle(getLocation())
|
await updateWindowTitle(getLocation())
|
||||||
})
|
})
|
||||||
syncSidebarState()
|
syncSidebarState()
|
||||||
|
syncWorkbenchTab()
|
||||||
})
|
})
|
||||||
|
|
||||||
const account = getCurrentAccount() as PersonAccount
|
const account = getCurrentAccount() as PersonAccount
|
||||||
@ -296,7 +374,13 @@
|
|||||||
async function syncLoc (loc: Location): Promise<void> {
|
async function syncLoc (loc: Location): Promise<void> {
|
||||||
accessDeniedStore.set(false)
|
accessDeniedStore.set(false)
|
||||||
const originalLoc = JSON.stringify(loc)
|
const originalLoc = JSON.stringify(loc)
|
||||||
|
if ($tabIdStore !== prevTab) {
|
||||||
|
if (prevTab) {
|
||||||
|
clear(1)
|
||||||
|
clear(2)
|
||||||
|
}
|
||||||
|
prevTab = $tabIdStore
|
||||||
|
}
|
||||||
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
|
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
|
||||||
// resolve short links
|
// resolve short links
|
||||||
const resolvedLoc = await resolveShortLink(loc)
|
const resolvedLoc = await resolveShortLink(loc)
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { AnySvelteComponent, closePanel, getCurrentLocation, Location, ModernTab, navigate } from '@hcengineering/ui'
|
||||||
|
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
|
||||||
|
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||||
|
import { type Application, WorkbenchTab } from '@hcengineering/workbench'
|
||||||
|
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
import { parseLinkId, showMenu } from '@hcengineering/view-resources'
|
||||||
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
|
|
||||||
|
import { closeTab, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
|
||||||
|
import workbench from '../plugin'
|
||||||
|
|
||||||
|
export let tab: WorkbenchTab
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||||
|
|
||||||
|
let name: string | undefined = undefined
|
||||||
|
let label: IntlString | undefined = undefined
|
||||||
|
let icon: Asset | AnySvelteComponent | undefined
|
||||||
|
let iconProps: Record<string, any> | undefined
|
||||||
|
|
||||||
|
async function getResolvedLocation (loc: Location, app?: Application): Promise<Location> {
|
||||||
|
if (app === undefined) return loc
|
||||||
|
if (app.locationResolver) {
|
||||||
|
const resolver = await getResource(app.locationResolver)
|
||||||
|
return (await resolver(loc))?.loc ?? loc
|
||||||
|
}
|
||||||
|
|
||||||
|
return loc
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTabName (loc: Location): Promise<string | undefined> {
|
||||||
|
if (loc.fragment == null) return
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
|
||||||
|
if (_class == null) return
|
||||||
|
|
||||||
|
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
|
||||||
|
if (mixin === undefined) return
|
||||||
|
const titleProvider = await getResource(mixin.titleProvider)
|
||||||
|
try {
|
||||||
|
const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
|
||||||
|
return await titleProvider(client, _id)
|
||||||
|
} catch (err: any) {
|
||||||
|
Analytics.handleError(err)
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTabData (tab: WorkbenchTab): Promise<void> {
|
||||||
|
const tabLoc = getTabLocation(tab)
|
||||||
|
const alias = tabLoc.path[2]
|
||||||
|
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias })[0]
|
||||||
|
|
||||||
|
if (application?.locationDataResolver) {
|
||||||
|
const resolver = await getResource(application.locationDataResolver)
|
||||||
|
const data = await resolver(tabLoc)
|
||||||
|
name = data.name
|
||||||
|
label = data.nameIntl ?? application.label ?? workbench.string.Tab
|
||||||
|
|
||||||
|
if (data.iconComponent) {
|
||||||
|
icon = await getResource(data.iconComponent)
|
||||||
|
} else {
|
||||||
|
icon = data.icon ?? application?.icon
|
||||||
|
}
|
||||||
|
iconProps = data.iconProps
|
||||||
|
} else {
|
||||||
|
const special = tabLoc.path[3]
|
||||||
|
const specialLabel = application?.navigatorModel?.specials?.find((s) => s.id === special)?.label
|
||||||
|
const resolvedLoc = await getResolvedLocation(tabLoc, application)
|
||||||
|
name = await getTabName(resolvedLoc)
|
||||||
|
label = specialLabel ?? application?.label ?? workbench.string.Tab
|
||||||
|
icon = application?.icon
|
||||||
|
iconProps = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: void updateTabData(tab)
|
||||||
|
|
||||||
|
function handleClickTab (): void {
|
||||||
|
selectTab(tab._id)
|
||||||
|
const tabLoc = getTabLocation(tab)
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
const currentApp = loc.path[2]
|
||||||
|
const newApp = tabLoc.path[2]
|
||||||
|
if (newApp && newApp !== currentApp) {
|
||||||
|
closePanel(false)
|
||||||
|
}
|
||||||
|
navigate(tabLoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloseTab (): void {
|
||||||
|
void closeTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMenu (event: MouseEvent): void {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
showMenu(event, { object: tab, baseMenuClass: workbench.class.WorkbenchTab })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModernTab
|
||||||
|
labelIntl={label}
|
||||||
|
label={name}
|
||||||
|
{icon}
|
||||||
|
{iconProps}
|
||||||
|
maxSize="12rem"
|
||||||
|
highlighted={$tabIdStore === tab._id}
|
||||||
|
canClose={$tabsStore.length > 1 && !tab.isPinned}
|
||||||
|
on:click={handleClickTab}
|
||||||
|
on:close={handleCloseTab}
|
||||||
|
on:contextmenu={handleMenu}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="prefix">
|
||||||
|
<ComponentExtensions extension={workbench.extensions.WorkbenchTabExtensions} props={{ tab }} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ModernTab>
|
@ -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 { createTab, tabsStore } from '../workbench'
|
||||||
|
import WorkbenchTabPresenter from './WorkbenchTabPresenter.svelte'
|
||||||
|
import { IconAdd, ButtonIcon } from '@hcengineering/ui'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root flex-gap-1">
|
||||||
|
{#each $tabsStore as tab}
|
||||||
|
<WorkbenchTabPresenter {tab} />
|
||||||
|
{/each}
|
||||||
|
<div class="ml-1-5 plus-button mr-1">
|
||||||
|
<ButtonIcon icon={IconAdd} size="min" kind="tertiary" on:click={createTab} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.plus-button {
|
||||||
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -23,7 +23,9 @@ import WorkbenchApp from './components/WorkbenchApp.svelte'
|
|||||||
import { doNavigate } from './utils'
|
import { doNavigate } from './utils'
|
||||||
import Workbench from './components/Workbench.svelte'
|
import Workbench from './components/Workbench.svelte'
|
||||||
import ServerManager from './components/ServerManager.svelte'
|
import ServerManager from './components/ServerManager.svelte'
|
||||||
|
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
|
||||||
import { isAdminUser } from '@hcengineering/presentation'
|
import { isAdminUser } from '@hcengineering/presentation'
|
||||||
|
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
|
||||||
|
|
||||||
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
|
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
|
||||||
return spaces.find((sp) => sp.archived) !== undefined
|
return spaces.find((sp) => sp.archived) !== undefined
|
||||||
@ -46,13 +48,18 @@ export default async (): Promise<Resources> => ({
|
|||||||
SpacePanel,
|
SpacePanel,
|
||||||
SpecialView,
|
SpecialView,
|
||||||
Workbench,
|
Workbench,
|
||||||
ServerManager
|
ServerManager,
|
||||||
|
WorkbenchTabs
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
HasArchiveSpaces: hasArchiveSpaces,
|
HasArchiveSpaces: hasArchiveSpaces,
|
||||||
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser()
|
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
|
||||||
|
CanCloseTab: canCloseTab
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Navigate: doNavigate
|
Navigate: doNavigate,
|
||||||
|
PinTab: pinTab,
|
||||||
|
UnpinTab: unpinTab,
|
||||||
|
CloseTab: closeTab
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -47,13 +47,15 @@ export default mergeIds(workbenchId, workbench, {
|
|||||||
WorkspaceCreating: '' as IntlString,
|
WorkspaceCreating: '' as IntlString,
|
||||||
AccessDenied: '' as IntlString,
|
AccessDenied: '' as IntlString,
|
||||||
Widget: '' as IntlString,
|
Widget: '' as IntlString,
|
||||||
WidgetPreference: '' as IntlString
|
WidgetPreference: '' as IntlString,
|
||||||
|
Tab: '' as IntlString
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
MobileAllowed: '' as Metadata<boolean>
|
MobileAllowed: '' as Metadata<boolean>
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
SpacePanel: '' as AnyComponent,
|
SpacePanel: '' as AnyComponent,
|
||||||
Workbench: '' as AnyComponent
|
Workbench: '' as AnyComponent,
|
||||||
|
WorkbenchTabs: '' as AnyComponent
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
138
plugins/workbench-resources/src/workbench.ts
Normal file
138
plugins/workbench-resources/src/workbench.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
//
|
||||||
|
// 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 { derived, get, writable } from 'svelte/store'
|
||||||
|
import core, { concatLink, getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||||
|
import workbench, { type WorkbenchTab } from '@hcengineering/workbench'
|
||||||
|
import {
|
||||||
|
location as locationStore,
|
||||||
|
locationToUrl,
|
||||||
|
parseLocation,
|
||||||
|
type Location,
|
||||||
|
navigate,
|
||||||
|
getCurrentLocation
|
||||||
|
} from '@hcengineering/ui'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
|
||||||
|
import { workspaceStore } from './utils'
|
||||||
|
|
||||||
|
export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>()
|
||||||
|
export const tabsStore = writable<WorkbenchTab[]>([])
|
||||||
|
export const currentTabStore = derived([tabIdStore, tabsStore], ([tabId, tabs]) => {
|
||||||
|
return tabs.find((tab) => tab._id === tabId)
|
||||||
|
})
|
||||||
|
|
||||||
|
workspaceStore.subscribe((workspace) => {
|
||||||
|
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
locationStore.subscribe((loc) => {
|
||||||
|
const workspace = get(workspaceStore)
|
||||||
|
if (workspace == null || workspace === '') return
|
||||||
|
const tab = get(currentTabStore)
|
||||||
|
if (tab == null) return
|
||||||
|
const tabId = tab._id
|
||||||
|
if (tabId == null || tab._id !== tabId) return
|
||||||
|
|
||||||
|
void getClient().update(tab, { location: locationToUrl(loc) })
|
||||||
|
})
|
||||||
|
|
||||||
|
tabIdStore.subscribe(saveTabToLocalStorage)
|
||||||
|
|
||||||
|
export function syncWorkbenchTab (): void {
|
||||||
|
const workspace = get(workspaceStore)
|
||||||
|
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabIdLocalStorageKey (workspace: string): string | undefined {
|
||||||
|
const me = getCurrentAccount()
|
||||||
|
if (me == null || workspace === '') return undefined
|
||||||
|
return `workbench.${workspace}.${me.person}.tab`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabFromLocalStorage (workspace: string): Ref<WorkbenchTab> | undefined {
|
||||||
|
const localStorageKey = getTabIdLocalStorageKey(workspace)
|
||||||
|
if (localStorageKey === undefined) return undefined
|
||||||
|
|
||||||
|
const tab = window.localStorage.getItem(localStorageKey)
|
||||||
|
if (tab == null || tab === '') return undefined
|
||||||
|
|
||||||
|
return tab as Ref<WorkbenchTab>
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTabToLocalStorage (_id: Ref<WorkbenchTab> | undefined): void {
|
||||||
|
const workspace = get(workspaceStore)
|
||||||
|
if (workspace == null || workspace === '') return
|
||||||
|
|
||||||
|
const localStorageKey = getTabIdLocalStorageKey(workspace)
|
||||||
|
if (localStorageKey === undefined) return
|
||||||
|
window.localStorage.setItem(localStorageKey, _id ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectTab (_id: Ref<WorkbenchTab>): void {
|
||||||
|
tabIdStore.set(_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTabLocation (tab: WorkbenchTab): Location {
|
||||||
|
const base = `${window.location.protocol}//${window.location.host}`
|
||||||
|
const url = new URL(concatLink(base, tab.location))
|
||||||
|
|
||||||
|
return parseLocation(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeTab (tab: WorkbenchTab): Promise<void> {
|
||||||
|
const tabs = get(tabsStore)
|
||||||
|
const index = tabs.findIndex((t) => t._id === tab._id)
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
tabsStore.update((tabs) => tabs.filter((t) => t._id !== tab._id))
|
||||||
|
if (get(tabIdStore) === tab._id) {
|
||||||
|
const newTab = tabs[index - 1] ?? tabs[index + 1]
|
||||||
|
tabIdStore.set(newTab?._id)
|
||||||
|
if (newTab !== undefined) {
|
||||||
|
navigate(getTabLocation(newTab))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
await client.remove(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTab (): Promise<void> {
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
const client = getClient()
|
||||||
|
const me = getCurrentAccount()
|
||||||
|
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||||
|
attachedTo: me._id,
|
||||||
|
location: locationToUrl(loc),
|
||||||
|
isPinned: false
|
||||||
|
})
|
||||||
|
|
||||||
|
selectTab(tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCloseTab (tab: WorkbenchTab): boolean {
|
||||||
|
const tabs = get(tabsStore)
|
||||||
|
return tabs.length > 1 && tabs.some((t) => t._id === tab._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pinTab (tab: WorkbenchTab): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
await client.update(tab, { isPinned: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unpinTab (tab: WorkbenchTab): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
await client.update(tab, { isPinned: false })
|
||||||
|
}
|
@ -30,6 +30,15 @@ import { ViewAction } from '@hcengineering/view'
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export interface LocationData {
|
||||||
|
name?: string
|
||||||
|
nameIntl?: IntlString
|
||||||
|
icon?: Asset
|
||||||
|
iconComponent?: AnyComponent
|
||||||
|
iconProps?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
export interface Application extends Doc {
|
export interface Application extends Doc {
|
||||||
label: IntlString
|
label: IntlString
|
||||||
alias: string
|
alias: string
|
||||||
@ -42,6 +51,7 @@ export interface Application extends Doc {
|
|||||||
aside?: AnyComponent
|
aside?: AnyComponent
|
||||||
|
|
||||||
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
|
||||||
|
locationDataResolver?: Resource<(loc: Location) => Promise<LocationData>>
|
||||||
|
|
||||||
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
|
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
|
||||||
component?: AnyComponent
|
component?: AnyComponent
|
||||||
@ -102,6 +112,11 @@ export interface TxSidebarEvent<T extends Record<string, any> = Record<string, a
|
|||||||
params: T
|
params: T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkbenchTab extends Preference {
|
||||||
|
location: string
|
||||||
|
isPinned: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -201,7 +216,8 @@ export default plugin(workbenchId, {
|
|||||||
HiddenApplication: '' as Ref<Class<HiddenApplication>>,
|
HiddenApplication: '' as Ref<Class<HiddenApplication>>,
|
||||||
Widget: '' as Ref<Class<Widget>>,
|
Widget: '' as Ref<Class<Widget>>,
|
||||||
WidgetPreference: '' as Ref<Class<WidgetPreference>>,
|
WidgetPreference: '' as Ref<Class<WidgetPreference>>,
|
||||||
TxSidebarEvent: '' as Ref<Class<TxSidebarEvent<Record<string, any>>>>
|
TxSidebarEvent: '' as Ref<Class<TxSidebarEvent<Record<string, any>>>>,
|
||||||
|
WorkbenchTab: '' as Ref<Class<WorkbenchTab>>
|
||||||
},
|
},
|
||||||
mixin: {
|
mixin: {
|
||||||
SpaceView: '' as Ref<Mixin<SpaceView>>
|
SpaceView: '' as Ref<Mixin<SpaceView>>
|
||||||
@ -238,7 +254,8 @@ export default plugin(workbenchId, {
|
|||||||
NavigationExpandedDefault: '' as Metadata<boolean>
|
NavigationExpandedDefault: '' as Metadata<boolean>
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
WorkbenchExtensions: '' as ComponentExtensionId
|
WorkbenchExtensions: '' as ComponentExtensionId,
|
||||||
|
WorkbenchTabExtensions: '' as ComponentExtensionId
|
||||||
},
|
},
|
||||||
actionImpl: {
|
actionImpl: {
|
||||||
Navigate: '' as ViewAction<{
|
Navigate: '' as ViewAction<{
|
||||||
|
Loading…
Reference in New Issue
Block a user