mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 13:35:04 +02:00
Add widgets sidebar (#6578)
This commit is contained in:
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user