mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +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,
|
||||
{
|
||||
label: chunter.string.ApplicationLabelChunter,
|
||||
locationDataResolver: chunter.function.LocationDataResolver,
|
||||
icon: chunter.icon.Chunter,
|
||||
alias: chunterId,
|
||||
hidden: false,
|
||||
@ -87,6 +88,11 @@ export function createModel (builder: Builder): void {
|
||||
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]
|
||||
|
||||
spaceClasses.forEach((spaceClass) => {
|
||||
|
@ -22,7 +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'
|
||||
import { type WidgetTab, type LocationData } from '@hcengineering/workbench'
|
||||
|
||||
export default mergeIds(chunterId, chunter, {
|
||||
component: {
|
||||
@ -34,7 +34,8 @@ export default mergeIds(chunterId, chunter, {
|
||||
ChatWidgetTab: '' as AnyComponent,
|
||||
ChatMessageNotificationLabel: '' as AnyComponent,
|
||||
ThreadNotificationPresenter: '' as AnyComponent,
|
||||
JoinChannelNotificationPresenter: '' as AnyComponent
|
||||
JoinChannelNotificationPresenter: '' as AnyComponent,
|
||||
WorkbenchTabExtension: '' as AnyComponent
|
||||
},
|
||||
action: {
|
||||
MarkCommentUnread: '' as Ref<Action>,
|
||||
@ -108,7 +109,8 @@ export default mergeIds(chunterId, chunter, {
|
||||
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>>,
|
||||
CloseChatWidgetTab: '' as Resource<(tab: WidgetTab) => Promise<void>>
|
||||
CloseChatWidgetTab: '' as Resource<(tab: WidgetTab) => Promise<void>>,
|
||||
LocationDataResolver: '' as Resource<(loc: Location) => Promise<LocationData>>
|
||||
},
|
||||
filter: {
|
||||
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@hcengineering/model-attachment": "^0.6.0",
|
||||
"@hcengineering/model-chunter": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-guest": "^0.6.0",
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/model-presentation": "^0.6.0",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
@ -48,7 +49,7 @@
|
||||
"@hcengineering/templates": "^0.6.11",
|
||||
"@hcengineering/ui": "^0.6.15",
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
"@hcengineering/model-guest": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.16",
|
||||
"cross-fetch": "^3.1.5"
|
||||
}
|
||||
}
|
||||
|
@ -324,6 +324,7 @@ export function createModel (builder: Builder): void {
|
||||
hidden: false,
|
||||
// component: contact.component.ContactsTabs,
|
||||
locationResolver: contact.resolver.Location,
|
||||
locationDataResolver: contact.resolver.LocationData,
|
||||
navigatorModel: {
|
||||
spaces: [],
|
||||
specials: [
|
||||
|
@ -21,9 +21,10 @@ import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineer
|
||||
import { type NotificationGroup } from '@hcengineering/notification'
|
||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||
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 ChatMessageViewlet } from '@hcengineering/chunter'
|
||||
import { type LocationData } from '@hcengineering/workbench'
|
||||
|
||||
export default mergeIds(contactId, contact, {
|
||||
activity: {
|
||||
@ -63,7 +64,6 @@ export default mergeIds(contactId, contact, {
|
||||
ChannelIcon: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Persons: '' as IntlString,
|
||||
SearchEmployee: '' as IntlString,
|
||||
SearchPerson: '' as IntlString,
|
||||
SearchOrganization: '' as IntlString,
|
||||
@ -97,7 +97,6 @@ export default mergeIds(contactId, contact, {
|
||||
|
||||
ConfigLabel: '' as IntlString,
|
||||
ConfigDescription: '' as IntlString,
|
||||
Employees: '' as IntlString,
|
||||
People: '' as IntlString
|
||||
},
|
||||
completion: {
|
||||
@ -142,5 +141,8 @@ export default mergeIds(contactId, contact, {
|
||||
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
|
||||
SetPersonStore: '' as Resource<(manager: DocManager<any>) => void>,
|
||||
PersonFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean>
|
||||
},
|
||||
resolver: {
|
||||
LocationData: '' as Resource<(loc: Location) => Promise<LocationData>>
|
||||
}
|
||||
})
|
||||
|
@ -30,13 +30,14 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/model": "^0.6.11",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@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/view": "^0.6.13",
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/workbench-resources": "^0.6.1",
|
||||
"@hcengineering/model-preference": "^0.6.0"
|
||||
"@hcengineering/workbench": "^0.6.16",
|
||||
"@hcengineering/workbench-resources": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,12 @@ import type {
|
||||
WidgetPreference,
|
||||
WidgetTab,
|
||||
WidgetType,
|
||||
SidebarEvent
|
||||
SidebarEvent,
|
||||
WorkbenchTab
|
||||
} from '@hcengineering/workbench'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import core, { TClass, TDoc, TTx } from '@hcengineering/model-core'
|
||||
import presentation from '@hcengineering/model-presentation'
|
||||
|
||||
import workbench from './plugin'
|
||||
|
||||
@ -98,6 +100,13 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent {
|
||||
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 {
|
||||
builder.createModel(
|
||||
TApplication,
|
||||
@ -106,7 +115,8 @@ export function createModel (builder: Builder): void {
|
||||
TApplicationNavModel,
|
||||
TWidget,
|
||||
TWidgetPreference,
|
||||
TTxSidebarEvent
|
||||
TTxSidebarEvent,
|
||||
TWorkbenchTab
|
||||
)
|
||||
|
||||
builder.mixin(workbench.class.Application, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
@ -133,6 +143,52 @@ export function createModel (builder: Builder): void {
|
||||
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
|
||||
|
@ -13,11 +13,12 @@
|
||||
// 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 AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import workbench from '@hcengineering/workbench-resources/src/plugin'
|
||||
import type { ActionCategory, ViewActionAvailabilityFunction } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(workbenchId, workbench, {
|
||||
component: {
|
||||
@ -30,6 +31,15 @@ export default mergeIds(workbenchId, workbench, {
|
||||
},
|
||||
function: {
|
||||
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-height: 0;
|
||||
border-radius: var(--small-focus-BorderRadius);
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius:0 ;
|
||||
|
||||
&:not(.rowContent) { flex-direction: column; }
|
||||
.panel-instance & {
|
||||
|
@ -150,6 +150,7 @@
|
||||
.icon {
|
||||
writing-mode: initial;
|
||||
text-orientation: initial;
|
||||
margin: 0;
|
||||
&.vertical {
|
||||
transform: rotate(90deg);
|
||||
text-orientation: upright;
|
||||
@ -158,6 +159,10 @@
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
|
||||
&.horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
&.vertical {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
@ -208,6 +208,8 @@
|
||||
z-index: 401;
|
||||
position: fixed;
|
||||
background-color: transparent;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
@media print {
|
||||
position: static;
|
||||
|
@ -161,7 +161,7 @@
|
||||
</button>
|
||||
</div>
|
||||
{/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" />
|
||||
</div>
|
||||
<div
|
||||
@ -267,4 +267,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-items {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -11,9 +11,11 @@
|
||||
}
|
||||
oldLoc = newLocation.path[0]
|
||||
})
|
||||
|
||||
$: sorted = $rootBarExtensions.sort((a, b) => a[1].order - b[1].order)
|
||||
</script>
|
||||
|
||||
{#each $rootBarExtensions as ext (ext[1].id)}
|
||||
{#each sorted as ext (ext[1].id)}
|
||||
{#if ext[0] === position}
|
||||
<div id={ext[1].id} style:margin-right={'1px'}>
|
||||
<Component
|
||||
|
@ -306,6 +306,7 @@ Array<
|
||||
id: string
|
||||
component: AnyComponent | AnySvelteComponent
|
||||
props?: Record<string, any>
|
||||
order: number
|
||||
}
|
||||
]
|
||||
>
|
||||
@ -331,14 +332,15 @@ export async function formatDuration (duration: number, language: string): Promi
|
||||
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) => {
|
||||
if (cur.find((p) => p[1].component === component) === undefined) {
|
||||
cur.push([
|
||||
pos,
|
||||
{
|
||||
id: component,
|
||||
component
|
||||
component,
|
||||
order: order ?? 1000
|
||||
}
|
||||
])
|
||||
}
|
||||
@ -369,6 +371,7 @@ export function pushRootBarProgressComponent (
|
||||
{
|
||||
id,
|
||||
component: RootStatusComponent,
|
||||
order: 10,
|
||||
props: {
|
||||
label,
|
||||
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 ChatWidget from './components/ChatWidget.svelte'
|
||||
import ChatWidgetTab from './components/ChatWidgetTab.svelte'
|
||||
import WorkbenchTabExtension from './components/WorkbenchTabExtension.svelte'
|
||||
|
||||
import {
|
||||
chunterSpaceLinkFragmentProvider,
|
||||
@ -59,6 +60,7 @@ import {
|
||||
getMessageLink,
|
||||
getMessageLocation,
|
||||
getThreadLink,
|
||||
locationDataResolver,
|
||||
openChannelInSidebar,
|
||||
openChannelInSidebarAction,
|
||||
openThreadInSidebar,
|
||||
@ -178,7 +180,8 @@ export default async (): Promise<Resources> => ({
|
||||
ChatMessagePreview,
|
||||
JoinChannelNotificationPresenter,
|
||||
ChatWidget,
|
||||
ChatWidgetTab
|
||||
ChatWidgetTab,
|
||||
WorkbenchTabExtension
|
||||
},
|
||||
activity: {
|
||||
ChannelCreatedMessage,
|
||||
@ -203,7 +206,8 @@ export default async (): Promise<Resources> => ({
|
||||
CloseChatWidgetTab: closeChatWidgetTab,
|
||||
OpenChannelInSidebar: openChannelInSidebar,
|
||||
CanTranslateMessage: canTranslateMessage,
|
||||
OpenThreadInSidebar: openThreadInSidebar
|
||||
OpenThreadInSidebar: openThreadInSidebar,
|
||||
LocationDataResolver: locationDataResolver
|
||||
},
|
||||
actionImpl: {
|
||||
ArchiveChannel,
|
||||
|
@ -4,7 +4,8 @@ import {
|
||||
getCurrentResolvedLocation,
|
||||
getLocation,
|
||||
type Location,
|
||||
navigate
|
||||
navigate,
|
||||
languageStore
|
||||
} from '@hcengineering/ui'
|
||||
import { type Ref, type Doc, type Class, generateId } from '@hcengineering/core'
|
||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||
@ -16,12 +17,12 @@ import {
|
||||
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 workbench, { type Widget, workbenchId, type LocationData } from '@hcengineering/workbench'
|
||||
import { classIcon, getObjectLinkId, parseLinkId } 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 { type Asset, type IntlString, translate } from '@hcengineering/platform'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
@ -385,3 +386,73 @@ export function closeChatWidgetTab (tab?: ChatWidgetTab): void {
|
||||
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/view": "^0.6.13",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
"@hcengineering/workbench": "^0.6.16",
|
||||
"svelte": "^4.2.12"
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +144,8 @@ import {
|
||||
getCurrentEmployeePosition,
|
||||
getPersonTooltip,
|
||||
grouppingPersonManager,
|
||||
resolveLocation
|
||||
resolveLocation,
|
||||
resolveLocationData
|
||||
} from './utils'
|
||||
|
||||
export * from './utils'
|
||||
@ -444,7 +445,8 @@ export default async (): Promise<Resources> => ({
|
||||
PersonFilterFunction: filterPerson
|
||||
},
|
||||
resolver: {
|
||||
Location: resolveLocation
|
||||
Location: resolveLocation,
|
||||
LocationData: resolveLocationData
|
||||
},
|
||||
aggregation: {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
|
@ -50,7 +50,7 @@ import core, {
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
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 { type TemplateDataProvider } from '@hcengineering/templates'
|
||||
import {
|
||||
@ -64,6 +64,7 @@ import {
|
||||
import view, { type Filter, type GrouppingManager } from '@hcengineering/view'
|
||||
import { accessDeniedStore, FilterQuery } from '@hcengineering/view-resources'
|
||||
import { derived, get, writable } from 'svelte/store'
|
||||
import { type LocationData } from '@hcengineering/workbench'
|
||||
|
||||
import contact from './plugin'
|
||||
import { personStore } from '.'
|
||||
@ -536,3 +537,30 @@ export function groupPersonAccountValuesWithEmpty (
|
||||
}
|
||||
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,
|
||||
AddGuest: '' as IntlString,
|
||||
Members: '' as IntlString,
|
||||
Contacts: '' as IntlString
|
||||
Contacts: '' as IntlString,
|
||||
Employees: '' as IntlString,
|
||||
Persons: '' as IntlString
|
||||
},
|
||||
viewlet: {
|
||||
TableMember: '' as Ref<Viewlet>,
|
||||
|
@ -175,7 +175,10 @@ export function getDocumentLinkId (doc: Document): string {
|
||||
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('-')
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1] as Ref<Document>
|
||||
|
@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
pushRootBarComponent('left', love.component.ControlExt)
|
||||
pushRootBarComponent('left', love.component.ControlExt, 20)
|
||||
lk.on(RoomEvent.TrackSubscribed, handleTrackSubscribed)
|
||||
lk.on(RoomEvent.TrackUnsubscribed, handleTrackUnsubscribed)
|
||||
})
|
||||
|
@ -34,6 +34,7 @@
|
||||
"WidgetPreferences": "Widget preferences",
|
||||
"OpenInSidebar": "Open in sidebar",
|
||||
"OpenInSidebarNewTab": "Open in sidebar new tab",
|
||||
"ConfigureWidgets": "Configure widgets"
|
||||
"ConfigureWidgets": "Configure widgets",
|
||||
"Tab": "Tab"
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@
|
||||
"WidgetPreferences": "Preferencias del widget",
|
||||
"OpenInSidebar": "Abrir en 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",
|
||||
"OpenInSidebar": "Ouvrir dans 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",
|
||||
"OpenInSidebar": "Abrir na barra lateral",
|
||||
"OpenInSidebarNewTab": "Abrir em uma nova aba da barra lateral",
|
||||
"ConfigureWidgets": "Configurar widgets"
|
||||
"ConfigureWidgets": "Configurar widgets",
|
||||
"Tab": "Aba"
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@
|
||||
"WidgetPreferences": "Настройки виджета",
|
||||
"OpenInSidebar": "Открыть в боковой панели",
|
||||
"OpenInSidebarNewTab": "Открыть в новой вкладке боковой панели",
|
||||
"ConfigureWidgets": "Настроить виджеты"
|
||||
"ConfigureWidgets": "Настроить виджеты",
|
||||
"Tab": "Вкладка"
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@
|
||||
"WidgetPreferences": "小部件首选项",
|
||||
"OpenInSidebar": "在侧边栏中打开",
|
||||
"OpenInSidebarNewTab": "在侧边栏新标签页中打开",
|
||||
"ConfigureWidgets": "配置小部件"
|
||||
"ConfigureWidgets": "配置小部件",
|
||||
"Tab": "选项卡"
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,20 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
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 notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
|
||||
import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
ActionContext,
|
||||
ComponentExtensions,
|
||||
@ -30,55 +39,62 @@
|
||||
reduceCalls
|
||||
} from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting'
|
||||
import support, { SupportStatus, supportLink } from '@hcengineering/support'
|
||||
import support, { supportLink, SupportStatus } from '@hcengineering/support'
|
||||
import {
|
||||
AnyComponent,
|
||||
areLocationsEqual,
|
||||
Button,
|
||||
closePanel,
|
||||
closePopup,
|
||||
closeTooltip,
|
||||
CompAndProps,
|
||||
Component,
|
||||
defineSeparators,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
Dock,
|
||||
getCurrentLocation,
|
||||
getLocation,
|
||||
IconSettings,
|
||||
Label,
|
||||
Location,
|
||||
location,
|
||||
locationStorageKeyId,
|
||||
locationToUrl,
|
||||
mainSeparators,
|
||||
navigate,
|
||||
openPanel,
|
||||
PanelInstance,
|
||||
Popup,
|
||||
PopupAlignment,
|
||||
PopupPosAlignment,
|
||||
PopupResult,
|
||||
ResolvedLocation,
|
||||
Separator,
|
||||
TooltipInstance,
|
||||
areLocationsEqual,
|
||||
closePanel,
|
||||
closePopup,
|
||||
closeTooltip,
|
||||
defineSeparators,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
getCurrentLocation,
|
||||
getLocation,
|
||||
location,
|
||||
locationStorageKeyId,
|
||||
navigate,
|
||||
openPanel,
|
||||
popupstore,
|
||||
pushRootBarComponent,
|
||||
ResolvedLocation,
|
||||
resolvedLocationStore,
|
||||
Separator,
|
||||
setResolvedLocation,
|
||||
showPopup,
|
||||
workbenchSeparators,
|
||||
mainSeparators
|
||||
TooltipInstance,
|
||||
workbenchSeparators
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import {
|
||||
accessDeniedStore,
|
||||
ActionHandler,
|
||||
ListSelectionProvider,
|
||||
NavLink,
|
||||
accessDeniedStore,
|
||||
migrateViewOpttions,
|
||||
NavLink,
|
||||
parseLinkId,
|
||||
updateFocus
|
||||
} 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 { subscribeMobile } from '../mobile'
|
||||
import workbench from '../plugin'
|
||||
@ -96,6 +112,7 @@
|
||||
import TopMenu from './icons/TopMenu.svelte'
|
||||
import WidgetsBar from './sidebar/Sidebar.svelte'
|
||||
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
|
||||
import { getTabLocation, selectTab, syncWorkbenchTab, tabIdStore, tabsStore } from '../workbench'
|
||||
|
||||
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(() => {
|
||||
pushRootBarComponent('right', view.component.SearchSelector)
|
||||
pushRootBarComponent('left', workbench.component.WorkbenchTabs, 30)
|
||||
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||
$workspacesStore = await getWorkspaceFn()
|
||||
await updateWindowTitle(getLocation())
|
||||
})
|
||||
syncSidebarState()
|
||||
syncWorkbenchTab()
|
||||
})
|
||||
|
||||
const account = getCurrentAccount() as PersonAccount
|
||||
@ -296,7 +374,13 @@
|
||||
async function syncLoc (loc: Location): Promise<void> {
|
||||
accessDeniedStore.set(false)
|
||||
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) {
|
||||
// resolve short links
|
||||
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 Workbench from './components/Workbench.svelte'
|
||||
import ServerManager from './components/ServerManager.svelte'
|
||||
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
|
||||
import { isAdminUser } from '@hcengineering/presentation'
|
||||
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
|
||||
|
||||
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
|
||||
return spaces.find((sp) => sp.archived) !== undefined
|
||||
@ -46,13 +48,18 @@ export default async (): Promise<Resources> => ({
|
||||
SpacePanel,
|
||||
SpecialView,
|
||||
Workbench,
|
||||
ServerManager
|
||||
ServerManager,
|
||||
WorkbenchTabs
|
||||
},
|
||||
function: {
|
||||
HasArchiveSpaces: hasArchiveSpaces,
|
||||
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser()
|
||||
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
|
||||
CanCloseTab: canCloseTab
|
||||
},
|
||||
actionImpl: {
|
||||
Navigate: doNavigate
|
||||
Navigate: doNavigate,
|
||||
PinTab: pinTab,
|
||||
UnpinTab: unpinTab,
|
||||
CloseTab: closeTab
|
||||
}
|
||||
})
|
||||
|
@ -47,13 +47,15 @@ export default mergeIds(workbenchId, workbench, {
|
||||
WorkspaceCreating: '' as IntlString,
|
||||
AccessDenied: '' as IntlString,
|
||||
Widget: '' as IntlString,
|
||||
WidgetPreference: '' as IntlString
|
||||
WidgetPreference: '' as IntlString,
|
||||
Tab: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
MobileAllowed: '' as Metadata<boolean>
|
||||
},
|
||||
component: {
|
||||
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
|
||||
*/
|
||||
|
||||
export interface LocationData {
|
||||
name?: string
|
||||
nameIntl?: IntlString
|
||||
icon?: Asset
|
||||
iconComponent?: AnyComponent
|
||||
iconProps?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface Application extends Doc {
|
||||
label: IntlString
|
||||
alias: string
|
||||
@ -42,6 +51,7 @@ export interface Application extends Doc {
|
||||
aside?: AnyComponent
|
||||
|
||||
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?: AnyComponent
|
||||
@ -102,6 +112,11 @@ export interface TxSidebarEvent<T extends Record<string, any> = Record<string, a
|
||||
params: T
|
||||
}
|
||||
|
||||
export interface WorkbenchTab extends Preference {
|
||||
location: string
|
||||
isPinned: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -201,7 +216,8 @@ export default plugin(workbenchId, {
|
||||
HiddenApplication: '' as Ref<Class<HiddenApplication>>,
|
||||
Widget: '' as Ref<Class<Widget>>,
|
||||
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: {
|
||||
SpaceView: '' as Ref<Mixin<SpaceView>>
|
||||
@ -238,7 +254,8 @@ export default plugin(workbenchId, {
|
||||
NavigationExpandedDefault: '' as Metadata<boolean>
|
||||
},
|
||||
extensions: {
|
||||
WorkbenchExtensions: '' as ComponentExtensionId
|
||||
WorkbenchExtensions: '' as ComponentExtensionId,
|
||||
WorkbenchTabExtensions: '' as ComponentExtensionId
|
||||
},
|
||||
actionImpl: {
|
||||
Navigate: '' as ViewAction<{
|
||||
|
Loading…
Reference in New Issue
Block a user