Fix pinned tabs and create tab (#6864)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-10-10 17:27:40 +04:00 committed by GitHub
parent e2d1b544d0
commit f6ff5de501
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 76 deletions

View File

@ -105,6 +105,7 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent {
@UX(workbench.string.Tab)
export class TWorkbenchTab extends TPreference implements WorkbenchTab {
location!: string
name?: string
isPinned!: boolean
}

View File

@ -73,9 +73,10 @@
<div class="close-button {orientation}">
<ButtonIcon icon={IconClose} size="min" on:click={() => dispatch('close')} />
</div>
{:else}
{:else if $$slots.postfix === undefined}
<div />
{/if}
<slot name="postfix" />
</div>
<style lang="scss">

View File

@ -87,6 +87,9 @@
<path fill-rule="evenodd" clip-rule="evenodd"
d="M21.7071 2.29289C21.3166 1.90237 20.6834 1.90237 20.2929 2.29289C19.9024 2.68342 19.9024 3.31658 20.2929 3.70711L20.5858 4L14.9628 9.623C14.7214 9.86439 14.394 10 14.0526 10H13.2426C10.2609 10 7.4013 11.1845 5.29289 13.2929C4.90237 13.6834 4.90237 14.3166 5.29289 14.7071L10.5859 20.0001L2 28.5859V30H3.41436L12.0001 21.4143L17.2929 26.7071C17.6834 27.0976 18.3166 27.0976 18.7071 26.7071C20.8155 24.5987 22 21.7391 22 18.7574V17.9474C22 17.606 22.1356 17.2786 22.377 17.0372L28 11.4142L28.2929 11.7071C28.6834 12.0976 29.3166 12.0976 29.7071 11.7071C30.0976 11.3166 30.0976 10.6834 29.7071 10.2929L21.7071 2.29289ZM16.377 11.0372L22 5.41421L26.5858 10L20.9628 15.623C20.3463 16.2395 20 17.0756 20 17.9474V18.7574C20 20.873 19.2746 22.9139 17.9617 24.5474L7.45256 14.0383C9.08614 12.7254 11.127 12 13.2426 12H14.0526C14.9244 12 15.7605 11.6537 16.377 11.0372Z"/>
</symbol>
<symbol id="pin-tack" viewBox="0 0 18 18">
<line x1="9" y1="16.25" x2="9" y2="12.25" stroke-linecap="round" stroke-linejoin="round" stroke="#212121"></line><path d="M14.25,12.25c-.089-.699-.318-1.76-.969-2.875-.335-.574-.703-1.028-1.031-1.375V3.75c0-1.105-.895-2-2-2h-2.5c-1.105,0-2,.895-2,2v4.25c-.329,.347-.697,.801-1.031,1.375-.65,1.115-.88,2.176-.969,2.875H14.25Z" stroke-linecap="round" stroke-linejoin="round"></path>
</symbol>
<symbol id="model" viewBox="0 0 24 24">
<path d="M17.2,7.8h3.6c1.1,0,2-0.9,2-2V4.2c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0H9.8V4c0-1.5-1.2-2.8-2.8-2.8H4 C2.5,1.2,1.2,2.5,1.2,4v2c0,1.5,1.2,2.8,2.8,2.8h3c1.5,0,2.8-1.2,2.8-2.8V5.8h2V18c0,1.5,1.2,2.8,2.8,2.8h0.8v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-0.8c-0.7,0-1.2-0.6-1.2-1.2v-4.8h2v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-2v-6h2v0C15.2,6.9,16.1,7.8,17.2,7.8z M8.2,6 c0,0.7-0.6,1.2-1.2,1.2H4C3.3,7.2,2.8,6.7,2.8,6V4c0-0.7,0.6-1.2,1.2-1.2h3c0.7,0,1.2,0.6,1.2,1.2V6z M16.8,19.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V19.2z M16.8,11.7 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V11.7z M16.8,4.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V4.2z"/>
</symbol>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -61,5 +61,6 @@ loadMetadata(view.icon, {
Undo: `${icons}#undo`,
Video: `${icons}#video`,
Audio: `${icons}#audio`,
File: `${icons}#file`
File: `${icons}#file`,
PinTack: `${icons}#pin-tack`
})

View File

@ -257,7 +257,8 @@ const view = plugin(viewId, {
Undo: '' as Asset,
Video: '' as Asset,
Audio: '' as Asset,
File: '' as Asset
File: '' as Asset,
PinTack: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,

View File

@ -13,85 +13,51 @@
// limitations under the License.
-->
<script lang="ts">
import { AnySvelteComponent, closePanel, getCurrentLocation, Location, ModernTab, navigate } from '@hcengineering/ui'
import {
AnySvelteComponent,
closePanel,
getCurrentLocation,
Icon,
ModernTab,
navigate,
languageStore,
locationToUrl
} 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 { Asset, getResource, translate } from '@hcengineering/platform'
import { WorkbenchTab } from '@hcengineering/workbench'
import view from '@hcengineering/view'
import { parseLinkId, showMenu } from '@hcengineering/view-resources'
import { Analytics } from '@hcengineering/analytics'
import { showMenu } from '@hcengineering/view-resources'
import { closeTab, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
import { closeTab, getTabDataByLocation, 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 name: string | undefined = tab.name
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> {
async function updateTabData (tab: WorkbenchTab, lang: string): Promise<void> {
const tabLoc = $tabIdStore === tab._id ? getCurrentLocation() : getTabLocation(tab)
const alias = tabLoc.path[2]
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias })[0]
const data = await getTabDataByLocation(tabLoc)
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
name = data.name ?? (await translate(data.label, {}, lang))
if (data.iconComponent !== undefined) {
icon = await getResource(data.iconComponent)
} 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
icon = data.icon
}
iconProps = data.iconProps
if (tab.name !== name && tab.location === locationToUrl(tabLoc)) {
await client.update(tab, { name })
}
}
$: void updateTabData(tab)
$: void updateTabData(tab, $languageStore)
function handleClickTab (): void {
selectTab(tab._id)
@ -118,7 +84,6 @@
</script>
<ModernTab
labelIntl={label}
label={name}
{icon}
{iconProps}
@ -132,4 +97,9 @@
<svelte:fragment slot="prefix">
<ComponentExtensions extension={workbench.extensions.WorkbenchTabExtensions} props={{ tab }} />
</svelte:fragment>
<svelte:fragment slot="postfix">
{#if tab.isPinned}
<Icon icon={view.icon.PinTack} size="x-small" />
{/if}
</svelte:fragment>
</ModernTab>

View File

@ -13,20 +13,26 @@
// 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 core, { type Class, concatLink, type Doc, getCurrentAccount, type Ref } from '@hcengineering/core'
import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench'
import {
location as locationStore,
locationToUrl,
parseLocation,
type Location,
navigate,
getCurrentLocation
getCurrentLocation,
languageStore,
type AnyComponent
} from '@hcengineering/ui'
import presentation, { getClient } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform'
import view from '@hcengineering/view'
import { type Asset, type IntlString, getMetadata, getResource, translate } from '@hcengineering/platform'
import { parseLinkId } from '@hcengineering/view-resources'
import { notificationId } from '@hcengineering/notification'
import { workspaceStore } from './utils'
import workbench from './plugin'
export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>()
export const tabsStore = writable<WorkbenchTab[]>([])
@ -38,7 +44,14 @@ workspaceStore.subscribe((workspace) => {
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
})
locationStore.subscribe((loc) => {
locationStore.subscribe(() => {
void syncTabLoc()
})
tabIdStore.subscribe(saveTabToLocalStorage)
async function syncTabLoc (): Promise<void> {
const loc = getCurrentLocation()
const workspace = get(workspaceStore)
if (workspace == null || workspace === '') return
const tab = get(currentTabStore)
@ -51,11 +64,22 @@ locationStore.subscribe((loc) => {
return
}
if (loc.path[2] === '' || loc.path[2] == null) return
void getClient().update(tab, { location: locationToUrl(loc) })
})
tabIdStore.subscribe(saveTabToLocalStorage)
const data = await getTabDataByLocation(loc)
const name = data.name ?? (await translate(data.label, {}, get(languageStore)))
if (tab.name !== undefined && name !== tab.name && tab.isPinned) {
const me = getCurrentAccount()
const _id = await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
location: locationToUrl(loc),
name,
attachedTo: me._id,
isPinned: false
})
selectTab(_id)
} else {
await getClient().update(tab, { location: locationToUrl(loc), name })
}
}
export function syncWorkbenchTab (): void {
const workspace = get(workspaceStore)
tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
@ -90,7 +114,7 @@ export function selectTab (_id: Ref<WorkbenchTab>): void {
tabIdStore.set(_id)
}
export function getTabLocation (tab: WorkbenchTab): Location {
export function getTabLocation (tab: Pick<WorkbenchTab, 'location'>): Location {
const base = `${window.location.protocol}//${window.location.host}`
const front = getMetadata(presentation.metadata.FrontUrl) ?? base
const url = new URL(concatLink(front, tab.location))
@ -120,13 +144,15 @@ export async function createTab (): Promise<void> {
const loc = getCurrentLocation()
const client = getClient()
const me = getCurrentAccount()
const defaultUrl = `${workbenchId}/${loc.path[1]}/${notificationId}`
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: me._id,
location: locationToUrl(loc),
location: defaultUrl,
isPinned: false
})
selectTab(tab)
navigate(getTabLocation({ location: defaultUrl }))
}
export function canCloseTab (tab: WorkbenchTab): boolean {
@ -143,3 +169,66 @@ export async function unpinTab (tab: WorkbenchTab): Promise<void> {
const client = getClient()
await client.update(tab, { isPinned: false })
}
export async function getTabDataByLocation (loc: Location): Promise<{
name?: string
label: IntlString
icon?: Asset
iconComponent?: AnyComponent
iconProps?: Record<string, any>
}> {
const client = getClient()
const appAlias = loc.path[2]
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias: appAlias })[0]
let name: string | undefined
let label: IntlString | undefined
let icon: Asset | undefined
let iconComponent: AnyComponent | undefined
let iconProps: Record<string, any> | undefined
if (application?.locationDataResolver != null) {
const resolver = await getResource(application.locationDataResolver)
const data = await resolver(loc)
name = data.name
label = data.nameIntl ?? application.label ?? workbench.string.Tab
iconComponent = data.iconComponent
icon = data.icon ?? application.icon
iconProps = data.iconProps
} else {
const special = loc.path[3]
const specialLabel = application?.navigatorModel?.specials?.find((s) => s.id === special)?.label
const resolvedLoc = await getResolvedLocation(loc, application)
name = await getDefaultTabName(resolvedLoc)
label = specialLabel ?? application?.label ?? workbench.string.Tab
icon = application?.icon
}
return { name, label, icon, iconComponent, iconProps }
}
async function getResolvedLocation (loc: Location, app?: Application): Promise<Location> {
if (app?.locationResolver === undefined) return loc
const resolver = await getResource(app.locationResolver)
return (await resolver(loc))?.loc ?? loc
}
async function getDefaultTabName (loc: Location): Promise<string | undefined> {
if (loc.fragment == null) return
const client = getClient()
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 linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
return await titleProvider(client, _id)
} catch (err: any) {
console.error(err)
}
}

View File

@ -113,6 +113,7 @@ export interface TxSidebarEvent<T extends Record<string, any> = Record<string, a
export interface WorkbenchTab extends Preference {
location: string
isPinned: boolean
name?: string
}
/**