TSK-865 Сделать красивый вид ссылок (#2771)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-03-20 14:45:52 +06:00 committed by GitHub
parent bbfee8f39b
commit ae84626521
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 281 additions and 226 deletions

View File

@ -19,7 +19,7 @@ import chunter from '@hcengineering/chunter-resources/src/plugin'
import type { Ref, Space, Doc } from '@hcengineering/core'
import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import type { AnyComponent, Location } from '@hcengineering/ui'
import type { Action, ActionCategory, ViewAction, ViewletDescriptor } from '@hcengineering/view'
export default mergeIds(chunterId, chunter, {
@ -98,7 +98,7 @@ export default mergeIds(chunterId, chunter, {
function: {
ChunterBrowserVisible: '' as Resource<(spaces: Space[]) => Promise<boolean>>,
GetLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
},
filter: {
CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,

View File

@ -19,7 +19,7 @@ import { mergeIds } from '@hcengineering/platform'
import { recruitId } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit-resources/src/plugin'
import { KanbanTemplate } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui'
import type { AnyComponent, Location } from '@hcengineering/ui'
import type { Action, ActionCategory, ViewAction, Viewlet } from '@hcengineering/view'
export default mergeIds(recruitId, recruit, {
@ -39,7 +39,7 @@ export default mergeIds(recruitId, recruit, {
Recruit: '' as Ref<ActionCategory>
},
function: {
GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetObjectLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
},
string: {

View File

@ -294,7 +294,7 @@ export class TLinkPresenter extends TDoc implements LinkPresenter {
@Mixin(view.mixin.LinkProvider, core.class.Class)
export class TLinkProvider extends TClass implements LinkProvider {
encode!: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
encode!: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
}
@Mixin(view.mixin.ObjectPanel, core.class.Class)

View File

@ -13,6 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { navigate, parseLocation } from '@hcengineering/ui'
export let href: string | undefined
export let disableClick = false
export let onClick: ((event: MouseEvent) => void) | undefined = undefined
@ -21,17 +23,28 @@
function clickHandler (e: MouseEvent) {
if (disableClick) return
onClick?.(e)
if (onClick) {
onClick(e)
} else if (href !== undefined) {
try {
const url = new URL(href)
if (url.origin === window.location.origin) {
e.preventDefault()
navigate(parseLocation(url))
}
} catch {}
}
}
</script>
{#if disableClick || onClick || href === undefined}
{#if disableClick || href === undefined}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class:cursor-pointer={!disableClick} class:noUnderline class:inline on:click={clickHandler}>
<slot />
</span>
{:else}
<a {href} class:noUnderline class:inline>
<a {href} class:noUnderline class:inline on:click={clickHandler}>
<slot />
</a>
{/if}

View File

@ -13,8 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import { locationToUrl, navigate, location, getCurrentLocation } from '../location'
import { location, locationToUrl, navigate } from '../location'
import { Location } from '../types'
export let app: string | undefined = undefined
@ -22,13 +21,7 @@
export let special: string | undefined = undefined
export let disabled = false
let loc = createLocation(getCurrentLocation(), app, space, special)
onDestroy(
location.subscribe(async (res) => {
loc = createLocation(res, app, space, special)
})
)
$: loc = createLocation($location, app, space, special)
$: href = locationToUrl(loc)

View File

@ -1,5 +1,5 @@
import { writable } from 'svelte/store'
import { getCurrentLocation, navigate } from './location'
import { get, writable } from 'svelte/store'
import { location, navigate } from './location'
import { AnyComponent, PopupAlignment } from './types'
export interface PanelProps {
@ -29,10 +29,10 @@ export function showPanel (
rightSection?: AnyComponent
): void {
openPanel(component, _id, _class, element, rightSection)
const location = getCurrentLocation()
if (location.fragment !== currentLocation) {
location.fragment = currentLocation
navigate(location)
const loc = get(location)
if (loc.fragment !== currentLocation) {
loc.fragment = currentLocation
navigate(loc)
}
}
@ -58,9 +58,9 @@ export function closePanel (shoulRedirect: boolean = true): void {
return { panel: undefined }
})
if (shoulRedirect) {
const location = getCurrentLocation()
location.fragment = undefined
const loc = get(location)
loc.fragment = undefined
currentLocation = undefined
navigate(location)
navigate(loc)
}
}

View File

@ -26,6 +26,12 @@ export interface Location {
fragment?: string // a value of fragment
}
export interface ResolvedLocation {
loc: Location
shouldNavigate: boolean
defaultLocation: Location
}
/**
* Returns true if locations are equal.
*/

View File

@ -17,16 +17,9 @@
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
import core, { Class, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import ui, {
EditWithIcon,
getCurrentLocation,
IconSearch,
Label,
navigate,
Loading,
TabList
} from '@hcengineering/ui'
import ui, { EditWithIcon, IconSearch, Label, Loading, location, navigate, TabList } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { get } from 'svelte/store'
import { dateFileBrowserFilters, FileBrowserSortMode, fileTypeFileBrowserFilters, sortModeToOptionObject } from '..'
import attachment from '../plugin'
import AttachmentsGalleryView from './AttachmentsGalleryView.svelte'
@ -37,11 +30,11 @@
export let withHeader: boolean = true
const client = getClient()
const loc = getCurrentLocation()
const loc = get(location)
const spaceId: Ref<Space> | undefined = loc.query?.spaceId as Ref<Space> | undefined
$: if (spaceId !== undefined) {
const loc = getCurrentLocation()
const loc = get(location)
loc.query = undefined
navigate(loc)
}

View File

@ -19,7 +19,8 @@
import { generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, navigate } from '@hcengineering/ui'
import { location, navigate } from '@hcengineering/ui'
import { get } from 'svelte/store'
import { createBacklinks } from '../backlinks'
import chunter from '../plugin'
import Channel from './Channel.svelte'
@ -76,7 +77,7 @@
}
function openThread (_id: Ref<Message>) {
const loc = getCurrentLocation()
const loc = get(location)
loc.path[4] = _id
navigate(loc)
}

View File

@ -27,7 +27,7 @@ import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, Resources, translate } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient, MessageBox } from '@hcengineering/presentation'
import { getCurrentLocation, navigate, showPopup } from '@hcengineering/ui'
import { location, navigate, showPopup } from '@hcengineering/ui'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
import TxBacklinkReference from './components/activity/TxBacklinkReference.svelte'
import TxCommentCreate from './components/activity/TxCommentCreate.svelte'
@ -51,10 +51,10 @@ import SavedMessages from './components/SavedMessages.svelte'
import Threads from './components/Threads.svelte'
import ThreadView from './components/ThreadView.svelte'
import { writable } from 'svelte/store'
import { get, writable } from 'svelte/store'
import { DisplayTx } from '../../activity/lib'
import { updateBacklinksList } from './backlinks'
import { getDmName, getFragment, getLink, resolveLocation } from './utils'
import { getDmName, getTitle, getLink, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
@ -134,7 +134,7 @@ export async function ArchiveChannel (channel: Channel, evt: any, afterArchive?:
client.update(channel, { archived: true })
if (afterArchive != null) afterArchive()
const loc = getCurrentLocation()
const loc = get(location)
if (loc.path[3] === channel._id) {
loc.path.length = 3
navigate(loc)
@ -243,7 +243,7 @@ export default async (): Promise<Resources> => ({
function: {
GetDmName: getDmName,
ChunterBrowserVisible: chunterBrowserVisible,
GetFragment: getFragment,
GetFragment: getTitle,
GetLink: getLink
},
activity: {

View File

@ -4,7 +4,7 @@ import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Class, Client, Doc, getCurrentAccount, Obj, Ref, Space, Timestamp } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { getCurrentLocation, getPanelURI, Location, navigate } from '@hcengineering/ui'
import { getCurrentLocation, getPanelURI, location, Location, navigate, ResolvedLocation } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import { get, writable } from 'svelte/store'
@ -70,7 +70,7 @@ export function getDay (time: Timestamp): Timestamp {
}
export function openMessageFromSpecial (message: ChunterMessage): void {
const loc = getCurrentLocation()
const loc = get(location)
if (message.attachedToClass === chunter.class.ChunterSpace) {
loc.path.length = 4
@ -84,7 +84,7 @@ export function openMessageFromSpecial (message: ChunterMessage): void {
}
export function navigateToSpecial (specialId: string): void {
const loc = getCurrentLocation()
const loc = get(location)
loc.path[3] = specialId
navigate(loc)
}
@ -120,14 +120,25 @@ export function scrollAndHighLight (): void {
}
export async function getLink (doc: Doc): Promise<string> {
const fragment = await getFragment(doc)
const fragment = await getTitle(doc)
const location = getCurrentLocation()
return await Promise.resolve(
`${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}#${fragment}`
)
}
export async function getFragment (doc: Doc): Promise<string> {
export async function getFragment (doc: Doc): Promise<Location> {
const loc = getCurrentLocation()
loc.path.length = 2
loc.fragment = undefined
loc.query = undefined
loc.path[2] = chunterId
loc.fragment = await getTitle(doc)
return loc
}
export async function getTitle (doc: Doc): Promise<string> {
const client = getClient()
const hierarchy = client.getHierarchy()
let clazz = hierarchy.getClass(doc._class)
@ -137,26 +148,25 @@ export async function getFragment (doc: Doc): Promise<string> {
label = clazz.shortLabel
}
label = label ?? doc._class
return `${chunterId}|${label}-${doc._id}`
return `${label}-${doc._id}`
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== chunterId) {
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== chunterId) {
return undefined
}
const shortLink = split[1]
const shortLink = loc.fragment
// shortlink
if (isShortId(shortLink)) {
if (shortLink !== undefined && isShortId(shortLink)) {
return await generateLocation(loc, shortLink)
}
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
async function generateLocation (loc: Location, shortLink: string): Promise<ResolvedLocation | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
@ -187,8 +197,15 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Loca
if (hierarchy.isDerived(doc._class, chunter.class.Message)) {
return {
path: [appComponent, workspace, chunterId, doc.space],
fragment: doc._id
loc: {
path: [appComponent, workspace, chunterId, doc.space],
fragment: doc._id
},
shouldNavigate: true,
defaultLocation: {
path: [appComponent, workspace, chunterId, doc.space],
fragment: doc._id
}
}
}
if (hierarchy.isDerived(doc._class, chunter.class.Comment)) {
@ -197,15 +214,29 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Loca
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
const component = panelComponent.component ?? view.component.EditDoc
return {
path: [appComponent, workspace],
fragment: getPanelURI(component, comment.attachedTo, comment.attachedToClass, 'content')
loc: {
path: [appComponent, workspace],
fragment: getPanelURI(component, comment.attachedTo, comment.attachedToClass, 'content')
},
shouldNavigate: false,
defaultLocation: {
path: [appComponent, workspace],
fragment: getPanelURI(component, comment.attachedTo, comment.attachedToClass, 'content')
}
}
}
if (hierarchy.isDerived(doc._class, chunter.class.ThreadMessage)) {
const msg = doc as ThreadMessage
return {
path: [appComponent, workspace, chunterId, doc.space, msg.attachedTo],
fragment: doc._id
loc: {
path: [appComponent, workspace, chunterId, doc.space, msg.attachedTo],
fragment: doc._id
},
shouldNavigate: true,
defaultLocation: {
path: [appComponent, workspace, chunterId, doc.space],
fragment: doc._id
}
}
}
}

View File

@ -18,7 +18,7 @@ import type { Employee } from '@hcengineering/contact'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import { AnyComponent } from '@hcengineering/ui'
import { AnyComponent, ResolvedLocation } from '@hcengineering/ui'
/**
* @public
@ -158,7 +158,7 @@ export default plugin(chunterId, {
ConvertToPrivate: '' as IntlString
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
},
app: {
Chunter: '' as Ref<Doc>

View File

@ -17,6 +17,7 @@
import contact, { contactId } from '@hcengineering/contact'
import { Doc } from '@hcengineering/core'
import { IntlString, mergeIds, Resource } from '@hcengineering/platform'
import { Location } from '@hcengineering/ui'
import { FilterFunction, SortFunc } from '@hcengineering/view'
export default mergeIds(contactId, contact, {
@ -66,7 +67,7 @@ export default mergeIds(contactId, contact, {
DisplayName: '' as IntlString
},
function: {
GetContactLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetContactLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
EmployeeSort: '' as SortFunc,
FilterChannelInResult: '' as FilterFunction,
FilterChannelNinResult: '' as FilterFunction

View File

@ -26,7 +26,7 @@ import {
import { Doc, getCurrentAccount, IdMap, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TemplateDataProvider } from '@hcengineering/templates'
import { getPanelURI, Location } from '@hcengineering/ui'
import { getCurrentLocation, getPanelURI, Location, ResolvedLocation } from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
@ -136,69 +136,51 @@ export async function getContactName (provider: TemplateDataProvider): Promise<s
}
}
export async function getContactLink (doc: Doc): Promise<string> {
const client = getClient()
const hierarchy = client.getHierarchy()
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
label = label ?? 'CONT'
export async function getContactLink (doc: Doc): Promise<Location> {
const loc = getCurrentLocation()
loc.path.length = 2
loc.fragment = undefined
loc.query = undefined
loc.path[2] = contactId
loc.path[3] = doc._id
const id = doc._id
return `${contactId}|${label}-${id}`
return loc
}
function isShortId (shortLink: string): boolean {
return /^\w+-\w+$/.test(shortLink)
function isId (id: Ref<Contact>): boolean {
return /^[0-9a-z]{24}$/.test(id)
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== contactId) {
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== contactId) {
return undefined
}
const shortLink = split[1]
// shortlink
if (isShortId(shortLink)) {
return await generateLocation(loc, shortLink)
const id = loc.path[3] as Ref<Contact>
if (isId(id)) {
return await generateLocation(loc, id)
}
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
}
const classLabel = tokens[0]
const lastId = tokens[1] as Ref<Contact>
async function generateLocation (loc: Location, id: Ref<Contact>): Promise<ResolvedLocation | undefined> {
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = hierarchy.getDescendants(contact.class.Contact)
let _class = contact.class.Contact
for (const clazz of classes) {
if (hierarchy.getClass(clazz).shortLabel === classLabel) {
_class = clazz
break
}
}
const doc = await client.findOne(_class, { _id: lastId })
const doc = await client.findOne(contact.class.Contact, { _id: id })
if (doc === undefined) {
console.error(`Could not find contact ${lastId}.`)
console.error(`Could not find contact ${id}.`)
return undefined
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
return {
path: [appComponent, workspace],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
loc: {
path: [appComponent, workspace],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
},
shouldNavigate: false,
defaultLocation: {
path: [appComponent, workspace, contactId],
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
}
}
}

View File

@ -18,7 +18,7 @@ import { Account, AttachedDoc, Class, Doc, Ref, Space, Timestamp, UXObject } fro
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import type { AnyComponent, IconSize } from '@hcengineering/ui'
import type { AnyComponent, IconSize, ResolvedLocation } from '@hcengineering/ui'
import { FilterMode, ViewAction, Viewlet } from '@hcengineering/view'
/**
@ -250,7 +250,7 @@ export const contactPlugin = plugin(contactId, {
FilterChannelNin: '' as Ref<FilterMode>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
},
templateFieldCategory: {
CurrentEmployee: '' as Ref<TemplateFieldCategory>,

View File

@ -1,7 +1,7 @@
import { Class, Client, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Applicant, recruitId, Review, Vacancy } from '@hcengineering/recruit'
import { getCurrentLocation, getPanelURI, Location } from '@hcengineering/ui'
import { getCurrentLocation, getPanelURI, Location, ResolvedLocation } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import recruit from './plugin'
@ -11,9 +11,9 @@ type RecruitDocument = Vacancy | Applicant | Review
export async function objectLinkProvider (doc: RecruitDocument): Promise<string> {
const location = getCurrentLocation()
return await Promise.resolve(
`${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}#${await getSequenceLink(
doc
)}`
`${window.location.protocol}//${window.location.host}/${workbenchId}/${
location.path[1]
}/${recruitId}/${await getSequenceId(doc)}`
)
}
@ -21,13 +21,12 @@ function isShortId (shortLink: string): boolean {
return /^\w+-\d+$/.test(shortLink)
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
if (split[0] !== recruitId) {
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== recruitId) {
return undefined
}
const shortLink = split[1]
const shortLink = loc.path[3]
// shortlink
if (isShortId(shortLink)) {
@ -37,7 +36,7 @@ export async function resolveLocation (loc: Location): Promise<Location | undefi
return undefined
}
async function generateLocation (loc: Location, shortLink: string): Promise<Location | undefined> {
async function generateLocation (loc: Location, shortLink: string): Promise<ResolvedLocation | undefined> {
const tokens = shortLink.split('-')
if (tokens.length < 2) {
return undefined
@ -68,14 +67,34 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Loca
const targetClass = hierarchy.getClass(_class)
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
const component = panelComponent.component ?? view.component.EditDoc
const defaultPath = [appComponent, workspace, recruitId]
if (_class === recruit.class.Vacancy) {
defaultPath.push('vacancies')
} else if (_class === recruit.class.Applicant) {
defaultPath.push('candidates')
}
return {
path: [appComponent, workspace],
fragment: getPanelURI(component, doc._id, doc._class, 'content')
loc: {
path: [appComponent, workspace],
fragment: getPanelURI(component, doc._id, doc._class, 'content')
},
shouldNavigate: false,
defaultLocation: {
path: defaultPath,
fragment: getPanelURI(component, doc._id, doc._class, 'content')
}
}
}
export async function getSequenceLink (doc: RecruitDocument): Promise<string> {
return `${recruitId}|${await getSequenceId(doc)}`
export async function getSequenceLink (doc: RecruitDocument): Promise<Location> {
const loc = getCurrentLocation()
loc.path.length = 2
loc.fragment = undefined
loc.query = undefined
loc.path[2] = recruitId
loc.path[3] = await getSequenceId(doc)
return loc
}
async function getTitle<T extends RecruitDocument> (

View File

@ -19,7 +19,7 @@ import type { AttachedData, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestam
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@hcengineering/task'
import { AnyComponent } from '@hcengineering/ui'
import { AnyComponent, ResolvedLocation } from '@hcengineering/ui'
import { TagReference } from '@hcengineering/tags'
/**
@ -175,7 +175,7 @@ const recruit = plugin(recruitId, {
Issue: '' as Asset
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
},
space: {
VacancyTemplates: '' as Ref<KanbanTemplateSpace>,

View File

@ -16,7 +16,8 @@
import core, { Class, Doc, Obj, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, getCurrentLocation, Icon, Label, navigate } from '@hcengineering/ui'
import { AnySvelteComponent, location, Icon, Label, navigate } from '@hcengineering/ui'
import { get } from 'svelte/store'
import setting from '../plugin'
import { filterDescendants } from '../utils'
import ClassAttributes from './ClassAttributes.svelte'
@ -32,14 +33,14 @@
| undefined
export let withoutHeader = false
const loc = getCurrentLocation()
const loc = get(location)
const client = getClient()
const hierarchy = client.getHierarchy()
let _class: Ref<Class<Doc>> | undefined = ofClass ?? (loc.query?._class as Ref<Class<Doc>> | undefined)
$: if (_class !== undefined && ofClass === undefined) {
const loc = getCurrentLocation()
const loc = get(location)
loc.query = undefined
navigate(loc)
}

View File

@ -1,7 +1,7 @@
import { Doc, DocumentUpdate, Ref, RelatedDocument, TxOperations } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Issue, Component, Sprint, Project, trackerId } from '@hcengineering/tracker'
import { getCurrentLocation, getPanelURI, Location, navigate } from '@hcengineering/ui'
import { Component, Issue, Project, Sprint, trackerId } from '@hcengineering/tracker'
import { getCurrentLocation, getPanelURI, Location, ResolvedLocation } from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { writable } from 'svelte/store'
import tracker from './plugin'
@ -43,8 +43,15 @@ export async function issueIdProvider (doc: Doc): Promise<string> {
return await getTitle(doc)
}
export async function issueLinkFragmentProvider (doc: Doc): Promise<string> {
return await getTitle(doc).then((p) => `${trackerId}|${p}`)
export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> {
const loc = getCurrentLocation()
loc.path.length = 2
loc.fragment = undefined
loc.query = undefined
loc.path[2] = trackerId
loc.path[3] = await getTitle(doc)
return loc
}
export async function issueTitleProvider (doc: Issue): Promise<string> {
@ -57,10 +64,10 @@ export async function issueLinkProvider (doc: Doc): Promise<string> {
export function generateIssueShortLink (issueId: string): string {
const location = getCurrentLocation()
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${trackerId}#${trackerId}|${issueId}`
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${trackerId}/${issueId}`
}
export async function generateIssueLocation (loc: Location, issueId: string): Promise<Location | undefined> {
export async function generateIssueLocation (loc: Location, issueId: string): Promise<ResolvedLocation | undefined> {
const tokens = issueId.split('-')
if (tokens.length < 2) {
return undefined
@ -83,32 +90,25 @@ export async function generateIssueLocation (loc: Location, issueId: string): Pr
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
return {
path: [appComponent, workspace],
fragment: generateIssuePanelUri(issue)
loc: {
path: [appComponent, workspace],
fragment: generateIssuePanelUri(issue)
},
shouldNavigate: false,
defaultLocation: {
path: [appComponent, workspace, trackerId, project._id, 'issues'],
fragment: generateIssuePanelUri(issue)
}
}
}
function checkOld (loc: Location): void {
const short = loc.path[3]
if (isIssueId(short)) {
loc.fragment = short
loc.path.length = 3
navigate(loc)
}
}
export async function resolveLocation (loc: Location): Promise<Location | undefined> {
const split = loc.fragment?.split('|') ?? []
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
const app = loc.path[2]
if (app !== trackerId && split[0] !== trackerId) {
if (app !== trackerId) {
return undefined
}
const shortLink = split[1] ?? loc.fragment
if (shortLink === undefined || shortLink === null || shortLink.trim() === '') {
checkOld(loc)
return undefined
}
const shortLink = loc.path[3]
// issue shortlink
if (isIssueId(shortLink)) {

View File

@ -16,7 +16,7 @@ import { Client, Doc, Ref, Space } from '@hcengineering/core'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { IssueDraft } from '@hcengineering/tracker'
import { AnyComponent } from '@hcengineering/ui'
import { AnyComponent, Location } from '@hcengineering/ui'
import { SortFunc, Viewlet, ViewQueryAction } from '@hcengineering/view'
import tracker, { trackerId } from '../../tracker/lib'
@ -376,7 +376,7 @@ export default mergeIds(trackerId, tracker, {
IssueTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
IssueStatusSort: '' as SortFunc,
IssuePrioritySort: '' as SortFunc,

View File

@ -18,7 +18,7 @@ import type { AttachedDoc, Class, Doc, Markup, Ref, RelatedDocument, Space, Time
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { TagCategory, TagElement } from '@hcengineering/tags'
import { AnyComponent, Location } from '@hcengineering/ui'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { Action, ActionCategory } from '@hcengineering/view'
import { TagReference } from '@hcengineering/tags'
@ -527,7 +527,7 @@ export default plugin(trackerId, {
DefaultProject: '' as Ref<Project>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<Location | undefined>>
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
},
string: {
NewRelatedIssue: '' as IntlString

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Doc, Hierarchy } from '@hcengineering/core'
import { getClient, NavLink } from '@hcengineering/presentation'
import { AnyComponent, getPanelURI } from '@hcengineering/ui'
import { AnyComponent, getPanelURI, locationToUrl } from '@hcengineering/ui'
import view from '../plugin'
import { getObjectLinkFragment } from '../utils'
@ -37,7 +37,8 @@
href = undefined
return
}
href = `#${await getObjectLinkFragment(hierarchy, object, props, component)}`
const loc = await getObjectLinkFragment(hierarchy, object, props, component)
href = `${window.location.origin}${locationToUrl(loc)}`
}
$: getHref(object)

View File

@ -4,7 +4,6 @@
import ui, {
Button,
closeTooltip,
getCurrentLocation,
IconDownOutline,
IconNavPrev,
IconUpOutline,
@ -27,11 +26,7 @@
const doc = await client.findOne($focusStore.focus._class, { _id: $focusStore.focus._id })
if (doc !== undefined) {
const link = await getObjectLinkFragment(client.getHierarchy(), doc, {}, $panelstore.panel.component)
const location = getCurrentLocation()
if (location.fragment !== link) {
location.fragment = link
navigate(location)
}
navigate(link)
}
}
}

View File

@ -659,7 +659,7 @@ export async function getObjectLinkFragment (
object: Doc,
props: Record<string, any> = {},
component: AnyComponent = view.component.EditDoc
): Promise<string> {
): Promise<Location> {
let clazz = hierarchy.getClass(object._class)
let provider = hierarchy.as(clazz, view.mixin.LinkProvider)
while (provider.encode === undefined && clazz.extends !== undefined) {
@ -673,5 +673,7 @@ export async function getObjectLinkFragment (
return res
}
}
return getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
const loc = getCurrentLocation()
loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
return loc
}

View File

@ -38,7 +38,8 @@ import type {
AnySvelteComponent,
PopupAlignment,
PopupPosAlignment,
Location as PlatformLocation
Location as PlatformLocation,
Location
} from '@hcengineering/ui'
/**
@ -551,7 +552,7 @@ export type OrderOption = [string, SortingOrder]
* @public
*/
export interface LinkProvider extends Class<Doc> {
encode: Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>
encode: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
}
/**

View File

@ -30,7 +30,6 @@
Component,
DatePickerPopup,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
Label,
location,
Location,
@ -41,6 +40,7 @@
PopupAlignment,
PopupPosAlignment,
resizeObserver,
ResolvedLocation,
showPopup,
TooltipInstance
} from '@hcengineering/ui'
@ -48,16 +48,17 @@
import { ActionContext, ActionHandler, migrateViewOpttions } from '@hcengineering/view-resources'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
import { getContext, onDestroy, onMount, tick } from 'svelte'
import { get } from 'svelte/store'
import { subscribeMobile } from '../mobile'
import workbench from '../plugin'
import AccountPopup from './AccountPopup.svelte'
import AppItem from './AppItem.svelte'
import Applications from './Applications.svelte'
import Settings from './icons/Settings.svelte'
import TopMenu from './icons/TopMenu.svelte'
import NavHeader from './NavHeader.svelte'
import Navigator from './Navigator.svelte'
import SpaceView from './SpaceView.svelte'
import Settings from './icons/Settings.svelte'
export let client: Client
let contentPanel: HTMLElement
@ -193,14 +194,13 @@
}
}
async function resolveShortLink (loc: Location): Promise<Location | undefined> {
async function resolveShortLink (loc: Location): Promise<ResolvedLocation | undefined> {
let locationResolver = currentApplication?.locationResolver
if (loc.fragment !== undefined && loc.fragment.trim().length > 0) {
const split = loc.fragment.split('|')
if (loc.path[2] !== undefined && loc.path[2].trim().length > 0) {
if (apps instanceof Promise) {
apps = await apps
}
const app = apps.find((p) => p.alias === split[0])
const app = apps.find((p) => p.alias === loc.path[2])
if (app?.locationResolver) {
locationResolver = app?.locationResolver
}
@ -211,27 +211,54 @@
}
}
async function syncLoc (loc: Location): Promise<void> {
let app = loc.path[2]
let space = loc.path[3] as Ref<Space>
let special = loc.path[4]
let fragment = loc.fragment
function mergeLoc (loc: Location, resolved: ResolvedLocation): Location {
const resolvedApp = resolved.loc.path[2]
const resolvedSpace = resolved.loc.path[3]
const resolvedSpecial = resolved.loc.path[4]
if (resolvedApp === undefined) {
loc.path[2] = (currentAppAlias as string) ?? resolved.defaultLocation.path[2]
loc.path[3] = currentSpace ?? (currentSpecial as string) ?? resolved.defaultLocation.path[3]
if (currentSpace !== undefined) {
loc.path[4] = currentSpecial ?? (asideId as string) ?? resolved.defaultLocation.path[4]
} else {
loc.path.length = 4
}
} else {
loc.path[2] = resolvedApp
if (resolvedSpace === undefined) {
loc.path[3] = currentSpace ?? (currentSpecial as string) ?? resolved.defaultLocation.path[3]
loc.path[4] = (currentSpecial as string) ?? resolved.defaultLocation.path[4]
} else {
loc.path[3] = resolvedSpace
loc.path[4] = resolvedSpecial ?? currentSpecial ?? (asideId as string) ?? resolved.defaultLocation.path[4]
}
}
for (let index = 0; index < loc.path.length; index++) {
const path = loc.path[index]
if (path === undefined) {
loc.path.length = index
break
}
}
loc.fragment = resolved.loc.fragment ?? loc.fragment ?? resolved.defaultLocation.fragment
return loc
}
async function syncLoc (loc: Location): Promise<void> {
const originalLoc = JSON.stringify(loc)
// resolve short links
const resolvedLocation = await resolveShortLink(loc)
if (resolvedLocation && !areLocationsEqual(loc, resolvedLocation)) {
if (app !== resolvedLocation.path[2] && resolvedLocation.path[2] !== undefined) {
loc.path[2] = app = resolvedLocation.path[2] ?? app
loc.path[3] = space = (resolvedLocation.path[3] as Ref<Space>) ?? space
loc.path[4] = special = resolvedLocation.path[4] ?? special
} else if (space !== (resolvedLocation.path[3] as Ref<Space>) && resolvedLocation.path[3] !== undefined) {
loc.path[3] = space = (resolvedLocation.path[3] as Ref<Space>) ?? space
loc.path[4] = special = resolvedLocation.path[4] ?? special
if (resolvedLocation && !areLocationsEqual(loc, resolvedLocation.loc)) {
loc = mergeLoc(loc, resolvedLocation)
if (resolvedLocation.shouldNavigate) {
navigate(loc)
return
}
loc.path[4] = special = resolvedLocation.path[4] ?? special
loc.fragment = fragment = resolvedLocation.fragment ?? fragment
navigate(loc, false)
}
const app = loc.path[2]
let space = loc.path[3] as Ref<Space>
let special = loc.path[4]
const fragment = loc.fragment
if (currentAppAlias !== app) {
clear(1)
@ -240,20 +267,25 @@
navigatorModel = currentApplication?.navigatorModel
}
if (space === undefined) {
if (
space === undefined &&
((navigatorModel?.spaces?.length ?? 0) > 0 || (navigatorModel?.specials?.length ?? 0) > 0)
) {
const last = localStorage.getItem(`platform_last_loc_${app}`)
if (last !== null) {
const newLocation: Location = JSON.parse(last)
if (newLocation.path[3] != null) {
loc.path[3] = newLocation.path[3] as Ref<Space>
loc.path[4] = newLocation.path[4]
space = loc.path[3] = newLocation.path[3] as Ref<Space>
special = loc.path[4] = newLocation.path[4]
if (loc.path[4] == null) {
loc.path.length = 4
} else {
loc.path.length = 5
}
navigate(loc)
return
if (fragment === undefined) {
navigate(loc)
return
}
}
}
}
@ -270,7 +302,7 @@
}
}
if (app !== undefined) {
localStorage.setItem(`platform_last_loc_${app}`, JSON.stringify(loc))
localStorage.setItem(`platform_last_loc_${app}`, originalLoc)
}
if (fragment !== currentFragment) {
currentFragment = fragment
@ -318,7 +350,7 @@
}
function closeAside (): void {
const loc = getCurrentLocation()
const loc = get(location)
loc.path.length = 4
checkOnHide()
navigate(loc)

View File

@ -64,6 +64,7 @@
action: async (_id: Ref<Doc>): Promise<void> => {
const loc = getCurrentLocation()
loc.path[3] = 'spaceBrowser'
loc.path.length = 4
dispatch('open')
navigate(loc)
}

View File

@ -16,7 +16,7 @@
import type { Class, Doc, Mixin, Obj, Ref, Space } from '@hcengineering/core'
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { AnyComponent, Location } from '@hcengineering/ui'
import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui'
import { ViewAction } from '@hcengineering/view'
import type { Preference } from '@hcengineering/preference'
@ -29,7 +29,7 @@ export interface Application extends Doc {
icon: Asset
hidden: boolean
navigatorModel?: NavigatorModel
locationResolver?: Resource<(loc: Location) => Promise<Location | undefined>>
locationResolver?: Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
// Component will be displayed in case navigator model is not defined, or nothing is selected in navigator model
component?: AnyComponent

View File

@ -270,26 +270,13 @@ export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promis
return result
}
async function getContactLink (doc: Doc, control: TriggerControl): Promise<string> {
const hierarchy = control.hierarchy
let clazz = hierarchy.getClass(doc._class)
let label = clazz.shortLabel
while (label === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
label = clazz.shortLabel
}
label = label ?? 'CONT'
return `${contactId}|${label}-${doc._id}`
}
/**
* @public
*/
export async function personHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const person = doc as Person
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const fragment = await getContactLink(doc, control)
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${fragment}`
const path = `${workbenchId}/${control.workspace.name}/${contactId}/${doc._id}`
const link = concatLink(front, path)
return `<a href="${link}">${getName(person)}</a>`
}
@ -308,8 +295,7 @@ export function personTextPresenter (doc: Doc): string {
export async function organizationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const organization = doc as Organization
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const fragment = await getContactLink(doc, control)
const path = `${workbenchId}/${control.workspace.name}/${contactId}#${fragment}`
const path = `${workbenchId}/${control.workspace.name}/${contactId}/${doc._id}`
const link = concatLink(front, path)
return `<a href="${link}">${organization.name}</a>`
}

View File

@ -51,10 +51,7 @@ function getSequenceId (doc: Vacancy | Applicant, control: TriggerControl): stri
export async function vacancyHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const vacancy = doc as Vacancy
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${vacancy._id}/#${recruitId}|${getSequenceId(
vacancy,
control
)}`
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${getSequenceId(vacancy, control)}`
const link = concatLink(front, path)
return `<a href="${link}">${vacancy.name}</a>`
}
@ -74,9 +71,9 @@ export async function applicationHTMLPresenter (doc: Doc, control: TriggerContro
const applicant = doc as Applicant
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const id = getSequenceId(applicant, control)
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${applicant.space}/#${recruitId}|${id}`
const path = `${workbenchId}/${control.workspace.name}/${recruitId}/${id}`
const link = concatLink(front, path)
return `<a href="${link}">id</a>`
return `<a href="${link}">${id}</a>`
}
/**

View File

@ -57,7 +57,7 @@ async function updateSubIssues (
export async function issueHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const issueName = await issueTextPresenter(doc, control)
const front = getMetadata(login.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.name}/${trackerId}/${doc.space}/issues/#${trackerId}|${issueName}`
const path = `${workbenchId}/${control.workspace.name}/${trackerId}/${issueName}`
const link = concatLink(front, path)
return `<a href="${link}">${issueName}</a>`
}