UBER-338: added AppSwitcher popup. (#3329)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-06-02 10:05:43 +03:00 committed by GitHub
parent 876d52c08d
commit d7400dc03a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 105 deletions

View File

@ -648,6 +648,7 @@ input.search {
.min-w-4 { min-width: 1rem; } .min-w-4 { min-width: 1rem; }
.min-w-8 { min-width: 2rem; } .min-w-8 { min-width: 2rem; }
.min-w-9 { min-width: 2.25rem; } .min-w-9 { min-width: 2.25rem; }
.min-w-60 { min-width: 15rem; }
.min-w-80 { min-width: 20rem; } .min-w-80 { min-width: 20rem; }
.min-w-100 { min-width: 25rem; } .min-w-100 { min-width: 25rem; }
.min-w-168 { min-width: 42rem; } .min-w-168 { min-width: 42rem; }

View File

@ -15,49 +15,19 @@
<script lang="ts"> <script lang="ts">
import type { IntlString, Asset } from '@hcengineering/platform' import type { IntlString, Asset } from '@hcengineering/platform'
import type { AnySvelteComponent } from '@hcengineering/ui' import type { AnySvelteComponent } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Icon, tooltip } from '@hcengineering/ui' import { Icon, tooltip } from '@hcengineering/ui'
import PreviewOn from './icons/PreviewOn.svelte'
import PreviewOff from './icons/PreviewOff.svelte'
export let label: IntlString export let label: IntlString
export let icon: Asset | AnySvelteComponent export let icon: Asset | AnySvelteComponent
export let selected: boolean export let selected: boolean
export let notify: boolean = false export let notify: boolean = false
export let hidden: boolean = false
export let editable: 'vertical' | 'horizontal' | false = false
const dispatch = createEventDispatcher()
</script> </script>
<button <button class="app" class:selected id={'app-' + label} use:tooltip={{ label }}>
class="app{editable ? ' ' + editable : ''}"
class:selected
class:hidden
id={'app-' + label}
use:tooltip={{ label }}
>
<div class="flex-center icon-container" class:noty={notify}> <div class="flex-center icon-container" class:noty={notify}>
<Icon {icon} size={'medium'} /> <Icon {icon} size={'medium'} />
</div> </div>
{#if notify}<div class="marker" />{/if} {#if notify}<div class="marker" />{/if}
{#if editable}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="starButton"
class:hidden
on:click|preventDefault|stopPropagation={() => {
hidden = !hidden
dispatch('visible', !hidden)
}}
>
{#if hidden}
<PreviewOff size={'small'} />
{:else}
<PreviewOn size={'small'} />
{/if}
</div>
{/if}
</button> </button>
<style lang="scss"> <style lang="scss">
@ -101,16 +71,6 @@
color: var(--theme-caption-color); color: var(--theme-caption-color);
} }
} }
&.hidden {
border: 1px dashed var(--theme-dark-color);
.icon-container {
color: var(--theme-dark-color);
}
&:hover .icon-container {
color: var(--theme-content-color);
}
}
} }
.marker { .marker {
@ -122,34 +82,4 @@
border-radius: 50%; border-radius: 50%;
background-color: var(--highlight-red); background-color: var(--highlight-red);
} }
.starButton {
position: absolute;
right: 0.25rem;
bottom: 0.25rem;
height: 1rem;
width: 1rem;
color: var(--activity-status-busy);
transform-origin: center center;
transform: scale(1);
opacity: 0.8;
z-index: 10000;
filter: drop-shadow(0 0 1px #000);
cursor: pointer;
&:hover {
transform: scale(1.2);
opacity: 1;
}
&.hidden {
color: var(--theme-warning-color);
transform: scale(0.7);
opacity: 0.5;
&:hover {
transform: scale(0.9);
opacity: 0.8;
}
}
}
</style> </style>

View File

@ -0,0 +1,99 @@
<!--
// Copyright © 2022 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 type { Ref } from '@hcengineering/core'
import type { Application } from '@hcengineering/workbench'
import { createQuery } from '@hcengineering/presentation'
import workbench from '@hcengineering/workbench'
import { hideApplication, showApplication } from '../utils'
import { Loading, IconCheck, Label, Icon } from '@hcengineering/ui'
// import Drag from './icons/Drag.svelte'
export let apps: Application[] = []
let activeElement: HTMLElement
const btns: HTMLElement[] = []
function focusTarget (target: HTMLElement): void {
activeElement = target
}
const keyDown = (ev: KeyboardEvent): void => {
if (ev.key === 'Tab') {
ev.preventDefault()
ev.stopPropagation()
}
const n = btns.indexOf(activeElement) ?? 0
if (ev.key === 'ArrowDown') {
if (n < btns.length - 1) {
activeElement = btns[n + 1]
}
ev.preventDefault()
ev.stopPropagation()
}
if (ev.key === 'ArrowUp') {
if (n > 0) {
activeElement = btns[n - 1]
}
ev.preventDefault()
ev.stopPropagation()
}
}
let loaded: boolean = false
let hiddenAppsIds: Ref<Application>[] = []
const hiddenAppsIdsQuery = createQuery()
hiddenAppsIdsQuery.query(workbench.class.HiddenApplication, {}, (res) => {
hiddenAppsIds = res.map((r) => r.attachedTo)
loaded = true
})
</script>
<div class="antiPopup min-w-60" on:keydown={keyDown}>
<div class="ap-space x2" />
<div class="ap-scroll">
<div class="ap-box">
{#if loaded}
{#each apps as app, i}
<button
bind:this={btns[i]}
class="ap-menuItem withIcon flex-row-center flex-grow"
class:hover={btns[i] === activeElement}
on:click={() => {
if (hiddenAppsIds.includes(app._id)) showApplication(app)
else hideApplication(app)
}}
on:mousemove={() => {
focusTarget(btns[i])
}}
>
<div class="icon mr-2"><Icon icon={app.icon} size={'small'} /></div>
<span class="label overflow-label flex-grow"><Label label={app.label} /></span>
<div class="ap-check">
{#if !hiddenAppsIds.includes(app._id)}
<IconCheck size={'small'} />
{/if}
</div>
</button>
{/each}
{:else}
<div class="ap-menuItem empty">
<Loading />
</div>
{/if}
</div>
</div>
<div class="ap-space x2" />
</div>

View File

@ -14,24 +14,24 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Ref } from '@hcengineering/core' import type { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Scroller } from '@hcengineering/ui' import { Scroller } from '@hcengineering/ui'
import { NavLink } from '@hcengineering/view-resources' import { NavLink } from '@hcengineering/view-resources'
import type { Application } from '@hcengineering/workbench' import type { Application } from '@hcengineering/workbench'
import { hideApplication, showApplication } from '../utils' import workbench from '@hcengineering/workbench'
import App from './App.svelte' import App from './App.svelte'
export let active: Ref<Application> | undefined export let active: Ref<Application> | undefined
export let apps: Application[] = [] export let apps: Application[] = []
export let direction: 'vertical' | 'horizontal' = 'vertical' export let direction: 'vertical' | 'horizontal' = 'vertical'
export let shown: boolean = false
const loaded: boolean = true let loaded: boolean = false
const hiddenAppsIds: Ref<Application>[] = [] let hiddenAppsIds: Ref<Application>[] = []
// const hiddenAppsIdsQuery = createQuery() const hiddenAppsIdsQuery = createQuery()
// hiddenAppsIdsQuery.query(workbench.class.HiddenApplication, {}, (res) => { hiddenAppsIdsQuery.query(workbench.class.HiddenApplication, {}, (res) => {
// hiddenAppsIds = res.map((r) => r.attachedTo) hiddenAppsIds = res.map((r) => r.attachedTo)
// loaded = true loaded = true
// }) })
</script> </script>
<div class="flex-{direction === 'horizontal' ? 'row-center' : 'col-center'} clear-mins apps-{direction} relative"> <div class="flex-{direction === 'horizontal' ? 'row-center' : 'col-center'} clear-mins apps-{direction} relative">
@ -44,20 +44,9 @@
contentDirection={direction} contentDirection={direction}
buttons={'union'} buttons={'union'}
> >
{#each apps.filter((it) => (shown ? true : !hiddenAppsIds.includes(it._id))) as app} {#each apps.filter((it) => !hiddenAppsIds.includes(it._id)) as app}
<NavLink app={app.alias} shrink={0}> <NavLink app={app.alias} shrink={0}>
<App <App selected={app._id === active} icon={app.icon} label={app.label} />
selected={app._id === active}
icon={app.icon}
label={app.label}
hidden={hiddenAppsIds.includes(app._id)}
editable={shown ? direction : false}
on:visible={(res) => {
if (res.detail === undefined) return
if (res.detail) showApplication(app)
else hideApplication(app)
}}
/>
</NavLink> </NavLink>
{/each} {/each}
<div class="apps-space-{direction}" /> <div class="apps-space-{direction}" />

View File

@ -19,6 +19,7 @@
import notification, { notificationId } from '@hcengineering/notification' import notification, { notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, NotificationClientImpl } from '@hcengineering/notification-resources' import { BrowserNotificatator, NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, getMetadata, getResource } from '@hcengineering/platform' import { IntlString, getMetadata, getResource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation' import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { import {
AnyComponent, AnyComponent,
@ -71,9 +72,10 @@
import SelectWorkspaceMenu from './SelectWorkspaceMenu.svelte' import SelectWorkspaceMenu from './SelectWorkspaceMenu.svelte'
import SpaceView from './SpaceView.svelte' import SpaceView from './SpaceView.svelte'
import TopMenu from './icons/TopMenu.svelte' import TopMenu from './icons/TopMenu.svelte'
import IconSettings from './icons/Settings.svelte'
import AppSwitcher from './AppSwitcher.svelte'
let contentPanel: HTMLElement let contentPanel: HTMLElement
let shownMenu: boolean = false
const { setTheme } = getContext('theme') as any const { setTheme } = getContext('theme') as any
@ -604,21 +606,15 @@
/> />
</NavLink> </NavLink>
<div class="divider" /> <div class="divider" />
<Applications <Applications apps={getApps(apps)} active={currentApplication?._id} direction={appsDirection} />
apps={getApps(apps)}
active={currentApplication?._id}
direction={appsDirection}
bind:shown={shownMenu}
/>
</div> </div>
<div class="info-box {appsDirection}" class:vertical-mobile={appsDirection === 'vertical'} class:mini={appsMini}> <div class="info-box {appsDirection}" class:vertical-mobile={appsDirection === 'vertical'} class:mini={appsMini}>
<!-- <AppItem <AppItem
icon={Settings} icon={IconSettings}
label={setting.string.Settings} label={setting.string.Settings}
selected={shownMenu}
size={appsMini ? 'small' : 'large'} size={appsMini ? 'small' : 'large'}
on:click={() => (shownMenu = !shownMenu)} on:click={() => showPopup(AppSwitcher, { apps: getApps(apps) }, popupPosition)}
/> --> />
<div class="flex-center" class:mt-3={appsDirection === 'vertical'} class:ml-2={appsDirection === 'horizontal'}> <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-click-events-have-key-events -->
<div <div