platform/plugins/workbench-resources/src/components/Workbench.svelte
Denis Bykhov 2f3bba642f
UBERF-5482 (#4619)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
2024-02-14 13:19:34 +07:00

1029 lines
33 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 contact, { Employee, PersonAccount } from '@hcengineering/contact'
import core, { AccountRole, Class, Doc, Ref, Space, getCurrentAccount } from '@hcengineering/core'
import login from '@hcengineering/login'
import notification, { DocNotifyContext, InboxNotification, inboxId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import support, { SupportStatus, supportLink } from '@hcengineering/support'
import {
AnyComponent,
Button,
CompAndProps,
Component,
IconSettings,
Label,
Location,
PanelInstance,
Popup,
PopupAlignment,
PopupPosAlignment,
PopupResult,
ResolvedLocation,
Separator,
TooltipInstance,
areLocationsEqual,
closePanel,
closePopup,
closeTooltip,
defineSeparators,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
getLocation,
location,
locationStorageKeyId,
navigate,
openPanel,
popupstore,
resolvedLocationStore,
setResolvedLocation,
showPopup,
workbenchSeparators
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
ActionHandler,
ListSelectionProvider,
NavLink,
migrateViewOpttions,
updateFocus
} from '@hcengineering/view-resources'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
import { getContext, onDestroy, onMount, tick } from 'svelte'
import { subscribeMobile } from '../mobile'
import workbench from '../plugin'
import { buildNavModel, signOut, 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'
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 asideId: string | 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
migrateViewOpttions()
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
const client = getClient()
let apps: Application[] | Promise<Application[]> = client
.findAll<Application>(workbench.class.Application, { hidden: false, _id: { $nin: excludedApps } })
.then((res) => (apps = res))
let panelInstance: PanelInstance
let popupInstance: Popup
let visibleNav: boolean = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true
async function toggleNav (): Promise<void> {
visibleNav = !visibleNav
closeTooltip()
if (currentApplication && navigatorModel && navigator) {
await tick()
panelInstance.fitPopupInstance()
popupInstance.fitPopupInstance()
}
}
onMount(() => {
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
$workspacesStore = await getWorkspaceFn()
await updateWindowTitle(getLocation())
})
})
const account = getCurrentAccount() as PersonAccount
let employee: Employee | undefined
const employeeQ = createQuery()
$: account &&
employeeQ.query<Employee>(
contact.mixin.Employee,
{
_id: account?.person as Ref<Employee>
},
(res) => {
employee = res[0]
},
{ limit: 1 }
)
const workspaceId = $location.path[1]
const inboxClient = InboxNotificationsClientImpl.getClient()
const inboxNotificationsByContextStore = inboxClient.inboxNotificationsByContext
let hasNotificationsFn: ((data: Map<Ref<DocNotifyContext>, InboxNotification[]>) => Promise<boolean>) | undefined =
undefined
let hasInboxNotifications = false
let syncPromise: Promise<void> | undefined = undefined
let locUpdate = 0
getResource(notification.function.HasInboxNotifications).then((f) => {
hasNotificationsFn = f
})
$: hasNotificationsFn?.($inboxNotificationsByContextStore).then((res) => {
hasInboxNotifications = res
})
const doSyncLoc = async (loc: Location, iteration: number): Promise<void> => {
if (workspaceId !== $location.path[1]) {
// Switch of workspace
return
}
closeTooltip()
closePopup()
await syncLoc(loc, iteration)
await updateWindowTitle(loc)
checkOnHide()
syncPromise = undefined
}
onDestroy(
location.subscribe((loc) => {
locUpdate++
if (syncPromise !== undefined) {
void syncPromise.then(() => doSyncLoc(loc, locUpdate))
} else {
syncPromise = doSyncLoc(loc, locUpdate)
}
})
)
let windowWorkspaceName = ''
async function updateWindowTitle (loc: Location): Promise<void> {
let ws = loc.path[1]
const wsName = $workspacesStore.find((it) => it.workspace === ws)
if (wsName !== undefined) {
ws = wsName?.workspaceName ?? wsName.workspace
windowWorkspaceName = ws
}
const docTitle = await getWindowTitle(loc)
if (docTitle !== undefined && docTitle !== '') {
document.title = ws == null ? docTitle : `${docTitle} - ${ws}`
} else {
const title = getMetadata(workbench.metadata.PlatformTitle) ?? 'Platform'
document.title = ws == null ? title : `${ws} - ${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 {
return await titleProvider(client, _id as Ref<Doc>)
} catch (err: any) {
console.error(err)
}
}
async function resolveShortLink (loc: Location): Promise<ResolvedLocation | undefined> {
let locationResolver = currentApplication?.locationResolver
if (loc.path[2] !== undefined && loc.path[2].trim().length > 0) {
if (apps instanceof Promise) {
apps = await apps
}
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[4] = asideId as string
}
} 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
loc.path[4] = resolvedSpecial ?? currentSpecial ?? 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 = resolved.loc.fragment ?? loc.fragment ?? resolved.defaultLocation.fragment
return loc
}
async function syncLoc (loc: Location, iteration: number): Promise<void> {
const originalLoc = JSON.stringify(loc)
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
// resolve short links
const resolvedLoc = await resolveShortLink(loc)
if (locUpdate !== iteration) {
return
}
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)
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 (locUpdate !== iteration) {
return
}
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)
currentAppAlias = app
currentApplication = await client.findOne<Application>(workbench.class.Application, { alias: app })
if (locUpdate !== iteration) {
return
}
navigatorModel = await buildNavModel(client, currentApplication)
if (locUpdate !== iteration) {
return
}
}
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)
if (locUpdate !== iteration) {
return
}
setSpaceSpecial(special)
}
}
if (special !== currentSpecial && (navigatorModel?.aside || currentApplication?.aside)) {
asideId = special
}
if (app !== undefined) {
localStorage.setItem(`${locationStorageKeyId}_${app}`, originalLoc)
}
currentQuery = loc.query
if (fragment !== currentFragment) {
currentFragment = fragment
if (fragment !== undefined && 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 doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> })
if (doc !== undefined) {
const provider = ListSelectionProvider.Find(doc._id)
updateFocus({
provider,
focus: doc
})
openPanel(
props[0] as AnyComponent,
props[1],
props[2],
(props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent
)
} else {
closePanel(false)
}
} else {
closePanel(false)
}
}
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:
asideId = undefined
if (currentSpace !== undefined) {
specialComponent = undefined
}
}
}
function closeAside (): void {
const loc = getLocation()
loc.path.length = 4
asideId = undefined
checkOnHide()
navigate(loc)
}
async function updateSpace (spaceId?: Ref<Space>): Promise<void> {
if (spaceId === currentSpace) return
clear(2)
if (spaceId === undefined) return
const space = await client.findOne<Space>(core.class.Space, { _id: spaceId })
if (space === undefined) return
currentSpace = spaceId
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
if (asideId !== undefined && spaceSpecial === asideId) return
clear(3)
if (spaceSpecial === undefined) return
specialComponent = getSpecialComponent(spaceSpecial)
if (specialComponent !== undefined) {
currentSpecial = spaceSpecial
} else if (navigatorModel?.aside !== undefined || currentApplication?.aside !== undefined) {
asideId = spaceSpecial
}
}
function getSpecialComponent (id: string): SpecialNavModel | undefined {
const sp = navigatorModel?.specials?.find((x) => x.id === id)
if (sp !== undefined) {
return sp
}
for (const s of navigatorModel?.spaces ?? []) {
const sp = s.specials?.find((x) => x.id === id)
if (sp !== undefined) {
return sp
}
}
}
let aside: HTMLElement
let cover: HTMLElement
let navFloat: boolean = !($deviceInfo.docWidth < 1024)
$: if ($deviceInfo.docWidth <= 1024 && !navFloat) {
visibleNav = false
navFloat = true
} else if ($deviceInfo.docWidth > 1024 && navFloat) {
if (getMetadata(workbench.metadata.NavigationExpandedDefault) === undefined) {
navFloat = false
visibleNav = true
}
}
const checkOnHide = (): void => {
if (visibleNav && $deviceInfo.docWidth <= 1024) visibleNav = false
}
let appsDirection: 'vertical' | 'horizontal'
$: appsDirection = $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 =
appsDirection === 'horizontal'
? 'account-portrait'
: appsDirection === 'vertical' && $deviceInfo.isMobile
? 'account-mobile'
: 'account'
let popupSpacePosition: PopupPosAlignment
$: popupSpacePosition = appsMini ? 'logo-mini' : appsDirection === 'horizontal' ? 'logo-portrait' : 'logo'
onMount(() => {
subscribeMobile(setTheme)
})
async function checkIsHeaderHidden (currentApplication: Application | undefined) {
return (
currentApplication?.checkIsHeaderHidden && (await (await getResource(currentApplication.checkIsHeaderHidden))())
)
}
async function checkIsHeaderDisabled (currentApplication: Application | undefined) {
return (
currentApplication?.checkIsHeaderDisabled &&
(await (
await getResource(currentApplication.checkIsHeaderDisabled)
)())
)
}
function getApps (apps: Application[] | Promise<Application[]>): Application[] {
if (apps instanceof Promise) {
return []
} else {
return apps
}
}
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)
</script>
{#if employee && !employee.active}
<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={() => {
signOut()
}}
/>
</div>
{:else if employee?.active || account.role === AccountRole.Owner}
<ActionHandler />
<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>
<clipPath id="nub-bg">
<path
d="M7.3.6 4.2 4.3C2.9 5.4 1.5 6 0 6v1h18V6c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2z"
/>
</clipPath>
<clipPath id="nub-border">
<path
d="M4.8 5.1 8 1.3s.1 0 .1-.1c.5-.3 1.4-.3 1.9.1L13.1 5l.1.1 1.2.9H18c-1.5 0-2.9-.6-4.2-1.7L10.7.6C9.9-.1 8.5-.2 7.5.4c0 .1-.1.1-.2.2L4.2 4.3C2.9 5.4 1.5 6 0 6h3.6l1.2-.9z"
/>
</clipPath>
</svg>
<div
class="workbench-container"
class:modern-app={['setting', 'time'].some((app) => currentApplication?.alias === app)}
style:flex-direction={appsDirection === 'horizontal' ? 'column-reverse' : 'row'}
>
<div class="antiPanel-application {appsDirection}" class:lastDivider={!visibleNav}>
<div
class="hamburger-container clear-mins"
class:portrait={appsDirection === 'horizontal'}
class:landscape={appsDirection === '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={visibleNav ? workbench.string.HideMenu : workbench.string.ShowMenu}
selected={!visibleNav}
size={appsMini ? 'small' : 'medium'}
on:click={toggleNav}
/>
</div>
<!-- <ActivityStatus status="active" /> -->
<NavLink app={inboxId} shrink={0}>
<AppItem
icon={notification.icon.Notifications}
label={notification.string.Inbox}
selected={currentAppAlias === inboxId || inboxPopup !== undefined}
on:click={(e) => {
if (e.metaKey || e.ctrlKey) return
if (currentAppAlias === inboxId && lastLoc !== undefined) {
e.preventDefault()
e.stopPropagation()
navigate(lastLoc)
lastLoc = undefined
} else {
lastLoc = $location
}
}}
notify={hasInboxNotifications}
/>
</NavLink>
<Applications apps={getApps(apps)} active={currentApplication?._id} direction={appsDirection} />
</div>
<div class="info-box {appsDirection}" class:vertical-mobile={appsDirection === 'vertical'} class:mini={appsMini}>
<AppItem
icon={IconSettings}
label={setting.string.Settings}
size={appsMini ? 'small' : 'large'}
on:click={() => showPopup(AppSwitcher, { apps: getApps(apps) }, popupPosition)}
/>
<a href={supportLink}>
<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={appsDirection === 'vertical'} class:ml-2={appsDirection === '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={{ avatar: employee?.avatar, size: 'small' }} />
</div>
</div>
</div>
</div>
<ActionContext
context={{
mode: 'workbench',
application: currentApplication?._id
}}
/>
<div class="workbench-container inner">
{#if currentApplication && navigatorModel && navigator && visibleNav}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if navFloat}<div class="cover shown" on:click={() => (visibleNav = false)} />{/if}
<div class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content">
{#if currentApplication}
<NavHeader label={currentApplication.label} />
{#if currentApplication.navHeaderComponent}
{#await checkIsHeaderHidden(currentApplication) then isHidden}
{#if !isHidden}
{#await checkIsHeaderDisabled(currentApplication) then disabled}
<Component
is={currentApplication.navHeaderComponent}
props={{
currentSpace,
currentSpecial,
currentFragment,
disabled
}}
shrink
/>
{/await}
{/if}
{/await}
{/if}
{/if}
<Navigator
{currentSpace}
{currentSpecial}
{currentFragment}
model={navigatorModel}
{currentApplication}
on:open={checkOnHide}
/>
<NavFooter>
{#if currentApplication.navFooterComponent}
<Component is={currentApplication.navFooterComponent} props={{ currentSpace }} />
{/if}
</NavFooter>
</div>
<Separator
name={'workbench'}
float={navFloat ? 'navigator' : true}
index={0}
color={'var(--theme-navpanel-border)'}
/>
</div>
<Separator name={'workbench'} float={navFloat} index={0} color={'var(--theme-navpanel-border)'} />
{/if}
<div class="antiPanel-component antiComponent" bind:this={contentPanel}>
{#if currentApplication && currentApplication.component}
<Component is={currentApplication.component} props={{ currentSpace, visibleNav, navFloat, appsDirection }} />
{:else if specialComponent}
<Component
is={specialComponent.component}
props={{
model: navigatorModel,
...specialComponent.componentProps,
currentSpace,
visibleNav,
navFloat,
appsDirection
}}
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, currentView, visibleNav, navFloat, appsDirection }}
/>
{:else}
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}
</div>
{#if asideId}
{@const asideComponent = navigatorModel?.aside ?? currentApplication?.aside}
{#if asideComponent !== undefined}
<Separator name={'workbench'} index={1} />
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{/if}
{/if}
</div>
</div>
<div bind:this={cover} class="cover" />
<TooltipInstance />
<PanelInstance bind:this={panelInstance} {contentPanel}>
<svelte:fragment slot="panel-header">
<ActionContext context={{ mode: 'panel' }} />
</svelte:fragment>
</PanelInstance>
<Popup bind:this={popupInstance} {contentPanel}>
<svelte:fragment slot="popup-header">
<ActionContext context={{ mode: 'popup' }} />
</svelte:fragment>
</Popup>
<BrowserNotificatator />
{/if}
<style lang="scss">
.workbench-container {
display: flex;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
touch-action: none;
&:not(.modern-app, .inner) {
border-top: 1px solid var(--theme-navpanel-divider);
border-left: 1px solid var(--theme-navpanel-color);
}
&.modern-app {
border-top: 1px solid transparent;
}
&.modern-app {
position: relative;
background-color: var(--theme-statusbar-color);
border-radius: var(--medium-BorderRadius);
&::after {
position: absolute;
content: '';
inset: 0;
border: 1px solid var(--theme-divider-color);
border-radius: var(--medium-BorderRadius);
pointer-events: none;
}
.antiPanel-application {
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.5rem;
height: 1.5rem;
}
.topmenu-container.mini {
left: calc(1.5rem + 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;
}
}
}
.cover {
position: fixed;
display: none;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
&.shown {
display: block;
}
}
.splitter {
position: relative;
width: 1px;
min-width: 1px;
max-width: 1px;
height: 100%;
background-color: var(--theme-divider-color);
transition: background-color 0.15s ease-in-out;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0.5rem;
height: 100%;
border-left: 2px solid transparent;
cursor: col-resize;
z-index: 1;
transition: border-color 0.15s ease-in-out;
}
&:hover,
&.hovered {
transition-duration: 0;
background-color: var(--primary-bg-color);
&::before {
transition-duration: 0;
border-left: 2px solid var(--primary-bg-color);
}
}
}
</style>