mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-24 09:16:43 +00:00
Improve tabs pinning (#6874)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
02c6f895cd
commit
b21f860c9a
@ -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([])
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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 =>
|
||||||
|
Loading…
Reference in New Issue
Block a user