mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 20:45:00 +02:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Vendored
+2
@@ -286,12 +286,14 @@
|
||||
"MINIO_ENDPOINT": "localhost:9000",
|
||||
"TRANSACTOR_URL": "ws://localhost:3333",
|
||||
"MONGO_URL": "mongodb://localhost:27017",
|
||||
"DB_URL": "mongodb://localhost:27017",
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
"TELEGRAM_DATABASE": "telegram-service",
|
||||
"ELASTIC_URL": "http://localhost:9200",
|
||||
"REKONI_URL": "http://localhost:4004",
|
||||
"MODEL_VERSION": "0.6.287"
|
||||
},
|
||||
"runtimeVersion": "20",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
"outputCapture": "std",
|
||||
|
||||
@@ -15,14 +15,13 @@ import documents, {
|
||||
import core, {
|
||||
AttachedData,
|
||||
BackupClient,
|
||||
CollaborativeDoc,
|
||||
Client as CoreClient,
|
||||
Data,
|
||||
MeasureContext,
|
||||
Ref,
|
||||
TxOperations,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
makeDocCollabId,
|
||||
systemAccountEmail,
|
||||
type Blob
|
||||
} from '@hcengineering/core'
|
||||
@@ -101,7 +100,7 @@ async function createDocument (
|
||||
abstract: '',
|
||||
effectiveDate: 0,
|
||||
reviewInterval: DEFAULT_PERIODIC_REVIEW_INTERVAL,
|
||||
content: makeCollaborativeDoc(generateId()),
|
||||
content: null,
|
||||
snapshots: 0,
|
||||
plannedEffectiveDate: 0
|
||||
}
|
||||
@@ -115,11 +114,6 @@ async function createDocument (
|
||||
|
||||
console.log('Creating controlled doc from template')
|
||||
|
||||
const copyContent = async (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> => {
|
||||
// intentionally left empty
|
||||
// even though the template has some content, it won't be used
|
||||
}
|
||||
|
||||
const { success } = await createControlledDocFromTemplate(
|
||||
txops,
|
||||
templateId,
|
||||
@@ -128,8 +122,7 @@ async function createDocument (
|
||||
space,
|
||||
undefined,
|
||||
undefined,
|
||||
documents.class.ControlledDocument,
|
||||
copyContent
|
||||
documents.class.ControlledDocument
|
||||
)
|
||||
if (!success) {
|
||||
throw new Error('Failed to create controlled document from template')
|
||||
@@ -184,7 +177,7 @@ async function createTemplateIfNotExist (
|
||||
approvers: [],
|
||||
coAuthors: [],
|
||||
changeControl: ccRecordId,
|
||||
content: makeCollaborativeDoc(generateId()),
|
||||
content: null,
|
||||
snapshots: 0,
|
||||
plannedEffectiveDate: 0
|
||||
}
|
||||
@@ -229,9 +222,7 @@ async function createSections (
|
||||
|
||||
console.log('Creating document content')
|
||||
|
||||
const collabId = doc.content
|
||||
|
||||
console.log(`Collab doc ID: ${collabId}`)
|
||||
const collabId = makeDocCollabId(doc, 'content')
|
||||
|
||||
try {
|
||||
let content: string = ''
|
||||
@@ -245,7 +236,7 @@ async function createSections (
|
||||
content += `<h1>${section.title}</h1>${section.content}`
|
||||
}
|
||||
|
||||
await collaborator.updateContent(collabId, { content })
|
||||
await collaborator.updateMarkup(collabId, content)
|
||||
} finally {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ services:
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- elastic
|
||||
- postgres
|
||||
- cockroach
|
||||
ports:
|
||||
- 4702:4702
|
||||
environment:
|
||||
|
||||
@@ -14,18 +14,18 @@
|
||||
//
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||
import { yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import { type Person } from '@hcengineering/contact'
|
||||
import core, {
|
||||
type Account,
|
||||
type AttachedData,
|
||||
type Class,
|
||||
type Blob as PlatformBlob,
|
||||
type CollaborativeDoc,
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
makeCollabId,
|
||||
type Mixin,
|
||||
type Ref,
|
||||
SortingOrder,
|
||||
@@ -42,7 +42,7 @@ import task, {
|
||||
type TaskType,
|
||||
type TaskTypeWithFactory
|
||||
} from '@hcengineering/task'
|
||||
import { jsonToMarkup, jsonToYDocNoSchema, parseMessageMarkdown } from '@hcengineering/text'
|
||||
import { jsonToMarkup, parseMessageMarkdown } from '@hcengineering/text'
|
||||
import tracker, {
|
||||
type Issue,
|
||||
type IssueParentInfo,
|
||||
@@ -52,7 +52,7 @@ import tracker, {
|
||||
TimeReportDayType
|
||||
} from '@hcengineering/tracker'
|
||||
import { type MarkdownPreprocessor, NoopMarkdownPreprocessor } from './preprocessor'
|
||||
import { type FileUploader, type UploadResult } from './uploader'
|
||||
import { type FileUploader } from './uploader'
|
||||
|
||||
export interface ImportWorkspace {
|
||||
projectTypes?: ImportProjectType[]
|
||||
@@ -281,14 +281,15 @@ export class WorkspaceImporter {
|
||||
): Promise<Ref<Document>> {
|
||||
const id = doc.id ?? generateId<Document>()
|
||||
const content = await doc.descrProvider()
|
||||
const collabId = await this.createCollaborativeContent(id, 'content', content, teamspaceId)
|
||||
const collabId = makeCollabId(document.class.Document, id, 'content')
|
||||
const contentId = await this.createCollaborativeContent(id, collabId, content, teamspaceId)
|
||||
|
||||
const lastRank = await getFirstRank(this.client, teamspaceId, parentId)
|
||||
const rank = makeRank(lastRank, undefined)
|
||||
|
||||
const attachedData: Data<Document> = {
|
||||
title: doc.title,
|
||||
content: collabId,
|
||||
content: contentId,
|
||||
parent: parentId,
|
||||
attachments: 0,
|
||||
embeddings: 0,
|
||||
@@ -391,7 +392,8 @@ export class WorkspaceImporter {
|
||||
): Promise<{ id: Ref<Issue>, identifier: string }> {
|
||||
const issueId = issue.id ?? generateId<Issue>()
|
||||
const content = await issue.descrProvider()
|
||||
const collabId = await this.createCollaborativeContent(issueId, 'description', content, project._id)
|
||||
const collabId = makeCollabId(tracker.class.Issue, issueId, 'description')
|
||||
const contentId = await this.createCollaborativeContent(issueId, collabId, content, project._id)
|
||||
|
||||
const { number, identifier } =
|
||||
issue.number !== undefined
|
||||
@@ -412,7 +414,7 @@ export class WorkspaceImporter {
|
||||
|
||||
const issueData: AttachedData<Issue> = {
|
||||
title: issue.title,
|
||||
description: collabId,
|
||||
description: contentId,
|
||||
assignee: issue.assignee ?? null,
|
||||
component: null,
|
||||
number,
|
||||
@@ -532,14 +534,10 @@ export class WorkspaceImporter {
|
||||
}
|
||||
|
||||
const file = new File([blob], attachment.title)
|
||||
const attachmentId = await this.createAttachment(
|
||||
attachment.id ?? generateId<Attachment>(),
|
||||
file,
|
||||
spaceId,
|
||||
parentId,
|
||||
parentClass
|
||||
)
|
||||
if (attachmentId === null) {
|
||||
|
||||
try {
|
||||
await this.createAttachment(attachment.id ?? generateId<Attachment>(), file, spaceId, parentId, parentClass)
|
||||
} catch {
|
||||
console.warn('Failed to upload attachment file: ', attachment.title)
|
||||
}
|
||||
}
|
||||
@@ -550,22 +548,9 @@ export class WorkspaceImporter {
|
||||
spaceId: Ref<Space>,
|
||||
parentId: Ref<Doc>,
|
||||
parentClass: Ref<Class<Doc<Space>>>
|
||||
): Promise<Ref<Attachment> | null> {
|
||||
const response = await this.fileUploader.uploadFile(id, id, file)
|
||||
if (response.status !== 200) {
|
||||
return null
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
if (responseText === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const uploadResult = JSON.parse(responseText) as UploadResult[]
|
||||
if (!Array.isArray(uploadResult) || uploadResult.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
): Promise<Ref<Attachment>> {
|
||||
const attachmentId = generateId<Attachment>()
|
||||
const blobId = await this.fileUploader.uploadFile(id, file)
|
||||
await this.client.addCollection(
|
||||
attachment.class.Attachment,
|
||||
spaceId,
|
||||
@@ -573,7 +558,7 @@ export class WorkspaceImporter {
|
||||
parentClass,
|
||||
'attachments',
|
||||
{
|
||||
file: uploadResult[0].id,
|
||||
file: blobId,
|
||||
lastModified: Date.now(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
@@ -581,25 +566,23 @@ export class WorkspaceImporter {
|
||||
},
|
||||
id
|
||||
)
|
||||
return id
|
||||
return attachmentId
|
||||
}
|
||||
|
||||
// Collaborative content handling
|
||||
private async createCollaborativeContent (
|
||||
id: Ref<Doc>,
|
||||
field: string,
|
||||
collabId: CollaborativeDoc,
|
||||
content: string,
|
||||
spaceId: Ref<Space>
|
||||
): Promise<CollaborativeDoc> {
|
||||
): Promise<Ref<PlatformBlob>> {
|
||||
const json = parseMessageMarkdown(content ?? '', 'image://')
|
||||
const processedJson = this.preprocessor.process(json, id, spaceId)
|
||||
const collabId = makeCollaborativeDoc(id, 'description')
|
||||
|
||||
const yDoc = jsonToYDocNoSchema(processedJson, field)
|
||||
const buffer = yDocToBuffer(yDoc)
|
||||
const markup = jsonToMarkup(processedJson)
|
||||
const buffer = Buffer.from(markup)
|
||||
|
||||
await this.fileUploader.uploadCollaborativeDoc(id, collabId, buffer)
|
||||
return collabId
|
||||
return await this.fileUploader.uploadCollaborativeDoc(collabId, buffer)
|
||||
}
|
||||
|
||||
async findIssueStatusByName (name: string): Promise<Ref<IssueStatus>> {
|
||||
|
||||
@@ -13,25 +13,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
concatLink,
|
||||
type Ref,
|
||||
type Blob as PlatformBlob,
|
||||
type Doc,
|
||||
type CollaborativeDoc,
|
||||
collaborativeDocParse
|
||||
concatLink,
|
||||
makeCollabJsonId
|
||||
} from '@hcengineering/core'
|
||||
|
||||
export interface FileUploader {
|
||||
uploadFile: (id: Ref<Doc>, name: string, file: File, contentType?: string) => Promise<Response>
|
||||
uploadCollaborativeDoc: (id: Ref<Doc>, collabId: CollaborativeDoc, data: Buffer) => Promise<Response>
|
||||
uploadFile: (name: string, file: Blob) => Promise<Ref<PlatformBlob>>
|
||||
uploadCollaborativeDoc: (collabId: CollaborativeDoc, data: Buffer) => Promise<Ref<PlatformBlob>>
|
||||
getFileUrl: (id: string) => string
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
key: 'file'
|
||||
id: Ref<PlatformBlob>
|
||||
}
|
||||
|
||||
export class FrontFileUploader implements FileUploader {
|
||||
constructor (
|
||||
private readonly frontUrl: string,
|
||||
@@ -41,31 +35,32 @@ export class FrontFileUploader implements FileUploader {
|
||||
this.getFileUrl = this.getFileUrl.bind(this)
|
||||
}
|
||||
|
||||
public async uploadFile (id: Ref<Doc>, name: string, file: File, contentType?: string): Promise<Response> {
|
||||
public async uploadFile (name: string, file: Blob): Promise<Ref<PlatformBlob>> {
|
||||
const form = new FormData()
|
||||
form.append('file', file, name)
|
||||
form.append('type', contentType ?? file.type)
|
||||
form.append('size', file.size.toString())
|
||||
form.append('name', file.name)
|
||||
form.append('id', id)
|
||||
form.append('data', new Blob([file]))
|
||||
|
||||
return await fetch(concatLink(this.frontUrl, '/files'), {
|
||||
const res = await fetch(concatLink(this.frontUrl, '/files'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.token
|
||||
},
|
||||
body: form
|
||||
})
|
||||
|
||||
if (res.ok && res.status === 200) {
|
||||
return name as Ref<PlatformBlob>
|
||||
}
|
||||
|
||||
throw new Error('Failed to upload file')
|
||||
}
|
||||
|
||||
public getFileUrl (id: string): string {
|
||||
return concatLink(this.frontUrl, `/files/${this.workspaceId}/${id}?file=${id}&workspace=${this.workspaceId}`)
|
||||
}
|
||||
|
||||
public async uploadCollaborativeDoc (id: Ref<Doc>, collabId: CollaborativeDoc, data: Buffer): Promise<Response> {
|
||||
const file = new File([data], collabId)
|
||||
const { documentId } = collaborativeDocParse(collabId)
|
||||
return await this.uploadFile(id, documentId, file, 'application/ydoc')
|
||||
public async uploadCollaborativeDoc (collabId: CollaborativeDoc, data: Buffer): Promise<Ref<PlatformBlob>> {
|
||||
const blobId = makeCollabJsonId(collabId)
|
||||
const blob = new Blob([data], { type: 'application/json' })
|
||||
return await this.uploadFile(blobId, blob)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,21 +12,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import {
|
||||
type AttachedData,
|
||||
type Blob,
|
||||
type Data,
|
||||
type Doc,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
type Ref,
|
||||
type TxOperations
|
||||
type TxOperations,
|
||||
generateId,
|
||||
makeCollabId
|
||||
} from '@hcengineering/core'
|
||||
import document, { type Document, getFirstRank, type Teamspace } from '@hcengineering/document'
|
||||
import document, { type Document, type Teamspace, getFirstRank } from '@hcengineering/document'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import {
|
||||
jsonToYDocNoSchema,
|
||||
jsonToMarkup,
|
||||
MarkupMarkType,
|
||||
type MarkupNode,
|
||||
MarkupNodeType,
|
||||
@@ -325,8 +323,6 @@ async function createDBPageWithAttachments (
|
||||
documentMetaMap?: Map<string, DocumentMetadata>
|
||||
): Promise<void> {
|
||||
const pageId = docMeta.id as Ref<Document>
|
||||
const collabId = makeCollaborativeDoc(pageId, 'content')
|
||||
|
||||
const parentId = parentMeta !== undefined ? (parentMeta.id as Ref<Document>) : document.ids.NoParent
|
||||
|
||||
const lastRank = await getFirstRank(client, space, parentId)
|
||||
@@ -334,7 +330,7 @@ async function createDBPageWithAttachments (
|
||||
|
||||
const object: Data<Document> = {
|
||||
title: docMeta.name,
|
||||
content: collabId,
|
||||
content: null,
|
||||
parent: parentId,
|
||||
attachments: 0,
|
||||
embeddings: 0,
|
||||
@@ -409,7 +405,7 @@ async function importAttachment (
|
||||
}
|
||||
|
||||
const file = new File([data], docMeta.name)
|
||||
await fileUploader.uploadFile(docMeta.id as Ref<Doc>, docMeta.id, file)
|
||||
await fileUploader.uploadFile(docMeta.id, file)
|
||||
|
||||
const attachedData: AttachedData<Attachment> = {
|
||||
file: docMeta.id as Ref<Blob>,
|
||||
@@ -444,13 +440,12 @@ async function importPageDocument (
|
||||
if (documentMetaMap !== undefined) {
|
||||
preProcessMarkdown(json, documentMetaMap, fileUploader)
|
||||
}
|
||||
const yDoc = jsonToYDocNoSchema(json, 'content')
|
||||
const buffer = yDocToBuffer(yDoc)
|
||||
const markup = jsonToMarkup(json)
|
||||
const buffer = Buffer.from(markup)
|
||||
|
||||
const id = docMeta.id as Ref<Document>
|
||||
const collabId = makeCollaborativeDoc(id, 'description')
|
||||
|
||||
await fileUploader.uploadCollaborativeDoc(id, collabId, buffer)
|
||||
const collabId = makeCollabId(document.class.Document, id, 'content')
|
||||
const blobId = await fileUploader.uploadCollaborativeDoc(collabId, buffer)
|
||||
|
||||
const parent = (parentMeta?.id as Ref<Document>) ?? document.ids.NoParent
|
||||
|
||||
@@ -459,7 +454,7 @@ async function importPageDocument (
|
||||
|
||||
const attachedData: Data<Document> = {
|
||||
title: docMeta.name,
|
||||
content: collabId,
|
||||
content: blobId,
|
||||
parent,
|
||||
attachments: 0,
|
||||
embeddings: 0,
|
||||
|
||||
@@ -277,7 +277,7 @@ module.exports = [
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
meta: {
|
||||
viewport: 'width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=1'
|
||||
viewport: 'width=device-width, initial-scale=1.0'
|
||||
}
|
||||
}),
|
||||
...(prod ? [new CompressionPlugin()] : []),
|
||||
|
||||
@@ -21,7 +21,6 @@ import core, {
|
||||
concatLink,
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
makeCollaborativeDoc,
|
||||
metricsToString,
|
||||
newMetrics,
|
||||
systemAccountEmail,
|
||||
@@ -594,7 +593,7 @@ async function generateVacancy (client: TxOperations, members: Ref<PersonAccount
|
||||
{
|
||||
name: generateId().toString(),
|
||||
number: 0,
|
||||
fullDescription: makeCollaborativeDoc(_id, 'fullDescription'),
|
||||
fullDescription: generateId(),
|
||||
type: recruit.template.DefaultVacancy,
|
||||
description: '',
|
||||
private: false,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { getAccountDB, listWorkspacesRaw } from '@hcengineering/account'
|
||||
import calendar from '@hcengineering/calendar'
|
||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||
import { loadCollaborativeDoc, saveCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import { loadCollabYdoc, saveCollabYdoc, yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import contact from '@hcengineering/contact'
|
||||
import core, {
|
||||
type ArrOf,
|
||||
@@ -56,6 +56,7 @@ import core, {
|
||||
generateId,
|
||||
getObjectValue,
|
||||
getWorkspaceId,
|
||||
makeDocCollabId,
|
||||
systemAccountEmail,
|
||||
toIdMap,
|
||||
updateAttribute
|
||||
@@ -1201,8 +1202,8 @@ async function updateId (
|
||||
const newMarkup = markup.replaceAll(doc._id, newId)
|
||||
await update(h, db, contentDoc, { [attrName]: newMarkup })
|
||||
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
|
||||
const collaborativeDoc = (contentDoc as any)[attr.name] as CollaborativeDoc
|
||||
await updateYDoc(ctx, collaborativeDoc, storage, workspaceId, contentDoc, newId, doc)
|
||||
const collabId = makeDocCollabId(contentDoc, attr.name)
|
||||
await updateYDoc(ctx, collabId, storage, workspaceId, contentDoc, newId, doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1250,7 +1251,7 @@ async function updateYDoc (
|
||||
doc: RelatedDocument
|
||||
): Promise<void> {
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(ctx, storage, workspaceId, _id)
|
||||
const ydoc = await loadCollabYdoc(ctx, storage, workspaceId, _id)
|
||||
if (ydoc === undefined) {
|
||||
ctx.error('document content not found', { document: contentDoc._id })
|
||||
return
|
||||
@@ -1264,7 +1265,7 @@ async function updateYDoc (
|
||||
})
|
||||
|
||||
if (updatedYDoc !== undefined) {
|
||||
await saveCollaborativeDoc(ctx, storage, workspaceId, _id, updatedYDoc)
|
||||
await saveCollabYdoc(ctx, storage, workspaceId, _id, updatedYDoc)
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the collaborative doc does not sem to exist yet
|
||||
|
||||
+2
-67
@@ -58,13 +58,7 @@ import serverClientPlugin, {
|
||||
listAccountWorkspaces,
|
||||
updateBackupInfo
|
||||
} from '@hcengineering/server-client'
|
||||
import {
|
||||
createBackupPipeline,
|
||||
getConfig,
|
||||
getServerPipeline,
|
||||
registerServerPlugins,
|
||||
registerStringLoaders
|
||||
} from '@hcengineering/server-pipeline'
|
||||
import { createBackupPipeline, getConfig } from '@hcengineering/server-pipeline'
|
||||
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
|
||||
import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool'
|
||||
import { createWorkspace, upgradeWorkspace } from '@hcengineering/workspace-service'
|
||||
@@ -89,8 +83,7 @@ import core, {
|
||||
type Ref,
|
||||
type Tx,
|
||||
type Version,
|
||||
type WorkspaceId,
|
||||
type WorkspaceIdWithUrl
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import { consoleModelLogger, type MigrateOperation } from '@hcengineering/model'
|
||||
import contact from '@hcengineering/model-contact'
|
||||
@@ -123,7 +116,6 @@ import {
|
||||
} from './clean'
|
||||
import { changeConfiguration } from './configuration'
|
||||
import { moveAccountDbFromMongoToPG, moveFromMongoToPG, moveWorkspaceFromMongoToPG } from './db'
|
||||
import { fixJsonMarkup, migrateMarkup } from './markup'
|
||||
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
|
||||
import { fixAccountEmails, renameAccount } from './renameAccount'
|
||||
import { moveFiles, showLostFiles } from './storage'
|
||||
@@ -1723,63 +1715,6 @@ export function devTool (
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('fix-json-markup-mongo <workspace>')
|
||||
.description('fixes double converted json markup')
|
||||
.action(async (workspace: string) => {
|
||||
const mongodbUri = getMongoDBUrl()
|
||||
await withStorage(async (adapter) => {
|
||||
const wsid = getWorkspaceId(workspace)
|
||||
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external')
|
||||
await fixJsonMarkup(toolCtx, mongodbUri, adapter, wsid, endpoint)
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('migrate-markup-mongo')
|
||||
.description('migrates collaborative markup to storage')
|
||||
.option('-w, --workspace <workspace>', 'Selected workspace only', '')
|
||||
.option('-c, --concurrency <concurrency>', 'Number of documents being processed concurrently', '10')
|
||||
.action(async (cmd: { workspace: string, concurrency: string }) => {
|
||||
const { dbUrl, txes } = prepareTools()
|
||||
const mongodbUri = getMongoDBUrl()
|
||||
await withDatabase(dbUrl, async (db) => {
|
||||
await withStorage(async (adapter) => {
|
||||
const workspaces = await listWorkspacesPure(db)
|
||||
const client = getMongoClient(mongodbUri)
|
||||
const _client = await client.getClient()
|
||||
let index = 0
|
||||
try {
|
||||
for (const workspace of workspaces) {
|
||||
if (cmd.workspace !== '' && workspace.workspace !== cmd.workspace) {
|
||||
continue
|
||||
}
|
||||
|
||||
const wsId = getWorkspaceId(workspace.workspace)
|
||||
console.log('processing workspace', workspace.workspace, index, workspaces.length)
|
||||
const wsUrl: WorkspaceIdWithUrl = {
|
||||
name: workspace.workspace,
|
||||
workspaceName: workspace.workspaceName ?? '',
|
||||
workspaceUrl: workspace.workspaceUrl ?? ''
|
||||
}
|
||||
|
||||
registerServerPlugins()
|
||||
registerStringLoaders()
|
||||
|
||||
const { pipeline } = await getServerPipeline(toolCtx, txes, dbUrl, wsUrl)
|
||||
|
||||
await migrateMarkup(toolCtx, adapter, wsId, _client, pipeline, parseInt(cmd.concurrency))
|
||||
|
||||
console.log('...done', workspace.workspace)
|
||||
index++
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('remove-duplicates-ids-mongo <workspaces>')
|
||||
.description('remove duplicates ids for futue migration')
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import { saveCollaborativeDoc } from '@hcengineering/collaboration'
|
||||
import core, {
|
||||
type AnyAttribute,
|
||||
type Class,
|
||||
type Client as CoreClient,
|
||||
type Doc,
|
||||
type Domain,
|
||||
type Hierarchy,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type WorkspaceId,
|
||||
collaborativeDocParse,
|
||||
makeCollaborativeDoc,
|
||||
RateLimiter
|
||||
} from '@hcengineering/core'
|
||||
import { getMongoClient, getWorkspaceMongoDB } from '@hcengineering/mongo'
|
||||
import { type Pipeline, type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { connect } from '@hcengineering/server-tool'
|
||||
import { jsonToText, markupToYDoc } from '@hcengineering/text'
|
||||
import { type Db, type FindCursor, type MongoClient } from 'mongodb'
|
||||
|
||||
export async function fixJsonMarkup (
|
||||
ctx: MeasureContext,
|
||||
mongoUrl: string,
|
||||
storageAdapter: StorageAdapter,
|
||||
workspaceId: WorkspaceId,
|
||||
transactorUrl: string
|
||||
): Promise<void> {
|
||||
const connection = (await connect(transactorUrl, workspaceId, undefined, {
|
||||
mode: 'backup'
|
||||
})) as unknown as CoreClient
|
||||
const hierarchy = connection.getHierarchy()
|
||||
|
||||
const client = getMongoClient(mongoUrl)
|
||||
const _client = await client.getClient()
|
||||
const db = getWorkspaceMongoDB(_client, workspaceId)
|
||||
|
||||
try {
|
||||
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.TypeMarkup)
|
||||
})
|
||||
if (filtered.length === 0) continue
|
||||
|
||||
await processFixJsonMarkupFor(ctx, domain, _class, filtered, workspaceId, db, storageAdapter)
|
||||
}
|
||||
} finally {
|
||||
client.close()
|
||||
await connection.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function processFixJsonMarkupFor (
|
||||
ctx: MeasureContext,
|
||||
domain: Domain,
|
||||
_class: Ref<Class<Doc>>,
|
||||
attributes: AnyAttribute[],
|
||||
workspaceId: WorkspaceId,
|
||||
db: Db,
|
||||
storageAdapter: StorageAdapter
|
||||
): Promise<void> {
|
||||
const collection = db.collection<Doc>(domain)
|
||||
const docs = await collection.find({ _class }).toArray()
|
||||
for (const doc of docs) {
|
||||
const update: Record<string, any> = {}
|
||||
const remove = []
|
||||
|
||||
for (const attribute of attributes) {
|
||||
try {
|
||||
const value = (doc as any)[attribute.name]
|
||||
if (value != null) {
|
||||
let res = value
|
||||
while (true) {
|
||||
try {
|
||||
const json = JSON.parse(res)
|
||||
const text = jsonToText(json)
|
||||
JSON.parse(text)
|
||||
res = text
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (res !== value) {
|
||||
update[attribute.name] = res
|
||||
remove.push(makeCollaborativeDoc(doc._id, attribute.name))
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
try {
|
||||
await collection.updateOne({ _id: doc._id }, { $set: update })
|
||||
} catch (err) {
|
||||
console.error('failed to update document', doc._class, doc._id, err)
|
||||
}
|
||||
}
|
||||
|
||||
if (remove.length > 0) {
|
||||
try {
|
||||
await storageAdapter.remove(ctx, workspaceId, remove)
|
||||
} catch (err) {
|
||||
console.error('failed to remove objects from storage', doc._class, doc._id, remove, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateMarkup (
|
||||
ctx: MeasureContext,
|
||||
storageAdapter: StorageAdapter,
|
||||
workspaceId: WorkspaceId,
|
||||
client: MongoClient,
|
||||
pipeline: Pipeline,
|
||||
concurrency: number
|
||||
): Promise<void> {
|
||||
const hierarchy = pipeline.context.hierarchy
|
||||
|
||||
const workspaceDb = client.db(workspaceId.name)
|
||||
|
||||
const classes = hierarchy.getDescendants(core.class.Doc)
|
||||
for (const _class of classes) {
|
||||
const domain = hierarchy.findDomain(_class)
|
||||
if (domain === undefined) continue
|
||||
|
||||
const allAttributes = hierarchy.getAllAttributes(_class)
|
||||
const attributes = Array.from(allAttributes.values()).filter((attribute) => {
|
||||
return hierarchy.isDerived(attribute.type._class, 'core:class:TypeCollaborativeMarkup' as Ref<Class<Doc>>)
|
||||
})
|
||||
|
||||
if (attributes.length === 0) continue
|
||||
if (hierarchy.isMixin(_class) && attributes.every((p) => p.attributeOf !== _class)) continue
|
||||
|
||||
const collection = workspaceDb.collection(domain)
|
||||
|
||||
const filter = hierarchy.isMixin(_class) ? { [_class]: { $exists: true } } : { _class }
|
||||
const iterator = collection.find<Doc>(filter)
|
||||
|
||||
try {
|
||||
await processMigrateMarkupFor(ctx, hierarchy, storageAdapter, workspaceId, attributes, iterator, concurrency)
|
||||
} finally {
|
||||
await iterator.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processMigrateMarkupFor (
|
||||
ctx: MeasureContext,
|
||||
hierarchy: Hierarchy,
|
||||
storageAdapter: StorageAdapter,
|
||||
workspaceId: WorkspaceId,
|
||||
attributes: AnyAttribute[],
|
||||
iterator: FindCursor<Doc>,
|
||||
concurrency: number
|
||||
): Promise<void> {
|
||||
const rateLimiter = new RateLimiter(concurrency)
|
||||
|
||||
let processed = 0
|
||||
|
||||
while (true) {
|
||||
const doc = await iterator.next()
|
||||
if (doc === null) break
|
||||
|
||||
const timestamp = Date.now()
|
||||
const revisionId = `${timestamp}`
|
||||
|
||||
await rateLimiter.exec(async () => {
|
||||
for (const attribute of attributes) {
|
||||
const collaborativeDoc = makeCollaborativeDoc(doc._id, attribute.name, revisionId)
|
||||
const { documentId } = collaborativeDocParse(collaborativeDoc)
|
||||
|
||||
const value = hierarchy.isMixin(attribute.attributeOf)
|
||||
? ((doc as any)[attribute.attributeOf]?.[attribute.name] as string)
|
||||
: ((doc as any)[attribute.name] as string)
|
||||
|
||||
if (value != null && value.startsWith('{')) {
|
||||
const blob = await storageAdapter.stat(ctx, workspaceId, documentId)
|
||||
// only for documents not in storage
|
||||
if (blob === undefined) {
|
||||
try {
|
||||
const ydoc = markupToYDoc(value, attribute.name)
|
||||
await saveCollaborativeDoc(ctx, storageAdapter, workspaceId, collaborativeDoc, ydoc)
|
||||
} catch (err) {
|
||||
console.error('failed to process document', doc._class, doc._id, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
processed += 1
|
||||
|
||||
if (processed % 100 === 0) {
|
||||
await rateLimiter.waitProcessing()
|
||||
console.log('...processing', processed)
|
||||
}
|
||||
}
|
||||
|
||||
await rateLimiter.waitProcessing()
|
||||
|
||||
console.log('processed', processed)
|
||||
}
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
IndexKind,
|
||||
type Blob,
|
||||
type Class,
|
||||
type CollaborativeDoc,
|
||||
type MarkupBlobRef,
|
||||
type Domain,
|
||||
type Ref,
|
||||
type Timestamp
|
||||
@@ -175,7 +175,7 @@ export class TMember extends TAttachedDoc implements Member {
|
||||
export class TOrganization extends TContact implements Organization {
|
||||
@Prop(TypeCollaborativeDoc(), core.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(Collection(contact.class.Member), contact.string.Members)
|
||||
members!: number
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import {
|
||||
clone,
|
||||
loadCollaborativeDoc,
|
||||
saveCollaborativeDoc,
|
||||
YAbstractType,
|
||||
YXmlElement,
|
||||
YXmlText
|
||||
YXmlText,
|
||||
YAbstractType,
|
||||
yXmlElementClone,
|
||||
loadCollabYdoc,
|
||||
saveCollabYdoc
|
||||
} from '@hcengineering/collaboration'
|
||||
import {
|
||||
type ChangeControl,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type Doc,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
makeDocCollabId,
|
||||
MeasureMetricsContext,
|
||||
type Ref,
|
||||
SortingOrder,
|
||||
@@ -144,7 +144,7 @@ async function createProductChangeControlTemplate (tx: TxOperations): Promise<vo
|
||||
minor: 1,
|
||||
state: DocumentState.Effective,
|
||||
commentSequence: 0,
|
||||
content: makeCollaborativeDoc(generateId())
|
||||
content: null
|
||||
},
|
||||
ccCategory
|
||||
)
|
||||
@@ -295,7 +295,8 @@ async function migrateDocSections (client: MigrationClient): Promise<void> {
|
||||
|
||||
// Migrate sections headers + content
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(ctx, storage, client.workspaceId, document.content)
|
||||
const collabId = makeDocCollabId(document, 'content')
|
||||
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
|
||||
if (ydoc === undefined) {
|
||||
// no content, ignore
|
||||
continue
|
||||
@@ -331,13 +332,17 @@ async function migrateDocSections (client: MigrationClient): Promise<void> {
|
||||
...(sectionContent
|
||||
.toArray()
|
||||
.map((item) =>
|
||||
item instanceof YAbstractType ? (item instanceof YXmlElement ? clone(item) : item.clone()) : item
|
||||
item instanceof YAbstractType
|
||||
? item instanceof YXmlElement
|
||||
? yXmlElementClone(item)
|
||||
: item.clone()
|
||||
: item
|
||||
) as any)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
await saveCollaborativeDoc(ctx, storage, client.workspaceId, document.content, ydoc)
|
||||
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
|
||||
} catch (err) {
|
||||
ctx.error('error collaborative document content migration', { error: err, document: document.title })
|
||||
}
|
||||
|
||||
@@ -48,17 +48,17 @@ import {
|
||||
DateRangeMode,
|
||||
IndexKind,
|
||||
type Class,
|
||||
type MarkupBlobRef,
|
||||
type Doc,
|
||||
type Domain,
|
||||
type Ref,
|
||||
type Timestamp,
|
||||
type Type,
|
||||
type CollectionSize,
|
||||
type CollaborativeDoc,
|
||||
type Role,
|
||||
type TypedSpace,
|
||||
type Account,
|
||||
type RolesAssignment,
|
||||
type Domain
|
||||
type RolesAssignment
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
ArrOf,
|
||||
@@ -259,7 +259,7 @@ export class TDocument extends TDoc implements Document {
|
||||
state!: DocumentState
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), documents.string.CollaborativeDocument)
|
||||
content!: CollaborativeDoc
|
||||
content!: MarkupBlobRef | null
|
||||
|
||||
@Prop(Collection(tags.class.TagReference), documents.string.Labels)
|
||||
labels?: CollectionSize<TagReference>
|
||||
@@ -425,7 +425,7 @@ export class TDocumentSnapshot extends TAttachedDoc implements DocumentSnapshot
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), documents.string.CollaborativeDocument)
|
||||
@Hidden()
|
||||
content!: CollaborativeDoc
|
||||
content!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeDocumentState(), documents.string.Status)
|
||||
state?: DocumentState
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
type Card,
|
||||
type Class,
|
||||
type ClassifierKind,
|
||||
type CollaborativeDoc,
|
||||
type MarkupBlobRef,
|
||||
type Collection,
|
||||
type Configuration,
|
||||
type ConfigurationElement,
|
||||
@@ -121,7 +121,7 @@ export class TCard extends TDoc implements Card {
|
||||
title!: string
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), core.string.Description)
|
||||
description!: CollaborativeDoc | null
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeString(), core.string.Id)
|
||||
identifier?: string | undefined
|
||||
@@ -373,10 +373,6 @@ export class TDomainIndexConfiguration extends TDoc implements DomainIndexConfig
|
||||
@Model(core.class.TypeCollaborativeDoc, core.class.Type)
|
||||
export class TTypeCollaborativeDoc extends TType {}
|
||||
|
||||
@UX(core.string.CollaborativeDocVersion)
|
||||
@Model(core.class.TypeCollaborativeDocVersion, core.class.Type)
|
||||
export class TTypeCollaborativeDocVersion extends TType {}
|
||||
|
||||
@UX(core.string.Rank)
|
||||
@Model(core.class.TypeRank, core.class.Type)
|
||||
export class TTypeRank extends TType {}
|
||||
|
||||
@@ -58,7 +58,6 @@ import {
|
||||
TTypeBlob,
|
||||
TTypeBoolean,
|
||||
TTypeCollaborativeDoc,
|
||||
TTypeCollaborativeDocVersion,
|
||||
TTypeDate,
|
||||
TTypeFileSize,
|
||||
TTypeHyperlink,
|
||||
@@ -125,7 +124,6 @@ export function createModel (builder: Builder): void {
|
||||
TEnumOf,
|
||||
TTypeMarkup,
|
||||
TTypeCollaborativeDoc,
|
||||
TTypeCollaborativeDocVersion,
|
||||
TArrOf,
|
||||
TRefTo,
|
||||
TTypeDate,
|
||||
|
||||
+164
-23
@@ -13,19 +13,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { saveCollaborativeDoc } from '@hcengineering/collaboration'
|
||||
import { saveCollabJson, saveCollabYdoc, yDocFromBuffer } from '@hcengineering/collaboration'
|
||||
import core, {
|
||||
collaborativeDocParse,
|
||||
coreId,
|
||||
DOMAIN_MODEL_TX,
|
||||
DOMAIN_SPACE,
|
||||
DOMAIN_STATUS,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
makeDocCollabId,
|
||||
makeCollabJsonId,
|
||||
makeCollabYdocId,
|
||||
MeasureMetricsContext,
|
||||
RateLimiter,
|
||||
type AnyAttribute,
|
||||
type Blob,
|
||||
type Class,
|
||||
type Doc,
|
||||
type Domain,
|
||||
@@ -48,7 +50,6 @@ import {
|
||||
type MigrationUpgradeClient
|
||||
} from '@hcengineering/model'
|
||||
import { type StorageAdapter } from '@hcengineering/storage'
|
||||
import { markupToYDoc } from '@hcengineering/text'
|
||||
|
||||
async function migrateStatusesToModel (client: MigrationClient): Promise<void> {
|
||||
// Move statuses to model:
|
||||
@@ -176,7 +177,7 @@ async function migrateCollaborativeContentToStorage (client: MigrationClient): P
|
||||
|
||||
const iterator = await client.traverse(domain, query)
|
||||
try {
|
||||
console.log('processing', _class)
|
||||
ctx.info('processing', { _class })
|
||||
await processMigrateContentFor(ctx, domain, attributes, client, storageAdapter, iterator)
|
||||
} finally {
|
||||
await iterator.close()
|
||||
@@ -204,9 +205,6 @@ async function processMigrateContentFor (
|
||||
break
|
||||
}
|
||||
|
||||
const timestamp = Date.now()
|
||||
const revisionId = `${timestamp}`
|
||||
|
||||
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
|
||||
|
||||
for (const doc of docs) {
|
||||
@@ -214,8 +212,6 @@ async function processMigrateContentFor (
|
||||
const update: MigrateUpdate<Doc> = {}
|
||||
|
||||
for (const attribute of attributes) {
|
||||
const collaborativeDoc = makeCollaborativeDoc(doc._id, attribute.name, revisionId)
|
||||
|
||||
const value = hierarchy.isMixin(attribute.attributeOf)
|
||||
? ((doc as any)[attribute.attributeOf]?.[attribute.name] as string)
|
||||
: ((doc as any)[attribute.name] as string)
|
||||
@@ -224,22 +220,20 @@ async function processMigrateContentFor (
|
||||
? `${attribute.attributeOf}.${attribute.name}`
|
||||
: attribute.name
|
||||
|
||||
const collabId = makeDocCollabId(doc, attribute.name)
|
||||
const blobId = makeCollabJsonId(collabId)
|
||||
|
||||
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) {
|
||||
try {
|
||||
const ydoc = markupToYDoc(value, attribute.name)
|
||||
await saveCollaborativeDoc(ctx, storageAdapter, client.workspaceId, collaborativeDoc, ydoc)
|
||||
} catch (err) {
|
||||
console.error('failed to process document', doc._class, doc._id, err)
|
||||
}
|
||||
try {
|
||||
const buffer = Buffer.from(value)
|
||||
await storageAdapter.put(ctx, client.workspaceId, blobId, buffer, 'application/json', buffer.length)
|
||||
} catch (err) {
|
||||
ctx.error('failed to process document', { _class: doc._class, _id: doc._id, err })
|
||||
}
|
||||
|
||||
update[attributeName] = collaborativeDoc
|
||||
update[attributeName] = blobId
|
||||
} else if (value == null || value === '') {
|
||||
update[attributeName] = collaborativeDoc
|
||||
update[attributeName] = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,10 +250,153 @@ async function processMigrateContentFor (
|
||||
}
|
||||
|
||||
processed += docs.length
|
||||
console.log('...processed', processed)
|
||||
ctx.info('...processed', { count: processed })
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateCollaborativeDocsToJson (client: MigrationClient): Promise<void> {
|
||||
const ctx = new MeasureMetricsContext('migrateCollaborativeDocsToJson', {})
|
||||
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 allAttributes = hierarchy.getAllAttributes(_class)
|
||||
const attributes = Array.from(allAttributes.values()).filter((attribute) => {
|
||||
return hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)
|
||||
})
|
||||
|
||||
if (attributes.length === 0) continue
|
||||
if (hierarchy.isMixin(_class) && attributes.every((p) => p.attributeOf !== _class)) continue
|
||||
|
||||
const query = hierarchy.isMixin(_class) ? { [_class]: { $exists: true } } : { _class }
|
||||
|
||||
const iterator = await client.traverse(domain, query)
|
||||
try {
|
||||
ctx.info('processing', { _class })
|
||||
await processMigrateJsonForDomain(ctx, domain, attributes, client, storageAdapter, iterator)
|
||||
} finally {
|
||||
await iterator.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processMigrateJsonForDomain (
|
||||
ctx: MeasureContext,
|
||||
domain: Domain,
|
||||
attributes: AnyAttribute[],
|
||||
client: MigrationClient,
|
||||
storageAdapter: StorageAdapter,
|
||||
iterator: MigrationIterator<Doc>
|
||||
): Promise<void> {
|
||||
const rateLimiter = new RateLimiter(10)
|
||||
|
||||
let processed = 0
|
||||
|
||||
while (true) {
|
||||
const docs = await iterator.next(100)
|
||||
if (docs === null || docs.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
|
||||
|
||||
for (const doc of docs) {
|
||||
await rateLimiter.exec(async () => {
|
||||
const update = await processMigrateJsonForDoc(ctx, doc, attributes, client, storageAdapter)
|
||||
if (Object.keys(update).length > 0) {
|
||||
operations.push({ filter: { _id: doc._id }, update })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await rateLimiter.waitProcessing()
|
||||
|
||||
if (operations.length > 0) {
|
||||
await client.bulk(domain, operations)
|
||||
}
|
||||
|
||||
processed += docs.length
|
||||
ctx.info('...processed', { count: processed })
|
||||
}
|
||||
}
|
||||
|
||||
async function processMigrateJsonForDoc (
|
||||
ctx: MeasureContext,
|
||||
doc: Doc,
|
||||
attributes: AnyAttribute[],
|
||||
client: MigrationClient,
|
||||
storageAdapter: StorageAdapter
|
||||
): Promise<MigrateUpdate<Doc>> {
|
||||
const { hierarchy, workspaceId } = client
|
||||
|
||||
const update: MigrateUpdate<Doc> = {}
|
||||
|
||||
for (const attribute of attributes) {
|
||||
const value = hierarchy.isMixin(attribute.attributeOf)
|
||||
? ((doc as any)[attribute.attributeOf]?.[attribute.name] as string)
|
||||
: ((doc as any)[attribute.name] as string)
|
||||
|
||||
if (value == null || value === '') {
|
||||
continue
|
||||
}
|
||||
|
||||
const attributeName = hierarchy.isMixin(attribute.attributeOf)
|
||||
? `${attribute.attributeOf}.${attribute.name}`
|
||||
: attribute.name
|
||||
|
||||
// Name of existing ydoc document
|
||||
// original value here looks like '65b7f82f4d422b89d4cbdd6f:HEAD:0'
|
||||
// where the first part is the blob id
|
||||
const currentYdocId = value.split(':')[0] as Ref<Blob>
|
||||
const collabId = makeDocCollabId(doc, attribute.name)
|
||||
|
||||
if (value.startsWith('{')) {
|
||||
// For some reason we have documents that are already markups
|
||||
const jsonId = await saveCollabJson(ctx, storageAdapter, workspaceId, collabId, value)
|
||||
update[attributeName] = jsonId
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = await storageAdapter.stat(ctx, workspaceId, currentYdocId)
|
||||
if (stat !== undefined) {
|
||||
if (stat.contentType.includes('application/ydoc')) {
|
||||
const buffer = await storageAdapter.read(ctx, workspaceId, currentYdocId)
|
||||
const ydoc = yDocFromBuffer(Buffer.concat(buffer as any))
|
||||
|
||||
// If document id has changed, save it with new name to ensure we will be able to load it later
|
||||
const ydocId = makeCollabYdocId(collabId)
|
||||
if (ydocId !== currentYdocId) {
|
||||
ctx.info('saving collaborative doc with new name', { collabId, ydocId, currentYdocId })
|
||||
await saveCollabYdoc(ctx, storageAdapter, workspaceId, collabId, ydoc)
|
||||
// do not bother with deletion so we can restore content is something goes wrong
|
||||
// await storageAdapter.remove(ctx, client.workspaceId, [currentYdocId])
|
||||
}
|
||||
|
||||
// Save document as JSON and save blob Id
|
||||
const jsonId = await saveCollabJson(ctx, storageAdapter, workspaceId, collabId, ydoc)
|
||||
update[attributeName] = jsonId
|
||||
} else {
|
||||
// it is not ydoc, do nothing
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// document is empty, unset
|
||||
const unset = update.$unset ?? {}
|
||||
update.$unset = { ...unset, [attribute.name]: 1 }
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.warn('failed to process collaborative doc', { workspaceId, collabId, currentYdocId, err })
|
||||
}
|
||||
}
|
||||
|
||||
return update
|
||||
}
|
||||
|
||||
export const coreOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, coreId, [
|
||||
@@ -351,6 +488,10 @@ export const coreOperation: MigrateOperation = {
|
||||
DOMAIN_MODEL_TX
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'collaborative-docs-to-json',
|
||||
func: migrateCollaborativeDocsToJson
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/model-activity": "^0.6.0",
|
||||
"@hcengineering/model-chunter": "^0.6.0",
|
||||
"@hcengineering/model-workbench": "^0.6.1",
|
||||
"@hcengineering/model-attachment": "^0.6.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright © 2022, 2023 Hardcore Engineering Inc.
|
||||
// Copyright © 2022, 2023, 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import type { Class, CollaborativeDoc, CollectionSize, Domain, Rank, Role, RolesAssignment } from '@hcengineering/core'
|
||||
import type { Class, CollectionSize, MarkupBlobRef, Domain, Rank, Role, RolesAssignment } from '@hcengineering/core'
|
||||
import { Account, AccountRole, IndexKind, Ref } from '@hcengineering/core'
|
||||
import {
|
||||
type Document,
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
Mixin,
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
TypeCollaborativeDoc,
|
||||
TypeCollaborativeDocVersion,
|
||||
TypeNumber,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
@@ -77,7 +77,7 @@ export class TDocument extends TDoc implements Document, Todoable {
|
||||
title!: string
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), document.string.Document)
|
||||
content!: CollaborativeDoc
|
||||
content!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeRef(document.class.Document), document.string.ParentDocument)
|
||||
parent!: Ref<Document>
|
||||
@@ -137,8 +137,9 @@ export class TDocumentSnapshot extends TDoc implements DocumentSnapshot {
|
||||
@Index(IndexKind.FullText)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeCollaborativeDocVersion(), document.string.Document)
|
||||
content!: CollaborativeDoc
|
||||
@Prop(TypeCollaborativeDoc(), document.string.Document)
|
||||
@ReadOnly()
|
||||
content!: MarkupBlobRef
|
||||
|
||||
@Prop(TypeRef(document.class.Document), document.string.ParentDocument)
|
||||
parent!: Ref<Document>
|
||||
@@ -312,6 +313,10 @@ function defineDocument (builder: Builder): void {
|
||||
component: document.component.DocumentIcon
|
||||
})
|
||||
|
||||
builder.mixin(document.class.Document, core.class.Class, view.mixin.AttributeEditor, {
|
||||
inlineEditor: document.component.DocumentInlineEditor
|
||||
})
|
||||
|
||||
// Actions
|
||||
|
||||
createAction(builder, {
|
||||
|
||||
@@ -13,8 +13,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DOMAIN_MODEL_TX, MeasureMetricsContext, SortingOrder, type CollaborativeDoc } from '@hcengineering/core'
|
||||
import { type Document, type DocumentSnapshot, type Teamspace } from '@hcengineering/document'
|
||||
import {
|
||||
type Class,
|
||||
type CollaborativeDoc,
|
||||
type Doc,
|
||||
type Ref,
|
||||
DOMAIN_TX,
|
||||
DOMAIN_MODEL_TX,
|
||||
MeasureMetricsContext,
|
||||
SortingOrder,
|
||||
makeDocCollabId
|
||||
} from '@hcengineering/core'
|
||||
import { type DocumentSnapshot, type Document, type Teamspace } from '@hcengineering/document'
|
||||
import {
|
||||
migrateSpaceRanks,
|
||||
tryMigrate,
|
||||
@@ -24,11 +34,13 @@ import {
|
||||
type MigrationDocumentQuery,
|
||||
type MigrationUpgradeClient
|
||||
} from '@hcengineering/model'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
import { DOMAIN_NOTIFICATION } from '@hcengineering/notification'
|
||||
import { type Asset } from '@hcengineering/platform'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
|
||||
import { loadCollaborativeDoc, saveCollaborativeDoc, yDocCopyXmlField } from '@hcengineering/collaboration'
|
||||
import { loadCollabYdoc, saveCollabYdoc, yDocCopyXmlField } from '@hcengineering/collaboration'
|
||||
import document, { documentId, DOMAIN_DOCUMENT } from './index'
|
||||
|
||||
async function migrateDocumentIcons (client: MigrationClient): Promise<void> {
|
||||
@@ -197,23 +209,23 @@ async function renameFieldsRevert (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
)
|
||||
|
||||
if (document.description.includes('%description:')) {
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(ctx, storage, client.workspaceId, document.description)
|
||||
if (ydoc === undefined) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const collabId = makeDocCollabId(document, 'content')
|
||||
|
||||
if (!ydoc.share.has('description') || ydoc.share.has('content')) {
|
||||
continue
|
||||
}
|
||||
|
||||
yDocCopyXmlField(ydoc, 'description', 'content')
|
||||
|
||||
await saveCollaborativeDoc(ctx, storage, client.workspaceId, document.description, ydoc)
|
||||
} catch (err) {
|
||||
ctx.error('error document content migration', { error: err, document: document.title })
|
||||
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
|
||||
if (ydoc === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!ydoc.share.has('description') || ydoc.share.has('content')) {
|
||||
continue
|
||||
}
|
||||
|
||||
yDocCopyXmlField(ydoc, 'description', 'content')
|
||||
|
||||
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
|
||||
} catch (err) {
|
||||
ctx.error('error document content migration', { error: err, document: document.title })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +258,9 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
|
||||
|
||||
for (const document of documents) {
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(ctx, storage, client.workspaceId, document.content)
|
||||
const collabId = makeDocCollabId(document, 'content')
|
||||
|
||||
const ydoc = await loadCollabYdoc(ctx, storage, client.workspaceId, collabId)
|
||||
if (ydoc === undefined) {
|
||||
ctx.error('document content not found', { document: document.title })
|
||||
continue
|
||||
@@ -260,7 +274,7 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
|
||||
if (ydoc.share.has('')) {
|
||||
yDocCopyXmlField(ydoc, '', 'content')
|
||||
if (ydoc.share.has('content')) {
|
||||
await saveCollaborativeDoc(ctx, storage, client.workspaceId, document.content, ydoc)
|
||||
await saveCollabYdoc(ctx, storage, client.workspaceId, collabId, ydoc)
|
||||
} else {
|
||||
ctx.error('document content still not found', { document: document.title })
|
||||
}
|
||||
@@ -281,6 +295,23 @@ async function migrateRanks (client: MigrationClient): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOldClasses (client: MigrationClient): Promise<void> {
|
||||
const classes = [
|
||||
'document:class:DocumentContent',
|
||||
'document:class:DocumentSnapshot',
|
||||
'document:class:DocumentVersion',
|
||||
'document:class:DocumentRequest'
|
||||
] as Ref<Class<Doc>>[]
|
||||
|
||||
for (const _class of classes) {
|
||||
await client.deleteMany(DOMAIN_DOCUMENT, { _class })
|
||||
await client.deleteMany(DOMAIN_ACTIVITY, { attachedToClass: _class })
|
||||
await client.deleteMany(DOMAIN_ACTIVITY, { objectClass: _class })
|
||||
await client.deleteMany(DOMAIN_NOTIFICATION, { attachedToClass: _class })
|
||||
await client.deleteMany(DOMAIN_TX, { objectClass: _class })
|
||||
await client.deleteMany(DOMAIN_TX, { 'tx.objectClass': _class })
|
||||
}
|
||||
}
|
||||
export const documentOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, documentId, [
|
||||
@@ -321,6 +352,10 @@ export const documentOperation: MigrateOperation = {
|
||||
{
|
||||
state: 'migrateRanks',
|
||||
func: migrateRanks
|
||||
},
|
||||
{
|
||||
state: 'removeOldClasses',
|
||||
func: removeOldClasses
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@ export default mergeIds(documentId, document, {
|
||||
component: {
|
||||
Documents: '' as AnyComponent,
|
||||
DocumentPresenter: '' as AnyComponent,
|
||||
DocumentInlineEditor: '' as AnyComponent,
|
||||
NotificationDocumentPresenter: '' as AnyComponent,
|
||||
TeamspaceSpacePresenter: '' as AnyComponent,
|
||||
Move: '' as AnyComponent,
|
||||
|
||||
@@ -13,14 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
AccountRole,
|
||||
DOMAIN_MODEL_TX,
|
||||
makeCollaborativeDoc,
|
||||
TxOperations,
|
||||
type Ref,
|
||||
type Status
|
||||
} from '@hcengineering/core'
|
||||
import { AccountRole, DOMAIN_MODEL_TX, TxOperations, type Ref, type Status } from '@hcengineering/core'
|
||||
import { leadId, type Lead } from '@hcengineering/lead'
|
||||
import {
|
||||
tryMigrate,
|
||||
@@ -213,44 +206,6 @@ export const leadOperation: MigrateOperation = {
|
||||
}
|
||||
}
|
||||
)
|
||||
const it = await client.traverse(DOMAIN_CONTACT, {
|
||||
_class: contact.class.Organization,
|
||||
description: { $exists: false }
|
||||
})
|
||||
while (true) {
|
||||
const docs = await it.next(50)
|
||||
if (docs == null || docs.length === 0) {
|
||||
break
|
||||
}
|
||||
await client.bulk(
|
||||
DOMAIN_CONTACT,
|
||||
docs.map((doc) => ({
|
||||
filter: { _id: doc._id },
|
||||
update: { $set: { description: makeCollaborativeDoc(doc._id, 'description') } }
|
||||
}))
|
||||
)
|
||||
}
|
||||
const it2 = await client.traverse(DOMAIN_CONTACT, { [lead.mixin.Customer + '.customerDescription']: null })
|
||||
while (true) {
|
||||
const docs = await it2.next(50)
|
||||
if (docs == null || docs.length === 0) {
|
||||
break
|
||||
}
|
||||
await client.bulk(
|
||||
DOMAIN_CONTACT,
|
||||
docs.map((doc) => ({
|
||||
filter: { _id: doc._id },
|
||||
update: {
|
||||
$set: {
|
||||
[lead.mixin.Customer + '.customerDescription']: makeCollaborativeDoc(
|
||||
docs[0]._id,
|
||||
'customerDescription'
|
||||
)
|
||||
}
|
||||
}
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { Employee } from '@hcengineering/contact'
|
||||
import {
|
||||
Account,
|
||||
IndexKind,
|
||||
type CollaborativeDoc,
|
||||
type MarkupBlobRef,
|
||||
type Role,
|
||||
type RolesAssignment,
|
||||
type Ref,
|
||||
@@ -97,7 +97,7 @@ export class TCustomer extends TContact implements Customer {
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), lead.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
customerDescription!: CollaborativeDoc
|
||||
customerDescription!: MarkupBlobRef | null
|
||||
}
|
||||
|
||||
@Mixin(lead.mixin.DefaultFunnelTypeData, lead.class.Funnel)
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
import contact, { type Employee, type Person } from '@hcengineering/contact'
|
||||
import {
|
||||
AccountRole,
|
||||
type CollaborativeDoc,
|
||||
type CollectionSize,
|
||||
DateRangeMode,
|
||||
type Doc,
|
||||
type Domain,
|
||||
DOMAIN_TRANSIENT,
|
||||
IndexKind,
|
||||
type MarkupBlobRef,
|
||||
type Ref,
|
||||
type Timestamp
|
||||
type Timestamp,
|
||||
DOMAIN_TRANSIENT,
|
||||
DateRangeMode,
|
||||
IndexKind
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
type DevicesPreference,
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
type Builder,
|
||||
Collection,
|
||||
Collection as PropCollection,
|
||||
Hidden,
|
||||
Index,
|
||||
Mixin,
|
||||
Model,
|
||||
@@ -91,7 +90,7 @@ export class TRoom extends TDoc implements Room {
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), core.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
type!: RoomType
|
||||
|
||||
@@ -203,16 +202,13 @@ export class TMeetingMinutes extends TAttachedDoc implements MeetingMinutes, Tod
|
||||
@ReadOnly()
|
||||
declare attachedTo: Ref<Doc>
|
||||
|
||||
@Hidden()
|
||||
sid!: string
|
||||
|
||||
@Prop(TypeString(), view.string.Title)
|
||||
@Index(IndexKind.FullText)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), core.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeAny(love.component.MeetingMinutesStatusPresenter, love.string.Status), love.string.Status, {
|
||||
editor: love.component.MeetingMinutesStatusPresenter
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import { type Space, TxOperations, type Ref, makeCollaborativeDoc } from '@hcengineering/core'
|
||||
import { type Space, TxOperations, type Ref } from '@hcengineering/core'
|
||||
import drive from '@hcengineering/drive'
|
||||
import {
|
||||
MeetingStatus,
|
||||
@@ -23,8 +23,7 @@ import {
|
||||
createDefaultRooms,
|
||||
isOffice,
|
||||
loveId,
|
||||
type Floor,
|
||||
type Room
|
||||
type Floor
|
||||
} from '@hcengineering/love'
|
||||
import {
|
||||
createDefaultSpace,
|
||||
@@ -90,7 +89,7 @@ async function createReception (client: MigrationUpgradeClient): Promise<void> {
|
||||
language: 'en',
|
||||
startWithTranscription: false,
|
||||
startWithRecording: false,
|
||||
description: makeCollaborativeDoc(love.ids.Reception, 'description')
|
||||
description: null
|
||||
},
|
||||
love.ids.Reception
|
||||
)
|
||||
@@ -156,18 +155,6 @@ export const loveOperation: MigrateOperation = {
|
||||
await client.move(DOMAIN_LOVE, { _class: love.class.MeetingMinutes }, DOMAIN_MEETING_MINUTES)
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'create-description-collaborative',
|
||||
func: async (client) => {
|
||||
const rooms = await client.find<Room>(DOMAIN_LOVE, { _class: { $in: [love.class.Room, love.class.Office] } })
|
||||
for (const room of rooms) {
|
||||
const description = room.description
|
||||
if (description == null) {
|
||||
await client.update(DOMAIN_LOVE, room, { description: makeCollaborativeDoc(room._id, 'description') })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'default-meeting-minutes-status',
|
||||
func: async (client) => {
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { Employee, Organization } from '@hcengineering/contact'
|
||||
import {
|
||||
Account,
|
||||
IndexKind,
|
||||
type CollaborativeDoc,
|
||||
type Collection,
|
||||
type MarkupBlobRef,
|
||||
type Domain,
|
||||
type Markup,
|
||||
type Ref,
|
||||
@@ -68,7 +68,7 @@ import recruit from './plugin'
|
||||
export class TVacancy extends TProject implements Vacancy {
|
||||
@Prop(TypeCollaborativeDoc(), recruit.string.FullDescription)
|
||||
@Index(IndexKind.FullText)
|
||||
fullDescription!: CollaborativeDoc
|
||||
fullDescription!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeCollection(attachment.class.Attachment), attachment.string.Attachments, {
|
||||
shortLabel: attachment.string.Files
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import core from '@hcengineering/model-core'
|
||||
|
||||
import { TDefaultProjectTypeData } from './types'
|
||||
import testManagement from './plugin'
|
||||
|
||||
export function defineDefaultSpace (builder: Builder): void {
|
||||
defineDefaultProject(builder)
|
||||
}
|
||||
|
||||
function defineDefaultProject (builder: Builder): void {
|
||||
builder.createModel(TDefaultProjectTypeData)
|
||||
|
||||
builder.createDoc(
|
||||
core.class.SpaceTypeDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
name: testManagement.string.TestProject,
|
||||
description: testManagement.string.FullDescription,
|
||||
icon: testManagement.icon.TestProject,
|
||||
baseClass: testManagement.class.TestProject,
|
||||
availablePermissions: [
|
||||
core.permission.UpdateSpace,
|
||||
core.permission.ArchiveSpace,
|
||||
core.permission.ForbidDeleteObject
|
||||
]
|
||||
},
|
||||
testManagement.descriptors.ProjectType
|
||||
)
|
||||
|
||||
builder.createDoc(core.class.SpaceType, core.space.Model, {
|
||||
name: 'Default project type',
|
||||
descriptor: testManagement.descriptors.ProjectType,
|
||||
roles: 0,
|
||||
targetClass: testManagement.mixin.DefaultProjectTypeData
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
import activity from '@hcengineering/activity'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
import { SortingOrder, type FindOptions } from '@hcengineering/core'
|
||||
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import view, { createAction } from '@hcengineering/model-view'
|
||||
@@ -25,7 +25,7 @@ import print from '@hcengineering/model-print'
|
||||
import tracker from '@hcengineering/model-tracker'
|
||||
import { type ViewOptionsModel } from '@hcengineering/view'
|
||||
|
||||
import { testManagementId } from '@hcengineering/test-management'
|
||||
import { testManagementId, type TestResult } from '@hcengineering/test-management'
|
||||
|
||||
import {
|
||||
DOMAIN_TEST_MANAGEMENT,
|
||||
@@ -37,8 +37,8 @@ import {
|
||||
TTestCase,
|
||||
TDefaultProjectTypeData,
|
||||
TTestRun,
|
||||
TTypeTestRunResult,
|
||||
TTestRunItem
|
||||
TTypeTestRunStatus,
|
||||
TTestResult
|
||||
} from './types'
|
||||
|
||||
import testManagement from './plugin'
|
||||
@@ -55,7 +55,6 @@ function defineApplication (builder: Builder): void {
|
||||
icon: testManagement.icon.TestManagementApplication,
|
||||
alias: testManagementId,
|
||||
hidden: false,
|
||||
locationResolver: testManagement.resolver.Location,
|
||||
navigatorModel: {
|
||||
spaces: [
|
||||
{
|
||||
@@ -74,9 +73,7 @@ function defineApplication (builder: Builder): void {
|
||||
componentProps: {
|
||||
_class: testManagement.class.TestCase,
|
||||
icon: testManagement.icon.TestLibrary,
|
||||
label: testManagement.string.TestLibrary,
|
||||
createLabel: testManagement.string.CreateTestCase,
|
||||
createComponent: testManagement.component.CreateTestCase
|
||||
label: testManagement.string.TestLibrary
|
||||
},
|
||||
navigationModel: {
|
||||
navigationComponent: view.component.FoldersBrowser,
|
||||
@@ -85,6 +82,7 @@ function defineApplication (builder: Builder): void {
|
||||
mainComponentLabel: testManagement.string.TestCases,
|
||||
mainComponentIcon: testManagement.icon.TestCases,
|
||||
createComponent: testManagement.component.CreateTestSuite,
|
||||
mainHeaderComponent: testManagement.component.RunButton,
|
||||
navigationComponentProps: {
|
||||
_class: testManagement.class.TestSuite,
|
||||
icon: testManagement.icon.TestSuites,
|
||||
@@ -100,21 +98,34 @@ function defineApplication (builder: Builder): void {
|
||||
},
|
||||
syncWithLocationQuery: true
|
||||
}
|
||||
}
|
||||
/* TODO: UBERF-8584
|
||||
},
|
||||
{
|
||||
id: opt.testRunsId,
|
||||
id: 'testRuns',
|
||||
label: testManagement.string.TestRuns,
|
||||
icon: testManagement.icon.TestRuns,
|
||||
component: workbench.component.SpecialView,
|
||||
componentProps: {
|
||||
_class: testManagement.class.TestRun,
|
||||
_class: testManagement.class.TestResult,
|
||||
icon: testManagement.icon.TestRuns,
|
||||
title: testManagement.string.TestRuns,
|
||||
createLabel: testManagement.string.NewTestRun,
|
||||
createComponent: testManagement.component.CreateTestRun
|
||||
label: testManagement.string.TestRuns
|
||||
},
|
||||
navigationModel: {
|
||||
navigationComponent: view.component.FoldersBrowser,
|
||||
navigationComponentLabel: testManagement.string.TestRun,
|
||||
navigationComponentIcon: testManagement.icon.TestRuns,
|
||||
mainComponentLabel: testManagement.string.TestResults,
|
||||
mainComponentIcon: testManagement.icon.TestResult,
|
||||
navigationComponentProps: {
|
||||
_class: testManagement.class.TestRun,
|
||||
icon: testManagement.icon.TestRuns,
|
||||
title: testManagement.string.TestSuites,
|
||||
titleKey: 'name',
|
||||
getFolderLink: testManagement.function.GetTestRunLink,
|
||||
plainList: true
|
||||
},
|
||||
syncWithLocationQuery: true
|
||||
}
|
||||
} */
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -135,8 +146,8 @@ export function createModel (builder: Builder): void {
|
||||
TTestCase,
|
||||
TDefaultProjectTypeData,
|
||||
TTestRun,
|
||||
TTestRunItem,
|
||||
TTypeTestRunResult
|
||||
TTypeTestRunStatus,
|
||||
TTestResult
|
||||
)
|
||||
|
||||
builder.mixin(testManagement.class.TestProject, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
@@ -204,7 +215,7 @@ function defineSpaceType (builder: Builder): void {
|
||||
core.class.SpaceType,
|
||||
core.space.Model,
|
||||
{
|
||||
name: 'Default project type',
|
||||
name: 'Default Test Management',
|
||||
descriptor: testManagement.descriptors.ProjectType,
|
||||
roles: 0,
|
||||
targetClass: testManagement.mixin.DefaultProjectTypeData
|
||||
@@ -250,13 +261,7 @@ function defineTestSuite (builder: Builder): void {
|
||||
// Actions
|
||||
|
||||
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [
|
||||
view.action.Open,
|
||||
view.action.OpenInNewTab,
|
||||
print.action.Print,
|
||||
tracker.action.EditRelatedTargets,
|
||||
tracker.action.NewRelatedIssue
|
||||
]
|
||||
actions: [print.action.Print, tracker.action.EditRelatedTargets]
|
||||
})
|
||||
|
||||
createAction(
|
||||
@@ -276,6 +281,24 @@ function defineTestSuite (builder: Builder): void {
|
||||
},
|
||||
testManagement.action.CreateChildTestSuite
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: testManagement.actionImpl.RunSelectedTests,
|
||||
label: testManagement.string.RunTestCases,
|
||||
icon: testManagement.icon.Run,
|
||||
category: testManagement.category.TestCase,
|
||||
input: 'selection',
|
||||
target: testManagement.class.TestCase,
|
||||
context: {
|
||||
mode: ['context'],
|
||||
application: testManagement.app.TestManagement,
|
||||
group: 'create'
|
||||
}
|
||||
},
|
||||
testManagement.action.RunSelectedTests
|
||||
)
|
||||
}
|
||||
|
||||
function defineTestCase (builder: Builder): void {
|
||||
@@ -306,6 +329,11 @@ function defineTestCase (builder: Builder): void {
|
||||
presenter: testManagement.component.TestSuiteRefPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['priority', 'status'],
|
||||
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
@@ -350,11 +378,14 @@ function defineTestCase (builder: Builder): void {
|
||||
hiddenKeys: ['title']
|
||||
},
|
||||
config: [
|
||||
{ key: '', displayProps: { fixed: 'left', key: 'lead' } },
|
||||
{ key: '', displayProps: { fixed: 'left' } },
|
||||
{
|
||||
key: 'status',
|
||||
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||
props: { kind: 'list', size: 'small', shouldShowName: false },
|
||||
displayProps: { key: 'status', fixed: 'left' }
|
||||
},
|
||||
{ key: '', displayProps: { fixed: 'left', key: 'lead' } },
|
||||
{ key: '', displayProps: { grow: true } },
|
||||
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } },
|
||||
{
|
||||
key: 'assignee',
|
||||
@@ -366,21 +397,6 @@ function defineTestCase (builder: Builder): void {
|
||||
},
|
||||
testManagement.viewlet.ListTestCase
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: testManagement.class.TestCase,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: ['', 'assignee', 'modifiedOn'],
|
||||
configOptions: {
|
||||
sortable: true
|
||||
},
|
||||
variant: 'short'
|
||||
},
|
||||
testManagement.viewlet.SuiteTestCases
|
||||
)
|
||||
}
|
||||
|
||||
function defineTestRun (builder: Builder): void {
|
||||
@@ -391,10 +407,6 @@ function defineTestRun (builder: Builder): void {
|
||||
components: { input: { component: chunter.component.ChatMessageInput } }
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectEditor, {
|
||||
editor: testManagement.component.EditTestRun
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: testManagement.component.EditTestRun
|
||||
})
|
||||
@@ -403,18 +415,111 @@ function defineTestRun (builder: Builder): void {
|
||||
presenter: testManagement.component.TestRunPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectIcon, {
|
||||
component: testManagement.component.TestResultStatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: testManagement.component.TestResultPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, activity.mixin.ActivityDoc, {})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: testManagement.class.TestResult,
|
||||
components: { input: { component: chunter.component.ChatMessageInput } }
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectEditor, {
|
||||
editor: testManagement.component.EditTestResult
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectEditorHeader, {
|
||||
editor: testManagement.component.TestResultHeader
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPanel, {
|
||||
component: testManagement.component.EditTestResult
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPanelFooter, {
|
||||
editor: testManagement.component.TestResultFooter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['assignee', 'status', 'testSuite'],
|
||||
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
|
||||
})
|
||||
|
||||
const viewOptions: ViewOptionsModel = {
|
||||
groupBy: ['testSuite'],
|
||||
orderBy: [
|
||||
['status', SortingOrder.Ascending],
|
||||
['modifiedOn', SortingOrder.Descending],
|
||||
['createdOn', SortingOrder.Descending]
|
||||
],
|
||||
other: [
|
||||
{
|
||||
key: 'shouldShowAll',
|
||||
type: 'toggle',
|
||||
defaultValue: false,
|
||||
actionTarget: 'category',
|
||||
action: view.function.ShowEmptyGroups,
|
||||
label: view.string.ShowEmptyGroups
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: testManagement.class.TestRun,
|
||||
attachTo: testManagement.class.TestResult,
|
||||
descriptor: view.viewlet.List,
|
||||
configOptions: {
|
||||
strict: true,
|
||||
hiddenKeys: ['title', 'status', 'modifiedOn']
|
||||
},
|
||||
config: [
|
||||
{ key: '', displayProps: { fixed: 'left' } },
|
||||
{
|
||||
key: 'status',
|
||||
props: { kind: 'list', size: 'small', shouldShowName: false }
|
||||
},
|
||||
{
|
||||
key: 'assignee',
|
||||
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' },
|
||||
displayProps: { key: 'assignee', fixed: 'right' }
|
||||
}
|
||||
],
|
||||
viewOptions,
|
||||
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||
options: {
|
||||
lookup: {
|
||||
testCase: testManagement.class.TestCase
|
||||
}
|
||||
} as FindOptions<TestResult>
|
||||
},
|
||||
testManagement.viewlet.TestResultList
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
attachTo: testManagement.class.TestResult,
|
||||
descriptor: view.viewlet.Table,
|
||||
config: [''],
|
||||
config: ['', 'testSuite', 'status', 'assignee'],
|
||||
configOptions: {
|
||||
strict: true
|
||||
}
|
||||
},
|
||||
options: {
|
||||
lookup: {
|
||||
testCase: testManagement.class.TestCase
|
||||
}
|
||||
} as FindOptions<TestResult>
|
||||
},
|
||||
testManagement.viewlet.TableTestRun
|
||||
testManagement.viewlet.TableTestResult
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,15 @@
|
||||
|
||||
import { testManagementId } from '@hcengineering/test-management'
|
||||
import testManganement from '@hcengineering/test-management-resources/src/plugin'
|
||||
import type { Doc, Ref } from '@hcengineering/core'
|
||||
import type { Ref } from '@hcengineering/core'
|
||||
import { mergeIds } from '@hcengineering/platform'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import type { Action, ActionCategory, ViewAction } from '@hcengineering/view'
|
||||
import type { ActionCategory } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(testManagementId, testManganement, {
|
||||
action: {
|
||||
DeleteTestCase: '' as Ref<Action<Doc, any>>,
|
||||
CreateChildTestSuite: '' as Ref<Action>,
|
||||
EditTestSuite: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
CreateChildTestSuite: '' as ViewAction,
|
||||
EditTestSuite: '' as ViewAction
|
||||
},
|
||||
category: {
|
||||
TestSuite: '' as Ref<ActionCategory>
|
||||
TestSuite: '' as Ref<ActionCategory>,
|
||||
TestCase: '' as Ref<ActionCategory>
|
||||
},
|
||||
component: {
|
||||
CreateTestCase: '' as AnyComponent,
|
||||
@@ -44,6 +36,10 @@ export default mergeIds(testManagementId, testManganement, {
|
||||
CreateTestRun: '' as AnyComponent,
|
||||
TestRunPresenter: '' as AnyComponent,
|
||||
EditTestRun: '' as AnyComponent,
|
||||
TestSuiteRefPresenter: '' as AnyComponent
|
||||
TestSuiteRefPresenter: '' as AnyComponent,
|
||||
RunButton: '' as AnyComponent,
|
||||
TestResultPresenter: '' as AnyComponent,
|
||||
EditTestResult: '' as AnyComponent,
|
||||
TestResultFooter: '' as AnyComponent
|
||||
}
|
||||
})
|
||||
|
||||
@@ -53,4 +53,12 @@ export function definePresenters (builder: Builder): void {
|
||||
builder.mixin(testManagement.class.TypeTestCaseStatus, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: testManagement.component.TestCaseStatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TypeTestRunStatus, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: testManagement.component.TestResultStatusPresenter
|
||||
})
|
||||
|
||||
builder.mixin(testManagement.class.TypeTestRunStatus, core.class.Class, view.mixin.AttributeEditor, {
|
||||
inlineEditor: testManagement.component.TestResultStatusEditor
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import type {
|
||||
TestCaseStatus,
|
||||
TestProject,
|
||||
TestRun,
|
||||
TestRunResult,
|
||||
TestRunItem
|
||||
TestRunStatus,
|
||||
TestResult
|
||||
} from '@hcengineering/test-management'
|
||||
import { type Attachment } from '@hcengineering/attachment'
|
||||
import contact from '@hcengineering/contact'
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
type Timestamp,
|
||||
type Type,
|
||||
type CollectionSize,
|
||||
type CollaborativeDoc,
|
||||
type MarkupBlobRef,
|
||||
type Class
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
@@ -163,7 +163,7 @@ export class TTestCase extends TAttachedDoc implements TestCase {
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeTestCaseType(), testManagement.string.TestType)
|
||||
@ReadOnly()
|
||||
@@ -196,37 +196,70 @@ export class TTestRun extends TDoc implements TestRun {
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeDate(DateRangeMode.DATETIME), testManagement.string.DueDate)
|
||||
dueDate?: Timestamp
|
||||
|
||||
@Prop(Collection(testManagement.class.TestRunItem), testManagement.string.TestRunItems, {
|
||||
shortLabel: testManagement.string.TestRunItem
|
||||
@Prop(Collection(testManagement.class.TestResult), testManagement.string.TestResult, {
|
||||
shortLabel: testManagement.string.TestResult
|
||||
})
|
||||
items?: CollectionSize<TestRunItem>
|
||||
results?: CollectionSize<TestResult>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function TypeTestRunResult (): Type<TestRunResult> {
|
||||
return { _class: testManagement.class.TypeTestRunResult, label: testManagement.string.TestRunResult }
|
||||
export function TypeTestRunStatus (): Type<TestRunStatus> {
|
||||
return { _class: testManagement.class.TypeTestRunStatus, label: testManagement.string.TestRunStatus }
|
||||
}
|
||||
|
||||
@Model(testManagement.class.TypeTestRunResult, core.class.Type, DOMAIN_TEST_MANAGEMENT)
|
||||
@UX(testManagement.string.TestRunResult)
|
||||
export class TTypeTestRunResult extends TType {}
|
||||
@Model(testManagement.class.TypeTestRunStatus, core.class.Type, DOMAIN_TEST_MANAGEMENT)
|
||||
@UX(testManagement.string.TestRunStatus)
|
||||
export class TTypeTestRunStatus extends TType {}
|
||||
|
||||
@Model(testManagement.class.TestRunItem, core.class.AttachedDoc, DOMAIN_TEST_MANAGEMENT)
|
||||
@UX(testManagement.string.TestRunItem)
|
||||
export class TTestRunItem extends TAttachedDoc implements TestRunItem {
|
||||
@Prop(TypeRef(testManagement.class.TestRun), testManagement.string.TestRun)
|
||||
testRun!: Ref<TestRun>
|
||||
// TODO: Refactor to associations
|
||||
@Model(testManagement.class.TestResult, core.class.AttachedDoc, DOMAIN_TEST_MANAGEMENT)
|
||||
@UX(testManagement.string.TestResult)
|
||||
export class TTestResult extends TAttachedDoc implements TestResult {
|
||||
@Prop(TypeRef(testManagement.class.TestRun), core.string.AttachedTo)
|
||||
@Index(IndexKind.Indexed)
|
||||
declare attachedTo: Ref<TestRun>
|
||||
|
||||
@Prop(TypeRef(testManagement.class.TestRun), core.string.AttachedToClass)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare attachedToClass: Ref<Class<TestRun>>
|
||||
|
||||
@Prop(TypeRef(testManagement.class.TestProject), core.string.Space)
|
||||
@Index(IndexKind.Indexed)
|
||||
@Hidden()
|
||||
declare space: Ref<TestProject>
|
||||
|
||||
@Prop(TypeString(), core.string.Collection)
|
||||
@Hidden()
|
||||
override collection: 'results' = 'results'
|
||||
|
||||
@Prop(TypeString(), testManagement.string.TestRunName)
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeRef(testManagement.class.TestCase), testManagement.string.TestCase)
|
||||
testCase!: Ref<TestCase>
|
||||
|
||||
@Prop(TypeTestRunResult(), testManagement.string.TestRunResult)
|
||||
result?: TestRunResult
|
||||
@Prop(TypeRef(testManagement.class.TestSuite), testManagement.string.TestSuite)
|
||||
testSuite?: Ref<TestSuite>
|
||||
|
||||
@Prop(TypeTestRunStatus(), testManagement.string.TestRunStatus)
|
||||
status?: TestRunStatus
|
||||
|
||||
@Prop(TypeRef(contact.mixin.Employee), testManagement.string.TestAssignee)
|
||||
assignee?: Ref<Employee>
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: CollectionSize<Attachment>
|
||||
|
||||
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
comments?: number
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
DOMAIN_MODEL,
|
||||
DateRangeMode,
|
||||
IndexKind,
|
||||
type CollaborativeDoc,
|
||||
type MarkupBlobRef,
|
||||
type Domain,
|
||||
type Markup,
|
||||
type Ref,
|
||||
@@ -185,7 +185,7 @@ export class TIssue extends TTask implements Issue {
|
||||
|
||||
@Prop(TypeCollaborativeDoc(), tracker.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description!: CollaborativeDoc
|
||||
description!: MarkupBlobRef | null
|
||||
|
||||
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.Status, {
|
||||
_id: tracker.attribute.IssueStatus,
|
||||
|
||||
@@ -514,10 +514,6 @@ export function createModel (builder: Builder): void {
|
||||
presenter: view.component.MarkupDiffPresenter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.TypeCollaborativeDocVersion, core.class.Class, view.mixin.InlineAttributEditor, {
|
||||
editor: view.component.CollaborativeDocEditor
|
||||
})
|
||||
|
||||
classPresenter(builder, core.class.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor)
|
||||
classPresenter(
|
||||
builder,
|
||||
|
||||
@@ -128,12 +128,12 @@ class PlatformClientImpl implements PlatformClient {
|
||||
await this.connection.close()
|
||||
}
|
||||
|
||||
private async processMarkup<T>(id: Ref<Doc>, data: WithMarkup<T>): Promise<T> {
|
||||
private async processMarkup<T>(_class: Ref<Class<Doc>>, id: Ref<Doc>, data: WithMarkup<T>): Promise<T> {
|
||||
const result: any = {}
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (value instanceof MarkupContent) {
|
||||
result[key] = this.markup.uploadMarkup(id, key, value.content, value.kind)
|
||||
result[key] = this.markup.uploadMarkup(_class, id, key, value.content, value.kind)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
@@ -151,7 +151,7 @@ class PlatformClientImpl implements PlatformClient {
|
||||
id?: Ref<T>
|
||||
): Promise<Ref<T>> {
|
||||
id ??= generateId()
|
||||
const data = await this.processMarkup<Data<T>>(id, attributes)
|
||||
const data = await this.processMarkup<Data<T>>(_class, id, attributes)
|
||||
return await this.client.createDoc(_class, space, data, id)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ class PlatformClientImpl implements PlatformClient {
|
||||
operations: WithMarkup<DocumentUpdate<T>>,
|
||||
retrieve?: boolean
|
||||
): Promise<TxResult> {
|
||||
const update = await this.processMarkup<DocumentUpdate<T>>(objectId, operations)
|
||||
const update = await this.processMarkup<DocumentUpdate<T>>(_class, objectId, operations)
|
||||
return await this.client.updateDoc(_class, space, objectId, update, retrieve)
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ class PlatformClientImpl implements PlatformClient {
|
||||
id?: Ref<P>
|
||||
): Promise<Ref<P>> {
|
||||
id ??= generateId()
|
||||
const data = await this.processMarkup<AttachedData<P>>(id, attributes)
|
||||
const data = await this.processMarkup<AttachedData<P>>(_class, id, attributes)
|
||||
return await this.client.addCollection(_class, space, attachedTo, attachedToClass, collection, data, id)
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ class PlatformClientImpl implements PlatformClient {
|
||||
operations: WithMarkup<DocumentUpdate<P>>,
|
||||
retrieve?: boolean
|
||||
): Promise<Ref<T>> {
|
||||
const update = await this.processMarkup<DocumentUpdate<P>>(objectId, operations)
|
||||
const update = await this.processMarkup<DocumentUpdate<P>>(_class, objectId, operations)
|
||||
return await this.client.updateCollection(
|
||||
_class,
|
||||
space,
|
||||
@@ -229,7 +229,7 @@ class PlatformClientImpl implements PlatformClient {
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinData<D, M>>
|
||||
): Promise<TxResult> {
|
||||
const data = await this.processMarkup<MixinData<D, M>>(objectId, attributes)
|
||||
const data = await this.processMarkup<MixinData<D, M>>(objectClass, objectId, attributes)
|
||||
return await this.client.createMixin(objectId, objectClass, objectSpace, mixin, data)
|
||||
}
|
||||
|
||||
@@ -240,18 +240,30 @@ class PlatformClientImpl implements PlatformClient {
|
||||
mixin: Ref<Mixin<M>>,
|
||||
attributes: WithMarkup<MixinUpdate<D, M>>
|
||||
): Promise<TxResult> {
|
||||
const update = await this.processMarkup<MixinUpdate<D, M>>(objectId, attributes)
|
||||
const update = await this.processMarkup<MixinUpdate<D, M>>(objectClass, objectId, attributes)
|
||||
return await this.client.updateMixin(objectId, objectClass, objectSpace, mixin, update)
|
||||
}
|
||||
|
||||
// Markup
|
||||
|
||||
async fetchMarkup (objectId: Ref<Doc>, objectAttr: string, markup: MarkupRef, format: MarkupFormat): Promise<string> {
|
||||
return await this.markup.fetchMarkup(objectId, objectAttr, markup, format)
|
||||
async fetchMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: MarkupRef,
|
||||
format: MarkupFormat
|
||||
): Promise<string> {
|
||||
return await this.markup.fetchMarkup(objectClass, objectId, objectAttr, markup, format)
|
||||
}
|
||||
|
||||
async uploadMarkup (objectId: Ref<Doc>, objectAttr: string, markup: string, format: MarkupFormat): Promise<MarkupRef> {
|
||||
return await this.markup.uploadMarkup(objectId, objectAttr, markup, format)
|
||||
async uploadMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: string,
|
||||
format: MarkupFormat
|
||||
): Promise<MarkupRef> {
|
||||
return await this.markup.uploadMarkup(objectClass, objectId, objectAttr, markup, format)
|
||||
}
|
||||
|
||||
// AsyncDisposable
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Doc, Markup, type Ref, concatLink, makeCollaborativeDoc } from '@hcengineering/core'
|
||||
import { type Class, type Doc, type Markup, type Ref, concatLink, makeCollabId } from '@hcengineering/core'
|
||||
import { type CollaboratorClient, getClient } from '@hcengineering/collaborator-client'
|
||||
import { parseMessageMarkdown, jsonToMarkup, markupToHTML, markupToMarkdown, htmlToMarkup } from '@hcengineering/text'
|
||||
|
||||
@@ -45,9 +45,15 @@ class MarkupOperationsImpl implements MarkupOperations {
|
||||
this.collaborator = getClient({ name: workspace }, token, config.COLLABORATOR_URL)
|
||||
}
|
||||
|
||||
async fetchMarkup (objectId: Ref<Doc>, objectAttr: string, doc: MarkupRef, format: MarkupFormat): Promise<string> {
|
||||
const content = await this.collaborator.getContent(doc)
|
||||
const markup = content[objectAttr] ?? ''
|
||||
async fetchMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
doc: MarkupRef,
|
||||
format: MarkupFormat
|
||||
): Promise<string> {
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
const markup = await this.collaborator.getMarkup(collabId, doc)
|
||||
|
||||
switch (format) {
|
||||
case 'markup':
|
||||
@@ -61,7 +67,13 @@ class MarkupOperationsImpl implements MarkupOperations {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadMarkup (objectId: Ref<Doc>, objectAttr: string, value: string, format: MarkupFormat): Promise<MarkupRef> {
|
||||
async uploadMarkup (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
value: string,
|
||||
format: MarkupFormat
|
||||
): Promise<MarkupRef> {
|
||||
let markup: Markup = ''
|
||||
|
||||
switch (format) {
|
||||
@@ -78,8 +90,7 @@ class MarkupOperationsImpl implements MarkupOperations {
|
||||
throw new Error('Unknown content format')
|
||||
}
|
||||
|
||||
const doc = makeCollaborativeDoc(objectId, objectAttr)
|
||||
await this.collaborator.updateContent(doc, { [objectAttr]: markup })
|
||||
return doc
|
||||
const collabId = makeCollabId(objectClass, objectId, objectAttr)
|
||||
return await this.collaborator.createMarkup(collabId, markup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { Class, type Blob, type Doc, type Ref } from '@hcengineering/core'
|
||||
|
||||
/** @public */
|
||||
export type MarkupRef = CollaborativeDoc
|
||||
export type MarkupRef = Ref<Blob>
|
||||
|
||||
/** @public */
|
||||
export type MarkupFormat = 'markup' | 'html' | 'markdown'
|
||||
@@ -45,21 +45,35 @@ export function markdown (content: string): MarkupContent {
|
||||
export interface MarkupOperations {
|
||||
/**
|
||||
* Retrieves markup content for a specified document object
|
||||
* * @param objectClass - Reference to the class of the document containing the markup
|
||||
* @param objectId - Reference to the document containing the markup
|
||||
* @param objectAttr - The attribute/field name where the markup is stored
|
||||
* @param id - Unique reference identifying the specific markup content
|
||||
* @param format - The format of the markup (e.g., HTML, Markdown, etc.)
|
||||
* @returns Promise containing the markup content as a string
|
||||
*/
|
||||
fetchMarkup: (objectId: Ref<Doc>, objectAttr: string, id: MarkupRef, format: MarkupFormat) => Promise<string>
|
||||
fetchMarkup: (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
id: MarkupRef,
|
||||
format: MarkupFormat
|
||||
) => Promise<string>
|
||||
|
||||
/**
|
||||
* Saves markup content for a document object
|
||||
* @param objectClass - Reference to the class of the document where markup should be stored
|
||||
* @param objectId - Reference to the document where markup should be stored
|
||||
* @param objectAttr - The attribute/field name where markup should be saved
|
||||
* @param markup - The actual markup content to be uploaded
|
||||
* @param format - The format of the provided markup (e.g., HTML, Markdown, etc.)
|
||||
* @returns Promise containing a reference to the newly saved markup
|
||||
*/
|
||||
uploadMarkup: (objectId: Ref<Doc>, objectAttr: string, markup: string, format: MarkupFormat) => Promise<MarkupRef>
|
||||
uploadMarkup: (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string,
|
||||
markup: string,
|
||||
format: MarkupFormat
|
||||
) => Promise<MarkupRef>
|
||||
}
|
||||
|
||||
@@ -13,26 +13,27 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { CollaborativeDoc } from '@hcengineering/core'
|
||||
import { DocumentId } from '../types'
|
||||
import { formatDocumentId, parseDocumentId } from '../utils'
|
||||
import core, { CollaborativeDoc, Doc, Ref } from '@hcengineering/core'
|
||||
import { encodeDocumentId, decodeDocumentId } from '../utils'
|
||||
|
||||
describe('utils', () => {
|
||||
it('formatDocumentId', () => {
|
||||
expect(formatDocumentId('ws1', 'doc1:HEAD:v1' as CollaborativeDoc)).toEqual('ws1://doc1:HEAD' as DocumentId)
|
||||
expect(formatDocumentId('ws1', 'doc1:HEAD:v1#doc2:v2:v2' as CollaborativeDoc)).toEqual(
|
||||
'ws1://doc1:HEAD/doc2:v2' as DocumentId
|
||||
)
|
||||
it('encodeDocumentId', () => {
|
||||
const doc: CollaborativeDoc = {
|
||||
objectClass: core.class.Card,
|
||||
objectId: 'doc1' as Ref<Doc>,
|
||||
objectAttr: 'description'
|
||||
}
|
||||
expect(encodeDocumentId('ws1', doc)).toEqual('ws1|core:class:Card|doc1|description')
|
||||
})
|
||||
|
||||
describe('parseDocumentId', () => {
|
||||
expect(parseDocumentId('ws1://doc1:HEAD' as DocumentId)).toEqual({
|
||||
describe('decodeDocumentId', () => {
|
||||
expect(decodeDocumentId('ws1|core:class:Card|doc1|description')).toEqual({
|
||||
workspaceId: 'ws1',
|
||||
collaborativeDoc: 'doc1:HEAD:HEAD' as CollaborativeDoc
|
||||
})
|
||||
expect(parseDocumentId('ws1://doc1:HEAD/doc2:v2' as DocumentId)).toEqual({
|
||||
workspaceId: 'ws1',
|
||||
collaborativeDoc: 'doc1:HEAD:HEAD#doc2:v2:v2' as CollaborativeDoc
|
||||
documentId: {
|
||||
objectClass: core.class.Card,
|
||||
objectId: 'doc1' as Ref<Doc>,
|
||||
objectAttr: 'description'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,18 +13,29 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { CollaborativeDoc, Markup, WorkspaceId, concatLink } from '@hcengineering/core'
|
||||
import { formatDocumentId } from './utils'
|
||||
import { Blob, CollaborativeDoc, Markup, MarkupBlobRef, Ref, WorkspaceId, concatLink } from '@hcengineering/core'
|
||||
import { encodeDocumentId } from './utils'
|
||||
|
||||
/** @public */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface GetContentRequest {}
|
||||
export interface GetContentRequest {
|
||||
source?: Ref<Blob>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface GetContentResponse {
|
||||
content: Record<string, Markup>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CreateContentRequest {
|
||||
content: Record<string, Markup>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CreateContentResponse {
|
||||
content: Record<string, MarkupBlobRef>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface UpdateContentRequest {
|
||||
content: Record<string, Markup>
|
||||
@@ -36,8 +47,9 @@ export interface UpdateContentResponse {}
|
||||
|
||||
/** @public */
|
||||
export interface CollaboratorClient {
|
||||
getContent: (document: CollaborativeDoc) => Promise<Record<string, Markup>>
|
||||
updateContent: (document: CollaborativeDoc, content: Record<string, Markup>) => Promise<void>
|
||||
getMarkup: (document: CollaborativeDoc, source?: Ref<Blob> | null) => Promise<Markup>
|
||||
createMarkup: (document: CollaborativeDoc, markup: Markup) => Promise<MarkupBlobRef>
|
||||
updateMarkup: (document: CollaborativeDoc, markup: Markup) => Promise<void>
|
||||
copyContent: (source: CollaborativeDoc, target: CollaborativeDoc) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -54,11 +66,11 @@ class CollaboratorClientImpl implements CollaboratorClient {
|
||||
private readonly collaboratorUrl: string
|
||||
) {}
|
||||
|
||||
private async rpc (document: CollaborativeDoc, method: string, payload: any): Promise<any> {
|
||||
private async rpc<P, R>(document: CollaborativeDoc, method: string, payload: P): Promise<R> {
|
||||
const workspace = this.workspace.name
|
||||
const documentId = formatDocumentId(workspace, document)
|
||||
const documentId = encodeDocumentId(workspace, document)
|
||||
|
||||
const url = concatLink(this.collaboratorUrl, '/rpc')
|
||||
const url = concatLink(this.collaboratorUrl, `/rpc/${encodeURIComponent(documentId)}`)
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -66,7 +78,7 @@ class CollaboratorClientImpl implements CollaboratorClient {
|
||||
Authorization: 'Bearer ' + this.token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ method, documentId, payload })
|
||||
body: JSON.stringify({ method, payload })
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -79,33 +91,58 @@ class CollaboratorClientImpl implements CollaboratorClient {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return result
|
||||
return result as R
|
||||
}
|
||||
|
||||
async getContent (document: CollaborativeDoc): Promise<Record<string, Markup>> {
|
||||
async getMarkup (document: CollaborativeDoc, source?: Ref<Blob> | null): Promise<Markup> {
|
||||
const payload: GetContentRequest = {
|
||||
source: source !== null ? source : undefined
|
||||
}
|
||||
|
||||
const res = await retry(
|
||||
3,
|
||||
async () => {
|
||||
return (await this.rpc(document, 'getContent', {})) as GetContentResponse
|
||||
return await this.rpc<GetContentRequest, GetContentResponse>(document, 'getContent', payload)
|
||||
},
|
||||
50
|
||||
)
|
||||
return res.content ?? {}
|
||||
|
||||
return res.content[document.objectAttr] ?? ''
|
||||
}
|
||||
|
||||
async updateContent (document: CollaborativeDoc, content: Record<string, Markup>): Promise<void> {
|
||||
async createMarkup (document: CollaborativeDoc, markup: Markup): Promise<MarkupBlobRef> {
|
||||
const content = {
|
||||
[document.objectAttr]: markup
|
||||
}
|
||||
|
||||
const res = await retry(
|
||||
3,
|
||||
async () => {
|
||||
return await this.rpc<CreateContentRequest, CreateContentResponse>(document, 'createContent', { content })
|
||||
},
|
||||
50
|
||||
)
|
||||
|
||||
return res.content[document.objectAttr]
|
||||
}
|
||||
|
||||
async updateMarkup (document: CollaborativeDoc, markup: Markup): Promise<void> {
|
||||
const content = {
|
||||
[document.objectAttr]: markup
|
||||
}
|
||||
|
||||
await retry(
|
||||
3,
|
||||
async () => {
|
||||
await this.rpc(document, 'updateContent', { content })
|
||||
await this.rpc<UpdateContentRequest, UpdateContentResponse>(document, 'updateContent', { content })
|
||||
},
|
||||
50
|
||||
)
|
||||
}
|
||||
|
||||
async copyContent (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> {
|
||||
const content = await this.getContent(source)
|
||||
await this.updateContent(target, content)
|
||||
async copyContent (source: CollaborativeDoc, target: CollaborativeDoc, content?: Ref<Blob>): Promise<void> {
|
||||
const markup = await this.getMarkup(source, content)
|
||||
await this.updateMarkup(target, markup)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,4 @@
|
||||
//
|
||||
|
||||
export * from './client'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -13,78 +13,26 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
Class,
|
||||
CollaborativeDoc,
|
||||
Doc,
|
||||
Ref,
|
||||
collaborativeDocChain,
|
||||
collaborativeDocFormat,
|
||||
collaborativeDocParse,
|
||||
collaborativeDocUnchain
|
||||
} from '@hcengineering/core'
|
||||
import { DocumentId, PlatformDocumentId } from './types'
|
||||
import { Class, CollaborativeDoc, Doc, Ref } from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* Formats collaborative document as Hocuspocus document name.
|
||||
*
|
||||
* The document name is used for document identification on the server so should remain the same even
|
||||
* when document is updated. Hence, we remove lastVersionId component from CollaborativeDoc.
|
||||
*
|
||||
* Example:
|
||||
* workspace1://doc1:HEAD/doc2:v1
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function formatDocumentId (workspaceId: string, collaborativeDoc: CollaborativeDoc): DocumentId {
|
||||
const path = collaborativeDocUnchain(collaborativeDoc)
|
||||
.map((p) => {
|
||||
const { documentId, versionId } = collaborativeDocParse(p)
|
||||
return `${documentId}:${versionId}`
|
||||
})
|
||||
.join('/')
|
||||
|
||||
return `${workspaceId}://${path}` as DocumentId
|
||||
/** @public */
|
||||
export function encodeDocumentId (workspaceId: string, documentId: CollaborativeDoc): string {
|
||||
const { objectClass, objectId, objectAttr } = documentId
|
||||
return [workspaceId, objectClass, objectId, objectAttr].join('|')
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function parseDocumentId (documentId: DocumentId): {
|
||||
export function decodeDocumentId (documentId: string): {
|
||||
workspaceId: string
|
||||
collaborativeDoc: CollaborativeDoc
|
||||
documentId: CollaborativeDoc
|
||||
} {
|
||||
const [workspaceId, path] = documentId.split('://')
|
||||
const segments = path.split('/')
|
||||
|
||||
const collaborativeDocs = segments.map((p) => {
|
||||
const [documentId, versionId] = p.split(':')
|
||||
return collaborativeDocFormat({ documentId, versionId, lastVersionId: versionId })
|
||||
})
|
||||
|
||||
const [workspaceId, objectClass, objectId, objectAttr] = documentId.split('|')
|
||||
return {
|
||||
workspaceId,
|
||||
collaborativeDoc: collaborativeDocChain(...collaborativeDocs)
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function formatPlatformDocumentId (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr: string
|
||||
): PlatformDocumentId {
|
||||
return `${objectClass}/${objectId}/${objectAttr}` as PlatformDocumentId
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function parsePlatformDocumentId (platformDocumentId: PlatformDocumentId): {
|
||||
objectClass: Ref<Class<Doc>>
|
||||
objectId: Ref<Doc>
|
||||
objectAttr: string
|
||||
} {
|
||||
const [objectClass, objectId, objectAttr] = platformDocumentId.split('/')
|
||||
return {
|
||||
objectClass: objectClass as Ref<Class<Doc>>,
|
||||
objectId: objectId as Ref<Doc>,
|
||||
objectAttr
|
||||
documentId: {
|
||||
objectClass: objectClass as Ref<Class<Doc>>,
|
||||
objectId: objectId as Ref<Doc>,
|
||||
objectAttr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"Enum": "Enum",
|
||||
"Members": "Members",
|
||||
"Hyperlink": "URL",
|
||||
"Collaborative": "Collaborative",
|
||||
"MarkupBlobRef": "Collaborative",
|
||||
"Object": "Object",
|
||||
"System": "System",
|
||||
"CreatedBy": "Created by",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"Enum": "Enum.",
|
||||
"Members": "Miembros",
|
||||
"Hyperlink": "Enlace",
|
||||
"Collaborative": "Colaborativo",
|
||||
"MarkupBlobRef": "Colaborativo",
|
||||
"Object": "Objeto",
|
||||
"System": "Sistema",
|
||||
"CreatedBy": "Creado por",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"Enum": "Énumération",
|
||||
"Members": "Membres",
|
||||
"Hyperlink": "URL",
|
||||
"Collaborative": "Collaboratif",
|
||||
"MarkupBlobRef": "Collaboratif",
|
||||
"Object": "Objet",
|
||||
"System": "Système",
|
||||
"CreatedBy": "Créé par",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"Enum": "Enumeração",
|
||||
"Members": "Membros",
|
||||
"Hyperlink": "URL",
|
||||
"Collaborative": "Colaborativo",
|
||||
"MarkupBlobRef": "Colaborativo",
|
||||
"Object": "Objeto",
|
||||
"System": "Sistema",
|
||||
"CreatedBy": "Criado por",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"Enum": "Справочник",
|
||||
"Members": "Участники",
|
||||
"Hyperlink": "URL",
|
||||
"Collaborative": "Коллаборативный",
|
||||
"MarkupBlobRef": "Коллаборативный",
|
||||
"Object": "Объект",
|
||||
"System": "Система",
|
||||
"CreatedBy": "Создан",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"Enum": "枚举",
|
||||
"Members": "成员",
|
||||
"Hyperlink": "URL",
|
||||
"Collaborative": "协作",
|
||||
"MarkupBlobRef": "协作",
|
||||
"Object": "对象",
|
||||
"System": "系统",
|
||||
"CreatedBy": "创建者",
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
CollaborativeDoc,
|
||||
collaborativeDocChain,
|
||||
collaborativeDocUnchain,
|
||||
collaborativeDocFormat,
|
||||
collaborativeDocParse,
|
||||
collaborativeDocFromCollaborativeDoc,
|
||||
collaborativeDocFromLastVersion,
|
||||
collaborativeDocWithVersion,
|
||||
collaborativeDocWithLastVersion,
|
||||
collaborativeDocWithSource
|
||||
} from '../collaboration'
|
||||
|
||||
describe('collaborative-doc', () => {
|
||||
describe('collaborativeDocChain', () => {
|
||||
it('chains one collaborative doc', async () => {
|
||||
expect(collaborativeDocChain('doc1:v1:v1' as CollaborativeDoc)).toEqual('doc1:v1:v1' as CollaborativeDoc)
|
||||
})
|
||||
it('chains multiple collaborative docs', async () => {
|
||||
expect(collaborativeDocChain('doc1:v1:v1' as CollaborativeDoc, 'doc2:v2:v2' as CollaborativeDoc)).toEqual(
|
||||
'doc1:v1:v1#doc2:v2:v2' as CollaborativeDoc
|
||||
)
|
||||
})
|
||||
it('chains multiple chained collaborative docs', async () => {
|
||||
expect(
|
||||
collaborativeDocChain('doc1:v1:v1#doc2:v2:v2' as CollaborativeDoc, 'doc3:v3:v3' as CollaborativeDoc)
|
||||
).toEqual('doc1:v1:v1#doc2:v2:v2#doc3:v3:v3' as CollaborativeDoc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocUnchain', () => {
|
||||
it('unchains one collaborative doc', async () => {
|
||||
expect(collaborativeDocUnchain('doc1:v1:v1' as CollaborativeDoc)).toEqual(['doc1:v1:v1'] as CollaborativeDoc[])
|
||||
})
|
||||
it('unchains multiple collaborative docs', async () => {
|
||||
expect(collaborativeDocUnchain('doc1:v1:v1#doc2:v2:v2' as CollaborativeDoc)).toEqual([
|
||||
'doc1:v1:v1',
|
||||
'doc2:v2:v2'
|
||||
] as CollaborativeDoc[])
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocParse', () => {
|
||||
it('parses collaborative doc id', async () => {
|
||||
expect(collaborativeDocParse('documentId' as CollaborativeDoc)).toEqual({
|
||||
documentId: 'documentId',
|
||||
versionId: 'HEAD',
|
||||
lastVersionId: 'HEAD',
|
||||
source: []
|
||||
})
|
||||
})
|
||||
it('parses collaborative doc id with versionId', async () => {
|
||||
expect(collaborativeDocParse('documentId:main' as CollaborativeDoc)).toEqual({
|
||||
documentId: 'documentId',
|
||||
versionId: 'main',
|
||||
lastVersionId: 'main',
|
||||
source: []
|
||||
})
|
||||
})
|
||||
it('parses collaborative doc id with versionId and lastVersionId', async () => {
|
||||
expect(collaborativeDocParse('documentId:HEAD:0' as CollaborativeDoc)).toEqual({
|
||||
documentId: 'documentId',
|
||||
versionId: 'HEAD',
|
||||
lastVersionId: '0',
|
||||
source: []
|
||||
})
|
||||
})
|
||||
it('parses collaborative doc id with versionId, lastVersionId, and source', async () => {
|
||||
expect(collaborativeDocParse('documentId:HEAD:0#documentId1:main#documentId2:HEAD' as CollaborativeDoc)).toEqual({
|
||||
documentId: 'documentId',
|
||||
versionId: 'HEAD',
|
||||
lastVersionId: '0',
|
||||
source: ['documentId1:main' as CollaborativeDoc, 'documentId2:HEAD' as CollaborativeDoc]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocFormat', () => {
|
||||
it('formats collaborative doc id', async () => {
|
||||
expect(
|
||||
collaborativeDocFormat({
|
||||
documentId: 'documentId',
|
||||
versionId: 'HEAD',
|
||||
lastVersionId: '0'
|
||||
})
|
||||
).toEqual('documentId:HEAD:0')
|
||||
})
|
||||
it('formats collaborative doc id with sources', async () => {
|
||||
expect(
|
||||
collaborativeDocFormat({
|
||||
documentId: 'documentId',
|
||||
versionId: 'HEAD',
|
||||
lastVersionId: '0',
|
||||
source: ['documentId1:main' as CollaborativeDoc, 'documentId2:HEAD' as CollaborativeDoc]
|
||||
})
|
||||
).toEqual('documentId:HEAD:0#documentId1:main#documentId2:HEAD')
|
||||
})
|
||||
it('formats collaborative doc id with invalid characters', async () => {
|
||||
expect(
|
||||
collaborativeDocFormat({
|
||||
documentId: 'doc:id',
|
||||
versionId: 'version#id',
|
||||
lastVersionId: 'last:version#id'
|
||||
})
|
||||
).toEqual('doc%id:version%id:last%version%id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocWithVersion', () => {
|
||||
it('updates collaborative doc version id', async () => {
|
||||
expect(collaborativeDocWithVersion('doc1:HEAD:HEAD' as CollaborativeDoc, 'v1')).toEqual('doc1:v1:v1')
|
||||
expect(collaborativeDocWithVersion('doc1:HEAD:v1' as CollaborativeDoc, 'v2')).toEqual('doc1:v2:v2')
|
||||
expect(collaborativeDocWithVersion('doc1:HEAD:v1#doc2:v1:v1' as CollaborativeDoc, 'v2')).toEqual(
|
||||
'doc1:v2:v2#doc2:v1:v1'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocWithLastVersion', () => {
|
||||
it('updates collaborative doc version id', async () => {
|
||||
expect(collaborativeDocWithLastVersion('doc1:HEAD:HEAD' as CollaborativeDoc, 'v1')).toEqual('doc1:HEAD:v1')
|
||||
expect(collaborativeDocWithLastVersion('doc1:HEAD:v1' as CollaborativeDoc, 'v2')).toEqual('doc1:HEAD:v2')
|
||||
expect(collaborativeDocWithLastVersion('doc1:HEAD:v1#doc2:v1:v1' as CollaborativeDoc, 'v2')).toEqual(
|
||||
'doc1:HEAD:v2#doc2:v1:v1'
|
||||
)
|
||||
expect(collaborativeDocWithLastVersion('doc1:v1:v1' as CollaborativeDoc, 'v2')).toEqual(
|
||||
// cannot update last version for non HEAD
|
||||
'doc1:v1:v1'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocWithSource', () => {
|
||||
it('updates collaborative doc version id', async () => {
|
||||
expect(
|
||||
collaborativeDocWithSource('doc1:HEAD:HEAD' as CollaborativeDoc, 'doc2:v1:v1' as CollaborativeDoc)
|
||||
).toEqual('doc1:HEAD:HEAD#doc2:v1:v1' as CollaborativeDoc)
|
||||
expect(collaborativeDocWithSource('doc1:v1:v1' as CollaborativeDoc, 'doc2:v2:v2' as CollaborativeDoc)).toEqual(
|
||||
'doc1:v1:v1#doc2:v2:v2' as CollaborativeDoc
|
||||
)
|
||||
expect(
|
||||
collaborativeDocWithSource('doc1:v1:v1' as CollaborativeDoc, 'doc2:v2:v2#doc3:v3:v3' as CollaborativeDoc)
|
||||
).toEqual('doc1:v1:v1#doc2:v2:v2#doc3:v3:v3' as CollaborativeDoc)
|
||||
expect(
|
||||
collaborativeDocWithSource('doc1:v1:v1#doc2:v2:v2' as CollaborativeDoc, 'doc3:v3:v3' as CollaborativeDoc)
|
||||
).toEqual('doc1:v1:v1#doc3:v3:v3' as CollaborativeDoc)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocFromLastVersion', () => {
|
||||
it('returns valid collaborative doc id', async () => {
|
||||
expect(collaborativeDocFromLastVersion('doc1:HEAD:HEAD#doc2:main:v2#doc3:main:v3' as CollaborativeDoc)).toEqual(
|
||||
'doc1:HEAD:HEAD#doc2:main:v2#doc3:main:v3'
|
||||
)
|
||||
expect(collaborativeDocFromLastVersion('doc1:HEAD:v1#doc2:main:v2#doc3:main:v3' as CollaborativeDoc)).toEqual(
|
||||
'doc1:v1:v1#doc2:main:v2#doc3:main:v3'
|
||||
)
|
||||
expect(collaborativeDocFromLastVersion('doc1:v1:v1#doc2:main:v2#doc3:main:v3' as CollaborativeDoc)).toEqual(
|
||||
'doc1:v1:v1#doc2:main:v2#doc3:main:v3'
|
||||
)
|
||||
expect(collaborativeDocFromLastVersion('doc1:HEAD:v1' as CollaborativeDoc)).toEqual('doc1:v1:v1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('collaborativeDocFromCollaborativeDoc', () => {
|
||||
it('returns valid collaborative doc id', async () => {
|
||||
expect(
|
||||
collaborativeDocFromCollaborativeDoc(
|
||||
'doc1:HEAD:HEAD' as CollaborativeDoc,
|
||||
'doc2:HEAD:v2#doc3:v3:v3' as CollaborativeDoc
|
||||
)
|
||||
).toEqual('doc1:HEAD:HEAD#doc2:v2:v2#doc3:v3:v3')
|
||||
|
||||
expect(
|
||||
collaborativeDocFromCollaborativeDoc(
|
||||
'doc1:HEAD:HEAD' as CollaborativeDoc,
|
||||
'doc2:v2:v2#doc3:v3:v3' as CollaborativeDoc
|
||||
)
|
||||
).toEqual('doc1:HEAD:HEAD#doc2:v2:v2#doc3:v3:v3')
|
||||
|
||||
expect(
|
||||
collaborativeDocFromCollaborativeDoc(
|
||||
'doc1:HEAD:HEAD' as CollaborativeDoc,
|
||||
'doc2:HEAD:HEAD#doc3:v3:v3' as CollaborativeDoc
|
||||
)
|
||||
).toEqual('doc1:HEAD:HEAD#doc2:HEAD:HEAD#doc3:v3:v3')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,6 @@
|
||||
//
|
||||
|
||||
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
|
||||
import { CollaborativeDoc } from './collaboration'
|
||||
import type { DocumentQuery } from './storage'
|
||||
|
||||
/**
|
||||
@@ -55,6 +54,13 @@ export type CollectionSize<T> = T[]['length']
|
||||
*/
|
||||
export type Rank = string
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Reference to blob containing snapshot of collaborative doc.
|
||||
*/
|
||||
export type MarkupBlobRef = Ref<Blob>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -76,7 +82,7 @@ export interface Doc<S extends Space = Space> extends Obj {
|
||||
|
||||
export interface Card extends Doc {
|
||||
title: string
|
||||
description?: CollaborativeDoc | null
|
||||
description?: MarkupBlobRef | null
|
||||
identifier?: string
|
||||
parent?: Ref<Card> | null
|
||||
}
|
||||
|
||||
@@ -13,175 +13,41 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Doc, Ref } from './classes'
|
||||
|
||||
/**
|
||||
* Identifier of the collaborative document holding collaborative content.
|
||||
*
|
||||
* Format:
|
||||
* {documentId}:{versionId}:{lastVersionId}
|
||||
* {documentId}:{versionId}
|
||||
*
|
||||
* Where:
|
||||
* - documentId is an identifier of the document in storage
|
||||
* - versionId is an identifier of the document version, HEAD for latest editable version
|
||||
* - lastVersionId is an identifier of the latest available version
|
||||
*
|
||||
* The collaborative document may contain one or more such sections chained with # (hash):
|
||||
* collaborativeDocId#collaborativeDocId#collaborativeDocId#...
|
||||
*
|
||||
* When collaborative document does not exist, it will be initialized from the first existing
|
||||
* document in the list.
|
||||
*
|
||||
* @public
|
||||
* */
|
||||
export type CollaborativeDoc = string & { __collaborativeDoc: true }
|
||||
import type { Class, MarkupBlobRef, Doc, Ref } from './classes'
|
||||
|
||||
/** @public */
|
||||
export type CollaborativeDocVersion = string | typeof CollaborativeDocVersionHead
|
||||
export interface CollaborativeDoc {
|
||||
objectClass: Ref<Class<Doc>>
|
||||
objectId: Ref<Doc>
|
||||
objectAttr: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const CollaborativeDocVersionHead = 'HEAD'
|
||||
|
||||
/** @public */
|
||||
export function makeCollaborativeDoc (
|
||||
objectId: Ref<Doc>,
|
||||
objectAttr?: string | undefined,
|
||||
versionId?: string | undefined
|
||||
export function makeCollabId<T extends Doc, U extends keyof T> (
|
||||
objectClass: Ref<Class<T>>,
|
||||
objectId: Ref<T>,
|
||||
objectAttr: Extract<U, string> | string
|
||||
): CollaborativeDoc {
|
||||
const storageDocumentId = objectAttr !== undefined && objectAttr !== '' ? `${objectId}%${objectAttr}` : `${objectId}`
|
||||
return collaborativeDocFormat({
|
||||
documentId: storageDocumentId,
|
||||
versionId: CollaborativeDocVersionHead,
|
||||
lastVersionId: versionId ?? '0'
|
||||
})
|
||||
return { objectClass, objectId, objectAttr }
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface CollaborativeDocData {
|
||||
// Id of the document in object storage
|
||||
documentId: string
|
||||
// Id of the document version
|
||||
// HEAD version represents the editable last document version
|
||||
// Otherwise, it is a readonly version
|
||||
versionId: CollaborativeDocVersion
|
||||
// For HEAD versionId it is the latest available document version
|
||||
// Otherwise, it is the same value as versionId
|
||||
lastVersionId: string
|
||||
|
||||
source?: CollaborativeDoc[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge several collaborative docs into single collaborative doc train.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocChain (...docs: CollaborativeDoc[]): CollaborativeDoc {
|
||||
return docs.join('#') as CollaborativeDoc
|
||||
}
|
||||
|
||||
/**
|
||||
* Split collaborative doc train into separate collaborative docs.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocUnchain (doc: CollaborativeDoc): CollaborativeDoc[] {
|
||||
return doc.split('#') as CollaborativeDoc[]
|
||||
export function makeDocCollabId<T extends Doc, U extends keyof T> (
|
||||
doc: T,
|
||||
objectAttr: Extract<U, string> | string
|
||||
): CollaborativeDoc {
|
||||
return makeCollabId(doc._class, doc._id, objectAttr)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function collaborativeDocParse (doc: CollaborativeDoc): CollaborativeDocData {
|
||||
const [first, ...other] = collaborativeDocUnchain(doc)
|
||||
const [documentId, versionId, lastVersionId] = first.split(':')
|
||||
return {
|
||||
documentId,
|
||||
versionId: versionId ?? CollaborativeDocVersionHead,
|
||||
lastVersionId: lastVersionId ?? versionId ?? CollaborativeDocVersionHead,
|
||||
source: other
|
||||
}
|
||||
export function makeCollabYdocId (doc: CollaborativeDoc): MarkupBlobRef {
|
||||
const { objectId, objectAttr } = doc
|
||||
return `${objectId}%${objectAttr}` as MarkupBlobRef
|
||||
}
|
||||
|
||||
const sanitize = (value: string): string => value.replace(/[:#]/g, '%')
|
||||
|
||||
/** @public */
|
||||
export function collaborativeDocFormat ({
|
||||
documentId,
|
||||
versionId,
|
||||
lastVersionId,
|
||||
source
|
||||
}: CollaborativeDocData): CollaborativeDoc {
|
||||
const parts = [sanitize(documentId), sanitize(versionId), sanitize(lastVersionId)]
|
||||
const collaborativeDoc = parts.join(':') as CollaborativeDoc
|
||||
return collaborativeDocChain(collaborativeDoc, ...(source ?? []))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates versionId component in the collaborative document.
|
||||
* Both versionId and lastVersionId will refer to the same collaborative document version.
|
||||
*
|
||||
* When versionId is not HEAD, the document will represent a readonly document version (snapshot).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocWithVersion (collaborativeDoc: CollaborativeDoc, versionId: string): CollaborativeDoc {
|
||||
const { documentId, source } = collaborativeDocParse(collaborativeDoc)
|
||||
return collaborativeDocFormat({ documentId, versionId, lastVersionId: versionId, source })
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lastVersionId component in the collaborative document.
|
||||
*
|
||||
* When document versionId is HEAD, the function is no-op.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocWithLastVersion (
|
||||
collaborativeDoc: CollaborativeDoc,
|
||||
lastVersionId: string
|
||||
): CollaborativeDoc {
|
||||
const { documentId, versionId, source } = collaborativeDocParse(collaborativeDoc)
|
||||
return versionId === CollaborativeDocVersionHead
|
||||
? collaborativeDocFormat({ documentId, versionId, lastVersionId, source })
|
||||
: collaborativeDoc
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces source component in the collaborative document.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocWithSource (
|
||||
collaborativeDoc: CollaborativeDoc,
|
||||
source: CollaborativeDoc
|
||||
): CollaborativeDoc {
|
||||
const { documentId, versionId, lastVersionId } = collaborativeDocParse(collaborativeDoc)
|
||||
return collaborativeDocFormat({ documentId, versionId, lastVersionId, source: [source] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates collaborative document that refers to the last version from the source collaborative document.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocFromLastVersion (collaborativeDoc: CollaborativeDoc): CollaborativeDoc {
|
||||
const { documentId, lastVersionId, source } = collaborativeDocParse(collaborativeDoc)
|
||||
return collaborativeDocFormat({
|
||||
documentId,
|
||||
versionId: lastVersionId,
|
||||
lastVersionId,
|
||||
source
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates collaborative document that refers to the last version from the source collaborative document.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function collaborativeDocFromCollaborativeDoc (
|
||||
collaborativeDoc: CollaborativeDoc,
|
||||
sourceCollaborativeDoc: CollaborativeDoc
|
||||
): CollaborativeDoc {
|
||||
return collaborativeDocWithSource(collaborativeDoc, collaborativeDocFromLastVersion(sourceCollaborativeDoc))
|
||||
export function makeCollabJsonId (doc: CollaborativeDoc): MarkupBlobRef {
|
||||
const timestamp = Date.now()
|
||||
const { objectId, objectAttr } = doc
|
||||
return [objectId, objectAttr, timestamp].join('-') as MarkupBlobRef
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
Blob,
|
||||
Card,
|
||||
Class,
|
||||
MarkupBlobRef,
|
||||
Collection,
|
||||
Configuration,
|
||||
ConfigurationElement,
|
||||
@@ -54,7 +55,6 @@ import type {
|
||||
TypedSpace,
|
||||
UserStatus
|
||||
} from './classes'
|
||||
import { CollaborativeDoc } from './collaboration'
|
||||
import { Status, StatusCategory } from './status'
|
||||
import type {
|
||||
Tx,
|
||||
@@ -120,8 +120,7 @@ export default plugin(coreId, {
|
||||
TypeBoolean: '' as Ref<Class<Type<boolean>>>,
|
||||
TypeTimestamp: '' as Ref<Class<Type<Timestamp>>>,
|
||||
TypeDate: '' as Ref<Class<Type<Timestamp | Date>>>,
|
||||
TypeCollaborativeDoc: '' as Ref<Class<Type<CollaborativeDoc>>>,
|
||||
TypeCollaborativeDocVersion: '' as Ref<Class<Type<CollaborativeDoc>>>,
|
||||
TypeCollaborativeDoc: '' as Ref<Class<Type<MarkupBlobRef>>>,
|
||||
RefTo: '' as Ref<Class<RefTo<Doc>>>,
|
||||
ArrOf: '' as Ref<Class<ArrOf<Doc>>>,
|
||||
Enum: '' as Ref<Class<Enum>>,
|
||||
@@ -185,9 +184,8 @@ export default plugin(coreId, {
|
||||
String: '' as IntlString,
|
||||
Record: '' as IntlString,
|
||||
Markup: '' as IntlString,
|
||||
Collaborative: '' as IntlString,
|
||||
CollaborativeDoc: '' as IntlString,
|
||||
CollaborativeDocVersion: '' as IntlString,
|
||||
MarkupBlobRef: '' as IntlString,
|
||||
Number: '' as IntlString,
|
||||
Boolean: '' as IntlString,
|
||||
Timestamp: '' as IntlString,
|
||||
|
||||
@@ -114,7 +114,6 @@ export class MeasureMetricsContext implements MeasureContext {
|
||||
this.logParams
|
||||
)
|
||||
result.id = this.id
|
||||
result.onEnd = this.onEnd.bind(this)
|
||||
result.contextData = this.contextData
|
||||
return result
|
||||
}
|
||||
@@ -190,8 +189,6 @@ export class MeasureMetricsContext implements MeasureContext {
|
||||
end (): void {
|
||||
this.done()
|
||||
}
|
||||
|
||||
async onEnd (ctx: MeasureContext): Promise<void> {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +217,10 @@ export function registerOperationLog (ctx: MeasureContext): { opLogMetrics?: Met
|
||||
}
|
||||
const op: OperationLog = { start: Date.now(), ops: [], end: -1 }
|
||||
let opLogMetrics: Metrics | undefined
|
||||
ctx.id = generateId()
|
||||
|
||||
if (ctx.id === undefined) {
|
||||
ctx.id = 'op_' + generateId()
|
||||
}
|
||||
if (ctx.metrics !== undefined) {
|
||||
if (ctx.metrics.opLog === undefined) {
|
||||
ctx.metrics.opLog = {}
|
||||
|
||||
@@ -111,6 +111,4 @@ export interface MeasureContext<Q = any> {
|
||||
// Mark current context as complete
|
||||
// If no value is passed, time difference will be used.
|
||||
end: (value?: number) => void
|
||||
|
||||
onEnd?: (ctx: MeasureContext) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@ export interface SessionData {
|
||||
branding: Branding | null
|
||||
|
||||
fulltextUpdates?: Map<Ref<DocIndexState>, DocIndexState>
|
||||
|
||||
asyncRequests?: (() => Promise<void>)[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import core, {
|
||||
Class,
|
||||
Classifier,
|
||||
ClassifierKind,
|
||||
CollaborativeDoc,
|
||||
MarkupBlobRef,
|
||||
Data,
|
||||
DateRangeMode,
|
||||
Doc,
|
||||
@@ -500,15 +500,8 @@ export function ArrOf<T extends PropertyType | Ref<Doc>> (type: Type<T>): TypeAr
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeCollaborativeDoc (): Type<CollaborativeDoc> {
|
||||
return { _class: core.class.TypeCollaborativeDoc, label: core.string.CollaborativeDoc }
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeCollaborativeDocVersion (): Type<CollaborativeDoc> {
|
||||
return { _class: core.class.TypeCollaborativeDocVersion, label: core.string.CollaborativeDocVersion }
|
||||
export function TypeCollaborativeDoc (): Type<MarkupBlobRef> {
|
||||
return { _class: core.class.TypeCollaborativeDoc, label: core.string.MarkupBlobRef }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Typ obsahu",
|
||||
"ContentTypeNotSupported": "Náhled není dostupný pro tento typ obsahu",
|
||||
"StartDrawing": "Načmárejte",
|
||||
"DrawingHistory": "Čmárání historie"
|
||||
"DrawingHistory": "Čmárání historie",
|
||||
"ColorAdd": "Přidejte předvolbu barev",
|
||||
"ColorRemove": "Odstraňte předvolbu barev",
|
||||
"ColorReset": "Resetovat předvolby barev"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Soubor je příliš velký"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Content type",
|
||||
"ContentTypeNotSupported": "Preview is not available for this content type",
|
||||
"StartDrawing": "Scribble over",
|
||||
"DrawingHistory": "Scribble history"
|
||||
"DrawingHistory": "Scribble history",
|
||||
"ColorAdd": "Add color preset",
|
||||
"ColorRemove": "Remove color preset",
|
||||
"ColorReset": "Reset color presets"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "File too large"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Tipo de contenido",
|
||||
"ContentTypeNotSupported": "La vista previa no está disponible para este tipo de contenido",
|
||||
"StartDrawing": "Garabatear encima",
|
||||
"DrawingHistory": "Historia de garabatos"
|
||||
"DrawingHistory": "Historia de garabatos",
|
||||
"ColorAdd": "Añadir ajuste preestablecido de color",
|
||||
"ColorRemove": "Eliminar el ajuste preestablecido de color",
|
||||
"ColorReset": "Restablecer ajustes preestablecidos de color"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Archivo demasiado grande"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Type de contenu",
|
||||
"ContentTypeNotSupported": "L'aperçu n'est pas disponible pour ce type de contenu",
|
||||
"StartDrawing": "Gribouiller dessus",
|
||||
"DrawingHistory": "Histoire de gribouillage"
|
||||
"DrawingHistory": "Histoire de gribouillage",
|
||||
"ColorAdd": "Ajouter un préréglage de couleur",
|
||||
"ColorRemove": "Supprimer le préréglage de couleur",
|
||||
"ColorReset": "Réinitialiser les préréglages de couleur"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Fichier trop volumineux"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Tipo di contenuto",
|
||||
"ContentTypeNotSupported": "Anteprima non disponibile per questo tipo di contenuto",
|
||||
"StartDrawing": "Scarabocchiare sopra",
|
||||
"DrawingHistory": "Scarabocchiare la storia"
|
||||
"DrawingHistory": "Scarabocchiare la storia",
|
||||
"ColorAdd": "Aggiungi preimpostazione colore",
|
||||
"ColorRemove": "Rimuovi preimpostazione colore",
|
||||
"ColorReset": "Ripristina i preset colore"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "File troppo grande"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Tipo de conteúdo",
|
||||
"ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo",
|
||||
"StartDrawing": "Scarabocchiare sopra",
|
||||
"DrawingHistory": "História de rabiscos"
|
||||
"DrawingHistory": "História de rabiscos",
|
||||
"ColorAdd": "Adicionar predefinição de cor",
|
||||
"ColorRemove": "Remover predefinição de cor",
|
||||
"ColorReset": "Repor predefinições de cores"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Ficheiro demasiado grande"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "Тип контента",
|
||||
"ContentTypeNotSupported": "Предварительный просмотр недоступен для этого типа контента",
|
||||
"StartDrawing": "Сделать набросок",
|
||||
"DrawingHistory": "История набросков"
|
||||
"DrawingHistory": "История набросков",
|
||||
"ColorAdd": "Добавить предустановленный цвет",
|
||||
"ColorRemove": "Удалить предустановленный цвет",
|
||||
"ColorReset": "Сбросить предустановленные цвета"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "Файл слишком большой"
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
"ContentType": "内容类型",
|
||||
"ContentTypeNotSupported": "此內容類型無法預覽",
|
||||
"StartDrawing": "随意涂鸦",
|
||||
"DrawingHistory": "涂鸦的历史"
|
||||
"DrawingHistory": "涂鸦的历史",
|
||||
"ColorAdd": "添加颜色预设",
|
||||
"ColorRemove": "删除颜色预设",
|
||||
"ColorReset": "重置颜色预设"
|
||||
},
|
||||
"status": {
|
||||
"FileTooLarge": "文件太大"
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
//
|
||||
|
||||
import { type CollaboratorClient, getClient as getCollaborator } from '@hcengineering/collaborator-client'
|
||||
import { type CollaborativeDoc, type Markup, getWorkspaceId } from '@hcengineering/core'
|
||||
import { type Blob, type CollaborativeDoc, type Markup, type Ref, getWorkspaceId } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import presentation from './plugin'
|
||||
|
||||
/** @public */
|
||||
export function getCollaboratorClient (): CollaboratorClient {
|
||||
function getClient (): CollaboratorClient {
|
||||
const workspaceId = getWorkspaceId(getMetadata(presentation.metadata.WorkspaceId) ?? '')
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const collaboratorURL = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''
|
||||
@@ -29,19 +28,25 @@ export function getCollaboratorClient (): CollaboratorClient {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function getMarkup (collaborativeDoc: CollaborativeDoc): Promise<Record<string, Markup>> {
|
||||
const client = getCollaboratorClient()
|
||||
return await client.getContent(collaborativeDoc)
|
||||
export async function getMarkup (doc: CollaborativeDoc, source: Ref<Blob> | null | undefined): Promise<Markup> {
|
||||
const client = getClient()
|
||||
return await client.getMarkup(doc, source)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function updateMarkup (collaborativeDoc: CollaborativeDoc, content: Record<string, Markup>): Promise<void> {
|
||||
const client = getCollaboratorClient()
|
||||
await client.updateContent(collaborativeDoc, content)
|
||||
export async function createMarkup (doc: CollaborativeDoc, markup: Markup): Promise<Ref<Blob>> {
|
||||
const client = getClient()
|
||||
return await client.createMarkup(doc, markup)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function copyDocument (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> {
|
||||
const client = getCollaboratorClient()
|
||||
export async function updateMarkup (doc: CollaborativeDoc, markup: Markup): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.updateMarkup(doc, markup)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function copyMarkup (source: CollaborativeDoc, target: CollaborativeDoc): Promise<void> {
|
||||
const client = getClient()
|
||||
await client.copyContent(source, target)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Button, IconDelete, IconEdit, resizeObserver } from '@hcengineering/ui'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { resizeObserver } from '@hcengineering/ui'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { drawing, type DrawingData, type DrawingTool } from '../drawing'
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
import { drawing, type DrawingCmd, type DrawingData, type DrawingTool } from '../drawing'
|
||||
import DrawingBoardToolbar from './DrawingBoardToolbar.svelte'
|
||||
|
||||
export let active = false
|
||||
export let readonly = true
|
||||
@@ -25,10 +26,11 @@
|
||||
export let drawings: DrawingData[]
|
||||
export let createDrawing: (data: any) => Promise<void>
|
||||
|
||||
let drawingTool: DrawingTool = 'pen'
|
||||
let penColor = 'blue'
|
||||
const penColors = ['red', 'green', 'blue', 'white', 'black']
|
||||
let drawingData: DrawingData | undefined
|
||||
let tool: DrawingTool
|
||||
let penColor: string
|
||||
let penWidth: number
|
||||
let eraserWidth: number
|
||||
let commands: DrawingCmd[] | undefined
|
||||
let board: HTMLDivElement
|
||||
let toolbar: HTMLDivElement
|
||||
let toolbarInside = false
|
||||
@@ -51,42 +53,58 @@
|
||||
if (readonly !== oldReadonly || drawings !== oldDrawings) {
|
||||
if (drawings !== undefined) {
|
||||
if (readonly) {
|
||||
if (modified && drawingData !== undefined) {
|
||||
createDrawing(drawingData).catch((error) => {
|
||||
console.error('Failed to save drawing', error)
|
||||
})
|
||||
}
|
||||
drawingData = drawings[0]
|
||||
saveDrawing()
|
||||
parseDrawing(drawings[0])
|
||||
modified = false
|
||||
} else {
|
||||
if (drawingData === undefined) {
|
||||
drawingData = {}
|
||||
if (commands === undefined) {
|
||||
commands = []
|
||||
} else {
|
||||
// Edit current content as a new drawing
|
||||
drawingData = {
|
||||
content: drawingData.content
|
||||
}
|
||||
commands = [...commands]
|
||||
}
|
||||
modified = false
|
||||
}
|
||||
} else {
|
||||
drawingData = undefined
|
||||
commands = undefined
|
||||
}
|
||||
oldDrawings = drawings
|
||||
oldReadonly = readonly
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (modified && drawingData !== undefined) {
|
||||
createDrawing(drawingData).catch((error) => {
|
||||
function parseDrawing (data: DrawingData | undefined): void {
|
||||
if (data?.content !== undefined && data?.content !== null) {
|
||||
try {
|
||||
commands = JSON.parse(data.content)
|
||||
} catch (error: any) {
|
||||
commands = []
|
||||
Analytics.handleError(error)
|
||||
console.error('Failed to parse drawing content', error)
|
||||
}
|
||||
} else {
|
||||
commands = []
|
||||
}
|
||||
}
|
||||
|
||||
function saveDrawing (): void {
|
||||
if (modified && commands !== undefined) {
|
||||
const data: DrawingData = {
|
||||
content: JSON.stringify(commands)
|
||||
}
|
||||
createDrawing(data).catch((error) => {
|
||||
Analytics.handleError(error)
|
||||
console.error('Failed to save drawing', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
saveDrawing()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if active && drawingData !== undefined}
|
||||
{#if active && commands !== undefined}
|
||||
<div
|
||||
{...$$restProps}
|
||||
style:position="relative"
|
||||
@@ -98,99 +116,32 @@
|
||||
readonly,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
drawingData,
|
||||
drawingTool,
|
||||
commands,
|
||||
tool,
|
||||
penColor,
|
||||
changed: (content) => {
|
||||
penWidth,
|
||||
eraserWidth,
|
||||
cmdAdded: () => {
|
||||
modified = true
|
||||
if (drawingData !== undefined) {
|
||||
drawingData.content = content
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if !readonly}
|
||||
<div class="toolbar" class:inside={toolbarInside} bind:this={toolbar}>
|
||||
<Button
|
||||
icon={IconDelete}
|
||||
kind="icon"
|
||||
on:click={() => {
|
||||
drawingData = {}
|
||||
modified = true
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
<Button
|
||||
icon={IconEdit}
|
||||
kind="icon"
|
||||
selected={drawingTool === 'pen'}
|
||||
on:click={() => {
|
||||
drawingTool = 'pen'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconEraser}
|
||||
kind="icon"
|
||||
selected={drawingTool === 'erase'}
|
||||
on:click={() => {
|
||||
drawingTool = 'erase'
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
{#each penColors as color}
|
||||
<Button
|
||||
kind="icon"
|
||||
selected={penColor === color}
|
||||
on:click={() => {
|
||||
penColor = color
|
||||
}}
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="colorIcon"
|
||||
class:emphasized={color === 'white' || color === 'black'}
|
||||
style:background={color}
|
||||
/>
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
<DrawingBoardToolbar
|
||||
placeInside={toolbarInside}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
bind:penColor
|
||||
bind:penWidth
|
||||
bind:eraserWidth
|
||||
on:clear={() => {
|
||||
commands = []
|
||||
modified = true
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
bottom: 100%;
|
||||
|
||||
&.inside {
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
bottom: unset;
|
||||
background-color: var(--theme-navpanel-color);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.colorIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin: -3px;
|
||||
|
||||
&.emphasized {
|
||||
box-shadow: 0px 0px 3px 0px var(--theme-button-contrast-enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import {
|
||||
Button,
|
||||
IconAdd,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconMoreH,
|
||||
IconRedo,
|
||||
SelectPopup,
|
||||
SelectPopupValueType,
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
import IconMove from './icons/Move.svelte'
|
||||
import { DrawingTool } from '../drawing'
|
||||
import presentation from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const maxColors = 8
|
||||
const minColors = 0
|
||||
const defaultColor = '#0000ff'
|
||||
const defaultColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff', '#000000']
|
||||
const storageKey = {
|
||||
color: 'drawingBoard.color',
|
||||
colors: 'drawingBoard.colors',
|
||||
penWidth: 'drawingBoard.penWidth',
|
||||
eraserWidth: 'drawingBoard.eraserWidth'
|
||||
}
|
||||
|
||||
export let tool: DrawingTool = 'pen'
|
||||
export let penColor: string
|
||||
export let penWidth: number
|
||||
export let eraserWidth: number
|
||||
export let placeInside = false
|
||||
export let showPanTool = false
|
||||
export let toolbar: HTMLDivElement | undefined
|
||||
|
||||
let colorSelector: HTMLInputElement
|
||||
let penColors: string[] = defaultColors
|
||||
|
||||
function showMenu (ev: MouseEvent): void {
|
||||
const items: SelectPopupValueType[] = []
|
||||
if (penColors.length < maxColors) {
|
||||
items.push({
|
||||
id: 'add-color',
|
||||
label: presentation.string.ColorAdd,
|
||||
icon: IconAdd
|
||||
})
|
||||
}
|
||||
if (penColors.length > minColors) {
|
||||
items.push({
|
||||
id: 'remove-color',
|
||||
label: presentation.string.ColorRemove,
|
||||
icon: IconDelete
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
id: 'reset-colors',
|
||||
label: presentation.string.ColorReset,
|
||||
icon: IconRedo
|
||||
})
|
||||
showPopup(SelectPopup, { value: items }, eventToHTMLElement(ev), (id) => {
|
||||
switch (id) {
|
||||
case 'add-color': {
|
||||
if (colorSelector !== undefined) {
|
||||
colorSelector.value = penColor
|
||||
colorSelector.click()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'remove-color': {
|
||||
penColors = penColors.filter((c: string) => c !== penColor)
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||
selectColor(penColors[0])
|
||||
break
|
||||
}
|
||||
case 'reset-colors': {
|
||||
penColors = defaultColors
|
||||
localStorage.removeItem(storageKey.colors)
|
||||
selectColor(penColors[0])
|
||||
break
|
||||
}
|
||||
case undefined: {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
console.error('Unknown command id', id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function addColorPreset (): void {
|
||||
penColor = penColor.toLowerCase()
|
||||
if (!penColors.includes(penColor)) {
|
||||
penColors = [...penColors, penColor]
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(penColors))
|
||||
}
|
||||
}
|
||||
|
||||
function selectColor (color: string): void {
|
||||
penColor = color ?? defaultColor
|
||||
localStorage.setItem(storageKey.color, penColor)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
const savedColors = localStorage.getItem(storageKey.colors)
|
||||
penColors = savedColors !== null ? JSON.parse(savedColors.toLowerCase()) : defaultColors
|
||||
} catch {
|
||||
penColors = defaultColors
|
||||
}
|
||||
penColor = (localStorage.getItem(storageKey.color) ?? penColor ?? defaultColor).toLowerCase()
|
||||
if (!penColors.includes(penColor)) {
|
||||
penColor = penColors[0] ?? defaultColor
|
||||
}
|
||||
penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '4')
|
||||
eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '30')
|
||||
})
|
||||
|
||||
function updatePenWidth (): void {
|
||||
localStorage.setItem(storageKey.penWidth, penWidth.toString())
|
||||
}
|
||||
|
||||
function updateEraserWidth (): void {
|
||||
localStorage.setItem(storageKey.eraserWidth, eraserWidth.toString())
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="toolbar" class:inside={placeInside} bind:this={toolbar}>
|
||||
<Button
|
||||
icon={IconDelete}
|
||||
kind="icon"
|
||||
on:click={() => {
|
||||
tool = 'pen'
|
||||
dispatch('clear')
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
<Button
|
||||
icon={IconEdit}
|
||||
kind="icon"
|
||||
selected={tool === 'pen'}
|
||||
on:click={() => {
|
||||
tool = 'pen'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconEraser}
|
||||
kind="icon"
|
||||
selected={tool === 'erase'}
|
||||
on:click={() => {
|
||||
tool = 'erase'
|
||||
}}
|
||||
/>
|
||||
{#if showPanTool}
|
||||
<Button
|
||||
icon={IconMove}
|
||||
kind="icon"
|
||||
selected={tool === 'pan'}
|
||||
on:click={() => {
|
||||
tool = 'pan'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="divider buttons-divider" />
|
||||
{#if tool === 'pen'}
|
||||
<input type="range" min={2} max={20} step={2} bind:value={penWidth} on:change={updatePenWidth} />
|
||||
{:else}
|
||||
<input type="range" min={10} max={100} step={10} bind:value={eraserWidth} on:change={updateEraserWidth} />
|
||||
{/if}
|
||||
<div class="divider buttons-divider" />
|
||||
{#each penColors as color}
|
||||
<Button
|
||||
kind="icon"
|
||||
selected={penColor === color}
|
||||
on:click={() => {
|
||||
tool = 'pen'
|
||||
selectColor(color)
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="colorIcon" style:background={color} />
|
||||
</Button>
|
||||
{/each}
|
||||
<div>
|
||||
<input
|
||||
type="color"
|
||||
class="colorSelector"
|
||||
bind:this={colorSelector}
|
||||
bind:value={penColor}
|
||||
on:change={addColorPreset}
|
||||
/>
|
||||
<Button kind="icon" icon={IconMoreH} on:click={showMenu} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
bottom: 100%;
|
||||
|
||||
&.inside {
|
||||
left: 0.5rem;
|
||||
top: 0.5rem;
|
||||
bottom: unset;
|
||||
background-color: var(--theme-popup-header);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
border: 1px solid var(--theme-popup-divider);
|
||||
box-shadow: 0.05rem 0.05rem 0.25rem rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.colorIcon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
margin: -0.15rem;
|
||||
box-shadow: 0px 0px 0.15rem 0px var(--theme-button-contrast-enabled);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.colorSelector {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { SortingOrder, type Blob, type Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Button, Dialog, IconHistory, IconScribble, showPopup, tooltip } from '@hcengineering/ui'
|
||||
@@ -63,6 +64,7 @@
|
||||
})
|
||||
.catch((error) => {
|
||||
drawingLoading = false
|
||||
Analytics.handleError(error)
|
||||
console.error('Failed to load drawings for file', file, error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
export let position: 'start' | 'middle' | 'end' | undefined = undefined
|
||||
export let selected = false
|
||||
export let color = 'var(--theme-bg-color)'
|
||||
export let noGap = false
|
||||
export let fontColor: string | undefined = undefined
|
||||
|
||||
let clientWidth = 0
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
<div
|
||||
class="asb-bar"
|
||||
class:selected
|
||||
class:noGap
|
||||
class:cursor-pointer={!selected && !$$slots.default}
|
||||
class:cursor-default={selected || $$slots.default}
|
||||
{title}
|
||||
@@ -91,7 +94,7 @@
|
||||
{/if}
|
||||
</svg>
|
||||
<div class="asb-label__container" class:selected class:disabled={!$$slots.default}>
|
||||
<div class="overflow-label">
|
||||
<div class="overflow-label" style={fontColor ? `color: ${fontColor};` : ''}>
|
||||
{#if $$slots.default}
|
||||
<slot />
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.646.146a.5.5 0 01.708 0l2 2a.5.5 0 01-.708.708L8.5 1.707V5.5a.5.5 0 01-1 0V1.707L6.354 2.854a.5.5 0 11-.708-.708l2-2zM8 10a.5.5 0 01.5.5v3.793l1.146-1.147a.5.5 0 01.708.708l-2 2a.5.5 0 01-.708 0l-2-2a.5.5 0 01.708-.708L7.5 14.293V10.5A.5.5 0 018 10zM.146 8.354a.5.5 0 010-.708l2-2a.5.5 0 11.708.708L1.707 7.5H5.5a.5.5 0 010 1H1.707l1.147 1.146a.5.5 0 01-.708.708l-2-2zM10 8a.5.5 0 01.5-.5h3.793l-1.147-1.146a.5.5 0 01.708-.708l2 2a.5.5 0 010 .708l-2 2a.5.5 0 01-.708-.708L14.293 8.5H10.5A.5.5 0 0110 8z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -19,15 +19,22 @@ export interface DrawingData {
|
||||
|
||||
export interface DrawingProps {
|
||||
readonly: boolean
|
||||
autoSize?: boolean
|
||||
imageWidth?: number
|
||||
imageHeight?: number
|
||||
drawingData: DrawingData
|
||||
drawingTool?: DrawingTool
|
||||
commandCount?: number
|
||||
commands: DrawingCmd[]
|
||||
offset?: Point
|
||||
tool?: DrawingTool
|
||||
penColor?: string
|
||||
changed?: (content: string) => void
|
||||
penWidth?: number
|
||||
eraserWidth?: number
|
||||
defaultCursor?: string
|
||||
cmdAdded?: (cmd: DrawingCmd) => void
|
||||
panned?: (offset: Point) => void
|
||||
}
|
||||
|
||||
interface DrawCmd {
|
||||
export interface DrawingCmd {
|
||||
lineWidth: number
|
||||
erasing: boolean
|
||||
penColor: string
|
||||
@@ -52,6 +59,7 @@ class DrawState {
|
||||
penWidth = 4
|
||||
eraserWidth = 30
|
||||
minLineLength = 6
|
||||
center: Point = { x: 0, y: 0 }
|
||||
offset: Point = { x: 0, y: 0 }
|
||||
points: Point[] = []
|
||||
scale: Point = { x: 1, y: 1 }
|
||||
@@ -71,8 +79,8 @@ class DrawState {
|
||||
|
||||
addPoint = (mouseX: number, mouseY: number): void => {
|
||||
this.points.push({
|
||||
x: mouseX * this.scale.x - this.offset.x,
|
||||
y: mouseY * this.scale.y - this.offset.y
|
||||
x: mouseX * this.scale.x - this.offset.x - this.center.x,
|
||||
y: mouseY * this.scale.y - this.offset.y - this.center.y
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,6 +94,8 @@ class DrawState {
|
||||
this.addPoint(x, y)
|
||||
}
|
||||
const erasing = this.tool === 'erase'
|
||||
this.ctx.save()
|
||||
this.ctx.translate(this.offset.x + this.center.x, this.offset.y + this.center.y)
|
||||
this.ctx.beginPath()
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.strokeStyle = this.penColor
|
||||
@@ -97,10 +107,13 @@ class DrawState {
|
||||
this.drawSmoothSegment(this.points, this.points.length - 1, lastPoint)
|
||||
this.ctx.stroke()
|
||||
}
|
||||
this.ctx.restore()
|
||||
})
|
||||
}
|
||||
|
||||
drawCommand = (cmd: DrawCmd): void => {
|
||||
drawCommand = (cmd: DrawingCmd): void => {
|
||||
this.ctx.save()
|
||||
this.ctx.translate(this.offset.x + this.center.x, this.offset.y + this.center.y)
|
||||
this.ctx.beginPath()
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.strokeStyle = cmd.penColor
|
||||
@@ -114,6 +127,7 @@ class DrawState {
|
||||
}
|
||||
this.ctx.stroke()
|
||||
}
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
drawPoint = (p: Point, erasing: boolean): void => {
|
||||
@@ -121,7 +135,7 @@ class DrawState {
|
||||
if (!erasing) {
|
||||
// Single point looks too small compared to a line of the same width
|
||||
// So make it a bit biggers
|
||||
r *= 1.5
|
||||
r *= 1 / r + 1
|
||||
}
|
||||
this.ctx.lineWidth = 0
|
||||
this.ctx.fillStyle = this.ctx.strokeStyle
|
||||
@@ -152,15 +166,13 @@ class DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
if (
|
||||
props.imageWidth === undefined ||
|
||||
props.imageHeight === undefined ||
|
||||
node.clientWidth === undefined ||
|
||||
node.clientHeight === undefined
|
||||
) {
|
||||
export function drawing (
|
||||
node: HTMLElement,
|
||||
props: DrawingProps
|
||||
): { canvas?: HTMLCanvasElement, update?: (props: DrawingProps) => void } {
|
||||
if (props.autoSize !== true && (props.imageWidth === undefined || props.imageHeight === undefined)) {
|
||||
console.error('Failed to create drawing: image size is not specified')
|
||||
return
|
||||
return {}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
@@ -169,43 +181,62 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
canvas.style.top = '0'
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.width = props.imageWidth
|
||||
canvas.height = props.imageHeight
|
||||
canvas.width = props.imageWidth ?? 0
|
||||
canvas.height = props.imageHeight ?? 0
|
||||
node.appendChild(canvas)
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx === null) {
|
||||
console.error('Failed to create drawing: unable to get 2d canvas context')
|
||||
node.removeChild(canvas)
|
||||
return
|
||||
return {}
|
||||
}
|
||||
|
||||
const canvasCursor = document.createElement('div')
|
||||
canvasCursor.style.visibility = 'hidden'
|
||||
canvasCursor.style.position = 'absolute'
|
||||
canvasCursor.style.borderRadius = '50%'
|
||||
canvasCursor.style.border = 'none'
|
||||
canvasCursor.style.cursor = 'none'
|
||||
canvasCursor.style.pointerEvents = 'none'
|
||||
canvasCursor.style.left = '50%'
|
||||
canvasCursor.style.top = '50%'
|
||||
node.appendChild(canvasCursor)
|
||||
|
||||
let readonly = props.readonly ?? false
|
||||
let prevPos: Point = { x: 0, y: 0 }
|
||||
|
||||
const draw = new DrawState(ctx)
|
||||
draw.tool = props.drawingTool ?? 'pan'
|
||||
draw.penColor = props.penColor ?? 'blue'
|
||||
draw.tool = props.tool ?? draw.tool
|
||||
draw.penColor = props.penColor ?? draw.penColor
|
||||
draw.penWidth = props.penWidth ?? draw.penWidth
|
||||
draw.eraserWidth = props.eraserWidth ?? draw.eraserWidth
|
||||
updateCanvasCursor()
|
||||
|
||||
let commands: DrawCmd[] = []
|
||||
let drawingData = props.drawingData
|
||||
parseData()
|
||||
let commands = props.commands
|
||||
let commandCount = props.commandCount ?? 0
|
||||
replayCommands({
|
||||
offset: {
|
||||
x: props.offset?.x ?? 0,
|
||||
y: props.offset?.y ?? 0
|
||||
}
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === canvas) {
|
||||
draw.scale = {
|
||||
x: canvas.width / entry.contentRect.width,
|
||||
y: canvas.height / entry.contentRect.height
|
||||
if (props.autoSize === true) {
|
||||
draw.scale = { x: 1, y: 1 }
|
||||
canvas.width = Math.floor(entry.contentRect.width)
|
||||
canvas.height = Math.floor(entry.contentRect.height)
|
||||
draw.center.x = canvas.width / 2
|
||||
draw.center.y = canvas.height / 2
|
||||
replayCommands({ offset: draw.offset })
|
||||
} else {
|
||||
draw.scale = {
|
||||
x: canvas.width / entry.contentRect.width,
|
||||
y: canvas.height / entry.contentRect.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,9 +286,18 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: if (draw.tool === 'pan')
|
||||
// Currently we only show drawing over attached images
|
||||
// Their sizes are fixed and no pan required
|
||||
if (draw.on && draw.tool === 'pan') {
|
||||
requestAnimationFrame(() => {
|
||||
replayCommands({
|
||||
offset: {
|
||||
x: draw.offset.x + x - prevPos.x,
|
||||
y: draw.offset.y + y - prevPos.y
|
||||
}
|
||||
})
|
||||
prevPos = { x, y }
|
||||
props.panned?.(draw.offset)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
canvas.onpointerup = (e) => {
|
||||
@@ -288,30 +328,31 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
}
|
||||
|
||||
function storeCommand (): void {
|
||||
if (draw.points.length > 1) {
|
||||
if (draw.points.length > 0) {
|
||||
const erasing = draw.tool === 'erase'
|
||||
const cmd: DrawCmd = {
|
||||
const cmd: DrawingCmd = {
|
||||
lineWidth: (erasing ? draw.eraserWidth : draw.penWidth) * draw.lineScale(),
|
||||
erasing,
|
||||
penColor: draw.penColor,
|
||||
points: draw.points
|
||||
}
|
||||
commands.push(cmd)
|
||||
props.changed?.(JSON.stringify(commands))
|
||||
props.cmdAdded?.(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
function updateCanvasCursor (): void {
|
||||
if (readonly) {
|
||||
canvasCursor.style.visibility = 'hidden'
|
||||
canvas.style.cursor = 'default'
|
||||
canvas.style.cursor = props.defaultCursor ?? 'default'
|
||||
} else if (draw.isDrawingTool()) {
|
||||
canvas.style.cursor = 'none'
|
||||
const erasing = draw.tool === 'erase'
|
||||
const w = draw.cursorWidth()
|
||||
canvasCursor.style.background = erasing ? 'none' : draw.penColor
|
||||
canvasCursor.style.border = erasing ? '1px solid #333' : 'none'
|
||||
canvasCursor.style.boxShadow = erasing ? '0px 0px 0px 1px #eee inset' : 'none'
|
||||
canvasCursor.style.boxShadow = erasing
|
||||
? '0px 0px 1px 1px white inset, 0px 0px 2px 1px black'
|
||||
: '0px 0px 3px 0px var(--theme-button-contrast-enabled)'
|
||||
canvasCursor.style.width = `${w}px`
|
||||
canvasCursor.style.height = `${w}px`
|
||||
} else if (draw.tool === 'pan') {
|
||||
@@ -328,43 +369,54 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
draw.offset = { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
function replayCommands (): void {
|
||||
draw.ctx.reset()
|
||||
for (const cmd of commands) {
|
||||
draw.drawCommand(cmd)
|
||||
function replayCommands ({ offset, startIndex }: { offset?: Point, startIndex?: number } = {}): void {
|
||||
if (startIndex === undefined || startIndex === 0) {
|
||||
clearCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
function parseData (): void {
|
||||
clearCanvas()
|
||||
if (drawingData.content !== undefined && drawingData.content !== null) {
|
||||
try {
|
||||
commands = JSON.parse(drawingData.content)
|
||||
replayCommands()
|
||||
} catch (error) {
|
||||
commands = []
|
||||
console.error('Failed to parse drawing content', error)
|
||||
}
|
||||
} else {
|
||||
commands = []
|
||||
if (offset !== undefined) {
|
||||
draw.offset = offset
|
||||
}
|
||||
for (let i = startIndex ?? 0; i < commands.length; i++) {
|
||||
draw.drawCommand(commands[i])
|
||||
}
|
||||
commandCount = commands.length
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
|
||||
update (props: DrawingProps) {
|
||||
if (drawingData !== props.drawingData) {
|
||||
drawingData = props.drawingData
|
||||
parseData()
|
||||
let replay = false
|
||||
let offset: Point | undefined
|
||||
let startIndex: number | undefined
|
||||
if (props.offset?.x !== draw.offset.x || props.offset?.y !== draw.offset.y) {
|
||||
offset = props.offset
|
||||
replay = true
|
||||
}
|
||||
if (commands !== props.commands) {
|
||||
commands = props.commands
|
||||
replay = true
|
||||
} else if (props.commandCount !== undefined && props.commandCount !== commandCount) {
|
||||
startIndex = commandCount
|
||||
replay = true
|
||||
}
|
||||
let updateCursor = false
|
||||
if (draw.tool !== props.drawingTool) {
|
||||
draw.tool = props.drawingTool ?? 'pen'
|
||||
if (draw.tool !== props.tool) {
|
||||
draw.tool = props.tool ?? 'pen'
|
||||
updateCursor = true
|
||||
}
|
||||
if (draw.penColor !== props.penColor) {
|
||||
draw.penColor = props.penColor ?? 'blue'
|
||||
updateCursor = true
|
||||
}
|
||||
if (draw.penWidth !== props.penWidth) {
|
||||
draw.penWidth = props.penWidth ?? 5
|
||||
updateCursor = true
|
||||
}
|
||||
if (draw.eraserWidth !== props.eraserWidth) {
|
||||
draw.eraserWidth = props.eraserWidth ?? 5
|
||||
updateCursor = true
|
||||
}
|
||||
if (props.readonly !== readonly) {
|
||||
readonly = props.readonly ?? false
|
||||
updateCursor = true
|
||||
@@ -372,6 +424,9 @@ export function drawing (node: HTMLElement, props: DrawingProps): any {
|
||||
if (updateCursor) {
|
||||
updateCanvasCursor()
|
||||
}
|
||||
if (replay) {
|
||||
replayCommands({ offset, startIndex })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,17 +173,18 @@ export function getFileUrl (file: string, filename?: string): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function uploadFile (file: File): Promise<Ref<PlatformBlob>> {
|
||||
const id = generateFileId()
|
||||
const params = getFileUploadParams(id, file)
|
||||
export async function uploadFile (file: File, uuid?: Ref<PlatformBlob>): Promise<Ref<PlatformBlob>> {
|
||||
uuid ??= generateFileId() as Ref<PlatformBlob>
|
||||
|
||||
const params = getFileUploadParams(uuid, file)
|
||||
|
||||
if (params.method === 'signed-url') {
|
||||
await uploadFileWithSignedUrl(file, id, params.url)
|
||||
await uploadFileWithSignedUrl(file, uuid, params.url)
|
||||
} else {
|
||||
await uploadFileWithFormData(file, id, params.url)
|
||||
await uploadFileWithFormData(file, uuid, params.url)
|
||||
}
|
||||
|
||||
return id as Ref<PlatformBlob>
|
||||
return uuid
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,7 @@ export { default as LiteMessageViewer } from './components/LiteMessageViewer.sve
|
||||
export { default as DownloadFileButton } from './components/DownloadFileButton.svelte'
|
||||
export { default as FileTypeIcon } from './components/FileTypeIcon.svelte'
|
||||
export { default as DrawingBoard } from './components/DrawingBoard.svelte'
|
||||
export { default as DrawingBoardToolbar } from './components/DrawingBoardToolbar.svelte'
|
||||
export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -123,7 +123,10 @@ export default plugin(presentationId, {
|
||||
ContentType: '' as IntlString,
|
||||
ContentTypeNotSupported: '' as IntlString,
|
||||
StartDrawing: '' as IntlString,
|
||||
DrawingHistory: '' as IntlString
|
||||
DrawingHistory: '' as IntlString,
|
||||
ColorAdd: '' as IntlString,
|
||||
ColorRemove: '' as IntlString,
|
||||
ColorReset: '' as IntlString
|
||||
},
|
||||
extension: {
|
||||
FilePreviewExtension: '' as ComponentExtensionId,
|
||||
|
||||
@@ -127,7 +127,7 @@ p:last-child { margin-block-end: 0; }
|
||||
.p-inline {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
// hyphens: auto;
|
||||
line-height: 150%;
|
||||
|
||||
&.contrast { color: var(--global-primary-TextColor); }
|
||||
|
||||
@@ -141,20 +141,38 @@
|
||||
}
|
||||
.antiPanel-navigator {
|
||||
position: relative;
|
||||
background-color: var(--theme-navpanel-color);
|
||||
|
||||
|
||||
&:not(.second) { background-color: var(--theme-navpanel-color); }
|
||||
&:not(.right) {
|
||||
min-width: 12.5rem;
|
||||
max-width: 22.5rem;
|
||||
width: 17.5rem;
|
||||
}
|
||||
&.right {
|
||||
border-radius: var(--medium-BorderRadius);
|
||||
}
|
||||
}
|
||||
&.right { border-radius: var(--medium-BorderRadius); }
|
||||
&.second.float {
|
||||
background-color: var(--theme-navpanel-color);
|
||||
filter: drop-shadow(2px 0 5px rgba(0, 0, 0, .2));
|
||||
z-index: 460;
|
||||
|
||||
&:not(.inner) {
|
||||
position: fixed;
|
||||
top: calc(var(--status-bar-height) + 3.5rem + 1px);
|
||||
height: calc(100% - var(--status-bar-height) - 3.5rem - 2px);
|
||||
border-left: 1px solid var(--theme-divider-color);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.antiPanel-navigator {
|
||||
&.portrait { left: 0; }
|
||||
&.landscape { left: var(--app-panel-width); }
|
||||
}
|
||||
|
||||
&.inner {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
left: 0;
|
||||
height: calc(100% - 3.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
&.fly:not(.second) {
|
||||
position: fixed;
|
||||
background-color: var(--theme-navpanel-color);
|
||||
z-index: 450;
|
||||
@@ -176,6 +194,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mobile-theme {
|
||||
.mobile-wrapper,
|
||||
|
||||
@@ -1493,6 +1493,10 @@
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
|
||||
&.noGap {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
&__back {
|
||||
padding: 1px 0.5px;
|
||||
height: calc(1.5rem + 2px);
|
||||
|
||||
@@ -326,6 +326,13 @@
|
||||
height: calc(100vh - 4rem);
|
||||
max-width: calc(100vw - 3rem);
|
||||
max-height: calc(100vh - 4rem);
|
||||
|
||||
.mobile-theme & {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getResourceC } from '@hcengineering/platform'
|
||||
import { afterUpdate, onMount } from 'svelte'
|
||||
|
||||
import { deviceOptionsStore as deviceInfo, resizeObserver } from '..'
|
||||
import { closePanel, PanelProps, panelstore } from '../panelup'
|
||||
import { fitPopupElement, popupstore } from '../popups'
|
||||
import { deviceOptionsStore as deviceInfo, resizeObserver } from '..'
|
||||
import type { AnySvelteComponent, PopupOptions, DeviceOptions } from '../types'
|
||||
import type { AnySvelteComponent, DeviceOptions, PopupOptions } from '../types'
|
||||
import Spinner from './Spinner.svelte'
|
||||
|
||||
export let contentPanel: HTMLElement | undefined
|
||||
@@ -51,7 +51,7 @@
|
||||
let keepSize: boolean = false
|
||||
|
||||
let props: PanelProps | undefined
|
||||
function _close () {
|
||||
function _close (): void {
|
||||
closePanel()
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
if ($panelstore.panel.component === undefined) {
|
||||
props = $panelstore.panel
|
||||
} else {
|
||||
getResource($panelstore.panel.component).then((r) => {
|
||||
getResourceC($panelstore.panel.component, (r) => {
|
||||
component = r
|
||||
props = $panelstore.panel
|
||||
})
|
||||
@@ -69,14 +69,14 @@
|
||||
props = undefined
|
||||
}
|
||||
|
||||
function escapeClose () {
|
||||
function escapeClose (): void {
|
||||
// Check if there is popup visible, then ignore
|
||||
if ($popupstore.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (componentInstance?.canClose) {
|
||||
if (!componentInstance.canClose()) {
|
||||
if (componentInstance?.canClose !== undefined) {
|
||||
if (!(componentInstance as { canClose: () => boolean }).canClose()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -84,20 +84,20 @@
|
||||
}
|
||||
|
||||
const fitPopup = (props: PanelProps, contentPanel: HTMLElement): void => {
|
||||
if (modalHTML) {
|
||||
if (modalHTML != null) {
|
||||
const device: DeviceOptions = $deviceInfo
|
||||
options = fitPopupElement(modalHTML, device, props.element, contentPanel)
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown (ev: KeyboardEvent) {
|
||||
function handleKeydown (ev: KeyboardEvent): void {
|
||||
if (ev.key === 'Escape') {
|
||||
escapeClose()
|
||||
}
|
||||
}
|
||||
|
||||
function _open (): void {
|
||||
if (modalHTML && props) {
|
||||
if (modalHTML != null && props != null) {
|
||||
if (props.element === 'content') {
|
||||
modalHTML.classList.add('bg')
|
||||
} else {
|
||||
@@ -107,28 +107,28 @@
|
||||
}
|
||||
|
||||
const _update = (): void => {
|
||||
if (props && contentPanel) {
|
||||
if (props != null && contentPanel != null) {
|
||||
fitPopup(props, contentPanel)
|
||||
}
|
||||
}
|
||||
|
||||
const checkResize = (el: Element) => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
const checkResize = (): void => {
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
})
|
||||
|
||||
$: if (contentPanel !== oldPanel) {
|
||||
oldPanel = contentPanel
|
||||
keepSize = false
|
||||
}
|
||||
$: if (props && contentPanel !== undefined) {
|
||||
$: if (props != null && contentPanel !== undefined) {
|
||||
fitPopup(props, contentPanel)
|
||||
if (!keepSize && props?.element === 'content') {
|
||||
keepSize = true
|
||||
@@ -138,26 +138,26 @@
|
||||
}
|
||||
|
||||
export function fitPopupInstance (): void {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:resize={() => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
}}
|
||||
on:keydown={(evt) => {
|
||||
if (props) handleKeydown(evt)
|
||||
if (props != null) handleKeydown(evt)
|
||||
}}
|
||||
on:beforeprint={() => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
}}
|
||||
on:afterprint={() => {
|
||||
if (props && contentPanel) fitPopup(props, contentPanel)
|
||||
if (props != null && contentPanel != null) fitPopup(props, contentPanel)
|
||||
}}
|
||||
/>
|
||||
{#if props}
|
||||
{#if !component}
|
||||
{#if !(component != null)}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<slot name="panel-header" />
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
export let shrink: boolean = false
|
||||
export let divScroll: HTMLElement | undefined | null = undefined
|
||||
export let divBox: HTMLElement | undefined | null = undefined
|
||||
export let scrollSnap: boolean = false
|
||||
export let checkForHeaders: boolean = false
|
||||
export let stickedScrollBars: boolean = false
|
||||
export let thinScrollBars: boolean = false
|
||||
@@ -571,6 +572,8 @@
|
||||
class="scroll relative flex-shrink flex-col"
|
||||
style:flex-direction={scrollDirection === 'vertical-reverse' ? 'column-reverse' : 'column'}
|
||||
class:disableOverscroll
|
||||
class:scrollSnapX={scrollSnap && contentDirection === 'horizontal'}
|
||||
class:scrollSnapY={scrollSnap && contentDirection === 'vertical'}
|
||||
style:overflow-x={horizontal ? 'auto' : 'hidden'}
|
||||
on:scroll={() => {
|
||||
if (onScroll) {
|
||||
@@ -879,6 +882,16 @@
|
||||
&.disableOverscroll {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
&.scrollSnapY {
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
&.scrollSnapX {
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
&.scrollSnapX,
|
||||
&.scrollSnapY {
|
||||
scroll-padding-inline: var(--spacing-1);
|
||||
}
|
||||
&::-webkit-scrollbar:vertical {
|
||||
display: none;
|
||||
width: 0;
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IconSize } from '@hcengineering/ui'
|
||||
import type { IconSize } from '../../types'
|
||||
|
||||
export let size: IconSize = 'small'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
+2
-1
@@ -13,7 +13,8 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { IconSize } from '@hcengineering/ui'
|
||||
import type { IconSize } from '../../types'
|
||||
|
||||
export let size: IconSize = 'small'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
@@ -140,7 +140,7 @@
|
||||
updateDeviceSize()
|
||||
|
||||
$: secondRow = checkAdaptiveMatching($deviceInfo.size, 'xs')
|
||||
$: asideFloat = $deviceInfo.navigator.float
|
||||
$: asideFloat = $deviceInfo.aside.float
|
||||
$: asideOpen = $deviceInfo.aside.visible
|
||||
$: appsMini =
|
||||
$deviceInfo.isMobile &&
|
||||
|
||||
@@ -242,6 +242,8 @@ export { default as IconCheckmark } from './components/icons/Checkmark.svelte'
|
||||
export { default as IconToDetails } from './components/icons/ToDetails.svelte'
|
||||
export { default as IconHistory } from './components/icons/History.svelte'
|
||||
export { default as IconScribble } from './components/icons/Scribble.svelte'
|
||||
export { default as IconMenuOpen } from './components/icons/MenuOpen.svelte'
|
||||
export { default as IconMenuClose } from './components/icons/MenuClose.svelte'
|
||||
|
||||
export { default as PanelInstance } from './components/PanelInstance.svelte'
|
||||
export { default as Panel } from './components/Panel.svelte'
|
||||
@@ -317,7 +319,7 @@ export const deviceOptionsStore = writable<DeviceOptions>({
|
||||
isPortrait: false,
|
||||
isMobile: false,
|
||||
navigator: { visible: true, float: false, direction: 'vertical' },
|
||||
aside: { visible: true },
|
||||
aside: { visible: true, float: false },
|
||||
fontSize: 0,
|
||||
size: null,
|
||||
sizes: { xs: false, sm: false, md: false, lg: false, xl: false, xxl: false },
|
||||
|
||||
@@ -11,7 +11,9 @@ export interface PanelProps {
|
||||
refit?: () => void
|
||||
}
|
||||
|
||||
export const panelstore = writable<{ panel?: PanelProps | undefined }>({ panel: undefined })
|
||||
export const panelstore = writable<{
|
||||
panel?: PanelProps | undefined
|
||||
}>({ panel: undefined })
|
||||
let currentLocation: string | undefined
|
||||
|
||||
export function getPanelURI (component: AnyComponent, _id: string, _class: string, element?: PopupAlignment): string {
|
||||
@@ -27,17 +29,18 @@ export function showPanel (
|
||||
_id: string,
|
||||
_class: string,
|
||||
element?: PopupAlignment,
|
||||
rightSection?: AnyComponent
|
||||
rightSection?: AnyComponent,
|
||||
doNavigate: boolean = true
|
||||
): void {
|
||||
openPanel(component, _id, _class, element, rightSection)
|
||||
const loc = getLocation()
|
||||
if (loc.fragment !== currentLocation) {
|
||||
if (doNavigate && loc.fragment !== currentLocation) {
|
||||
loc.fragment = currentLocation
|
||||
navigate(loc)
|
||||
}
|
||||
}
|
||||
|
||||
export function openPanel (
|
||||
function openPanel (
|
||||
component: AnyComponent,
|
||||
_id: string,
|
||||
_class: string,
|
||||
|
||||
@@ -381,7 +381,7 @@ export interface DeviceOptions {
|
||||
isPortrait: boolean
|
||||
isMobile: boolean
|
||||
navigator: { visible: boolean, float: boolean, direction: 'vertical' | 'horizontal' }
|
||||
aside: { visible: boolean }
|
||||
aside: { visible: boolean, float: boolean }
|
||||
fontSize: number
|
||||
size: WidthType | null
|
||||
sizes: Record<WidthType, boolean>
|
||||
|
||||
@@ -56,7 +56,6 @@ export async function translate (text: Markup, lang: string): Promise<TranslateR
|
||||
|
||||
export async function connectMeeting (
|
||||
roomId: Ref<Room>,
|
||||
sid: string,
|
||||
language: RoomLanguage,
|
||||
options: Partial<ConnectMeetingRequest>
|
||||
): Promise<void> {
|
||||
@@ -68,7 +67,7 @@ export async function connectMeeting (
|
||||
}
|
||||
|
||||
try {
|
||||
const req: ConnectMeetingRequest = { roomId, roomSid: sid, transcription: options.transcription ?? false, language }
|
||||
const req: ConnectMeetingRequest = { roomId, transcription: options.transcription ?? false, language }
|
||||
await fetch(concatLink(url, 'love/connect'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -61,7 +61,6 @@ export interface TranslateResponse {
|
||||
|
||||
export interface ConnectMeetingRequest {
|
||||
roomId: Ref<Room>
|
||||
roomSid: string
|
||||
language: RoomLanguage
|
||||
transcription: boolean
|
||||
}
|
||||
@@ -74,7 +73,6 @@ export interface PostTranscriptRequest {
|
||||
transcript: string
|
||||
participant: Ref<Person>
|
||||
roomName: string
|
||||
final: boolean
|
||||
}
|
||||
|
||||
export interface IdentityResponse {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</script>
|
||||
|
||||
{#if attachments.length}
|
||||
<Scroller contentDirection={'horizontal'} horizontal gap={'gap-3'}>
|
||||
<Scroller contentDirection={'horizontal'} horizontal gap={'gap-3'} scrollSnap>
|
||||
{#each attachments as attachment}
|
||||
<AttachmentPreview
|
||||
value={attachment}
|
||||
|
||||
@@ -109,5 +109,6 @@
|
||||
.content {
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -183,12 +183,10 @@ export function showAttachmentPreviewPopup (value: WithLookup<Attachment>): Popu
|
||||
content: data.content
|
||||
})
|
||||
const newDrawing = await client.findOne(attachment.class.Drawing, { _id: newId })
|
||||
if (newDrawing !== undefined) {
|
||||
return newDrawing
|
||||
} else {
|
||||
console.error('Unable to find just created drawing')
|
||||
return data
|
||||
if (newDrawing === undefined) {
|
||||
throw new Error('Unable to find just created drawing')
|
||||
}
|
||||
return newDrawing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import core, {
|
||||
SortingOrder,
|
||||
Status,
|
||||
TxOperations,
|
||||
generateId,
|
||||
makeCollaborativeDoc
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import recruit, { Applicant, Vacancy } from '@hcengineering/recruit'
|
||||
import task, { ProjectType, makeRank } from '@hcengineering/task'
|
||||
@@ -41,7 +40,7 @@ export async function createVacancy (
|
||||
{
|
||||
name,
|
||||
description: type.shortDescription ?? '',
|
||||
fullDescription: makeCollaborativeDoc(id, 'fullDescription'),
|
||||
fullDescription: null,
|
||||
private: false,
|
||||
archived: false,
|
||||
company,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs, Header } from '@hcengineering/ui'
|
||||
import { Breadcrumbs, Header, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import { CalendarMode } from '../index'
|
||||
@@ -43,6 +43,7 @@
|
||||
adaptive="autoExtra"
|
||||
doubleRowWidth={350}
|
||||
closeOnEscape={false}
|
||||
topIndent={$deviceInfo.navigator.float && !$deviceInfo.aside.float}
|
||||
on:close
|
||||
>
|
||||
<div class="title">
|
||||
|
||||
@@ -170,6 +170,7 @@
|
||||
class="antiPanel-navigator {$deviceInfo.navigator.direction === 'horizontal'
|
||||
? 'portrait'
|
||||
: 'landscape'} border-left"
|
||||
class:fly={$deviceInfo.navigator.float}
|
||||
>
|
||||
<div class="antiPanel-wrap__content hulyNavPanel-container">
|
||||
<ChatNavigator {object} {currentSpecial} on:select={handleChannelSelected} />
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
AttachedData,
|
||||
fillDefaults,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
makeCollabId,
|
||||
Ref,
|
||||
TxOperations,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { Card, getClient, InlineAttributeBar, updateMarkup } from '@hcengineering/presentation'
|
||||
import { EmptyMarkup } from '@hcengineering/text'
|
||||
import { Card, createMarkup, getClient, InlineAttributeBar } from '@hcengineering/presentation'
|
||||
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||
import { Button, createFocusManager, EditBox, FocusHandler, IconAttachment, IconInfo, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
|
||||
const object: Organization = {
|
||||
name: '',
|
||||
description: makeCollaborativeDoc(id, 'description'),
|
||||
attachments: 0
|
||||
} as unknown as Organization
|
||||
|
||||
@@ -59,8 +58,11 @@
|
||||
fillDefaults(hierarchy, object, contact.class.Organization)
|
||||
|
||||
async function createOrganization (): Promise<void> {
|
||||
if (!isEmptyMarkup(description)) {
|
||||
const target = makeCollabId(contact.class.Organization, id, 'description')
|
||||
object.description = await createMarkup(target, description)
|
||||
}
|
||||
const op = client.apply()
|
||||
await updateMarkup(object.description, { description })
|
||||
await op.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
|
||||
await descriptionBox.createAttachments(id, op)
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ import {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
CollaborativeDoc,
|
||||
Doc,
|
||||
Ref,
|
||||
Space,
|
||||
Timestamp,
|
||||
UXObject,
|
||||
type Blob,
|
||||
type MarkupBlobRef,
|
||||
type Data,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
@@ -137,7 +137,7 @@ export interface Member extends AttachedDoc {
|
||||
*/
|
||||
export interface Organization extends Contact {
|
||||
members: number
|
||||
description: CollaborativeDoc
|
||||
description: MarkupBlobRef | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,17 +17,8 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import {
|
||||
AttachedData,
|
||||
Class,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
getCurrentAccount,
|
||||
Mixin,
|
||||
Ref,
|
||||
SortingOrder
|
||||
} from '@hcengineering/core'
|
||||
import { Card, copyDocument, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { AttachedData, Class, generateId, getCurrentAccount, Mixin, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { createFocusManager, EditBox, FocusHandler } from '@hcengineering/ui'
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
import {
|
||||
@@ -36,10 +27,10 @@
|
||||
type DocumentCategory,
|
||||
type ChangeControl,
|
||||
type DocumentSpace,
|
||||
DocumentState,
|
||||
createControlledDocFromTemplate
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import { createControlledDocFromTemplate } from '../docutils'
|
||||
import documents from '../plugin'
|
||||
|
||||
export let documentClass: Ref<Class<ControlledDocument>> = documents.class.ControlledDocument
|
||||
@@ -78,7 +69,7 @@
|
||||
approvers: [],
|
||||
coAuthors: [],
|
||||
changeControl: '' as Ref<ChangeControl>,
|
||||
content: makeCollaborativeDoc(generateId())
|
||||
content: null
|
||||
}
|
||||
|
||||
let templateId: Ref<DocumentTemplate> | undefined = initTemplateId
|
||||
@@ -91,17 +82,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
await createControlledDocFromTemplate(
|
||||
client,
|
||||
templateId,
|
||||
id,
|
||||
object,
|
||||
space,
|
||||
undefined,
|
||||
undefined,
|
||||
documentClass,
|
||||
copyDocument
|
||||
)
|
||||
await createControlledDocFromTemplate(client, templateId, id, object, space, undefined, undefined, documentClass)
|
||||
|
||||
dispatch('close', id)
|
||||
}
|
||||
@@ -109,7 +90,7 @@
|
||||
const manager = createFocusManager()
|
||||
|
||||
$: if (templateId === undefined) {
|
||||
client
|
||||
void client
|
||||
.findOne(
|
||||
templateMixin,
|
||||
{ _class: documentClass, _id: { $nin: excludedTemplates ?? [] } },
|
||||
@@ -134,6 +115,7 @@
|
||||
object.template = template._id
|
||||
object.prefix = template.prefix
|
||||
object.category = template.category
|
||||
object.content = template.content
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -274,16 +274,18 @@
|
||||
$: canShowSidebar = $editorMode !== 'comparing'
|
||||
$: sideBar = canShowSidebar ? $availableRightPanelTabs : []
|
||||
|
||||
$: collaborativeDoc = $controlledDocument?.content
|
||||
$: initialCollaborativeDoc = $controlledDocumentTemplate?.content
|
||||
|
||||
$: workspace = $resolvedLocationStore.path[1].toUpperCase()
|
||||
|
||||
$: docReference = getDocReference($controlledDocument)
|
||||
$: templateReference = getDocReference($controlledDocumentTemplate)
|
||||
|
||||
$: attribute = {
|
||||
key: 'content',
|
||||
attr: client.getHierarchy().getAttribute(documents.class.ControlledDocument, 'content')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $controlledDocument !== null && collaborativeDoc !== undefined}
|
||||
{#if $controlledDocument !== null && attribute !== undefined}
|
||||
<Panel
|
||||
bind:innerWidth
|
||||
isHeader={false}
|
||||
@@ -430,13 +432,7 @@
|
||||
|
||||
<svelte:component this={DocumentTemplateFooter} slot="page-footer" {templateReference} />
|
||||
|
||||
<Collaboration
|
||||
{collaborativeDoc}
|
||||
{initialCollaborativeDoc}
|
||||
objectClass={$controlledDocument._class}
|
||||
objectId={$controlledDocument._id}
|
||||
objectAttr="content"
|
||||
>
|
||||
<Collaboration object={$controlledDocument} {attribute}>
|
||||
{#if $editorMode === 'comparing'}
|
||||
<DocumentDiffViewer />
|
||||
{:else}
|
||||
|
||||
+4
-6
@@ -18,13 +18,12 @@
|
||||
import {
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
makeCollaborativeDoc,
|
||||
type AttachedData,
|
||||
type Class,
|
||||
type Data,
|
||||
type Ref
|
||||
} from '@hcengineering/core'
|
||||
import { MessageBox, copyDocument, getClient } from '@hcengineering/presentation'
|
||||
import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
addNotification,
|
||||
@@ -43,10 +42,10 @@
|
||||
type DocumentTemplate,
|
||||
DocumentState,
|
||||
createChangeControl,
|
||||
createControlledDocFromTemplate,
|
||||
DEFAULT_PERIODIC_REVIEW_INTERVAL
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import { createControlledDocFromTemplate } from '../../docutils'
|
||||
import documents from '../../plugin'
|
||||
import { getProjectDocumentLink } from '../../navigation'
|
||||
import InfoStep from './steps/InfoStep.svelte'
|
||||
@@ -122,7 +121,7 @@
|
||||
state: DocumentState.Draft,
|
||||
snapshots: 0,
|
||||
changeControl: ccRecordId,
|
||||
content: makeCollaborativeDoc(generateId()),
|
||||
content: null,
|
||||
|
||||
requests: 0,
|
||||
reviewers: [],
|
||||
@@ -161,8 +160,7 @@
|
||||
_space,
|
||||
$locationStep.project,
|
||||
$locationStep.parent,
|
||||
documents.class.ControlledDocument,
|
||||
copyDocument
|
||||
documents.class.ControlledDocument
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
|
||||
+2
-3
@@ -32,8 +32,7 @@
|
||||
type Ref,
|
||||
type Mixin,
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
makeCollaborativeDoc
|
||||
getCurrentAccount
|
||||
} from '@hcengineering/core'
|
||||
import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
@@ -118,7 +117,7 @@
|
||||
state: DocumentState.Draft,
|
||||
snapshots: 0,
|
||||
changeControl: ccRecordId,
|
||||
content: makeCollaborativeDoc(generateId()),
|
||||
content: null,
|
||||
|
||||
requests: 0,
|
||||
reviewers: [],
|
||||
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { makeDocCollabId, type Doc } from '@hcengineering/core'
|
||||
import { CollaborationIds, type Ydoc } from '@hcengineering/text-editor'
|
||||
import {
|
||||
CollaborationDiffViewer,
|
||||
@@ -89,9 +89,8 @@
|
||||
}
|
||||
loading = true
|
||||
|
||||
const data = createTiptapCollaborationData({
|
||||
document: $compareTo.content
|
||||
})
|
||||
const compareToDoc = makeDocCollabId($compareTo, 'content')
|
||||
const data = createTiptapCollaborationData(compareToDoc, $compareTo.content)
|
||||
comparedYdoc = data.ydoc
|
||||
comparedProvider = data.provider
|
||||
void comparedProvider.loaded.then(() => (loading = false))
|
||||
|
||||
+9
-19
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||
import { merge } from 'effector'
|
||||
import { type CollaborativeDoc, type Ref, type Blob, generateId } from '@hcengineering/core'
|
||||
import { type Ref, type Blob, generateId } from '@hcengineering/core'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
@@ -40,7 +40,6 @@
|
||||
import {
|
||||
$areDocumentCommentPopupsOpened as areDocumentCommentPopupsOpened,
|
||||
$controlledDocument as controlledDocument,
|
||||
$controlledDocumentTemplate as controlledDocumentTemplate,
|
||||
$isEditable as isEditable,
|
||||
$documentCommentHighlightedLocation as documentCommentHighlightedLocation,
|
||||
$areDocumentCommentPopupsOpened as arePopupsOpened,
|
||||
@@ -67,16 +66,6 @@
|
||||
let editor: Editor
|
||||
let title = $controlledDocument?.title ?? ''
|
||||
|
||||
let collaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocument !== null) {
|
||||
collaborativeDoc = $controlledDocument.content
|
||||
}
|
||||
|
||||
let initialCollaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocumentTemplate !== null) {
|
||||
initialCollaborativeDoc = $controlledDocumentTemplate.content
|
||||
}
|
||||
|
||||
$: isTemplate =
|
||||
$controlledDocument != null && hierarchy.hasMixin($controlledDocument, documents.mixin.DocumentTemplate)
|
||||
|
||||
@@ -237,9 +226,14 @@
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
$: attribute = {
|
||||
key: 'content',
|
||||
attr: client.getHierarchy().getAttribute(documents.class.ControlledDocument, 'content')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $controlledDocument && collaborativeDoc}
|
||||
{#if $controlledDocument && attribute}
|
||||
<DocumentPrintTitlePage />
|
||||
|
||||
{#if headings.length > 0}
|
||||
@@ -270,14 +264,10 @@
|
||||
</DocumentTitle>
|
||||
<CollaboratorEditor
|
||||
bind:this={textEditor}
|
||||
objectId={$controlledDocument._id}
|
||||
objectClass={$controlledDocument._class}
|
||||
objectSpace={$controlledDocument.space}
|
||||
{collaborativeDoc}
|
||||
{initialCollaborativeDoc}
|
||||
object={$controlledDocument}
|
||||
{attribute}
|
||||
{user}
|
||||
readonly={!$isEditable}
|
||||
field="content"
|
||||
editorAttributes={{ style: 'padding: 0 2em; margin: 0 -2em;' }}
|
||||
overflow="none"
|
||||
canShowPopups={!$areDocumentCommentPopupsOpened}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user