UBERF-4725 Migrate collaborative content (#5717)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-08-22 18:03:42 +07:00 committed by GitHub
parent 0e72b85978
commit df2a9b2708
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 702 additions and 820 deletions

View File

@ -243,6 +243,7 @@ jobs:
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/front.log
docker logs $(docker ps | grep collaborator | cut -f 1 -d ' ') > logs/collaborator.log
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -438,6 +439,7 @@ jobs:
docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/uweb-transactor.log docker logs $(docker ps | grep transactor | cut -f 1 -d ' ') > logs/uweb-transactor.log
docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/uweb-account.log docker logs $(docker ps | grep account | cut -f 1 -d ' ') > logs/uweb-account.log
docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/uweb-front.log docker logs $(docker ps | grep front | cut -f 1 -d ' ') > logs/uweb-front.log
docker logs $(docker ps | grep collaborator | cut -f 1 -d ' ') > logs/uweb-collaborator.log
- name: Upload test results - name: Upload test results
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

6
.vscode/launch.json vendored
View File

@ -43,7 +43,6 @@
"SERVER_SECRET": "secret", "SERVER_SECRET": "secret",
"ENABLE_CONSOLE": "true", "ENABLE_CONSOLE": "true",
"COLLABORATOR_URL": "ws://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"REKONI_URL": "http://localhost:4004", "REKONI_URL": "http://localhost:4004",
"FRONT_URL": "http://localhost:8080", "FRONT_URL": "http://localhost:8080",
"ACCOUNTS_URL": "http://localhost:3000", "ACCOUNTS_URL": "http://localhost:3000",
@ -104,7 +103,6 @@
"UPLOAD_URL": "/files", "UPLOAD_URL": "/files",
"SERVER_PORT": "8087", "SERVER_PORT": "8087",
"COLLABORATOR_URL": "ws://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"CALENDAR_URL": "http://localhost:8095", "CALENDAR_URL": "http://localhost:8095",
"GMAIL_URL": "http://localhost:8088", "GMAIL_URL": "http://localhost:8088",
"TELEGRAM_URL": "http://localhost:8086", "TELEGRAM_URL": "http://localhost:8086",
@ -239,7 +237,7 @@
"CLIENT_ID": "${env:POD_GITHUB_CLIENTID}", "CLIENT_ID": "${env:POD_GITHUB_CLIENTID}",
"CLIENT_SECRET": "${env:POD_GITHUB_CLIENT_SECRET}", "CLIENT_SECRET": "${env:POD_GITHUB_CLIENT_SECRET}",
"PRIVATE_KEY": "${env:POD_GITHUB_PRIVATE_KEY}", "PRIVATE_KEY": "${env:POD_GITHUB_PRIVATE_KEY}",
"COLLABORATOR_API_URL": "http://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"SYSTEM_EMAIL": "anticrm@hc.engineering", "SYSTEM_EMAIL": "anticrm@hc.engineering",
"MINIO_ENDPOINT": "localhost", "MINIO_ENDPOINT": "localhost",
"MINIO_ACCESS_KEY": "minioadmin", "MINIO_ACCESS_KEY": "minioadmin",
@ -280,7 +278,7 @@
"env": { "env": {
"SERVER_SECRET": "secret", "SERVER_SECRET": "secret",
"ACCOUNTS_URL": "http://localhost:3000", "ACCOUNTS_URL": "http://localhost:3000",
"COLLABORATOR_API_URL": "http://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin", "STORAGE_CONFIG": "minio|localhost?accessKey=minioadmin&secretKey=minioadmin",
"MONGO_URL": "mongodb://localhost:27017" "MONGO_URL": "mongodb://localhost:27017"
}, },

View File

@ -203,7 +203,6 @@ export async function configurePlatform (): Promise<void> {
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL) setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
setMetadata(presentation.metadata.FilesURL, config.FILES_URL) setMetadata(presentation.metadata.FilesURL, config.FILES_URL)
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL) setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL)
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))
setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL)

View File

@ -6,7 +6,6 @@ import { ScreenSource } from '@hcengineering/love'
export interface Config { export interface Config {
ACCOUNTS_URL: string ACCOUNTS_URL: string
COLLABORATOR_URL: string COLLABORATOR_URL: string
COLLABORATOR_API_URL: string
FRONT_URL: string FRONT_URL: string
FILES_URL: string FILES_URL: string
UPLOAD_URL: string UPLOAD_URL: string

View File

@ -15,7 +15,7 @@
"build:watch": "compile", "build:watch": "compile",
"_phase:bundle": "rushx bundle", "_phase:bundle": "rushx bundle",
"bundle": "mkdir -p bundle && node esbuild.js", "bundle": "mkdir -p bundle && node esbuild.js",
"run-local": "cross-env SERVER_SECRET=secret MONGO_URL=mongodb://localhost:27017 COLLABORATOR_URL=ws://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin PRODUCT_ID=ezqms node --nolazy -r ts-node/register ./src/__start.ts", "run-local": "cross-env SERVER_SECRET=secret MONGO_URL=mongodb://localhost:27017 COLLABORATOR_URL=ws://localhost:3078 STORAGE_CONFIG=minio|minio?accessKey=minioadmin&secretKey=minioadmin PRODUCT_ID=ezqms node --nolazy -r ts-node/register ./src/__start.ts",
"run": "cross-env node -r ts-node/register --max-old-space-size=8000 ./src/__start.ts", "run": "cross-env node -r ts-node/register --max-old-space-size=8000 ./src/__start.ts",
"format": "format src", "format": "format src",
"test": "jest --passWithNoTests --silent", "test": "jest --passWithNoTests --silent",

View File

@ -8,7 +8,7 @@ import { HtmlConversionBackend } from './convert/convert'
export interface Config { export interface Config {
doc: string doc: string
token: string token: string
collaboratorApiURL: string collaboratorURL: string
uploadURL: string uploadURL: string
workspaceId: WorkspaceId workspaceId: WorkspaceId
owner: Ref<Employee> owner: Ref<Employee>

View File

@ -21,7 +21,7 @@ import core, {
Ref, Ref,
TxOperations, TxOperations,
generateId, generateId,
getCollaborativeDoc, makeCollaborativeDoc,
systemAccountEmail, systemAccountEmail,
type Blob type Blob
} from '@hcengineering/core' } from '@hcengineering/core'
@ -100,7 +100,7 @@ async function createDocument (
abstract: '', abstract: '',
effectiveDate: 0, effectiveDate: 0,
reviewInterval: DEFAULT_PERIODIC_REVIEW_INTERVAL, reviewInterval: DEFAULT_PERIODIC_REVIEW_INTERVAL,
content: getCollaborativeDoc(generateId()), content: makeCollaborativeDoc(generateId()),
snapshots: 0, snapshots: 0,
plannedEffectiveDate: 0 plannedEffectiveDate: 0
} }
@ -168,7 +168,7 @@ async function createTemplateIfNotExist (
approvers: [], approvers: [],
coAuthors: [], coAuthors: [],
changeControl: ccRecordId, changeControl: ccRecordId,
content: getCollaborativeDoc(generateId()), content: makeCollaborativeDoc(generateId()),
snapshots: 0, snapshots: 0,
plannedEffectiveDate: 0 plannedEffectiveDate: 0
} }
@ -208,8 +208,8 @@ async function createSections (
throw new Error(`Invalid document: ${JSON.stringify(doc)}`) throw new Error(`Invalid document: ${JSON.stringify(doc)}`)
} }
const { collaboratorApiURL, token, workspaceId } = config const { collaboratorURL, token, workspaceId } = config
const collaborator = getCollaboratorClient(txops.getHierarchy(), workspaceId, token, collaboratorApiURL) const collaborator = getCollaboratorClient(workspaceId, token, collaboratorURL)
console.log('Creating document content') console.log('Creating document content')
@ -229,7 +229,7 @@ async function createSections (
content += `<h1>${section.title}</h1>${section.content}` content += `<h1>${section.title}</h1>${section.content}`
} }
await collaborator.updateContent(collabId, 'content', content) await collaborator.updateContent(collabId, { content })
} finally { } finally {
// do nothing // do nothing
} }

View File

@ -44,8 +44,8 @@ export function docImportTool (): void {
process.exit(1) process.exit(1)
} }
const collaboratorApiUrl = process.env.COLLABORATOR_API_URL const collaboratorUrl = process.env.COLLABORATOR_URL
if (collaboratorApiUrl === undefined) { if (collaboratorUrl === undefined) {
console.error('please provide collaborator url') console.error('please provide collaborator url')
process.exit(1) process.exit(1)
} }
@ -104,7 +104,7 @@ export function docImportTool (): void {
space: cmd.space, space: cmd.space,
uploadURL: uploadUrl, uploadURL: uploadUrl,
storageAdapter, storageAdapter,
collaboratorApiURL: collaboratorApiUrl, collaboratorURL: collaboratorUrl,
token: generateToken(systemAccountEmail, workspaceId) token: generateToken(systemAccountEmail, workspaceId)
} }

View File

@ -106,7 +106,6 @@ services:
- TELEGRAM_URL=http://localhost:8086 - TELEGRAM_URL=http://localhost:8086
- REKONI_URL=http://localhost:4004 - REKONI_URL=http://localhost:4004
- COLLABORATOR_URL=ws://localhost:3078 - COLLABORATOR_URL=ws://localhost:3078
- COLLABORATOR_API_URL=http://localhost:3078
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- GITHUB_URL=http://localhost:3500 - GITHUB_URL=http://localhost:3500
- PRINT_URL=http://localhost:4005 - PRINT_URL=http://localhost:4005

View File

@ -93,7 +93,6 @@ services:
- TELEGRAM_URL=http://localhost:8086 - TELEGRAM_URL=http://localhost:8086
- REKONI_URL=http://localhost:4004 - REKONI_URL=http://localhost:4004
- COLLABORATOR_URL=ws://localhost:3078 - COLLABORATOR_URL=ws://localhost:3078
- COLLABORATOR_API_URL=http://localhost:3078
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- GITHUB_URL=http://localhost:3500 - GITHUB_URL=http://localhost:3500
- PRINT_URL=http://localhost:4005 - PRINT_URL=http://localhost:4005

View File

@ -5,9 +5,8 @@ FRONT_URL=http://localhost:8080
REKONI_URL=http://localhost:4004 REKONI_URL=http://localhost:4004
COLLABORATOR_URL=ws://locahost:3078 COLLABORATOR_URL=ws://locahost:3078
COLLABORATOR_API_URL=http://locahost:3078
PRINT_URL=http://localhost:4005 PRINT_URL=http://localhost:4005
SIGN_URL=http://localhost:4006 SIGN_URL=http://localhost:4006
ANALYTICS_COLLECTOR_URL=http://localhost:4007 ANALYTICS_COLLECTOR_URL=http://localhost:4007

View File

@ -1,7 +1,6 @@
{ {
"ACCOUNTS_URL":"http://localhost:3000", "ACCOUNTS_URL":"http://localhost:3000",
"COLLABORATOR_URL": "ws://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"UPLOAD_URL":"/files", "UPLOAD_URL":"/files",
"REKONI_URL": "http://localhost:4004", "REKONI_URL": "http://localhost:4004",
"PRINT_URL": "http://localhost:4005", "PRINT_URL": "http://localhost:4005",

View File

@ -6,6 +6,5 @@
"GMAIL_URL": "https://gmail.hc.engineering", "GMAIL_URL": "https://gmail.hc.engineering",
"CALENDAR_URL": "https://calendar.hc.engineering", "CALENDAR_URL": "https://calendar.hc.engineering",
"REKONI_URL": "https://rekoni.hc.engineering", "REKONI_URL": "https://rekoni.hc.engineering",
"COLLABORATOR_URL": "wss://collaborator.hc.engineering", "COLLABORATOR_URL": "wss://collaborator.hc.engineering"
"COLLABORATOR_API_URL": "https://collaborator.hc.engineering"
} }

View File

@ -1,7 +1,6 @@
{ {
"ACCOUNTS_URL":"/account", "ACCOUNTS_URL":"/account",
"COLLABORATOR_URL": "ws://localhost:3078", "COLLABORATOR_URL": "ws://localhost:3078",
"COLLABORATOR_API_URL": "http://localhost:3078",
"UPLOAD_URL":"/files", "UPLOAD_URL":"/files",
"TELEGRAM_URL": "http://localhost:8086", "TELEGRAM_URL": "http://localhost:8086",
"GMAIL_URL": "http://localhost:8088", "GMAIL_URL": "http://localhost:8088",

View File

@ -124,7 +124,6 @@ export interface Config {
MODEL_VERSION: string MODEL_VERSION: string
VERSION: string VERSION: string
COLLABORATOR_URL: string COLLABORATOR_URL: string
COLLABORATOR_API_URL: string
REKONI_URL: string REKONI_URL: string
TELEGRAM_URL: string TELEGRAM_URL: string
GMAIL_URL: string GMAIL_URL: string
@ -288,7 +287,6 @@ export async function configurePlatform() {
setMetadata(presentation.metadata.FilesURL, config.FILES_URL) setMetadata(presentation.metadata.FilesURL, config.FILES_URL)
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL) setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL) setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL)
setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL) setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL)
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG)) setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))

View File

@ -1214,10 +1214,6 @@ async function updateId (
const markup = (contentDoc as any)[attrName] as Markup const markup = (contentDoc as any)[attrName] as Markup
const newMarkup = markup.replaceAll(doc._id, newId) const newMarkup = markup.replaceAll(doc._id, newId)
await update(h, db, contentDoc, { [attrName]: newMarkup }) await update(h, db, contentDoc, { [attrName]: newMarkup })
} else if (attr.type._class === core.class.TypeCollaborativeMarkup) {
const markup = (contentDoc as any)[attrName]
const newMarkup = markup.replaceAll(doc._id, newId)
await update(h, db, contentDoc, { [attrName]: newMarkup })
} else if (attr.type._class === core.class.TypeCollaborativeDoc) { } else if (attr.type._class === core.class.TypeCollaborativeDoc) {
const collaborativeDoc = (contentDoc as any)[attr.name] as CollaborativeDoc const collaborativeDoc = (contentDoc as any)[attr.name] as CollaborativeDoc
await updateYDoc(ctx, collaborativeDoc, storage, workspaceId, contentDoc, newId, doc) await updateYDoc(ctx, collaborativeDoc, storage, workspaceId, contentDoc, newId, doc)

View File

@ -7,7 +7,7 @@ import core, {
type MeasureContext, type MeasureContext,
type Ref, type Ref,
type WorkspaceId, type WorkspaceId,
getCollaborativeDocId makeCollaborativeDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { getMongoClient, getWorkspaceDB } from '@hcengineering/mongo' import { getMongoClient, getWorkspaceDB } from '@hcengineering/mongo'
import { type StorageAdapter } from '@hcengineering/server-core' import { type StorageAdapter } from '@hcengineering/server-core'
@ -39,10 +39,7 @@ export async function fixJsonMarkup (
const attributes = hierarchy.getAllAttributes(_class) const attributes = hierarchy.getAllAttributes(_class)
const filtered = Array.from(attributes.values()).filter((attribute) => { const filtered = Array.from(attributes.values()).filter((attribute) => {
return ( return hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup)
hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) ||
hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)
)
}) })
if (filtered.length === 0) continue if (filtered.length === 0) continue
@ -88,7 +85,7 @@ async function processFixJsonMarkupFor (
} }
if (res !== value) { if (res !== value) {
update[attribute.name] = res update[attribute.name] = res
remove.push(getCollaborativeDocId(doc._id, attribute.name)) remove.push(makeCollaborativeDoc(doc._id, attribute.name))
} }
} }
} catch {} } catch {}

View File

@ -46,9 +46,7 @@ async function migrateMarkup (client: MigrationClient): Promise<void> {
DOMAIN_ACTIVITY, DOMAIN_ACTIVITY,
{ {
_class: activity.class.DocUpdateMessage, _class: activity.class.DocUpdateMessage,
'attributeUpdates.attrClass': { 'attributeUpdates.attrClass': core.class.TypeMarkup
$in: [core.class.TypeMarkup, core.class.TypeCollaborativeMarkup]
}
}, },
{ {
projection: { projection: {

View File

@ -39,8 +39,8 @@ import {
IndexKind, IndexKind,
type Blob, type Blob,
type Class, type Class,
type CollaborativeDoc,
type Domain, type Domain,
type Markup,
type Ref, type Ref,
type Timestamp type Timestamp
} from '@hcengineering/core' } from '@hcengineering/core'
@ -54,7 +54,7 @@ import {
ReadOnly, ReadOnly,
TypeBlob, TypeBlob,
TypeBoolean, TypeBoolean,
TypeCollaborativeMarkup, TypeCollaborativeDoc,
TypeDate, TypeDate,
TypeRecord, TypeRecord,
TypeRef, TypeRef,
@ -173,9 +173,9 @@ export class TMember extends TAttachedDoc implements Member {
@Model(contact.class.Organization, contact.class.Contact) @Model(contact.class.Organization, contact.class.Contact)
@UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name', undefined, contact.string.Organizations) @UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name', undefined, contact.string.Organizations)
export class TOrganization extends TContact implements Organization { export class TOrganization extends TContact implements Organization {
@Prop(TypeCollaborativeMarkup(), core.string.Description) @Prop(TypeCollaborativeDoc(), core.string.Description)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description?: Markup description!: CollaborativeDoc
@Prop(Collection(contact.class.Member), contact.string.Members) @Prop(Collection(contact.class.Member), contact.string.Members)
members!: number members!: number

View File

@ -8,7 +8,7 @@ import {
TxOperations, TxOperations,
generateId, generateId,
DOMAIN_TX, DOMAIN_TX,
getCollaborativeDoc, makeCollaborativeDoc,
MeasureMetricsContext, MeasureMetricsContext,
type Class, type Class,
type Doc, type Doc,
@ -143,7 +143,7 @@ async function createProductChangeControlTemplate (tx: TxOperations): Promise<vo
minor: 1, minor: 1,
state: DocumentState.Effective, state: DocumentState.Effective,
commentSequence: 0, commentSequence: 0,
content: getCollaborativeDoc(generateId()) content: makeCollaborativeDoc(generateId())
}, },
ccCategory ccCategory
) )

View File

@ -31,6 +31,8 @@
"@hcengineering/core": "^0.6.32", "@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11", "@hcengineering/model": "^0.6.11",
"@hcengineering/platform": "^0.6.11", "@hcengineering/platform": "^0.6.11",
"@hcengineering/storage": "^0.6.0" "@hcengineering/storage": "^0.6.0",
"@hcengineering/collaboration": "^0.6.0",
"@hcengineering/text": "^0.6.5"
} }
} }

View File

@ -233,10 +233,6 @@ export class TTypeFileSize extends TType {}
@Model(core.class.TypeMarkup, core.class.Type) @Model(core.class.TypeMarkup, core.class.Type)
export class TTypeMarkup extends TType {} export class TTypeMarkup extends TType {}
@UX(core.string.Collaborative)
@Model(core.class.TypeCollaborativeMarkup, core.class.Type)
export class TTypeCollaborativeMarkup extends TType {}
@UX(core.string.Ref) @UX(core.string.Ref)
@Model(core.class.RefTo, core.class.Type) @Model(core.class.RefTo, core.class.Type)
export class TRefTo extends TType implements RefTo<Doc> { export class TRefTo extends TType implements RefTo<Doc> {

View File

@ -61,7 +61,6 @@ import {
TTypeBoolean, TTypeBoolean,
TTypeCollaborativeDoc, TTypeCollaborativeDoc,
TTypeCollaborativeDocVersion, TTypeCollaborativeDocVersion,
TTypeCollaborativeMarkup,
TTypeDate, TTypeDate,
TTypeFileSize, TTypeFileSize,
TTypeHyperlink, TTypeHyperlink,
@ -141,7 +140,6 @@ export function createModel (builder: Builder): void {
TTypeMarkup, TTypeMarkup,
TTypeCollaborativeDoc, TTypeCollaborativeDoc,
TTypeCollaborativeDocVersion, TTypeCollaborativeDocVersion,
TTypeCollaborativeMarkup,
TArrOf, TArrOf,
TRefTo, TRefTo,
TTypeDate, TTypeDate,

View File

@ -13,16 +13,23 @@
// limitations under the License. // limitations under the License.
// //
import { saveCollaborativeDoc, takeCollaborativeDocSnapshot } from '@hcengineering/collaboration'
import core, { import core, {
DOMAIN_BLOB, DOMAIN_BLOB,
DOMAIN_DOC_INDEX_STATE, DOMAIN_DOC_INDEX_STATE,
DOMAIN_STATUS, DOMAIN_STATUS,
DOMAIN_TX, DOMAIN_TX,
MeasureMetricsContext, MeasureMetricsContext,
collaborativeDocParse,
coreId, coreId,
generateId, generateId,
isClassIndexable, isClassIndexable,
makeCollaborativeDoc,
type AnyAttribute,
type Blob, type Blob,
type Doc,
type Domain,
type MeasureContext,
type Ref, type Ref,
type Space, type Space,
type Status, type Status,
@ -33,10 +40,14 @@ import {
tryMigrate, tryMigrate,
tryUpgrade, tryUpgrade,
type MigrateOperation, type MigrateOperation,
type MigrateUpdate,
type MigrationClient, type MigrationClient,
type MigrationDocumentQuery,
type MigrationIterator,
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import { type StorageAdapterEx } from '@hcengineering/storage' import { type StorageAdapter, type StorageAdapterEx } from '@hcengineering/storage'
import { markupToYDoc } from '@hcengineering/text'
import { DOMAIN_SPACE } from './security' import { DOMAIN_SPACE } from './security'
async function migrateStatusesToModel (client: MigrationClient): Promise<void> { async function migrateStatusesToModel (client: MigrationClient): Promise<void> {
@ -143,6 +154,101 @@ async function migrateStatusTransactions (client: MigrationClient): Promise<void
) )
} }
async function migrateCollaborativeContentToStorage (client: MigrationClient): Promise<void> {
const ctx = new MeasureMetricsContext('migrate_content', {})
const storageAdapter = client.storageAdapter
const hierarchy = client.hierarchy
const classes = hierarchy.getDescendants(core.class.Doc)
for (const _class of classes) {
const domain = hierarchy.findDomain(_class)
if (domain === undefined) continue
const attributes = hierarchy.getAllAttributes(_class)
const filtered = Array.from(attributes.values()).filter((attribute) => {
return hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)
})
if (filtered.length === 0) continue
const iterator = await client.traverse(domain, { _class })
try {
console.log('processing', _class)
await processMigrateContentFor(ctx, domain, filtered, client, storageAdapter, iterator)
} finally {
await iterator.close()
}
}
}
async function processMigrateContentFor (
ctx: MeasureContext,
domain: Domain,
attributes: AnyAttribute[],
client: MigrationClient,
storageAdapter: StorageAdapter,
iterator: MigrationIterator<Doc>
): Promise<void> {
let processed = 0
while (true) {
const docs = await iterator.next(1000)
if (docs === null || docs.length === 0) {
break
}
const timestamp = Date.now()
const revisionId = `${timestamp}`
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
for (const doc of docs) {
const update: MigrateUpdate<Doc> = {}
for (const attribute of attributes) {
const collaborativeDoc = makeCollaborativeDoc(doc._id, attribute.name, revisionId)
const value = (doc as any)[attribute.name] as string
if (value != null && value.startsWith('{')) {
const { documentId } = collaborativeDocParse(collaborativeDoc)
const blob = await storageAdapter.stat(ctx, client.workspaceId, documentId)
// only for documents not in storage
if (blob === undefined) {
const ydoc = markupToYDoc(value, attribute.name)
await saveCollaborativeDoc(storageAdapter, client.workspaceId, collaborativeDoc, ydoc, ctx)
await takeCollaborativeDocSnapshot(
storageAdapter,
client.workspaceId,
collaborativeDoc,
ydoc,
{
versionId: revisionId,
name: 'Migration to storage',
createdBy: core.account.System,
createdOn: Date.now()
},
ctx
)
}
update[attribute.name] = collaborativeDoc
} else if (value == null) {
update[attribute.name] = makeCollaborativeDoc(doc._id, attribute.name, revisionId)
}
}
if (Object.keys(update).length > 0) {
operations.push({ filter: { _id: doc._id }, update })
}
}
if (operations.length > 0) {
await client.bulk(domain, operations)
}
processed += docs.length
console.log('...processed', processed)
}
}
export const coreOperation: MigrateOperation = { export const coreOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
// We need to delete all documents in doc index state for missing classes // We need to delete all documents in doc index state for missing classes
@ -189,6 +295,10 @@ export const coreOperation: MigrateOperation = {
func: async (client: MigrationClient) => { func: async (client: MigrationClient) => {
await client.update(DOMAIN_DOC_INDEX_STATE, {}, { $set: { needIndex: true } }) await client.update(DOMAIN_DOC_INDEX_STATE, {}, { $set: { needIndex: true } })
} }
},
{
state: 'collaborative-content-to-storage',
func: migrateCollaborativeContentToStorage
} }
]) ])
}, },

View File

@ -13,15 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { type Attachment } from '@hcengineering/attachment' import { DOMAIN_TX, MeasureMetricsContext } from '@hcengineering/core'
import {
DOMAIN_TX,
getCollaborativeDoc,
MeasureMetricsContext,
type Class,
type Doc,
type Ref
} from '@hcengineering/core'
import { type Document, type Teamspace } from '@hcengineering/document' import { type Document, type Teamspace } from '@hcengineering/document'
import { import {
tryMigrate, tryMigrate,
@ -29,126 +21,12 @@ import {
type MigrationClient, type MigrationClient,
type MigrationUpgradeClient type MigrationUpgradeClient
} from '@hcengineering/model' } from '@hcengineering/model'
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import { type Asset } from '@hcengineering/platform' import { type Asset } from '@hcengineering/platform'
import document, { documentId, DOMAIN_DOCUMENT } from './index' import document, { documentId, DOMAIN_DOCUMENT } from './index'
import { loadCollaborativeDoc, saveCollaborativeDoc, yDocCopyXmlField } from '@hcengineering/collaboration' import { loadCollaborativeDoc, saveCollaborativeDoc, yDocCopyXmlField } from '@hcengineering/collaboration'
async function migrateCollaborativeContent (client: MigrationClient): Promise<void> {
const attachedFiles = await client.find<Attachment>(DOMAIN_ATTACHMENT, {
_class: 'document:class:CollaboratorDocument' as Ref<Class<Doc>>,
attachedToClass: document.class.Document
})
for (const attachment of attachedFiles) {
const collaborativeDoc = getCollaborativeDoc(attachment._id)
await client.update(
DOMAIN_DOCUMENT,
{
_id: attachment.attachedTo,
_class: attachment.attachedToClass,
content: {
$exists: false
}
},
{
$set: {
content: collaborativeDoc
}
}
)
}
// delete snapshots in old format
await client.deleteMany(DOMAIN_DOCUMENT, {
_class: 'document:class:DocumentSnapshot' as Ref<Class<Doc>>,
contentId: { $exists: true }
})
await client.update(
DOMAIN_DOCUMENT,
{
_class: document.class.Document,
snapshots: { $gt: 0 }
},
{
$set: {
snapshots: 0
}
}
)
// delete old snapshot transactions
await client.deleteMany(DOMAIN_TX, {
_class: core.class.TxCollectionCUD,
objectClass: document.class.Document,
collection: 'snapshots'
})
}
async function fixCollaborativeContentId (client: MigrationClient): Promise<void> {
const documents = await client.find<Document>(DOMAIN_DOCUMENT, {
content: { $exists: true }
})
// there was a wrong migration that assigned incorrect collaborative doc id
for (const document of documents) {
if (!document.content.includes(':')) {
await client.update(DOMAIN_DOCUMENT, { _id: document._id }, { content: getCollaborativeDoc(document.content) })
}
}
}
async function migrateWrongDomainContent (client: MigrationClient): Promise<void> {
// migrate content saved into wrong domain
const attachedFiles = await client.find<Attachment>(DOMAIN_DOCUMENT, {
_class: 'document:class:CollaboratorDocument' as Ref<Class<Doc>>,
attachedToClass: document.class.Document
})
for (const attachment of attachedFiles) {
const collaborativeDoc = getCollaborativeDoc(attachment._id)
await client.update(
DOMAIN_DOCUMENT,
{
_id: attachment.attachedTo,
_class: attachment.attachedToClass,
content: {
$exists: false
}
},
{
$set: {
content: collaborativeDoc
}
}
)
}
await client.move(
DOMAIN_DOCUMENT,
{
_class: 'document:class:CollaboratorDocument' as Ref<Class<Doc>>,
attachedToClass: document.class.Document
},
DOMAIN_ATTACHMENT
)
}
async function migrateDeleteCollaboratorDocument (client: MigrationClient): Promise<void> {
await client.deleteMany(DOMAIN_ATTACHMENT, { _class: 'document:class:CollaboratorDocument' as Ref<Class<Doc>> })
await client.deleteMany(DOMAIN_DOCUMENT, { _class: 'document:class:CollaboratorDocument' as Ref<Class<Doc>> })
await client.deleteMany(DOMAIN_TX, {
_class: core.class.TxCollectionCUD,
collection: 'attachments',
'tx.objectClass': 'document:class:CollaboratorDocument' as Ref<Class<Doc>>
})
}
async function migrateDocumentIcons (client: MigrationClient): Promise<void> { async function migrateDocumentIcons (client: MigrationClient): Promise<void> {
await client.update<Teamspace>( await client.update<Teamspace>(
DOMAIN_SPACE, DOMAIN_SPACE,
@ -173,60 +51,6 @@ async function migrateDocumentIcons (client: MigrationClient): Promise<void> {
) )
} }
async function setNoParent (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_DOCUMENT,
{
_class: document.class.Document,
attachedTo: { $exists: false }
},
{
$set: {
attachedTo: document.ids.NoParent,
attachedToClass: document.class.Document
}
}
)
const docsWithParent = (await client.find(DOMAIN_DOCUMENT, {
_class: document.class.Document,
attachedTo: { $exists: true, $ne: document.ids.NoParent }
})) as Document[]
for (const doc of docsWithParent) {
const parent = await client.find(DOMAIN_DOCUMENT, {
_class: document.class.Document,
_id: doc.attachedTo
})
if (parent.length === 0) continue
if (parent[0].space !== doc.space) {
await client.update(
DOMAIN_DOCUMENT,
{
_class: document.class.Document,
_id: doc._id
},
{
$set: {
attachedTo: document.ids.NoParent
}
}
)
}
}
await client.update(
DOMAIN_DOCUMENT,
{
_class: document.class.Document,
attachedTo: ''
},
{
$set: {
attachedTo: document.ids.NoParent
}
}
)
}
async function migrateTeamspaces (client: MigrationClient): Promise<void> { async function migrateTeamspaces (client: MigrationClient): Promise<void> {
await client.update( await client.update(
DOMAIN_SPACE, DOMAIN_SPACE,
@ -306,28 +130,6 @@ async function migrateContentField (client: MigrationClient): Promise<void> {
export const documentOperation: MigrateOperation = { export const documentOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, documentId, [ await tryMigrate(client, documentId, [
{
state: 'migrate-no-parent',
func: async (client) => {
await setNoParent(client)
}
},
{
state: 'collaborativeContent',
func: migrateCollaborativeContent
},
{
state: 'fixCollaborativeContentId',
func: fixCollaborativeContentId
},
{
state: 'wrongDomainContent',
func: migrateWrongDomainContent
},
{
state: 'deleteCollaboratorDocument',
func: migrateDeleteCollaboratorDocument
},
{ {
state: 'updateDocumentIcons', state: 'updateDocumentIcons',
func: migrateDocumentIcons func: migrateDocumentIcons

View File

@ -17,6 +17,7 @@ import type { Employee } from '@hcengineering/contact'
import { import {
Account, Account,
IndexKind, IndexKind,
type CollaborativeDoc,
type Role, type Role,
type RolesAssignment, type RolesAssignment,
type Ref, type Ref,
@ -31,7 +32,7 @@ import {
Model, Model,
Prop, Prop,
ReadOnly, ReadOnly,
TypeCollaborativeMarkup, TypeCollaborativeDoc,
TypeDate, TypeDate,
TypeMarkup, TypeMarkup,
TypeRef, TypeRef,
@ -94,9 +95,9 @@ export class TCustomer extends TContact implements Customer {
@Prop(Collection(lead.class.Lead), lead.string.Leads) @Prop(Collection(lead.class.Lead), lead.string.Leads)
leads?: number leads?: number
@Prop(TypeCollaborativeMarkup(), core.string.Description) @Prop(TypeCollaborativeDoc(), core.string.Description)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: string description!: CollaborativeDoc
} }
@Mixin(lead.mixin.DefaultFunnelTypeData, lead.class.Funnel) @Mixin(lead.mixin.DefaultFunnelTypeData, lead.class.Funnel)

View File

@ -17,6 +17,7 @@ import type { Employee, Organization } from '@hcengineering/contact'
import { import {
Account, Account,
IndexKind, IndexKind,
type CollaborativeDoc,
type Domain, type Domain,
type Markup, type Markup,
type Ref, type Ref,
@ -34,7 +35,7 @@ import {
Prop, Prop,
ReadOnly, ReadOnly,
TypeBoolean, TypeBoolean,
TypeCollaborativeMarkup, TypeCollaborativeDoc,
TypeDate, TypeDate,
TypeMarkup, TypeMarkup,
TypeRef, TypeRef,
@ -63,9 +64,9 @@ import recruit from './plugin'
@Model(recruit.class.Vacancy, task.class.Project) @Model(recruit.class.Vacancy, task.class.Project)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy, 'VCN', 'name', undefined, recruit.string.Vacancies) @UX(recruit.string.Vacancy, recruit.icon.Vacancy, 'VCN', 'name', undefined, recruit.string.Vacancies)
export class TVacancy extends TProject implements Vacancy { export class TVacancy extends TProject implements Vacancy {
@Prop(TypeCollaborativeMarkup(), recruit.string.FullDescription) @Prop(TypeCollaborativeDoc(), recruit.string.FullDescription)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
fullDescription?: string fullDescription!: CollaborativeDoc
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files }) @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number attachments?: number

View File

@ -34,10 +34,7 @@ async function migrateMarkup (client: MigrationClient): Promise<void> {
const attributes = hierarchy.getAllAttributes(_class) const attributes = hierarchy.getAllAttributes(_class)
const filtered = Array.from(attributes.values()).filter((attribute) => { const filtered = Array.from(attributes.values()).filter((attribute) => {
return ( return hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup)
hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) ||
hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)
)
}) })
if (filtered.length === 0) continue if (filtered.length === 0) continue
@ -106,10 +103,7 @@ async function fixMigrateMarkup (client: MigrationClient): Promise<void> {
const attributes = hierarchy.getAllAttributes(_class) const attributes = hierarchy.getAllAttributes(_class)
const filtered = Array.from(attributes.values()).filter((attribute) => { const filtered = Array.from(attributes.values()).filter((attribute) => {
return ( return hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup)
hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) ||
hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)
)
}) })
if (filtered.length === 0) continue if (filtered.length === 0) continue

View File

@ -18,8 +18,9 @@ import contact, { type Employee, type Person } from '@hcengineering/contact'
import { import {
DOMAIN_MODEL, DOMAIN_MODEL,
DateRangeMode, DateRangeMode,
type Domain,
IndexKind, IndexKind,
type CollaborativeDoc,
type Domain,
type Markup, type Markup,
type Ref, type Ref,
type RelatedDocument, type RelatedDocument,
@ -39,7 +40,7 @@ import {
Model, Model,
Prop, Prop,
ReadOnly, ReadOnly,
TypeCollaborativeMarkup, TypeCollaborativeDoc,
TypeDate, TypeDate,
TypeMarkup, TypeMarkup,
TypeNumber, TypeNumber,
@ -182,9 +183,9 @@ export class TIssue extends TTask implements Issue {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
title!: string title!: string
@Prop(TypeCollaborativeMarkup(), tracker.string.Description) @Prop(TypeCollaborativeDoc(), tracker.string.Description)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: Markup description!: CollaborativeDoc
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, { @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, {
_id: tracker.attribute.IssueStatus, _id: tracker.attribute.IssueStatus,
@ -275,7 +276,7 @@ export class TIssueTemplate extends TDoc implements IssueTemplate {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
title!: string title!: string
@Prop(TypeCollaborativeMarkup(), tracker.string.Description) @Prop(TypeMarkup(), tracker.string.Description)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
description!: Markup description!: Markup

View File

@ -495,17 +495,8 @@ export function createModel (builder: Builder): void {
editor: view.component.HTMLEditor editor: view.component.HTMLEditor
}) })
classPresenter( builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.ActivityAttributePresenter, {
builder, presenter: view.component.MarkupDiffPresenter
core.class.TypeCollaborativeMarkup,
view.component.MarkupPresenter,
undefined,
undefined,
view.component.MarkupDiffPresenter
)
builder.mixin(core.class.TypeCollaborativeMarkup, core.class.Class, view.mixin.InlineAttributEditor, {
editor: view.component.CollaborativeHTMLEditor
}) })
builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.InlineAttributEditor, { builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.InlineAttributEditor, {

View File

@ -16,33 +16,39 @@
import { import {
Account, Account,
CollaborativeDoc, CollaborativeDoc,
Hierarchy,
Markup, Markup,
Ref, Ref,
Timestamp, Timestamp,
WorkspaceId, WorkspaceId,
collaborativeDocWithLastVersion,
collaborativeDocWithVersion, collaborativeDocWithVersion,
concatLink concatLink
} from '@hcengineering/core' } from '@hcengineering/core'
import { DocumentId } from './types' import { DocumentId } from './types'
import { formatMinioDocumentId } from './utils' import { formatMinioDocumentId } from './utils'
/** @public */
export interface DocumentSnapshotParams {
createdBy: Ref<Account>
versionId: string
versionName?: string
}
/** @public */ /** @public */
export interface GetContentRequest { export interface GetContentRequest {
documentId: DocumentId documentId: DocumentId
field: string
} }
/** @public */ /** @public */
export interface GetContentResponse { export interface GetContentResponse {
html: string content: Record<string, Markup>
} }
/** @public */ /** @public */
export interface UpdateContentRequest { export interface UpdateContentRequest {
documentId: DocumentId documentId: DocumentId
field: string content: Record<string, Markup>
html: string snapshot?: DocumentSnapshotParams
} }
/** @public */ /** @public */
@ -54,6 +60,7 @@ export interface CopyContentRequest {
documentId: DocumentId documentId: DocumentId
sourceField: string sourceField: string
targetField: string targetField: string
snapshot?: DocumentSnapshotParams
} }
/** @public */ /** @public */
@ -82,8 +89,7 @@ export interface RemoveDocumentResponse {}
/** @public */ /** @public */
export interface TakeSnapshotRequest { export interface TakeSnapshotRequest {
documentId: DocumentId documentId: DocumentId
createdBy: Ref<Account> snapshot: DocumentSnapshotParams
snapshotName: string
} }
/** @public */ /** @public */
@ -95,38 +101,36 @@ export interface TakeSnapshotResponse {
createdOn: Timestamp createdOn: Timestamp
} }
/** @public */
export interface CollaborativeDocSnapshotParams {
snapshotName: string
createdBy: Ref<Account>
}
/** @public */ /** @public */
export interface CollaboratorClient { export interface CollaboratorClient {
// field operations // field operations
getContent: (collaborativeDoc: CollaborativeDoc, field: string) => Promise<Markup> getContent: (collaborativeDoc: CollaborativeDoc) => Promise<Record<string, Markup>>
updateContent: (collaborativeDoc: CollaborativeDoc, field: string, value: Markup) => Promise<void> updateContent: (
copyContent: (collaborativeDoc: CollaborativeDoc, sourceField: string, targetField: string) => Promise<void> document: CollaborativeDoc,
content: Record<string, Markup>,
snapshot?: DocumentSnapshotParams
) => Promise<CollaborativeDoc>
copyContent: (
document: CollaborativeDoc,
sourceField: string,
targetField: string,
snapshot?: DocumentSnapshotParams
) => Promise<CollaborativeDoc>
// document operations // document operations
branch: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise<void> branch: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise<void>
remove: (collaborativeDoc: CollaborativeDoc) => Promise<void> remove: (collaborativeDoc: CollaborativeDoc) => Promise<void>
snapshot: (collaborativeDoc: CollaborativeDoc, params: CollaborativeDocSnapshotParams) => Promise<CollaborativeDoc> snapshot: (collaborativeDoc: CollaborativeDoc, params: DocumentSnapshotParams) => Promise<CollaborativeDoc>
} }
/** @public */ /** @public */
export function getClient ( export function getClient (workspaceId: WorkspaceId, token: string, collaboratorUrl: string): CollaboratorClient {
hierarchy: Hierarchy, const url = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://')
workspaceId: WorkspaceId, return new CollaboratorClientImpl(workspaceId, token, url)
token: string,
collaboratorUrl: string
): CollaboratorClient {
return new CollaboratorClientImpl(hierarchy, workspaceId, token, collaboratorUrl)
} }
class CollaboratorClientImpl implements CollaboratorClient { class CollaboratorClientImpl implements CollaboratorClient {
constructor ( constructor (
private readonly hierarchy: Hierarchy,
private readonly workspace: WorkspaceId, private readonly workspace: WorkspaceId,
private readonly token: string, private readonly token: string,
private readonly collaboratorUrl: string private readonly collaboratorUrl: string
@ -153,30 +157,43 @@ class CollaboratorClientImpl implements CollaboratorClient {
return result return result
} }
async getContent (document: CollaborativeDoc, field: string): Promise<Markup> { async getContent (document: CollaborativeDoc): Promise<Record<string, Markup>> {
const workspace = this.workspace.name const workspace = this.workspace.name
const documentId = formatMinioDocumentId(workspace, document) const documentId = formatMinioDocumentId(workspace, document)
const payload: GetContentRequest = { documentId, field } const payload: GetContentRequest = { documentId }
const res = (await this.rpc('getContent', payload)) as GetContentResponse const res = (await this.rpc('getContent', payload)) as GetContentResponse
return res.html ?? '' return res.content ?? {}
} }
async updateContent (document: CollaborativeDoc, field: string, value: Markup): Promise<void> { async updateContent (
document: CollaborativeDoc,
content: Record<string, Markup>,
snapshot?: DocumentSnapshotParams
): Promise<CollaborativeDoc> {
const workspace = this.workspace.name const workspace = this.workspace.name
const documentId = formatMinioDocumentId(workspace, document) const documentId = formatMinioDocumentId(workspace, document)
const payload: UpdateContentRequest = { documentId, field, html: value } const payload: UpdateContentRequest = { documentId, content, snapshot }
await this.rpc('updateContent', payload) await this.rpc('updateContent', payload)
return snapshot !== undefined ? collaborativeDocWithLastVersion(document, snapshot.versionId) : document
} }
async copyContent (document: CollaborativeDoc, sourceField: string, targetField: string): Promise<void> { async copyContent (
document: CollaborativeDoc,
sourceField: string,
targetField: string,
snapshot?: DocumentSnapshotParams
): Promise<CollaborativeDoc> {
const workspace = this.workspace.name const workspace = this.workspace.name
const documentId = formatMinioDocumentId(workspace, document) const documentId = formatMinioDocumentId(workspace, document)
const payload: CopyContentRequest = { documentId, sourceField, targetField } const payload: CopyContentRequest = { documentId, sourceField, targetField, snapshot }
await this.rpc('copyContent', payload) await this.rpc('copyContent', payload)
return snapshot !== undefined ? collaborativeDocWithLastVersion(document, snapshot.versionId) : document
} }
async branch (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> { async branch (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> {
@ -198,11 +215,11 @@ class CollaboratorClientImpl implements CollaboratorClient {
await this.rpc('removeDocument', payload) await this.rpc('removeDocument', payload)
} }
async snapshot (document: CollaborativeDoc, params: CollaborativeDocSnapshotParams): Promise<CollaborativeDoc> { async snapshot (document: CollaborativeDoc, snapshot: DocumentSnapshotParams): Promise<CollaborativeDoc> {
const workspace = this.workspace.name const workspace = this.workspace.name
const documentId = formatMinioDocumentId(workspace, document) const documentId = formatMinioDocumentId(workspace, document)
const payload: TakeSnapshotRequest = { documentId, ...params } const payload: TakeSnapshotRequest = { documentId, snapshot }
const res = (await this.rpc('takeSnapshot', payload)) as TakeSnapshotResponse const res = (await this.rpc('takeSnapshot', payload)) as TakeSnapshotResponse
return collaborativeDocWithVersion(document, res.versionId) return collaborativeDocWithVersion(document, res.versionId)

View File

@ -17,7 +17,6 @@ import {
Class, Class,
CollaborativeDoc, CollaborativeDoc,
Doc, Doc,
Domain,
Ref, Ref,
collaborativeDocChain, collaborativeDocChain,
collaborativeDocFormat, collaborativeDocFormat,
@ -80,24 +79,21 @@ export function parseDocumentId (documentId: DocumentId): {
/** @public */ /** @public */
export function formatPlatformDocumentId ( export function formatPlatformDocumentId (
objectDomain: Domain,
objectClass: Ref<Class<Doc>>, objectClass: Ref<Class<Doc>>,
objectId: Ref<Doc>, objectId: Ref<Doc>,
objectAttr: string objectAttr: string
): PlatformDocumentId { ): PlatformDocumentId {
return `${objectDomain}/${objectClass}/${objectId}/${objectAttr}` as PlatformDocumentId return `${objectClass}/${objectId}/${objectAttr}` as PlatformDocumentId
} }
/** @public */ /** @public */
export function parsePlatformDocumentId (platformDocumentId: PlatformDocumentId): { export function parsePlatformDocumentId (platformDocumentId: PlatformDocumentId): {
objectDomain: Domain
objectClass: Ref<Class<Doc>> objectClass: Ref<Class<Doc>>
objectId: Ref<Doc> objectId: Ref<Doc>
objectAttr: string objectAttr: string
} { } {
const [objectDomain, objectClass, objectId, objectAttr] = platformDocumentId.split('/') const [objectClass, objectId, objectAttr] = platformDocumentId.split('/')
return { return {
objectDomain: objectDomain as Domain,
objectClass: objectClass as Ref<Class<Doc>>, objectClass: objectClass as Ref<Class<Doc>>,
objectId: objectId as Ref<Doc>, objectId: objectId as Ref<Doc>,
objectAttr objectAttr

View File

@ -44,16 +44,16 @@ export type CollaborativeDocVersion = string | typeof CollaborativeDocVersionHea
export const CollaborativeDocVersionHead = 'HEAD' export const CollaborativeDocVersionHead = 'HEAD'
/** @public */ /** @public */
export function getCollaborativeDocId (objectId: Ref<Doc>, objectAttr?: string | undefined): string { export function makeCollaborativeDoc (
return objectAttr !== undefined && objectAttr !== '' ? `${objectId}%${objectAttr}` : `${objectId}` objectId: Ref<Doc>,
} objectAttr?: string | undefined,
versionId?: string | undefined
/** @public */ ): CollaborativeDoc {
export function getCollaborativeDoc (documentId: string): CollaborativeDoc { const storageDocumentId = objectAttr !== undefined && objectAttr !== '' ? `${objectId}%${objectAttr}` : `${objectId}`
return collaborativeDocFormat({ return collaborativeDocFormat({
documentId, documentId: storageDocumentId,
versionId: CollaborativeDocVersionHead, versionId: CollaborativeDocVersionHead,
lastVersionId: CollaborativeDocVersionHead lastVersionId: versionId ?? '0'
}) })
} }

View File

@ -34,7 +34,6 @@ import type {
Hyperlink, Hyperlink,
IndexingConfiguration, IndexingConfiguration,
Interface, Interface,
Markup,
MigrationState, MigrationState,
Obj, Obj,
Permission, Permission,
@ -122,7 +121,6 @@ export default plugin(coreId, {
TypeDate: '' as Ref<Class<Type<Timestamp | Date>>>, TypeDate: '' as Ref<Class<Type<Timestamp | Date>>>,
TypeCollaborativeDoc: '' as Ref<Class<Type<CollaborativeDoc>>>, TypeCollaborativeDoc: '' as Ref<Class<Type<CollaborativeDoc>>>,
TypeCollaborativeDocVersion: '' as Ref<Class<Type<CollaborativeDoc>>>, TypeCollaborativeDocVersion: '' as Ref<Class<Type<CollaborativeDoc>>>,
TypeCollaborativeMarkup: '' as Ref<Class<Type<Markup>>>,
RefTo: '' as Ref<Class<RefTo<Doc>>>, RefTo: '' as Ref<Class<RefTo<Doc>>>,
ArrOf: '' as Ref<Class<ArrOf<Doc>>>, ArrOf: '' as Ref<Class<ArrOf<Doc>>>,
Enum: '' as Ref<Class<Enum>>, Enum: '' as Ref<Class<Enum>>,

View File

@ -265,21 +265,25 @@ function _generateTx (tx: ClassTxes): Tx[] {
[ClassifierKind.INTERFACE]: core.class.Interface, [ClassifierKind.INTERFACE]: core.class.Interface,
[ClassifierKind.MIXIN]: core.class.Mixin [ClassifierKind.MIXIN]: core.class.Mixin
} }
const createTx = txFactory.createTxCreateDoc<Doc>( const createTx = txFactory.createTxCreateDoc<Classifier>(
_cl[tx.kind], _cl[tx.kind],
core.space.Model, core.space.Model,
{ {
...(tx.domain !== undefined ? { domain: tx.domain } : {}), ...(tx.domain !== undefined ? { domain: tx.domain } : {}),
kind: tx.kind, kind: tx.kind,
label: tx.label,
icon: tx.icon,
...(tx.kind === ClassifierKind.INTERFACE ...(tx.kind === ClassifierKind.INTERFACE
? { extends: tx.implements } ? { extends: tx.implements }
: { extends: tx.extends, implements: tx.implements }), : { extends: tx.extends, implements: tx.implements }),
label: tx.label, ...(tx.kind === ClassifierKind.INTERFACE
icon: tx.icon, ? { extends: tx.implements }
shortLabel: tx.shortLabel, : {
sortingKey: tx.sortingKey, shortLabel: tx.shortLabel,
filteringKey: tx.filteringKey, sortingKey: tx.sortingKey,
pluralLabel: tx.pluralLabel filteringKey: tx.filteringKey,
pluralLabel: tx.pluralLabel
})
}, },
objectId objectId
) )
@ -412,13 +416,6 @@ export function TypeMarkup (): Type<Markup> {
return { _class: core.class.TypeMarkup, label: core.string.Markup } return { _class: core.class.TypeMarkup, label: core.string.Markup }
} }
/**
* @public
*/
export function TypeCollaborativeMarkup (): Type<Markup> {
return { _class: core.class.TypeCollaborativeMarkup, label: core.string.Collaborative }
}
/** /**
* @public * @public
*/ */

View File

@ -13,34 +13,36 @@
// limitations under the License. // limitations under the License.
// //
import { type CollaboratorClient, getClient as getCollaborator } from '@hcengineering/collaborator-client' import {
type CollaboratorClient,
getClient as getCollaborator,
type DocumentSnapshotParams
} from '@hcengineering/collaborator-client'
import { type CollaborativeDoc, type Markup, getCurrentAccount, getWorkspaceId } from '@hcengineering/core' import { type CollaborativeDoc, type Markup, getCurrentAccount, getWorkspaceId } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { getCurrentLocation } from '@hcengineering/ui' import { getCurrentLocation } from '@hcengineering/ui'
import { getClient } from '.'
import presentation from './plugin' import presentation from './plugin'
/** @public */ /** @public */
export function getCollaboratorClient (): CollaboratorClient { export function getCollaboratorClient (): CollaboratorClient {
const workspaceId = getWorkspaceId(getCurrentLocation().path[1] ?? '') const workspaceId = getWorkspaceId(getCurrentLocation().path[1] ?? '')
const hierarchy = getClient().getHierarchy()
const token = getMetadata(presentation.metadata.Token) ?? '' const token = getMetadata(presentation.metadata.Token) ?? ''
const collaboratorURL = getMetadata(presentation.metadata.CollaboratorApiUrl) ?? '' const collaboratorURL = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''
return getCollaborator(hierarchy, workspaceId, token, collaboratorURL) return getCollaborator(workspaceId, token, collaboratorURL)
} }
/** @public */ /** @public */
export async function getMarkup (collaborativeDoc: CollaborativeDoc, field: string): Promise<Markup> { export async function getMarkup (collaborativeDoc: CollaborativeDoc): Promise<Record<string, Markup>> {
const client = getCollaboratorClient() const client = getCollaboratorClient()
return await client.getContent(collaborativeDoc, field) return await client.getContent(collaborativeDoc)
} }
/** @public */ /** @public */
export async function updateMarkup (collaborativeDoc: CollaborativeDoc, field: string, value: Markup): Promise<void> { export async function updateMarkup (collaborativeDoc: CollaborativeDoc, content: Record<string, Markup>): Promise<void> {
const client = getCollaboratorClient() const client = getCollaboratorClient()
await client.updateContent(collaborativeDoc, field, value) await client.updateContent(collaborativeDoc, content)
} }
/** @public */ /** @public */
@ -60,12 +62,10 @@ export async function copyDocument (source: CollaborativeDoc, target: Collaborat
} }
/** @public */ /** @public */
export async function takeSnapshot ( export async function takeSnapshot (collaborativeDoc: CollaborativeDoc, versionName: string): Promise<CollaborativeDoc> {
collaborativeDoc: CollaborativeDoc,
snapshotName: string
): Promise<CollaborativeDoc> {
const client = getCollaboratorClient() const client = getCollaboratorClient()
const createdBy = getCurrentAccount()._id const createdBy = getCurrentAccount()._id
return await client.snapshot(collaborativeDoc, { createdBy, snapshotName }) const snapshot: DocumentSnapshotParams = { createdBy, versionId: `${Date.now()}`, versionName }
return await client.snapshot(collaborativeDoc, snapshot)
} }

View File

@ -132,7 +132,6 @@ export default plugin(presentationId, {
UploadURL: '' as Metadata<string>, UploadURL: '' as Metadata<string>,
FilesURL: '' as Metadata<string>, FilesURL: '' as Metadata<string>,
CollaboratorUrl: '' as Metadata<string>, CollaboratorUrl: '' as Metadata<string>,
CollaboratorApiUrl: '' as Metadata<string>,
Token: '' as Metadata<string>, Token: '' as Metadata<string>,
Endpoint: '' as Metadata<string>, Endpoint: '' as Metadata<string>,
Workspace: '' as Metadata<string>, Workspace: '' as Metadata<string>,

View File

@ -533,9 +533,6 @@ export function getAttributePresenterClass (
if (hierarchy.isDerived(attrClass, core.class.TypeMarkup)) { if (hierarchy.isDerived(attrClass, core.class.TypeMarkup)) {
category = 'inplace' category = 'inplace'
} }
if (hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup)) {
category = 'inplace'
}
if (hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc)) { if (hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc)) {
category = 'inplace' category = 'inplace'
} }

View File

@ -64,7 +64,7 @@ const defaultSchema = getSchema(defaultExtensions)
* @public * @public
*/ */
export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node { export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node {
schema ??= extensions === undefined ? defaultSchema : getSchema(extensions ?? defaultExtensions) schema ??= getSchema(extensions ?? defaultExtensions)
try { try {
const body = yDocToProsemirrorJSON(ydoc, field) const body = yDocToProsemirrorJSON(ydoc, field)

View File

@ -59,7 +59,6 @@ const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
core.class.TypeDate, core.class.TypeDate,
core.class.TypeFileSize, core.class.TypeFileSize,
core.class.TypeMarkup, core.class.TypeMarkup,
core.class.TypeCollaborativeMarkup,
core.class.TypeHyperlink core.class.TypeHyperlink
] ]

View File

@ -144,7 +144,6 @@ export function getIsTextType (attributeModel?: AttributeModel): boolean {
return ( return (
attributeModel.attribute?.type?._class === core.class.TypeMarkup || attributeModel.attribute?.type?._class === core.class.TypeMarkup ||
attributeModel.attribute?.type?._class === core.class.TypeCollaborativeMarkup ||
attributeModel.attribute?.type?._class === core.class.TypeCollaborativeDoc attributeModel.attribute?.type?._class === core.class.TypeCollaborativeDoc
) )
} }

View File

@ -1,5 +1,16 @@
import { Organization } from '@hcengineering/contact' import { Organization } from '@hcengineering/contact'
import core, { Account, Client, Data, Doc, Ref, SortingOrder, Status, TxOperations } from '@hcengineering/core' import core, {
Account,
Client,
Data,
Doc,
Ref,
SortingOrder,
Status,
TxOperations,
generateId,
makeCollaborativeDoc
} from '@hcengineering/core'
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit' import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { ProjectType, makeRank } from '@hcengineering/task' import task, { ProjectType, makeRank } from '@hcengineering/task'
@ -23,17 +34,25 @@ export async function createVacancy (
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, { const id: Ref<Vacancy> = generateId()
name, await client.createDoc(
description: type.shortDescription ?? '', recruit.class.Vacancy,
fullDescription: type.description, core.space.Space,
private: false, {
archived: false, name,
company, description: type.shortDescription ?? '',
number: (incResult as any).object.sequence, fullDescription: makeCollaborativeDoc(id, 'fullDescription'),
members: [], private: false,
type: typeId archived: false,
}) company,
number: (incResult as any).object.sequence,
members: [],
type: typeId
},
id
)
// TODO type.description
return id return id
} }

View File

@ -52,6 +52,7 @@
"@hcengineering/presentation": "^0.6.3", "@hcengineering/presentation": "^0.6.3",
"@hcengineering/setting": "^0.6.17", "@hcengineering/setting": "^0.6.17",
"@hcengineering/templates": "^0.6.11", "@hcengineering/templates": "^0.6.11",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-editor": "^0.6.0", "@hcengineering/text-editor": "^0.6.0",
"@hcengineering/text-editor-resources": "^0.6.0", "@hcengineering/text-editor-resources": "^0.6.0",
"@hcengineering/theme": "^0.6.5", "@hcengineering/theme": "^0.6.5",

View File

@ -13,13 +13,22 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, ContactEvents, findContacts, Organization } from '@hcengineering/contact' import { Attachment } from '@hcengineering/attachment'
import core, { AttachedData, fillDefaults, generateId, Ref, TxOperations, WithLookup } from '@hcengineering/core' import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Card, getClient, InlineAttributeBar } from '@hcengineering/presentation' import { Channel, ContactEvents, Organization, findContacts } from '@hcengineering/contact'
import core, {
AttachedData,
fillDefaults,
generateId,
makeCollaborativeDoc,
Ref,
TxOperations,
WithLookup
} from '@hcengineering/core'
import { Card, getClient, InlineAttributeBar, updateMarkup } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text'
import { Button, createFocusManager, EditBox, FocusHandler, IconAttachment, IconInfo, Label } from '@hcengineering/ui' import { Button, createFocusManager, EditBox, FocusHandler, IconAttachment, IconInfo, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Attachment } from '@hcengineering/attachment'
import contact from '../plugin' import contact from '../plugin'
import ChannelsDropdown from './ChannelsDropdown.svelte' import ChannelsDropdown from './ChannelsDropdown.svelte'
@ -37,7 +46,7 @@
const object: Organization = { const object: Organization = {
name: '', name: '',
description: '', description: makeCollaborativeDoc(id, 'description'),
attachments: 0 attachments: 0
} as unknown as Organization } as unknown as Organization
@ -45,9 +54,12 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
let description = EmptyMarkup
fillDefaults(hierarchy, object, contact.class.Organization) fillDefaults(hierarchy, object, contact.class.Organization)
async function createOrganization (): Promise<void> { async function createOrganization (): Promise<void> {
await updateMarkup(object.description, { description })
await client.createDoc(contact.class.Organization, contact.space.Contacts, object, id) await client.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
await descriptionBox.createAttachments(id) await descriptionBox.createAttachments(id)
@ -119,7 +131,7 @@
space={contact.space.Contacts} space={contact.space.Contacts}
alwaysEdit alwaysEdit
showButtons={false} showButtons={false}
bind:content={object.description} bind:content={description}
placeholder={core.string.Description} placeholder={core.string.Description}
kind="indented" kind="indented"
isScrollable={false} isScrollable={false}

View File

@ -18,6 +18,7 @@ import {
Account, Account,
AttachedDoc, AttachedDoc,
Class, Class,
CollaborativeDoc,
Doc, Doc,
Ref, Ref,
Space, Space,
@ -136,7 +137,7 @@ export interface Member extends AttachedDoc {
*/ */
export interface Organization extends Contact { export interface Organization extends Contact {
members: number members: number
description?: string description: CollaborativeDoc
} }
/** /**

View File

@ -21,7 +21,7 @@
AttachedData, AttachedData,
Class, Class,
generateId, generateId,
getCollaborativeDoc, makeCollaborativeDoc,
getCurrentAccount, getCurrentAccount,
Mixin, Mixin,
Ref, Ref,
@ -78,7 +78,7 @@
approvers: [], approvers: [],
coAuthors: [], coAuthors: [],
changeControl: '' as Ref<ChangeControl>, changeControl: '' as Ref<ChangeControl>,
content: getCollaborativeDoc(generateId()) content: makeCollaborativeDoc(generateId())
} }
let templateId: Ref<DocumentTemplate> | undefined = initTemplateId let templateId: Ref<DocumentTemplate> | undefined = initTemplateId

View File

@ -18,11 +18,11 @@
import { import {
generateId, generateId,
getCurrentAccount, getCurrentAccount,
makeCollaborativeDoc,
type AttachedData, type AttachedData,
type Class, type Class,
type Data, type Data,
type Ref, type Ref
getCollaborativeDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { MessageBox, getClient } from '@hcengineering/presentation' import { MessageBox, getClient } from '@hcengineering/presentation'
import { import {
@ -122,7 +122,7 @@
state: DocumentState.Draft, state: DocumentState.Draft,
snapshots: 0, snapshots: 0,
changeControl: ccRecordId, changeControl: ccRecordId,
content: getCollaborativeDoc(generateId()), content: makeCollaborativeDoc(generateId()),
requests: 0, requests: 0,
reviewers: [], reviewers: [],

View File

@ -32,8 +32,8 @@
type Ref, type Ref,
type Mixin, type Mixin,
generateId, generateId,
getCollaborativeDoc, getCurrentAccount,
getCurrentAccount makeCollaborativeDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { MessageBox, getClient } from '@hcengineering/presentation' import { MessageBox, getClient } from '@hcengineering/presentation'
import { import {
@ -118,7 +118,7 @@
state: DocumentState.Draft, state: DocumentState.Draft,
snapshots: 0, snapshots: 0,
changeControl: ccRecordId, changeControl: ccRecordId,
content: getCollaborativeDoc(generateId()), content: makeCollaborativeDoc(generateId()),
requests: 0, requests: 0,
reviewers: [], reviewers: [],

View File

@ -18,11 +18,12 @@ import {
type AttachedData, type AttachedData,
type Class, type Class,
type CollaborativeDoc, type CollaborativeDoc,
type Doc,
type Ref, type Ref,
type TxOperations, type TxOperations,
Mixin, Mixin,
generateId, generateId,
getCollaborativeDoc makeCollaborativeDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { import {
type Document, type Document,
@ -33,6 +34,7 @@ import {
type DocumentMeta, type DocumentMeta,
type Project, type Project,
DocumentState, DocumentState,
HierarchyDocument,
ProjectDocument ProjectDocument
} from './types' } from './types'
@ -261,7 +263,7 @@ export async function createDocumentTemplate (
} }
) )
await ops.addCollection( await ops.addCollection<DocumentMeta, HierarchyDocument>(
_class, _class,
space, space,
metaId, metaId,
@ -301,5 +303,7 @@ export function getCollaborativeDocForDocument (
prefix = prefix.substring(0, prefix.length - 1) prefix = prefix.substring(0, prefix.length - 1)
} }
return getCollaborativeDoc(`${prefix}-${seqNumber}-${major}.${minor}${next ? '.next' : ''}-` + generateId()) return makeCollaborativeDoc(
(`${prefix}-${seqNumber}-${major}.${minor}${next ? '.next' : ''}-` + generateId()) as Ref<Doc>
)
} }

View File

@ -308,6 +308,7 @@
size: 'large', size: 'large',
fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor' fill: doc.color !== undefined ? getPlatformColorDef(doc.color, $themeStore.dark).icon : 'currentColor'
}} }}
disabled={readonly}
on:click={chooseIcon} on:click={chooseIcon}
/> />
</div> </div>

View File

@ -13,15 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { import { type AttachedData, type Client, type Ref, type TxOperations, makeCollaborativeDoc } from '@hcengineering/core'
getCollaborativeDoc, import { type Document, type Teamspace, documentId } from '@hcengineering/document'
getCollaborativeDocId,
type AttachedData,
type Client,
type Ref,
type TxOperations
} from '@hcengineering/core'
import { documentId, type Document, type Teamspace } from '@hcengineering/document'
import { getMetadata, translate } from '@hcengineering/platform' import { getMetadata, translate } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation' import presentation, { getClient } from '@hcengineering/presentation'
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui' import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
@ -39,11 +32,10 @@ export async function createEmptyDocument (
data: Partial<Pick<AttachedData<Document>, 'name' | 'icon' | 'color'>> = {} data: Partial<Pick<AttachedData<Document>, 'name' | 'icon' | 'color'>> = {}
): Promise<void> { ): Promise<void> {
const name = await translate(document.string.Untitled, {}) const name = await translate(document.string.Untitled, {})
const collaborativeDocId = getCollaborativeDocId(id, 'content')
const object: AttachedData<Document> = { const object: AttachedData<Document> = {
name, name,
content: getCollaborativeDoc(collaborativeDocId), content: makeCollaborativeDoc(id, 'content'),
attachments: 0, attachments: 0,
children: 0, children: 0,
embeddings: 0, embeddings: 0,

View File

@ -16,9 +16,20 @@
import { AvatarType, Channel, combineName, Contact, findContacts } from '@hcengineering/contact' import { AvatarType, Channel, combineName, Contact, findContacts } from '@hcengineering/contact'
import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources' import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources'
import contact from '@hcengineering/contact-resources/src/plugin' import contact from '@hcengineering/contact-resources/src/plugin'
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@hcengineering/core' import {
AttachedData,
Class,
Data,
Doc,
MixinData,
Ref,
WithLookup,
generateId,
makeCollaborativeDoc
} from '@hcengineering/core'
import { Customer, LeadEvents } from '@hcengineering/lead' import { Customer, LeadEvents } from '@hcengineering/lead'
import { Card, getClient, InlineAttributeBar } from '@hcengineering/presentation' import { Card, getClient, InlineAttributeBar, updateMarkup } from '@hcengineering/presentation'
import { EmptyMarkup, StyledTextBox } from '@hcengineering/text-editor-resources'
import { import {
Button, Button,
createFocusManager, createFocusManager,
@ -36,6 +47,7 @@
let firstName = '' let firstName = ''
let lastName = '' let lastName = ''
let description = EmptyMarkup
export function canClose (): boolean { export function canClose (): boolean {
return firstName === '' && lastName === '' return firstName === '' && lastName === ''
@ -58,9 +70,9 @@
return targetClass === contact.class.Person ? combineName(firstName.trim(), lastName.trim()) : objectName return targetClass === contact.class.Person ? combineName(firstName.trim(), lastName.trim()) : objectName
} }
async function createCustomer () { async function createCustomer (): Promise<void> {
const candidate: Data<Contact> = { const candidate: Data<Contact> = {
name: formatName(targetClass._id, firstName, lastName, object.name), name,
city: object.city, city: object.city,
avatarType: AvatarType.COLOR avatarType: AvatarType.COLOR
} }
@ -71,9 +83,11 @@
candidate.avatarProps = info.avatarProps candidate.avatarProps = info.avatarProps
} }
const candidateData: MixinData<Contact, Customer> = { const candidateData: MixinData<Contact, Customer> = {
description: object.description description: makeCollaborativeDoc(customerId, 'description')
} }
await updateMarkup(candidateData.description, { description })
const id = await client.createDoc(targetClass._id, contact.space.Contacts, { ...candidate, ...object }, customerId) const id = await client.createDoc(targetClass._id, contact.space.Contacts, { ...candidate, ...object }, customerId)
await client.createMixin( await client.createMixin(
id as Ref<Contact>, id as Ref<Contact>,
@ -131,19 +145,18 @@
} }
) )
} }
$: canSave = formatName(targetClass._id, firstName, lastName, object.name).length > 0 $: name = formatName(targetClass._id, firstName, lastName, object.name)
$: canSave = name.trim().length > 0
const manager = createFocusManager() const manager = createFocusManager()
let matches: WithLookup<Contact>[] = [] let matches: WithLookup<Contact>[] = []
let matchedChannels: AttachedData<Channel>[] = [] let matchedChannels: AttachedData<Channel>[] = []
$: if (targetClass !== undefined) { $: if (targetClass !== undefined) {
findContacts(client, targetClass._id, formatName(targetClass._id, firstName, lastName, object.name), channels).then( void findContacts(client, targetClass._id, name, channels).then((p) => {
(p) => { matches = p.contacts
matches = p.contacts matchedChannels = p.channels
matchedChannels = p.channels })
}
)
} }
</script> </script>
@ -185,12 +198,16 @@
focusIndex={3} focusIndex={3}
/> />
</div> </div>
<EditBox <div class="mt-1">
placeholder={lead.string.IssueDescriptionPlaceholder} <StyledTextBox
bind:value={object.description} bind:content={description}
kind={'small-style'} placeholder={lead.string.IssueDescriptionPlaceholder}
focusIndex={4} kind={'normal'}
/> alwaysEdit={true}
showButtons={false}
focusIndex={4}
/>
</div>
</div> </div>
<div class="ml-4 flex"> <div class="ml-4 flex">
<EditableAvatar <EditableAvatar

View File

@ -15,7 +15,7 @@
// //
import type { Contact } from '@hcengineering/contact' import type { Contact } from '@hcengineering/contact'
import type { Attribute, Class, Doc, Markup, Ref, Status, Timestamp } from '@hcengineering/core' import type { Attribute, Class, CollaborativeDoc, Doc, Markup, Ref, Status, Timestamp } from '@hcengineering/core'
import { Mixin } from '@hcengineering/core' import { Mixin } from '@hcengineering/core'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform' import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
@ -36,7 +36,7 @@ export interface Funnel extends Project {
export interface Customer extends Contact { export interface Customer extends Contact {
leads?: number leads?: number
description: string description: CollaborativeDoc
} }
/** /**

View File

@ -18,6 +18,7 @@
import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources' import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources'
import core, { import core, {
Account, Account,
AttachedData,
Data, Data,
Ref, Ref,
Role, Role,
@ -25,10 +26,18 @@
SortingOrder, SortingOrder,
fillDefaults, fillDefaults,
generateId, generateId,
getCurrentAccount getCurrentAccount,
makeCollaborativeDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import { Card, InlineAttributeBar, MessageBox, createQuery, getClient } from '@hcengineering/presentation' import {
Card,
InlineAttributeBar,
MessageBox,
createQuery,
getClient,
updateMarkup
} from '@hcengineering/presentation'
import { RecruitEvents, Vacancy, Vacancy as VacancyClass } from '@hcengineering/recruit' import { RecruitEvents, Vacancy, Vacancy as VacancyClass } from '@hcengineering/recruit'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import task, { ProjectType, makeRank } from '@hcengineering/task' import task, { ProjectType, makeRank } from '@hcengineering/task'
@ -88,7 +97,7 @@
attachments: 0, attachments: 0,
comments: 0, comments: 0,
company: '' as Ref<Organization>, company: '' as Ref<Organization>,
fullDescription: '', fullDescription: makeCollaborativeDoc(objectId, 'fullDescription'),
location: '', location: '',
type: typeId as Ref<ProjectType> type: typeId as Ref<ProjectType>
} }
@ -103,7 +112,7 @@
$: typeId && $: typeId &&
templateQ.query(task.class.ProjectType, { _id: typeId }, (result) => { templateQ.query(task.class.ProjectType, { _id: typeId }, (result) => {
const { _class, _id, description, targetClass, ...templateData } = result[0] const { _class, _id, description, targetClass, ...templateData } = result[0]
vacancyData = { ...(templateData as unknown as Data<VacancyClass>), fullDescription: description } vacancyData = { ...(templateData as unknown as Data<VacancyClass>) }
if (appliedTemplateId !== typeId) { if (appliedTemplateId !== typeId) {
fullDescription = description ?? '' fullDescription = description ?? ''
appliedTemplateId = typeId appliedTemplateId = typeId
@ -172,10 +181,11 @@
} }
const number = (incResult as any).object.sequence const number = (incResult as any).object.sequence
const resId: Ref<Issue> = generateId()
const identifier = `${project?.identifier}-${number}` const identifier = `${project?.identifier}-${number}`
const resId = await client.addCollection(tracker.class.Issue, space, parent, tracker.class.Issue, 'subIssues', { const data: AttachedData<Issue> = {
title: template.title + ` (${name})`, title: template.title + ` (${name})`,
description: template.description, description: makeCollaborativeDoc(resId, 'description'),
assignee: template.assignee, assignee: template.assignee,
component: template.component, component: template.component,
milestone: template.milestone, milestone: template.milestone,
@ -195,7 +205,10 @@
childInfo: [], childInfo: [],
kind: taskType._id, kind: taskType._id,
identifier identifier
}) }
await updateMarkup(data.description, { description: template.description })
await client.addCollection(tracker.class.Issue, space, parent, tracker.class.Issue, 'subIssues', data, resId)
if ((template.labels?.length ?? 0) > 0) { if ((template.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: template.labels } }) const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: template.labels } })
for (const label of tagElements) { for (const label of tagElements) {
@ -224,7 +237,7 @@
...vacancyData, ...vacancyData,
name, name,
description: template?.shortDescription ?? '', description: template?.shortDescription ?? '',
fullDescription, fullDescription: makeCollaborativeDoc(objectId, 'fullDescription'),
private: false, private: false,
archived: false, archived: false,
number: (incResult as any).object.sequence, number: (incResult as any).object.sequence,
@ -235,6 +248,8 @@
type: typeId type: typeId
} }
await updateMarkup(data.fullDescription, { fullDescription })
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId) const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId)
Analytics.handleEvent(RecruitEvents.VacancyCreated, { Analytics.handleEvent(RecruitEvents.VacancyCreated, {

View File

@ -15,13 +15,13 @@
import { Event } from '@hcengineering/calendar' import { Event } from '@hcengineering/calendar'
import type { Channel, Organization, Person } from '@hcengineering/contact' import type { Channel, Organization, Person } from '@hcengineering/contact'
import type { AttachedData, AttachedDoc, Markup, Ref, Status, Timestamp } from '@hcengineering/core' import type { AttachedData, AttachedDoc, CollaborativeDoc, Markup, Ref, Status, Timestamp } from '@hcengineering/core'
import { TagReference } from '@hcengineering/tags' import { TagReference } from '@hcengineering/tags'
import type { Project, Task } from '@hcengineering/task' import type { Project, Task } from '@hcengineering/task'
/** @public */ /** @public */
export interface Vacancy extends Project { export interface Vacancy extends Project {
fullDescription?: string fullDescription: CollaborativeDoc
attachments?: number attachments?: number
dueTo?: Timestamp dueTo?: Timestamp
location?: string location?: string

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import core, { CollaborativeDoc, Doc, getCollaborativeDoc, getCollaborativeDocId } from '@hcengineering/core' import { Doc } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { KeyedAttribute, getAttribute, getClient } from '@hcengineering/presentation' import { KeyedAttribute, getAttribute, getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, registerFocus } from '@hcengineering/ui' import { AnySvelteComponent, registerFocus } from '@hcengineering/ui'
@ -47,20 +47,7 @@
let editor: CollaborativeTextEditor let editor: CollaborativeTextEditor
$: collaborativeDoc = getCollaborativeDocFromAttribute(object, key) $: collaborativeDoc = getAttribute(getClient(), object, key)
function getCollaborativeDocFromAttribute (object: Doc, key: KeyedAttribute): CollaborativeDoc {
const value = getAttribute(getClient(), object, key)
if (key.attr.type._class === core.class.TypeCollaborativeDoc) {
return value as CollaborativeDoc
} else if (key.attr.type._class === core.class.TypeCollaborativeDocVersion) {
return value as CollaborativeDoc
} else {
// TODO Remove this when we migrate to minio
const collaborativeDocId = getCollaborativeDocId(object._id, key.key)
return getCollaborativeDoc(collaborativeDocId)
}
}
// Focusable control with index // Focusable control with index
let canBlur = true let canBlur = true
@ -104,8 +91,8 @@
<CollaborativeTextEditor <CollaborativeTextEditor
bind:this={editor} bind:this={editor}
{collaborativeDoc} {collaborativeDoc}
objectClass={object._class}
objectId={object._id} objectId={object._id}
objectClass={key.attr.attributeOf}
objectSpace={object.space} objectSpace={object.space}
objectAttr={key.key} objectAttr={key.key}
{user} {user}

View File

@ -71,10 +71,10 @@
export let initialCollaborativeDoc: CollaborativeDoc | undefined = undefined export let initialCollaborativeDoc: CollaborativeDoc | undefined = undefined
export let field: string export let field: string
export let objectClass: Ref<Class<Doc>> | undefined export let objectClass: Ref<Class<Doc>> | undefined = undefined
export let objectId: Ref<Doc> | undefined export let objectId: Ref<Doc> | undefined = undefined
export let objectSpace: Ref<Space> | undefined = undefined export let objectSpace: Ref<Space> | undefined = undefined
export let objectAttr: string | undefined export let objectAttr: string | undefined = undefined
export let user: CollaborationUser export let user: CollaborationUser
export let userComponent: AnySvelteComponent | undefined = undefined export let userComponent: AnySvelteComponent | undefined = undefined

View File

@ -21,7 +21,6 @@ import {
formatPlatformDocumentId as origFormatPlatformDocumentId formatPlatformDocumentId as origFormatPlatformDocumentId
} from '@hcengineering/collaborator-client' } from '@hcengineering/collaborator-client'
import { getCurrentLocation } from '@hcengineering/ui' import { getCurrentLocation } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
function getWorkspace (): string { function getWorkspace (): string {
return getCurrentLocation().path[1] ?? '' return getCurrentLocation().path[1] ?? ''
@ -37,6 +36,5 @@ export function formatPlatformDocumentId (
objectId: Ref<Doc>, objectId: Ref<Doc>,
objectAttr: string objectAttr: string
): PlatformDocumentId { ): PlatformDocumentId {
const objectDomain = getClient().getHierarchy().getDomain(objectClass) return origFormatPlatformDocumentId(objectClass, objectId, objectAttr)
return origFormatPlatformDocumentId(objectDomain, objectClass, objectId, objectAttr)
} }

View File

@ -27,6 +27,7 @@
SortingOrder, SortingOrder,
fillDefaults, fillDefaults,
generateId, generateId,
makeCollaborativeDoc,
toIdMap toIdMap
} from '@hcengineering/core' } from '@hcengineering/core'
import { getResource, translate } from '@hcengineering/platform' import { getResource, translate } from '@hcengineering/platform'
@ -41,7 +42,9 @@
MultipleDraftController, MultipleDraftController,
SpaceSelector, SpaceSelector,
createQuery, createQuery,
getClient getClient,
getMarkup,
updateMarkup
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { TaskType, makeRank } from '@hcengineering/task' import { TaskType, makeRank } from '@hcengineering/task'
@ -190,7 +193,6 @@
if (originalIssue !== undefined && !ignoreOriginal) { if (originalIssue !== undefined && !ignoreOriginal) {
const res: IssueDraft = { const res: IssueDraft = {
...base, ...base,
description: originalIssue.description,
status: originalIssue.status, status: originalIssue.status,
priority: originalIssue.priority, priority: originalIssue.priority,
component: originalIssue.component, component: originalIssue.component,
@ -200,6 +202,9 @@
parentIssue: originalIssue.parents[0]?.parentId, parentIssue: originalIssue.parents[0]?.parentId,
title: `${originalIssue.title} (copy)` title: `${originalIssue.title} (copy)`
} }
void getMarkup(originalIssue.description).then((res) => {
object.description = res.description
})
void client.findAll(tags.class.TagReference, { attachedTo: originalIssue._id }).then((p) => { void client.findAll(tags.class.TagReference, { attachedTo: originalIssue._id }).then((p) => {
object.labels = p object.labels = p
}) })
@ -471,7 +476,7 @@
const value: DocData<Issue> = { const value: DocData<Issue> = {
title: getTitle(object.title), title: getTitle(object.title),
description: object.description, description: makeCollaborativeDoc(_id, 'description'),
assignee: object.assignee, assignee: object.assignee,
component: object.component, component: object.component,
milestone: object.milestone, milestone: object.milestone,
@ -504,6 +509,8 @@
identifier identifier
} }
await updateMarkup(value.description, { description: object.description })
await docCreateManager.commit(operations, _id, currentProject, value, 'pre') await docCreateManager.commit(operations, _id, currentProject, value, 'pre')
await operations.addCollection( await operations.addCollection(

View File

@ -14,8 +14,8 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import core, { AttachedData, Doc, Ref, SortingOrder } from '@hcengineering/core' import core, { AttachedData, Doc, Ref, SortingOrder, makeCollaborativeDoc } from '@hcengineering/core'
import { DraftController, draftsStore, getClient, deleteFile } from '@hcengineering/presentation' import { DraftController, draftsStore, getClient, deleteFile, updateMarkup } from '@hcengineering/presentation'
import tags from '@hcengineering/tags' import tags from '@hcengineering/tags'
import { makeRank } from '@hcengineering/task' import { makeRank } from '@hcengineering/task'
import { Component, Issue, IssueDraft, IssueParentInfo, Milestone, Project } from '@hcengineering/tracker' import { Component, Issue, IssueDraft, IssueParentInfo, Milestone, Project } from '@hcengineering/tracker'
@ -71,7 +71,7 @@
const childId = subIssue._id const childId = subIssue._id
const cvalue: AttachedData<Issue> = { const cvalue: AttachedData<Issue> = {
title: subIssue.title.trim(), title: subIssue.title.trim(),
description: subIssue.description, description: makeCollaborativeDoc(childId, 'description'),
assignee: subIssue.assignee, assignee: subIssue.assignee,
component: subIssue.component, component: subIssue.component,
milestone: subIssue.milestone, milestone: subIssue.milestone,
@ -92,6 +92,7 @@
kind: subIssue.kind, kind: subIssue.kind,
identifier: `${project.identifier}-${number}` identifier: `${project.identifier}-${number}`
} }
await updateMarkup(cvalue.description, { description: subIssue.description })
await client.addCollection( await client.addCollection(
tracker.class.Issue, tracker.class.Issue,
project._id, project._id,

View File

@ -15,11 +15,13 @@
<script lang="ts"> <script lang="ts">
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import { AttachmentDocList } from '@hcengineering/attachment-resources' import { AttachmentDocList } from '@hcengineering/attachment-resources'
import { ChatMessagePopup } from '@hcengineering/chunter-resources'
import { Ref } from '@hcengineering/core' import { Ref } from '@hcengineering/core'
import { IconForward, MessageViewer, createQuery, getClient } from '@hcengineering/presentation' import { IconForward, createQuery, getClient } from '@hcengineering/presentation'
import { CollaborativeTextEditor } from '@hcengineering/text-editor-resources'
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { Label, Scroller, resizeObserver } from '@hcengineering/ui' import { Label, Scroller, resizeObserver } from '@hcengineering/ui'
import { ChatMessagePopup } from '@hcengineering/chunter-resources' import { getCollaborationUser } from '@hcengineering/view-resources'
import tracker from '../../plugin' import tracker from '../../plugin'
import AssigneeEditor from './AssigneeEditor.svelte' import AssigneeEditor from './AssigneeEditor.svelte'
@ -34,6 +36,8 @@
const client = getClient() const client = getClient()
const issueQuery = createQuery() const issueQuery = createQuery()
const user = getCollaborationUser()
$: issueQuery.query( $: issueQuery.query(
object._class, object._class,
{ _id: object._id }, { _id: object._id },
@ -91,7 +95,9 @@
{#if issue.description} {#if issue.description}
<div class="description-container" class:masked={cHeight > limit} style:max-height={`${limit}px`}> <div class="description-container" class:masked={cHeight > limit} style:max-height={`${limit}px`}>
<div class="description-content" use:resizeObserver={(element) => (cHeight = element.clientHeight)}> <div class="description-content" use:resizeObserver={(element) => (cHeight = element.clientHeight)}>
<MessageViewer message={issue.description} /> {#key issue._id}
<CollaborativeTextEditor collaborativeDoc={issue.description} field="description" {user} readonly />
{/key}
</div> </div>
</div> </div>
{:else} {:else}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources' import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
import { Class, Doc, Ref, WithLookup } from '@hcengineering/core' import { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
@ -45,7 +45,7 @@
let title = '' let title = ''
let innerWidth: number let innerWidth: number
let descriptionBox: AttachmentStyleBoxCollabEditor let descriptionBox: AttachmentStyleBoxEditor
const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res()) const inboxClient = getResource(notification.function.GetInboxNotificationsClient).then((res) => res())
@ -147,7 +147,7 @@
> >
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" on:blur={save} /> <EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" on:blur={save} />
<div class="w-full mt-6"> <div class="w-full mt-6">
<AttachmentStyleBoxCollabEditor <AttachmentStyleBoxEditor
focusIndex={30} focusIndex={30}
object={template} object={template}
key={{ key: 'description', attr: descriptionKey }} key={{ key: 'description', attr: descriptionKey }}

View File

@ -18,6 +18,7 @@ import {
AttachedDoc, AttachedDoc,
Attribute, Attribute,
Class, Class,
CollaborativeDoc,
CollectionSize, CollectionSize,
Data, Data,
Doc, Doc,
@ -181,7 +182,7 @@ export interface Milestone extends Doc {
export interface Issue extends Task { export interface Issue extends Task {
attachedTo: Ref<Issue> attachedTo: Ref<Issue>
title: string title: string
description: Markup description: CollaborativeDoc
status: Ref<IssueStatus> status: Ref<IssueStatus>
priority: IssuePriority priority: IssuePriority

View File

@ -8,8 +8,9 @@
const token: string = getMetadata(presentation.metadata.Token) ?? '' const token: string = getMetadata(presentation.metadata.Token) ?? ''
async function fetchCollabStats (tick: number): Promise<void> { async function fetchCollabStats (tick: number): Promise<void> {
const collaborator = getMetadata(presentation.metadata.CollaboratorApiUrl) const collaboratorUrl = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''
await fetch(collaborator + `/api/v1/statistics?token=${token}`, {}) const collaboratorApiUrl = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://')
await fetch(collaboratorApiUrl + `/api/v1/statistics?token=${token}`, {})
.then(async (json) => { .then(async (json) => {
dataCollab = await json.json() dataCollab = await json.json()
}) })

View File

@ -4,7 +4,6 @@ export ACCOUNTS_URL=http://localhost:3333
export UPLOAD_URL=http://localhost:3333/files export UPLOAD_URL=http://localhost:3333/files
export ELASTIC_URL=http://elastic:9200 export ELASTIC_URL=http://elastic:9200
export COLLABORATOR_URL=ws://localhost:3078 export COLLABORATOR_URL=ws://localhost:3078
export COLLABORATOR_API_URL=http://localhost:3078
export MINIO_ENDPOINT=minio export MINIO_ENDPOINT=minio
export MINIO_ACCESS_KEY=minioadmin export MINIO_ACCESS_KEY=minioadmin
export MINIO_SECRET_KEY=minioadmin export MINIO_SECRET_KEY=minioadmin

View File

@ -54,6 +54,7 @@ setMetadata(notification.metadata.PushPublicKey, config.pushPublicKey)
setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey) setMetadata(serverNotification.metadata.PushPrivateKey, config.pushPrivateKey)
setMetadata(serverNotification.metadata.PushSubject, config.pushSubject) setMetadata(serverNotification.metadata.PushSubject, config.pushSubject)
setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName) setMetadata(serverCore.metadata.ElasticIndexName, config.elasticIndexName)
setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1')
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL) setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE) setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)

View File

@ -80,7 +80,6 @@ services:
- REKONI_URL=http://rekoni:4005 - REKONI_URL=http://rekoni:4005
- TELEGRAM_URL=http://localhost:8086 - TELEGRAM_URL=http://localhost:8086
- COLLABORATOR_URL=ws://localhost:3079 - COLLABORATOR_URL=ws://localhost:3079
- COLLABORATOR_API_URL=http://localhost:3079
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- BRANDING_URL=http://localhost:8083/branding-test.json - BRANDING_URL=http://localhost:8083/branding-test.json
transactor: transactor:

View File

@ -375,7 +375,7 @@ async function getMessageNotifyResult (
} }
function isMarkupType (type: Ref<Class<Type<any>>>): boolean { function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup return type === core.class.TypeMarkup
} }
function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean { function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {

View File

@ -303,7 +303,6 @@ export async function getTxAttributesUpdates (
if ( if (
hierarchy.isDerived(attrClass, core.class.TypeMarkup) || hierarchy.isDerived(attrClass, core.class.TypeMarkup) ||
hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup) ||
hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc) || hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc) ||
mixin === notification.mixin.Collaborators mixin === notification.mixin.Collaborators
) { ) {

View File

@ -16,7 +16,7 @@
import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core' import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core'
import core, { TxProcessor } from '@hcengineering/core' import core, { TxProcessor } from '@hcengineering/core'
import { removeCollaborativeDoc } from '@hcengineering/collaboration' import { removeCollaborativeDoc } from '@hcengineering/collaboration'
import type { TriggerControl } from '@hcengineering/server-core' import { type TriggerControl } from '@hcengineering/server-core'
/** /**
* @public * @public

View File

@ -14,14 +14,6 @@
"scripts": { "scripts": {
"build": "compile", "build": "compile",
"build:watch": "compile", "build:watch": "compile",
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
"_phase:docker-staging": "rushx docker:staging",
"bundle": "mkdir -p bundle && esbuild src/__start.ts --bundle --platform=node > bundle/bundle.js",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/collaborator",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/collaborator staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/collaborator",
"run-local": "cross-env MONGO_URL=mongodb://localhost:27017 SECRET=secret MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin ts-node src/__start.ts",
"format": "format src", "format": "format src",
"test": "jest --passWithNoTests --silent", "test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src", "_phase:build": "compile transpile src",

View File

@ -13,10 +13,10 @@
// limitations under the License. // limitations under the License.
// //
import { WorkspaceLoginInfo } from '@hcengineering/account' import { ClientWorkspaceInfo } from '@hcengineering/account'
import config from './config' import config from './config'
export async function getWorkspaceInfo (token: string): Promise<WorkspaceLoginInfo> { export async function getWorkspaceInfo (token: string): Promise<ClientWorkspaceInfo> {
const accountsUrl = config.AccountsUrl const accountsUrl = config.AccountsUrl
const workspaceInfo = await ( const workspaceInfo = await (
await fetch(accountsUrl, { await fetch(accountsUrl, {
@ -32,5 +32,5 @@ export async function getWorkspaceInfo (token: string): Promise<WorkspaceLoginIn
}) })
).json() ).json()
return workspaceInfo.result as WorkspaceLoginInfo return workspaceInfo.result as ClientWorkspaceInfo
} }

View File

@ -13,9 +13,10 @@
// limitations under the License. // limitations under the License.
// //
import { yDocCopyXmlField } from '@hcengineering/collaboration' import { YDocVersion, takeCollaborativeDocSnapshot, yDocCopyXmlField } from '@hcengineering/collaboration'
import { type CopyContentRequest, type CopyContentResponse } from '@hcengineering/collaborator-client' import { parseDocumentId, type CopyContentRequest, type CopyContentResponse } from '@hcengineering/collaborator-client'
import { MeasureContext } from '@hcengineering/core' import { MeasureContext } from '@hcengineering/core'
import { Doc as YDoc } from 'yjs'
import { Context } from '../../context' import { Context } from '../../context'
import { RpcMethodParams } from '../rpc' import { RpcMethodParams } from '../rpc'
@ -25,17 +26,36 @@ export async function copyContent (
payload: CopyContentRequest, payload: CopyContentRequest,
params: RpcMethodParams params: RpcMethodParams
): Promise<CopyContentResponse> { ): Promise<CopyContentResponse> {
const { documentId, sourceField, targetField } = payload const { documentId, sourceField, targetField, snapshot } = payload
const { hocuspocus } = params const { hocuspocus, storageAdapter } = params
const { workspaceId } = context
const connection = await ctx.with('connect', {}, async () => { const connection = await ctx.with('connect', {}, async () => {
return await hocuspocus.openDirectConnection(documentId, context) return await hocuspocus.openDirectConnection(documentId, context)
}) })
try { try {
await connection.transact((document) => { await ctx.with('copy', {}, async () => {
yDocCopyXmlField(document, sourceField, targetField) await connection.transact((document) => {
yDocCopyXmlField(document, sourceField, targetField)
})
}) })
if (snapshot !== undefined && snapshot.versionId !== 'HEAD') {
const ydoc = connection.document ?? new YDoc()
const { collaborativeDoc } = parseDocumentId(documentId)
const version: YDocVersion = {
versionId: snapshot.versionId,
name: snapshot.versionName ?? snapshot.versionId,
createdBy: snapshot.createdBy,
createdOn: Date.now()
}
await ctx.with('snapshot', {}, async () => {
await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
})
}
} finally { } finally {
await connection.disconnect() await connection.disconnect()
} }

View File

@ -24,7 +24,7 @@ export async function getContent (
payload: GetContentRequest, payload: GetContentRequest,
params: RpcMethodParams params: RpcMethodParams
): Promise<GetContentResponse> { ): Promise<GetContentResponse> {
const { documentId, field } = payload const { documentId } = payload
const { hocuspocus, transformer } = params const { hocuspocus, transformer } = params
const connection = await ctx.with('connect', {}, async () => { const connection = await ctx.with('connect', {}, async () => {
@ -32,15 +32,17 @@ export async function getContent (
}) })
try { try {
const html = await ctx.with('transform', {}, async () => { const content = await ctx.with('transform', {}, async () => {
let content = '' const object: Record<string, string> = {}
await connection.transact((document) => { await connection.transact((document) => {
content = transformer.fromYdoc(document, field) document.share.forEach((_, field) => {
object[field] = transformer.fromYdoc(document, field)
})
}) })
return content return object
}) })
return { html } return { content }
} finally { } finally {
await connection.disconnect() await connection.disconnect()
} }

View File

@ -13,19 +13,13 @@
// limitations under the License. // limitations under the License.
// //
import { import { YDocVersion, takeCollaborativeDocSnapshot } from '@hcengineering/collaboration'
YDocVersion,
collaborativeHistoryDocId,
createYdocSnapshot,
yDocFromStorage,
yDocToStorage
} from '@hcengineering/collaboration'
import { import {
parseDocumentId, parseDocumentId,
type TakeSnapshotRequest, type TakeSnapshotRequest,
type TakeSnapshotResponse type TakeSnapshotResponse
} from '@hcengineering/collaborator-client' } from '@hcengineering/collaborator-client'
import { CollaborativeDocVersionHead, MeasureContext, collaborativeDocParse, generateId } from '@hcengineering/core' import { CollaborativeDocVersionHead, MeasureContext, collaborativeDocParse } from '@hcengineering/core'
import { Doc as YDoc } from 'yjs' import { Doc as YDoc } from 'yjs'
import { Context } from '../../context' import { Context } from '../../context'
import { RpcMethodParams } from '../rpc' import { RpcMethodParams } from '../rpc'
@ -36,19 +30,19 @@ export async function takeSnapshot (
payload: TakeSnapshotRequest, payload: TakeSnapshotRequest,
params: RpcMethodParams params: RpcMethodParams
): Promise<TakeSnapshotResponse> { ): Promise<TakeSnapshotResponse> {
const { documentId, snapshotName, createdBy } = payload const { documentId, snapshot } = payload
const { hocuspocus, storageAdapter } = params const { hocuspocus, storageAdapter } = params
const { workspaceId } = context const { workspaceId } = context
const version: YDocVersion = { const version: YDocVersion = {
versionId: generateId(), versionId: snapshot.versionId,
name: snapshotName, name: snapshot.versionName ?? snapshot.versionId,
createdBy, createdBy: snapshot.createdBy,
createdOn: Date.now() createdOn: Date.now()
} }
const { collaborativeDoc } = parseDocumentId(documentId) const { collaborativeDoc } = parseDocumentId(documentId)
const { documentId: contentDocumentId, versionId } = collaborativeDocParse(collaborativeDoc) const { versionId } = collaborativeDocParse(collaborativeDoc)
if (versionId !== CollaborativeDocVersionHead) { if (versionId !== CollaborativeDocVersionHead) {
throw new Error('invalid document version') throw new Error('invalid document version')
} }
@ -58,21 +52,10 @@ export async function takeSnapshot (
}) })
try { try {
// load history document directly from storage const ydoc = connection.document ?? new YDoc()
const historyDocumentId = collaborativeHistoryDocId(contentDocumentId)
const yHistory =
(await ctx.with('yDocFromStorage', {}, async () => {
return await yDocFromStorage(ctx, storageAdapter, workspaceId, historyDocumentId)
})) ?? new YDoc()
await ctx.with('createYdocSnapshot', {}, async () => { await ctx.with('snapshot', {}, async () => {
await connection.transact((yContent) => { await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
createYdocSnapshot(yContent, yHistory, version)
})
})
await ctx.with('yDocToStorage', {}, async () => {
await yDocToStorage(ctx, storageAdapter, workspaceId, historyDocumentId, yHistory)
}) })
return { ...version } return { ...version }

View File

@ -14,8 +14,13 @@
// //
import { MeasureContext } from '@hcengineering/core' import { MeasureContext } from '@hcengineering/core'
import { type UpdateContentRequest, type UpdateContentResponse } from '@hcengineering/collaborator-client' import {
import { applyUpdate, encodeStateAsUpdate } from 'yjs' parseDocumentId,
type UpdateContentRequest,
type UpdateContentResponse
} from '@hcengineering/collaborator-client'
import { YDocVersion, takeCollaborativeDocSnapshot } from '@hcengineering/collaboration'
import { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs'
import { Context } from '../../context' import { Context } from '../../context'
import { RpcMethodParams } from '../rpc' import { RpcMethodParams } from '../rpc'
@ -25,12 +30,18 @@ export async function updateContent (
payload: UpdateContentRequest, payload: UpdateContentRequest,
params: RpcMethodParams params: RpcMethodParams
): Promise<UpdateContentResponse> { ): Promise<UpdateContentResponse> {
const { documentId, field, html } = payload const { documentId, content, snapshot } = payload
const { hocuspocus, transformer } = params const { hocuspocus, transformer, storageAdapter } = params
const { workspaceId } = context
const update = await ctx.with('transform', {}, () => { const updates = await ctx.with('transform', {}, () => {
const ydoc = transformer.toYdoc(html, field) const updates: Record<string, Uint8Array> = {}
return encodeStateAsUpdate(ydoc)
Object.entries(content).forEach(([field, markup]) => {
const ydoc = transformer.toYdoc(markup, field)
updates[field] = encodeStateAsUpdate(ydoc)
})
return updates
}) })
const connection = await ctx.with('connect', {}, async () => { const connection = await ctx.with('connect', {}, async () => {
@ -40,13 +51,31 @@ export async function updateContent (
try { try {
await ctx.with('update', {}, async () => { await ctx.with('update', {}, async () => {
await connection.transact((document) => { await connection.transact((document) => {
const fragment = document.getXmlFragment(field)
document.transact(() => { document.transact(() => {
fragment.delete(0, fragment.length) Object.entries(updates).forEach(([field, update]) => {
applyUpdate(document, update) const fragment = document.getXmlFragment(field)
fragment.delete(0, fragment.length)
applyUpdate(document, update)
})
}) })
}) })
}) })
if (snapshot !== undefined && snapshot.versionId !== 'HEAD') {
const ydoc = connection.document ?? new YDoc()
const { collaborativeDoc } = parseDocumentId(documentId)
const version: YDocVersion = {
versionId: snapshot.versionId,
name: snapshot.versionName ?? snapshot.versionId,
createdBy: snapshot.createdBy,
createdOn: Date.now()
}
await ctx.with('snapshot', {}, async () => {
await takeCollaborativeDocSnapshot(storageAdapter, workspaceId, collaborativeDoc, ydoc, version, ctx)
})
}
} finally { } finally {
await connection.disconnect() await connection.disconnect()
} }

View File

@ -15,7 +15,6 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core' import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core'
import type { MongoClientReference } from '@hcengineering/mongo'
import type { StorageAdapter } from '@hcengineering/server-core' import type { StorageAdapter } from '@hcengineering/server-core'
import { Token, decodeToken } from '@hcengineering/server-token' import { Token, decodeToken } from '@hcengineering/server-token'
import { ServerKit } from '@hcengineering/text' import { ServerKit } from '@hcengineering/text'
@ -44,16 +43,10 @@ export type Shutdown = () => Promise<void>
/** /**
* @public * @public
*/ */
export async function start ( export async function start (ctx: MeasureContext, config: Config, storageAdapter: StorageAdapter): Promise<Shutdown> {
ctx: MeasureContext,
config: Config,
storageAdapter: StorageAdapter,
mongoClient: MongoClientReference
): Promise<Shutdown> {
const port = config.Port const port = config.Port
ctx.info('Starting collaborator server', { port }) ctx.info('Starting collaborator server', { port })
const mongo = await mongoClient.getClient()
const app = express() const app = express()
app.use(cors()) app.use(cors())
@ -116,7 +109,7 @@ export async function start (
}), }),
new StorageExtension({ new StorageExtension({
ctx: extensionsCtx.newChild('storage', {}), ctx: extensionsCtx.newChild('storage', {}),
adapter: new PlatformStorageAdapter(storageAdapter, mongo, transformerFactory) adapter: new PlatformStorageAdapter(storageAdapter)
}) })
] ]
}) })

View File

@ -18,7 +18,6 @@ import { setMetadata } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token' import serverToken from '@hcengineering/server-token'
import type { MeasureContext } from '@hcengineering/core' import type { MeasureContext } from '@hcengineering/core'
import { getMongoClient } from '@hcengineering/mongo'
import type { StorageConfiguration } from '@hcengineering/server-core' import type { StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage' import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import config from './config' import config from './config'
@ -33,15 +32,11 @@ export async function startCollaborator (ctx: MeasureContext, onClose?: () => vo
const storageConfig: StorageConfiguration = storageConfigFromEnv() const storageConfig: StorageConfiguration = storageConfigFromEnv()
const storageAdapter = buildStorageFromConfig(storageConfig, config.MongoUrl) const storageAdapter = buildStorageFromConfig(storageConfig, config.MongoUrl)
const mongoClient = getMongoClient(config.MongoUrl) const shutdown = await start(ctx, config, storageAdapter)
const shutdown = await start(ctx, config, storageAdapter, mongoClient)
const close = (): void => { const close = (): void => {
void storageAdapter.close() void storageAdapter.close()
void shutdown().then(() => { void shutdown()
mongoClient.close()
})
onClose?.() onClose?.()
} }

View File

@ -27,27 +27,18 @@ import {
} from '@hcengineering/collaborator-client' } from '@hcengineering/collaborator-client'
import core, { import core, {
CollaborativeDoc, CollaborativeDoc,
Doc,
MeasureContext, MeasureContext,
TxOperations, TxOperations,
collaborativeDocWithLastVersion, collaborativeDocWithLastVersion
toWorkspaceString
} from '@hcengineering/core' } from '@hcengineering/core'
import { StorageAdapter } from '@hcengineering/server-core' import { StorageAdapter } from '@hcengineering/server-core'
import { areEqualMarkups } from '@hcengineering/text'
import { MongoClient } from 'mongodb'
import { Doc as YDoc } from 'yjs' import { Doc as YDoc } from 'yjs'
import { Context } from '../context' import { Context } from '../context'
import { TransformerFactory } from '../types'
import { CollabStorageAdapter } from './adapter' import { CollabStorageAdapter } from './adapter'
export class PlatformStorageAdapter implements CollabStorageAdapter { export class PlatformStorageAdapter implements CollabStorageAdapter {
constructor ( constructor (private readonly storage: StorageAdapter) {}
private readonly storage: StorageAdapter,
private readonly mongodb: MongoClient,
private readonly transformerFactory: TransformerFactory
) {}
async loadDocument (ctx: MeasureContext, documentId: DocumentId, context: Context): Promise<YDoc | undefined> { async loadDocument (ctx: MeasureContext, documentId: DocumentId, context: Context): Promise<YDoc | undefined> {
// try to load document content // try to load document content
@ -83,28 +74,6 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
} }
// finally try to load from the platform
const { platformDocumentId } = context
if (platformDocumentId !== undefined) {
ctx.info('load document platform content', { documentId, platformDocumentId })
const ydoc = await ctx.with('load-from-platform', {}, async (ctx) => {
try {
return await this.loadDocumentFromPlatform(ctx, platformDocumentId, context)
} catch (err) {
ctx.error('failed to load platform document', { documentId, platformDocumentId, error: err })
throw err
}
})
// if document was loaded from the initial content or storage we need to save
// it to ensure the next time we load it from the ydoc document
if (ydoc !== undefined) {
ctx.info('save document content', { documentId, platformDocumentId })
await this.saveDocumentToStorage(ctx, documentId, ydoc, context)
return ydoc
}
}
// nothing found // nothing found
return undefined return undefined
} }
@ -139,7 +108,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
if (platformDocumentId !== undefined) { if (platformDocumentId !== undefined) {
ctx.info('save document content to platform', { documentId, platformDocumentId }) ctx.info('save document content to platform', { documentId, platformDocumentId })
await ctx.with('save-to-platform', {}, async (ctx) => { await ctx.with('save-to-platform', {}, async (ctx) => {
await this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, document, snapshot, context) await this.saveDocumentToPlatform(ctx, client, documentId, platformDocumentId, snapshot)
}) })
} }
} finally { } finally {
@ -203,46 +172,18 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
return yDocVersion return yDocVersion
} }
async loadDocumentFromPlatform (
ctx: MeasureContext,
platformDocumentId: PlatformDocumentId,
context: Context
): Promise<YDoc | undefined> {
const { workspaceId } = context
const { objectDomain, objectId, objectAttr } = parsePlatformDocumentId(platformDocumentId)
const doc = await ctx.with('query', {}, async () => {
const db = this.mongodb.db(toWorkspaceString(workspaceId))
return await db.collection<Doc>(objectDomain).findOne({ _id: objectId }, { projection: { [objectAttr]: 1 } })
})
const content = doc !== null && objectAttr in doc ? ((doc as any)[objectAttr] as string) : ''
if (content.startsWith('{') && content.endsWith('}')) {
return await ctx.with('transform', {}, () => {
const transformer = this.transformerFactory(workspaceId)
return transformer.toYdoc(content, objectAttr)
})
}
// the content does not seem to be an HTML document
return undefined
}
async saveDocumentToPlatform ( async saveDocumentToPlatform (
ctx: MeasureContext, ctx: MeasureContext,
client: Omit<TxOperations, 'close'>, client: Omit<TxOperations, 'close'>,
documentName: string, documentName: string,
platformDocumentId: PlatformDocumentId, platformDocumentId: PlatformDocumentId,
document: YDoc, snapshot: YDocVersion | undefined
snapshot: YDocVersion | undefined,
context: Context
): Promise<void> { ): Promise<void> {
const { workspaceId } = context
const { objectClass, objectId, objectAttr } = parsePlatformDocumentId(platformDocumentId) const { objectClass, objectId, objectAttr } = parsePlatformDocumentId(platformDocumentId)
const attribute = client.getHierarchy().findAttribute(objectClass, objectAttr) const attribute = client.getHierarchy().findAttribute(objectClass, objectAttr)
if (attribute === undefined) { if (attribute === undefined) {
ctx.info('attribute not found', { documentName, objectClass, objectAttr }) ctx.warn('attribute not found', { documentName, objectClass, objectAttr })
return return
} }
@ -251,6 +192,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
}) })
if (current === undefined) { if (current === undefined) {
ctx.warn('document not found', { documentName, objectClass, objectId })
return return
} }
@ -265,17 +207,8 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
await ctx.with('update', {}, async () => { await ctx.with('update', {}, async () => {
await client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc }) await client.diffUpdate(current, { [objectAttr]: newCollaborativeDoc })
}) })
} else if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)) { } else {
// TODO a temporary solution while we are keeping Markup in Mongo ctx.error('unsupported attribute type', { documentName, objectClass, objectAttr })
const content = await ctx.with('transform', {}, () => {
const transformer = this.transformerFactory(workspaceId)
return transformer.fromYdoc(document, objectAttr)
})
if (!areEqualMarkups(content, (current as any)[objectAttr])) {
await ctx.with('update', {}, async () => {
await client.diffUpdate(current, { [objectAttr]: content })
})
}
} }
} }
} }

View File

@ -374,10 +374,7 @@ function updateDoc2Elastic (
)) ))
) { ) {
let vvv = vv let vvv = vv
if ( if (attribute.type._class === core.class.TypeMarkup) {
attribute.type._class === core.class.TypeMarkup ||
attribute.type._class === core.class.TypeCollaborativeMarkup
) {
ctx.withSync('markup-to-json-text', {}, () => { ctx.withSync('markup-to-json-text', {}, () => {
vvv = jsonToText(markupToJSON(vv)) vvv = jsonToText(markupToJSON(vv))
}) })

View File

@ -251,7 +251,7 @@ export async function extractIndexedValues (
continue continue
} }
if (keyAttr.type._class === core.class.TypeMarkup || keyAttr.type._class === core.class.TypeCollaborativeMarkup) { if (keyAttr.type._class === core.class.TypeMarkup) {
sourceContent = jsonToText(markupToJSON(sourceContent)) sourceContent = jsonToText(markupToJSON(sourceContent))
} }

View File

@ -14,7 +14,6 @@ Front service is suited to deliver application bundles and resource assets, it a
* TELEGRAM_URL: Specifies the URL of the Telegram service. * TELEGRAM_URL: Specifies the URL of the Telegram service.
* REKONI_URL: Specifies the URL of the Rekoni service. * REKONI_URL: Specifies the URL of the Rekoni service.
* COLLABORATOR_URL: Specifies the URL of the collaborator service. * COLLABORATOR_URL: Specifies the URL of the collaborator service.
* COLLABORATOR_API_URL: Specifies the URL of the collaborator API.
* MODEL_VERSION: Specifies the required model version. * MODEL_VERSION: Specifies the required model version.
* SERVER_SECRET: Specifies the server secret. * SERVER_SECRET: Specifies the server secret.
* PREVIEW_CONFIG: Specifies the preview configuration. * PREVIEW_CONFIG: Specifies the preview configuration.

View File

@ -248,7 +248,6 @@ export function start (
gmailUrl: string gmailUrl: string
calendarUrl: string calendarUrl: string
collaboratorUrl: string collaboratorUrl: string
collaboratorApiUrl: string
brandingUrl?: string brandingUrl?: string
previewConfig: string previewConfig: string
pushPublicKey?: string pushPublicKey?: string
@ -299,7 +298,6 @@ export function start (
GMAIL_URL: config.gmailUrl, GMAIL_URL: config.gmailUrl,
CALENDAR_URL: config.calendarUrl, CALENDAR_URL: config.calendarUrl,
COLLABORATOR_URL: config.collaboratorUrl, COLLABORATOR_URL: config.collaboratorUrl,
COLLABORATOR_API_URL: config.collaboratorApiUrl,
BRANDING_URL: config.brandingUrl, BRANDING_URL: config.brandingUrl,
PREVIEW_CONFIG: config.previewConfig, PREVIEW_CONFIG: config.previewConfig,
PUSH_PUBLIC_KEY: config.pushPublicKey, PUSH_PUBLIC_KEY: config.pushPublicKey,

View File

@ -81,12 +81,6 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
process.exit(1) process.exit(1)
} }
const collaboratorApiUrl = process.env.COLLABORATOR_API_URL
if (collaboratorApiUrl === undefined) {
console.error('please provide collaborator api url')
process.exit(1)
}
const modelVersion = process.env.MODEL_VERSION const modelVersion = process.env.MODEL_VERSION
if (modelVersion === undefined) { if (modelVersion === undefined) {
console.error('please provide model version requirement') console.error('please provide model version requirement')
@ -135,7 +129,6 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
rekoniUrl, rekoniUrl,
calendarUrl, calendarUrl,
collaboratorUrl, collaboratorUrl,
collaboratorApiUrl,
brandingUrl, brandingUrl,
previewConfig, previewConfig,
pushPublicKey pushPublicKey

View File

@ -22,7 +22,7 @@
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/ai-bot staging", "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/ai-bot staging",
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/ai-bot", "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/ai-bot",
"docker:tbuild": "rush bundle --to @hcengineering/pod-ai-bot && docker build -t hardcoreeng/ai-bot . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/ai-bot", "docker:tbuild": "rush bundle --to @hcengineering/pod-ai-bot && docker build -t hardcoreeng/ai-bot . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/ai-bot",
"run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts", "run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts",
"format": "format src", "format": "format src",
"_phase:build": "compile transpile src", "_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent", "_phase:test": "jest --passWithNoTests --silent",

View File

@ -22,7 +22,7 @@
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector staging", "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector staging",
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector", "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/analytics-collector",
"docker:tbuild": "rush bundle --to @hcengineering/pod-analytics-collector && docker build -t hardcoreeng/analytics-collector . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/analytics-collector", "docker:tbuild": "rush bundle --to @hcengineering/pod-analytics-collector && docker build -t hardcoreeng/analytics-collector . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/analytics-collector",
"run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts", "run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts",
"format": "format src", "format": "format src",
"_phase:build": "compile transpile src", "_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent", "_phase:test": "jest --passWithNoTests --silent",

View File

@ -248,10 +248,7 @@ async function migrateMarkup (client: MigrationClient): Promise<void> {
for (const _class of classes) { for (const _class of classes) {
const attributes = hierarchy.getAllAttributes(_class) const attributes = hierarchy.getAllAttributes(_class)
const filtered = Array.from(attributes.values()).filter((attribute) => { const filtered = Array.from(attributes.values()).filter((attribute) => {
return ( return hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup)
hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) ||
hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)
)
}) })
if (filtered.length === 0) continue if (filtered.length === 0) continue

View File

@ -22,7 +22,7 @@
"docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/github staging", "docker:staging": "../../../common/scripts/docker_tag.sh hardcoreeng/github staging",
"docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/github", "docker:push": "../../../common/scripts/docker_tag.sh hardcoreeng/github",
"docker:tbuild": "rush bundle --to @hcengineering/pod-github && docker build -t hardcoreeng/github . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/github", "docker:tbuild": "rush bundle --to @hcengineering/pod-github && docker build -t hardcoreeng/github . --platform=linux/amd64 && ../../../common/scripts/docker_tag_push.sh hardcoreeng/github",
"run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 COLLABORATOR_API_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts", "run-local": "cross-env APP_ID=$(cat ../../../../uberflow_private/appid) PRIVATE_KEY=\"$(cat ../../../../uberflow_private/private-key.pem)\" CLIENT_ID=$(cat ../../../../uberflow_private/client-id) CLIENT_SECRET=$(cat ../../../../uberflow_private/client-secret) SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000/ COLLABORATOR_URL=http://localhost:3078 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost ts-node src/index.ts",
"format": "format src", "format": "format src",
"_phase:build": "compile transpile src", "_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent", "_phase:test": "jest --passWithNoTests --silent",

View File

@ -4,14 +4,14 @@
// //
import { CollaboratorClient, getClient as getCollaboratorClient } from '@hcengineering/collaborator-client' import { CollaboratorClient, getClient as getCollaboratorClient } from '@hcengineering/collaborator-client'
import { Hierarchy, WorkspaceId } from '@hcengineering/core' import { WorkspaceId } from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token' import { generateToken } from '@hcengineering/server-token'
import config from './config' import config from './config'
/** /**
* @public * @public
*/ */
export function createCollaboratorClient (hierarchy: Hierarchy, workspaceId: WorkspaceId): CollaboratorClient { export function createCollaboratorClient (workspaceId: WorkspaceId): CollaboratorClient {
const token = generateToken(config.SystemEmail, workspaceId, { mode: 'github' }) const token = generateToken(config.SystemEmail, workspaceId, { mode: 'github' })
return getCollaboratorClient(hierarchy, workspaceId, token, config.CollaboratorURL) return getCollaboratorClient(workspaceId, token, config.CollaboratorURL)
} }

View File

@ -54,7 +54,7 @@ const envMap: { [key in keyof Config]: string } = {
MongoURL: 'MONGO_URL', MongoURL: 'MONGO_URL',
ConfigurationDB: 'MONGO_DB', ConfigurationDB: 'MONGO_DB',
CollaboratorURL: 'COLLABORATOR_API_URL', CollaboratorURL: 'COLLABORATOR_URL',
ProductID: 'PRODUCT_ID', ProductID: 'PRODUCT_ID',

View File

@ -5,7 +5,7 @@
import { Branding, TxOperations, WorkspaceIdWithUrl } from '@hcengineering/core' import { Branding, TxOperations, WorkspaceIdWithUrl } from '@hcengineering/core'
import { MarkupMarkType, MarkupNode, MarkupNodeType, traverseMarkupNode } from '@hcengineering/text' import { MarkupMarkType, MarkupNode, MarkupNodeType, traverseMarkupNode } from '@hcengineering/text'
import { getPublicLink } from '@hcengineering/server-guest-resources' import { getPublicLink } from '@hcengineering/server-guest-resources'
import { Issue } from '@hcengineering/tracker' import { Task } from '@hcengineering/task'
const githubLinkText = process.env.LINK_TEXT ?? 'Huly&reg;:' const githubLinkText = process.env.LINK_TEXT ?? 'Huly&reg;:'
@ -46,7 +46,7 @@ export async function stripGuestLink (markdown: MarkupNode): Promise<void> {
} }
export async function appendGuestLink ( export async function appendGuestLink (
client: TxOperations, client: TxOperations,
doc: Issue, doc: Task,
markdown: MarkupNode, markdown: MarkupNode,
workspace: WorkspaceIdWithUrl, workspace: WorkspaceIdWithUrl,
branding: Branding | null branding: Branding | null

View File

@ -14,16 +14,16 @@ import core, {
Account, Account,
AttachedDoc, AttachedDoc,
Class, Class,
CollaborativeDoc,
Doc, Doc,
DocumentUpdate, DocumentUpdate,
Markup,
MeasureContext, MeasureContext,
Ref, Ref,
Space, Space,
Status, Status,
TxOperations, TxOperations,
generateId, generateId
getCollaborativeDoc,
getCollaborativeDocId
} from '@hcengineering/core' } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import { LiveQuery } from '@hcengineering/query' import { LiveQuery } from '@hcengineering/query'
@ -72,11 +72,18 @@ import {
isGHWriteAllowed isGHWriteAllowed
} from './utils' } from './utils'
/**
* @public
*/
export type WithMarkup<T> = {
[P in keyof T]: T[P] extends CollaborativeDoc ? Markup : T[P]
}
/** /**
* @public * @public
*/ */
export type GithubIssueData = Omit< export type GithubIssueData = Omit<
Issue, WithMarkup<Issue>,
| 'commits' | 'commits'
| 'attachments' | 'attachments'
| 'commits' | 'commits'
@ -103,6 +110,11 @@ Issue,
> & > &
Record<string, any> Record<string, any>
/**
* @public
*/
export type IssueUpdate = DocumentUpdate<WithMarkup<Issue>>
/** /**
* @public * @public
*/ */
@ -325,7 +337,7 @@ export abstract class IssueSyncManagerBase {
async handleUpdate ( async handleUpdate (
external: IssueExternalData, external: IssueExternalData,
derivedClient: TxOperations, derivedClient: TxOperations,
update: DocumentUpdate<Issue>, update: IssueUpdate,
account: Ref<Account>, account: Ref<Account>,
prj: GithubProject, prj: GithubProject,
needSync: boolean, needSync: boolean,
@ -334,7 +346,7 @@ export abstract class IssueSyncManagerBase {
state: DocSyncInfo, state: DocSyncInfo,
existing: Issue, existing: Issue,
external: IssueExternalData, external: IssueExternalData,
update: DocumentUpdate<Issue> update: IssueUpdate
) => Promise<boolean>, ) => Promise<boolean>,
extraSyncUpdate?: DocumentUpdate<DocSyncInfo> extraSyncUpdate?: DocumentUpdate<DocSyncInfo>
): Promise<void> { ): Promise<void> {
@ -354,13 +366,22 @@ export abstract class IssueSyncManagerBase {
const lastModified = new Date().getTime() const lastModified = new Date().getTime()
if (doc !== undefined && ((await verifyUpdate?.(syncData, doc, external, update)) ?? true)) { if (doc !== undefined && ((await verifyUpdate?.(syncData, doc, external, update)) ?? true)) {
const issueData: DocumentUpdate<Issue> = { ...update, description: doc.description }
if ( if (
update.description !== undefined && update.description !== undefined &&
!areEqualMarkups(update.description, syncData.current?.description ?? '') !areEqualMarkups(update.description, syncData.current?.description ?? '')
) { ) {
try { try {
const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(doc._id, 'description')) const versionId = `${Date.now()}`
await this.collaborator.updateContent(collaborativeDoc, 'description', update.description) issueData.description = await this.collaborator.updateContent(
doc.description,
{ description: update.description },
{
versionId,
versionName: versionId,
createdBy: account
}
)
} catch (err: any) { } catch (err: any) {
Analytics.handleError(err) Analytics.handleError(err)
this.ctx.error(err) this.ctx.error(err)
@ -405,7 +426,12 @@ export abstract class IssueSyncManagerBase {
}, },
lastModified lastModified
) )
await this.client.diffUpdate(this.client.getHierarchy().as(doc, prj.mixinClass), update, lastModified, account) await this.client.diffUpdate(
this.client.getHierarchy().as(doc, prj.mixinClass),
issueData,
lastModified,
account
)
this.provider.sync() this.provider.sync()
} }
} }
@ -649,7 +675,7 @@ export abstract class IssueSyncManagerBase {
abstract performIssueFieldsUpdate ( abstract performIssueFieldsUpdate (
info: DocSyncInfo, info: DocSyncInfo,
existing: Issue, existing: WithMarkup<Issue>,
platformUpdate: DocumentUpdate<Issue>, platformUpdate: DocumentUpdate<Issue>,
issueData: GithubIssueData, issueData: GithubIssueData,
container: ContainerFocus, container: ContainerFocus,
@ -662,7 +688,7 @@ export abstract class IssueSyncManagerBase {
async handleDiffUpdate ( async handleDiffUpdate (
target: IssueSyncTarget, target: IssueSyncTarget,
existing: Issue, existing: WithMarkup<Issue>,
info: DocSyncInfo, info: DocSyncInfo,
issueData: GithubIssueData, issueData: GithubIssueData,
container: ContainerFocus, container: ContainerFocus,
@ -894,21 +920,30 @@ export abstract class IssueSyncManagerBase {
} }
} }
if (Object.keys(update).length > 0) { // Update collaborative description
// Update collaborative description if (update.description !== undefined) {
this.ctx.info(`<= perform ${issueExternal.url} update to collaborator`, { this.ctx.info(`<= perform ${issueExternal.url} update to collaborator`, {
workspace: this.provider.getWorkspaceId().name workspace: this.provider.getWorkspaceId().name
}) })
if (update.description !== undefined) { try {
try { const versionId = `${Date.now()}`
issueData.description = update.description issueData.description = update.description
const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existingIssue._id, 'description')) update.description = await this.collaborator.updateContent(
await this.collaborator.updateContent(collaborativeDoc, 'description', update.description) existingIssue.description,
} catch (err: any) { { description: update.description },
Analytics.handleError(err) {
this.ctx.error('error during description update', err) versionId,
} versionName: versionId,
createdBy: account
}
)
} catch (err: any) {
Analytics.handleError(err)
this.ctx.error('error during description update', err)
} }
}
if (Object.keys(update).length > 0) {
// We have some fields to update of existing from external // We have some fields to update of existing from external
this.ctx.info(`<= perform ${issueExternal.url} update to platform`, { this.ctx.info(`<= perform ${issueExternal.url} update to platform`, {
...update, ...update,
@ -930,7 +965,7 @@ export abstract class IssueSyncManagerBase {
private async notifyConnected ( private async notifyConnected (
container: ContainerFocus, container: ContainerFocus,
info: DocSyncInfo, info: DocSyncInfo,
existing: Issue, existing: WithMarkup<Issue>,
issueExternal: IssueExternalData issueExternal: IssueExternalData
): Promise<void> { ): Promise<void> {
const repo = container.repository.find((it) => it._id === info.repository) as GithubIntegrationRepository const repo = container.repository.find((it) => it._id === info.repository) as GithubIntegrationRepository
@ -948,9 +983,9 @@ export abstract class IssueSyncManagerBase {
async collectIssueUpdate ( async collectIssueUpdate (
info: DocSyncInfo, info: DocSyncInfo,
doc: Issue, doc: WithMarkup<Issue>,
platformUpdate: DocumentUpdate<Issue>, platformUpdate: DocumentUpdate<Issue>,
issueData: Pick<Issue, 'title' | 'description' | 'assignee' | 'status'>, issueData: Pick<WithMarkup<Issue>, 'title' | 'description' | 'assignee' | 'status'>,
container: ContainerFocus, container: ContainerFocus,
issueExternal: IssueExternalData, issueExternal: IssueExternalData,
_class: Ref<Class<Issue>> _class: Ref<Class<Issue>>

View File

@ -19,11 +19,9 @@ import core, {
TxOperations, TxOperations,
cutObjectArray, cutObjectArray,
generateId, generateId,
getCollaborativeDoc, makeCollaborativeDoc
getCollaborativeDocId
} from '@hcengineering/core' } from '@hcengineering/core'
import task, { TaskType, calcRank } from '@hcengineering/task' import task, { TaskType, calcRank } from '@hcengineering/task'
import { isEmptyMarkup } from '@hcengineering/text'
import tracker, { Issue, IssuePriority } from '@hcengineering/tracker' import tracker, { Issue, IssuePriority } from '@hcengineering/tracker'
import { Issue as GithubIssue, IssuesEvent, ProjectsV2ItemEvent } from '@octokit/webhooks-types' import { Issue as GithubIssue, IssuesEvent, ProjectsV2ItemEvent } from '@octokit/webhooks-types'
import github, { import github, {
@ -47,7 +45,7 @@ import {
} from '../types' } from '../types'
import { IssueExternalData, issueDetails } from './githubTypes' import { IssueExternalData, issueDetails } from './githubTypes'
import { appendGuestLink } from './guest' import { appendGuestLink } from './guest'
import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget } from './issueBase' import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget, IssueUpdate, WithMarkup } from './issueBase'
import { syncConfig } from './syncConfig' import { syncConfig } from './syncConfig'
import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils' import { getSince, gqlp, guessStatus, isGHWriteAllowed, syncRunner } from './utils'
@ -214,7 +212,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
break break
} }
case 'edited': { case 'edited': {
const update: DocumentUpdate<Issue> = {} const update: IssueUpdate = {}
const du: DocumentUpdate<DocSyncInfo> = {} const du: DocumentUpdate<DocSyncInfo> = {}
if (event.changes.body !== undefined) { if (event.changes.body !== undefined) {
update.description = await this.provider.getMarkup(integration, event.issue.body, this.stripGuestLink) update.description = await this.provider.getMarkup(integration, event.issue.body, this.stripGuestLink)
@ -373,6 +371,11 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
return { needSync: githubSyncVersion } return { needSync: githubSyncVersion }
} }
const description = await this.ctx.withLog('query collaborative description', {}, async () => {
const content = await this.collaborator.getContent((existing as Issue).description)
return content.description ?? ''
})
this.ctx.info('create github issue', { this.ctx.info('create github issue', {
title: (existing as Issue).title, title: (existing as Issue).title,
number: (existing as Issue).number, number: (existing as Issue).number,
@ -382,7 +385,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
'create github issue', 'create github issue',
{}, {},
async () => { async () => {
this.createPromise = this.createGithubIssue(container, existing as Issue, repository) this.createPromise = this.createGithubIssue(container, { ...(existing as Issue), description }, repository)
return await this.createPromise return await this.createPromise
}, },
{ id: (existing as Issue).identifier, workspace: this.provider.getWorkspaceId().name } { id: (existing as Issue).identifier, workspace: this.provider.getWorkspaceId().name }
@ -612,13 +615,12 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
} }
} else { } else {
try { try {
const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existing._id, 'description'))
const description = await this.ctx.withLog( const description = await this.ctx.withLog(
'query collaborative description', 'query collaborative description',
{}, {},
async () => { async () => {
const content = await this.collaborator.getContent(collaborativeDoc, 'description') const content = await this.collaborator.getContent((existing as Issue).description)
return isEmptyMarkup(content) ? (existing as Issue).description : content return content.description ?? ''
}, },
{ url: issueExternal.url } { url: issueExternal.url }
) )
@ -657,9 +659,9 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
async performIssueFieldsUpdate ( async performIssueFieldsUpdate (
info: DocSyncInfo, info: DocSyncInfo,
existing: Issue, existing: WithMarkup<Issue>,
platformUpdate: DocumentUpdate<Issue>, platformUpdate: DocumentUpdate<Issue>,
issueData: Pick<Issue, 'title' | 'description' | 'assignee' | 'status' | 'remainingTime' | 'component'>, issueData: Pick<WithMarkup<Issue>, 'title' | 'description' | 'assignee' | 'status' | 'remainingTime' | 'component'>,
container: ContainerFocus, container: ContainerFocus,
issueExternal: IssueExternalData, issueExternal: IssueExternalData,
okit: Octokit, okit: Octokit,
@ -751,7 +753,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
async createGithubIssue ( async createGithubIssue (
container: ContainerFocus, container: ContainerFocus,
existing: Issue, existing: WithMarkup<Issue>,
repository: GithubIntegrationRepository repository: GithubIntegrationRepository
): Promise<IssueExternalData | undefined> { ): Promise<IssueExternalData | undefined> {
const existingIssue = existing const existingIssue = existing
@ -847,8 +849,13 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
const number = (incResult as any).object.sequence const number = (incResult as any).object.sequence
const issueId = info._id as unknown as Ref<Issue>
const { description, ...update } = issueData
const value: AttachedData<Issue> = { const value: AttachedData<Issue> = {
...issueData, ...update,
description: makeCollaborativeDoc(issueId, 'description'),
kind: taskType, kind: taskType,
component: null, component: null,
milestone: null, milestone: null,
@ -867,7 +874,8 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
childInfo: [], childInfo: [],
identifier: `${prj.identifier}-${number}` identifier: `${prj.identifier}-${number}`
} }
const issueId = info._id as unknown as Ref<Issue>
await this.collaborator.updateContent(value.description, { description })
await this.client.addCollection( await this.client.addCollection(
tracker.class.Issue, tracker.class.Issue,

View File

@ -16,11 +16,9 @@ import core, {
WithLookup, WithLookup,
cutObjectArray, cutObjectArray,
generateId, generateId,
getCollaborativeDoc, makeCollaborativeDoc
getCollaborativeDocId
} from '@hcengineering/core' } from '@hcengineering/core'
import task, { TaskType, calcRank, makeRank } from '@hcengineering/task' import task, { TaskType, calcRank, makeRank } from '@hcengineering/task'
import { isEmptyMarkup } from '@hcengineering/text'
import time, { ToDo, ToDoPriority } from '@hcengineering/time' import time, { ToDo, ToDoPriority } from '@hcengineering/time'
import tracker, { Issue, IssuePriority, IssueStatus, Project } from '@hcengineering/tracker' import tracker, { Issue, IssuePriority, IssueStatus, Project } from '@hcengineering/tracker'
import { OctokitResponse } from '@octokit/types' import { OctokitResponse } from '@octokit/types'
@ -59,13 +57,15 @@ import {
toReviewDecision, toReviewDecision,
toReviewState toReviewState
} from './githubTypes' } from './githubTypes'
import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget } from './issueBase' import { GithubIssueData, IssueSyncManagerBase, IssueSyncTarget, WithMarkup } from './issueBase'
import { syncConfig } from './syncConfig' import { syncConfig } from './syncConfig'
import { errorToObj, getSinceRaw, gqlp, guessStatus, isGHWriteAllowed, syncDerivedDocuments, syncRunner } from './utils' import { errorToObj, getSinceRaw, gqlp, guessStatus, isGHWriteAllowed, syncDerivedDocuments, syncRunner } from './utils'
type GithubPullRequestData = GithubIssueData & type GithubPullRequestData = GithubIssueData &
Omit<GithubPullRequest, keyof Issue | 'commits' | 'reviews' | 'reviewComments'> Omit<GithubPullRequest, keyof Issue | 'commits' | 'reviews' | 'reviewComments'>
type GithubPullRequestUpdate = DocumentUpdate<WithMarkup<GithubPullRequest>>
export class PullRequestSyncManager extends IssueSyncManagerBase implements DocSyncManager { export class PullRequestSyncManager extends IssueSyncManagerBase implements DocSyncManager {
externalDerivedSync = true externalDerivedSync = true
async handleEvent<T>(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise<void> { async handleEvent<T>(integration: IntegrationContainer, derivedClient: TxOperations, evt: T): Promise<void> {
@ -194,7 +194,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
break break
} }
case 'edited': { case 'edited': {
const update: DocumentUpdate<GithubPullRequest> = {} const update: GithubPullRequestUpdate = {}
const du: DocumentUpdate<DocSyncInfo> = {} const du: DocumentUpdate<DocSyncInfo> = {}
if (event.changes.title !== undefined) { if (event.changes.title !== undefined) {
update.title = event.pull_request.title update.title = event.pull_request.title
@ -584,9 +584,8 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
'query collaborative pull request description', 'query collaborative pull request description',
{}, {},
async () => { async () => {
const collaborativeDoc = getCollaborativeDoc(getCollaborativeDocId(existing._id, 'description')) const content = await this.collaborator.getContent((existing as any).description)
const content = await this.collaborator.getContent(collaborativeDoc, 'description') return content.description
return isEmptyMarkup(content) ? (existing as Issue).description : content
}, },
{ url: pullRequestExternal.url } { url: pullRequestExternal.url }
) )
@ -987,7 +986,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
info: DocSyncInfo, info: DocSyncInfo,
existing: Issue, existing: Issue,
platformUpdate: DocumentUpdate<Issue>, platformUpdate: DocumentUpdate<Issue>,
issueData: Pick<Issue, 'title' | 'description' | 'assignee' | 'status' | 'remainingTime' | 'component'>, issueData: Pick<WithMarkup<Issue>, 'title' | 'description' | 'assignee' | 'status' | 'remainingTime' | 'component'>,
container: ContainerFocus, container: ContainerFocus,
issueExternal: IssueExternalData, issueExternal: IssueExternalData,
okit: Octokit, okit: Octokit,
@ -1162,10 +1161,14 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
account account
) )
const prId = info._id as unknown as Ref<GithubPullRequest>
const { description, ...data } = pullRequestData
const project = (incResult as any).object as Project const project = (incResult as any).object as Project
const number = project.sequence const number = project.sequence
const value: AttachedData<GithubPullRequest> = { const value: AttachedData<GithubPullRequest> = {
...pullRequestData, ...data,
description: makeCollaborativeDoc(prId, 'description'),
kind: taskType, kind: taskType,
component: null, component: null,
milestone: null, milestone: null,
@ -1188,7 +1191,8 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
reviews: 0 reviews: 0
} }
const prId = info._id as unknown as Ref<GithubPullRequest> await this.collaborator.updateContent(value.description, { description })
await client.addCollection( await client.addCollection(
github.class.GithubPullRequest, github.class.GithubPullRequest,
info.space, info.space,

View File

@ -320,7 +320,7 @@ export class GithubWorker implements IntegrationManager {
this.repositoryManager = new RepositorySyncMapper(this.ctx.newChild('repository', {}), this._client, this.app) this.repositoryManager = new RepositorySyncMapper(this.ctx.newChild('repository', {}), this._client, this.app)
this.collaborator = createCollaboratorClient(this._client.getHierarchy(), this.workspace) this.collaborator = createCollaboratorClient(this.workspace)
this.personMapper = new UsersSyncManager(this.ctx.newChild('users', {}), this._client, this.liveQuery) this.personMapper = new UsersSyncManager(this.ctx.newChild('users', {}), this._client, this.liveQuery)

View File

@ -74,7 +74,6 @@ services:
- REKONI_URL=http://rekoni:4007 - REKONI_URL=http://rekoni:4007
- TELEGRAM_URL=http://localhost:8086 - TELEGRAM_URL=http://localhost:8086
- COLLABORATOR_URL=ws://localhost:3079 - COLLABORATOR_URL=ws://localhost:3079
- COLLABORATOR_API_URL=http://localhost:3079
- STORAGE_CONFIG=${STORAGE_CONFIG} - STORAGE_CONFIG=${STORAGE_CONFIG}
- BRANDING_URL=http://localhost:8083/branding-test.json - BRANDING_URL=http://localhost:8083/branding-test.json
transactor: transactor:
@ -102,6 +101,7 @@ services:
- REKONI_URL=http://rekoni:7 - REKONI_URL=http://rekoni:7
- FRONT_URL=http://localhost:8083 - FRONT_URL=http://localhost:8083
- ACCOUNTS_URL=http://account:3003 - ACCOUNTS_URL=http://account:3003
- COLLABORATOR_URL=http://collaborator:3079
- LAST_NAME_FIRST=true - LAST_NAME_FIRST=true
- ELASTIC_INDEX_NAME=local_storage_index - ELASTIC_INDEX_NAME=local_storage_index
- BRANDING_PATH=/var/cfg/branding.json - BRANDING_PATH=/var/cfg/branding.json
@ -112,9 +112,9 @@ services:
- minio - minio
- transactor - transactor
ports: ports:
- 3079:3078 - 3079:3079
environment: environment:
- COLLABORATOR_PORT=3078 - COLLABORATOR_PORT=3079
- SECRET=secret - SECRET=secret
- ACCOUNTS_URL=http://account:3003 - ACCOUNTS_URL=http://account:3003
- MONGO_URL=mongodb://mongodb:27018 - MONGO_URL=mongodb://mongodb:27018

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test' import { test } from '@playwright/test'
import { NewDocument } from './model/documents/types' import { NewDocument } from './model/documents/types'
import { LeftSideMenuPage } from './model/left-side-menu-page' import { LeftSideMenuPage } from './model/left-side-menu-page'
import { DocumentsPage } from './model/documents/documents-page' import { DocumentsPage } from './model/documents/documents-page'
@ -17,8 +17,6 @@ test.use({
storageState: PlatformSetting storageState: PlatformSetting
}) })
const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 }
test.describe('Fulltext index', () => { test.describe('Fulltext index', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished() await (await page.goto(`${PlatformURI}/workbench/sanity-ws`))?.finished()
@ -65,21 +63,17 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newDocument.title, 1)
await spotlight.checkSearchResult(newDocument.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by content', async () => { await test.step('search by content', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(contentId)
await spotlight.fillSearchInput(contentId) await spotlight.checkSearchResult(newDocument.title, 1)
await spotlight.checkSearchResult(newDocument.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
@ -120,30 +114,24 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by old title', async () => { await test.step('search by old title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.checkSearchResult(newDocument.title, 0)
await spotlight.checkSearchResult(newDocument.title, 0) await spotlight.checkSearchResult(updatedTitle, 0)
await spotlight.checkSearchResult(updatedTitle, 0) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(updatedTitleId)
await spotlight.fillSearchInput(updatedTitleId) await spotlight.checkSearchResult(updatedTitle, 1)
await spotlight.checkSearchResult(updatedTitle, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by content', async () => { await test.step('search by content', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(updatedContentId)
await spotlight.fillSearchInput(updatedContentId) await spotlight.checkSearchResult(updatedTitle, 1)
await spotlight.checkSearchResult(updatedTitle, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
@ -169,12 +157,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newDocument.title, 1)
await spotlight.checkSearchResult(newDocument.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('remove document', async () => { await test.step('remove document', async () => {
@ -185,12 +171,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newDocument.title, 0)
await spotlight.checkSearchResult(newDocument.title, 0) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
}) })
@ -224,21 +208,17 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 1)
await spotlight.checkSearchResult(newIssue.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by content', async () => { await test.step('search by content', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(contentId)
await spotlight.fillSearchInput(contentId) await spotlight.checkSearchResult(newIssue.title, 1)
await spotlight.checkSearchResult(newIssue.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
@ -262,12 +242,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 1)
await spotlight.checkSearchResult(newIssue.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('update issue', async () => { await test.step('update issue', async () => {
@ -281,30 +259,24 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by old title', async () => { await test.step('search by old title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 0)
await spotlight.checkSearchResult(newIssue.title, 0) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(updatedTitleId)
await spotlight.fillSearchInput(updatedTitleId) await spotlight.checkSearchResult(updatedTitle, 1)
await spotlight.checkSearchResult(updatedTitle, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('search by content', async () => { await test.step('search by content', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(updatedContentId)
await spotlight.fillSearchInput(updatedContentId) await spotlight.checkSearchResult(updatedTitle, 1)
await spotlight.checkSearchResult(updatedTitle, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
@ -323,12 +295,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 1)
await spotlight.checkSearchResult(newIssue.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('remove issue', async () => { await test.step('remove issue', async () => {
@ -341,12 +311,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 0)
await spotlight.checkSearchResult(newIssue.title, 0) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
}) })
@ -375,12 +343,10 @@ test.describe('Fulltext index', () => {
}) })
await test.step('search by title', async () => { await test.step('search by title', async () => {
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 1)
await spotlight.checkSearchResult(newIssue.title, 1) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
await test.step('create workspace', async () => { await test.step('create workspace', async () => {
@ -401,12 +367,10 @@ test.describe('Fulltext index', () => {
await test.step('search by title', async () => { await test.step('search by title', async () => {
await leftSideMenuPage.clickTracker() await leftSideMenuPage.clickTracker()
await expect(async () => { await spotlight.open()
await spotlight.open() await spotlight.fillSearchInput(titleId)
await spotlight.fillSearchInput(titleId) await spotlight.checkSearchResult(newIssue.title, 0)
await spotlight.checkSearchResult(newIssue.title, 0) await spotlight.close()
await spotlight.close()
}).toPass(retryOptions)
}) })
}) })
}) })