Add widgets sidebar (#6578)

This commit is contained in:
Kristina
2024-09-18 09:27:13 +07:00
committed by GitHub
parent d38df9c600
commit a1cee24473
84 changed files with 2922 additions and 600 deletions
@@ -65,7 +65,8 @@
rootBarExtensions,
setResolvedLocation,
showPopup,
workbenchSeparators
workbenchSeparators,
mainSeparators
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import {
@@ -93,6 +94,8 @@
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'
let contentPanel: HTMLElement
@@ -150,6 +153,7 @@
$workspacesStore = await getWorkspaceFn()
await updateWindowTitle(getLocation())
})
syncSidebarState()
})
const account = getCurrentAccount() as PersonAccount
@@ -607,6 +611,7 @@
let lastLoc: Location | undefined = undefined
defineSeparators('workbench', workbenchSeparators)
defineSeparators('main', mainSeparators)
$: mainNavigator = currentApplication && navigatorModel && navigator && $deviceInfo.navigator.visible
$: elementPanel = $deviceInfo.replacedPanel ?? contentPanel
@@ -615,6 +620,14 @@
person && client.getHierarchy().hasMixin(person, contact.mixin.Employee)
? !client.getHierarchy().as(person, contact.mixin.Employee).active
: false
let asideComponent: AnyComponent | undefined
$: if (asideId !== undefined && navigatorModel !== undefined) {
asideComponent = navigatorModel?.aside ?? currentApplication?.aside
} else {
asideComponent = undefined
}
</script>
{#if person && deactivated && !isAdminUser()}
@@ -852,17 +865,18 @@
<SpaceView {currentSpace} {currentView} {createItemDialog} {createItemLabel} />
{/if}
</div>
{#if asideId && navigatorModel !== undefined}
{@const asideComponent = navigatorModel?.aside ?? currentApplication?.aside}
{#if asideComponent !== undefined}
<Separator name={'workbench'} index={1} color={'transparent'} separatorSize={0} short />
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{/if}
{#if asideComponent !== undefined}
<Separator name={'workbench'} index={1} color={'transparent'} separatorSize={0} short />
<div class="antiPanel-component antiComponent aside" bind:this={aside}>
<Component is={asideComponent} props={{ currentSpace, _id: asideId }} on:close={closeAside} />
</div>
{/if}
</div>
</div>
{#if $sidebarStore.variant === SidebarVariant.EXPANDED}
<Separator name={'main'} index={0} color={'transparent'} separatorSize={0} short />
{/if}
<WidgetsBar />
<Dock />
<div bind:this={cover} class="cover" />
<TooltipInstance />
@@ -0,0 +1,70 @@
<!--
// 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 { createQuery, getClient } from '@hcengineering/presentation'
import { WidgetPreference } from '@hcengineering/workbench'
import workbench from '../../plugin'
import { sidebarStore, SidebarVariant } from '../../sidebar'
import WidgetsBarMini from './SidebarMini.svelte'
import WidgetsBarExpanded from './SidebarExpanded.svelte'
const client = getClient()
const widgets = client.getModel().findAllSync(workbench.class.Widget, {})
const preferencesQuery = createQuery()
let preferences: WidgetPreference[] = []
$: preferencesQuery.query(workbench.class.WidgetPreference, {}, (res) => {
preferences = res
})
$: widgetId = $sidebarStore.widget
$: widget = widgets.find((it) => it._id === widgetId)
$: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : widget?.size
</script>
<div class="antiPanel-component antiComponent root size-{size}" id="sidebar">
{#if $sidebarStore.variant === SidebarVariant.MINI}
<WidgetsBarMini {widgets} {preferences} />
{:else if $sidebarStore.variant === SidebarVariant.EXPANDED}
<WidgetsBarExpanded {widgets} {preferences} />
{/if}
</div>
<style lang="scss">
.root {
position: relative;
background-color: var(--theme-panel-color);
&.size-mini {
width: 3.5rem !important;
min-width: 3.5rem !important;
max-width: 3.5rem !important;
}
&.size-small {
width: 10rem !important;
min-width: 10rem !important;
max-width: 10rem !important;
}
&.size-medium {
width: 25rem !important;
min-width: 25rem !important;
max-width: 25rem !important;
}
}
</style>
@@ -0,0 +1,172 @@
<!--
// 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 { Widget, WidgetPreference, WidgetTab } from '@hcengineering/workbench'
import { Ref } from '@hcengineering/core'
import {
Component,
resizeObserver,
location as locationStore,
Location,
Header,
Breadcrumbs
} from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { closeWidgetTab, sidebarStore, SidebarVariant, WidgetState, openWidgetTab, closeWidget } from '../../sidebar'
import WidgetsBar from './widgets/WidgetsBar.svelte'
import SidebarTabs from './SidebarTabs.svelte'
export let widgets: Widget[] = []
export let preferences: WidgetPreference[] = []
let widgetId: Ref<Widget> | undefined = undefined
let widget: Widget | undefined = undefined
let widgetState: WidgetState | undefined = undefined
let tabId: string | undefined = undefined
let tab: WidgetTab | undefined = undefined
let tabs: WidgetTab[] = []
$: widgetId = $sidebarStore.widget
$: widget = widgetId !== undefined ? widgets.find((it) => it._id === widgetId) : undefined
$: widgetState = widget !== undefined ? $sidebarStore.widgetsState.get(widget._id) : undefined
$: tabId = widgetState?.tab
$: tabs = widgetState?.tabs ?? []
$: tab = tabId !== undefined ? tabs.find((it) => it.id === tabId) ?? tabs[0] : tabs[0]
$: if ($sidebarStore.widget === undefined) {
sidebarStore.update((s) => ({ ...s, variant: SidebarVariant.MINI }))
}
const unsubscribe = locationStore.subscribe((loc: Location) => {
if (widget === undefined) return
for (const tab of tabs) {
if (tab.allowedPath !== undefined && !tab.isPinned) {
const path = loc.path.join('/')
if (!path.startsWith(tab.allowedPath)) {
void handleTabClose(tab.id, widget)
}
}
}
})
onDestroy(() => {
unsubscribe()
})
async function handleTabClose (tabId: string, widget?: Widget): Promise<void> {
if (widget === undefined) return
await closeWidgetTab(widget, tabId)
}
function handleTabOpen (tabId: string, widget?: Widget): void {
if (widget === undefined) return
openWidgetTab(widget._id, tabId)
}
let componentHeight = '0px'
let componentWidth = '0px'
function resize (element: Element): void {
componentHeight = `${element.clientHeight}px`
componentWidth = `${element.clientWidth}px`
}
</script>
<div class="root">
<div class="content">
{#if widget?.component}
<div class="component" use:resizeObserver={resize}>
{#if widget.headerLabel}
<Header
allowFullsize={false}
type="type-aside"
hideBefore={true}
hideActions={false}
hideDescription={true}
adaptive="disabled"
on:close={() => {
if (widget !== undefined) {
closeWidget(widget._id)
}
}}
>
<Breadcrumbs items={[{ label: widget.headerLabel }]} currentOnly />
</Header>
{/if}
<Component
is={widget?.component}
props={{ tab, widgetState, height: componentHeight, width: componentWidth, widget }}
on:close={() => {
if (widget !== undefined) {
closeWidget(widget._id)
}
}}
/>
</div>
{/if}
</div>
{#if widget !== undefined && tabs.length > 0}
<SidebarTabs
{tabs}
selected={tab?.id}
{widget}
on:close={(e) => {
void handleTabClose(e.detail, widget)
}}
on:open={(e) => {
handleTabOpen(e.detail, widget)
}}
/>
{/if}
<WidgetsBar {widgets} {preferences} selected={widgetId} />
</div>
<style lang="scss">
.root {
display: flex;
flex: 1;
height: 100%;
overflow: hidden;
}
.content {
display: flex;
flex-direction: column;
border-top: 1px solid var(--theme-divider-color);
overflow: auto;
flex: 1;
width: calc(100% - 3.5rem);
height: 100%;
background: var(--theme-panel-color);
border-right: 1px solid var(--global-ui-BorderColor);
border-top-left-radius: var(--small-focus-BorderRadius);
border-bottom-left-radius: var(--small-focus-BorderRadius);
}
.component {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
</style>
@@ -0,0 +1,26 @@
<!--
// 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 { Widget, WidgetPreference } from '@hcengineering/workbench'
import { Ref } from '@hcengineering/core'
import WidgetsBar from './widgets/WidgetsBar.svelte'
export let widgets: Widget[] = []
export let preferences: WidgetPreference[] = []
export let selected: Ref<Widget> | undefined = undefined
</script>
<WidgetsBar {widgets} {preferences} {selected} />
@@ -0,0 +1,57 @@
<!--
// 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 { Action, Menu, ModernTab, showPopup } from '@hcengineering/ui'
import { Widget, WidgetTab } from '@hcengineering/workbench'
import { getResource } from '@hcengineering/platform'
export let tab: WidgetTab
export let widget: Widget
export let selected = false
export let actions: Action[] = []
$: icon = tab.icon ?? widget.icon
$: if (tab.iconComponent) {
void getResource(tab.iconComponent).then((res) => {
icon = res
})
}
function handleMenu (event: MouseEvent): void {
if (actions.length === 0) {
return
}
event.preventDefault()
event.stopPropagation()
showPopup(Menu, { actions }, event.target as HTMLElement)
}
</script>
<ModernTab
label={tab.name}
labelIntl={tab.nameIntl ?? widget.label}
highlighted={selected}
orientation="vertical"
kind={tab.isPinned ? 'secondary' : 'primary'}
{icon}
iconProps={tab.iconProps}
canClose={!tab.isPinned}
maxSize="13.5rem"
on:close
on:click
on:contextmenu={handleMenu}
/>
@@ -0,0 +1,99 @@
<!--
// 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 { Widget, WidgetTab } from '@hcengineering/workbench'
import { createEventDispatcher } from 'svelte'
import presentation from '@hcengineering/presentation'
import { Action, Component } from '@hcengineering/ui'
import view from '@hcengineering/view'
import SidebarTab from './SidebarTab.svelte'
import { closeWidgetTab, pinWidgetTab, unpinWidgetTab } from '../../sidebar'
export let tabs: WidgetTab[] = []
export let widget: Widget
export let selected: string | undefined = undefined
const dispatch = createEventDispatcher()
function getActions (tab: WidgetTab): Action[] {
const pinAction: Action = {
label: view.string.Pin,
icon: view.icon.Pin,
action: async () => {
pinWidgetTab(widget, tab.id)
}
}
const unpinAction: Action = {
label: view.string.Unpin,
icon: view.icon.Pin,
action: async () => {
unpinWidgetTab(widget, tab.id)
}
}
const closeAction: Action = {
label: presentation.string.Close,
icon: view.icon.Delete,
action: async () => {
void closeWidgetTab(widget, tab.id)
}
}
return [tab.isPinned ? unpinAction : pinAction, closeAction]
}
</script>
<div class="tabs">
{#each tabs as tab}
{#if widget.tabComponent}
<Component
is={widget.tabComponent}
props={{ tab, widget, selected: tab.id === selected, actions: getActions(tab) }}
on:close={() => dispatch('close', tab.id)}
on:click={() => {
dispatch('open', tab.id)
}}
/>
{:else}
<SidebarTab
{tab}
{widget}
actions={getActions(tab)}
selected={tab.id === selected}
on:close={() => dispatch('close', tab.id)}
on:click={() => {
dispatch('open', tab.id)
}}
/>
{/if}
{/each}
</div>
<style lang="scss">
.tabs {
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
width: 30px;
min-width: 30px;
max-width: 30px;
flex: 1;
border-right: 1px solid var(--theme-divider-color);
border-top: 1px solid var(--theme-divider-color);
gap: 0.25rem;
align-items: center;
padding: 0.25rem 0;
}
</style>
@@ -0,0 +1,104 @@
<!--
// 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 { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench'
import { CheckBox, Grid, Modal } from '@hcengineering/ui'
import core, { Ref } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
import WidgetPresenter from './WidgetPresenter.svelte'
import workbench from '../../../plugin'
export let widgets: Widget[] = []
const dispatch = createEventDispatcher()
const client = getClient()
const preferencesQuery = createQuery()
const widgetsUpdates = new Map<Ref<Widget>, boolean>()
let preferences: WidgetPreference[] = []
$: preferencesQuery.query(workbench.class.WidgetPreference, {}, (res) => {
preferences = res
})
function handleClose (): void {
dispatch('close')
}
function isEnabled (
widget: Widget,
widgetsUpdates: Map<Ref<Widget>, boolean>,
preference?: WidgetPreference
): boolean {
return widgetsUpdates.get(widget._id) ?? preference?.enabled ?? false
}
function handleCheck (widget: Widget, preference?: WidgetPreference): void {
const newValue = !isEnabled(widget, widgetsUpdates, preference)
widgetsUpdates.set(widget._id, newValue)
}
async function handleApply (): Promise<void> {
for (const [widget, enabled] of widgetsUpdates) {
const preference = preferences.find((it) => it.attachedTo === widget)
if (preference !== undefined) {
await client.diffUpdate(preference, { enabled })
} else {
await client.createDoc(workbench.class.WidgetPreference, core.space.Workspace, { attachedTo: widget, enabled })
}
}
dispatch('close')
}
</script>
<Modal
label={workbench.string.ConfigureWidgets}
type="type-popup"
okLabel={presentation.string.Save}
okAction={handleApply}
canSave={true}
onCancel={handleClose}
on:close
>
<Grid column={1} rowGap={0.5}>
{#each widgets as widget}
{#if widget.type === WidgetType.Configurable}
{@const preference = preferences.find((it) => it.attachedTo === widget._id)}
<div class="item">
<CheckBox
size="small"
checked={isEnabled(widget, widgetsUpdates, preference)}
on:value={() => {
handleCheck(widget, preference)
}}
/>
<WidgetPresenter {widget} withLabel />
</div>
{/if}
{/each}
</Grid>
</Modal>
<style lang="scss">
.item {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>
@@ -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 { Widget } from '@hcengineering/workbench'
import { Label } from '@hcengineering/ui'
import AppItem from '../../AppItem.svelte'
export let widget: Widget
export let withLabel = false
export let highlighted = false
</script>
<div class="root">
<AppItem label={widget.label} icon={widget.icon} selected={highlighted} size="small" on:click />
{#if withLabel}
<Label label={widget.label} />
{/if}
</div>
<style lang="scss">
.root {
display: flex;
align-items: center;
}
</style>
@@ -0,0 +1,120 @@
<!--
// 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 { Widget, WidgetPreference, WidgetType } from '@hcengineering/workbench'
import { IconSettings, ModernButton, showPopup } from '@hcengineering/ui'
import { Ref } from '@hcengineering/core'
import WidgetPresenter from './/WidgetPresenter.svelte'
import AddWidgetsPopup from './AddWidgetsPopup.svelte'
import { openWidget, sidebarStore, SidebarVariant } from '../../../sidebar'
export let widgets: Widget[] = []
export let preferences: WidgetPreference[] = []
export let selected: Ref<Widget> | undefined = undefined
function handleAddWidget (): void {
showPopup(AddWidgetsPopup, { widgets })
}
function handleSelectWidget (widget: Widget): void {
if (selected === widget._id) {
sidebarStore.update((state) => ({ ...state, widget: undefined, variant: SidebarVariant.MINI }))
} else {
openWidget(widget)
}
}
$: fixedWidgets = widgets.filter((widget) => widget.type === WidgetType.Fixed)
$: flexibleWidgets = widgets.filter(
(widget) => widget.type === WidgetType.Flexible && $sidebarStore.widgetsState.has(widget._id)
)
$: configurableWidgets = preferences
.filter((it) => it.enabled)
.sort((a, b) => a.modifiedOn - b.modifiedOn)
.map((it) => widgets.find((widget) => widget._id === it.attachedTo))
.filter((widget): widget is Widget => widget !== undefined && widget.type === WidgetType.Configurable)
</script>
<div class="root">
<div class="block">
{#each fixedWidgets as widget}
<WidgetPresenter
{widget}
highlighted={widget._id === selected}
on:click={() => {
handleSelectWidget(widget)
}}
/>
{/each}
{#if configurableWidgets.length > 0}
<div class="separator" />
{#each configurableWidgets as widget}
<WidgetPresenter
{widget}
highlighted={widget._id === selected}
on:click={() => {
handleSelectWidget(widget)
}}
/>
{/each}
{/if}
{#if flexibleWidgets.length > 0}
<div class="separator" />
{#each flexibleWidgets as widget}
<WidgetPresenter
{widget}
highlighted={widget._id === selected}
on:click={() => {
handleSelectWidget(widget)
}}
/>
{/each}
{/if}
{#if widgets.some((widget) => widget.type === WidgetType.Configurable)}
<div class="separator" />
<ModernButton icon={IconSettings} size="small" on:click={handleAddWidget} />
{/if}
</div>
</div>
<style lang="scss">
.root {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 0.5rem 0;
width: 3.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
border-top: 1px solid var(--theme-divider-color);
overflow-y: auto;
}
.block {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.separator {
width: 2rem;
height: 1px;
background-color: var(--global-ui-BorderColor);
}
</style>
+1
View File
@@ -37,6 +37,7 @@ export { default as TreeSeparator } from './components/navigator/TreeSeparator.s
export { SpecialView }
export * from './utils'
export * from './sidebar'
export default async (): Promise<Resources> => ({
component: {
WorkbenchApp,
+3 -1
View File
@@ -45,7 +45,9 @@ export default mergeIds(workbenchId, workbench, {
MobileNotSupported: '' as IntlString,
LogInAnyway: '' as IntlString,
WorkspaceCreating: '' as IntlString,
AccessDenied: '' as IntlString
AccessDenied: '' as IntlString,
Widget: '' as IntlString,
WidgetPreference: '' as IntlString
},
metadata: {
MobileAllowed: '' as Metadata<boolean>
+294
View File
@@ -0,0 +1,294 @@
//
// 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 { type Widget, type WidgetTab } from '@hcengineering/workbench'
import { getCurrentAccount, type Ref } from '@hcengineering/core'
import { get, writable } from 'svelte/store'
import { getCurrentLocation } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import { workspaceStore } from './utils'
export enum SidebarVariant {
MINI = 'mini',
EXPANDED = 'expanded'
}
export interface WidgetState {
_id: Ref<Widget>
data?: Record<string, any>
tabs: WidgetTab[]
tab?: string
}
export interface SidebarState {
variant: SidebarVariant
widgetsState: Map<Ref<Widget>, WidgetState>
widget?: Ref<Widget>
}
export const defaultSidebarState: SidebarState = {
variant: SidebarVariant.MINI,
widgetsState: new Map()
}
export const sidebarStore = writable<SidebarState>(defaultSidebarState)
workspaceStore.subscribe((workspace) => {
sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? ''))
})
sidebarStore.subscribe(setSidebarStateToLocalStorage)
export function syncSidebarState (): void {
const workspace = get(workspaceStore)
sidebarStore.set(getSidebarStateFromLocalStorage(workspace ?? ''))
}
function getSideBarLocalStorageKey (workspace: string): string | undefined {
const me = getCurrentAccount()
if (me == null || workspace === '') return undefined
return `workbench.${workspace}.${me.person}.sidebar.state.`
}
function getSidebarStateFromLocalStorage (workspace: string): SidebarState {
const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace)
if (sidebarStateLocalStorageKey === undefined) return defaultSidebarState
const state = window.localStorage.getItem(sidebarStateLocalStorageKey)
if (state == null || state === '') return defaultSidebarState
try {
const parsed = JSON.parse(state)
return {
...defaultSidebarState,
...parsed,
widgetsState: new Map(Object.entries(parsed.widgetsState ?? {}))
}
} catch (e) {
console.error(e)
setSidebarStateToLocalStorage(defaultSidebarState)
return defaultSidebarState
}
}
function setSidebarStateToLocalStorage (state: SidebarState): void {
const workspace = get(workspaceStore)
if (workspace == null || workspace === '') return
const sidebarStateLocalStorageKey = getSideBarLocalStorageKey(workspace)
if (sidebarStateLocalStorageKey === undefined) return
window.localStorage.setItem(
sidebarStateLocalStorageKey,
JSON.stringify({ ...state, widgetsState: Object.fromEntries(state.widgetsState.entries()) })
)
}
export function openWidget (widget: Widget, data?: Record<string, any>): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
widgetsState.set(widget._id, { _id: widget._id, data, tab: widgetState?.tab, tabs: widgetState?.tabs ?? [] })
sidebarStore.set({
...state,
widgetsState,
variant: SidebarVariant.EXPANDED,
widget: widget._id
})
}
export function closeWidget (widget: Ref<Widget>): void {
const state = get(sidebarStore)
const { widgetsState } = state
widgetsState.delete(widget)
if (state.widget === widget) {
sidebarStore.set({
...state,
widgetsState,
variant: SidebarVariant.MINI,
widget: undefined
})
} else {
sidebarStore.set({
...state,
widgetsState
})
}
}
export async function closeWidgetTab (widget: Widget, tab: string): Promise<void> {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
if (widgetState === undefined) return
const tabs = widgetState.tabs
const newTabs = tabs.filter((it) => it.id !== tab)
const closedTab = tabs.find((it) => it.id === tab)
if (widget.onTabClose !== undefined && closedTab !== undefined) {
const fn = await getResource(widget.onTabClose)
void fn(closedTab)
}
if (newTabs.length === 0) {
if (widget.closeIfNoTabs === true) {
widgetsState.delete(widget._id)
sidebarStore.set({ ...state, widgetsState, variant: SidebarVariant.MINI })
} else {
widgetsState.set(widget._id, { ...widgetState, tabs: [], tab: undefined })
sidebarStore.set({ ...state, widgetsState })
}
return
}
const shouldReplace = widgetState.tab === tab
if (!shouldReplace) {
widgetsState.set(widget._id, { ...widgetState, tabs: newTabs })
} else {
const index = tabs.findIndex((it) => it.id === widgetState.tab)
const newTab = index === -1 ? newTabs[0] : tabs[index + 1] ?? tabs[index - 1] ?? newTabs[0]
widgetsState.set(widget._id, { ...widgetState, tabs: newTabs, tab: newTab.id })
}
sidebarStore.set({
...state,
...widgetsState
})
}
export function openWidgetTab (widget: Ref<Widget>, tab: string): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget)
if (widgetState === undefined) return
const newTab = widgetState.tabs.find((it) => it.id === tab)
if (newTab === undefined) return
widgetsState.set(widget, { ...widgetState, tab })
sidebarStore.set({
...state,
widgetsState
})
}
export function createWidgetTab (widget: Widget, tab: WidgetTab, newTab = false): void {
openWidget(widget)
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
const currentTabs = widgetState?.tabs ?? []
const opened = currentTabs.some(({ id }) => id === tab.id) ?? false
let newTabs: WidgetTab[]
if (opened) {
newTabs = currentTabs.map((it) => (it.id === tab.id ? { ...tab, isPinned: it.isPinned } : it))
} else if (newTab || currentTabs.length === 0) {
newTabs = [...currentTabs, tab]
} else {
const current =
currentTabs.find(({ id }) => id === widgetState?.tab) ?? currentTabs.find(({ isPinned }) => isPinned === false)
const shouldReplace = current !== undefined && current.isPinned !== true
newTabs = shouldReplace ? currentTabs.map((it) => (it.id === current?.id ? tab : it)) : [...currentTabs, tab]
}
widgetsState.set(widget._id, {
_id: widget._id,
tabs: newTabs,
tab: tab.id
})
sidebarStore.set({
...state,
widgetsState
})
}
export function pinWidgetTab (widget: Widget, tabId: string): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
if (widgetState === undefined) return
const tabs = widgetState.tabs
.map((it) => (it.id === tabId ? { ...it, isPinned: true } : it))
.sort((a, b) => (a.isPinned === b.isPinned ? 0 : a.isPinned === true ? -1 : 1))
widgetsState.set(widget._id, { ...widgetState, tabs })
sidebarStore.set({
...state,
widgetsState
})
}
export function unpinWidgetTab (widget: Widget, tabId: string): void {
const state = get(sidebarStore)
const { widgetsState } = state
const widgetState = widgetsState.get(widget._id)
if (widgetState === undefined) return
const tab = widgetState.tabs.find((it) => it.id === tabId)
if (tab?.allowedPath !== undefined) {
const loc = getCurrentLocation()
const path = loc.path.join('/')
if (!path.startsWith(tab.allowedPath)) {
void closeWidgetTab(widget, tabId)
}
}
const tabs = widgetState.tabs
.map((it) => (it.id === tabId ? { ...it, isPinned: false } : it))
.sort((a, b) => (a.isPinned === b.isPinned ? 0 : a.isPinned === true ? -1 : 1))
widgetsState.set(widget._id, { ...widgetState, tabs })
sidebarStore.set({
...state,
widgetsState
})
}
function isDescendant (parent: HTMLElement, child: HTMLElement): boolean {
let node = child.parentNode
while (node != null) {
if (node === parent) {
return true
}
node = node.parentNode
}
return false
}
export function isElementFromSidebar (element: HTMLElement): boolean {
const sidebarElement = document.getElementById('sidebar')
if (sidebarElement == null) {
return false
}
return isDescendant(sidebarElement, element)
}
+5 -2
View File
@@ -25,12 +25,14 @@ import {
closePanel,
fetchMetadataLocalStorage,
getCurrentLocation,
type Location,
navigate,
setMetadataLocalStorage
setMetadataLocalStorage,
location
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import workbench, { type Application, type NavigatorModel } from '@hcengineering/workbench'
import { writable } from 'svelte/store'
import { derived, writable } from 'svelte/store'
export const workspaceCreating = writable<number | undefined>(undefined)
@@ -155,6 +157,7 @@ export async function showApplication (app: Application): Promise<void> {
}
export const workspacesStore = writable<Workspace[]>([])
export const workspaceStore = derived(location, (loc: Location) => loc.path[1])
/**
* @public