platform/plugins/workbench-resources/src/components/Workbench.svelte
Victor Ilyushchenko c7847a035c
Improved URL-to-reference conversion (#8916)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
2025-05-14 10:08:51 +07:00

1229 lines
40 KiB
Svelte

<!--
// Copyright © 2022, 2023 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 { Analytics } from '@hcengineering/analytics'
import contact, { getCurrentEmployee } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import core, {
AccountRole,
Class,
Doc,
getCurrentAccount,
hasAccountRole,
Ref,
SortingOrder,
Space
} from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { broadcastEvent, getMetadata, getResource, IntlString, translate } from '@hcengineering/platform'
import {
ActionContext,
ComponentExtensions,
createQuery,
getClient,
isAdminUser,
reduceCalls,
createNotificationsQuery
} from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
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,
languageStore,
Location,
location,
locationStorageKeyId,
locationToUrl,
mainSeparators,
navigate,
showPanel,
PanelInstance,
Popup,
PopupAlignment,
PopupPosAlignment,
PopupResult,
popupstore,
pushRootBarComponent,
ResolvedLocation,
resolvedLocationStore,
Separator,
setResolvedLocation,
showPopup,
TooltipInstance,
workbenchSeparators,
resizeObserver,
isSameSegments
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
accessDeniedStore,
ActionHandler,
ListSelectionProvider,
migrateViewOpttions,
NavLink,
parseLinkId,
updateFocus
} from '@hcengineering/view-resources'
import type {
Application,
NavigatorModel,
SpecialNavModel,
ViewConfiguration,
WorkbenchTab
} from '@hcengineering/workbench'
import { getContext, onDestroy, onMount, tick } from 'svelte'
import chat, { chatId } from '@hcengineering/chat'
import { subscribeMobile } from '../mobile'
import workbench from '../plugin'
import { buildNavModel, logOut, workspacesStore } from '../utils'
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
import AppSwitcher from './AppSwitcher.svelte'
import Applications from './Applications.svelte'
import Logo from './Logo.svelte'
import NavFooter from './NavFooter.svelte'
import NavHeader from './NavHeader.svelte'
import Navigator from './Navigator.svelte'
import SelectWorkspaceMenu from './SelectWorkspaceMenu.svelte'
import SpaceView from './SpaceView.svelte'
import TopMenu from './icons/TopMenu.svelte'
import WidgetsBar from './sidebar/Sidebar.svelte'
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
import {
getTabDataByLocation,
getTabLocation,
prevTabIdStore,
selectTab,
syncWorkbenchTab,
tabIdStore,
tabsStore
} from '../workbench'
import { get } from 'svelte/store'
import inbox, { inboxId } from '@hcengineering/inbox'
const HIDE_NAVIGATOR = 720
const FLOAT_ASIDE = 1024 // lg
let contentPanel: HTMLElement
const { setTheme } = getContext<{ setTheme: (theme: string) => void }>('theme')
let currentAppAlias: string | undefined
let currentSpace: Ref<Space> | undefined
let currentSpecial: string | undefined
let currentQuery: Record<string, string | null> | undefined
let specialComponent: SpecialNavModel | undefined
let currentFragment: string | undefined = ''
let currentApplication: Application | undefined
let navigatorModel: NavigatorModel | undefined
let currentView: ViewConfiguration | undefined
let createItemDialog: AnyComponent | undefined
let createItemLabel: IntlString | undefined
const account = getCurrentAccount()
const me = getCurrentEmployee()
$: person = $personByIdStore.get(me)
migrateViewOpttions()
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
const client = getClient()
const apps: Application[] = client
.getModel()
.findAllSync<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
let panelInstance: PanelInstance
let popupInstance: Popup
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const mobileAdaptive = $deviceInfo.isMobile && $deviceInfo.minWidth
const defaultNavigator = !(getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true)
const savedNavigator = localStorage.getItem('hiddenNavigator')
let hiddenNavigator: boolean = savedNavigator !== null ? savedNavigator === 'true' : defaultNavigator
let hiddenAside: boolean = true
$deviceInfo.navigator.visible = !hiddenNavigator
async function toggleNav (): Promise<void> {
$deviceInfo.navigator.visible = !$deviceInfo.navigator.visible
if (!$deviceInfo.navigator.float) {
hiddenNavigator = !$deviceInfo.navigator.visible
localStorage.setItem('hiddenNavigator', `${hiddenNavigator}`)
}
closeTooltip()
if (currentApplication && navigatorModel) {
await tick()
panelInstance.fitPopupInstance()
popupInstance.fitPopupInstance()
}
}
let tabs: WorkbenchTab[] = []
let areTabsLoaded = false
const query = createQuery()
$: query.query(
workbench.class.WorkbenchTab,
{ attachedTo: account.uuid },
(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> {
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 data = await getTabDataByLocation(loc)
const name = data.name ?? (await translate(data.label, {}, get(languageStore)))
const tabByName = get(tabsStore).find((t) => {
if (t.location === url) return true
if (t.name !== name) return false
const tabLoc = getTabLocation(t)
return tabLoc.path[2] === loc.path[2] && tabLoc.path[3] === loc.path[3]
})
if (tabByName !== undefined) {
selectTab(tabByName._id)
prevTabIdStore.set(tabByName._id)
} else {
const tabToReplace = tabs.findLast((t) => !t.isPinned)
if (tabToReplace !== undefined) {
const op = client.apply(undefined, undefined, true)
await op.update(tabToReplace, {
location: url
})
await op.commit()
selectTab(tabToReplace._id)
prevTabIdStore.set(tabToReplace._id)
} else {
console.log('Creating new tab on init')
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: account.uuid,
location: url,
isPinned: false
})
selectTab(_id)
prevTabIdStore.set(_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 workspaceId = $location.path[1]
const inboxClient = InboxNotificationsClientImpl.createClient()
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined =
undefined
let hasInboxNotifications = false
void getResource(notification.function.HasInboxNotifications).then((f) => {
hasNotificationsFn = f
})
$: void hasNotificationsFn?.($inboxNotificationsByContextStore).then((res) => {
hasInboxNotifications = res
})
const doSyncLoc = reduceCalls(async (loc: Location): Promise<void> => {
if (workspaceId !== $location.path[1]) {
tabs = []
// Switch of workspace
return
}
closeTooltip()
closePopup()
await syncLoc(loc)
await updateWindowTitle(loc)
checkOnHide()
})
onDestroy(
location.subscribe((loc) => {
void doSyncLoc(loc)
})
)
let windowWorkspaceName = ''
async function updateWindowTitle (loc: Location): Promise<void> {
let wsUrl = loc.path[1]
const ws = $workspacesStore.find((it) => it.url === wsUrl)
if (ws !== undefined) {
wsUrl = ws?.name ?? ws.url
windowWorkspaceName = wsUrl
}
const docTitle = await getWindowTitle(loc)
if (docTitle !== undefined && docTitle !== '') {
document.title = wsUrl == null ? docTitle : `${docTitle} - ${wsUrl}`
} else {
const title = getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform'
document.title = wsUrl == null ? title : `${wsUrl} - ${title}`
}
void broadcastEvent(workbench.event.NotifyTitle, document.title)
}
async function getWindowTitle (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 resolveShortLink (loc: Location): Promise<ResolvedLocation | undefined> {
let locationResolver = currentApplication?.locationResolver
if (loc.path[2] != null && loc.path[2].trim().length > 0) {
const app = apps.find((p) => p.alias === loc.path[2])
if (app?.locationResolver) {
locationResolver = app?.locationResolver
}
}
if (locationResolver) {
const resolver = await getResource(locationResolver)
return await resolver(loc)
}
}
function mergeLoc (loc: Location, resolved: ResolvedLocation): Location {
const resolvedApp = resolved.loc.path[2]
const resolvedSpace = resolved.loc.path[3]
const resolvedSpecial = resolved.loc.path[4]
if (resolvedApp === undefined) {
if (currentAppAlias === undefined) {
loc.path = [loc.path[0], loc.path[1], ...resolved.defaultLocation.path.splice(2)]
} else {
const isSameApp = currentAppAlias === loc.path[2]
loc.path[2] = currentAppAlias ?? resolved.defaultLocation.path[2]
loc.path[3] = currentSpace ?? currentSpecial ?? resolved.defaultLocation.path[3]
if (loc.path[3] !== undefined && isSameApp) {
// setting space special/aside only if it belongs to the same app
if (currentSpace && currentSpecial) {
loc.path[4] = currentSpecial
} else if (loc.path[3] === resolved.defaultLocation.path[3]) {
loc.path[4] = resolved.defaultLocation.path[4]
}
} else {
loc.path.length = 4
}
}
} else {
loc.path[2] = resolvedApp
if (resolvedSpace === undefined) {
loc.path[3] = currentSpace ?? (currentSpecial as string) ?? resolved.defaultLocation.path[3]
loc.path[4] = (currentSpecial as string) ?? resolved.defaultLocation.path[4]
} else {
loc.path[3] = resolvedSpace
if (resolvedSpecial) {
loc.path[4] = resolvedSpecial
} else if (currentSpace && currentSpecial) {
loc.path[4] = currentSpecial
} else {
loc.path[4] = resolved.defaultLocation.path[4]
}
}
}
for (let index = 0; index < loc.path.length; index++) {
const path = loc.path[index]
if (path === undefined) {
loc.path.length = index
break
}
}
loc.query = resolved.loc.query ?? loc.query ?? currentQuery ?? resolved.defaultLocation.query
loc.fragment =
(loc.fragment ?? '') !== '' && resolved.loc.fragment === resolved.defaultLocation.fragment
? loc.fragment
: resolved.loc.fragment ?? resolved.defaultLocation.fragment
return loc
}
async function syncLoc (loc: Location): Promise<void> {
accessDeniedStore.set(false)
const originalLoc = JSON.stringify(loc)
if ($tabIdStore !== $prevTabIdStore) {
if ($prevTabIdStore) {
const prevTab = tabs.find((t) => t._id === $prevTabIdStore)
const prevTabLoc = prevTab ? getTabLocation(prevTab) : undefined
if (prevTabLoc === undefined || prevTabLoc.path[2] !== loc.path[2]) {
clear(1)
}
}
prevTabIdStore.set($tabIdStore)
}
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
// resolve short links
const resolvedLoc = await resolveShortLink(loc)
if (resolvedLoc !== undefined && !areLocationsEqual(loc, resolvedLoc.loc)) {
loc = mergeLoc(loc, resolvedLoc)
}
}
setResolvedLocation(loc)
const app = loc.path[2]
let space = loc.path[3] as Ref<Space>
let special = loc.path[4]
const fragment = loc.fragment
let navigateDone = false
if (app === undefined) {
const last = localStorage.getItem(`${locationStorageKeyId}_${loc.path[1]}`)
if (last != null) {
const lastValue = JSON.parse(last)
if (isSameSegments(lastValue, loc, 2)) {
navigateDone = navigate(lastValue)
if (navigateDone) {
return
}
}
}
if (app === undefined && !navigateDone) {
const appShort = getMetadata(workbench.metadata.DefaultApplication) as Ref<Application>
if (appShort == null) return
const spaceRef = getMetadata(workbench.metadata.DefaultSpace) as Ref<Space>
const specialRef = getMetadata(workbench.metadata.DefaultSpecial) as Ref<Space>
const loc = getCurrentLocation()
// Be sure URI is not yet changed
if (loc.path[2] === undefined && loc.path[0] === 'workbench') {
loc.path[2] = appShort
let len = 3
if (spaceRef !== undefined && specialRef !== undefined) {
const spaceObj = await client.findOne<Space>(core.class.Space, { _id: spaceRef })
if (spaceObj !== undefined) {
loc.path[3] = spaceRef
loc.path[4] = specialRef
len = 5
}
}
loc.path.length = len
if (navigate(loc)) {
return
}
}
}
}
if (currentAppAlias !== app) {
clear(1)
currentApplication = await client.findOne<Application>(workbench.class.Application, { alias: app })
currentAppAlias = currentApplication?.alias
navigatorModel = await buildNavModel(client, currentApplication)
}
if (
space === undefined &&
((navigatorModel?.spaces?.length ?? 0) > 0 || (navigatorModel?.specials?.length ?? 0) > 0)
) {
const last = localStorage.getItem(`${locationStorageKeyId}_${app}`)
if (last !== null) {
const newLocation: Location = JSON.parse(last)
if (newLocation.path[3] != null) {
space = loc.path[3] = newLocation.path[3] as Ref<Space>
special = loc.path[4] = newLocation.path[4]
if (loc.path[4] == null) {
loc.path.length = 4
} else {
loc.path.length = 5
}
if (fragment === undefined) {
navigate(loc)
return
}
}
}
}
if (currentSpecial === undefined || currentSpecial !== space) {
const newSpecial = space !== undefined ? getSpecialComponent(space) : undefined
if (newSpecial !== undefined) {
clear(2)
specialComponent = newSpecial
currentSpecial = space
} else {
await updateSpace(space)
setSpaceSpecial(special)
}
}
if (app !== undefined) {
localStorage.setItem(`${locationStorageKeyId}_${app}`, originalLoc)
}
currentQuery = loc.query
if (fragment !== currentFragment) {
currentFragment = fragment
if (fragment != null && fragment.trim().length > 0) {
await setOpenPanelFocus(fragment)
} else {
closePanel()
}
}
}
async function setOpenPanelFocus (fragment: string): Promise<void> {
const props = decodeURIComponent(fragment).split('|')
if (props.length >= 3) {
const _class = props[2] as Ref<Class<Doc>>
const _id = await parseLinkId(linkProviders, props[1], _class)
const doc = await client.findOne<Doc>(_class, { _id })
panelDoc = { _class, _id }
if (doc !== undefined) {
const provider = ListSelectionProvider.Find(doc._id)
updateFocus({
provider,
focus: doc
})
showPanel(
props[0] as AnyComponent,
_id,
_class,
(props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent,
false
)
} else {
accessDeniedStore.set(true)
closePanel(false)
}
} else {
closePanel(false)
}
}
let panelDoc: undefined | { _id: Ref<Doc>, _class: Ref<Class<Doc>> } = undefined
const panelQuery = createQuery()
$: if (panelDoc !== undefined) {
panelQuery.query(panelDoc._class, { _id: panelDoc._id }, (r) => {
if (r.length === 0) {
closePanel(false)
panelDoc = undefined
}
})
}
function clear (level: number): void {
switch (level) {
case 1:
currentAppAlias = undefined
currentApplication = undefined
navigatorModel = undefined
// eslint-disable-next-line no-fallthrough
case 2:
currentSpace = undefined
currentSpecial = undefined
currentView = undefined
createItemDialog = undefined
createItemLabel = undefined
specialComponent = undefined
// eslint-disable-next-line no-fallthrough
case 3:
if (currentSpace !== undefined) {
specialComponent = undefined
}
}
}
async function updateSpace (spaceId?: Ref<Space>): Promise<void> {
if (spaceId === currentSpace) return
clear(2)
currentSpace = spaceId
if (spaceId === undefined) return
const space = await client.findOne<Space>(core.class.Space, { _id: spaceId })
if (space === undefined) return
const spaceClass = client.getHierarchy().getClass(space._class)
const view = client.getHierarchy().as(spaceClass, workbench.mixin.SpaceView)
currentView = view.view
createItemDialog = currentView?.createItemDialog
createItemLabel = currentView?.createItemLabel
}
function setSpaceSpecial (spaceSpecial: string | undefined): void {
if (currentSpecial !== undefined && spaceSpecial === currentSpecial) return
clear(3)
if (spaceSpecial === undefined) return
specialComponent = getSpecialComponent(spaceSpecial)
if (specialComponent !== undefined) {
currentSpecial = spaceSpecial
}
}
function getSpecialComponent (id: string): SpecialNavModel | undefined {
const sp = navigatorModel?.specials?.find((x) => x.id === id)
if (sp !== undefined) {
if (sp.accessLevel !== undefined && !hasAccountRole(account, sp.accessLevel)) {
return undefined
}
return sp
}
for (const s of navigatorModel?.spaces ?? []) {
const sp = s.specials?.find((x) => x.id === id)
if (sp !== undefined) {
return sp
}
}
}
let cover: HTMLElement
let workbenchWidth: number = $deviceInfo.docWidth
$deviceInfo.navigator.float = workbenchWidth <= HIDE_NAVIGATOR
const checkWorkbenchWidth = (): void => {
if (workbenchWidth <= HIDE_NAVIGATOR && !$deviceInfo.navigator.float) {
$deviceInfo.navigator.visible = false
$deviceInfo.navigator.float = true
} else if (workbenchWidth > HIDE_NAVIGATOR && $deviceInfo.navigator.float) {
$deviceInfo.navigator.float = false
$deviceInfo.navigator.visible = !hiddenNavigator
}
}
checkWorkbenchWidth()
$: if ($deviceInfo.docWidth <= FLOAT_ASIDE && !$sidebarStore.float) {
hiddenAside = $sidebarStore.variant === SidebarVariant.MINI
$sidebarStore.float = true
} else if ($deviceInfo.docWidth > FLOAT_ASIDE && $sidebarStore.float) {
$sidebarStore.float = false
$sidebarStore.variant = hiddenAside ? SidebarVariant.MINI : SidebarVariant.EXPANDED
}
const checkOnHide = (): void => {
if ($deviceInfo.navigator.visible && $deviceInfo.navigator.float) $deviceInfo.navigator.visible = false
}
let oldNavVisible: boolean = $deviceInfo.navigator.visible
let oldASideVisible: boolean = $sidebarStore.variant !== SidebarVariant.MINI
$: if (
oldNavVisible !== $deviceInfo.navigator.visible ||
oldASideVisible !== ($sidebarStore.variant !== SidebarVariant.MINI)
) {
if (mobileAdaptive && $deviceInfo.navigator.float) {
if ($deviceInfo.navigator.visible && $sidebarStore.variant !== SidebarVariant.MINI) {
if (oldNavVisible) $deviceInfo.navigator.visible = false
else $sidebarStore.variant = SidebarVariant.MINI
}
}
oldNavVisible = $deviceInfo.navigator.visible
oldASideVisible = $sidebarStore.variant !== SidebarVariant.MINI
}
$: if (
$sidebarStore.float &&
$sidebarStore.variant !== SidebarVariant.MINI &&
$sidebarStore.widget === undefined &&
$sidebarStore.widgetsState.size > 0
) {
$sidebarStore.widget = Array.from($sidebarStore.widgetsState.keys())[0]
}
location.subscribe(() => {
if (mobileAdaptive && $sidebarStore.variant !== SidebarVariant.MINI) $sidebarStore.variant = SidebarVariant.MINI
})
$: $deviceInfo.navigator.direction = $deviceInfo.isMobile && $deviceInfo.isPortrait ? 'horizontal' : 'vertical'
let appsMini: boolean
$: appsMini =
$deviceInfo.isMobile &&
(($deviceInfo.isPortrait && $deviceInfo.docWidth <= 480) ||
(!$deviceInfo.isPortrait && $deviceInfo.docHeight <= 480))
let popupPosition: PopupPosAlignment
$: popupPosition =
$deviceInfo.navigator.direction === 'horizontal'
? 'account-portrait'
: $deviceInfo.navigator.direction === 'vertical' && $deviceInfo.isMobile
? 'account-mobile'
: 'account'
let popupSpacePosition: PopupPosAlignment
$: popupSpacePosition = appsMini
? 'logo-mini'
: $deviceInfo.navigator.direction === 'horizontal'
? 'logo-portrait'
: 'logo'
onMount(() => {
subscribeMobile(setTheme)
})
function checkInbox (popups: CompAndProps[]) {
if (inboxPopup !== undefined) {
const exists = popups.find((p) => p.id === inboxPopup?.id)
if (!exists) {
inboxPopup = undefined
}
}
}
let supportStatus: SupportStatus | undefined = undefined
function handleSupportStatusChanged (status: SupportStatus) {
supportStatus = status
}
const supportClient = getResource(support.function.GetSupport).then(
async (res) =>
await res((status) => {
handleSupportStatusChanged(status)
})
)
onDestroy(async () => {
await supportClient?.then((support) => {
support?.destroy()
})
})
let supportWidgetLoading = false
async function handleToggleSupportWidget (): Promise<void> {
const timer = setTimeout(() => {
supportWidgetLoading = true
}, 100)
const support = await supportClient
await support.toggleWidget()
clearTimeout(timer)
supportWidgetLoading = false
}
$: checkInbox($popupstore)
let inboxPopup: PopupResult | undefined = undefined
let lastLoc: Location | undefined = undefined
defineSeparators('workbench', workbenchSeparators)
defineSeparators('main', mainSeparators)
$: mainNavigator = currentApplication && navigatorModel && $deviceInfo.navigator.visible
$: elementPanel = $deviceInfo.replacedPanel ?? contentPanel
$: deactivated =
person && client.getHierarchy().hasMixin(person, contact.mixin.Employee)
? !client.getHierarchy().as(person, contact.mixin.Employee).active
: false
</script>
{#if person && deactivated && !isAdminUser()}
<div class="flex-col-center justify-center h-full flex-grow">
<h1><Label label={workbench.string.AccountDisabled} /></h1>
<Label label={workbench.string.AccountDisabledDescr} />
<Button
label={setting.string.Signout}
kind={'link'}
size={'small'}
on:click={() => {
void logOut().then(() => {
navigate({ path: [loginId] })
})
}}
/>
</div>
{:else if person || account.role === AccountRole.Owner || isAdminUser()}
<ActionHandler {currentSpace} />
<svg class="svg-mask">
<clipPath id="notify-normal">
<path d="M12,14c0-3.3,2.7-6,6-6c0.7,0,1.4,0.1,2,0.4V0H0v20h18C14.7,20,12,17.3,12,14z" />
<path d="M18,20h2v-0.4C19.4,19.9,18.7,20,18,20z" />
</clipPath>
<clipPath id="notify-small">
<path d="M10.5,12.2c0-2.9,2.4-5.2,5.2-5.2c0.6,0,1.2,0.1,1.8,0.3V0H0v17.5h15.8C12.9,17.5,10.5,15.1,10.5,12.2z" />
<path d="M15.8,17.5h1.8v-0.4C17,17.4,16.4,17.5,15.8,17.5z" />
</clipPath>
</svg>
<div class="workbench-container apps-{$deviceInfo.navigator.direction}">
<div
class="antiPanel-application {$deviceInfo.navigator.direction} no-print"
class:lastDivider={!$deviceInfo.navigator.visible}
>
<div
class="hamburger-container clear-mins"
class:portrait={$deviceInfo.navigator.direction === 'horizontal'}
class:landscape={$deviceInfo.navigator.direction === 'vertical'}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="logo-container clear-mins"
class:mini={appsMini}
on:click={() => {
showPopup(SelectWorkspaceMenu, {}, popupSpacePosition)
}}
>
<Logo mini={appsMini} workspace={windowWorkspaceName ?? $resolvedLocationStore.path[1]} />
</div>
<div class="topmenu-container clear-mins flex-no-shrink" class:mini={appsMini}>
<AppItem
icon={TopMenu}
label={$deviceInfo.navigator.visible ? workbench.string.HideMenu : workbench.string.ShowMenu}
selected={!$deviceInfo.navigator.visible}
size={appsMini ? 'small' : 'medium'}
on:click={toggleNav}
/>
</div>
<!-- <ActivityStatus status="active" /> -->
<NavLink
app={notificationId}
shrink={0}
disabled={!$deviceInfo.navigator.visible && $deviceInfo.navigator.float && currentAppAlias === notificationId}
>
<AppItem
icon={notification.icon.Notifications}
label={notification.string.Inbox}
selected={currentAppAlias === notificationId || inboxPopup !== undefined}
navigator={(currentAppAlias === notificationId || inboxPopup !== undefined) &&
$deviceInfo.navigator.visible}
on:click={(e) => {
if (e.metaKey || e.ctrlKey) return
if (!$deviceInfo.navigator.visible && $deviceInfo.navigator.float && currentAppAlias === notificationId) {
toggleNav()
} else if (currentAppAlias === notificationId && lastLoc !== undefined) {
e.preventDefault()
e.stopPropagation()
navigate(lastLoc)
lastLoc = undefined
} else {
lastLoc = $location
}
}}
notify={hasInboxNotifications}
/>
</NavLink>
<Applications
{apps}
active={currentApplication?._id}
direction={$deviceInfo.navigator.direction}
on:toggleNav={toggleNav}
/>
</div>
<div
class="info-box {$deviceInfo.navigator.direction}"
class:vertical-mobile={$deviceInfo.navigator.direction === 'vertical'}
class:mini={appsMini}
>
<AppItem
icon={IconSettings}
label={setting.string.Settings}
size={appsMini ? 'small' : 'large'}
on:click={() => showPopup(AppSwitcher, { apps }, popupPosition)}
/>
<a href={supportLink} target="_blank" rel="noopener noreferrer">
<AppItem
icon={support.icon.Support}
label={support.string.ContactUs}
size={appsMini ? 'small' : 'large'}
notify={supportStatus?.hasUnreadMessages}
selected={supportStatus?.visible}
loading={supportWidgetLoading}
/>
</a>
<!-- {#await supportClient then client}
{#if client}
<AppItem
icon={support.icon.Support}
label={support.string.ContactUs}
size={appsMini ? 'small' : 'large'}
notify={supportStatus?.hasUnreadMessages}
selected={supportStatus?.visible}
loading={supportWidgetLoading}
on:click={async () => {
await handleToggleSupportWidget()
}}
/>
{/if}
{/await} -->
<div
class="flex-center"
class:mt-3={$deviceInfo.navigator.direction === 'vertical'}
class:ml-2={$deviceInfo.navigator.direction === 'horizontal'}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
id="profile-button"
class="cursor-pointer"
on:click|stopPropagation={() => showPopup(AccountPopup, {}, popupPosition)}
>
<Component
is={contact.component.Avatar}
props={{ person, name: person?.name, size: 'small', showStatus: true }}
/>
</div>
</div>
</div>
</div>
<ActionContext
context={{
mode: 'workbench',
application: currentApplication?._id
}}
/>
<div class="flex-row-center w-full h-full">
<div
class="workbench-container inner"
class:rounded={$sidebarStore.variant === SidebarVariant.EXPANDED}
use:resizeObserver={(element) => {
workbenchWidth = element.clientWidth
checkWorkbenchWidth()
}}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $deviceInfo.navigator.float && $deviceInfo.navigator.visible}
<div class="cover shown" on:click={() => ($deviceInfo.navigator.visible = false)} />
{/if}
{#if mainNavigator}
<div
class="antiPanel-navigator no-print {$deviceInfo.navigator.direction === 'horizontal'
? 'portrait'
: 'landscape'} border-left"
class:fly={$deviceInfo.navigator.float}
>
<div class="antiPanel-wrap__content hulyNavPanel-container">
{#if currentApplication}
<NavHeader label={currentApplication.label} />
{#if currentApplication.navHeaderComponent}
<Component
is={currentApplication.navHeaderComponent}
props={{
currentSpace,
currentSpecial,
currentFragment
}}
shrink
/>
{/if}
{/if}
<Navigator
{currentSpace}
{currentSpecial}
{currentFragment}
model={navigatorModel}
{currentApplication}
on:open={checkOnHide}
/>
<NavFooter>
{#if currentApplication && currentApplication.navFooterComponent}
<Component is={currentApplication.navFooterComponent} props={{ currentSpace }} />
{/if}
</NavFooter>
</div>
{#if !($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.minWidth)}
<Separator
name={'workbench'}
float={$deviceInfo.navigator.float ? 'navigator' : true}
index={0}
color={'var(--theme-navpanel-border)'}
/>
{/if}
</div>
<Separator
name={'workbench'}
float={$deviceInfo.navigator.float}
index={0}
color={'transparent'}
separatorSize={0}
short
/>
{/if}
<div
bind:this={contentPanel}
class={navigatorModel === undefined ? 'hulyPanels-container' : 'hulyComponent overflow-hidden'}
class:straighteningCorners={$sidebarStore.float &&
$sidebarStore.variant === SidebarVariant.EXPANDED &&
!(mobileAdaptive && $deviceInfo.isPortrait)}
data-id={'contentPanel'}
>
{#if currentApplication && currentApplication.component}
<Component
is={currentApplication.component}
props={{
currentSpace,
workbenchWidth
}}
/>
{:else if specialComponent}
<Component
is={specialComponent.component}
props={{
model: navigatorModel,
...specialComponent.componentProps,
currentSpace,
space: currentSpace,
navigationModel: specialComponent?.navigationModel,
workbenchWidth,
queryBuilder: specialComponent?.queryBuilder
}}
on:action={(e) => {
if (e?.detail) {
const loc = getCurrentLocation()
loc.query = { ...loc.query, ...e.detail }
navigate(loc)
}
}}
/>
{:else if currentView?.component !== undefined}
<Component
is={currentView.component}
props={{ ...currentView.componentProps, currentSpace, currentView, workbenchWidth }}
/>
{:else if $accessDeniedStore}
<div class="flex-center h-full">
<h2><Label label={workbench.string.AccessDenied} /></h2>
</div>
{:else}
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}
</div>
</div>
{#if $sidebarStore.variant === SidebarVariant.EXPANDED && !$sidebarStore.float}
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short />
{/if}
<WidgetsBar />
</div>
</div>
<Dock />
<div bind:this={cover} class="cover" />
<TooltipInstance />
<PanelInstance bind:this={panelInstance} contentPanel={elementPanel}>
<svelte:fragment slot="panel-header">
<ActionContext context={{ mode: 'panel' }} />
</svelte:fragment>
</PanelInstance>
<Popup bind:this={popupInstance} contentPanel={elementPanel}>
<svelte:fragment slot="popup-header">
<ActionContext context={{ mode: 'popup' }} />
</svelte:fragment>
</Popup>
<div class="hidden max-w-0 max-h-0">
<ComponentExtensions extension={workbench.extensions.WorkbenchExtensions} />
</div>
<BrowserNotificatator />
{/if}
<style lang="scss">
.workbench-container {
position: relative;
display: flex;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
background-color: var(--theme-panel-color);
touch-action: none;
&.apps-horizontal {
flex-direction: column-reverse;
}
&.inner {
background-color: var(--theme-navpanel-color);
.straighteningCorners {
border-radius: var(--medium-BorderRadius) 0 0 var(--medium-BorderRadius);
}
&.rounded {
border-radius: 0 var(--medium-BorderRadius) var(--medium-BorderRadius) 0;
}
}
&:not(.inner)::after {
position: absolute;
content: '';
inset: 0;
border: 1px solid var(--theme-divider-color);
border-radius: var(--medium-BorderRadius);
pointer-events: none;
}
.antiPanel-application.horizontal {
border-radius: 0 0 var(--medium-BorderRadius) var(--medium-BorderRadius);
border-top: none;
}
.antiPanel-application:not(.horizontal) {
border-radius: var(--medium-BorderRadius) 0 0 var(--medium-BorderRadius);
border-right: none;
}
}
.hamburger-container {
display: flex;
align-items: center;
&.portrait {
margin-left: 1rem;
.logo-container {
margin-right: 0.5rem;
}
.topmenu-container {
margin-right: 0.5rem;
}
}
&.landscape {
flex-direction: column;
margin-top: 1.25rem;
.logo-container {
margin-bottom: 0.25rem;
}
.topmenu-container {
margin-bottom: 1rem;
}
}
.logo-container,
.topmenu-container,
.spacer {
flex-shrink: 0;
}
.spacer {
width: 0.25rem;
height: 0.25rem;
}
.logo-container.mini,
.topmenu-container.mini {
position: fixed;
top: 4px;
}
.logo-container.mini {
left: 4px;
width: 1.75rem;
height: 1.75rem;
}
.topmenu-container.mini {
left: calc(1.75rem + 8px);
}
}
.info-box {
display: flex;
align-items: center;
&.vertical {
flex-direction: column;
margin-bottom: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--theme-navpanel-divider);
&-mobile {
margin-bottom: 1rem;
}
&.mini > *:not(:last-child) {
margin-bottom: 0.25rem;
}
}
&.horizontal {
margin-right: 1rem;
padding-left: 1rem;
border-left: 1px solid var(--theme-navpanel-divider);
&:not(.mini) > *:not(:last-child) {
margin-right: 0.75rem;
}
&.mini > *:not(:last-child) {
margin-right: 0.25rem;
}
}
}
.new-world {
display: flex;
align-items: center;
&.vertical {
flex-direction: column;
margin-top: auto;
border-top: 1px solid var(--theme-navpanel-divider);
padding: 0.5rem 0;
gap: 0.25rem;
}
&.horizontal {
margin-right: 1rem;
padding-left: 1rem;
border-left: 1px solid var(--theme-navpanel-divider);
&:not(.mini) > *:not(:last-child) {
margin-right: 0.75rem;
}
&.mini > *:not(:last-child) {
margin-right: 0.25rem;
}
}
}
.cover {
position: fixed;
display: none;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
&.shown {
display: block;
}
}
@media print {
.workbench-container:has(~ .panel-instance) {
display: none;
}
}
</style>