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>