UBER-769 Implement backlinks in server plugin (#3626)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2023-08-24 22:53:18 +07:00
committed by GitHub
parent 821ed6cd7b
commit 2c585b2ecd
41 changed files with 813 additions and 132 deletions
+73 -3
View File
@@ -206,6 +206,7 @@ specifiers:
'@rush-temp/templates-assets': file:./projects/templates-assets.tgz
'@rush-temp/templates-resources': file:./projects/templates-resources.tgz
'@rush-temp/tests-sanity': file:./projects/tests-sanity.tgz
'@rush-temp/text': file:./projects/text.tgz
'@rush-temp/text-editor': file:./projects/text-editor.tgz
'@rush-temp/theme': file:./projects/theme.tgz
'@rush-temp/tool': file:./projects/tool.tgz
@@ -249,6 +250,7 @@ specifiers:
'@tiptap/extension-task-item': ^2.0.3
'@tiptap/extension-task-list': ^2.0.3
'@tiptap/extension-typography': ^2.0.3
'@tiptap/html': ^2.0.3
'@tiptap/pm': ^2.0.3
'@tiptap/prosemirror-tables': ^1.1.4
'@tiptap/starter-kit': ^2.0.3
@@ -601,6 +603,7 @@ dependencies:
'@rush-temp/templates-assets': file:projects/templates-assets.tgz
'@rush-temp/templates-resources': file:projects/templates-resources.tgz_a1d864769aaf53d09b76fe134ab55e60
'@rush-temp/tests-sanity': file:projects/tests-sanity.tgz
'@rush-temp/text': file:projects/text.tgz_a7abb371f17d2bc77abec7bc53398db5
'@rush-temp/text-editor': file:projects/text-editor.tgz_1e646fd62a3adc9d0b41f256dafe3d38
'@rush-temp/theme': file:projects/theme.tgz_a1d864769aaf53d09b76fe134ab55e60
'@rush-temp/tool': file:projects/tool.tgz_bufferutil@4.0.7
@@ -644,6 +647,7 @@ dependencies:
'@tiptap/extension-task-item': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/extension-task-list': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-typography': 2.0.3_@tiptap+core@2.0.3
'@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/pm': 2.0.3_@tiptap+core@2.0.3
'@tiptap/prosemirror-tables': 1.1.4_a7abb371f17d2bc77abec7bc53398db5
'@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3
@@ -6587,6 +6591,17 @@ packages:
'@tiptap/core': 2.0.3_@tiptap+pm@2.0.3
dev: false
/@tiptap/html/2.1.6_fc2f730f0ef238c57ef10dc1abce4607:
resolution: {integrity: sha512-/7PcivMMty2Xw8oNEOqRi1W4WlD9afrmA3vlgNYGnO+knmwG5VzyhYV+oDlKAWvYqfrPxtfGnhNEQywzyHKbBg==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/pm': ^2.0.0
dependencies:
'@tiptap/core': 2.0.3_@tiptap+pm@2.0.3
'@tiptap/pm': 2.0.3_@tiptap+core@2.0.3
zeed-dom: 0.9.26
dev: false
/@tiptap/pm/2.0.3_@tiptap+core@2.0.3:
resolution: {integrity: sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==}
peerDependencies:
@@ -17210,6 +17225,13 @@ packages:
commander: 2.20.3
dev: false
/zeed-dom/0.9.26:
resolution: {integrity: sha512-HWjX8rA3Y/RI32zby3KIN1D+mgskce+She4K7kRyyx62OiVxJ5FnYm8vWq0YVAja3Tf2S1M0XAc6O2lRFcMgcQ==}
engines: {node: '>=14.13.1'}
dependencies:
css-what: 6.1.0
dev: false
file:projects/account.tgz_bufferutil@4.0.7:
resolution: {integrity: sha512-4tS8wBCO4BXrFfa11XEyJzCw6AG+7romshdy1O3kWS2by/z8IpsFcblvOk3Q3zaIExY6MSA0Wjdf+9od8vJMDA==, tarball: file:projects/account.tgz}
id: file:projects/account.tgz
@@ -20363,7 +20385,7 @@ packages:
dev: false
file:projects/prod.tgz_91152849c259b1b3925f7bb3d415bc80:
resolution: {integrity: sha512-dB1fl4vWrudVeRZJRFyOejr/xRHzIj4ZgkssAuS59WZIVVbyGvMgjPhC/tCGovi/O581wEJBBO1fgVLfOOXXiw==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-wZWIAN+CPlPommWoqqH2yf11giIQ+B7Gbx6NQ8hkQ3PeZS2zJWsN3Ki+4vYu+BOZHwthzEJn3Y9LhKsyTL9m2A==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@@ -20743,11 +20765,15 @@ packages:
dev: false
file:projects/server-chunter-resources.tgz:
resolution: {integrity: sha512-kNwG1PW31TnvKu/srs+aikf6idi3OZVIt5JTPnSBvGQ++A7ZUz/Cal5onvxUP72/zSfl2VIPSWUn6WViYx5xyg==, tarball: file:projects/server-chunter-resources.tgz}
resolution: {integrity: sha512-/t2NvPZEV7xa90ZuDnc/Bi5KK9bXAozhOmghDjB0ztXFX9QRibqnObPNkR6lKP4QCHeihGFeow0gp5lqqb0DbQ==, tarball: file:projects/server-chunter-resources.tgz}
name: '@rush-temp/server-chunter-resources'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@tiptap/core': 2.0.3_@tiptap+pm@2.0.3
'@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/pm': 2.0.3_@tiptap+core@2.0.3
'@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3
'@types/heft-jest': 1.0.3
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
@@ -22052,7 +22078,7 @@ packages:
dev: false
file:projects/text-editor.tgz_1e646fd62a3adc9d0b41f256dafe3d38:
resolution: {integrity: sha512-veer1goACGzBNOXgkYDbqPx/5obONA8llhpWAv2M+M/I2tumjLZxG4ohodKBQNa0ijAZ+MUgRlcaS9c8MsKF6A==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-Omwv7uuoSX4eicxv4jQUEegyhpg0QWeFuCr+DxCR3j7yfsFfF5mUCleLcX7LWh+M9EXbdldBdcHE3OriJg5ECQ==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0
@@ -22128,6 +22154,50 @@ packages:
- y-protocols
dev: false
file:projects/text.tgz_a7abb371f17d2bc77abec7bc53398db5:
resolution: {integrity: sha512-QfLKyYxcLXLoqBMnbP4k2ND5871b+ycKCz7+NZ7xBtPtEb3Ee6jPdFrf8wbmnIYX/dLV0VThNKXcJ4ZiWxNvbA==, tarball: file:projects/text.tgz}
id: file:projects/text.tgz
name: '@rush-temp/text'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.11
'@tiptap/core': 2.0.3_@tiptap+pm@2.0.3
'@tiptap/extension-gapcursor': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/extension-heading': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-highlight': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-link': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/extension-mention': 2.0.3_dc69df04ca9f1acb766e21a98969ce2c
'@tiptap/extension-table': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/extension-table-cell': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-table-header': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-table-row': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-task-item': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/extension-task-list': 2.0.3_@tiptap+core@2.0.3
'@tiptap/extension-typography': 2.0.3_@tiptap+core@2.0.3
'@tiptap/html': 2.1.6_fc2f730f0ef238c57ef10dc1abce4607
'@tiptap/pm': 2.0.3_@tiptap+core@2.0.3
'@tiptap/prosemirror-tables': 1.1.4_a7abb371f17d2bc77abec7bc53398db5
'@tiptap/starter-kit': 2.0.3_@tiptap+pm@2.0.3
'@tiptap/suggestion': 2.0.3_fc2f730f0ef238c57ef10dc1abce4607
'@types/heft-jest': 1.0.3
'@typescript-eslint/eslint-plugin': 5.42.1_d506b9be61cb4ac2646ecbc6e0680464
'@typescript-eslint/parser': 5.42.1_eslint@8.27.0+typescript@4.8.4
eslint: 8.27.0
eslint-config-standard-with-typescript: 23.0.0_c9fe9619f50f4e82337a86c3af25e566
eslint-plugin-import: 2.26.0_eslint@8.27.0
eslint-plugin-n: 15.5.1_eslint@8.27.0
eslint-plugin-promise: 6.1.1_eslint@8.27.0
prettier: 2.8.8
typescript: 4.8.4
transitivePeerDependencies:
- prosemirror-keymap
- prosemirror-model
- prosemirror-state
- prosemirror-transform
- prosemirror-view
- supports-color
dev: false
file:projects/theme.tgz_a1d864769aaf53d09b76fe134ab55e60:
resolution: {integrity: sha512-wnu6qKDGjkOGG0e/hcmvG2yHZv9oOFqO1UDrxFCa/+/oMuTnaRsSscLUjPiYWE3iHDFXZNaNJzVrZc7tCwXXQw==, tarball: file:projects/theme.tgz}
id: file:projects/theme.tgz
+1
View File
@@ -47,6 +47,7 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/theme": "^0.6.3",
"@hcengineering/text": "^0.6.0",
"svelte": "3.55.1",
"@hcengineering/login": "^0.6.7",
"@hcengineering/login-assets": "^0.6.0",
+5 -1
View File
@@ -1,5 +1,5 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022, 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@@ -41,6 +41,10 @@ export function createModel (builder: Builder): void {
}
)
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.BacklinkTrigger
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverChunter.trigger.ChunterTrigger
})
+1
View File
@@ -36,6 +36,7 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.27",
"@hcengineering/ui": "^0.6.10",
"@hcengineering/text": "^0.6.0",
"svelte": "3.55.1",
"@tiptap/core": "^2.0.3",
"@tiptap/pm": "^2.0.3",
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "default"
}
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@hcengineering/text",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"test": "heft test",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.26.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"prettier": "^2.7.1",
"@rushstack/heft": "^0.47.9",
"typescript": "^4.3.5"
},
"dependencies": {
"@tiptap/core": "^2.0.3",
"@tiptap/html": "^2.0.3",
"@tiptap/pm": "^2.0.3",
"@tiptap/starter-kit": "^2.0.3",
"@tiptap/extension-gapcursor": "^2.0.3",
"@tiptap/extension-heading": "^2.0.3",
"@tiptap/extension-highlight": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-mention": "^2.0.3",
"@tiptap/extension-table": "^2.0.3",
"@tiptap/extension-table-cell": "^2.0.3",
"@tiptap/extension-table-header": "^2.0.3",
"@tiptap/extension-table-row": "^2.0.3",
"@tiptap/extension-task-item": "^2.0.3",
"@tiptap/extension-task-list": "^2.0.3",
"@tiptap/extension-typography": "^2.0.3",
"@tiptap/suggestion": "^2.0.3",
"@tiptap/prosemirror-tables": "^1.1.4"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
+94
View File
@@ -0,0 +1,94 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { AnyExtension } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import Typography from '@tiptap/extension-typography'
import StarterKit from '@tiptap/starter-kit'
/**
* @public
*/
export const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
/**
* @public
*/
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
/**
* @public
*/
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
/**
* @public
*/
export const defaultExtensions: AnyExtension[] = [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: {
levels: headingLevels
}
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
...tableExtensions,
...taskListExtensions
]
+35
View File
@@ -0,0 +1,35 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Extensions, getSchema } from '@tiptap/core'
import { generateJSON, generateHTML } from '@tiptap/html'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
/**
* @public
*/
export function getHTML (node: ProseMirrorNode, extensions: Extensions): string {
return generateHTML(node.toJSON(), extensions)
}
/**
* @public
*/
export function parseHTML (content: string, extensions: Extensions): ProseMirrorNode {
const schema = getSchema(extensions)
const json = generateJSON(content, extensions)
return ProseMirrorNode.fromJSON(schema, json)
}
+18
View File
@@ -0,0 +1,18 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './extensions'
export * from './html'
export * from './nodes'
+16
View File
@@ -0,0 +1,16 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './reference'
+88
View File
@@ -0,0 +1,88 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Node, mergeAttributes } from '@tiptap/core'
/**
* @public
*/
export const ReferenceNode = Node.create({
name: 'reference',
group: 'inline',
content: 'inline*',
inline: true,
addAttributes () {
return {
id: {
default: null,
parseHTML: (element) => element.getAttribute('data-id'),
renderHTML: (attributes) => {
if (attributes.id === null) {
return {}
}
return {
'data-id': attributes.id
}
}
},
objectClass: {
default: null,
parseHTML: (element) => element.getAttribute('data-objectclass'),
renderHTML: (attributes) => {
if (attributes.objectClass === null) {
return {}
}
return {
'data-objectclass': attributes.objectClass
}
}
},
label: {
default: null,
parseHTML: (element) => element.getAttribute('data-label'),
renderHTML: (attributes) => {
if (attributes.label === null) {
return {}
}
return {
'data-label': attributes.label
}
}
},
class: {
default: null
}
}
},
parseHTML () {
return [
{
tag: `span[data-type="${this.name}"]`
}
]
},
renderHTML ({ HTMLAttributes }) {
return ['span', mergeAttributes({ 'data-type': this.name }, HTMLAttributes), 0]
}
})
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
}
}
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Markup, Ref, updateAttribute } from '@hcengineering/core'
import { Class, Doc, Ref, updateAttribute } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getAttribute, getClient, KeyedAttribute } from '@hcengineering/presentation'
@@ -27,7 +27,6 @@
export let key: KeyedAttribute
export let placeholder: IntlString
export let focusIndex = -1
export let updateBacklinks: ((doc: Doc, description: Markup) => void) | undefined = undefined
let _id: Ref<Doc> | undefined = undefined
let _class: Ref<Class<Doc>> | undefined = undefined
const client = getClient()
@@ -69,7 +68,6 @@
setTimeout(() => {
dispatch('saved', false)
}, 2500)
updateBacklinks?.(object, description)
}
await descriptionBox.createAttachments()
+1
View File
@@ -22,6 +22,7 @@
"Content": "Content",
"Comment": "Comment",
"Message": "Message",
"MessageOn": "Message on",
"Reference": "Reference",
"Chat": "Chat",
"In": "In",
+1
View File
@@ -22,6 +22,7 @@
"Content": "Содержимое",
"Comment": "Комментарий",
"Message": "Сообщение",
"MessageOn": "Сообщение в",
"MentionNotification": "Упомянул",
"Reference": "Ссылка",
"Chat": "Чат",
+2 -51
View File
@@ -1,56 +1,7 @@
import { Backlink, getBacklinks } from '@hcengineering/chunter'
import contact, { PersonAccount } from '@hcengineering/contact'
import { Account, Class, Client, Data, Doc, DocumentQuery, Ref, TxOperations } from '@hcengineering/core'
import { Backlink } from '@hcengineering/chunter'
import { Data, DocumentQuery, TxOperations } from '@hcengineering/core'
import chunter from './plugin'
export async function getUser (
client: Client,
user: Ref<PersonAccount> | Ref<Account>
): Promise<PersonAccount | undefined> {
return await client.findOne(contact.class.PersonAccount, { _id: user as Ref<PersonAccount> })
}
export function getTime (time: number): string {
let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
if (!isToday(time)) {
options = {
month: 'numeric',
day: 'numeric',
...options
}
}
return new Date(time).toLocaleString('default', options)
}
export function isToday (time: number): boolean {
const current = new Date()
const target = new Date(time)
return (
current.getDate() === target.getDate() &&
current.getMonth() === target.getMonth() &&
current.getFullYear() === target.getFullYear()
)
}
/**
* @public
*/
export async function updateBacklinks (
client: TxOperations,
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Promise<void> {
const q: DocumentQuery<Backlink> = { backlinkId, backlinkClass, collection: 'backlinks' }
if (attachedDocId !== undefined) {
q.attachedDocId = attachedDocId
}
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
await updateBacklinksList(client, q, backlinks)
}
/**
* @public
*/
@@ -15,7 +15,7 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { ChunterMessage, ChunterSpace, Message, createBacklinks } from '@hcengineering/chunter'
import { ChunterMessage, ChunterSpace, Message } from '@hcengineering/chunter'
import { Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getLocation, navigate } from '@hcengineering/ui'
@@ -48,9 +48,6 @@
_id
)
// Create an backlink to document
await createBacklinks(client, space, chunter.class.ChunterSpace, _id, message)
_id = generateId()
isScrollForced = true
loading = false
@@ -15,7 +15,7 @@
-->
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Comment, createBacklinks } from '@hcengineering/chunter'
import { Comment } from '@hcengineering/chunter'
import { AttachedData, Doc, generateId, Ref } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import chunter from '../plugin'
@@ -105,9 +105,6 @@
_id
)
// Create an backlink to document
await createBacklinks(client, object._id, object._class, _id, message)
// Remove draft from Local Storage
comment = getDefault()
_id = comment._id
@@ -19,10 +19,11 @@
import chunter from '@hcengineering/chunter'
import { Person, PersonAccount, getName } from '@hcengineering/contact'
import { Avatar, personByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref } from '@hcengineering/core'
import { MessageViewer, getClient } from '@hcengineering/presentation'
import { Icon, ShowMore, TimeSince } from '@hcengineering/ui'
import { LinkPresenter } from '@hcengineering/view-resources'
import { Doc, IdMap, Ref } from '@hcengineering/core'
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
import { Icon, Label, ShowMore, TimeSince } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { LinkPresenter, getObjectPresenter } from '@hcengineering/view-resources'
export let value: Comment
export let inline: boolean = false
@@ -48,6 +49,17 @@
$: links = getLinks(value.message)
let presenter: AttributeModel | undefined
getObjectPresenter(client, value.attachedToClass, { key: '' }).then((p) => {
presenter = p
})
let doc: Doc | undefined = undefined
const docQuery = createQuery()
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[doc] = res
})
function getLinks (content: string): HTMLLinkElement[] {
const parser = new DOMParser()
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
@@ -69,13 +81,21 @@
</script>
{#if inline}
<a class="flex-presenter inline-presenter" href="#{disabled ? null : ''}">
<div class="icon">
<Icon icon={chunter.icon.Thread} size={'small'} />
</div>
<span class="label nowrap">Message</span>
</a>
&nbsp;<span class="content-dark-color">#{cutId(value._id.toString())}</span>
<div class="flex-presenter inline-presenter">
{#if presenter && doc}
<span class="labels-row" style:text-transform={'lowercase'}><Label label={chunter.string.MessageOn} /></span>
&nbsp;
<div class="icon">
<Icon icon={chunter.icon.Thread} size={'small'} />
</div>
&nbsp;
<svelte:component this={presenter.presenter} value={doc} {inline} {disabled} />
{:else}
<Label label={chunter.string.Message} />
&nbsp;
<span class="content-dark-color">#{cutId(value._id.toString())}</span>
{/if}
</div>
{:else}
<div class="flex-row-top">
{#await getEmployee(value, $personByIdStore, $personAccountByIdStore) then employee}
@@ -1,6 +1,6 @@
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { DirectMessage, Message, createBacklinks, getDirectChannel } from '@hcengineering/chunter'
import chunter, { DirectMessage, Message, getDirectChannel } from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import { Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
@@ -46,8 +46,6 @@
messageId
)
await createBacklinks(client, space, chunter.class.ChunterSpace, messageId, message)
messageId = generateId()
}
</script>
@@ -13,10 +13,44 @@
// limitations under the License.
-->
<script lang="ts">
import { Message } from '@hcengineering/chunter'
import { MessageViewer } from '@hcengineering/presentation'
import chunter, { Message } from '@hcengineering/chunter'
import { Doc } from '@hcengineering/core'
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
import { Icon, Label } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
export let value: Message
export let inline: boolean = false
export let disabled = false
const client = getClient()
let presenter: AttributeModel | undefined
getObjectPresenter(client, value.attachedToClass, { key: '' }).then((p) => {
presenter = p
})
let doc: Doc | undefined = undefined
const docQuery = createQuery()
$: docQuery.query(value.attachedToClass, { _id: value.attachedTo }, (res) => {
;[doc] = res
})
</script>
<div><MessageViewer message={value.content} /></div>
{#if inline}
{#if presenter && doc}
<div class="flex-presenter inline-presenter">
<div class="icon">
<Icon icon={chunter.icon.Thread} size={'small'} />
</div>
<span class="labels-row" style:text-transform={'lowercase'}>
<Label label={chunter.string.MessageOn} />
</span>
&nbsp;
<svelte:component this={presenter.presenter} value={doc} inline {disabled} />
</div>
{/if}
{:else}
<div><MessageViewer message={value.content} /></div>
{/if}
@@ -15,14 +15,13 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { createBacklinks, type ChunterSpace, type Message, type ThreadMessage } from '@hcengineering/chunter'
import { type ChunterSpace, type Message, type ThreadMessage } from '@hcengineering/chunter'
import contact, { Person, PersonAccount, getName } from '@hcengineering/contact'
import { personByIdStore } from '@hcengineering/contact-resources'
import core, { FindOptions, IdMap, Ref, SortingOrder, generateId, getCurrentAccount } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import chunter from '../plugin'
import ChannelPresenter from './ChannelPresenter.svelte'
import DmPresenter from './DmPresenter.svelte'
@@ -136,9 +135,6 @@
commentId
)
// Create an backlink to document
await createBacklinks(client, parent._id, parent._class, commentId, message)
commentId = generateId()
loading = false
}
@@ -15,14 +15,13 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { createBacklinks, type ChunterMessage, type Message, type ThreadMessage } from '@hcengineering/chunter'
import { type ChunterMessage, type Message, type ThreadMessage } from '@hcengineering/chunter'
import core, { Doc, Ref, Space, generateId, getCurrentAccount } from '@hcengineering/core'
import { DocUpdates } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { IconClose, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
import { afterUpdate, beforeUpdate, createEventDispatcher } from 'svelte'
import chunter from '../plugin'
import { isMessageHighlighted, messageIdForScroll, scrollAndHighLight, shouldScrollToMessage } from '../utils'
import ChannelSeparator from './ChannelSeparator.svelte'
@@ -149,9 +148,6 @@
commentId
)
// Create an backlink to document
await createBacklinks(client, _id, chunter.class.Message, commentId, message)
commentId = generateId()
isScrollForced = true
loading = false
@@ -19,7 +19,6 @@
import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { updateBacklinks } from '../../backlinks'
import chunter from '../../plugin'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { LinkPresenter } from '@hcengineering/view-resources'
@@ -49,8 +48,6 @@
attachments
}
)
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, message)
} finally {
loading = false
}
-1
View File
@@ -67,7 +67,6 @@ import { getDmName, getLink, getTitle, resolveLocation } from './utils'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
export { CommentPopup, CommentsPresenter }
export { updateBacklinks } from './backlinks'
async function MarkUnread (object: Message): Promise<void> {
const client = NotificationClientImpl.getClient()
+1
View File
@@ -180,6 +180,7 @@ export default plugin(chunterId, {
UnarchiveChannel: '' as IntlString,
ArchiveConfirm: '' as IntlString,
Message: '' as IntlString,
MessageOn: '' as IntlString,
UnarchiveConfirm: '' as IntlString,
ConvertToPrivate: '' as IntlString
},
@@ -15,7 +15,6 @@
-->
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import core, { ClassifierKind, Data, Doc, Mixin, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
@@ -193,9 +192,6 @@
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
@@ -15,7 +15,6 @@
<script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import chunter, { Comment } from '@hcengineering/chunter'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import { PersonAccount } from '@hcengineering/contact'
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
@@ -69,8 +68,6 @@
await client.createMixin(_id, chunter.class.Comment, value.space, request.mixin.RequestDecisionComment, {})
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, message)
refInput.createAttachments()
loading = false
}
@@ -81,8 +78,6 @@
attachments
})
// We need to update backlinks before and after.
await updateBacklinks(client, value.attachedTo, value.attachedToClass, value._id, message)
loading = false
}
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import chunter, { createBacklinks } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter'
import { Employee } from '@hcengineering/contact'
import core, { Account, AttachedData, Doc, fillDefaults, generateId, Ref, SortingOrder } from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform'
@@ -429,9 +429,6 @@
}
)
// Create an backlink to document
await createBacklinks(client, _id, tracker.class.Issue, _id, object.description)
draftController.remove()
resetObject()
descriptionBox?.removeDraft(false)
@@ -1,6 +1,5 @@
<script lang="ts">
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import { getClient } from '@hcengineering/presentation'
import { Component } from '@hcengineering/tracker'
import { EditBox } from '@hcengineering/ui'
@@ -52,8 +51,5 @@
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
@@ -46,7 +46,6 @@
import CopyToClipboard from './CopyToClipboard.svelte'
import SubIssueSelector from './SubIssueSelector.svelte'
import SubIssues from './SubIssues.svelte'
import { updateBacklinks } from '@hcengineering/chunter-resources'
export let _id: Ref<Issue>
export let _class: Ref<Class<Issue>>
@@ -227,9 +226,6 @@
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
@@ -19,7 +19,6 @@
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { updateBacklinks } from '@hcengineering/chunter-resources'
export let object: Milestone
@@ -65,8 +64,5 @@
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={tracker.string.IssueDescriptionPlaceholder}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
@@ -15,7 +15,6 @@
<script lang="ts">
import { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createBacklinks } from '@hcengineering/chunter'
import { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import { Milestone, MilestoneStatus, Project } from '@hcengineering/tracker'
import ui, { DatePresenter, EditBox } from '@hcengineering/ui'
@@ -39,9 +38,7 @@
}
async function onSave () {
const _id = await client.createDoc(tracker.class.Milestone, space, object)
// Create an backlink to document
await createBacklinks(client, _id, tracker.class.Milestone, _id, object.description ?? '')
await client.createDoc(tracker.class.Milestone, space, object)
}
</script>
@@ -37,7 +37,6 @@
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import tracker from '../../plugin'
import { updateBacklinks } from '@hcengineering/chunter-resources'
import SubIssueTemplates from './IssueTemplateChilds.svelte'
import TemplateControlPanel from './TemplateControlPanel.svelte'
@@ -185,9 +184,6 @@
on:saved={(evt) => {
saved = evt.detail
}}
updateBacklinks={(doc, description) => {
updateBacklinks(client, doc._id, doc._class, doc._id, description)
}}
/>
</div>
<div class="mt-6">
+5
View File
@@ -481,6 +481,11 @@
"projectFolder": "packages/theme",
"shouldPublish": true
},
{
"packageName": "@hcengineering/text",
"projectFolder": "packages/text",
"shouldPublish": true
},
{
"packageName": "@hcengineering/ui",
"projectFolder": "packages/ui",
@@ -6,6 +6,7 @@
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"test": "heft test",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
@@ -36,6 +37,8 @@
"@hcengineering/workbench": "^0.6.8",
"@hcengineering/notification": "^0.6.14",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0"
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server": "^0.6.4",
"@hcengineering/text": "^0.6.0"
}
}
@@ -0,0 +1,72 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Class, Doc, Ref } from '@hcengineering/core'
import { getBacklinks } from '../backlinks'
describe('extractBacklinks', () => {
it('should return no backlinks for empty document', () => {
const content = '<p></p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
content
)
expect(backlinks).toEqual([])
})
it('should parse single backlink', () => {
const content =
'<p>hello <span class="reference" data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John">@Appleseed John</span> </p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
content
)
expect(backlinks.length).toBe(1)
expect(backlinks).toEqual([
{
attachedTo: 'id',
attachedToClass: 'contact:class:Person',
collection: 'backlinks',
backlinkId: 'backlinkId',
backlinkClass: 'backlinkClass',
message:
'hello <span data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John" class="reference">@Appleseed John</span>',
attachedDocId: 'attachedDocId'
}
])
})
it('should parse single backlink for multiple references', () => {
const content =
'<p><span class="reference" data-type="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> <span data-type="reference" class="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> </p>'
const backlinks = getBacklinks(
'backlinkId' as Ref<Doc>,
'backlinkClass' as Ref<Class<Doc>>,
'attachedDocId' as Ref<Doc>,
content
)
expect(backlinks.length).toBe(1)
})
})
@@ -0,0 +1,54 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Backlink } from '@hcengineering/chunter'
import { Class, Data, Doc, Ref } from '@hcengineering/core'
import { defaultExtensions, getHTML, parseHTML, ReferenceNode } from '@hcengineering/text'
const extensions = [...defaultExtensions, ReferenceNode]
export function getBacklinks (
backlinkId: Ref<Doc>,
backlinkClass: Ref<Class<Doc>>,
attachedDocId: Ref<Doc> | undefined,
content: string
): Array<Data<Backlink>> {
const doc = parseHTML(content, extensions)
const result: Array<Data<Backlink>> = []
doc.descendants((node, _pos, parent): boolean => {
if (node.type.name === ReferenceNode.name) {
const ato = node.attrs.id as Ref<Doc>
const atoClass = node.attrs.objectClass as Ref<Class<Doc>>
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
result.push({
attachedTo: ato,
attachedToClass: atoClass,
collection: 'backlinks',
backlinkId,
backlinkClass,
message: parent !== null ? getHTML(parent, extensions) : '',
attachedDocId
})
}
}
return true
})
return result
}
+192 -1
View File
@@ -1,5 +1,5 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
// Copyright © 2022, 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@@ -29,6 +29,7 @@ import core, {
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxFactory,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
@@ -38,6 +39,130 @@ import { getMetadata } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { getDocCollaborators, getMixinTx, pushNotification } from '@hcengineering/server-notification-resources'
import { workbenchId } from '@hcengineering/workbench'
import { getBacklinks } from './backlinks'
function getCreateBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Doc): Tx[] {
const txes: Tx[] = []
const backlinkId = doc._id
const backlinkClass = doc._class
const attachedDocId = doc._id
const attributes = control.hierarchy.getAllAttributes(doc._class)
for (const attr of attributes.values()) {
if (attr.type._class === core.class.TypeMarkup) {
const content = (doc as any)[attr.name]?.toString() ?? ''
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
for (const backlink of backlinks) {
const innerTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink)
const collectionTx = txFactory.createTxCollectionCUD(
backlink.attachedToClass,
backlink.attachedTo,
chunter.space.Backlinks,
backlink.collection,
innerTx
)
txes.push(collectionTx)
}
}
}
return txes
}
async function getUpdateBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Doc): Promise<Tx[]> {
const txes: Tx[] = []
const backlinkId = doc._id
const backlinkClass = doc._class
const attachedDocId = doc._id
const attributes = control.hierarchy.getAllAttributes(doc._class)
for (const attr of attributes.values()) {
if (attr.type._class === core.class.TypeMarkup) {
const content = (doc as any)[attr.name]?.toString() ?? ''
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
const current = await control.findAll(chunter.class.Backlink, {
backlinkId,
backlinkClass,
attachedDocId,
collection: 'backlinks'
})
for (const c of current) {
// Find existing and check if we need to update message
const pos = backlinks.findIndex(
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
)
if (pos !== -1) {
// Update existing backlinks when message changed
const data = backlinks[pos]
if (c.message !== data.message) {
const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, {
message: data.message
})
txes.push(
txFactory.createTxCollectionCUD(
c.attachedToClass,
c.attachedTo,
chunter.space.Backlinks,
c.collection,
innerTx
)
)
}
backlinks.splice(pos, 1)
} else {
// Remove not found backlinks
const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id)
txes.push(
txFactory.createTxCollectionCUD(
c.attachedToClass,
c.attachedTo,
chunter.space.Backlinks,
c.collection,
innerTx
)
)
}
}
// Add missing backlinks
for (const backlink of backlinks) {
const backlinkTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink)
txes.push(
txFactory.createTxCollectionCUD(
backlink.attachedToClass,
backlink.attachedTo,
chunter.space.Backlinks,
backlink.collection,
backlinkTx
)
)
}
}
}
return txes
}
async function getRemoveBacklinksTxes (control: TriggerControl, txFactory: TxFactory, doc: Ref<Doc>): Promise<Tx[]> {
const txes: Tx[] = []
const backlinks = await control.findAll(chunter.class.Backlink, { attachedDocId: doc, collection: 'backlinks' })
for (const b of backlinks) {
const innerTx = txFactory.createTxRemoveDoc(b._class, b.space, b._id)
txes.push(
txFactory.createTxCollectionCUD(b.attachedToClass, b.attachedTo, chunter.space.Backlinks, b.collection, innerTx)
)
}
return txes
}
/**
* @public
@@ -193,6 +318,62 @@ async function ThreadMessageDelete (tx: Tx, control: TriggerControl): Promise<Tx
return [updateTx]
}
async function BacklinksCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (ctx._class !== core.class.TxCreateDoc) return []
if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
const txFactory = new TxFactory(control.txFactory.account)
const doc = TxProcessor.createDoc2Doc(ctx)
const txes: Tx[] = getCreateBacklinksTxes(control, txFactory, doc)
if (txes.length !== 0) {
await control.apply(txes, true)
}
return []
}
async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
if (ctx._class !== core.class.TxUpdateDoc) return []
if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0]
if (rawDoc === undefined) return []
const txFactory = new TxFactory(control.txFactory.account)
if (rawDoc !== undefined) {
const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx)
const txes: Tx[] = await getUpdateBacklinksTxes(control, txFactory, doc)
if (txes.length !== 0) {
await control.apply(txes, true)
}
}
return []
}
async function BacklinksRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
if (ctx._class !== core.class.TxRemoveDoc) return []
if (hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
const txFactory = new TxFactory(control.txFactory.account)
const txes: Tx[] = await getRemoveBacklinksTxes(control, txFactory, ctx.objectId)
if (txes.length !== 0) {
await control.apply(txes, true)
}
return []
}
/**
* @public
*/
@@ -255,6 +436,15 @@ export async function OnMessageSent (tx: Tx, control: TriggerControl): Promise<T
return res
}
/**
* @public
*/
export async function BacklinkTrigger (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const promises = [BacklinksCreate(tx, control), BacklinksUpdate(tx, control), BacklinksRemove(tx, control)]
const res = await Promise.all(promises)
return res.flat()
}
/**
* @public
*/
@@ -321,6 +511,7 @@ export async function IsChannelMessage (
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
BacklinkTrigger,
ChunterTrigger,
OnMessageSent
},
+1
View File
@@ -29,6 +29,7 @@ export const serverChunterId = 'server-chunter' as Plugin
*/
export default plugin(serverChunterId, {
trigger: {
BacklinkTrigger: '' as Resource<TriggerFunc>,
ChunterTrigger: '' as Resource<TriggerFunc>,
OnMessageSent: '' as Resource<TriggerFunc>
},