Improve tabs pinning (#6874)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-10-11 15:34:09 +04:00 committed by GitHub
parent 02c6f895cd
commit b21f860c9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 137 additions and 33 deletions

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Location, location, locationToUrl, navigate } from '@hcengineering/ui' import { Location, location, locationStorageKeyId, locationToUrl, navigate } from '@hcengineering/ui'
import { setFilters } from '../../filter' import { setFilters } from '../../filter'
export let app: string | undefined = undefined export let app: string | undefined = undefined
@ -21,6 +21,7 @@
export let special: string | undefined = undefined export let special: string | undefined = undefined
export let disabled = false export let disabled = false
export let shrink: number | undefined = undefined export let shrink: number | undefined = undefined
export let restoreLastLocation = false
$: loc = createLocation($location, app, space, special) $: loc = createLocation($location, app, space, special)
@ -32,6 +33,20 @@
space: string | undefined, space: string | undefined,
special: string | undefined special: string | undefined
): Location { ): Location {
if (restoreLastLocation) {
const last = localStorage.getItem(`${locationStorageKeyId}_${app}`)
if (last != null) {
try {
const newLocation: Location = JSON.parse(last)
if (newLocation.path[2] === app && newLocation.path[3] != null) {
return newLocation
}
} catch (e) {
// ignore
}
}
}
const location: Location = { const location: Location = {
path: [...loc.path] path: [...loc.path]
} }
@ -50,7 +65,7 @@
return location return location
} }
function clickHandler (e: MouseEvent) { function clickHandler (e: MouseEvent): void {
if (e.metaKey || e.ctrlKey) return if (e.metaKey || e.ctrlKey) return
e.preventDefault() e.preventDefault()
setFilters([]) setFilters([])

View File

@ -58,13 +58,13 @@
buttons={'union'} buttons={'union'}
> >
{#each topApps as app} {#each topApps as app}
<NavLink app={app.alias} shrink={0}> <NavLink app={app.alias} shrink={0} disabled={app._id === active}>
<AppItem selected={app._id === active} icon={app.icon} label={app.label} /> <AppItem selected={app._id === active} icon={app.icon} label={app.label} />
</NavLink> </NavLink>
{/each} {/each}
<div class="divider" /> <div class="divider" />
{#each bottomdApps as app} {#each bottomdApps as app}
<NavLink app={app.alias} shrink={0}> <NavLink app={app.alias} shrink={0} disabled={app._id === active}>
<AppItem selected={app._id === active} icon={app.icon} label={app.label} /> <AppItem selected={app._id === active} icon={app.icon} label={app.label} />
</NavLink> </NavLink>
{/each} {/each}

View File

@ -29,7 +29,7 @@
import login from '@hcengineering/login' import login from '@hcengineering/login'
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification' import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { BrowserNotificatator, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { broadcastEvent, getMetadata, getResource, IntlString } from '@hcengineering/platform' import { broadcastEvent, getMetadata, getResource, IntlString, translate } from '@hcengineering/platform'
import { import {
ActionContext, ActionContext,
ComponentExtensions, ComponentExtensions,
@ -56,6 +56,7 @@
getLocation, getLocation,
IconSettings, IconSettings,
Label, Label,
languageStore,
Location, Location,
location, location,
locationStorageKeyId, locationStorageKeyId,
@ -112,7 +113,16 @@
import TopMenu from './icons/TopMenu.svelte' import TopMenu from './icons/TopMenu.svelte'
import WidgetsBar from './sidebar/Sidebar.svelte' import WidgetsBar from './sidebar/Sidebar.svelte'
import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar' import { sidebarStore, SidebarVariant, syncSidebarState } from '../sidebar'
import { getTabLocation, selectTab, syncWorkbenchTab, tabIdStore, tabsStore } from '../workbench' import {
getTabDataByLocation,
getTabLocation,
prevTabIdStore,
selectTab,
syncWorkbenchTab,
tabIdStore,
tabsStore
} from '../workbench'
import { get } from 'svelte/store'
let contentPanel: HTMLElement let contentPanel: HTMLElement
@ -161,7 +171,6 @@
let tabs: WorkbenchTab[] = [] let tabs: WorkbenchTab[] = []
let areTabsLoaded = false let areTabsLoaded = false
let prevTab: Ref<WorkbenchTab> | undefined
const query = createQuery() const query = createQuery()
$: query.query( $: query.query(
@ -190,26 +199,35 @@
const isLocEqual = tabLoc ? areLocationsEqual(loc, tabLoc) : false const isLocEqual = tabLoc ? areLocationsEqual(loc, tabLoc) : false
if (!isLocEqual) { if (!isLocEqual) {
const url = locationToUrl(loc) const url = locationToUrl(loc)
const tabByUrl = tabs.find((t) => t.location === url) const data = await getTabDataByLocation(loc)
if (tabByUrl !== undefined) { const name = data.name ?? (await translate(data.label, {}, get(languageStore)))
prevTab = tabByUrl._id const tabByName = get(tabsStore).find((t) => {
selectTab(tabByUrl._id) if (t.location === url) return true
if (t.name !== name) return false
const tabLoc = getTabLocation(t)
return tabLoc.path[2] === loc.path[2] && tabLoc.path[3] === loc.path[3]
})
if (tabByName !== undefined) {
selectTab(tabByName._id)
prevTabIdStore.set(tabByName._id)
} else { } else {
const tabToReplace = tabs.findLast((t) => !t.isPinned) const tabToReplace = tabs.findLast((t) => !t.isPinned)
if (tabToReplace !== undefined) { if (tabToReplace !== undefined) {
await client.update(tabToReplace, { await client.update(tabToReplace, {
location: url location: url
}) })
prevTab = tabToReplace._id
selectTab(tabToReplace._id) selectTab(tabToReplace._id)
prevTabIdStore.set(tabToReplace._id)
} else { } else {
const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, { const _id = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: account._id, attachedTo: account._id,
location: url, location: url,
isPinned: false isPinned: false
}) })
prevTab = _id
selectTab(_id) selectTab(_id)
prevTabIdStore.set(_id)
} }
} }
} }
@ -372,12 +390,16 @@
async function syncLoc (loc: Location): Promise<void> { async function syncLoc (loc: Location): Promise<void> {
accessDeniedStore.set(false) accessDeniedStore.set(false)
const originalLoc = JSON.stringify(loc) const originalLoc = JSON.stringify(loc)
if ($tabIdStore !== prevTab) { if ($tabIdStore !== $prevTabIdStore) {
if (prevTab) { if ($prevTabIdStore) {
clear(1) const prevTab = tabs.find((t) => t._id === $prevTabIdStore)
clear(2) const prevTabLoc = prevTab ? getTabLocation(prevTab) : undefined
if (prevTabLoc === undefined || prevTabLoc.path[2] !== loc.path[2]) {
clear(1)
clear(2)
}
} }
prevTab = $tabIdStore prevTabIdStore.set($tabIdStore)
} }
if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) { if (loc.path.length > 3 && getSpecialComponent(loc.path[3]) === undefined) {
// resolve short links // resolve short links
@ -766,7 +788,7 @@
/> />
</div> </div>
<!-- <ActivityStatus status="active" /> --> <!-- <ActivityStatus status="active" /> -->
<NavLink app={notificationId} shrink={0}> <NavLink app={notificationId} shrink={0} restoreLastLocation>
<AppItem <AppItem
icon={notification.icon.Notifications} icon={notification.icon.Notifications}
label={notification.string.Inbox} label={notification.string.Inbox}

View File

@ -100,6 +100,8 @@
<svelte:fragment slot="postfix"> <svelte:fragment slot="postfix">
{#if tab.isPinned} {#if tab.isPinned}
<Icon icon={view.icon.PinTack} size="x-small" /> <Icon icon={view.icon.PinTack} size="x-small" />
{:else if $tabsStore.length === 1}
<div />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</ModernTab> </ModernTab>

View File

@ -13,7 +13,15 @@
// limitations under the License. // limitations under the License.
// //
import { derived, get, writable } from 'svelte/store' import { derived, get, writable } from 'svelte/store'
import core, { type Class, concatLink, type Doc, getCurrentAccount, type Ref } from '@hcengineering/core' import core, {
type Class,
concatLink,
type Doc,
getCurrentAccount,
type Ref,
RateLimiter,
generateId
} from '@hcengineering/core'
import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench' import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench'
import { import {
location as locationStore, location as locationStore,
@ -23,29 +31,40 @@ import {
navigate, navigate,
getCurrentLocation, getCurrentLocation,
languageStore, languageStore,
type AnyComponent type AnyComponent,
locationStorageKeyId
} from '@hcengineering/ui' } from '@hcengineering/ui'
import presentation, { getClient } from '@hcengineering/presentation' import presentation, { getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { type Asset, type IntlString, getMetadata, getResource, translate } from '@hcengineering/platform' import { type Asset, type IntlString, getMetadata, getResource, translate } from '@hcengineering/platform'
import { parseLinkId } from '@hcengineering/view-resources' import { parseLinkId } from '@hcengineering/view-resources'
import { notificationId } from '@hcengineering/notification' import notification, { notificationId } from '@hcengineering/notification'
import { workspaceStore } from './utils' import { workspaceStore } from './utils'
import workbench from './plugin' import workbench from './plugin'
export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>() export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>()
export const prevTabIdStore = writable<Ref<WorkbenchTab> | undefined>()
export const tabsStore = writable<WorkbenchTab[]>([]) export const tabsStore = writable<WorkbenchTab[]>([])
export const currentTabStore = derived([tabIdStore, tabsStore], ([tabId, tabs]) => { export const currentTabStore = derived([tabIdStore, tabsStore], ([tabId, tabs]) => {
return tabs.find((tab) => tab._id === tabId) return tabs.find((tab) => tab._id === tabId)
}) })
let prevTabId: Ref<WorkbenchTab> | undefined
tabIdStore.subscribe((value) => {
prevTabIdStore.set(prevTabId)
prevTabId = value
})
// Use rate limiter to control tab creation, preventing multiple tabs during fast location changing
const limiter = new RateLimiter(1)
workspaceStore.subscribe((workspace) => { workspaceStore.subscribe((workspace) => {
tabIdStore.set(getTabFromLocalStorage(workspace ?? '')) tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
}) })
locationStore.subscribe(() => { locationStore.subscribe((l: Location) => {
void syncTabLoc() void limiter.add(syncTabLoc)
}) })
tabIdStore.subscribe(saveTabToLocalStorage) tabIdStore.subscribe(saveTabToLocalStorage)
@ -64,20 +83,51 @@ async function syncTabLoc (): Promise<void> {
return return
} }
if (loc.path[2] === '' || loc.path[2] == null) return if (loc.path[2] === '' || loc.path[2] == null) return
const url = locationToUrl(loc)
const data = await getTabDataByLocation(loc) const data = await getTabDataByLocation(loc)
const name = data.name ?? (await translate(data.label, {}, get(languageStore))) const name = data.name ?? (await translate(data.label, {}, get(languageStore)))
const tabByName = get(tabsStore).find((t) => {
if (t.name !== name) return false
const tabLoc = getTabLocation(t)
return tabLoc.path[2] === loc.path[2] && tabLoc.path[3] === loc.path[3]
})
if (tab.name !== undefined && name !== tab.name && tab.isPinned) { if (tab.name !== undefined && name !== tab.name && tab.isPinned) {
if (tabByName !== undefined) {
selectTab(tabByName._id)
return
}
const me = getCurrentAccount() const me = getCurrentAccount()
const _id = await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, { const tab: WorkbenchTab = {
location: locationToUrl(loc), _id: generateId(),
_class: workbench.class.WorkbenchTab,
space: core.space.Workspace,
location: url,
name, name,
attachedTo: me._id, attachedTo: me._id,
isPinned: false isPinned: false,
}) modifiedOn: Date.now(),
selectTab(_id) modifiedBy: me._id
}
await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, tab, tab._id)
tabsStore.update((tabs) => [...tabs, tab])
selectTab(tab._id)
} else { } else {
await getClient().update(tab, { location: locationToUrl(loc), name }) // TODO: Fix this
// if (
// tabByName !== undefined &&
// tabByName._id !== tab._id &&
// (loc.path[2] !== tabLoc.path[2] || loc.path[3] !== tabLoc.path[3])
// ) {
// selectTab(tabByName._id)
// return
// }
await getClient().update(tab, { location: url, name })
} }
} }
export function syncWorkbenchTab (): void { export function syncWorkbenchTab (): void {
@ -144,11 +194,24 @@ export async function createTab (): Promise<void> {
const loc = getCurrentLocation() const loc = getCurrentLocation()
const client = getClient() const client = getClient()
const me = getCurrentAccount() const me = getCurrentAccount()
const defaultUrl = `${workbenchId}/${loc.path[1]}/${notificationId}` let defaultUrl = `${workbenchId}/${loc.path[1]}/${notificationId}`
try {
const last = localStorage.getItem(`${locationStorageKeyId}_${notificationId}`)
const lastLocation: Location | undefined = last != null ? JSON.parse(last) : undefined
if (lastLocation != null && lastLocation.path[2] === notificationId) {
defaultUrl = locationToUrl(lastLocation)
}
} catch (e) {
// ignore
}
const name = await translate(notification.string.Inbox, {}, get(languageStore))
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, { const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: me._id, attachedTo: me._id,
location: defaultUrl, location: defaultUrl,
isPinned: false isPinned: false,
name
}) })
selectTab(tab) selectTab(tab)

View File

@ -28,7 +28,9 @@ export class ChannelPage extends CommonPage {
.locator('xpath=following-sibling::div[1]') .locator('xpath=following-sibling::div[1]')
.locator('button', { hasText: channel }) .locator('button', { hasText: channel })
readonly chooseChannel = (channel: string): Locator => this.page.getByRole('button', { name: channel }) readonly chooseChannel = (channel: string): Locator =>
this.page.locator('div.antiPanel-navigator').getByRole('button', { name: channel })
readonly closePopupWindow = (): Locator => this.page.locator('.notifyPopup button[data-id="btnNotifyClose"]') readonly closePopupWindow = (): Locator => this.page.locator('.notifyPopup button[data-id="btnNotifyClose"]')
readonly openAddMemberToChannel = (userName: string): Locator => this.page.getByRole('button', { name: userName }) readonly openAddMemberToChannel = (userName: string): Locator => this.page.getByRole('button', { name: userName })
readonly addMemberToChannelTableButton = (userName: string): Locator => readonly addMemberToChannelTableButton = (userName: string): Locator =>