UBERF-7239: Support short/custom links in inbox/chat/planner (#5815)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-06-18 16:21:51 +04:00 committed by GitHub
parent 61a0833ec4
commit 1e9fea356e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 630 additions and 244 deletions

View File

@ -301,6 +301,11 @@ function defineDocument (builder: Builder): void {
encode: document.function.GetObjectLinkFragment encode: document.function.GetObjectLinkFragment
}) })
builder.mixin(document.class.Document, core.class.Class, view.mixin.LinkIdProvider, {
encode: document.function.GetDocumentLinkId,
decode: document.function.ParseDocumentId
})
builder.mixin(document.class.Document, core.class.Class, view.mixin.ObjectIcon, { builder.mixin(document.class.Document, core.class.Class, view.mixin.ObjectIcon, {
component: document.component.DocumentIcon component: document.component.DocumentIcon
}) })

View File

@ -932,6 +932,26 @@ export function createModel (builder: Builder): void {
encode: recruit.function.GetIdObjectLinkFragment encode: recruit.function.GetIdObjectLinkFragment
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.LinkIdProvider, {
encode: recruit.function.IdProvider,
decode: recruit.function.ParseLinkId
})
builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.LinkIdProvider, {
encode: recruit.function.IdProvider,
decode: recruit.function.ParseLinkId
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.LinkIdProvider, {
encode: recruit.function.IdProvider,
decode: recruit.function.ParseLinkId
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.LinkIdProvider, {
encode: recruit.function.IdProvider,
decode: recruit.function.ParseLinkId
})
builder.createDoc( builder.createDoc(
view.class.ActionCategory, view.class.ActionCategory,
core.space.Model, core.space.Model,

View File

@ -58,7 +58,8 @@ export default mergeIds(recruitId, recruit, {
GetTalentId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetTalentId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
HideDoneState: '' as ViewQueryAction, HideDoneState: '' as ViewQueryAction,
HideArchivedVacancies: '' as ViewQueryAction, HideArchivedVacancies: '' as ViewQueryAction,
ApplicantHasEmail: '' as Resource<ViewActionAvailabilityFunction> ApplicantHasEmail: '' as Resource<ViewActionAvailabilityFunction>,
ParseLinkId: '' as Resource<(id: string) => Promise<Ref<Doc> | undefined>>
}, },
string: { string: {
ApplicationsShort: '' as IntlString, ApplicationsShort: '' as IntlString,

View File

@ -35,6 +35,7 @@
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/model-core": "^0.6.0", "@hcengineering/model-core": "^0.6.0",
"@hcengineering/document": "^0.6.0", "@hcengineering/document": "^0.6.0",
"@hcengineering/server-document": "^0.6.0" "@hcengineering/server-document": "^0.6.0",
"@hcengineering/server-view": "^0.6.0"
} }
} }

View File

@ -10,6 +10,7 @@ import document from '@hcengineering/document'
import serverCore from '@hcengineering/server-core' import serverCore from '@hcengineering/server-core'
import serverDocument from '@hcengineering/server-document' import serverDocument from '@hcengineering/server-document'
import serverNotification from '@hcengineering/server-notification' import serverNotification from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
export { serverDocumentId } from '@hcengineering/server-document' export { serverDocumentId } from '@hcengineering/server-document'
@ -22,6 +23,10 @@ export function createModel (builder: Builder): void {
presenter: serverDocument.function.DocumentTextPresenter presenter: serverDocument.function.DocumentTextPresenter
}) })
builder.mixin(document.class.Document, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverDocument.function.DocumentLinkIdProvider
})
builder.mixin(document.class.Document, core.class.Class, serverCore.mixin.SearchPresenter, { builder.mixin(document.class.Document, core.class.Class, serverCore.mixin.SearchPresenter, {
searchConfig: { searchConfig: {
iconConfig: { iconConfig: {

View File

@ -37,6 +37,7 @@
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/model-recruit": "^0.6.0", "@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/notification": "^0.6.23", "@hcengineering/notification": "^0.6.23",
"@hcengineering/server-notification": "^0.6.1" "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-view": "^0.6.0"
} }
} }

View File

@ -23,6 +23,7 @@ import serverNotification from '@hcengineering/server-notification'
import serverRecruit from '@hcengineering/server-recruit' import serverRecruit from '@hcengineering/server-recruit'
import serverContact from '@hcengineering/server-contact' import serverContact from '@hcengineering/server-contact'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import serverView from '@hcengineering/server-view'
export { serverRecruitId } from '@hcengineering/server-recruit' export { serverRecruitId } from '@hcengineering/server-recruit'
@ -43,6 +44,22 @@ export function createModel (builder: Builder): void {
presenter: serverRecruit.function.VacancyTextPresenter presenter: serverRecruit.function.VacancyTextPresenter
}) })
builder.mixin(recruit.class.Applicant, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverRecruit.function.LinkIdProvider
})
builder.mixin(recruit.class.Opinion, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverRecruit.function.LinkIdProvider
})
builder.mixin(recruit.class.Review, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverRecruit.function.LinkIdProvider
})
builder.mixin(recruit.class.Vacancy, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverRecruit.function.LinkIdProvider
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverRecruit.trigger.OnRecruitUpdate trigger: serverRecruit.trigger.OnRecruitUpdate
}) })

View File

@ -36,6 +36,8 @@
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/model-tracker": "^0.6.0", "@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/server-tracker": "^0.6.0", "@hcengineering/server-tracker": "^0.6.0",
"@hcengineering/contact": "^0.6.24" "@hcengineering/contact": "^0.6.24",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/server-view": "^0.6.0"
} }
} }

View File

@ -21,6 +21,7 @@ import serverCore from '@hcengineering/server-core'
import serverNotification from '@hcengineering/server-notification' import serverNotification from '@hcengineering/server-notification'
import serverTracker from '@hcengineering/server-tracker' import serverTracker from '@hcengineering/server-tracker'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import serverView from '@hcengineering/server-view'
export { serverTrackerId } from '@hcengineering/server-tracker' export { serverTrackerId } from '@hcengineering/server-tracker'
@ -37,6 +38,10 @@ export function createModel (builder: Builder): void {
presenter: serverTracker.function.IssueNotificationContentProvider presenter: serverTracker.function.IssueNotificationContentProvider
}) })
builder.mixin(tracker.class.Issue, core.class.Class, serverView.mixin.ServerLinkIdProvider, {
encode: serverTracker.function.IssueLinkIdProvider
})
builder.mixin(tracker.class.Issue, core.class.Class, serverCore.mixin.SearchPresenter, { builder.mixin(tracker.class.Issue, core.class.Class, serverCore.mixin.SearchPresenter, {
searchConfig: { searchConfig: {
iconConfig: { iconConfig: {

View File

@ -30,8 +30,9 @@
"dependencies": { "dependencies": {
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11", "@hcengineering/model": "^0.6.11",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-view": "^0.6.0", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-core": "^0.6.1" "@hcengineering/server-view": "^0.6.0"
} }
} }

View File

@ -13,14 +13,23 @@
// limitations under the License. // limitations under the License.
// //
import core from '@hcengineering/core' import core, { type Doc } from '@hcengineering/core'
import { type Builder } from '@hcengineering/model' import { type Builder, Mixin } from '@hcengineering/model'
import serverCore from '@hcengineering/server-core' import serverCore, { type TriggerControl } from '@hcengineering/server-core'
import serverView from '@hcengineering/server-view' import serverView, { type ServerLinkIdProvider } from '@hcengineering/server-view'
import { TClass } from '@hcengineering/model-core'
import { type Resource } from '@hcengineering/platform'
export { serverViewId } from '@hcengineering/server-view' export { serverViewId } from '@hcengineering/server-view'
@Mixin(serverView.mixin.ServerLinkIdProvider, core.class.Class)
export class TServerLinkIdProvider extends TClass implements ServerLinkIdProvider {
encode!: Resource<(doc: Doc, control: TriggerControl) => Promise<string>>
}
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TServerLinkIdProvider)
builder.createDoc(serverCore.class.Trigger, core.space.Model, { builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverView.trigger.OnCustomAttributeRemove trigger: serverView.trigger.OnCustomAttributeRemove
}) })

View File

@ -438,6 +438,11 @@ export function createModel (builder: Builder): void {
builder.mixin(tracker.class.Component, core.class.Class, activity.mixin.ActivityDoc, {}) builder.mixin(tracker.class.Component, core.class.Class, activity.mixin.ActivityDoc, {})
builder.mixin(tracker.class.IssueTemplate, core.class.Class, activity.mixin.ActivityDoc, {}) builder.mixin(tracker.class.IssueTemplate, core.class.Class, activity.mixin.ActivityDoc, {})
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.LinkIdProvider, {
encode: tracker.function.GetIssueId,
decode: tracker.function.GetIssueIdByIdentifier
})
builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, { builder.createDoc(activity.class.ActivityMessageControl, core.space.Model, {
objectClass: tracker.class.Issue, objectClass: tracker.class.Issue,
skip: [ skip: [

View File

@ -90,7 +90,8 @@ import {
type ObjectIcon, type ObjectIcon,
type ObjectTooltip, type ObjectTooltip,
type AttrPresenter, type AttrPresenter,
type AttributeCategory type AttributeCategory,
type LinkIdProvider
} from '@hcengineering/view' } from '@hcengineering/view'
import view from './plugin' import view from './plugin'
@ -352,6 +353,12 @@ export class TLinkProvider extends TClass implements LinkProvider {
encode!: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>> encode!: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
} }
@Mixin(view.mixin.LinkIdProvider, core.class.Class)
export class TLinkIdProvider extends TClass implements LinkIdProvider {
encode!: Resource<(doc: Doc) => Promise<string>>
decode!: Resource<(id: string) => Promise<Ref<Doc> | undefined>>
}
@Mixin(view.mixin.ObjectPanel, core.class.Class) @Mixin(view.mixin.ObjectPanel, core.class.Class)
export class TObjectPanel extends TClass implements ObjectPanel { export class TObjectPanel extends TClass implements ObjectPanel {
component!: AnyComponent component!: AnyComponent
@ -450,7 +457,8 @@ export function createModel (builder: Builder): void {
TObjectIdentifier, TObjectIdentifier,
TObjectTooltip, TObjectTooltip,
TObjectIcon, TObjectIcon,
TAttrPresenter TAttrPresenter,
TLinkIdProvider
) )
classPresenter( classPresenter(

View File

@ -53,12 +53,12 @@ export function openPanel (
}) })
} }
export function closePanel (shoulRedirect: boolean = true): void { export function closePanel (shouldRedirect: boolean = true): void {
currentLocation = undefined currentLocation = undefined
panelstore.update(() => { panelstore.update(() => {
return { panel: undefined } return { panel: undefined }
}) })
if (shoulRedirect) { if (shouldRedirect) {
const loc = getLocation() const loc = getLocation()
loc.fragment = undefined loc.fragment = undefined
navigate(loc) navigate(loc)

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Doc, Ref, Class } from '@hcengineering/core' import { Doc, Ref, Class } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { import {
Component, Component,
defineSeparators, defineSeparators,
@ -26,29 +26,32 @@
restoreLocation, restoreLocation,
deviceOptionsStore as deviceInfo deviceOptionsStore as deviceInfo
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench' import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources' import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { chunterId } from '@hcengineering/chunter' import { chunterId } from '@hcengineering/chunter'
import { ActivityMessage } from '@hcengineering/activity' import view, { decodeObjectURI } from '@hcengineering/view'
import { parseLinkId, getObjectLinkId } from '@hcengineering/view-resources'
import ChatNavigator from './navigator/ChatNavigator.svelte' import ChatNavigator from './navigator/ChatNavigator.svelte'
import ChannelView from '../ChannelView.svelte' import ChannelView from '../ChannelView.svelte'
import { chatSpecials, loadSavedAttachments } from './utils' import { chatSpecials, loadSavedAttachments } from './utils'
import { SelectChannelEvent } from './types' import { SelectChannelEvent } from './types'
import { decodeChannelURI, openChannel } from '../../navigation' import { openChannel } from '../../navigation'
const notificationsClient = InboxNotificationsClientImpl.getClient() const notificationsClient = InboxNotificationsClientImpl.getClient()
const contextByDocStore = notificationsClient.contextByDoc const contextByDocStore = notificationsClient.contextByDoc
const objectQuery = createQuery() const objectQuery = createQuery()
const client = getClient()
const navigatorModel: NavigatorModel = { const navigatorModel: NavigatorModel = {
spaces: [], spaces: [],
specials: chatSpecials specials: chatSpecials
} }
let selectedData: { _id: Ref<Doc>, _class: Ref<Class<Doc>> } | undefined = undefined const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let selectedData: { id: string, _class: Ref<Class<Doc>> } | undefined = undefined
let currentSpecial: SpecialNavModel | undefined let currentSpecial: SpecialNavModel | undefined
@ -58,10 +61,18 @@
syncLocation(loc) syncLocation(loc)
}) })
$: void loadObject(selectedData?._id, selectedData?._class) $: void loadObject(selectedData?.id, selectedData?._class)
async function loadObject (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): Promise<void> { async function loadObject (id?: string, _class?: Ref<Class<Doc>>): Promise<void> {
if (_id == null || _class == null || _class === '') { if (id == null || _class == null || _class === '') {
object = undefined
objectQuery.unsubscribe()
return
}
const _id: Ref<Doc> | undefined = await parseLinkId(linkProviders, id, _class)
if (_id === undefined) {
object = undefined object = undefined
objectQuery.unsubscribe() objectQuery.unsubscribe()
return return
@ -84,7 +95,7 @@
const id = loc.path[3] const id = loc.path[3]
if (!id) { if (id == null || id === '') {
currentSpecial = undefined currentSpecial = undefined
selectedData = undefined selectedData = undefined
object = undefined object = undefined
@ -98,26 +109,30 @@
selectedData = undefined selectedData = undefined
object = undefined object = undefined
} else { } else {
const [_id, _class] = decodeChannelURI(loc.path[3]) const [id, _class] = decodeObjectURI(loc.path[3])
selectedData = { _id, _class } selectedData = { id, _class }
} }
} }
function handleChannelSelected (event: CustomEvent): void { async function handleChannelSelected (event: CustomEvent): Promise<void> {
if (event.detail === null) { if (event.detail === null) {
selectedData = undefined selectedData = undefined
return return
} }
const detail = (event.detail ?? {}) as SelectChannelEvent const detail = (event.detail ?? {}) as SelectChannelEvent
const _class = detail.object._class
const _id = detail.object._id
selectedData = { _id: detail.object._id, _class: detail.object._class } const id = await getObjectLinkId(linkProviders, _id, _class, detail.object)
if (selectedData._id !== object?._id) { selectedData = { id, _class }
if (_id !== object?._id) {
object = detail.object object = detail.object
} }
openChannel(selectedData._id, selectedData._class) openChannel(selectedData.id, selectedData._class)
} }
defineSeparators('chat', [ defineSeparators('chat', [
@ -134,7 +149,7 @@
{#if $deviceInfo.navigator.visible} {#if $deviceInfo.navigator.visible}
<div class="antiPanel-navigator {$deviceInfo.navigator.direction === 'horizontal' ? 'portrait' : 'landscape'}"> <div class="antiPanel-navigator {$deviceInfo.navigator.direction === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content hulyNavPanel-container"> <div class="antiPanel-wrap__content hulyNavPanel-container">
<ChatNavigator objectId={selectedData?._id} {object} {currentSpecial} on:select={handleChannelSelected} /> <ChatNavigator {object} {currentSpecial} on:select={handleChannelSelected} />
</div> </div>
<Separator name="chat" float={$deviceInfo.navigator.float ? 'navigator' : true} index={0} /> <Separator name="chat" float={$deviceInfo.navigator.float ? 'navigator' : true} index={0} />
</div> </div>

View File

@ -25,7 +25,6 @@
import ChatNavSection from './ChatNavSection.svelte' import ChatNavSection from './ChatNavSection.svelte'
import chunter from '../../../plugin' import chunter from '../../../plugin'
export let objectId: Ref<Doc> | undefined
export let object: Doc | undefined export let object: Doc | undefined
export let model: ChatNavGroupModel export let model: ChatNavGroupModel
@ -191,7 +190,7 @@
id={section.id} id={section.id}
objects={section.objects} objects={section.objects}
{contexts} {contexts}
{objectId} objectId={object?._id}
header={section.label} header={section.label}
actions={getSectionActions(section, contexts)} actions={getSectionActions(section, contexts)}
sortFn={model.sortFn} sortFn={model.sortFn}

View File

@ -17,7 +17,7 @@
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui' import { Action, IconEdit } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources' import { getActions, getObjectLinkId } from '@hcengineering/view-resources'
import { import {
getNotificationsCount, getNotificationsCount,
InboxNotificationsClientImpl, InboxNotificationsClientImpl,
@ -68,6 +68,8 @@
actions = res actions = res
}) })
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
async function getChannelActions (context: DocNotifyContext | undefined, object: Doc): Promise<Action[]> { async function getChannelActions (context: DocNotifyContext | undefined, object: Doc): Promise<Action[]> {
const result: Action[] = [] const result: Action[] = []
@ -79,7 +81,8 @@
icon: view.icon.Open, icon: view.icon.Open,
label: view.string.Open, label: view.string.Open,
action: async () => { action: async () => {
openChannel(object._id, object._class) const id = await getObjectLinkId(linkProviders, object._id, object._class, object)
openChannel(id, object._class)
} }
}) })

View File

@ -29,7 +29,6 @@
import { userSearch } from '../../../index' import { userSearch } from '../../../index'
import { navigateToSpecial } from '../../../navigation' import { navigateToSpecial } from '../../../navigation'
export let objectId: Ref<Doc> | undefined
export let object: Doc | undefined export let object: Doc | undefined
export let currentSpecial: SpecialNavModel | undefined export let currentSpecial: SpecialNavModel | undefined
@ -112,7 +111,7 @@
</div> </div>
<Scroller shrink> <Scroller shrink>
{#each chatNavGroupModels as model} {#each chatNavGroupModels as model}
<ChatNavGroup {object} {objectId} {model} on:select /> <ChatNavGroup {object} {model} on:select />
{/each} {/each}
</Scroller> </Scroller>
<NavFooter /> <NavFooter />

View File

@ -4,22 +4,16 @@ import type { ActivityMessage } from '@hcengineering/activity'
import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter' import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter'
import { notificationId } from '@hcengineering/notification' import { notificationId } from '@hcengineering/notification'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import { getObjectLinkId } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import view, { encodeObjectURI, decodeObjectURI } from '@hcengineering/view'
import { chatSpecials } from './components/chat/utils' import { chatSpecials } from './components/chat/utils'
import { isThreadMessage } from './utils' import { isThreadMessage } from './utils'
export function decodeChannelURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] { export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
return decodeURIComponent(value).split('|') as [Ref<Doc>, Ref<Class<Doc>>]
}
function encodeChannelURI (_id: Ref<Doc>, _class: Ref<Class<Doc>>): string {
return [_id, _class].join('|')
}
export function openChannel (_id: Ref<Doc>, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
const loc = getCurrentLocation() const loc = getCurrentLocation()
const id = encodeObjectURI(_id, _class)
const id = encodeChannelURI(_id, _class)
if (loc.path[3] === id) { if (loc.path[3] === id) {
return return
@ -45,12 +39,18 @@ export async function openMessageFromSpecial (message?: ActivityMessage): Promis
} }
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
const client = getClient()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
if (isThreadMessage(message)) { if (isThreadMessage(message)) {
loc.path[3] = encodeChannelURI(message.objectId, message.objectClass) const id = await getObjectLinkId(providers, message.objectId, message.objectClass)
loc.path[3] = encodeObjectURI(id, message.objectClass)
loc.path[4] = message.attachedTo loc.path[4] = message.attachedTo
} else { } else {
loc.path[3] = encodeChannelURI(message.attachedTo, message.attachedToClass) const id = await getObjectLinkId(providers, message.attachedTo, message.attachedToClass)
loc.path[3] = encodeObjectURI(id, message.attachedToClass)
} }
loc.query = { ...loc.query, message: message._id } loc.query = { ...loc.query, message: message._id }
@ -66,47 +66,58 @@ export function navigateToSpecial (specialId: string): void {
} }
export async function getMessageLink (message: ActivityMessage): Promise<string> { export async function getMessageLink (message: ActivityMessage): Promise<string> {
const client = getClient()
const location = getCurrentResolvedLocation() const location = getCurrentResolvedLocation()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let threadParent = '' let threadParent = ''
let _id: Ref<Doc> let _id: string
let _class: Ref<Class<Doc>> let _class: Ref<Class<Doc>>
if (isThreadMessage(message)) { if (isThreadMessage(message)) {
threadParent = `/${message.attachedTo}` threadParent = `/${message.attachedTo}`
_id = message.objectId _id = await getObjectLinkId(providers, message.objectId, message.objectClass)
_class = message.objectClass _class = message.objectClass
} else { } else {
_id = message.attachedTo _id = await getObjectLinkId(providers, message.attachedTo, message.attachedToClass)
_class = message.attachedToClass _class = message.attachedToClass
} }
const id = encodeChannelURI(_id, _class) const id = encodeObjectURI(_id, _class)
return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}/${id}${threadParent}?message=${message._id}` return `${window.location.protocol}//${window.location.host}/${workbenchId}/${location.path[1]}/${chunterId}/${id}${threadParent}?message=${message._id}`
} }
export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Promise<Location> { export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Promise<Location> {
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
const client = getClient()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const id = await getObjectLinkId(providers, doc._id, doc._class, doc)
loc.path.length = 2 loc.path.length = 2
loc.fragment = undefined loc.fragment = undefined
loc.query = undefined loc.query = undefined
loc.path[2] = chunterId loc.path[2] = chunterId
loc.path[3] = encodeChannelURI(doc._id, doc._class) loc.path[3] = encodeObjectURI(id, doc._class)
return loc return loc
} }
export function buildThreadLink ( export async function buildThreadLink (
loc: Location, loc: Location,
_id: Ref<Doc>, _id: Ref<Doc>,
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
threadParent: Ref<ActivityMessage> threadParent: Ref<ActivityMessage>,
): Location { doc?: Doc
): Promise<Location> {
const client = getClient()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const id = await getObjectLinkId(providers, _id, _class, doc)
const specials = chatSpecials.map(({ id }) => id) const specials = chatSpecials.map(({ id }) => id)
const id = encodeChannelURI(_id, _class) const objectURI = encodeObjectURI(id, _class)
const isSameChannel = loc.path[3] === id const isSameChannel = loc.path[3] === objectURI
if (!isSameChannel) { if (!isSameChannel) {
loc.query = { message: threadParent } loc.query = { message: threadParent }
@ -121,7 +132,7 @@ export function buildThreadLink (
loc.path[2] = chunterId loc.path[2] = chunterId
} }
loc.path[3] = id loc.path[3] = objectURI
loc.path[4] = threadParent loc.path[4] = threadParent
loc.fragment = undefined loc.fragment = undefined
@ -131,7 +142,7 @@ export function buildThreadLink (
export async function getThreadLink (doc: ThreadMessage): Promise<Location> { export async function getThreadLink (doc: ThreadMessage): Promise<Location> {
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
return buildThreadLink(loc, doc.objectId, doc.objectClass, doc.attachedTo) return await buildThreadLink(loc, doc.objectId, doc.objectClass, doc.attachedTo, doc)
} }
export async function replyToThread (message: ActivityMessage): Promise<void> { export async function replyToThread (message: ActivityMessage): Promise<void> {
@ -141,11 +152,37 @@ export async function replyToThread (message: ActivityMessage): Promise<void> {
loc.path[2] = chunterId loc.path[2] = chunterId
} }
navigate(buildThreadLink(loc, message.attachedTo, message.attachedToClass, message._id)) const newLoc = await buildThreadLink(loc, message.attachedTo, message.attachedToClass, message._id)
navigate(newLoc)
} }
export async function getMessageLocation (doc: ActivityMessage): Promise<Location> { export async function getMessageLocation (doc: ActivityMessage): Promise<Location> {
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
return buildThreadLink(loc, doc.attachedTo, doc.attachedToClass, doc._id) return await buildThreadLink(loc, doc.attachedTo, doc.attachedToClass, doc._id)
}
export async function resetChunterLocIfEqual (_id: Ref<Doc>, _class: Ref<Class<Doc>>, doc?: Doc): Promise<void> {
const loc = getCurrentLocation()
if (loc.path[2] !== chunterId) {
return
}
const client = getClient()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const id = await getObjectLinkId(providers, _id, _class, doc)
const [locId] = decodeObjectURI(loc.path[3])
if (locId !== id) {
return
}
loc.path[3] = ''
loc.path[4] = ''
loc.query = {}
loc.path.length = 3
navigate(loc)
} }

View File

@ -12,13 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
type Channel,
type ChatMessage,
chunterId,
type DirectMessage,
type ThreadMessage
} from '@hcengineering/chunter'
import contact, { type Employee, getName, type Person, type PersonAccount } from '@hcengineering/contact' import contact, { type Employee, getName, type Person, type PersonAccount } from '@hcengineering/contact'
import { employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources' import { employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources'
import { import {
@ -34,7 +28,7 @@ import {
type Timestamp type Timestamp
} from '@hcengineering/core' } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent, getCurrentLocation, navigate } from '@hcengineering/ui' import { type AnySvelteComponent } from '@hcengineering/ui'
import { type Asset, translate } from '@hcengineering/platform' import { type Asset, translate } from '@hcengineering/platform'
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources' import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import activity, { import activity, {
@ -55,7 +49,7 @@ import { get, type Unsubscriber } from 'svelte/store'
import chunter from './plugin' import chunter from './plugin'
import DirectIcon from './components/DirectIcon.svelte' import DirectIcon from './components/DirectIcon.svelte'
import ChannelIcon from './components/ChannelIcon.svelte' import ChannelIcon from './components/ChannelIcon.svelte'
import { decodeChannelURI } from './navigation' import { resetChunterLocIfEqual } from './navigation'
export async function getDmName (client: Client, space?: Space): Promise<string> { export async function getDmName (client: Client, space?: Space): Promise<string> {
if (space === undefined) { if (space === undefined) {
@ -350,7 +344,7 @@ export async function leaveChannel (channel: Channel, value: Ref<Account> | Arra
const context = await client.findOne(notification.class.DocNotifyContext, { attachedTo: channel._id }) const context = await client.findOne(notification.class.DocNotifyContext, { attachedTo: channel._id })
await client.update(channel, { $pull: { members: value } }) await client.update(channel, { $pull: { members: value } })
await removeChannelAction(context) await removeChannelAction(context, undefined, { object: channel })
} }
} }
@ -402,37 +396,31 @@ export async function readChannelMessages (
} }
} }
function resetChunterLoc (objectId: Ref<Doc>): void { export async function leaveChannelAction (
const loc = getCurrentLocation() context?: DocNotifyContext,
const [_id] = decodeChannelURI(loc.path[3]) _?: Event,
props?: { object?: Channel }
if (loc.path[2] !== chunterId || _id !== objectId) { ): Promise<void> {
return
}
loc.path[3] = ''
loc.path[4] = ''
loc.query = {}
loc.path.length = 3
navigate(loc)
}
export async function leaveChannelAction (context?: DocNotifyContext): Promise<void> {
if (context === undefined) { if (context === undefined) {
return return
} }
const client = getClient() const client = getClient()
const channel = await client.findOne(chunter.class.Channel, { _id: context.attachedTo as Ref<Channel> }) const channel =
props?.object ?? (await client.findOne(chunter.class.Channel, { _id: context.attachedTo as Ref<Channel> }))
if (channel === undefined) { if (channel === undefined) {
return return
} }
await leaveChannel(channel, getCurrentAccount()._id) await leaveChannel(channel, getCurrentAccount()._id)
resetChunterLoc(channel._id) await resetChunterLocIfEqual(channel._id, channel._class, channel)
} }
export async function removeChannelAction (context?: DocNotifyContext): Promise<void> { export async function removeChannelAction (
context?: DocNotifyContext,
_?: Event,
props?: { object?: Doc }
): Promise<void> {
if (context === undefined) { if (context === undefined) {
return return
} }
@ -442,7 +430,7 @@ export async function removeChannelAction (context?: DocNotifyContext): Promise<
await archiveContextNotifications(context) await archiveContextNotifications(context)
await client.remove(context) await client.remove(context)
resetChunterLoc(context.attachedTo) await resetChunterLocIfEqual(context.attachedTo, context.attachedToClass, props?.object)
} }
export function isThreadMessage (message: ActivityMessage): message is ThreadMessage { export function isThreadMessage (message: ActivityMessage): message is ThreadMessage {

View File

@ -46,7 +46,15 @@ import TeamspaceSpacePresenter from './components/navigator/TeamspaceSpacePresen
import CreateTeamspace from './components/teamspace/CreateTeamspace.svelte' import CreateTeamspace from './components/teamspace/CreateTeamspace.svelte'
import document from './plugin' import document from './plugin'
import { createEmptyDocument, documentTitleProvider, getDocumentLink, getDocumentUrl, resolveLocation } from './utils' import {
createEmptyDocument,
documentTitleProvider,
getDocumentLink,
getDocumentLinkId,
getDocumentUrl,
parseDocumentId,
resolveLocation
} from './utils'
const toObjectSearchResult = (e: WithLookup<Document>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Document>): ObjectSearchResult => ({
doc: e, doc: e,
@ -187,7 +195,9 @@ export default async (): Promise<Resources> => ({
GetObjectLinkFragment: getDocumentLink, GetObjectLinkFragment: getDocumentLink,
DocumentTitleProvider: documentTitleProvider, DocumentTitleProvider: documentTitleProvider,
CanLockDocument: canLockDocument, CanLockDocument: canLockDocument,
CanUnlockDocument: canUnlockDocument CanUnlockDocument: canUnlockDocument,
GetDocumentLinkId: getDocumentLinkId,
ParseDocumentId: parseDocumentId
}, },
resolver: { resolver: {
Location: resolveLocation Location: resolveLocation

View File

@ -14,7 +14,7 @@
// //
import { type Client, type Doc, type Ref } from '@hcengineering/core' import { type Client, type Doc, type Ref } from '@hcengineering/core'
import document, { documentId } from '@hcengineering/document' import document, { type Document, documentId } from '@hcengineering/document'
import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform' import { mergeIds, type IntlString, type Resource } from '@hcengineering/platform'
import { type AnyComponent, type Location } from '@hcengineering/ui' import { type AnyComponent, type Location } from '@hcengineering/ui'
@ -29,7 +29,9 @@ export default mergeIds(documentId, document, {
function: { function: {
DocumentTitleProvider: '' as Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>, DocumentTitleProvider: '' as Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>,
GetDocumentLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetDocumentLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>> GetObjectLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetDocumentLinkId: '' as Resource<(doc: Doc) => Promise<string>>,
ParseDocumentId: '' as Resource<(id: string) => Promise<Ref<Document> | undefined>>
}, },
string: { string: {
DocumentNamePlaceholder: '' as IntlString, DocumentNamePlaceholder: '' as IntlString,

View File

@ -105,12 +105,17 @@ export async function generateLocation (loc: Location, id: Ref<Document>): Promi
} }
export function getDocumentIdFromFragment (fragment: string): Ref<Document> | undefined { export function getDocumentIdFromFragment (fragment: string): Ref<Document> | undefined {
const [, _id] = decodeURIComponent(fragment).split('|') const [, id] = decodeURIComponent(fragment).split('|')
return _id as Ref<Document>
if (id == null) {
return undefined
}
return (parseDocumentId(id) ?? id) as Ref<Document>
} }
export function getDocumentUrl (doc: Document): string { export function getDocumentUrl (doc: Document): string {
const id = getDocumentId(doc) const id = getDocumentLinkId(doc)
const location = getCurrentResolvedLocation() const location = getCurrentResolvedLocation()
const frontUrl = getMetadata(presentation.metadata.FrontUrl) const frontUrl = getMetadata(presentation.metadata.FrontUrl)
@ -124,17 +129,17 @@ export function getDocumentLink (doc: Document): Location {
loc.fragment = undefined loc.fragment = undefined
loc.query = undefined loc.query = undefined
loc.path[2] = documentId loc.path[2] = documentId
loc.path[3] = getDocumentId(doc) loc.path[3] = getDocumentLinkId(doc)
return loc return loc
} }
function getDocumentId (doc: Document): string { export function getDocumentLinkId (doc: Document): string {
const slug = slugify(doc.name, { lower: true }) const slug = slugify(doc.name, { lower: true })
return `${slug}-${doc._id}` return `${slug}-${doc._id}`
} }
function parseDocumentId (shortLink: string): Ref<Document> | undefined { export function parseDocumentId (shortLink: string): Ref<Document> | undefined {
const parts = shortLink.split('-') const parts = shortLink.split('-')
if (parts.length > 1) { if (parts.length > 1) {
return parts[parts.length - 1] as Ref<Document> return parts[parts.length - 1] as Ref<Document>

View File

@ -39,7 +39,7 @@
deviceOptionsStore as deviceInfo deviceOptionsStore as deviceInfo
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ListSelectionProvider, restrictionStore, updateFocus } from '@hcengineering/view-resources' import { ListSelectionProvider, parseLinkId, restrictionStore, updateFocus } from '@hcengineering/view-resources'
import workbench, { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench' import workbench, { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
import { SpaceView, buildNavModel } from '@hcengineering/workbench-resources' import { SpaceView, buildNavModel } from '@hcengineering/workbench-resources'
import guest from '../plugin' import guest from '../plugin'
@ -189,11 +189,17 @@
} }
} }
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
async function setOpenPanelFocus (fragment: string): Promise<void> { async function setOpenPanelFocus (fragment: string): Promise<void> {
const props = decodeURIComponent(fragment).split('|') const props = decodeURIComponent(fragment).split('|')
if (props.length >= 3) { if (props.length >= 3) {
const doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> }) const id = props[1]
const _class = props[2] as Ref<Class<Doc>>
const _id = await parseLinkId(linkProviders, id, _class)
const doc = await client.findOne<Doc>(_class, { _id })
if (doc !== undefined) { if (doc !== undefined) {
await checkAccess(doc) await checkAccess(doc)
const provider = ListSelectionProvider.Find(doc._id) const provider = ListSelectionProvider.Find(doc._id)
@ -203,8 +209,8 @@
}) })
openPanel( openPanel(
props[0] as AnyComponent, props[0] as AnyComponent,
props[1], _id,
props[2], _class,
(props[3] ?? undefined) as PopupAlignment, (props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent (props[4] ?? undefined) as AnyComponent
) )
@ -253,14 +259,15 @@
async function getWindowTitle (loc: Location): Promise<string | undefined> { async function getWindowTitle (loc: Location): Promise<string | undefined> {
if (loc.fragment == null) return if (loc.fragment == null) return
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const [, _id, _class] = decodeURIComponent(loc.fragment).split('|') const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
if (_class == null) return if (_class == null) return
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle) const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
if (mixin === undefined) return if (mixin === undefined) return
const titleProvider = await getResource(mixin.titleProvider) const titleProvider = await getResource(mixin.titleProvider)
try { try {
return await titleProvider(client, _id as Ref<Doc>) const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
return await titleProvider(client, _id)
} catch (err: any) { } catch (err: any) {
Analytics.handleError(err) Analytics.handleError(err)
console.error(err) console.error(err)

View File

@ -13,15 +13,9 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
ActivityInboxNotification,
decodeObjectURI,
DocNotifyContext,
InboxNotification,
notificationId
} from '@hcengineering/notification'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation' import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import view from '@hcengineering/view' import view, { decodeObjectURI } from '@hcengineering/view'
import { import {
AnyComponent, AnyComponent,
Component, Component,
@ -36,16 +30,16 @@
TabList, TabList,
deviceOptionsStore as deviceInfo deviceOptionsStore as deviceInfo
} from '@hcengineering/ui' } from '@hcengineering/ui'
import chunter, { ThreadMessage } from '@hcengineering/chunter' import chunter from '@hcengineering/chunter'
import activity, { ActivityMessage } from '@hcengineering/activity' import activity, { ActivityMessage } from '@hcengineering/activity'
import { isActivityMessageClass, isReactionMessage } from '@hcengineering/activity-resources'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core' import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
import { parseLinkId } from '@hcengineering/view-resources'
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient' import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import SettingsButton from './SettingsButton.svelte' import SettingsButton from './SettingsButton.svelte'
import { getDisplayInboxData, isMentionNotification, openInboxDoc, resolveLocation } from '../../utils' import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
import { InboxData, InboxNotificationsFilter } from '../../types' import { InboxData, InboxNotificationsFilter } from '../../types'
import InboxGroupedListView from './InboxGroupedListView.svelte' import InboxGroupedListView from './InboxGroupedListView.svelte'
import notification from '../../plugin' import notification from '../../plugin'
@ -69,6 +63,8 @@
labelIntl: notification.string.All labelIntl: notification.string.All
} }
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let showArchive = false let showArchive = false
let archivedActivityNotifications: InboxNotification[] = [] let archivedActivityNotifications: InboxNotification[] = []
let archivedOtherNotifications: InboxNotification[] = [] let archivedOtherNotifications: InboxNotification[] = []
@ -156,8 +152,9 @@
return return
} }
const [_id] = decodeObjectURI(loc?.loc.path[3] ?? '') const [id, _class] = decodeObjectURI(loc?.loc.path[3] ?? '')
const context = $contextByDocStore.get(_id) const _id = await parseLinkId(linkProviders, id, _class)
const context = _id ? $contextByDocStore.get(_id) : undefined
selectedContextId = context?._id selectedContextId = context?._id
@ -229,56 +226,13 @@
selectedContextId = selectedContext?._id selectedContextId = selectedContext?._id
if (selectedContext === undefined) { if (selectedContext === undefined) {
openInboxDoc() resetInboxContext()
return return
} }
const selectedNotification: InboxNotification | undefined = event?.detail?.notification const selectedNotification: InboxNotification | undefined = event?.detail?.notification
if (isMentionNotification(selectedNotification) && isActivityMessageClass(selectedNotification.mentionedInClass)) { void selectInboxContext(linkProviders, selectedContext, selectedNotification)
const selectedMsg = selectedNotification.mentionedIn as Ref<ActivityMessage>
openInboxDoc(
selectedContext.attachedTo,
selectedContext.attachedToClass,
isActivityMessageClass(selectedContext.attachedToClass)
? (selectedContext.attachedTo as Ref<ActivityMessage>)
: undefined,
selectedMsg
)
} else if (hierarchy.isDerived(selectedContext.attachedToClass, activity.class.ActivityMessage)) {
const message = event?.detail?.notification?.$lookup?.attachedTo
if (selectedContext.attachedToClass === chunter.class.ThreadMessage) {
const thread = await client.findOne(chunter.class.ThreadMessage, {
_id: selectedContext.attachedTo as Ref<ThreadMessage>
})
openInboxDoc(selectedContext.attachedTo, selectedContext.attachedToClass, thread?.attachedTo, thread?._id)
} else if (isReactionMessage(message)) {
openInboxDoc(
selectedContext.attachedTo,
selectedContext.attachedToClass,
undefined,
selectedContext.attachedTo as Ref<ActivityMessage>
)
} else {
const selectedMsg = (selectedNotification as ActivityInboxNotification)?.attachedTo
openInboxDoc(
selectedContext.attachedTo,
selectedContext.attachedToClass,
selectedMsg ? (selectedContext.attachedTo as Ref<ActivityMessage>) : undefined,
selectedMsg ?? (selectedContext.attachedTo as Ref<ActivityMessage>)
)
}
} else {
openInboxDoc(
selectedContext.attachedTo,
selectedContext.attachedToClass,
undefined,
(selectedNotification as ActivityInboxNotification)?.attachedTo
)
}
} }
async function updateSelectedPanel (selectedContext?: DocNotifyContext): Promise<void> { async function updateSelectedPanel (selectedContext?: DocNotifyContext): Promise<void> {

View File

@ -18,7 +18,13 @@ import activity, {
type DisplayDocUpdateMessage, type DisplayDocUpdateMessage,
type DocUpdateMessage type DocUpdateMessage
} from '@hcengineering/activity' } from '@hcengineering/activity'
import { activityMessagesComparator, combineActivityMessages, messageInFocus } from '@hcengineering/activity-resources' import {
activityMessagesComparator,
combineActivityMessages,
isActivityMessageClass,
isReactionMessage,
messageInFocus
} from '@hcengineering/activity-resources'
import { import {
SortingOrder, SortingOrder,
getCurrentAccount, getCurrentAccount,
@ -31,8 +37,6 @@ import {
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { import notification, {
NotificationStatus, NotificationStatus,
decodeObjectURI,
encodeObjectURI,
notificationId, notificationId,
type ActivityInboxNotification, type ActivityInboxNotification,
type Collaborators, type Collaborators,
@ -57,6 +61,9 @@ import { get, writable } from 'svelte/store'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types' import { type InboxData, type InboxNotificationsFilter } from './types'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { getObjectLinkId } from '@hcengineering/view-resources'
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> { export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
if (docNotifyContext.hidden) { if (docNotifyContext.hidden) {
@ -470,7 +477,7 @@ export async function resolveLocation (loc: Location): Promise<ResolvedLocation
async function generateLocation ( async function generateLocation (
loc: Location, loc: Location,
_id: Ref<Doc>, _id: string,
_class: Ref<Class<Doc>> _class: Ref<Class<Doc>>
): Promise<ResolvedLocation | undefined> { ): Promise<ResolvedLocation | undefined> {
const client = getClient() const client = getClient()
@ -511,12 +518,13 @@ async function generateLocation (
} }
} }
export function openInboxDoc ( async function navigateToInboxDoc (
providers: LinkIdProvider[],
_id?: Ref<Doc>, _id?: Ref<Doc>,
_class?: Ref<Class<Doc>>, _class?: Ref<Class<Doc>>,
thread?: Ref<ActivityMessage>, thread?: Ref<ActivityMessage>,
message?: Ref<ActivityMessage> message?: Ref<ActivityMessage>
): void { ): Promise<void> {
const loc = getLocation() const loc = getLocation()
if (loc.path[2] !== notificationId) { if (loc.path[2] !== notificationId) {
@ -524,14 +532,13 @@ export function openInboxDoc (
} }
if (_id === undefined || _class === undefined) { if (_id === undefined || _class === undefined) {
loc.query = { message: null } resetInboxContext()
loc.path.length = 3
localStorage.setItem(`${locationStorageKeyId}_${notificationId}`, JSON.stringify(loc))
navigate(loc)
return return
} }
loc.path[3] = encodeObjectURI(_id, _class) const id = await getObjectLinkId(providers, _id, _class)
loc.path[3] = encodeObjectURI(id, _class)
if (thread !== undefined) { if (thread !== undefined) {
loc.path[4] = thread loc.path[4] = thread
@ -546,6 +553,96 @@ export function openInboxDoc (
navigate(loc) navigate(loc)
} }
export function resetInboxContext (): void {
const loc = getLocation()
if (loc.path[2] !== notificationId) {
return
}
loc.query = { message: null }
loc.path.length = 3
localStorage.setItem(`${locationStorageKeyId}_${notificationId}`, JSON.stringify(loc))
navigate(loc)
}
export async function selectInboxContext (
linkProviders: LinkIdProvider[],
context: DocNotifyContext,
notification?: WithLookup<InboxNotification>
): Promise<void> {
const client = getClient()
const hierarchy = client.getHierarchy()
if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) {
const selectedMsg = notification.mentionedIn as Ref<ActivityMessage>
void navigateToInboxDoc(
linkProviders,
context.attachedTo,
context.attachedToClass,
isActivityMessageClass(context.attachedToClass) ? (context.attachedTo as Ref<ActivityMessage>) : undefined,
selectedMsg
)
return
}
if (hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage)) {
const message = (notification as WithLookup<ActivityInboxNotification>)?.$lookup?.attachedTo
if (context.attachedToClass === chunter.class.ThreadMessage) {
const thread = await client.findOne(
chunter.class.ThreadMessage,
{
_id: context.attachedTo as Ref<ThreadMessage>
},
{ projection: { _id: 1, attachedTo: 1 } }
)
void navigateToInboxDoc(
linkProviders,
context.attachedTo,
context.attachedToClass,
thread?.attachedTo,
thread?._id
)
return
}
if (isReactionMessage(message)) {
void navigateToInboxDoc(
linkProviders,
context.attachedTo,
context.attachedToClass,
undefined,
context.attachedTo as Ref<ActivityMessage>
)
return
}
const selectedMsg = (notification as ActivityInboxNotification)?.attachedTo
void navigateToInboxDoc(
linkProviders,
context.attachedTo,
context.attachedToClass,
selectedMsg !== undefined ? (context.attachedTo as Ref<ActivityMessage>) : undefined,
selectedMsg ?? (context.attachedTo as Ref<ActivityMessage>)
)
return
}
void navigateToInboxDoc(
linkProviders,
context.attachedTo,
context.attachedToClass,
undefined,
(notification as ActivityInboxNotification)?.attachedTo
)
}
export const pushAllowed = writable<boolean>(false) export const pushAllowed = writable<boolean>(false)
export async function checkPermission (value: boolean): Promise<boolean> { export async function checkPermission (value: boolean): Promise<boolean> {

View File

@ -420,5 +420,4 @@ const notification = plugin(notificationId, {
} }
}) })
export * from './utils'
export default notification export default notification

View File

@ -76,6 +76,7 @@ import {
getTalentId, getTalentId,
getVacTitle, getVacTitle,
objectLinkProvider, objectLinkProvider,
parseLinkId,
resolveLocation resolveLocation
} from './utils' } from './utils'
@ -411,7 +412,8 @@ export default async (): Promise<Resources> => ({
GetIdObjectLinkFragment: getObjectLink, GetIdObjectLinkFragment: getObjectLink,
HideDoneState: hideDoneState, HideDoneState: hideDoneState,
HideArchivedVacancies: hideArchivedVacancies, HideArchivedVacancies: hideArchivedVacancies,
ApplicantHasEmail: applicantHasEmail ApplicantHasEmail: applicantHasEmail,
ParseLinkId: parseLinkId
}, },
resolver: { resolver: {
Location: resolveLocation Location: resolveLocation

View File

@ -149,7 +149,7 @@ export default mergeIds(recruitId, recruit, {
CreateCandidate: '' as AnyComponent CreateCandidate: '' as AnyComponent
}, },
function: { function: {
IdProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, IdProvider: '' as Resource<(doc: Doc) => Promise<string>>,
AppTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, AppTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
AppIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, AppIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
VacTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, VacTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,

View File

@ -92,15 +92,41 @@ async function generateIdLocation (loc: Location, shortLink: string): Promise<Re
} }
} }
async function generateLocation (loc: Location, shortLink: string): Promise<ResolvedLocation | undefined> { export async function parseLinkId (id: string): Promise<Ref<Doc> | undefined> {
if (isShortId(id)) {
const client = getClient()
const hierarchy = client.getHierarchy()
const data = getShortLinkData(hierarchy, id)
if (data === undefined) {
return id as Ref<Doc>
}
const [_class, , number] = data
if (_class === undefined) {
return id as Ref<Doc>
}
const doc = await client.findOne(_class, { number }, { projection: { _id: 1 } })
return doc?._id
}
return id as Ref<Doc>
}
function getShortLinkData (
hierarchy: Hierarchy,
shortLink: string
): [Ref<Class<Doc>> | undefined, string, number] | undefined {
const tokens = shortLink.split('-') const tokens = shortLink.split('-')
if (tokens.length < 2) { if (tokens.length < 2) {
return undefined return undefined
} }
const classLabel = tokens[0] const classLabel = tokens[0]
const number = Number(tokens[1]) const number = Number(tokens[1])
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = [recruit.class.Applicant, recruit.class.Vacancy, recruit.class.Review] const classes = [recruit.class.Applicant, recruit.class.Vacancy, recruit.class.Review]
let _class: Ref<Class<Doc>> | undefined let _class: Ref<Class<Doc>> | undefined
for (const clazz of classes) { for (const clazz of classes) {
@ -109,6 +135,21 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Reso
break break
} }
} }
return [_class, classLabel, number]
}
async function generateLocation (loc: Location, shortLink: string): Promise<ResolvedLocation | undefined> {
const client = getClient()
const hierarchy = client.getHierarchy()
const data = getShortLinkData(hierarchy, shortLink)
if (data === undefined) {
return
}
const [_class, classLabel, number] = data
if (_class === undefined) { if (_class === undefined) {
console.error(`Not found class with short label ${classLabel}`) console.error(`Not found class with short label ${classLabel}`)
return undefined return undefined

View File

@ -41,7 +41,6 @@
import ToDoGroup from './ToDoGroup.svelte' import ToDoGroup from './ToDoGroup.svelte'
import MenuClose from './icons/MenuClose.svelte' import MenuClose from './icons/MenuClose.svelte'
import MenuOpen from './icons/MenuOpen.svelte' import MenuOpen from './icons/MenuOpen.svelte'
import IconDiff from './icons/Diff.svelte'
import time from '../plugin' import time from '../plugin'
export let mode: ToDosMode export let mode: ToDosMode

View File

@ -20,6 +20,7 @@
import { Component, showPanel } from '@hcengineering/ui' import { Component, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import time from '../plugin' import time from '../plugin'
import { getObjectLinkId } from '@hcengineering/view-resources'
export let todo: ToDo export let todo: ToDo
export let kind: 'default' | 'todo-line' = 'default' export let kind: 'default' | 'todo-line' = 'default'
@ -27,6 +28,9 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
$: presenter = hierarchy.classHierarchyMixin<Doc, ItemPresenter>(todo.attachedToClass, time.mixin.ItemPresenter) $: presenter = hierarchy.classHierarchyMixin<Doc, ItemPresenter>(todo.attachedToClass, time.mixin.ItemPresenter)
let doc: Doc | undefined = undefined let doc: Doc | undefined = undefined
@ -35,12 +39,14 @@
doc = res[0] doc = res[0]
}) })
async function click (event: MouseEvent) { async function click (event: MouseEvent): Promise<void> {
event.stopPropagation() event.stopPropagation()
if (!doc) return if (!doc) return
const panelComponent = hierarchy.classHierarchyMixin<Class<Doc>, ObjectPanel>(doc._class, view.mixin.ObjectPanel) const panelComponent = hierarchy.classHierarchyMixin<Class<Doc>, ObjectPanel>(doc._class, view.mixin.ObjectPanel)
const component = panelComponent?.component ?? view.component.EditDoc const component = panelComponent?.component ?? view.component.EditDoc
showPanel(component, doc._id, doc._class, 'content') const id = await getObjectLinkId(linkProviders, doc._id, doc._class, doc)
showPanel(component, id, doc._class, 'content')
} }
</script> </script>

View File

@ -15,9 +15,7 @@
<script lang="ts"> <script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources' import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core' import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, { import presentation, {
ActionContext, ActionContext,
ComponentExtensions, ComponentExtensions,
@ -44,21 +42,23 @@
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { DocNavLink, ParentsNavigator, showMenu } from '@hcengineering/view-resources' import { DocNavLink, ParentsNavigator, showMenu } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { generateIssueShortLink } from '../../../issues' import { generateIssueShortLink, getIssueIdByIdentifier } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import IssueStatusActivity from '../IssueStatusActivity.svelte' import IssueStatusActivity from '../IssueStatusActivity.svelte'
import ControlPanel from './ControlPanel.svelte' import ControlPanel from './ControlPanel.svelte'
import CopyToClipboard from './CopyToClipboard.svelte' import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.svelte' import SubIssueSelector from './SubIssueSelector.svelte'
import SubIssues from './SubIssues.svelte' import SubIssues from './SubIssues.svelte'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
export let _id: Ref<Issue> export let _id: Ref<Issue> | string
export let _class: Ref<Class<Issue>> export let _class: Ref<Class<Issue>>
export let embedded: boolean = false export let embedded: boolean = false
export let kind: 'default' | 'modern' = 'default' export let kind: 'default' | 'modern' = 'default'
export let readonly: boolean = false export let readonly: boolean = false
let lastId: Ref<Doc> = _id let lastId: Ref<Issue> | undefined
const queryClient = createQuery() const queryClient = createQuery()
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
@ -70,28 +70,39 @@
let descriptionBox: AttachmentStyleBoxCollabEditor let descriptionBox: AttachmentStyleBoxCollabEditor
let showAllMixins: boolean let showAllMixins: boolean
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res()) const inboxClient = InboxNotificationsClientImpl.getClient()
$: read(_id) let issueId: Ref<Issue> | undefined
function read (_id: Ref<Doc>): void {
if (lastId !== _id) { $: void getIssueIdByIdentifier(_id).then((res) => {
issueId = res ?? (_id as Ref<Issue>)
if (lastId === undefined) {
lastId = issueId
}
})
$: read(issueId)
function read (_id?: Ref<Issue>): void {
if (_id && lastId && lastId !== _id) {
const prev = lastId const prev = lastId
lastId = _id lastId = _id
void inboxClient.then((client) => client.readDoc(getClient(), prev)) void inboxClient.readDoc(getClient(), prev)
} }
} }
onDestroy(async () => { onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id)) if (issueId === undefined) return
void inboxClient.readDoc(getClient(), issueId)
}) })
$: _id !== undefined && $: if (issueId !== undefined && _class !== undefined) {
_class !== undefined &&
queryClient.query<Issue>( queryClient.query<Issue>(
_class, _class,
{ _id }, { _id: issueId },
async (result) => { async (result) => {
if (lastId !== _id) { if (lastId !== issueId) {
await save() await save()
} }
;[issue] = result ;[issue] = result
@ -103,6 +114,7 @@
limit: 1 limit: 1
} }
) )
}
$: canSave = title.trim().length > 0 $: canSave = title.trim().length > 0
$: hasParentIssue = issue?.attachedTo !== tracker.ids.NoParent $: hasParentIssue = issue?.attachedTo !== tracker.ids.NoParent

View File

@ -89,6 +89,7 @@ import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svel
import IssueExtra from './components/issues/IssueExtra.svelte' import IssueExtra from './components/issues/IssueExtra.svelte'
import IssueStatusPresenter from './components/issues/IssueStatusPresenter.svelte' import IssueStatusPresenter from './components/issues/IssueStatusPresenter.svelte'
import { import {
getIssueIdByIdentifier,
getIssueTitle, getIssueTitle,
getTitle, getTitle,
issueIdentifierProvider, issueIdentifierProvider,
@ -689,6 +690,7 @@ export default async (): Promise<Resources> => ({
ComponentTitleProvider: getComponentTitle, ComponentTitleProvider: getComponentTitle,
MilestoneTitleProvider: getMilestoneTitle, MilestoneTitleProvider: getMilestoneTitle,
GetIssueId: getTitle, GetIssueId: getTitle,
GetIssueIdByIdentifier: getIssueIdByIdentifier,
GetIssueLink: issueLinkProvider, GetIssueLink: issueLinkProvider,
GetIssueLinkFragment: issueLinkFragmentProvider, GetIssueLinkFragment: issueLinkFragmentProvider,
GetIssueTitle: getIssueTitle, GetIssueTitle: getIssueTitle,

View File

@ -41,7 +41,7 @@ export async function getTitle (doc: Doc): Promise<string> {
} }
export function generateIssuePanelUri (issue: Issue): string { export function generateIssuePanelUri (issue: Issue): string {
return getPanelURI(tracker.component.EditIssue, issue._id, issue._class, 'content') return getPanelURI(tracker.component.EditIssue, issue.identifier, issue._class, 'content')
} }
export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> { export async function issueLinkFragmentProvider (doc: Doc): Promise<Location> {
@ -126,3 +126,10 @@ export async function updateIssueRelation (
} }
await client.update(value, update) await client.update(value, update)
} }
export async function getIssueIdByIdentifier (identifier: string): Promise<Ref<Issue> | undefined> {
const client = getClient()
const issue = await client.findOne(tracker.class.Issue, { identifier }, { projection: { _id: 1 } })
return issue?._id
}

View File

@ -16,7 +16,7 @@ import { type StatusCategory, type Client, type Doc, type Ref, type Space } from
import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform' import { mergeIds } from '@hcengineering/platform'
import { type ProjectType } from '@hcengineering/task' import { type ProjectType } from '@hcengineering/task'
import tracker, { trackerId, type IssueDraft } from '@hcengineering/tracker' import tracker, { trackerId, type IssueDraft, type Issue } from '@hcengineering/tracker'
import { type AnyComponent, type Location } from '@hcengineering/ui' import { type AnyComponent, type Location } from '@hcengineering/ui'
import { import {
type CreateAggregationManagerFunc, type CreateAggregationManagerFunc,
@ -378,7 +378,7 @@ export default mergeIds(trackerId, tracker, {
IssueIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, IssueIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
ComponentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, ComponentTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
MilestoneTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>, MilestoneTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
GetIssueId: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetIssueId: '' as Resource<(doc: Doc) => Promise<string>>,
GetIssueLink: '' 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<Location>>, GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>, GetIssueTitle: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<string>>,
@ -393,7 +393,8 @@ export default mergeIds(trackerId, tracker, {
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>, GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>, IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>,
IssueChatTitleProvider: '' as Resource<(object: Doc) => string>, IssueChatTitleProvider: '' as Resource<(object: Doc) => string>,
GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array<Ref<StatusCategory>>> GetIssueStatusCategories: '' as Resource<(project: ProjectType) => Array<Ref<StatusCategory>>>,
GetIssueIdByIdentifier: '' as Resource<(id: string) => Promise<Ref<Issue> | undefined>>
}, },
aggregation: { aggregation: {
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc, CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,

View File

@ -31,26 +31,37 @@
import view, { AttributeCategory } from '@hcengineering/view' import view, { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { DocNavLink, ParentsNavigator, getDocAttrsInfo, getDocLabel, getDocMixins, showMenu } from '..' import { DocNavLink, ParentsNavigator, getDocAttrsInfo, getDocLabel, getDocMixins, showMenu, parseLinkId } from '..'
import { getCollectionCounter } from '../utils' import { getCollectionCounter } from '../utils'
import DocAttributeBar from './DocAttributeBar.svelte' import DocAttributeBar from './DocAttributeBar.svelte'
export let _id: Ref<Doc> export let _id: Ref<Doc> | string
export let _class: Ref<Class<Doc>> export let _class: Ref<Class<Doc>>
export let embedded: boolean = false export let embedded: boolean = false
export let readonly: boolean = false export let readonly: boolean = false
let realObjectClass: Ref<Class<Doc>> = _class let realObjectClass: Ref<Class<Doc>> = _class
let lastId: Ref<Doc> = _id let lastId: Ref<Doc> | undefined
let objectId: Ref<Doc> | undefined
let object: Doc let object: Doc
const pClient = getClient() const pClient = getClient()
const hierarchy = pClient.getHierarchy() const hierarchy = pClient.getHierarchy()
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res()) const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
const linkProviders = pClient.getModel().findAllSync(view.mixin.LinkIdProvider, {})
$: read(_id) $: void parseLinkId(linkProviders, _id, _class).then((res) => {
function read (_id: Ref<Doc>): void { objectId = res ?? (_id as Ref<Doc>)
if (lastId !== _id) { if (lastId !== undefined) {
return
}
lastId = objectId
})
$: read(objectId)
function read (_id?: Ref<Doc>): void {
if (objectId && lastId && lastId !== _id) {
const prev = lastId const prev = lastId
lastId = _id lastId = _id
void inboxClient.then(async (client) => { void inboxClient.then(async (client) => {
@ -61,14 +72,15 @@
onDestroy(async () => { onDestroy(async () => {
await inboxClient.then(async (client) => { await inboxClient.then(async (client) => {
await client.readDoc(pClient, _id) if (objectId === undefined) return
await client.readDoc(pClient, objectId)
}) })
}) })
const query = createQuery() const query = createQuery()
$: updateQuery(_id, _class) $: updateQuery(objectId, _class)
function updateQuery (_id: Ref<Doc>, _class: Ref<Class<Doc>>): void { function updateQuery (_id?: Ref<Doc>, _class?: Ref<Class<Doc>>): void {
if (_id && _class) { if (_id && _class) {
query.query(_class, { _id }, (result) => { query.query(_class, { _id }, (result) => {
object = result[0] object = result[0]
@ -146,12 +158,13 @@
$: editorFooter = getEditorFooter(_class, object) $: editorFooter = getEditorFooter(_class, object)
const getEditorOrDefault = reduceCalls(async function (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<void> { const getEditorOrDefault = reduceCalls(async function (_class: Ref<Class<Doc>>, _id?: Ref<Doc>): Promise<void> {
if (objectId === undefined) return
await updateKeys() await updateKeys()
mainEditor = getEditor(_class) mainEditor = getEditor(_class)
}) })
$: void getEditorOrDefault(realObjectClass, _id) $: void getEditorOrDefault(realObjectClass, objectId)
let title: string | undefined = undefined let title: string | undefined = undefined
let rawTitle: string = '' let rawTitle: string = ''

View File

@ -87,7 +87,8 @@ import view, {
type BuildModelOptions, type BuildModelOptions,
type CollectionPresenter, type CollectionPresenter,
type Viewlet, type Viewlet,
type ViewletDescriptor type ViewletDescriptor,
type LinkIdProvider
} from '@hcengineering/view' } from '@hcengineering/view'
import contact, { getName, type Contact, type PersonAccount } from '@hcengineering/contact' import contact, { getName, type Contact, type PersonAccount } from '@hcengineering/contact'
@ -1023,8 +1024,16 @@ export async function getObjectLinkFragment (
} }
} }
const loc = getCurrentResolvedLocation() const loc = getCurrentResolvedLocation()
const idProvider = hierarchy.classHierarchyMixin(Hierarchy.mixinOrClass(object), view.mixin.LinkIdProvider)
let id: string = object._id
if (idProvider !== undefined) {
const encodeFn = await getResource(idProvider.encode)
id = await encodeFn(object)
}
if (hasResource(component) === true) { if (hasResource(component) === true) {
loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content') loc.fragment = getPanelURI(component, id, Hierarchy.mixinOrClass(object), 'content')
} }
return loc return loc
} }
@ -1448,3 +1457,43 @@ export function getCollaborationUser (): CollaborationUser {
color color
} }
} }
export async function getObjectLinkId (
providers: LinkIdProvider[],
_id: Ref<Doc>,
_class: Ref<Class<Doc>>,
doc?: Doc
): Promise<string> {
const provider = providers.find(({ _id }) => _id === _class)
if (provider === undefined) {
return _id
}
const client = getClient()
const object = doc ?? (await client.findOne(_class, { _id }))
if (object === undefined) {
return _id
}
const encodeFn = await getResource(provider.encode)
return await encodeFn(object)
}
export async function parseLinkId<T extends Doc> (
providers: LinkIdProvider[],
id: string,
_class: Ref<Class<T>>
): Promise<Ref<T>> {
const provider = providers.find(({ _id }) => _id === _class)
if (provider === undefined) {
return id as Ref<T>
}
const decodeFn = await getResource(provider.decode)
const _id = await decodeFn(id)
return (_id ?? id) as Ref<T>
}

View File

@ -60,10 +60,12 @@ import {
ViewAction, ViewAction,
Viewlet, Viewlet,
ViewletDescriptor, ViewletDescriptor,
ViewletPreference ViewletPreference,
LinkIdProvider
} from './types' } from './types'
export * from './types' export * from './types'
export * from './utils'
/** /**
* @public * @public
@ -103,6 +105,7 @@ const view = plugin(viewId, {
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>, AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>,
ObjectPanel: '' as Ref<Mixin<ObjectPanel>>, ObjectPanel: '' as Ref<Mixin<ObjectPanel>>,
LinkProvider: '' as Ref<Mixin<LinkProvider>>, LinkProvider: '' as Ref<Mixin<LinkProvider>>,
LinkIdProvider: '' as Ref<Mixin<LinkIdProvider>>,
SpacePresenter: '' as Ref<Mixin<SpacePresenter>>, SpacePresenter: '' as Ref<Mixin<SpacePresenter>>,
AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>, AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>,
Aggregation: '' as Ref<Mixin<Aggregation>>, Aggregation: '' as Ref<Mixin<Aggregation>>,

View File

@ -756,6 +756,11 @@ export interface LinkProvider extends Class<Doc> {
encode: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>> encode: Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
} }
export interface LinkIdProvider extends Class<Doc> {
encode: Resource<(doc: Doc) => Promise<string>>
decode: Resource<(id: string) => Promise<Ref<Doc> | undefined>>
}
/** /**
* @public * @public
*/ */

View File

@ -13,12 +13,12 @@
// limitations under the License. // limitations under the License.
// //
import { Ref, Doc, Class } from '@hcengineering/core' import type { Class, Doc, Ref } from '@hcengineering/core'
export function decodeObjectURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] { export function decodeObjectURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] {
return decodeURIComponent(value).split('|') as [Ref<Doc>, Ref<Class<Doc>>] return decodeURIComponent(value).split('|') as [Ref<Doc>, Ref<Class<Doc>>]
} }
export function encodeObjectURI (_id: Ref<Doc>, _class: Ref<Class<Doc>>): string { export function encodeObjectURI (_id: string, _class: Ref<Class<Doc>>): string {
return [_id, _class].join('|') return [_id, _class].join('|')
} }

View File

@ -67,7 +67,8 @@
NavLink, NavLink,
accessDeniedStore, accessDeniedStore,
migrateViewOpttions, migrateViewOpttions,
updateFocus updateFocus,
parseLinkId
} from '@hcengineering/view-resources' } from '@hcengineering/view-resources'
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench' import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
import { getContext, onDestroy, onMount, tick } from 'svelte' import { getContext, onDestroy, onMount, tick } from 'svelte'
@ -117,7 +118,10 @@
let panelInstance: PanelInstance let panelInstance: PanelInstance
let popupInstance: Popup let popupInstance: Popup
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
$deviceInfo.navigator.visible = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true $deviceInfo.navigator.visible = getMetadata(workbench.metadata.NavigationExpandedDefault) ?? true
async function toggleNav (): Promise<void> { async function toggleNav (): Promise<void> {
$deviceInfo.navigator.visible = !$deviceInfo.navigator.visible $deviceInfo.navigator.visible = !$deviceInfo.navigator.visible
closeTooltip() closeTooltip()
@ -202,14 +206,15 @@
async function getWindowTitle (loc: Location): Promise<string | undefined> { async function getWindowTitle (loc: Location): Promise<string | undefined> {
if (loc.fragment == null) return if (loc.fragment == null) return
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const [, _id, _class] = decodeURIComponent(loc.fragment).split('|') const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
if (_class == null) return if (_class == null) return
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle) const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
if (mixin === undefined) return if (mixin === undefined) return
const titleProvider = await getResource(mixin.titleProvider) const titleProvider = await getResource(mixin.titleProvider)
try { try {
return await titleProvider(client, _id as Ref<Doc>) const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
return await titleProvider(client, _id)
} catch (err: any) { } catch (err: any) {
Analytics.handleError(err) Analytics.handleError(err)
console.error(err) console.error(err)
@ -398,7 +403,9 @@
const props = decodeURIComponent(fragment).split('|') const props = decodeURIComponent(fragment).split('|')
if (props.length >= 3) { if (props.length >= 3) {
const doc = await client.findOne<Doc>(props[2] as Ref<Class<Doc>>, { _id: props[1] as Ref<Doc> }) const _class = props[2] as Ref<Class<Doc>>
const _id = await parseLinkId(linkProviders, props[1], _class)
const doc = await client.findOne<Doc>(_class, { _id })
if (doc !== undefined) { if (doc !== undefined) {
const provider = ListSelectionProvider.Find(doc._id) const provider = ListSelectionProvider.Find(doc._id)
@ -408,8 +415,8 @@
}) })
openPanel( openPanel(
props[0] as AnyComponent, props[0] as AnyComponent,
props[1], _id,
props[2], _class,
(props[3] ?? undefined) as PopupAlignment, (props[3] ?? undefined) as PopupAlignment,
(props[4] ?? undefined) as AnyComponent (props[4] ?? undefined) as AnyComponent
) )

View File

@ -26,6 +26,10 @@ export async function documentHTMLPresenter (doc: Doc, control: TriggerControl):
return `<a href="${link}">${document.name}</a>` return `<a href="${link}">${document.name}</a>`
} }
export async function documentLinkIdProvider (doc: Document): Promise<string> {
return getDocumentId(doc)
}
/** /**
* @public * @public
*/ */
@ -38,6 +42,7 @@ export async function documentTextPresenter (doc: Doc): Promise<string> {
export default async () => ({ export default async () => ({
function: { function: {
DocumentHTMLPresenter: documentHTMLPresenter, DocumentHTMLPresenter: documentHTMLPresenter,
DocumentTextPresenter: documentTextPresenter DocumentTextPresenter: documentTextPresenter,
DocumentLinkIdProvider: documentLinkIdProvider
} }
}) })

View File

@ -39,6 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/server-core": "^0.6.1", "@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1" "@hcengineering/server-notification": "^0.6.1"
} }

View File

@ -3,6 +3,7 @@
// //
// //
import { Doc } from '@hcengineering/core'
import type { Plugin, Resource } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { Presenter } from '@hcengineering/server-notification' import { Presenter } from '@hcengineering/server-notification'
@ -18,6 +19,7 @@ export const serverDocumentId = 'server-document' as Plugin
export default plugin(serverDocumentId, { export default plugin(serverDocumentId, {
function: { function: {
DocumentHTMLPresenter: '' as Resource<Presenter>, DocumentHTMLPresenter: '' as Resource<Presenter>,
DocumentTextPresenter: '' as Resource<Presenter> DocumentTextPresenter: '' as Resource<Presenter>,
DocumentLinkIdProvider: '' as Resource<(doc: Doc) => Promise<string>>
} }
}) })

View File

@ -49,6 +49,7 @@
"@hcengineering/view": "^0.6.13", "@hcengineering/view": "^0.6.13",
"@hcengineering/text": "^0.6.5", "@hcengineering/text": "^0.6.5",
"@hcengineering/contact": "^0.6.24", "@hcengineering/contact": "^0.6.24",
"@hcengineering/server-view": "^0.6.0",
"web-push": "~3.6.7" "web-push": "~3.6.7"
} }
} }

View File

@ -58,7 +58,6 @@ import notification, {
Collaborators, Collaborators,
CommonInboxNotification, CommonInboxNotification,
DocNotifyContext, DocNotifyContext,
encodeObjectURI,
InboxNotification, InboxNotification,
MentionInboxNotification, MentionInboxNotification,
notificationId, notificationId,
@ -80,6 +79,9 @@ import serverNotification, {
import { stripTags } from '@hcengineering/text' import { stripTags } from '@hcengineering/text'
import { workbenchId } from '@hcengineering/workbench' import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push' import webpush, { WebPushError } from 'web-push'
import { encodeObjectURI } from '@hcengineering/view'
import serverView from '@hcengineering/server-view'
import { Content, NotifyResult, NotifyParams } from './types' import { Content, NotifyResult, NotifyParams } from './types'
import { import {
getHTMLPresenter, getHTMLPresenter,
@ -554,12 +556,24 @@ export async function createPushFromInbox (
cache.set(senderPerson._id, senderPerson) cache.set(senderPerson._id, senderPerson)
} }
const path = [ const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {})
workbenchId, const provider = linkProviders.find(({ _id }) => _id === attachedToClass)
control.workspace.workspaceUrl,
notificationId, let id: string = attachedTo
encodeObjectURI(attachedTo, attachedToClass)
] if (provider !== undefined) {
const encodeFn = await getResource(provider.encode)
const doc = cache.get(attachedTo) ?? (await control.findAll(attachedToClass, { _id: attachedTo }))[0]
if (doc === undefined) {
return
}
cache.set(doc._id, doc)
id = await encodeFn(doc, control)
}
const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, attachedToClass)]
await createPushNotification(control, targetUser, title, body, _id, senderPerson, path) await createPushNotification(control, targetUser, title, body, _id, senderPerson, path)
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, notification.space.Notifications, { return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, notification.space.Notifications, {
user: targetUser, user: targetUser,

View File

@ -110,7 +110,8 @@ export default async () => ({
VacancyHTMLPresenter: vacancyHTMLPresenter, VacancyHTMLPresenter: vacancyHTMLPresenter,
VacancyTextPresenter: vacancyTextPresenter, VacancyTextPresenter: vacancyTextPresenter,
ApplicationHTMLPresenter: applicationHTMLPresenter, ApplicationHTMLPresenter: applicationHTMLPresenter,
ApplicationTextPresenter: applicationTextPresenter ApplicationTextPresenter: applicationTextPresenter,
LinkIdProvider: getSequenceId
}, },
trigger: { trigger: {
OnRecruitUpdate OnRecruitUpdate

View File

@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-core": "^0.6.1" "@hcengineering/server-core": "^0.6.1",
"@hcengineering/core": "^0.6.32"
} }
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { Doc } from '@hcengineering/core'
import type { Plugin, Resource } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core' import { TriggerFunc } from '@hcengineering/server-core'
@ -31,7 +32,8 @@ export default plugin(serverRecruitId, {
ApplicationHTMLPresenter: '' as Resource<Presenter>, ApplicationHTMLPresenter: '' as Resource<Presenter>,
ApplicationTextPresenter: '' as Resource<Presenter>, ApplicationTextPresenter: '' as Resource<Presenter>,
VacancyHTMLPresenter: '' as Resource<Presenter>, VacancyHTMLPresenter: '' as Resource<Presenter>,
VacancyTextPresenter: '' as Resource<Presenter> VacancyTextPresenter: '' as Resource<Presenter>,
LinkIdProvider: '' as Resource<(doc: Doc) => Promise<string>>
}, },
trigger: { trigger: {
OnRecruitUpdate: '' as Resource<TriggerFunc> OnRecruitUpdate: '' as Resource<TriggerFunc>

View File

@ -501,12 +501,17 @@ function updateIssueParentEstimations (
} }
} }
async function issueLinkIdProvider (issue: Issue): Promise<string> {
return issue.identifier
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({ export default async () => ({
function: { function: {
IssueHTMLPresenter: issueHTMLPresenter, IssueHTMLPresenter: issueHTMLPresenter,
IssueTextPresenter: issueTextPresenter, IssueTextPresenter: issueTextPresenter,
IssueNotificationContentProvider: getIssueNotificationContent IssueNotificationContentProvider: getIssueNotificationContent,
IssueLinkIdProvider: issueLinkIdProvider
}, },
trigger: { trigger: {
OnIssueUpdate, OnIssueUpdate,

View File

@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-core": "^0.6.1" "@hcengineering/server-core": "^0.6.1",
"@hcengineering/core": "^0.6.32"
} }
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { Doc } from '@hcengineering/core'
import type { Plugin, Resource } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core' import { TriggerFunc } from '@hcengineering/server-core'
@ -30,7 +31,8 @@ export default plugin(serverTrackerId, {
function: { function: {
IssueHTMLPresenter: '' as Resource<Presenter>, IssueHTMLPresenter: '' as Resource<Presenter>,
IssueTextPresenter: '' as Resource<Presenter>, IssueTextPresenter: '' as Resource<Presenter>,
IssueNotificationContentProvider: '' as Resource<NotificationContentProvider> IssueNotificationContentProvider: '' as Resource<NotificationContentProvider>,
IssueLinkIdProvider: '' as Resource<(doc: Doc) => Promise<string>>
}, },
trigger: { trigger: {
OnIssueUpdate: '' as Resource<TriggerFunc>, OnIssueUpdate: '' as Resource<TriggerFunc>,

View File

@ -40,6 +40,7 @@
"dependencies": { "dependencies": {
"@hcengineering/server-notification": "^0.6.1", "@hcengineering/server-notification": "^0.6.1",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/server-core": "^0.6.1" "@hcengineering/server-core": "^0.6.1"
} }
} }

View File

@ -14,18 +14,26 @@
// //
import type { Plugin, Resource } from '@hcengineering/platform' import type { Plugin, Resource } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core' import { TriggerControl, TriggerFunc } from '@hcengineering/server-core'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { Class, Doc, Mixin, Ref } from '@hcengineering/core'
/** /**
* @public * @public
*/ */
export const serverViewId = 'server-view' as Plugin export const serverViewId = 'server-view' as Plugin
export interface ServerLinkIdProvider extends Class<Doc> {
encode: Resource<(doc: Doc, control: TriggerControl) => Promise<string>>
}
/** /**
* @public * @public
*/ */
export default plugin(serverViewId, { export default plugin(serverViewId, {
mixin: {
ServerLinkIdProvider: '' as Ref<Mixin<ServerLinkIdProvider>>
},
trigger: { trigger: {
OnCustomAttributeRemove: '' as Resource<TriggerFunc> OnCustomAttributeRemove: '' as Resource<TriggerFunc>
} }