mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 18:27:44 +02:00
Add workbench tabs (#6788)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -16,11 +16,20 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
import core, { AccountRole, Class, Doc, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import core, {
|
||||
AccountRole,
|
||||
Class,
|
||||
Doc,
|
||||
getCurrentAccount,
|
||||
hasAccountRole,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { IntlString, broadcastEvent, getMetadata, getResource } from '@hcengineering/platform'
|
||||
import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
ActionContext,
|
||||
ComponentExtensions,
|
||||
@@ -30,55 +39,62 @@
|
||||
reduceCalls
|
||||
} from '@hcengineering/presentation'
|
||||
import setting from '@hcengineering/setting'
|
||||
import support, { SupportStatus, supportLink } from '@hcengineering/support'
|
||||
import support, { supportLink, SupportStatus } from '@hcengineering/support'
|
||||
import {
|
||||
AnyComponent,
|
||||
areLocationsEqual,
|
||||
Button,
|
||||
closePanel,
|
||||
closePopup,
|
||||
closeTooltip,
|
||||
CompAndProps,
|
||||
Component,
|
||||
defineSeparators,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
Dock,
|
||||
getCurrentLocation,
|
||||
getLocation,
|
||||
IconSettings,
|
||||
Label,
|
||||
Location,
|
||||
location,
|
||||
locationStorageKeyId,
|
||||
locationToUrl,
|
||||
mainSeparators,
|
||||
navigate,
|
||||
openPanel,
|
||||
PanelInstance,
|
||||
Popup,
|
||||
PopupAlignment,
|
||||
PopupPosAlignment,
|
||||
PopupResult,
|
||||
ResolvedLocation,
|
||||
Separator,
|
||||
TooltipInstance,
|
||||
areLocationsEqual,
|
||||
closePanel,
|
||||
closePopup,
|
||||
closeTooltip,
|
||||
defineSeparators,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
getCurrentLocation,
|
||||
getLocation,
|
||||
location,
|
||||
locationStorageKeyId,
|
||||
navigate,
|
||||
openPanel,
|
||||
popupstore,
|
||||
pushRootBarComponent,
|
||||
ResolvedLocation,
|
||||
resolvedLocationStore,
|
||||
Separator,
|
||||
setResolvedLocation,
|
||||
showPopup,
|
||||
workbenchSeparators,
|
||||
mainSeparators
|
||||
TooltipInstance,
|
||||
workbenchSeparators
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import {
|
||||
accessDeniedStore,
|
||||
ActionHandler,
|
||||
ListSelectionProvider,
|
||||
NavLink,
|
||||
accessDeniedStore,
|
||||
migrateViewOpttions,
|
||||
NavLink,
|
||||
parseLinkId,
|
||||
updateFocus
|
||||
} from '@hcengineering/view-resources'
|
||||
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
|
||||
import type {
|
||||
Application,
|
||||
NavigatorModel,
|
||||
SpecialNavModel,
|
||||
ViewConfiguration,
|
||||
WorkbenchTab
|
||||
} from '@hcengineering/workbench'
|
||||
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
||||
import { subscribeMobile } from '../mobile'
|
||||
import workbench from '../plugin'
|
||||
@@ -96,6 +112,7 @@
|
||||
import TopMenu from './icons/TopMenu.svelte'
|
||||
import WidgetsBar from './sidebar/Sidebar.svelte'
|
||||
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
|
||||
import { getTabLocation, selectTab, syncWorkbenchTab, tabIdStore, tabsStore } from '../workbench'
|
||||
|
||||
let contentPanel: HTMLElement
|
||||
|
||||
@@ -142,13 +159,74 @@
|
||||
}
|
||||
}
|
||||
|
||||
let tabs: WorkbenchTab[] = []
|
||||
let areTabsLoaded = false
|
||||
let prevTab: Ref<WorkbenchTab> | undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(
|
||||
workbench.class.WorkbenchTab,
|
||||
{},
|
||||
(res) => {
|
||||
tabs = res
|
||||
tabsStore.set(tabs)
|
||||
if (!areTabsLoaded) {
|
||||
void initCurrentTab(tabs)
|
||||
areTabsLoaded = true
|
||||
}
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
isPinned: SortingOrder.Descending,
|
||||
createdOn: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function initCurrentTab (tabs: WorkbenchTab[]): Promise<void> {
|
||||
if (tabs.length === 0) {
|
||||
const loc = getCurrentLocation()
|
||||
|
||||
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||
attachedTo: account._id,
|
||||
location: locationToUrl(loc),
|
||||
isPinned: false
|
||||
})
|
||||
prevTab = _id
|
||||
selectTab(_id)
|
||||
} else {
|
||||
const tab = tabs.find((t) => t._id === $tabIdStore)
|
||||
const loc = getCurrentLocation()
|
||||
const tabLoc = tab ? getTabLocation(tab) : undefined
|
||||
const isLocEqual = tabLoc ? areLocationsEqual(loc, tabLoc) : false
|
||||
if (!isLocEqual) {
|
||||
const url = locationToUrl(loc)
|
||||
const tabByUrl = tabs.find((t) => t.location === url)
|
||||
if (tabByUrl !== undefined) {
|
||||
prevTab = tabByUrl._id
|
||||
selectTab(tabByUrl._id)
|
||||
} else {
|
||||
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||
attachedTo: account._id,
|
||||
location: url,
|
||||
isPinned: false
|
||||
})
|
||||
prevTab = _id
|
||||
selectTab(_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
pushRootBarComponent('right', view.component.SearchSelector)
|
||||
pushRootBarComponent('left', workbench.component.WorkbenchTabs, 30)
|
||||
void getResource(login.function.GetWorkspaces).then(async (getWorkspaceFn) => {
|
||||
$workspacesStore = await getWorkspaceFn()
|
||||
await updateWindowTitle(getLocation())
|
||||
})
|
||||
syncSidebarState()
|
||||
syncWorkbenchTab()
|
||||
})
|
||||
|
||||
const account = getCurrentAccount() as PersonAccount
|
||||
@@ -296,7 +374,13 @@
|
||||
async function syncLoc (loc: Location): Promise<void> {
|
||||
accessDeniedStore.set(false)
|
||||
const originalLoc = JSON.stringify(loc)
|
||||
|
||||
if ($tabIdStore !== prevTab) {
|
||||
if (prevTab) {
|
||||
clear(1)
|
||||
clear(2)
|
||||
}
|
||||
prevTab = $tabIdStore
|
||||
}
|
||||
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
|
||||
// resolve short links
|
||||
const resolvedLoc = await resolveShortLink(loc)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<!--
|
||||
// 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 { AnySvelteComponent, closePanel, getCurrentLocation, Location, ModernTab, navigate } from '@hcengineering/ui'
|
||||
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import { type Application, WorkbenchTab } from '@hcengineering/workbench'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import view from '@hcengineering/view'
|
||||
import { parseLinkId, showMenu } from '@hcengineering/view-resources'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
|
||||
import { closeTab, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
|
||||
import workbench from '../plugin'
|
||||
|
||||
export let tab: WorkbenchTab
|
||||
|
||||
const client = getClient()
|
||||
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
|
||||
|
||||
let name: string | undefined = undefined
|
||||
let label: IntlString | undefined = undefined
|
||||
let icon: Asset | AnySvelteComponent | undefined
|
||||
let iconProps: Record<string, any> | undefined
|
||||
|
||||
async function getResolvedLocation (loc: Location, app?: Application): Promise<Location> {
|
||||
if (app === undefined) return loc
|
||||
if (app.locationResolver) {
|
||||
const resolver = await getResource(app.locationResolver)
|
||||
return (await resolver(loc))?.loc ?? loc
|
||||
}
|
||||
|
||||
return loc
|
||||
}
|
||||
|
||||
async function getTabName (loc: Location): Promise<string | undefined> {
|
||||
if (loc.fragment == null) return
|
||||
const hierarchy = client.getHierarchy()
|
||||
const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
|
||||
if (_class == null) return
|
||||
|
||||
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
|
||||
if (mixin === undefined) return
|
||||
const titleProvider = await getResource(mixin.titleProvider)
|
||||
try {
|
||||
const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
|
||||
return await titleProvider(client, _id)
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTabData (tab: WorkbenchTab): Promise<void> {
|
||||
const tabLoc = getTabLocation(tab)
|
||||
const alias = tabLoc.path[2]
|
||||
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias })[0]
|
||||
|
||||
if (application?.locationDataResolver) {
|
||||
const resolver = await getResource(application.locationDataResolver)
|
||||
const data = await resolver(tabLoc)
|
||||
name = data.name
|
||||
label = data.nameIntl ?? application.label ?? workbench.string.Tab
|
||||
|
||||
if (data.iconComponent) {
|
||||
icon = await getResource(data.iconComponent)
|
||||
} else {
|
||||
icon = data.icon ?? application?.icon
|
||||
}
|
||||
iconProps = data.iconProps
|
||||
} else {
|
||||
const special = tabLoc.path[3]
|
||||
const specialLabel = application?.navigatorModel?.specials?.find((s) => s.id === special)?.label
|
||||
const resolvedLoc = await getResolvedLocation(tabLoc, application)
|
||||
name = await getTabName(resolvedLoc)
|
||||
label = specialLabel ?? application?.label ?? workbench.string.Tab
|
||||
icon = application?.icon
|
||||
iconProps = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$: void updateTabData(tab)
|
||||
|
||||
function handleClickTab (): void {
|
||||
selectTab(tab._id)
|
||||
const tabLoc = getTabLocation(tab)
|
||||
const loc = getCurrentLocation()
|
||||
const currentApp = loc.path[2]
|
||||
const newApp = tabLoc.path[2]
|
||||
if (newApp && newApp !== currentApp) {
|
||||
closePanel(false)
|
||||
}
|
||||
navigate(tabLoc)
|
||||
}
|
||||
|
||||
function handleCloseTab (): void {
|
||||
void closeTab(tab)
|
||||
}
|
||||
|
||||
function handleMenu (event: MouseEvent): void {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
showMenu(event, { object: tab, baseMenuClass: workbench.class.WorkbenchTab })
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModernTab
|
||||
labelIntl={label}
|
||||
label={name}
|
||||
{icon}
|
||||
{iconProps}
|
||||
maxSize="12rem"
|
||||
highlighted={$tabIdStore === tab._id}
|
||||
canClose={$tabsStore.length > 1 && !tab.isPinned}
|
||||
on:click={handleClickTab}
|
||||
on:close={handleCloseTab}
|
||||
on:contextmenu={handleMenu}
|
||||
>
|
||||
<svelte:fragment slot="prefix">
|
||||
<ComponentExtensions extension={workbench.extensions.WorkbenchTabExtensions} props={{ tab }} />
|
||||
</svelte:fragment>
|
||||
</ModernTab>
|
||||
@@ -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 { createTab, tabsStore } from '../workbench'
|
||||
import WorkbenchTabPresenter from './WorkbenchTabPresenter.svelte'
|
||||
import { IconAdd, ButtonIcon } from '@hcengineering/ui'
|
||||
</script>
|
||||
|
||||
<div class="root flex-gap-1">
|
||||
{#each $tabsStore as tab}
|
||||
<WorkbenchTabPresenter {tab} />
|
||||
{/each}
|
||||
<div class="ml-1-5 plus-button mr-1">
|
||||
<ButtonIcon icon={IconAdd} size="min" kind="tertiary" on:click={createTab} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.plus-button {
|
||||
height: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,9 @@ import WorkbenchApp from './components/WorkbenchApp.svelte'
|
||||
import { doNavigate } from './utils'
|
||||
import Workbench from './components/Workbench.svelte'
|
||||
import ServerManager from './components/ServerManager.svelte'
|
||||
import WorkbenchTabs from './components/WorkbenchTabs.svelte'
|
||||
import { isAdminUser } from '@hcengineering/presentation'
|
||||
import { canCloseTab, closeTab, pinTab, unpinTab } from './workbench'
|
||||
|
||||
async function hasArchiveSpaces (spaces: Space[]): Promise<boolean> {
|
||||
return spaces.find((sp) => sp.archived) !== undefined
|
||||
@@ -46,13 +48,18 @@ export default async (): Promise<Resources> => ({
|
||||
SpacePanel,
|
||||
SpecialView,
|
||||
Workbench,
|
||||
ServerManager
|
||||
ServerManager,
|
||||
WorkbenchTabs
|
||||
},
|
||||
function: {
|
||||
HasArchiveSpaces: hasArchiveSpaces,
|
||||
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser()
|
||||
IsOwner: async (docs: Space[]) => getCurrentAccount().role === AccountRole.Owner || isAdminUser(),
|
||||
CanCloseTab: canCloseTab
|
||||
},
|
||||
actionImpl: {
|
||||
Navigate: doNavigate
|
||||
Navigate: doNavigate,
|
||||
PinTab: pinTab,
|
||||
UnpinTab: unpinTab,
|
||||
CloseTab: closeTab
|
||||
}
|
||||
})
|
||||
|
||||
@@ -47,13 +47,15 @@ export default mergeIds(workbenchId, workbench, {
|
||||
WorkspaceCreating: '' as IntlString,
|
||||
AccessDenied: '' as IntlString,
|
||||
Widget: '' as IntlString,
|
||||
WidgetPreference: '' as IntlString
|
||||
WidgetPreference: '' as IntlString,
|
||||
Tab: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
MobileAllowed: '' as Metadata<boolean>
|
||||
},
|
||||
component: {
|
||||
SpacePanel: '' as AnyComponent,
|
||||
Workbench: '' as AnyComponent
|
||||
Workbench: '' as AnyComponent,
|
||||
WorkbenchTabs: '' as AnyComponent
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//
|
||||
// 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 { derived, get, writable } from 'svelte/store'
|
||||
import core, { concatLink, getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import workbench, { type WorkbenchTab } from '@hcengineering/workbench'
|
||||
import {
|
||||
location as locationStore,
|
||||
locationToUrl,
|
||||
parseLocation,
|
||||
type Location,
|
||||
navigate,
|
||||
getCurrentLocation
|
||||
} from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
import { workspaceStore } from './utils'
|
||||
|
||||
export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>()
|
||||
export const tabsStore = writable<WorkbenchTab[]>([])
|
||||
export const currentTabStore = derived([tabIdStore, tabsStore], ([tabId, tabs]) => {
|
||||
return tabs.find((tab) => tab._id === tabId)
|
||||
})
|
||||
|
||||
workspaceStore.subscribe((workspace) => {
|
||||
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
|
||||
})
|
||||
|
||||
locationStore.subscribe((loc) => {
|
||||
const workspace = get(workspaceStore)
|
||||
if (workspace == null || workspace === '') return
|
||||
const tab = get(currentTabStore)
|
||||
if (tab == null) return
|
||||
const tabId = tab._id
|
||||
if (tabId == null || tab._id !== tabId) return
|
||||
|
||||
void getClient().update(tab, { location: locationToUrl(loc) })
|
||||
})
|
||||
|
||||
tabIdStore.subscribe(saveTabToLocalStorage)
|
||||
|
||||
export function syncWorkbenchTab (): void {
|
||||
const workspace = get(workspaceStore)
|
||||
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
|
||||
}
|
||||
|
||||
function getTabIdLocalStorageKey (workspace: string): string | undefined {
|
||||
const me = getCurrentAccount()
|
||||
if (me == null || workspace === '') return undefined
|
||||
return `workbench.${workspace}.${me.person}.tab`
|
||||
}
|
||||
|
||||
function getTabFromLocalStorage (workspace: string): Ref<WorkbenchTab> | undefined {
|
||||
const localStorageKey = getTabIdLocalStorageKey(workspace)
|
||||
if (localStorageKey === undefined) return undefined
|
||||
|
||||
const tab = window.localStorage.getItem(localStorageKey)
|
||||
if (tab == null || tab === '') return undefined
|
||||
|
||||
return tab as Ref<WorkbenchTab>
|
||||
}
|
||||
|
||||
function saveTabToLocalStorage (_id: Ref<WorkbenchTab> | undefined): void {
|
||||
const workspace = get(workspaceStore)
|
||||
if (workspace == null || workspace === '') return
|
||||
|
||||
const localStorageKey = getTabIdLocalStorageKey(workspace)
|
||||
if (localStorageKey === undefined) return
|
||||
window.localStorage.setItem(localStorageKey, _id ?? '')
|
||||
}
|
||||
|
||||
export function selectTab (_id: Ref<WorkbenchTab>): void {
|
||||
tabIdStore.set(_id)
|
||||
}
|
||||
|
||||
export function getTabLocation (tab: WorkbenchTab): Location {
|
||||
const base = `${window.location.protocol}//${window.location.host}`
|
||||
const url = new URL(concatLink(base, tab.location))
|
||||
|
||||
return parseLocation(url)
|
||||
}
|
||||
|
||||
export async function closeTab (tab: WorkbenchTab): Promise<void> {
|
||||
const tabs = get(tabsStore)
|
||||
const index = tabs.findIndex((t) => t._id === tab._id)
|
||||
if (index === -1) return
|
||||
|
||||
tabsStore.update((tabs) => tabs.filter((t) => t._id !== tab._id))
|
||||
if (get(tabIdStore) === tab._id) {
|
||||
const newTab = tabs[index - 1] ?? tabs[index + 1]
|
||||
tabIdStore.set(newTab?._id)
|
||||
if (newTab !== undefined) {
|
||||
navigate(getTabLocation(newTab))
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
await client.remove(tab)
|
||||
}
|
||||
|
||||
export async function createTab (): Promise<void> {
|
||||
const loc = getCurrentLocation()
|
||||
const client = getClient()
|
||||
const me = getCurrentAccount()
|
||||
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
|
||||
attachedTo: me._id,
|
||||
location: locationToUrl(loc),
|
||||
isPinned: false
|
||||
})
|
||||
|
||||
selectTab(tab)
|
||||
}
|
||||
|
||||
export function canCloseTab (tab: WorkbenchTab): boolean {
|
||||
const tabs = get(tabsStore)
|
||||
return tabs.length > 1 && tabs.some((t) => t._id === tab._id)
|
||||
}
|
||||
|
||||
export async function pinTab (tab: WorkbenchTab): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.update(tab, { isPinned: true })
|
||||
}
|
||||
|
||||
export async function unpinTab (tab: WorkbenchTab): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.update(tab, { isPinned: false })
|
||||
}
|
||||
Reference in New Issue
Block a user