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
})
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, {
component: document.component.DocumentIcon
})

View File

@ -932,6 +932,26 @@ export function createModel (builder: Builder): void {
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(
view.class.ActionCategory,
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>>,
HideDoneState: '' as ViewQueryAction,
HideArchivedVacancies: '' as ViewQueryAction,
ApplicantHasEmail: '' as Resource<ViewActionAvailabilityFunction>
ApplicantHasEmail: '' as Resource<ViewActionAvailabilityFunction>,
ParseLinkId: '' as Resource<(id: string) => Promise<Ref<Doc> | undefined>>
},
string: {
ApplicationsShort: '' as IntlString,

View File

@ -35,6 +35,7 @@
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/model-core": "^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 serverDocument from '@hcengineering/server-document'
import serverNotification from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
export { serverDocumentId } from '@hcengineering/server-document'
@ -22,6 +23,10 @@ export function createModel (builder: Builder): void {
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, {
searchConfig: {
iconConfig: {

View File

@ -37,6 +37,7 @@
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/model-recruit": "^0.6.0",
"@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 serverContact from '@hcengineering/server-contact'
import contact from '@hcengineering/contact'
import serverView from '@hcengineering/server-view'
export { serverRecruitId } from '@hcengineering/server-recruit'
@ -43,6 +44,22 @@ export function createModel (builder: Builder): void {
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, {
trigger: serverRecruit.trigger.OnRecruitUpdate
})

View File

@ -36,6 +36,8 @@
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/model-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 serverTracker from '@hcengineering/server-tracker'
import contact from '@hcengineering/contact'
import serverView from '@hcengineering/server-view'
export { serverTrackerId } from '@hcengineering/server-tracker'
@ -37,6 +38,10 @@ export function createModel (builder: Builder): void {
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, {
searchConfig: {
iconConfig: {

View File

@ -30,8 +30,9 @@
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11",
"@hcengineering/model-core": "^0.6.0",
"@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.
//
import core from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import serverCore from '@hcengineering/server-core'
import serverView from '@hcengineering/server-view'
import core, { type Doc } from '@hcengineering/core'
import { type Builder, Mixin } from '@hcengineering/model'
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
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'
@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 {
builder.createModel(TServerLinkIdProvider)
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
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.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, {
objectClass: tracker.class.Issue,
skip: [

View File

@ -90,7 +90,8 @@ import {
type ObjectIcon,
type ObjectTooltip,
type AttrPresenter,
type AttributeCategory
type AttributeCategory,
type LinkIdProvider
} from '@hcengineering/view'
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>>
}
@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)
export class TObjectPanel extends TClass implements ObjectPanel {
component!: AnyComponent
@ -450,7 +457,8 @@ export function createModel (builder: Builder): void {
TObjectIdentifier,
TObjectTooltip,
TObjectIcon,
TAttrPresenter
TAttrPresenter,
TLinkIdProvider
)
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
panelstore.update(() => {
return { panel: undefined }
})
if (shoulRedirect) {
if (shouldRedirect) {
const loc = getLocation()
loc.fragment = undefined
navigate(loc)

View File

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

View File

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

View File

@ -17,7 +17,7 @@
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources'
import { getActions, getObjectLinkId } from '@hcengineering/view-resources'
import {
getNotificationsCount,
InboxNotificationsClientImpl,
@ -68,6 +68,8 @@
actions = res
})
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
async function getChannelActions (context: DocNotifyContext | undefined, object: Doc): Promise<Action[]> {
const result: Action[] = []
@ -79,7 +81,8 @@
icon: view.icon.Open,
label: view.string.Open,
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 { navigateToSpecial } from '../../../navigation'
export let objectId: Ref<Doc> | undefined
export let object: Doc | undefined
export let currentSpecial: SpecialNavModel | undefined
@ -112,7 +111,7 @@
</div>
<Scroller shrink>
{#each chatNavGroupModels as model}
<ChatNavGroup {object} {objectId} {model} on:select />
<ChatNavGroup {object} {model} on:select />
{/each}
</Scroller>
<NavFooter />

View File

@ -4,22 +4,16 @@ import type { ActivityMessage } from '@hcengineering/activity'
import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter'
import { notificationId } from '@hcengineering/notification'
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 { isThreadMessage } from './utils'
export function decodeChannelURI (value: string): [Ref<Doc>, Ref<Class<Doc>>] {
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 {
export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void {
const loc = getCurrentLocation()
const id = encodeChannelURI(_id, _class)
const id = encodeObjectURI(_id, _class)
if (loc.path[3] === id) {
return
@ -45,12 +39,18 @@ export async function openMessageFromSpecial (message?: ActivityMessage): Promis
}
const loc = getCurrentResolvedLocation()
const client = getClient()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
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
} 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 }
@ -66,47 +66,58 @@ export function navigateToSpecial (specialId: string): void {
}
export async function getMessageLink (message: ActivityMessage): Promise<string> {
const client = getClient()
const location = getCurrentResolvedLocation()
const providers = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let threadParent = ''
let _id: Ref<Doc>
let _id: string
let _class: Ref<Class<Doc>>
if (isThreadMessage(message)) {
threadParent = `/${message.attachedTo}`
_id = message.objectId
_id = await getObjectLinkId(providers, message.objectId, message.objectClass)
_class = message.objectClass
} else {
_id = message.attachedTo
_id = await getObjectLinkId(providers, message.attachedTo, 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}`
}
export async function chunterSpaceLinkFragmentProvider (doc: ChunterSpace): Promise<Location> {
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.fragment = undefined
loc.query = undefined
loc.path[2] = chunterId
loc.path[3] = encodeChannelURI(doc._id, doc._class)
loc.path[3] = encodeObjectURI(id, doc._class)
return loc
}
export function buildThreadLink (
export async function buildThreadLink (
loc: Location,
_id: Ref<Doc>,
_class: Ref<Class<Doc>>,
threadParent: Ref<ActivityMessage>
): Location {
threadParent: Ref<ActivityMessage>,
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 id = encodeChannelURI(_id, _class)
const isSameChannel = loc.path[3] === id
const objectURI = encodeObjectURI(id, _class)
const isSameChannel = loc.path[3] === objectURI
if (!isSameChannel) {
loc.query = { message: threadParent }
@ -121,7 +132,7 @@ export function buildThreadLink (
loc.path[2] = chunterId
}
loc.path[3] = id
loc.path[3] = objectURI
loc.path[4] = threadParent
loc.fragment = undefined
@ -131,7 +142,7 @@ export function buildThreadLink (
export async function getThreadLink (doc: ThreadMessage): Promise<Location> {
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> {
@ -141,11 +152,37 @@ export async function replyToThread (message: ActivityMessage): Promise<void> {
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> {
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
// limitations under the License.
//
import {
type Channel,
type ChatMessage,
chunterId,
type DirectMessage,
type ThreadMessage
} from '@hcengineering/chunter'
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
import contact, { type Employee, getName, type Person, type PersonAccount } from '@hcengineering/contact'
import { employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources'
import {
@ -34,7 +28,7 @@ import {
type Timestamp
} from '@hcengineering/core'
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 { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import activity, {
@ -55,7 +49,7 @@ import { get, type Unsubscriber } from 'svelte/store'
import chunter from './plugin'
import DirectIcon from './components/DirectIcon.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> {
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 })
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 {
const loc = getCurrentLocation()
const [_id] = decodeChannelURI(loc.path[3])
if (loc.path[2] !== chunterId || _id !== objectId) {
return
}
loc.path[3] = ''
loc.path[4] = ''
loc.query = {}
loc.path.length = 3
navigate(loc)
}
export async function leaveChannelAction (context?: DocNotifyContext): Promise<void> {
export async function leaveChannelAction (
context?: DocNotifyContext,
_?: Event,
props?: { object?: Channel }
): Promise<void> {
if (context === undefined) {
return
}
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) {
return
}
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) {
return
}
@ -442,7 +430,7 @@ export async function removeChannelAction (context?: DocNotifyContext): Promise<
await archiveContextNotifications(context)
await client.remove(context)
resetChunterLoc(context.attachedTo)
await resetChunterLocIfEqual(context.attachedTo, context.attachedToClass, props?.object)
}
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 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 => ({
doc: e,
@ -187,7 +195,9 @@ export default async (): Promise<Resources> => ({
GetObjectLinkFragment: getDocumentLink,
DocumentTitleProvider: documentTitleProvider,
CanLockDocument: canLockDocument,
CanUnlockDocument: canUnlockDocument
CanUnlockDocument: canUnlockDocument,
GetDocumentLinkId: getDocumentLinkId,
ParseDocumentId: parseDocumentId
},
resolver: {
Location: resolveLocation

View File

@ -14,7 +14,7 @@
//
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 { type AnyComponent, type Location } from '@hcengineering/ui'
@ -29,7 +29,9 @@ export default mergeIds(documentId, document, {
function: {
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>>,
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: {
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 {
const [, _id] = decodeURIComponent(fragment).split('|')
return _id as Ref<Document>
const [, id] = decodeURIComponent(fragment).split('|')
if (id == null) {
return undefined
}
return (parseDocumentId(id) ?? id) as Ref<Document>
}
export function getDocumentUrl (doc: Document): string {
const id = getDocumentId(doc)
const id = getDocumentLinkId(doc)
const location = getCurrentResolvedLocation()
const frontUrl = getMetadata(presentation.metadata.FrontUrl)
@ -124,17 +129,17 @@ export function getDocumentLink (doc: Document): Location {
loc.fragment = undefined
loc.query = undefined
loc.path[2] = documentId
loc.path[3] = getDocumentId(doc)
loc.path[3] = getDocumentLinkId(doc)
return loc
}
function getDocumentId (doc: Document): string {
export function getDocumentLinkId (doc: Document): string {
const slug = slugify(doc.name, { lower: true })
return `${slug}-${doc._id}`
}
function parseDocumentId (shortLink: string): Ref<Document> | undefined {
export function parseDocumentId (shortLink: string): Ref<Document> | undefined {
const parts = shortLink.split('-')
if (parts.length > 1) {
return parts[parts.length - 1] as Ref<Document>

View File

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

View File

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

View File

@ -18,7 +18,13 @@ import activity, {
type DisplayDocUpdateMessage,
type DocUpdateMessage
} from '@hcengineering/activity'
import { activityMessagesComparator, combineActivityMessages, messageInFocus } from '@hcengineering/activity-resources'
import {
activityMessagesComparator,
combineActivityMessages,
isActivityMessageClass,
isReactionMessage,
messageInFocus
} from '@hcengineering/activity-resources'
import {
SortingOrder,
getCurrentAccount,
@ -31,8 +37,6 @@ import {
} from '@hcengineering/core'
import notification, {
NotificationStatus,
decodeObjectURI,
encodeObjectURI,
notificationId,
type ActivityInboxNotification,
type Collaborators,
@ -57,6 +61,9 @@ import { get, writable } from 'svelte/store'
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
import { type InboxData, type InboxNotificationsFilter } from './types'
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> {
if (docNotifyContext.hidden) {
@ -470,7 +477,7 @@ export async function resolveLocation (loc: Location): Promise<ResolvedLocation
async function generateLocation (
loc: Location,
_id: Ref<Doc>,
_id: string,
_class: Ref<Class<Doc>>
): Promise<ResolvedLocation | undefined> {
const client = getClient()
@ -511,12 +518,13 @@ async function generateLocation (
}
}
export function openInboxDoc (
async function navigateToInboxDoc (
providers: LinkIdProvider[],
_id?: Ref<Doc>,
_class?: Ref<Class<Doc>>,
thread?: Ref<ActivityMessage>,
message?: Ref<ActivityMessage>
): void {
): Promise<void> {
const loc = getLocation()
if (loc.path[2] !== notificationId) {
@ -524,14 +532,13 @@ export function openInboxDoc (
}
if (_id === undefined || _class === undefined) {
loc.query = { message: null }
loc.path.length = 3
localStorage.setItem(`${locationStorageKeyId}_${notificationId}`, JSON.stringify(loc))
navigate(loc)
resetInboxContext()
return
}
loc.path[3] = encodeObjectURI(_id, _class)
const id = await getObjectLinkId(providers, _id, _class)
loc.path[3] = encodeObjectURI(id, _class)
if (thread !== undefined) {
loc.path[4] = thread
@ -546,6 +553,96 @@ export function openInboxDoc (
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 async function checkPermission (value: boolean): Promise<boolean> {

View File

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

View File

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

View File

@ -149,7 +149,7 @@ export default mergeIds(recruitId, recruit, {
CreateCandidate: '' as AnyComponent
},
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>>,
AppIdentifierProvider: '' 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('-')
if (tokens.length < 2) {
return undefined
}
const classLabel = tokens[0]
const number = Number(tokens[1])
const client = getClient()
const hierarchy = client.getHierarchy()
const classes = [recruit.class.Applicant, recruit.class.Vacancy, recruit.class.Review]
let _class: Ref<Class<Doc>> | undefined
for (const clazz of classes) {
@ -109,6 +135,21 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Reso
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) {
console.error(`Not found class with short label ${classLabel}`)
return undefined

View File

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

View File

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

View File

@ -15,9 +15,7 @@
<script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, {
ActionContext,
ComponentExtensions,
@ -44,21 +42,23 @@
import view from '@hcengineering/view'
import { DocNavLink, ParentsNavigator, showMenu } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy } from 'svelte'
import { generateIssueShortLink } from '../../../issues'
import { generateIssueShortLink, getIssueIdByIdentifier } from '../../../issues'
import tracker from '../../../plugin'
import IssueStatusActivity from '../IssueStatusActivity.svelte'
import ControlPanel from './ControlPanel.svelte'
import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.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 embedded: boolean = false
export let kind: 'default' | 'modern' = 'default'
export let readonly: boolean = false
let lastId: Ref<Doc> = _id
let lastId: Ref<Issue> | undefined
const queryClient = createQuery()
const dispatch = createEventDispatcher()
const client = getClient()
@ -70,28 +70,39 @@
let descriptionBox: AttachmentStyleBoxCollabEditor
let showAllMixins: boolean
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
const inboxClient = InboxNotificationsClientImpl.getClient()
$: read(_id)
function read (_id: Ref<Doc>): void {
if (lastId !== _id) {
let issueId: Ref<Issue> | undefined
$: 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
lastId = _id
void inboxClient.then((client) => client.readDoc(getClient(), prev))
void inboxClient.readDoc(getClient(), prev)
}
}
onDestroy(async () => {
void inboxClient.then((client) => client.readDoc(getClient(), _id))
if (issueId === undefined) return
void inboxClient.readDoc(getClient(), issueId)
})
$: _id !== undefined &&
_class !== undefined &&
$: if (issueId !== undefined && _class !== undefined) {
queryClient.query<Issue>(
_class,
{ _id },
{ _id: issueId },
async (result) => {
if (lastId !== _id) {
if (lastId !== issueId) {
await save()
}
;[issue] = result
@ -103,6 +114,7 @@
limit: 1
}
)
}
$: canSave = title.trim().length > 0
$: 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 IssueStatusPresenter from './components/issues/IssueStatusPresenter.svelte'
import {
getIssueIdByIdentifier,
getIssueTitle,
getTitle,
issueIdentifierProvider,
@ -689,6 +690,7 @@ export default async (): Promise<Resources> => ({
ComponentTitleProvider: getComponentTitle,
MilestoneTitleProvider: getMilestoneTitle,
GetIssueId: getTitle,
GetIssueIdByIdentifier: getIssueIdByIdentifier,
GetIssueLink: issueLinkProvider,
GetIssueLinkFragment: issueLinkFragmentProvider,
GetIssueTitle: getIssueTitle,

View File

@ -41,7 +41,7 @@ export async function getTitle (doc: Doc): Promise<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> {
@ -126,3 +126,10 @@ export async function updateIssueRelation (
}
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 { mergeIds } from '@hcengineering/platform'
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 CreateAggregationManagerFunc,
@ -378,7 +378,7 @@ export default mergeIds(trackerId, tracker, {
IssueIdentifierProvider: '' 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>>,
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>>,
GetIssueLinkFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
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[]>>,
IsProjectJoined: '' as Resource<(space: Space) => Promise<boolean>>,
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: {
CreateComponentAggregationManager: '' as CreateAggregationManagerFunc,

View File

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

View File

@ -87,7 +87,8 @@ import view, {
type BuildModelOptions,
type CollectionPresenter,
type Viewlet,
type ViewletDescriptor
type ViewletDescriptor,
type LinkIdProvider
} from '@hcengineering/view'
import contact, { getName, type Contact, type PersonAccount } from '@hcengineering/contact'
@ -1023,8 +1024,16 @@ export async function getObjectLinkFragment (
}
}
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) {
loc.fragment = getPanelURI(component, object._id, Hierarchy.mixinOrClass(object), 'content')
loc.fragment = getPanelURI(component, id, Hierarchy.mixinOrClass(object), 'content')
}
return loc
}
@ -1448,3 +1457,43 @@ export function getCollaborationUser (): CollaborationUser {
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,
Viewlet,
ViewletDescriptor,
ViewletPreference
ViewletPreference,
LinkIdProvider
} from './types'
export * from './types'
export * from './utils'
/**
* @public
@ -103,6 +105,7 @@ const view = plugin(viewId, {
AllValuesFunc: '' as Ref<Mixin<AllValuesFunc>>,
ObjectPanel: '' as Ref<Mixin<ObjectPanel>>,
LinkProvider: '' as Ref<Mixin<LinkProvider>>,
LinkIdProvider: '' as Ref<Mixin<LinkIdProvider>>,
SpacePresenter: '' as Ref<Mixin<SpacePresenter>>,
AttributeFilterPresenter: '' as Ref<Mixin<AttributeFilterPresenter>>,
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>>
}
export interface LinkIdProvider extends Class<Doc> {
encode: Resource<(doc: Doc) => Promise<string>>
decode: Resource<(id: string) => Promise<Ref<Doc> | undefined>>
}
/**
* @public
*/

View File

@ -13,12 +13,12 @@
// 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>>] {
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('|')
}

View File

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

View File

@ -39,6 +39,7 @@
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"@hcengineering/server-core": "^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 { plugin } from '@hcengineering/platform'
import { Presenter } from '@hcengineering/server-notification'
@ -18,6 +19,7 @@ export const serverDocumentId = 'server-document' as Plugin
export default plugin(serverDocumentId, {
function: {
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/text": "^0.6.5",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/server-view": "^0.6.0",
"web-push": "~3.6.7"
}
}

View File

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

View File

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

View File

@ -40,6 +40,7 @@
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@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.
//
import { Doc } from '@hcengineering/core'
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core'
@ -31,7 +32,8 @@ export default plugin(serverRecruitId, {
ApplicationHTMLPresenter: '' as Resource<Presenter>,
ApplicationTextPresenter: '' as Resource<Presenter>,
VacancyHTMLPresenter: '' as Resource<Presenter>,
VacancyTextPresenter: '' as Resource<Presenter>
VacancyTextPresenter: '' as Resource<Presenter>,
LinkIdProvider: '' as Resource<(doc: Doc) => Promise<string>>
},
trigger: {
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
export default async () => ({
function: {
IssueHTMLPresenter: issueHTMLPresenter,
IssueTextPresenter: issueTextPresenter,
IssueNotificationContentProvider: getIssueNotificationContent
IssueNotificationContentProvider: getIssueNotificationContent,
IssueLinkIdProvider: issueLinkIdProvider
},
trigger: {
OnIssueUpdate,

View File

@ -40,6 +40,7 @@
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@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.
//
import { Doc } from '@hcengineering/core'
import type { Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { TriggerFunc } from '@hcengineering/server-core'
@ -30,7 +31,8 @@ export default plugin(serverTrackerId, {
function: {
IssueHTMLPresenter: '' as Resource<Presenter>,
IssueTextPresenter: '' as Resource<Presenter>,
IssueNotificationContentProvider: '' as Resource<NotificationContentProvider>
IssueNotificationContentProvider: '' as Resource<NotificationContentProvider>,
IssueLinkIdProvider: '' as Resource<(doc: Doc) => Promise<string>>
},
trigger: {
OnIssueUpdate: '' as Resource<TriggerFunc>,

View File

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

View File

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