mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 20:14:57 +02:00
Merge remote-tracking branch 'origin/develop'
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Document } from 'domhandler'
|
||||
import { Markup, Ref, generateId } from '@hcengineering/core'
|
||||
import { DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { Markup } from '@hcengineering/core'
|
||||
|
||||
import { GenericNodeSpec, NodeType, SectionSpec, SectionType, TocSectionSpec } from './types'
|
||||
import { AnyContainer, createNodeExtractor } from './nodes'
|
||||
@@ -11,7 +10,6 @@ import { AnyContainer, createNodeExtractor } from './nodes'
|
||||
* can be found in a document (TOC, history, etc.)
|
||||
*/
|
||||
export interface ExtractedSection {
|
||||
id: Ref<DocumentSection>
|
||||
type: SectionType
|
||||
title: string
|
||||
content: Markup
|
||||
@@ -25,7 +23,6 @@ export function extractSections (doc: Document, sectionSpecs: SectionSpec[]): Ex
|
||||
try {
|
||||
const extractedSection = sectionExtractor.extract(doc)
|
||||
sections.push({
|
||||
id: generateId(),
|
||||
type: section.type,
|
||||
title: extractedSection.getTitle(),
|
||||
content: extractedSection.getContent()
|
||||
|
||||
@@ -2,15 +2,12 @@ import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { getClient as getCollaboratorClient } from '@hcengineering/collaborator-client'
|
||||
import documents, {
|
||||
ChangeControl,
|
||||
CollaborativeDocumentSection,
|
||||
ControlledDocument,
|
||||
DEFAULT_PERIODIC_REVIEW_INTERVAL,
|
||||
DEFAULT_SECTION_TITLE,
|
||||
Document,
|
||||
DocumentCategory,
|
||||
DocumentState,
|
||||
DocumentTemplate,
|
||||
calcRank,
|
||||
createChangeControl,
|
||||
createControlledDocFromTemplate,
|
||||
createDocumentTemplate
|
||||
@@ -36,7 +33,6 @@ import { parseDocument } from 'htmlparser2'
|
||||
import { Config } from './config'
|
||||
import { ExtractedFile } from './extract/extract'
|
||||
import { ExtractedSection } from './extract/sections'
|
||||
import { compareStrExact } from './helpers'
|
||||
|
||||
export default async function importExtractedFile (
|
||||
ctx: MeasureContext,
|
||||
@@ -56,6 +52,10 @@ export default async function importExtractedFile (
|
||||
try {
|
||||
const docId = await createDocument(txops, extractedFile, config)
|
||||
const createdDoc = await txops.findOne(documents.class.Document, { _id: docId })
|
||||
if (createdDoc == null) {
|
||||
throw new Error(`Failed to obtain created document: ${docId}`)
|
||||
}
|
||||
|
||||
await createSections(ctx, txops, extractedFile, config, createdDoc)
|
||||
} finally {
|
||||
await txops.close()
|
||||
@@ -87,7 +87,6 @@ async function createDocument (
|
||||
major: 0,
|
||||
minor: 1,
|
||||
commentSequence: 0,
|
||||
sections: 0,
|
||||
template: templateId,
|
||||
state: DocumentState.Draft,
|
||||
requests: 0,
|
||||
@@ -185,10 +184,7 @@ async function createTemplateIfNotExist (
|
||||
prefix,
|
||||
data,
|
||||
category,
|
||||
owner,
|
||||
{
|
||||
title: DEFAULT_SECTION_TITLE
|
||||
}
|
||||
owner
|
||||
)
|
||||
if (!success) {
|
||||
throw new Error('Failed to create document template')
|
||||
@@ -206,95 +202,34 @@ async function createSections (
|
||||
txops: TxOperations,
|
||||
extractedFile: ExtractedFile,
|
||||
config: Config,
|
||||
doc?: Document
|
||||
doc: Document
|
||||
): Promise<void> {
|
||||
if (doc?.template == null) {
|
||||
throw new Error(`Invalid document: ${JSON.stringify(doc)}`)
|
||||
}
|
||||
|
||||
const h = txops.getHierarchy()
|
||||
|
||||
const { space, collaboratorApiURL, token, workspaceId } = config
|
||||
const { collaboratorApiURL, token, workspaceId } = config
|
||||
const collaborator = getCollaboratorClient(txops.getHierarchy(), workspaceId, token, collaboratorApiURL)
|
||||
|
||||
console.log('Creating document sections')
|
||||
console.log('Creating document content')
|
||||
|
||||
const collabId = doc.content
|
||||
|
||||
console.log(`Collab doc ID: ${collabId}`)
|
||||
|
||||
const docSections = await txops.findAll(documents.class.DocumentSection, { attachedTo: doc._id })
|
||||
const shouldMergeSections = docSections.some((s) => s.title !== DEFAULT_SECTION_TITLE)
|
||||
|
||||
try {
|
||||
let prevSection: { rank: string } | undefined
|
||||
let content: string = ''
|
||||
for (const section of extractedFile.sections) {
|
||||
if (section.type !== 'generic') {
|
||||
continue
|
||||
}
|
||||
|
||||
const existingSection = shouldMergeSections
|
||||
? docSections.find((s) => s.title !== DEFAULT_SECTION_TITLE && compareStrExact(s.title, section.title))
|
||||
: undefined
|
||||
await processImages(ctx, txops, section, config, doc)
|
||||
|
||||
// skipping sections that are not present in the document/template
|
||||
if (shouldMergeSections && existingSection == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingSection == null) {
|
||||
const sectionData: AttachedData<CollaborativeDocumentSection> = {
|
||||
title: section.title,
|
||||
rank: calcRank(prevSection, undefined),
|
||||
key: section.id,
|
||||
collaboratorSectionId: section.id
|
||||
}
|
||||
|
||||
console.log(`Creating section data: ${JSON.stringify(sectionData)}`)
|
||||
|
||||
await txops.addCollection(
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
space,
|
||||
doc._id,
|
||||
doc._class,
|
||||
'sections',
|
||||
sectionData,
|
||||
section.id
|
||||
)
|
||||
|
||||
prevSection = sectionData
|
||||
} else {
|
||||
prevSection = existingSection
|
||||
}
|
||||
|
||||
await processImages(ctx, txops, section, config)
|
||||
|
||||
const collabSectionId =
|
||||
existingSection != null && h.isDerived(existingSection._class, documents.class.CollaborativeDocumentSection)
|
||||
? (existingSection as CollaborativeDocumentSection).collaboratorSectionId
|
||||
: section.id
|
||||
|
||||
await collaborator.updateContent(collabId, collabSectionId, section.content)
|
||||
content += `<h1>${section.title}</h1>${section.content}`
|
||||
}
|
||||
|
||||
// deleting the default section if it was the only one and there were other sections added from the extracted doc
|
||||
// doing it after import, so that the trigger doesn't re-create a new empty section
|
||||
if (
|
||||
docSections.length === 1 &&
|
||||
docSections[0].title === DEFAULT_SECTION_TITLE &&
|
||||
extractedFile.sections.some((es) => es.type === 'generic')
|
||||
) {
|
||||
const defaultSection = docSections[0]
|
||||
|
||||
await txops.removeCollection(
|
||||
defaultSection._class,
|
||||
defaultSection.space,
|
||||
defaultSection._id,
|
||||
doc._id,
|
||||
doc._class,
|
||||
'sections'
|
||||
)
|
||||
}
|
||||
await collaborator.updateContent(collabId, 'content', content)
|
||||
} finally {
|
||||
// do nothing
|
||||
}
|
||||
@@ -304,12 +239,13 @@ export async function processImages (
|
||||
ctx: MeasureContext,
|
||||
txops: TxOperations,
|
||||
section: ExtractedSection,
|
||||
config: Config
|
||||
config: Config,
|
||||
doc: Document
|
||||
): Promise<void> {
|
||||
const dom = parseDocument(section.content)
|
||||
const imageNodes = findAll((n) => n.tagName === 'img', dom.children)
|
||||
|
||||
const { storageAdapter, workspaceId, uploadURL, space } = config
|
||||
const { storageAdapter, workspaceId, uploadURL } = config
|
||||
|
||||
const imageUploads = imageNodes.map(async (img) => {
|
||||
const src = img.attribs.src
|
||||
@@ -335,9 +271,9 @@ export async function processImages (
|
||||
const attachmentId: Ref<Attachment> = generateId()
|
||||
await txops.addCollection(
|
||||
attachment.class.Attachment,
|
||||
space,
|
||||
section.id,
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
doc.space,
|
||||
doc._id,
|
||||
doc._class,
|
||||
'attachments',
|
||||
{
|
||||
file: uuid as Ref<Blob>,
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@hcengineering/notification": "^0.6.23",
|
||||
"@hcengineering/model-notification": "^0.6.0",
|
||||
"@hcengineering/chunter": "^0.6.20",
|
||||
"@hcengineering/text-editor": "^0.6.0"
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/collaboration": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,17 +57,10 @@ import {
|
||||
TDocumentCategory,
|
||||
TControlledDocument,
|
||||
TChangeControl,
|
||||
TDocumentSection,
|
||||
TCollaborativeDocumentSection,
|
||||
TAttachmentsDocumentSection,
|
||||
TSequence,
|
||||
TDocumentRequest,
|
||||
TDocumentReviewRequest,
|
||||
TDocumentApprovalRequest,
|
||||
TDocumentSectionEditor,
|
||||
TDocumentSectionPresenter,
|
||||
TDocumentSectionCreator,
|
||||
TDocumentTemplateSection,
|
||||
TTypeDocumentState,
|
||||
TTypeControlledDocumentState,
|
||||
TDocumentComment
|
||||
@@ -97,20 +90,11 @@ export function createModel (builder: Builder): void {
|
||||
TDocumentCategory,
|
||||
TControlledDocument,
|
||||
TChangeControl,
|
||||
|
||||
TDocumentSection,
|
||||
TCollaborativeDocumentSection,
|
||||
TAttachmentsDocumentSection,
|
||||
|
||||
TSequence,
|
||||
|
||||
TDocumentRequest,
|
||||
TDocumentReviewRequest,
|
||||
TDocumentApprovalRequest,
|
||||
TDocumentSectionEditor,
|
||||
TDocumentSectionPresenter,
|
||||
TDocumentSectionCreator,
|
||||
TDocumentTemplateSection,
|
||||
|
||||
TTypeDocumentState,
|
||||
TTypeControlledDocumentState,
|
||||
@@ -413,39 +397,6 @@ export function createModel (builder: Builder): void {
|
||||
encode: documents.function.GetDocumentMetaLinkFragment
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.CollaborativeDocumentSection, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: documents.component.CollaborativeSectionPresenter
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.CollaborativeDocumentSection, core.class.Class, documents.mixin.DocumentSectionEditor, {
|
||||
editor: documents.component.CollaborativeSectionEditor
|
||||
})
|
||||
|
||||
builder.mixin(
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
core.class.Class,
|
||||
documents.mixin.DocumentSectionCreator,
|
||||
{
|
||||
creator: documents.function.CollaborativeSectionCreator
|
||||
}
|
||||
)
|
||||
|
||||
builder.mixin(documents.class.AttachmentsDocumentSection, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: documents.component.AttachmentsSectionPresenter
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.AttachmentsDocumentSection, core.class.Class, documents.mixin.DocumentSectionEditor, {
|
||||
editor: documents.component.AttachmentsSectionEditor
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.AttachmentsDocumentSection, core.class.Class, documents.mixin.DocumentSectionCreator, {
|
||||
creator: documents.function.AttachmentsSectionCreator
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.DocumentSection, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Open, print.action.Print, tracker.action.NewRelatedIssue]
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.Document, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: documents.component.DocumentPresenter
|
||||
})
|
||||
@@ -692,87 +643,6 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
)
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.AddCollaborativeSectionAbove,
|
||||
label: documents.string.AddSectionAbove,
|
||||
icon: documents.icon.ArrowUp,
|
||||
input: 'any',
|
||||
category: view.category.General,
|
||||
target: documents.class.CollaborativeDocumentSection,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'create'
|
||||
}
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.AddCollaborativeSectionBelow,
|
||||
label: documents.string.AddSectionBelow,
|
||||
icon: documents.icon.ArrowDown,
|
||||
input: 'any',
|
||||
category: view.category.General,
|
||||
target: documents.class.CollaborativeDocumentSection,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'create'
|
||||
}
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.Duplicate,
|
||||
label: documents.string.Duplicate,
|
||||
icon: documents.icon.Duplicate,
|
||||
input: 'any',
|
||||
category: view.category.General,
|
||||
target: documents.class.DocumentSection,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'edit'
|
||||
}
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.DeleteCollaborativeSection,
|
||||
label: view.string.Delete,
|
||||
icon: view.icon.Delete,
|
||||
keyBinding: ['Meta + Backspace'],
|
||||
category: view.category.General,
|
||||
input: 'any',
|
||||
target: documents.class.DocumentSection,
|
||||
context: { mode: ['context', 'browser'], group: 'edit' },
|
||||
override: [view.action.Delete]
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.EditDescription,
|
||||
label: documents.string.EditDescription,
|
||||
icon: documents.icon.EditDescription,
|
||||
input: 'any',
|
||||
category: view.category.General,
|
||||
target: documents.mixin.DocumentTemplateSection,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
})
|
||||
|
||||
createAction(builder, {
|
||||
action: documents.actionImpl.EditGuidance,
|
||||
label: documents.string.EditGuidance,
|
||||
icon: documents.icon.EditGuidance,
|
||||
input: 'any',
|
||||
category: view.category.General,
|
||||
target: documents.mixin.DocumentTemplateSection,
|
||||
context: {
|
||||
mode: ['context', 'browser'],
|
||||
group: 'tools'
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.CollaborativeDocumentSection, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [view.action.Delete]
|
||||
})
|
||||
|
||||
builder.mixin(documents.class.DocumentSpace, core.class.Class, view.mixin.IgnoreActions, {
|
||||
actions: [tracker.action.EditRelatedTargets]
|
||||
})
|
||||
@@ -980,7 +850,6 @@ export function defineNotifications (builder: Builder): void {
|
||||
'author',
|
||||
'content',
|
||||
'labels',
|
||||
'sections',
|
||||
'abstract',
|
||||
'snapshots',
|
||||
'requests',
|
||||
|
||||
@@ -2,10 +2,23 @@
|
||||
// Copyright @ 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import { type Data, type Ref, TxOperations, generateId, DOMAIN_TX, getCollaborativeDoc } from '@hcengineering/core'
|
||||
import {
|
||||
type Data,
|
||||
type Ref,
|
||||
TxOperations,
|
||||
generateId,
|
||||
DOMAIN_TX,
|
||||
getCollaborativeDoc,
|
||||
MeasureMetricsContext,
|
||||
type Class,
|
||||
type Doc,
|
||||
SortingOrder
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
createDefaultSpace,
|
||||
createOrUpdate,
|
||||
type MigrateUpdate,
|
||||
type MigrationDocumentQuery,
|
||||
tryMigrate,
|
||||
tryUpgrade,
|
||||
type MigrateOperation,
|
||||
@@ -23,8 +36,18 @@ import {
|
||||
type ControlledDocument,
|
||||
createChangeControl
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
loadCollaborativeDoc,
|
||||
saveCollaborativeDoc,
|
||||
YXmlElement,
|
||||
YXmlText,
|
||||
YAbstractType,
|
||||
clone
|
||||
} from '@hcengineering/collaboration'
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
|
||||
|
||||
import documents from './index'
|
||||
import documents, { DOMAIN_DOCUMENTS } from './index'
|
||||
|
||||
async function createTemplatesSpace (tx: TxOperations): Promise<void> {
|
||||
const existingSpace = await tx.findOne(documents.class.DocumentSpace, {
|
||||
@@ -73,39 +96,6 @@ async function createQualityDocumentsSpace (tx: TxOperations): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function fixChangeControlsForDocs (tx: TxOperations): Promise<void> {
|
||||
const defaultCCSpec: Data<ChangeControl> = {
|
||||
description: '',
|
||||
reason: '',
|
||||
impact: '',
|
||||
impactedDocuments: []
|
||||
}
|
||||
const controlledDocuments = await tx.findAll(
|
||||
documents.class.ControlledDocument,
|
||||
{},
|
||||
{ lookup: { changeControl: documents.class.ChangeControl } }
|
||||
)
|
||||
|
||||
for (const cdoc of controlledDocuments) {
|
||||
const existingCC = await tx.findOne(documents.class.ChangeControl, { _id: cdoc.changeControl })
|
||||
|
||||
if (existingCC !== undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const newCc = await tx.createDoc(
|
||||
documents.class.ChangeControl,
|
||||
cdoc.space,
|
||||
defaultCCSpec,
|
||||
cdoc.changeControl?.length > 0 ? cdoc.changeControl : undefined
|
||||
)
|
||||
|
||||
if (cdoc.changeControl === undefined) {
|
||||
await tx.update(cdoc, { changeControl: newCc })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createProductChangeControlTemplate (tx: TxOperations): Promise<void> {
|
||||
const ccCategory = 'documents:category:DOC - CC' as Ref<DocumentCategory>
|
||||
const productChangeControlTemplate = await tx.findOne(documents.mixin.DocumentTemplate, {
|
||||
@@ -152,15 +142,10 @@ async function createProductChangeControlTemplate (tx: TxOperations): Promise<vo
|
||||
major: 0,
|
||||
minor: 1,
|
||||
state: DocumentState.Effective,
|
||||
sections: 0,
|
||||
commentSequence: 0,
|
||||
content: getCollaborativeDoc(generateId())
|
||||
},
|
||||
ccCategory,
|
||||
undefined,
|
||||
{
|
||||
title: 'Section 1'
|
||||
}
|
||||
ccCategory
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
@@ -284,12 +269,103 @@ async function migrateSpaceTypes (client: MigrationClient): Promise<void> {
|
||||
)
|
||||
}
|
||||
|
||||
async function migrateDocSections (client: MigrationClient): Promise<void> {
|
||||
const ctx = new MeasureMetricsContext('migrate_doc_sections', {})
|
||||
const storage = client.storageAdapter
|
||||
|
||||
const targetDocuments = await client.find<ControlledDocument>(DOMAIN_DOCUMENTS, {
|
||||
_class: documents.class.ControlledDocument
|
||||
})
|
||||
const attachmentsOps: { filter: MigrationDocumentQuery<Attachment>, update: MigrateUpdate<Attachment> }[] = []
|
||||
|
||||
for (const document of targetDocuments) {
|
||||
const targetSections: any = await client.find(
|
||||
DOMAIN_DOCUMENTS,
|
||||
{
|
||||
_class: 'documents:class:CollaborativeDocumentSection' as Ref<Class<Doc>>,
|
||||
attachedTo: document._id
|
||||
},
|
||||
{
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
|
||||
// Migrate sections headers + content
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(storage, client.workspaceId, document.content, ctx)
|
||||
if (ydoc === undefined) {
|
||||
ctx.error('collaborative document content not found', { document: document.title })
|
||||
continue
|
||||
}
|
||||
|
||||
if (ydoc.share.has('content')) {
|
||||
// Already migrated?
|
||||
continue
|
||||
}
|
||||
|
||||
const content = ydoc.getXmlFragment('content')
|
||||
|
||||
ydoc.transact((tr) => {
|
||||
for (const section of targetSections) {
|
||||
const sectionTemplate = section['documents:mixin:DocumentTemplateSection']
|
||||
const sectionNote = sectionTemplate?.description ?? sectionTemplate?.guidance
|
||||
const titleXml = new YXmlText()
|
||||
titleXml.insert(
|
||||
0,
|
||||
section.title,
|
||||
sectionNote !== undefined && sectionNote !== ''
|
||||
? { note: { kind: 'neutral', title: sectionNote } }
|
||||
: undefined
|
||||
)
|
||||
|
||||
const sectionContent = ydoc.getXmlFragment(section.collaboratorSectionId)
|
||||
const sectionTitle = new YXmlElement('heading')
|
||||
sectionTitle.setAttribute('level', 1 as any)
|
||||
sectionTitle.insert(0, [titleXml])
|
||||
|
||||
content.push([
|
||||
sectionTitle,
|
||||
...(sectionContent
|
||||
.toArray()
|
||||
.map((item) =>
|
||||
item instanceof YAbstractType ? (item instanceof YXmlElement ? clone(item) : item.clone()) : item
|
||||
) as any)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
await saveCollaborativeDoc(storage, client.workspaceId, document.content, ydoc, ctx)
|
||||
} catch (err) {
|
||||
ctx.error('error collaborative document content migration', { error: err, document: document.title })
|
||||
}
|
||||
|
||||
attachmentsOps.push({
|
||||
filter: {
|
||||
_class: attachment.class.Attachment,
|
||||
attachedTo: { $in: targetSections.map((s: any) => s._id) }
|
||||
},
|
||||
update: {
|
||||
attachedTo: document._id,
|
||||
attachedToClass: document._class
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (attachmentsOps.length > 0) {
|
||||
await client.bulk(DOMAIN_ATTACHMENT, attachmentsOps)
|
||||
}
|
||||
}
|
||||
|
||||
export const documentsOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, documentsId, [
|
||||
{
|
||||
state: 'migrateSpaceTypes',
|
||||
func: migrateSpaceTypes
|
||||
},
|
||||
{
|
||||
state: 'migrateDocSections',
|
||||
func: migrateDocSections
|
||||
}
|
||||
])
|
||||
},
|
||||
@@ -305,7 +381,6 @@ export const documentsOperation: MigrateOperation = {
|
||||
await createTemplateSequence(tx)
|
||||
await createTagCategories(tx)
|
||||
await createDocumentCategories(tx)
|
||||
await fixChangeControlsForDocs(tx)
|
||||
await createProductChangeControlTemplate(tx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,20 +26,14 @@ import { type TextActionVisibleFunction, type TextActionFunction } from '@hcengi
|
||||
|
||||
export default mergeIds(documentsId, documents, {
|
||||
component: {
|
||||
DocumentTemplateSectionPresenter: '' as AnyComponent,
|
||||
ContentSectionPresenter: '' as AnyComponent,
|
||||
AttachmentSectionPresenter: '' as AnyComponent,
|
||||
DocumentVersions: '' as AnyComponent,
|
||||
EditDocumentContent: '' as AnyComponent,
|
||||
EditDocumentAttachment: '' as AnyComponent,
|
||||
TemplateSectionPresenter: '' as AnyComponent,
|
||||
|
||||
// new model components
|
||||
CategoryPresenter: '' as AnyComponent,
|
||||
Categories: '' as AnyComponent,
|
||||
DocumentTemplates: '' as AnyComponent,
|
||||
CollaborativeSectionPresenter: '' as AnyComponent,
|
||||
AttachmentsSectionPresenter: '' as AnyComponent,
|
||||
StateFilterValuePresenter: '' as AnyComponent,
|
||||
ControlledStateFilterValuePresenter: '' as AnyComponent,
|
||||
|
||||
@@ -63,12 +57,6 @@ export default mergeIds(documentsId, documents, {
|
||||
IsCommentVisible: '' as Resource<TextActionVisibleFunction>
|
||||
},
|
||||
actionImpl: {
|
||||
AddCollaborativeSectionAbove: '' as ViewAction,
|
||||
AddCollaborativeSectionBelow: '' as ViewAction,
|
||||
DeleteCollaborativeSection: '' as ViewAction,
|
||||
Duplicate: '' as ViewAction,
|
||||
EditDescription: '' as ViewAction,
|
||||
EditGuidance: '' as ViewAction,
|
||||
CreateChildDocument: '' as ViewAction,
|
||||
CreateChildTemplate: '' as ViewAction,
|
||||
CreateDocument: '' as ViewAction,
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
|
||||
import request from '@hcengineering/request'
|
||||
import {
|
||||
type AttachmentsDocumentSection,
|
||||
type ChangeControl,
|
||||
type CollaborativeDocumentSection,
|
||||
type ControlledDocument,
|
||||
type ControlledDocumentState,
|
||||
type Document,
|
||||
@@ -26,17 +24,12 @@ import {
|
||||
type DocumentCategory,
|
||||
type DocumentRequest,
|
||||
type DocumentReviewRequest,
|
||||
type DocumentSection,
|
||||
type DocumentComment,
|
||||
type DocumentSectionCreator,
|
||||
type DocumentSectionEditor,
|
||||
type DocumentSectionPresenter,
|
||||
type DocumentSpace,
|
||||
type DocumentSpaceType,
|
||||
type DocumentSpaceTypeDescriptor,
|
||||
type DocumentState,
|
||||
type DocumentTemplate,
|
||||
type DocumentTemplateSection,
|
||||
type Sequence,
|
||||
type DocumentMeta,
|
||||
type ExternalSpace,
|
||||
@@ -54,10 +47,8 @@ import contact, { type Employee } from '@hcengineering/contact'
|
||||
import {
|
||||
DateRangeMode,
|
||||
IndexKind,
|
||||
type AttachedData,
|
||||
type Class,
|
||||
type Doc,
|
||||
type Markup,
|
||||
type Ref,
|
||||
type Timestamp,
|
||||
type Type,
|
||||
@@ -91,16 +82,14 @@ import attachment from '@hcengineering/model-attachment'
|
||||
import chunter, { TChatMessage } from '@hcengineering/model-chunter'
|
||||
import core, {
|
||||
TAttachedDoc,
|
||||
TClass,
|
||||
TDoc,
|
||||
TTypedSpace,
|
||||
TType,
|
||||
TSpaceTypeDescriptor,
|
||||
TSpaceType
|
||||
} from '@hcengineering/model-core'
|
||||
import { getEmbeddedLabel, type Resource } from '@hcengineering/platform'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import tags, { type TagReference } from '@hcengineering/tags'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
import training, { type Training, type TrainingRequest } from '@hcengineering/training'
|
||||
|
||||
import documents from './plugin'
|
||||
@@ -275,9 +264,6 @@ export class TDocument extends TDoc implements Document {
|
||||
@Prop(Collection(tags.class.TagReference), documents.string.Labels)
|
||||
labels?: CollectionSize<TagReference>
|
||||
|
||||
@Prop(Collection(documents.class.DocumentSection), documents.string.Sections)
|
||||
sections!: CollectionSize<DocumentSection>
|
||||
|
||||
@Prop(TypeString(), documents.string.MetaAbstract)
|
||||
@Index(IndexKind.FullText)
|
||||
abstract?: string
|
||||
@@ -291,6 +277,9 @@ export class TDocument extends TDoc implements Document {
|
||||
|
||||
@Prop(Collection(documents.class.DocumentSnapshot), documents.string.Snapshots)
|
||||
snapshots?: CollectionSize<DocumentSnapshot>
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: CollectionSize<Attachment>
|
||||
}
|
||||
|
||||
@Model(documents.class.HierarchyDocument, documents.class.Document)
|
||||
@@ -428,25 +417,6 @@ export class TChangeControl extends TDoc implements ChangeControl {
|
||||
impactedDocuments!: Ref<Document>[]
|
||||
}
|
||||
|
||||
@Model(documents.class.DocumentSection, core.class.AttachedDoc, DOMAIN_DOCUMENTS)
|
||||
@UX(documents.string.Section)
|
||||
export class TDocumentSection extends TAttachedDoc implements DocumentSection {
|
||||
@Prop(TypeString(), documents.string.Title)
|
||||
@Index(IndexKind.FullText)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeString(), documents.string.Rank)
|
||||
@Hidden()
|
||||
rank!: string
|
||||
|
||||
@Prop(TypeString(), documents.string.Key)
|
||||
@Hidden()
|
||||
key!: string
|
||||
|
||||
@Prop(TypeRef(documents.mixin.DocumentTemplateSection), documents.string.SectionTemplate)
|
||||
templateSectionId?: Ref<DocumentTemplateSection>
|
||||
}
|
||||
|
||||
@Model(documents.class.DocumentSnapshot, core.class.AttachedDoc, DOMAIN_DOCUMENTS)
|
||||
@UX(documents.string.Snapshot)
|
||||
export class TDocumentSnapshot extends TAttachedDoc implements DocumentSnapshot {
|
||||
@@ -459,9 +429,6 @@ export class TDocumentSnapshot extends TAttachedDoc implements DocumentSnapshot
|
||||
|
||||
@Prop(TypeDocumentState(), documents.string.Status)
|
||||
state?: DocumentState
|
||||
|
||||
@Prop(Collection(documents.class.DocumentSection), documents.string.Sections)
|
||||
sections!: CollectionSize<DocumentSection>
|
||||
}
|
||||
|
||||
@Model(documents.class.ControlledDocumentSnapshot, documents.class.DocumentSnapshot)
|
||||
@@ -473,9 +440,6 @@ export class TControlledDocumentSnapshot extends TDocumentSnapshot implements Co
|
||||
|
||||
@Model(documents.class.DocumentComment, chunter.class.ChatMessage)
|
||||
export class TDocumentComment extends TChatMessage implements DocumentComment {
|
||||
@Prop(TypeString(), documents.string.SectionKey)
|
||||
sectionKey?: string
|
||||
|
||||
@Prop(TypeString(), documents.string.ID)
|
||||
nodeId?: string
|
||||
|
||||
@@ -486,42 +450,6 @@ export class TDocumentComment extends TChatMessage implements DocumentComment {
|
||||
index?: number
|
||||
}
|
||||
|
||||
@Mixin(documents.mixin.DocumentTemplateSection, documents.class.DocumentSection)
|
||||
@UX(documents.string.SectionTemplate)
|
||||
export class TDocumentTemplateSection extends TDocumentSection implements DocumentTemplateSection {
|
||||
@Prop(TypeBoolean(), documents.string.Required)
|
||||
mandatory?: boolean
|
||||
|
||||
@Prop(TypeString(), documents.string.Description)
|
||||
@Index(IndexKind.FullText)
|
||||
description?: string
|
||||
|
||||
@Prop(TypeMarkup(), documents.string.Guidance)
|
||||
@Index(IndexKind.FullText)
|
||||
guidance?: Markup
|
||||
}
|
||||
|
||||
@Model(documents.class.CollaborativeDocumentSection, documents.class.DocumentSection)
|
||||
@UX(documents.string.CollaborativeSection)
|
||||
export class TCollaborativeDocumentSection extends TDocumentSection implements CollaborativeDocumentSection {
|
||||
@Prop(TypeString(), documents.string.CollaboratorSectionId)
|
||||
@Hidden()
|
||||
collaboratorSectionId!: string
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: CollectionSize<Attachment>
|
||||
}
|
||||
|
||||
@Model(documents.class.AttachmentsDocumentSection, documents.class.DocumentSection)
|
||||
@UX(documents.string.AttachmentsSection)
|
||||
export class TAttachmentsDocumentSection extends TDocumentSection implements AttachmentsDocumentSection {
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: CollectionSize<Attachment>
|
||||
|
||||
@Prop(TypeNumber(), documents.string.AttachmentsMax)
|
||||
maximum?: number
|
||||
}
|
||||
|
||||
@Model(documents.class.Sequence, core.class.Doc, DOMAIN_DOCUMENTS)
|
||||
export class TSequence extends TDoc implements Sequence {
|
||||
attachedTo!: Ref<Class<Doc>>
|
||||
@@ -540,27 +468,6 @@ export class TDocumentReviewRequest extends TDocumentRequest implements Document
|
||||
@UX(documents.string.DocumentApprovalRequest)
|
||||
export class TDocumentApprovalRequest extends TDocumentRequest implements DocumentApprovalRequest {}
|
||||
|
||||
@Mixin(documents.mixin.DocumentSectionEditor, core.class.Class)
|
||||
export class TDocumentSectionEditor extends TClass implements DocumentSectionEditor {
|
||||
editor!: AnyComponent
|
||||
}
|
||||
|
||||
@Mixin(documents.mixin.DocumentSectionPresenter, core.class.Class)
|
||||
export class TDocumentSectionPresenter extends TClass implements DocumentSectionPresenter {
|
||||
presenter!: AnyComponent
|
||||
}
|
||||
|
||||
@Mixin(documents.mixin.DocumentSectionCreator, core.class.Class)
|
||||
export class TDocumentSectionCreator extends TClass implements DocumentSectionCreator {
|
||||
creator!: Resource<
|
||||
(
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: DocumentSection
|
||||
) => AttachedData<DocumentSection>
|
||||
>
|
||||
}
|
||||
|
||||
@Mixin(documents.mixin.DocumentSpaceTypeData, documents.class.DocumentSpace)
|
||||
@UX(getEmbeddedLabel('Default Documents'), documents.icon.Document)
|
||||
export class TDocumentSpaceTypeData extends TDocumentSpace implements RolesAssignment {
|
||||
|
||||
@@ -34,7 +34,8 @@ import {
|
||||
type Ref,
|
||||
type Space,
|
||||
type Timestamp,
|
||||
type Tx
|
||||
type Tx,
|
||||
type TxCUD
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
ArrOf,
|
||||
@@ -216,6 +217,8 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext {
|
||||
|
||||
@Prop(TypeBoolean(), notification.string.Pinned)
|
||||
isPinned!: boolean
|
||||
|
||||
tx?: Ref<TxCUD<Doc>>
|
||||
}
|
||||
|
||||
@Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION)
|
||||
|
||||
@@ -186,6 +186,42 @@ export async function migrateNotificationsSpace (client: MigrationClient): Promi
|
||||
await client.deleteMany(DOMAIN_USER_NOTIFY, { _class: notification.class.BrowserNotification })
|
||||
}
|
||||
|
||||
export async function migrateDuplicateContexts (client: MigrationClient): Promise<void> {
|
||||
const personSpaces = await client.find<PersonSpace>(DOMAIN_SPACE, { _class: contact.class.PersonSpace }, {})
|
||||
|
||||
for (const space of personSpaces) {
|
||||
const contexts = await client.find<DocNotifyContext>(
|
||||
DOMAIN_DOC_NOTIFY,
|
||||
{ _class: notification.class.DocNotifyContext, space: space._id },
|
||||
{}
|
||||
)
|
||||
const toRemove = new Set<Ref<DocNotifyContext>>()
|
||||
const contextByUser = new Map<string, DocNotifyContext>()
|
||||
|
||||
for (const context of contexts) {
|
||||
const key = context.objectId + '.' + context.user
|
||||
const existContext = contextByUser.get(key)
|
||||
|
||||
if (existContext != null) {
|
||||
const existLastViewedTimestamp = existContext.lastViewedTimestamp ?? 0
|
||||
const newLastViewedTimestamp = context.lastViewedTimestamp ?? 0
|
||||
if (existLastViewedTimestamp > newLastViewedTimestamp) {
|
||||
toRemove.add(context._id)
|
||||
} else {
|
||||
toRemove.add(existContext._id)
|
||||
contextByUser.set(key, context)
|
||||
}
|
||||
} else {
|
||||
contextByUser.set(key, context)
|
||||
}
|
||||
}
|
||||
if (toRemove.size > 0) {
|
||||
await client.deleteMany(DOMAIN_DOC_NOTIFY, { _id: { $in: Array.from(toRemove) } })
|
||||
await client.deleteMany(DOMAIN_NOTIFICATION, { docNotifyContext: { $in: Array.from(toRemove) } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateSettings (client: MigrationClient): Promise<void> {
|
||||
await client.update(
|
||||
DOMAIN_PREFERENCE,
|
||||
@@ -343,6 +379,10 @@ export const notificationOperation: MigrateOperation = {
|
||||
{ objectSpace: core.space.Space }
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'migrate-duplicated-contexts-v1',
|
||||
func: migrateDuplicateContexts
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -13,15 +13,6 @@ import serverNotification from '@hcengineering/server-notification'
|
||||
export { serverDocumentsId } from '@hcengineering/server-controlled-documents/src/index'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverDocuments.trigger.OnCollaborativeSectionDeleted,
|
||||
txMatch: {
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx.objectClass': documents.class.CollaborativeDocumentSection,
|
||||
'tx._class': core.class.TxRemoveDoc
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverDocuments.trigger.OnDocDeleted,
|
||||
txMatch: {
|
||||
|
||||
@@ -340,4 +340,16 @@ export function createModel (builder: Builder): void {
|
||||
category: 100,
|
||||
index: 5
|
||||
})
|
||||
|
||||
builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, {
|
||||
action: textEditor.function.ConfigureNote,
|
||||
icon: textEditor.icon.Note,
|
||||
visibilityTester: textEditor.function.IsEditableNote,
|
||||
isActive: {
|
||||
name: 'note'
|
||||
},
|
||||
label: textEditor.string.Note,
|
||||
category: 110,
|
||||
index: 5
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ export default mergeIds(textEditorId, textEditor, {
|
||||
OpenImage: '' as Resource<TextActionFunction>,
|
||||
ExpandImage: '' as Resource<TextActionFunction>,
|
||||
MoreImageActions: '' as Resource<TextActionFunction>,
|
||||
ConfigureNote: '' as Resource<TextActionFunction>,
|
||||
|
||||
IsEditableTableActive: '' as Resource<TextActionVisibleFunction>,
|
||||
IsEditableNote: '' as Resource<TextActionVisibleFunction>,
|
||||
IsEditable: '' as Resource<TextActionVisibleFunction>,
|
||||
IsHeadingVisible: '' as Resource<TextActionVisibleFunction>
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
handleOkClick()
|
||||
} else if (event.key === 'Enter') {
|
||||
// ignore customized editable divs to not interrupt multiline behavior
|
||||
if (!target.isContentEditable) {
|
||||
if (!target.isContentEditable && target.nodeName !== 'TEXTAREA') {
|
||||
event.preventDefault()
|
||||
focusManager?.next(1)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export * from './markup/utils'
|
||||
export * from './nodes'
|
||||
export * from './ydoc'
|
||||
export * from './marks/code'
|
||||
export * from './marks/noteBase'
|
||||
export * from './markdown'
|
||||
export * from './markdown/serializer'
|
||||
export * from './markdown/parser'
|
||||
|
||||
@@ -31,6 +31,7 @@ import { TodoItemNode, TodoListNode } from '../nodes/todo'
|
||||
import { CodeBlockExtension, codeBlockOptions } from '../nodes'
|
||||
import { DefaultKit, DefaultKitOptions } from './default-kit'
|
||||
import { CodeExtension, codeOptions } from '../marks/code'
|
||||
import { NoteBaseExtension } from '../marks/noteBase'
|
||||
|
||||
const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
|
||||
|
||||
@@ -87,7 +88,8 @@ export const ServerKit = Extension.create<ServerKitOptions>({
|
||||
TodoItemNode,
|
||||
TodoListNode,
|
||||
ReferenceNode,
|
||||
NodeUuid
|
||||
NodeUuid,
|
||||
NoteBaseExtension
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Mark } from '@tiptap/core'
|
||||
import { getDataAttribute } from '../nodes'
|
||||
|
||||
export const name = 'note'
|
||||
export enum NoteKind {
|
||||
Neutral = 'neutral',
|
||||
Dangerous = 'dangerous',
|
||||
DangerousLight = 'dangerous-light',
|
||||
Warning = 'warning',
|
||||
WarningLight = 'warning-light',
|
||||
Positive = 'positive',
|
||||
PositiveLight = 'positive-light',
|
||||
Primary = 'primary',
|
||||
PrimaryLight = 'primary-light'
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
export interface Commands<ReturnType> {
|
||||
[name]: {
|
||||
setNote: (text: string, kind: NoteKind) => ReturnType
|
||||
unsetNote: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const NoteBaseExtension = Mark.create({
|
||||
name,
|
||||
|
||||
parseHTML () {
|
||||
return [
|
||||
{
|
||||
tag: `span[data-mark="${name}"]`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML ({ HTMLAttributes, mark }) {
|
||||
return [
|
||||
'span',
|
||||
{ ...HTMLAttributes, 'data-mark': this.name, class: `theme-text-editor-note-anchor ${mark.attrs.kind}` },
|
||||
0
|
||||
]
|
||||
},
|
||||
|
||||
addAttributes () {
|
||||
return {
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
kind: getDataAttribute('kind', { default: NoteKind.Neutral })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -27,8 +27,8 @@ import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model
|
||||
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
|
||||
|
||||
/** @public */
|
||||
export function getMarkup (editor: Editor): Markup {
|
||||
return jsonToMarkup(editor.getJSON() as MarkupNode)
|
||||
export function getMarkup (editor?: Editor): Markup {
|
||||
return jsonToMarkup(editor?.getJSON() as MarkupNode)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Attribute } from '@tiptap/core'
|
||||
*/
|
||||
export function getDataAttribute (
|
||||
name: string,
|
||||
options?: Omit<Attribute, 'parseHTML' | 'renderHTML'>
|
||||
options?: Partial<Omit<Attribute, 'parseHTML' | 'renderHTML'>>
|
||||
): Partial<Attribute> {
|
||||
const dataName = `data-${name}`
|
||||
|
||||
|
||||
@@ -238,6 +238,16 @@
|
||||
--theme-won-color: #34DB80;
|
||||
--theme-caret-color: #fff;
|
||||
|
||||
--theme-text-editor-note-anchor-bg-neutral: #2C2C2C; /* Gray, no saturation change needed */
|
||||
--theme-text-editor-note-anchor-bg-dangerous: #8F4040;
|
||||
--theme-text-editor-note-anchor-bg-dangerous-light: #8E6464;
|
||||
--theme-text-editor-note-anchor-bg-warning: #A88D4E;
|
||||
--theme-text-editor-note-anchor-bg-warning-light: #8A8666;
|
||||
--theme-text-editor-note-anchor-bg-positive: #596941;
|
||||
--theme-text-editor-note-anchor-bg-positive-light: #7B9589;
|
||||
--theme-text-editor-note-anchor-bg-primary: #688797;
|
||||
--theme-text-editor-note-anchor-bg-primary-light: #747C81;
|
||||
|
||||
--accent-bg-color: #27282b;
|
||||
--accent-shadow: rgb(0 0 0 / 10%) 0px 2px 4px;
|
||||
|
||||
@@ -487,6 +497,16 @@
|
||||
--theme-won-color: #34DB80; // Dark
|
||||
--theme-caret-color: #669AFF;
|
||||
|
||||
--theme-text-editor-note-anchor-bg-neutral: #F3F3F3;
|
||||
--theme-text-editor-note-anchor-bg-dangerous: #DF8D8B;
|
||||
--theme-text-editor-note-anchor-bg-dangerous-light: #EECECE;
|
||||
--theme-text-editor-note-anchor-bg-warning: #FDE5A4;
|
||||
--theme-text-editor-note-anchor-bg-warning-light: #FEF4D1;
|
||||
--theme-text-editor-note-anchor-bg-positive: #BED6AF;
|
||||
--theme-text-editor-note-anchor-bg-positive-light: #DEE9D9;
|
||||
--theme-text-editor-note-anchor-bg-primary: #AAC5E9;
|
||||
--theme-text-editor-note-anchor-bg-primary-light: #D5E5F5;
|
||||
|
||||
--accent-bg-color: #eff0f2; // HZ
|
||||
--accent-shadow: rgb(0 0 0 / 10%) 0px 2px 4px; // Dark
|
||||
|
||||
|
||||
@@ -317,6 +317,11 @@
|
||||
&.text-editor-highlighted-node-selected, &:hover {
|
||||
background-color: var(--text-editor-highlighted-node-warning-active-background-color);
|
||||
}
|
||||
|
||||
@media print {
|
||||
background-color: inherit !important;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-highlighted-node-delete {
|
||||
@@ -330,6 +335,65 @@
|
||||
color: var(--text-editor-highlighted-node-add-font-color)
|
||||
}
|
||||
|
||||
.text-editor-note-marker {
|
||||
padding-left: 0.25rem;
|
||||
color: transparent;
|
||||
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%20id%3D%22note%22%3E%3Crect%20width%3D%22256%22%20height%3D%22256%22%20fill%3D%22none%22%3E%3C%2Frect%3E%3Cline%20x1%3D%2296%22%20x2%3D%22160%22%20y1%3D%2296%22%20y2%3D%2296%22%20fill%3D%22none%22%20stroke%3D%22%23313131%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cline%20x1%3D%2296%22%20x2%3D%22160%22%20y1%3D%22128%22%20y2%3D%22128%22%20fill%3D%22none%22%20stroke%3D%22%23313131%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cline%20x1%3D%2296%22%20x2%3D%22128%22%20y1%3D%22160%22%20y2%3D%22160%22%20fill%3D%22none%22%20stroke%3D%22%23313131%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23313131%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%20d%3D%22M156.68629%2C216H48a8%2C8%2C0%2C0%2C1-8-8V48a8%2C8%2C0%2C0%2C1%2C8-8H208a8%2C8%2C0%2C0%2C1%2C8%2C8V156.68629a8%2C8%2C0%2C0%2C1-2.34315%2C5.65686l-51.3137%2C51.3137A8%2C8%2C0%2C0%2C1%2C156.68629%2C216Z%22%3E%3C%2Fpath%3E%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23313131%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%20points%3D%22215.277%20159.992%20160%20159.992%20160%20215.272%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E');
|
||||
background-repeat: no-repeat;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-dark .text-editor-note-marker {
|
||||
background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20256%22%20id%3D%22note%22%3E%3Crect%20width%3D%22256%22%20height%3D%22256%22%20fill%3D%22none%22%3E%3C%2Frect%3E%3Cline%20x1%3D%2296%22%20x2%3D%22160%22%20y1%3D%2296%22%20y2%3D%2296%22%20fill%3D%22none%22%20stroke%3D%22%23FDFDF7%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cline%20x1%3D%2296%22%20x2%3D%22160%22%20y1%3D%22128%22%20y2%3D%22128%22%20fill%3D%22none%22%20stroke%3D%22%23FDFDF7%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cline%20x1%3D%2296%22%20x2%3D%22128%22%20y1%3D%22160%22%20y2%3D%22160%22%20fill%3D%22none%22%20stroke%3D%22%23FDFDF7%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%3E%3C%2Fline%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23FDFDF7%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%20d%3D%22M156.68629%2C216H48a8%2C8%2C0%2C0%2C1-8-8V48a8%2C8%2C0%2C0%2C1%2C8-8H208a8%2C8%2C0%2C0%2C1%2C8%2C8V156.68629a8%2C8%2C0%2C0%2C1-2.34315%2C5.65686l-51.3137%2C51.3137A8%2C8%2C0%2C0%2C1%2C156.68629%2C216Z%22%3E%3C%2Fpath%3E%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23FDFDF7%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%228%22%20points%3D%22215.277%20159.992%20160%20159.992%20160%20215.272%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E');
|
||||
}
|
||||
|
||||
.theme-text-editor-note-anchor {
|
||||
&.neutral {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-neutral);
|
||||
}
|
||||
|
||||
&.dangerous {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-dangerous);
|
||||
}
|
||||
|
||||
&.dangerous-light {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-dangerous-light);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-warning);
|
||||
}
|
||||
|
||||
&.warning-light {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-warning-light);
|
||||
}
|
||||
|
||||
&.positive {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-positive);
|
||||
}
|
||||
|
||||
&.positive-light {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-positive-light);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-primary);
|
||||
}
|
||||
|
||||
&.primary-light {
|
||||
background-color: var(--theme-text-editor-note-anchor-bg-primary-light);
|
||||
}
|
||||
|
||||
@media print {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.text-editor-popup {
|
||||
background-color: var(--theme-comp-header-color);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
@@ -369,6 +369,10 @@
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
@media print {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
&__main, &__aside {
|
||||
height: 100%;
|
||||
@@ -538,6 +542,7 @@
|
||||
|
||||
@media print {
|
||||
border: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
// &.asideShown .popupPanel-body__main {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
removeNotificationHandler()
|
||||
if (closeTimeout && timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
'top-left': NotificationPosition.TopLeft,
|
||||
'top-right': NotificationPosition.TopRight
|
||||
}
|
||||
|
||||
const maxVisibleNotifications = 3
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
<div class="notifications">
|
||||
{#each Object.entries(positionByClassName) as [className, position]}
|
||||
<div class={className} style:z-index={9999}>
|
||||
{#each $store as notification (notification.id)}
|
||||
{#each $store.slice(0, maxVisibleNotifications) as notification (notification.id)}
|
||||
{#if notification.position === position}
|
||||
<Notification {notification} />
|
||||
{/if}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function addNotification (
|
||||
title,
|
||||
subTitle,
|
||||
severity,
|
||||
position: NotificationPosition.BottomRight,
|
||||
position: NotificationPosition.TopRight,
|
||||
component,
|
||||
closeTimeout,
|
||||
params
|
||||
|
||||
@@ -58,21 +58,6 @@
|
||||
<symbol id="arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M12.295 8.295L8.5 12.085V2.5C8.5 2.22386 8.27614 2 8 2C7.72386 2 7.5 2.22386 7.5 2.5V12.085L3.705 8.295L3 9L8 14L13 9L12.295 8.295Z"/>
|
||||
</symbol>
|
||||
<symbol id="duplicate" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3C5 1.89543 5.89543 1 7 1H12C13.1046 1 14 1.89543 14 3V10C14 11.1046 13.1046 12 12 12H7C5.89543 12 5 11.1046 5 10V3ZM7 2C6.44772 2 6 2.44772 6 3V10C6 10.5523 6.44772 11 7 11H12C12.5523 11 13 10.5523 13 10V3C13 2.44772 12.5523 2 12 2H7Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 5C3.44772 5 3 5.44772 3 6V13C3 13.5523 3.44772 14 4 14H9C9.55228 14 10 13.5523 10 13V12H11V13C11 14.1046 10.1046 15 9 15H4C2.89543 15 2 14.1046 2 13V6C2 4.89543 2.89543 4 4 4H5V5H4Z"/>
|
||||
</symbol>
|
||||
<symbol id="edit-description" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
<path d="M2 4C2 2.89543 2.89543 2 4 2H11C12.1046 2 13 2.89543 13 4L12 5V4C12 3.44772 11.5523 3 11 3H4C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H7L6 11H4C2.89543 11 2 10.1046 2 9V4Z" fill="black" fill-opacity="0.6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.7 7.09988L13.9 6.29988C13.5 5.89988 12.9 5.89988 12.5 6.29988L6 12.7999V14.9999H8.2L14.7 8.49988C15.1 8.09988 15.1 7.49988 14.7 7.09988ZM13.2 6.99988L14 7.79988L12.5 9.29988L11.7 8.49988L13.2 6.99988ZM7 13.9999V13.1999L11 9.19988L11.8 9.99988L7.8 13.9999H7Z" fill="black" fill-opacity="0.6"/>
|
||||
<path d="M4.5529 4.27638C4.67639 4.02939 4.97673 3.92928 5.22372 4.05277L5.67093 4.27638C5.79181 4.33682 5.9021 4.41227 6 4.49982C6.0979 4.41227 6.20819 4.33682 6.32907 4.27638L6.77628 4.05277C7.02327 3.92928 7.32361 4.02939 7.4471 4.27638C7.5706 4.52337 7.47049 4.82371 7.2235 4.9472L6.77628 5.17081C6.61073 5.25358 6.50477 5.42084 6.50005 5.60517L6.50011 5.61802V7.38195L6.50005 7.3948C6.50477 7.57914 6.61073 7.74639 6.77628 7.82917L7.2235 8.05277C7.47049 8.17627 7.5706 8.4766 7.4471 8.72359C7.32361 8.97058 7.02327 9.0707 6.77628 8.9472L6.32907 8.72359C6.20819 8.66315 6.0979 8.5877 6 8.50015C5.9021 8.5877 5.79181 8.66315 5.67093 8.72359L5.22372 8.9472C4.97673 9.0707 4.67639 8.97058 4.5529 8.72359C4.4294 8.4766 4.52951 8.17627 4.7765 8.05277L5.22372 7.82917C5.38927 7.74639 5.49523 7.57914 5.49995 7.3948L5.49989 7.38195V5.61802L5.49995 5.60517C5.49523 5.42084 5.38927 5.25358 5.22372 5.17081L4.7765 4.9472C4.52951 4.82371 4.4294 4.52337 4.5529 4.27638Z" fill="black" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
<symbol id="edit-guidance" viewBox="0 0 16 16" width="16" height="16" fill="none">
|
||||
<path d="M1 6.5C1 9.07663 2.77181 11.2394 5.16351 11.8365L6.02479 10.9752C3.76289 10.7378 2 8.82478 2 6.5C2 4.01472 4.01472 2 6.5 2C8.82478 2 10.7378 3.76289 10.9752 6.02479L11.8365 5.16351C11.2394 2.77181 9.07663 1 6.5 1C3.46243 1 1 3.46243 1 6.5Z" fill="black" fill-opacity="0.6"/>
|
||||
<path d="M6.08332 3.62628C6.20666 3.54386 6.35166 3.49988 6.5 3.49988C6.69891 3.49988 6.88968 3.5789 7.03033 3.71955C7.17098 3.8602 7.25 4.05097 7.25 4.24988C7.25 4.39821 7.20601 4.54322 7.1236 4.66656C7.04119 4.78989 6.92406 4.88602 6.78701 4.94279C6.64997 4.99955 6.49917 5.01441 6.35368 4.98547C6.2082 4.95653 6.07456 4.8851 5.96967 4.78021C5.86478 4.67532 5.79335 4.54168 5.76441 4.3962C5.73547 4.25071 5.75032 4.09991 5.80709 3.96287C5.86386 3.82582 5.95999 3.70869 6.08332 3.62628Z" fill="black" fill-opacity="0.6"/>
|
||||
<path d="M7 5.99988V8.49988H7.5C7.77614 8.49988 8 8.72373 8 8.99988C8 9.27602 7.77614 9.49988 7.5 9.49988H5.5C5.22386 9.49988 5 9.27602 5 8.99988C5 8.72373 5.22386 8.49988 5.5 8.49988H6V6.49988H5.5C5.22386 6.49988 5 6.27602 5 5.99988C5 5.72374 5.22386 5.49988 5.5 5.49988H6.5C6.77614 5.49988 7 5.72374 7 5.99988Z" fill="black" fill-opacity="0.6"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.7 7.09988L13.9 6.29988C13.5 5.89988 12.9 5.89988 12.5 6.29988L6 12.7999V14.9999H8.2L14.7 8.49988C15.1 8.09988 15.1 7.49988 14.7 7.09988ZM13.2 6.99988L14 7.79988L12.5 9.29988L11.7 8.49988L13.2 6.99988ZM7 13.9999V13.1999L11 9.19988L11.8 9.99988L7.8 13.9999H7Z" fill="black" fill-opacity="0.6"/>
|
||||
</symbol>
|
||||
<symbol id="configure" viewBox="0 0 32 32" fill="currentColor">
|
||||
<path d="M8.99976 1.99976C8.44747 1.99976 7.99976 2.44747 7.99976 2.99976V6.09976C5.69976 6.59976 3.99976 8.59976 3.99976 10.9998C3.99976 13.3998 5.69976 15.3998 7.99976 15.8998L7.99976 28.9998C7.99976 29.552 8.44747 29.9998 8.99976 29.9998C9.55204 29.9998 9.99976 29.552 9.99976 28.9998L9.99976 15.8998C12.2998 15.3998 13.9998 13.3998 13.9998 10.9998C13.9998 8.59976 12.2998 6.59976 9.99976 6.09976V2.99976C9.99976 2.44747 9.55204 1.99976 8.99976 1.99976ZM11.9998 10.9998C11.9998 12.6998 10.6998 13.9998 8.99976 13.9998C7.29976 13.9998 5.99976 12.6998 5.99976 10.9998C5.99976 9.29976 7.29976 7.99976 8.99976 7.99976C10.6998 7.99976 11.9998 9.29976 11.9998 10.9998Z"/>
|
||||
<path d="M22.9998 29.9998C23.552 29.9998 23.9998 29.552 23.9998 28.9998V25.8998C26.2998 25.3998 27.9998 23.3998 27.9998 20.9998C27.9998 18.5998 26.2998 16.5998 23.9998 16.0998V2.99976C23.9998 2.44747 23.552 1.99976 22.9998 1.99976C22.4475 1.99976 21.9998 2.44747 21.9998 2.99976V16.0998C19.6998 16.5998 17.9998 18.5998 17.9998 20.9998C17.9998 23.3998 19.6998 25.3998 21.9998 25.8998V28.9998C21.9998 29.552 22.4475 29.9998 22.9998 29.9998ZM19.9998 20.9998C19.9998 19.2998 21.2998 17.9998 22.9998 17.9998C24.6998 17.9998 25.9998 19.2998 25.9998 20.9998C25.9998 22.6998 24.6998 23.9998 22.9998 23.9998C21.2998 23.9998 19.9998 22.6998 19.9998 20.9998Z"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.7 KiB |
@@ -9,11 +9,7 @@
|
||||
"Requests": "Requests",
|
||||
"EffectiveDate": "Effective date",
|
||||
"PlannedEffectiveDate": "Planned effective date",
|
||||
"Section": "Section",
|
||||
"Rank": "Rang",
|
||||
"CollaborativeSection": "Collaborative section",
|
||||
"CollaboratorSectionId": "Collaborator section ID",
|
||||
"AttachmentsSection": "Attachments section",
|
||||
"DocumentRequest": "Request",
|
||||
"DocumentReviewRequest": "Document review request",
|
||||
"DocumentApprovalRequest": "Document approval request",
|
||||
@@ -22,7 +18,6 @@
|
||||
"Guidance": "Guidance",
|
||||
"Required": "Required",
|
||||
"Description": "Description",
|
||||
"SectionTemplate": "Section template",
|
||||
"Major": "Major",
|
||||
"Minor": "Minor",
|
||||
"Patch": "Patch",
|
||||
@@ -104,8 +99,6 @@
|
||||
"DocumentCodePlaceholder": "DOC-1",
|
||||
"DocumentPrefixPlaceholder": "DOC",
|
||||
"DocumentPrefix": "Documents prefix",
|
||||
"Sections": "Sections",
|
||||
"TemplateSectionTitle": "Title",
|
||||
"DocumentTemplateCreateLabel": "Template",
|
||||
"DocumentCategoryCreateLabel": "Category",
|
||||
"CreateDocumentCategory": "Create a category",
|
||||
@@ -114,6 +107,7 @@
|
||||
"AttachmentsMax": "Maximum allowed attachments",
|
||||
"Resolve": "Resolve",
|
||||
"Unresolve": "Unresolve",
|
||||
"Pending": "Pending",
|
||||
"Resolved": "Resolved",
|
||||
"ShowResolved": "Show resolved comments",
|
||||
"Ordering": "Ordering",
|
||||
@@ -152,12 +146,6 @@
|
||||
"ChangeControl": "Change Control",
|
||||
"ReviewInterval": "Review interval",
|
||||
|
||||
"DocTemplateDeleteSectionTitle": "Confirm deletion of section",
|
||||
"DocTemplateDeleteSectionConfirm": "Are you sure that you want to permanently remove section \"{section}\" from the template?",
|
||||
|
||||
"AddSectionAbove": "Add new section above",
|
||||
"AddSectionBelow": "Add new section below",
|
||||
|
||||
"SelectReviewers": "Select reviewers",
|
||||
"SelectApprovers": "Select approvers",
|
||||
"RequestsToReviewTheDoc": "requests you to review the document",
|
||||
@@ -179,8 +167,6 @@
|
||||
"InfoStepTitle": "Info",
|
||||
"TeamStepTitle": "Team",
|
||||
|
||||
"Duplicate": "Duplicate",
|
||||
|
||||
"TitleAndDescr": "Title and description",
|
||||
"Reason": "Reason",
|
||||
"AbstractPlaceholder": "What is this document about? Who will need it and when? ...",
|
||||
@@ -193,7 +179,6 @@
|
||||
"EditDocument": "Edit document",
|
||||
|
||||
"Key": "Key",
|
||||
"SectionKey": "Section key",
|
||||
"CommentsSequence": "Comments sequence",
|
||||
|
||||
"Email": "Email",
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
"Requests": "Demandes",
|
||||
"EffectiveDate": "Date d'effet",
|
||||
"PlannedEffectiveDate": "Date d'effet prévue",
|
||||
"Section": "Section",
|
||||
"Rank": "Rang",
|
||||
"CollaborativeSection": "Section collaborative",
|
||||
"CollaboratorSectionId": "ID de section du collaborateur",
|
||||
"AttachmentsSection": "Section des pièces jointes",
|
||||
"DocumentRequest": "Demande",
|
||||
"DocumentReviewRequest": "Demande de révision de document",
|
||||
"DocumentApprovalRequest": "Demande d'approbation de document",
|
||||
@@ -22,7 +18,6 @@
|
||||
"Guidance": "Orientation",
|
||||
"Required": "Requis",
|
||||
"Description": "Description",
|
||||
"SectionTemplate": "Modèle de section",
|
||||
"Major": "Majeur",
|
||||
"Minor": "Mineur",
|
||||
"Patch": "Correctif",
|
||||
@@ -95,8 +90,6 @@
|
||||
"DocumentCodePlaceholder": "DOC-1",
|
||||
"DocumentPrefixPlaceholder": "DOC",
|
||||
"DocumentPrefix": "Préfixe des documents",
|
||||
"Sections": "Sections",
|
||||
"TemplateSectionTitle": "Titre",
|
||||
"DocumentTemplateCreateLabel": "Modèle",
|
||||
"DocumentCategoryCreateLabel": "Catégorie",
|
||||
"CreateDocumentCategory": "Créer une catégorie",
|
||||
@@ -105,6 +98,7 @@
|
||||
"AttachmentsMax": "Nombre maximum de pièces jointes autorisé",
|
||||
"Resolve": "Résoudre",
|
||||
"Unresolve": "Non résolu",
|
||||
"Pending": "En attente",
|
||||
"Resolved": "Résolu",
|
||||
"ShowResolved": "Afficher les commentaires résolus",
|
||||
"Ordering": "Ordonnancement",
|
||||
@@ -137,10 +131,6 @@
|
||||
"CreateDraftQmsTemplates": "Créer des modèles QMS brouillons",
|
||||
"ChangeControl": "Contrôle des modifications",
|
||||
"ReviewInterval": "Intervalle de révision",
|
||||
"DocTemplateDeleteSectionTitle": "Confirmer la suppression de la section",
|
||||
"DocTemplateDeleteSectionConfirm": "Êtes-vous sûr de vouloir supprimer définitivement la section \"{section}\" du modèle ?",
|
||||
"AddSectionAbove": "Ajouter une nouvelle section au-dessus",
|
||||
"AddSectionBelow": "Ajouter une nouvelle section en dessous",
|
||||
"SelectReviewers": "Sélectionner les réviseurs",
|
||||
"SelectApprovers": "Sélectionner les approuveurs",
|
||||
"RequestsToReviewTheDoc": "vous demande de réviser le document",
|
||||
@@ -158,7 +148,6 @@
|
||||
"TemplateStepTitle": "Modèle",
|
||||
"InfoStepTitle": "Info",
|
||||
"TeamStepTitle": "Équipe",
|
||||
"Duplicate": "Dupliquer",
|
||||
"TitleAndDescr": "Titre et description",
|
||||
"Reason": "Raison",
|
||||
"AbstractPlaceholder": "De quoi parle ce document ? Qui en aura besoin et quand ? ...",
|
||||
@@ -169,7 +158,6 @@
|
||||
"ReasonPlaceholder": "Précisez la raison...",
|
||||
"EditDocument": "Modifier le document",
|
||||
"Key": "Clé",
|
||||
"SectionKey": "Clé de section",
|
||||
"CommentsSequence": "Séquence des commentaires",
|
||||
"Email": "E-mail",
|
||||
"Password": "Mot de passe",
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
"Requests": "Запросы",
|
||||
"EffectiveDate": "Дата выпуска",
|
||||
"PlannedEffectiveDate": "Планируемая дата выпуска",
|
||||
"Section": "Section",
|
||||
"Rank": "Ранг",
|
||||
"CollaborativeSection": "Коллаборативная секция",
|
||||
"CollaboratorSectionId": "ID коллаборационной секции",
|
||||
"AttachmentsSection": "Секция прикрепленных файлов",
|
||||
"DocumentRequest": "Запрос",
|
||||
"DocumentReviewRequest": "Запрос рецензии документа",
|
||||
"DocumentApprovalRequest": "Запрос утверждения документа",
|
||||
@@ -22,7 +18,6 @@
|
||||
"Guidance": "Руководство",
|
||||
"Required": "Обязательно",
|
||||
"Description": "Описание",
|
||||
"SectionTemplate": "Шаблон секции",
|
||||
"Major": "Мажорная",
|
||||
"Minor": "Минорная",
|
||||
"Patch": "Патч",
|
||||
@@ -104,8 +99,6 @@
|
||||
"DocumentCodePlaceholder": "ДОК-1",
|
||||
"DocumentPrefixPlaceholder": "ДОК",
|
||||
"DocumentPrefix": "Префикс документов",
|
||||
"Sections": "Разделы",
|
||||
"TemplateSectionTitle": "Заголовок",
|
||||
"DocumentTemplateCreateLabel": "Шалблон",
|
||||
"DocumentCategoryCreateLabel": "Категория",
|
||||
"CreateDocumentCategory": "Создать категорию документа",
|
||||
@@ -114,6 +107,7 @@
|
||||
"AttachmentsMax": "Максимальное число допустимых файлов",
|
||||
"Resolve": "Пометить выполненным",
|
||||
"Unresolve": "Пометить невыполненным",
|
||||
"Pending": "В ожидании",
|
||||
"Resolved": "Выполненный",
|
||||
"ShowResolved": "Показать выполненные",
|
||||
"Ordering": "Сортировка",
|
||||
@@ -152,12 +146,6 @@
|
||||
"ChangeControl": "Контроль Изменений",
|
||||
"ReviewInterval": "Интервал ревью",
|
||||
|
||||
"DocTemplateDeleteSectionTitle": "Подтверждение удаления секции",
|
||||
"DocTemplateDeleteSectionConfirm": "Вы действительно хотите навсегда удалить секцию \"{section}\" из шаблона?",
|
||||
|
||||
"AddSectionAbove": "Добавить новую секцию сверху",
|
||||
"AddSectionBelow": "Добавить новую секцию снизу",
|
||||
|
||||
"SelectReviewers": "Выберите рецензентов",
|
||||
"SelectApprovers": "Выберите утверждающих",
|
||||
"RequestsToReviewTheDoc": "запрашивает у вас рецензию на документ",
|
||||
@@ -179,8 +167,6 @@
|
||||
"InfoStepTitle": "Инфо",
|
||||
"TeamStepTitle": "Команда",
|
||||
|
||||
"Duplicate": "Дублировать",
|
||||
|
||||
"TitleAndDescr": "Заголовок и описание",
|
||||
"Reason": "Причина",
|
||||
"AbstractPlaceholder": "О чем этот документ? Кто будет его использовать и как? ...",
|
||||
@@ -193,7 +179,6 @@
|
||||
"EditDocument": "Редактировать документ",
|
||||
|
||||
"Key": "Ключ",
|
||||
"SectionKey": "Ключ секции",
|
||||
"CommentsSequence": "Последовательность комментариев",
|
||||
|
||||
"Email": "Email",
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
"Requests": "请求",
|
||||
"EffectiveDate": "生效日期",
|
||||
"PlannedEffectiveDate": "计划生效日期",
|
||||
"Section": "章节",
|
||||
"Rank": "等级",
|
||||
"CollaborativeSection": "协作章节",
|
||||
"CollaboratorSectionId": "协作章节 ID",
|
||||
"AttachmentsSection": "附件章节",
|
||||
"DocumentRequest": "请求",
|
||||
"DocumentReviewRequest": "文档审查请求",
|
||||
"DocumentApprovalRequest": "文档审批请求",
|
||||
@@ -22,7 +18,6 @@
|
||||
"Guidance": "指导",
|
||||
"Required": "必需的",
|
||||
"Description": "描述",
|
||||
"SectionTemplate": "章节模板",
|
||||
"Major": "主要",
|
||||
"Minor": "次要",
|
||||
"Patch": "补丁",
|
||||
@@ -104,8 +99,6 @@
|
||||
"DocumentCodePlaceholder": "DOC-1",
|
||||
"DocumentPrefixPlaceholder": "DOC",
|
||||
"DocumentPrefix": "文档前缀",
|
||||
"Sections": "章节",
|
||||
"TemplateSectionTitle": "标题",
|
||||
"DocumentTemplateCreateLabel": "模板",
|
||||
"DocumentCategoryCreateLabel": "类别",
|
||||
"CreateDocumentCategory": "创建类别",
|
||||
@@ -114,6 +107,7 @@
|
||||
"AttachmentsMax": "允许的最大附件数",
|
||||
"Resolve": "解决",
|
||||
"Unresolve": "取消解决",
|
||||
"Pending": "待定",
|
||||
"Resolved": "已解决",
|
||||
"ShowResolved": "显示已解决的评论",
|
||||
"Ordering": "排序",
|
||||
@@ -152,12 +146,6 @@
|
||||
"ChangeControl": "变更控制",
|
||||
"ReviewInterval": "审查间隔",
|
||||
|
||||
"DocTemplateDeleteSectionTitle": "确认删除章节",
|
||||
"DocTemplateDeleteSectionConfirm": "您确定要从模板中永久删除章节 \"{section}\" 吗?",
|
||||
|
||||
"AddSectionAbove": "在上方添加新章节",
|
||||
"AddSectionBelow": "在下方添加新章节",
|
||||
|
||||
"SelectReviewers": "选择审查人",
|
||||
"SelectApprovers": "选择批准人",
|
||||
"RequestsToReviewTheDoc": "请求您审查文档",
|
||||
@@ -179,8 +167,6 @@
|
||||
"InfoStepTitle": "信息",
|
||||
"TeamStepTitle": "团队",
|
||||
|
||||
"Duplicate": "复制",
|
||||
|
||||
"TitleAndDescr": "标题和描述",
|
||||
"Reason": "原因",
|
||||
"AbstractPlaceholder": "这份文档是关于什么的?谁需要它以及何时需要?...",
|
||||
@@ -193,7 +179,6 @@
|
||||
"EditDocument": "编辑文档",
|
||||
|
||||
"Key": "键",
|
||||
"SectionKey": "章节键",
|
||||
"CommentsSequence": "评论顺序",
|
||||
|
||||
"Email": "电子邮件",
|
||||
|
||||
@@ -30,8 +30,5 @@ loadMetadata(documents.icon, {
|
||||
StateObsolete: `${icons}#state-obsolete`,
|
||||
ArrowUp: `${icons}#arrow-up`,
|
||||
ArrowDown: `${icons}#arrow-down`,
|
||||
Duplicate: `${icons}#duplicate`,
|
||||
EditDescription: `${icons}#edit-description`,
|
||||
EditGuidance: `${icons}#edit-guidance`,
|
||||
Configure: `${icons}#configure`
|
||||
})
|
||||
|
||||
@@ -72,7 +72,6 @@
|
||||
category: '' as Ref<DocumentCategory>,
|
||||
abstract: '',
|
||||
state: DocumentState.Draft,
|
||||
sections: 0,
|
||||
requests: 0,
|
||||
snapshots: 0,
|
||||
reviewers: [],
|
||||
@@ -131,9 +130,6 @@
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
|
||||
// Note: previously there was inline document section editing. Restore later if will be needed as per UX.
|
||||
// See QE CreateReport component for reference. May unify general approach on creating docs with content right away.
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
-43
@@ -1,43 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DocumentTemplateSection } from '@hcengineering/controlled-documents'
|
||||
import document from '../plugin'
|
||||
import { getClient, MessageBox } from '@hcengineering/presentation'
|
||||
|
||||
export let object: DocumentTemplateSection
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function deleteSection (): Promise<void> {
|
||||
client.removeCollection(
|
||||
object._class,
|
||||
object.space,
|
||||
object._id,
|
||||
object.attachedTo,
|
||||
object.attachedToClass,
|
||||
object.collection
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<MessageBox
|
||||
label={document.string.DocTemplateDeleteSectionTitle}
|
||||
labelProps={document.string.DocTemplateDeleteSectionTitle}
|
||||
message={document.string.DocTemplateDeleteSectionConfirm}
|
||||
params={{ section: object.title }}
|
||||
action={deleteSection}
|
||||
on:close
|
||||
/>
|
||||
@@ -1,120 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Icon, Label } from '@hcengineering/ui'
|
||||
import Chevron from './icons/Chevron.svelte'
|
||||
|
||||
export let sectionType: IntlString | undefined = undefined
|
||||
export let expanded = false
|
||||
export let editable = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function handleSectionToggled (): void {
|
||||
dispatch('toggle')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class:pb-1={!expanded}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="section flex flex-grow" class:mb-0={!expanded}>
|
||||
<div draggable={true} class:before-header={!editable} class:tools={editable}>
|
||||
<slot name="before-header" />
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<div class="flex-row-center flex-grow fs-title text-xl dark-color">
|
||||
<div class="chevron hdr-alignment" class:expanded on:click|stopPropagation={handleSectionToggled}>
|
||||
<Icon icon={Chevron} size="large" />
|
||||
</div>
|
||||
<span class="hdr-alignment" on:click|stopPropagation={handleSectionToggled}><slot name="index" /></span>
|
||||
<span>. </span>
|
||||
<span class="label nowrap inline-flex flex-gap-1 flex-grow hdr-title hdr-alignment">
|
||||
{#if sectionType}
|
||||
<Label label={sectionType} />
|
||||
{/if}
|
||||
<slot name="header" />
|
||||
</span>
|
||||
<div class="tools hdr-alignment">
|
||||
<slot name="header-extra" />
|
||||
</div>
|
||||
<div class="buttons-group small-gap hdr-alignment">
|
||||
<slot name="tools" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-container flex-col" class:expanded>
|
||||
<div class="before-content">
|
||||
<slot name="before-content" />
|
||||
</div>
|
||||
|
||||
<div class="content flex-grow">
|
||||
<slot name="content" {editable} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.section {
|
||||
padding: 1rem 0 0 1rem;
|
||||
|
||||
&:hover {
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
}
|
||||
.tools {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.before-header {
|
||||
visibility: hidden;
|
||||
}
|
||||
.tools {
|
||||
visibility: hidden;
|
||||
}
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
font-size: 0.675rem;
|
||||
color: var(--theme-caption-color);
|
||||
&.expanded {
|
||||
transform: rotateZ(90deg);
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
.content-container {
|
||||
&:not(.expanded) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
margin: 0.25rem 1rem 1rem 3.25rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.before-content {
|
||||
padding: 0 3rem;
|
||||
}
|
||||
.hdr-title {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.hdr-alignment {
|
||||
position: relative;
|
||||
align-self: baseline;
|
||||
}
|
||||
</style>
|
||||
-1
@@ -120,7 +120,6 @@
|
||||
author: currentUser.person as Ref<Employee>,
|
||||
owner: currentUser.person as Ref<Employee>,
|
||||
state: DocumentState.Draft,
|
||||
sections: 0,
|
||||
snapshots: 0,
|
||||
changeControl: ccRecordId,
|
||||
content: getCollaborativeDoc(generateId()),
|
||||
|
||||
+1
-4
@@ -18,7 +18,6 @@
|
||||
ChangeControl,
|
||||
ControlledDocument,
|
||||
DEFAULT_PERIODIC_REVIEW_INTERVAL,
|
||||
DEFAULT_SECTION_TITLE,
|
||||
DocumentState,
|
||||
DocumentTemplate,
|
||||
TEMPLATE_PREFIX,
|
||||
@@ -117,7 +116,6 @@
|
||||
author: currentUser.person as Ref<Employee>,
|
||||
owner: currentUser.person as Ref<Employee>,
|
||||
state: DocumentState.Draft,
|
||||
sections: 0,
|
||||
snapshots: 0,
|
||||
changeControl: ccRecordId,
|
||||
content: getCollaborativeDoc(generateId()),
|
||||
@@ -166,8 +164,7 @@
|
||||
docObject.docPrefix,
|
||||
spec,
|
||||
category,
|
||||
currentUser.person as Ref<Employee>,
|
||||
{ title: DEFAULT_SECTION_TITLE }
|
||||
currentUser.person as Ref<Employee>
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// 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 chunter from '@hcengineering/chunter'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { Heading } from '@hcengineering/text-editor'
|
||||
import { Button, Component, EditBox } from '@hcengineering/ui'
|
||||
import documents, { Document, DocumentSection, DocumentTemplateSection } from '@hcengineering/controlled-documents'
|
||||
|
||||
import Info from '../icons/Info.svelte'
|
||||
import {
|
||||
$documentCommentHighlightedLocation as highlighted,
|
||||
$canAddDocumentComments as canAddDocumentComments,
|
||||
$canViewDocumentComments as canViewDocumentComments,
|
||||
$collapsedDocumentSectionIds as collapsedSectionIds,
|
||||
$documentSectionEditingDescription as documentSectionEditingDescription,
|
||||
$groupedDocumentComments as groupedDocumentComments,
|
||||
$isEditable as isEditable,
|
||||
documentCommentsDisplayRequested,
|
||||
documentSectionToggled,
|
||||
showAddCommentPopupFx,
|
||||
updateDocumentSectionDescriptionFx
|
||||
} from '../../stores/editors/document'
|
||||
import { openGuidanceEditor } from '../../utils'
|
||||
import FieldSectionEditor from '../FieldSectionEditor.svelte'
|
||||
import DescriptionEditor from './editors/DescriptionEditor.svelte'
|
||||
|
||||
export let document: Document
|
||||
export let value: DocumentSection
|
||||
export let index: number
|
||||
export let dragging = false
|
||||
|
||||
export let headings: Heading[] = [] // out
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const h = client.getHierarchy()
|
||||
|
||||
$: editor = h.as(h.getClass(value._class), documents.mixin.DocumentSectionEditor)?.editor
|
||||
|
||||
let sectionHasGuidance = false
|
||||
let templateSection: DocumentTemplateSection | undefined
|
||||
$: getDocumentTemplateSection(value)
|
||||
async function getDocumentTemplateSection (section: DocumentSection) {
|
||||
if (h.hasMixin(value, documents.mixin.DocumentTemplateSection)) {
|
||||
templateSection = h.as(value, documents.mixin.DocumentTemplateSection)
|
||||
} else if (value.templateSectionId != null) {
|
||||
templateSection = await client.findOne(documents.mixin.DocumentTemplateSection, { _id: value.templateSectionId })
|
||||
}
|
||||
|
||||
if (templateSection) {
|
||||
const guidance = templateSection.guidance
|
||||
sectionHasGuidance = !isEmptyMarkup(guidance)
|
||||
}
|
||||
}
|
||||
|
||||
let contentHeadings: Heading[] = []
|
||||
$: sectionHeading = makeSectionHeading(value, index)
|
||||
$: headings = [sectionHeading, ...contentHeadings]
|
||||
|
||||
function handleSectionContentHeadings (headings: Heading[]): void {
|
||||
contentHeadings = headings
|
||||
}
|
||||
|
||||
function makeSectionHeading (section: DocumentSection, index: number): Heading {
|
||||
index = index + 1
|
||||
const id = `section-${index}`
|
||||
const title = `${index}. ${section.title}`
|
||||
return { id, title, level: 0 }
|
||||
}
|
||||
|
||||
async function handleOpenAddCommentPopup (ev?: Event): Promise<void> {
|
||||
if (!$canAddDocumentComments) {
|
||||
return
|
||||
}
|
||||
|
||||
await showAddCommentPopupFx({
|
||||
element: (ev as MouseEvent).target as HTMLElement,
|
||||
sectionKey: value.key
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisplayDocumentComments (ev?: Event): void {
|
||||
if (!$canViewDocumentComments) {
|
||||
return
|
||||
}
|
||||
if (!$groupedDocumentComments.hasDocumentComments(value.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
ev?.stopPropagation()
|
||||
|
||||
documentCommentsDisplayRequested({ element: (ev as MouseEvent).target as HTMLElement, sectionKey: value.key })
|
||||
}
|
||||
|
||||
async function updateSectionTitle (title: string) {
|
||||
await client.update(value, { title })
|
||||
}
|
||||
|
||||
let sectionElement: HTMLElement
|
||||
export function getSectionElement (): HTMLElement {
|
||||
return sectionElement
|
||||
}
|
||||
|
||||
let title = value.title
|
||||
|
||||
let descr = ''
|
||||
$: getDescription(templateSection)
|
||||
$: isActiveSectionNode = !!$highlighted && $highlighted.sectionKey === value.key && !$highlighted.nodeId
|
||||
function getDescription (section?: DocumentTemplateSection) {
|
||||
if (section != null) {
|
||||
descr = h.as(section, documents.mixin.DocumentTemplateSection).description ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
$: isEditingDescription = $documentSectionEditingDescription === value._id
|
||||
|
||||
async function stopEditingDescription () {
|
||||
if (!templateSection) {
|
||||
return
|
||||
}
|
||||
updateDocumentSectionDescriptionFx({ section: templateSection, description: descr })
|
||||
}
|
||||
|
||||
function showGuidance (ev: MouseEvent) {
|
||||
if (templateSection == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const isEditingTemplate = templateSection._id === value._id
|
||||
openGuidanceEditor(client, templateSection, index + 1, isEditingTemplate ? 'canEdit' : 'readonly', ev)
|
||||
}
|
||||
</script>
|
||||
|
||||
<FieldSectionEditor
|
||||
editable={$isEditable}
|
||||
expanded={!$collapsedSectionIds.has(value._id)}
|
||||
on:toggle={() => documentSectionToggled(value._id)}
|
||||
>
|
||||
<svelte:fragment slot="before-header">
|
||||
<slot name="before-header" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="index">
|
||||
<span class="sectionTitle">
|
||||
{index + 1}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id={sectionHeading.id}
|
||||
class:text-editor-highlighted-node-warning={isActiveSectionNode ||
|
||||
$groupedDocumentComments.hasDocumentComments(value.key)}
|
||||
class:text-editor-highlighted-node-selected={isActiveSectionNode}
|
||||
class="sectionTitle"
|
||||
bind:this={sectionElement}
|
||||
on:click={handleDisplayDocumentComments}
|
||||
>
|
||||
{#if $isEditable && !dragging}
|
||||
<EditBox maxWidth="100%" bind:value={title} propagateClick on:change={() => updateSectionTitle(title)} />
|
||||
{:else}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header-extra">
|
||||
<div class="flex-row-center ml-2 mr-10">
|
||||
{#if $canAddDocumentComments}
|
||||
<Button icon={chunter.icon.Chunter} kind="list-header" size="small" on:click={handleOpenAddCommentPopup} />
|
||||
{/if}
|
||||
{#if templateSection && sectionHasGuidance}
|
||||
<Button icon={Info} kind="list-header" size="small" on:click={showGuidance} />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="before-content">
|
||||
{#if isEditingDescription || descr.length > 0}
|
||||
<div class="section-descr no-print">
|
||||
<DescriptionEditor bind:value={descr} disabled={!isEditingDescription} on:blur={stopEditingDescription} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<Component
|
||||
is={editor}
|
||||
props={{
|
||||
value,
|
||||
document,
|
||||
onHeadings: handleSectionContentHeadings
|
||||
}}
|
||||
on:change
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</FieldSectionEditor>
|
||||
|
||||
<style lang="scss">
|
||||
.sectionTitle {
|
||||
font-size: 1.75rem;
|
||||
line-height: 150%;
|
||||
}
|
||||
</style>
|
||||
+21
-70
@@ -4,6 +4,7 @@
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import { CollaborationIds, type Ydoc } from '@hcengineering/text-editor'
|
||||
import {
|
||||
CollaborationDiffViewer,
|
||||
StringDiffViewer,
|
||||
TiptapCollabProvider,
|
||||
createTiptapCollaborationData,
|
||||
@@ -15,67 +16,25 @@
|
||||
ControlledDocumentSnapshot,
|
||||
ControlledDocumentState,
|
||||
Document,
|
||||
DocumentSection,
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import plugin from '../../plugin'
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
$controlledDocumentSections as sections,
|
||||
$comparedDocument as compareTo,
|
||||
$comparedDocumentSections as compareToSections,
|
||||
$documentComparisonVersions as documentComparisonVersions,
|
||||
ComparisonSectionPair,
|
||||
comparisonRequested,
|
||||
loadComparedDocumentSectionsFx
|
||||
comparisonRequested
|
||||
} from '../../stores/editors/document'
|
||||
import { COLLABORATOR_URL, TOKEN, getTranslatedControlledDocStates, getTranslatedDocumentStates } from '../../utils'
|
||||
import DocumentSectionPairDiffViewer from './DocumentSectionPairDiffViewer.svelte'
|
||||
import DocumentTitle from './DocumentTitle.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const ydoc = getContext<Ydoc>(CollaborationIds.Doc)
|
||||
|
||||
let collapsedPairIndices = new Set<number>()
|
||||
let comparedYdoc: Ydoc | undefined = undefined
|
||||
let comparedProvider: TiptapCollabProvider | undefined = undefined
|
||||
let loading = true
|
||||
const isLoadPending = loadComparedDocumentSectionsFx.pending
|
||||
|
||||
const handleSectionDiffPairs = (firstSections: DocumentSection[], secondSections: DocumentSection[]) => {
|
||||
const result: ComparisonSectionPair[] = []
|
||||
const firstSectionKeys = new Set(firstSections.map((section) => section.key))
|
||||
const secondSectionKeys = new Set(secondSections.map((section) => section.key))
|
||||
let secondIndex = 0
|
||||
let firstIndex = 0
|
||||
while (firstIndex < firstSections.length) {
|
||||
const firstSection = firstSections[firstIndex]
|
||||
if (secondSectionKeys.has(firstSection.key)) {
|
||||
while (secondIndex < secondSections.length && !firstSectionKeys.has(secondSections[secondIndex].key)) {
|
||||
result.push([null, { section: secondSections[secondIndex], index: secondIndex + 1 }])
|
||||
secondIndex++
|
||||
}
|
||||
}
|
||||
if (secondIndex < secondSections.length && firstSection.key === secondSections[secondIndex].key) {
|
||||
result.push([
|
||||
{ section: firstSection, index: firstIndex + 1 },
|
||||
{ section: secondSections[secondIndex], index: secondIndex + 1 }
|
||||
])
|
||||
secondIndex++
|
||||
} else {
|
||||
result.push([{ section: firstSection, index: firstIndex + 1 }, null])
|
||||
}
|
||||
firstIndex++
|
||||
}
|
||||
|
||||
while (secondIndex < secondSections.length) {
|
||||
result.push([null, { section: secondSections[secondIndex], index: secondIndex + 1 }])
|
||||
secondIndex++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const handleSelect = (event: CustomEvent<ListItem>) => {
|
||||
const version = $documentComparisonVersions.find((item) => item._id === event.detail._id)
|
||||
@@ -142,8 +101,6 @@
|
||||
comparedProvider.loaded.then(() => (loading = false))
|
||||
}
|
||||
|
||||
$: sectionDiffPairs = handleSectionDiffPairs($sections, $compareToSections)
|
||||
|
||||
onDestroy(() => {
|
||||
comparedProvider?.destroy()
|
||||
})
|
||||
@@ -167,35 +124,29 @@
|
||||
on:selected={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
{#if loading || $isLoadPending}
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<Scroller>
|
||||
<div class="antiAccordion">
|
||||
<div class="pl-7">
|
||||
<DocumentTitle>
|
||||
<StringDiffViewer
|
||||
value={$controlledDocument?.title ?? ''}
|
||||
compareTo={(isDocument($compareTo) ? $compareTo : $controlledDocument)?.title ?? ''}
|
||||
/>
|
||||
</DocumentTitle>
|
||||
</div>
|
||||
{#each sectionDiffPairs as pair, index}
|
||||
<DocumentSectionPairDiffViewer
|
||||
{pair}
|
||||
firstYdoc={ydoc}
|
||||
secondYdoc={comparedYdoc}
|
||||
expanded={!collapsedPairIndices.has(index)}
|
||||
on:toggle={() => {
|
||||
if (collapsedPairIndices.has(index)) {
|
||||
collapsedPairIndices.delete(index)
|
||||
} else {
|
||||
collapsedPairIndices.add(index)
|
||||
}
|
||||
collapsedPairIndices = new Set(collapsedPairIndices)
|
||||
}}
|
||||
<div class="root">
|
||||
<DocumentTitle>
|
||||
<StringDiffViewer
|
||||
value={$controlledDocument?.title ?? ''}
|
||||
compareTo={(isDocument($compareTo) ? $compareTo : $controlledDocument)?.title ?? ''}
|
||||
/>
|
||||
{/each}
|
||||
</DocumentTitle>
|
||||
<CollaborationDiffViewer field="content" comparedField="content" {ydoc} {comparedYdoc} />
|
||||
<div class="bottomSpacing" />
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: 0 3.25rem;
|
||||
}
|
||||
|
||||
.bottomSpacing {
|
||||
padding-bottom: 30vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Ydoc } from '@hcengineering/text-editor'
|
||||
import { CollaborationDiffViewer, StringDiffViewer } from '@hcengineering/text-editor-resources'
|
||||
import documents, { DocumentSection, CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
|
||||
import plugin from '../../plugin'
|
||||
import { ComparisonSectionPair } from '../../stores/editors/document'
|
||||
import FieldSectionEditor from '../FieldSectionEditor.svelte'
|
||||
|
||||
export let pair: ComparisonSectionPair
|
||||
export let firstYdoc: Ydoc
|
||||
export let secondYdoc: Ydoc | undefined = undefined
|
||||
export let expanded = false
|
||||
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
|
||||
const getSectionField = (section: DocumentSection | undefined | null) => {
|
||||
if (!section) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return (section as CollaborativeDocumentSection).collaboratorSectionId
|
||||
}
|
||||
$: _class = pair[0]?.section._class ?? pair[1]?.section._class
|
||||
</script>
|
||||
|
||||
<FieldSectionEditor {expanded} editable={false} on:toggle>
|
||||
<svelte:fragment slot="index">
|
||||
<StringDiffViewer value={`${pair[0]?.index ?? ''}`} compareTo={`${pair[1]?.index ?? ''}`} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<StringDiffViewer value={pair[0]?.section.title ?? ''} compareTo={pair[1]?.section.title ?? ''} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if _class && hierarchy.isDerived(_class, documents.class.CollaborativeDocumentSection)}
|
||||
<CollaborationDiffViewer
|
||||
field={getSectionField(pair[0]?.section)}
|
||||
comparedField={getSectionField(pair[1]?.section)}
|
||||
ydoc={firstYdoc}
|
||||
comparedYdoc={secondYdoc}
|
||||
/>
|
||||
{:else}
|
||||
{plugin.string.ComparisonModeNotSupported}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</FieldSectionEditor>
|
||||
+276
-192
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
// Copyright © 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
|
||||
@@ -13,52 +13,114 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||
import { merge } from 'effector'
|
||||
import { type CollaborativeDoc, type Ref, type Blob, generateId } from '@hcengineering/core'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Heading } from '@hcengineering/text-editor'
|
||||
import { TableOfContents, TableOfContentsContent } from '@hcengineering/text-editor-resources'
|
||||
import { EditBox, IconCircles, Scroller } from '@hcengineering/ui'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import { DocumentSection, calcRank } from '@hcengineering/controlled-documents'
|
||||
import { onDestroy, tick } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import view from '@hcengineering/view'
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import documents from '@hcengineering/controlled-documents'
|
||||
import { Editor, Heading } from '@hcengineering/text-editor'
|
||||
import {
|
||||
CollaboratorEditor,
|
||||
TableOfContents,
|
||||
TableOfContentsContent,
|
||||
FocusExtension,
|
||||
HeadingsExtension,
|
||||
IsEmptyContentExtension,
|
||||
NodeHighlightExtension,
|
||||
NodeHighlightType,
|
||||
highlightUpdateCommand,
|
||||
getNodeElement
|
||||
} from '@hcengineering/text-editor-resources'
|
||||
import { navigate, EditBox, Scroller } from '@hcengineering/ui'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
|
||||
import {
|
||||
$areDocumentCommentPopupsOpened as areDocumentCommentPopupsOpened,
|
||||
$controlledDocument as controlledDocument,
|
||||
documentCommentsLocationNavigateRequested,
|
||||
documentSectionCollapsed,
|
||||
documentSectionExpanded,
|
||||
$controlledDocumentTemplate as controlledDocumentTemplate,
|
||||
$isEditable as isEditable,
|
||||
$controlledDocumentSectionIds as sectionIds,
|
||||
$controlledDocumentSections as sections
|
||||
$documentCommentHighlightedLocation as documentCommentHighlightedLocation,
|
||||
$areDocumentCommentPopupsOpened as arePopupsOpened,
|
||||
$canAddDocumentComments as canAddDocumentComments,
|
||||
$canViewDocumentComments as canViewDocumentComments,
|
||||
$documentComments as documentComments,
|
||||
documentCommentsDisplayRequested,
|
||||
documentCommentsHighlightUpdated,
|
||||
documentCommentsLocationNavigateRequested
|
||||
} from '../../stores/editors/document'
|
||||
import DocSectionEditor from './DocSectionEditor.svelte'
|
||||
import DocumentTitle from './DocumentTitle.svelte'
|
||||
import DocumentPrintTitlePage from '../print/DocumentPrintTitlePage.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const sectionRefs: Record<string, HTMLElement> = {}
|
||||
let divScroll: HTMLElement
|
||||
const editors: DocSectionEditor[] = []
|
||||
const sectionHeadings: Record<Ref<DocumentSection>, Heading[]> = {}
|
||||
|
||||
let draggingId: Ref<DocumentSection> | null = null
|
||||
let dragOverId: Ref<DocumentSection> | null = null
|
||||
let dragging = false
|
||||
const hierarchy = client.getHierarchy()
|
||||
const user = getCollaborationUser()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let headings: Heading[] = []
|
||||
let textEditor: CollaboratorEditor
|
||||
let selectedNodeId: string | null | undefined = undefined
|
||||
let isFocused = false
|
||||
let isEmpty = true
|
||||
let editor: Editor
|
||||
let title = $controlledDocument?.title ?? ''
|
||||
|
||||
$: headings = $sectionIds.map((sectionId) => sectionHeadings[sectionId] ?? []).flat()
|
||||
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)
|
||||
|
||||
function handleRefreshHighlight () {
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
textEditor.commands()?.command(highlightUpdateCommand())
|
||||
}
|
||||
|
||||
const unsubscribeHighlightRefresh = merge([documentCommentHighlightedLocation, documentComments.updates]).subscribe({
|
||||
next: () => {
|
||||
handleRefreshHighlight()
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeNavigateToLocation = documentCommentsLocationNavigateRequested.subscribe({
|
||||
next: ({ sectionKey }) => {
|
||||
const element = sectionRefs[sectionKey]
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
next: async ({ nodeId }) => {
|
||||
if (!nodeId) {
|
||||
handleRefreshHighlight()
|
||||
return
|
||||
}
|
||||
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
await tick()
|
||||
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeHighlightRefresh()
|
||||
unsubscribeNavigateToLocation()
|
||||
})
|
||||
|
||||
const handleUpdateTitle = () => {
|
||||
if (!$controlledDocument || !title) {
|
||||
return
|
||||
@@ -70,150 +132,173 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetDrag (ev?: DragEvent): void {
|
||||
draggingId = null
|
||||
dragOverId = null
|
||||
dragging = false
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, id: Ref<DocumentSection>): void {
|
||||
if (ev.dataTransfer) {
|
||||
dragging = true
|
||||
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
const index = $sectionIds.indexOf(id)
|
||||
ev.dataTransfer.setDragImage(editors[index].getSectionElement(), 0, 0)
|
||||
|
||||
draggingId = id
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDrop (ev: DragEvent, toId: Ref<DocumentSection>): Promise<void> {
|
||||
if (ev.dataTransfer && draggingId !== null && toId !== draggingId) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
const draggingIndex = $sectionIds.indexOf(draggingId)
|
||||
const toIndex = $sectionIds.indexOf(toId)
|
||||
|
||||
const [prev, next] = [
|
||||
$sections[draggingIndex < toIndex ? toIndex : toIndex - 1],
|
||||
$sections[draggingIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
|
||||
const section = $sections[draggingIndex]
|
||||
|
||||
// workaround to not display editor toolbar
|
||||
documentSectionCollapsed(draggingId)
|
||||
documentSectionCollapsed(toId)
|
||||
|
||||
await client.update(section, { rank: calcRank(prev, next) })
|
||||
}
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function openSectionMenu (ev: MouseEvent, section: DocumentSection): void {
|
||||
if (!$sections || $sections.length === 0) {
|
||||
return
|
||||
}
|
||||
showMenu(ev, { object: section })
|
||||
}
|
||||
|
||||
async function handleShowHeading (heading: Heading): Promise<void> {
|
||||
const sectionId = $sectionIds.find((sectionId) => sectionHeadings[sectionId]?.includes(heading))
|
||||
if (sectionId) {
|
||||
documentSectionExpanded(sectionId)
|
||||
}
|
||||
|
||||
await tick()
|
||||
|
||||
const element = window.document.getElementById(heading.id)
|
||||
element?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
onDestroy(unsubscribeNavigateToLocation)
|
||||
function handleNodeHighlight (id: string) {
|
||||
if ($documentCommentHighlightedLocation) {
|
||||
const { nodeId } = $documentCommentHighlightedLocation
|
||||
if (nodeId === id) {
|
||||
return { type: NodeHighlightType.WARNING, isActive: true }
|
||||
}
|
||||
}
|
||||
|
||||
if ($documentComments.some((c) => c.nodeId === id)) {
|
||||
return { type: NodeHighlightType.WARNING }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function handleShowDocumentComments (uuid: string) {
|
||||
if (!uuid) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsDisplayRequested({
|
||||
element: getNodeElement(editor, uuid),
|
||||
nodeId: uuid
|
||||
})
|
||||
}
|
||||
|
||||
async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
|
||||
if ($controlledDocument === undefined || $controlledDocument === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||
const uuid = await uploadFile(file)
|
||||
const attachmentId: Ref<Attachment> = generateId()
|
||||
|
||||
await client.addCollection(
|
||||
attachment.class.Attachment,
|
||||
$controlledDocument.space,
|
||||
$controlledDocument._id,
|
||||
$controlledDocument._class,
|
||||
'attachments',
|
||||
{
|
||||
file: uuid,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
},
|
||||
attachmentId
|
||||
)
|
||||
|
||||
return { file: uuid, type: file.type }
|
||||
} catch (err: any) {
|
||||
await setPlatformStatus(unknownError(err))
|
||||
} finally {
|
||||
dispatch('change')
|
||||
}
|
||||
}
|
||||
|
||||
function handleExtensions () {
|
||||
return [
|
||||
FocusExtension.configure({
|
||||
onFocus (focused) {
|
||||
isFocused = focused
|
||||
}
|
||||
}),
|
||||
IsEmptyContentExtension.configure({
|
||||
onChange (empty) {
|
||||
isEmpty = empty
|
||||
}
|
||||
}),
|
||||
HeadingsExtension.configure({
|
||||
onChange: (h) => {
|
||||
headings = h
|
||||
}
|
||||
}),
|
||||
NodeHighlightExtension.configure({
|
||||
isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments,
|
||||
getNodeHighlight: handleNodeHighlight,
|
||||
onNodeSelected: (uuid: string | null) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
if (isFocused) {
|
||||
documentCommentsHighlightUpdated(selectedNodeId !== null ? { nodeId: selectedNodeId } : null)
|
||||
}
|
||||
},
|
||||
onNodeClicked: (uuid: string) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
|
||||
if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) {
|
||||
handleShowDocumentComments(selectedNodeId)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $controlledDocument}
|
||||
{#if $controlledDocument && collaborativeDoc}
|
||||
<DocumentPrintTitlePage />
|
||||
|
||||
<div class="root flex-col relative">
|
||||
<div class="toc">
|
||||
<TableOfContents items={headings} on:select={(ev) => handleShowHeading(ev.detail)} />
|
||||
{#if headings.length > 0}
|
||||
<div class="tocContent only-print">
|
||||
<TableOfContentsContent items={headings} enumerated={true} />
|
||||
</div>
|
||||
{#if headings.length > 0}
|
||||
<div class="only-print ml-12">
|
||||
<TableOfContentsContent items={headings} />
|
||||
</div>
|
||||
<div class="pagebreak" />
|
||||
{/if}
|
||||
<Scroller bind:divScroll>
|
||||
<div class="antiAccordion">
|
||||
<div class="doc-title">
|
||||
<DocumentTitle>
|
||||
{#if $isEditable}
|
||||
<EditBox
|
||||
value={title}
|
||||
on:value={(event) => {
|
||||
title = event.detail
|
||||
}}
|
||||
on:blur={handleUpdateTitle}
|
||||
/>
|
||||
{:else}
|
||||
{$controlledDocument.title}
|
||||
{/if}
|
||||
</DocumentTitle>
|
||||
</div>
|
||||
{#if $sections}
|
||||
{#each $sections as section, i (section._id)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="row"
|
||||
bind:this={sectionRefs[section.key]}
|
||||
class:is-dragging={section._id === draggingId}
|
||||
class:drag-over-highlight={section._id === dragOverId}
|
||||
on:dragstart={(ev) => {
|
||||
handleDragStart(ev, section._id)
|
||||
<div class="pagebreak" />
|
||||
{/if}
|
||||
|
||||
<div class="root relative">
|
||||
<div class="toc">
|
||||
<TableOfContents items={headings} enumerated={true} on:select={(ev) => handleShowHeading(ev.detail)} />
|
||||
</div>
|
||||
<Scroller>
|
||||
<div class="content">
|
||||
<DocumentTitle>
|
||||
{#if $isEditable}
|
||||
<EditBox
|
||||
value={title}
|
||||
on:value={(event) => {
|
||||
title = event.detail
|
||||
}}
|
||||
on:dragleave|preventDefault={() => {
|
||||
if (dragOverId === section._id) dragOverId = null
|
||||
return false
|
||||
}}
|
||||
on:dragover|preventDefault={() => {
|
||||
dragOverId = section._id
|
||||
return false
|
||||
}}
|
||||
on:dragend={resetDrag}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, section._id)}
|
||||
animate:flip={{ duration: 400 }}
|
||||
>
|
||||
<DocSectionEditor
|
||||
bind:headings={sectionHeadings[section._id]}
|
||||
bind:this={editors[i]}
|
||||
value={section}
|
||||
document={$controlledDocument}
|
||||
index={i}
|
||||
{dragging}
|
||||
on:change={resetDrag}
|
||||
>
|
||||
<div class="m0 flex-row-center draggable-container" slot="before-header">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="draggable-mark"
|
||||
class:dragging
|
||||
on:click={(ev) => {
|
||||
openSectionMenu(ev, section)
|
||||
}}
|
||||
>
|
||||
<IconCircles size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</DocSectionEditor>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
on:blur={handleUpdateTitle}
|
||||
/>
|
||||
{:else}
|
||||
{$controlledDocument.title}
|
||||
{/if}
|
||||
</DocumentTitle>
|
||||
<CollaboratorEditor
|
||||
bind:this={textEditor}
|
||||
objectId={$controlledDocument._id}
|
||||
objectClass={$controlledDocument._class}
|
||||
objectSpace={$controlledDocument.space}
|
||||
{collaborativeDoc}
|
||||
{initialCollaborativeDoc}
|
||||
{user}
|
||||
readonly={!$isEditable}
|
||||
field="content"
|
||||
editorAttributes={{ style: 'padding: 0 2em; margin: 0 -2em;' }}
|
||||
overflow="none"
|
||||
canShowPopups={!$areDocumentCommentPopupsOpened}
|
||||
onExtensions={handleExtensions}
|
||||
kitOptions={{
|
||||
note: {
|
||||
readonly: !isTemplate
|
||||
}
|
||||
}}
|
||||
on:editor={(e) => (editor = e.detail)}
|
||||
on:open-document={async (event) => {
|
||||
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
|
||||
if (doc != null) {
|
||||
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
|
||||
navigate(location)
|
||||
}
|
||||
}}
|
||||
attachFile={async (file) => {
|
||||
return await createEmbedding(file)
|
||||
}}
|
||||
/>
|
||||
<div class="bottomSpacing no-print" />
|
||||
</div>
|
||||
</Scroller>
|
||||
@@ -226,6 +311,34 @@
|
||||
|
||||
@media print {
|
||||
margin-left: -1rem;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
// Workaround to quickly enumerate headings for controlled docs
|
||||
:global(h1) {
|
||||
counter-increment: h1;
|
||||
counter-reset: h2;
|
||||
|
||||
&::before {
|
||||
content: counter(h1) '. ';
|
||||
}
|
||||
}
|
||||
|
||||
:global(h2) {
|
||||
counter-increment: h2;
|
||||
counter-reset: h3;
|
||||
|
||||
&::before {
|
||||
content: counter(h1) '.' counter(h2) '. ';
|
||||
}
|
||||
}
|
||||
|
||||
:global(h3) {
|
||||
counter-increment: h3;
|
||||
|
||||
&::before {
|
||||
content: counter(h1) '.' counter(h2) '.' counter(h3) '. ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,44 +351,15 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
padding-left: 3.25rem;
|
||||
.tocContent {
|
||||
padding-left: 2.25rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
margin-left: 0;
|
||||
|
||||
.draggable-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.draggable-mark {
|
||||
padding: 0.375rem 0.125rem;
|
||||
&:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
position: relative;
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding-left: 3.25rem;
|
||||
}
|
||||
|
||||
.bottomSpacing {
|
||||
padding-bottom: 30vh;
|
||||
}
|
||||
.drag-over-highlight {
|
||||
opacity: 0.2;
|
||||
}
|
||||
</style>
|
||||
|
||||
+2
-1
@@ -24,9 +24,10 @@
|
||||
export let value: Document | undefined
|
||||
export let readonly = true
|
||||
|
||||
let abstract = value?.abstract
|
||||
const client = getClient()
|
||||
|
||||
$: abstract = value?.abstract
|
||||
|
||||
const handleUpdateAbstract = () => {
|
||||
if (readonly) {
|
||||
return
|
||||
|
||||
-50
@@ -1,50 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentsDocumentSection, DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { Attachments } from '@hcengineering/attachment-resources'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let value: DocumentSection
|
||||
export let readonly = false
|
||||
export let showHeader = true
|
||||
export let withScroll = true
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', {})
|
||||
})
|
||||
|
||||
$: attachmentSection = value as AttachmentsDocumentSection
|
||||
$: attachmentsProps = {
|
||||
objectId: value._id,
|
||||
space: value.space,
|
||||
_class: value._class,
|
||||
attachments: attachmentSection.attachments ?? 0,
|
||||
readonly,
|
||||
showHeader
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if withScroll}
|
||||
<Scroller autoscroll>
|
||||
<div class="mr-6">
|
||||
<Attachments {...attachmentsProps} />
|
||||
</div>
|
||||
</Scroller>
|
||||
{:else}
|
||||
<Attachments {...attachmentsProps} />
|
||||
{/if}
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// 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 { merge } from 'effector'
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||
import { CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { navigate } from '@hcengineering/ui'
|
||||
import { CollaborativeDoc, Ref, generateId, Blob } from '@hcengineering/core'
|
||||
import view from '@hcengineering/view'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Editor, Heading } from '@hcengineering/text-editor'
|
||||
import {
|
||||
CollaboratorEditor,
|
||||
FocusExtension,
|
||||
HeadingsExtension,
|
||||
IsEmptyContentExtension,
|
||||
NodeHighlightExtension,
|
||||
NodeHighlightType,
|
||||
highlightUpdateCommand,
|
||||
getNodeElement
|
||||
} from '@hcengineering/text-editor-resources'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
|
||||
import {
|
||||
$documentCommentHighlightedLocation as highlighted,
|
||||
$areDocumentCommentPopupsOpened as arePopupsOpened,
|
||||
$canAddDocumentComments as canAddDocumentComments,
|
||||
$canViewDocumentComments as canViewDocumentComments,
|
||||
$controlledDocument as controlledDocument,
|
||||
$controlledDocumentTemplate as controlledDocumentTemplate,
|
||||
$groupedDocumentComments as groupedDocumentComments,
|
||||
$isEditable as isEditable,
|
||||
documentCommentsDisplayRequested,
|
||||
documentCommentsHighlightUpdated,
|
||||
documentCommentsLocationNavigateRequested
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
export let value: CollaborativeDocumentSection
|
||||
export let onHeadings: (headings: Heading[]) => void
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const user = getCollaborationUser()
|
||||
|
||||
let selectedNodeId: string | null | undefined = undefined
|
||||
let textEditor: CollaboratorEditor
|
||||
let isFocused = false
|
||||
let isEmpty = true
|
||||
let editor: Editor
|
||||
|
||||
const handleRefreshHighlight = () => {
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
textEditor.commands()?.command(highlightUpdateCommand())
|
||||
}
|
||||
|
||||
const unsubscribeHighlightRefresh = merge([highlighted, groupedDocumentComments.updates]).subscribe({
|
||||
next: () => {
|
||||
handleRefreshHighlight()
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeNavigateToLocation = documentCommentsLocationNavigateRequested.subscribe({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
next: async ({ nodeId, sectionKey }) => {
|
||||
if (sectionKey !== value.key || !nodeId) {
|
||||
handleRefreshHighlight()
|
||||
return
|
||||
}
|
||||
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
await tick()
|
||||
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleNodeHighlight = (id: string) => {
|
||||
if ($highlighted) {
|
||||
const { sectionKey, nodeId } = $highlighted
|
||||
if (nodeId === id && sectionKey === value.key) {
|
||||
return { type: NodeHighlightType.WARNING, isActive: true }
|
||||
}
|
||||
}
|
||||
|
||||
if ($groupedDocumentComments.hasDocumentComments(value.key, id)) {
|
||||
return { type: NodeHighlightType.WARNING }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const handleShowDocumentComments = (uuid: string) => {
|
||||
if (!uuid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$groupedDocumentComments.hasDocumentComments(value.key, uuid)) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsDisplayRequested({
|
||||
element: getNodeElement(editor, uuid),
|
||||
nodeId: uuid,
|
||||
sectionKey: value.key
|
||||
})
|
||||
}
|
||||
|
||||
const handleExtensions = () => [
|
||||
FocusExtension.configure({
|
||||
onFocus (focused) {
|
||||
isFocused = focused
|
||||
}
|
||||
}),
|
||||
IsEmptyContentExtension.configure({
|
||||
onChange (empty) {
|
||||
isEmpty = empty
|
||||
}
|
||||
}),
|
||||
HeadingsExtension.configure({
|
||||
prefixId: `section-${value._id}`,
|
||||
onChange: onHeadings
|
||||
}),
|
||||
NodeHighlightExtension.configure({
|
||||
isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments,
|
||||
getNodeHighlight: handleNodeHighlight,
|
||||
onNodeSelected: (uuid: string | null) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
if (isFocused) {
|
||||
documentCommentsHighlightUpdated(
|
||||
selectedNodeId && $groupedDocumentComments.hasDocumentComments(value.key, uuid)
|
||||
? { sectionKey: value.key, nodeId: selectedNodeId }
|
||||
: null
|
||||
)
|
||||
}
|
||||
},
|
||||
onNodeClicked: (uuid: string) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
|
||||
if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) {
|
||||
handleShowDocumentComments(selectedNodeId)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
async function createEmbedding (
|
||||
file: File,
|
||||
section: CollaborativeDocumentSection
|
||||
): Promise<{ file: Ref<Blob>, type: string } | undefined> {
|
||||
if ($controlledDocument === undefined || $controlledDocument === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||
const uuid = await uploadFile(file)
|
||||
const attachmentId: Ref<Attachment> = generateId()
|
||||
|
||||
await client.addCollection(
|
||||
attachment.class.Attachment,
|
||||
section.space,
|
||||
section._id,
|
||||
section._class,
|
||||
'attachments',
|
||||
{
|
||||
file: uuid,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
},
|
||||
attachmentId
|
||||
)
|
||||
|
||||
return { file: uuid, type: file.type }
|
||||
} catch (err: any) {
|
||||
await setPlatformStatus(unknownError(err))
|
||||
} finally {
|
||||
dispatch('change')
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeHighlightRefresh()
|
||||
unsubscribeNavigateToLocation()
|
||||
})
|
||||
|
||||
let collaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocument !== null) {
|
||||
collaborativeDoc = $controlledDocument.content
|
||||
}
|
||||
|
||||
let initialCollaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocumentTemplate !== null) {
|
||||
initialCollaborativeDoc = $controlledDocumentTemplate.content
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if collaborativeDoc}
|
||||
{#key value._id}
|
||||
<CollaboratorEditor
|
||||
bind:this={textEditor}
|
||||
objectId={value._id}
|
||||
objectClass={value._class}
|
||||
objectSpace={value.space}
|
||||
{collaborativeDoc}
|
||||
{initialCollaborativeDoc}
|
||||
{user}
|
||||
readonly={!$isEditable}
|
||||
field={value.collaboratorSectionId}
|
||||
editorAttributes={{ style: 'padding: 0 2em; margin: 0 -2em;' }}
|
||||
overflow="none"
|
||||
canShowPopups={!$arePopupsOpened}
|
||||
onExtensions={handleExtensions}
|
||||
on:editor={(e) => (editor = e.detail)}
|
||||
on:open-document={async (event) => {
|
||||
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
|
||||
if (doc != null) {
|
||||
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
|
||||
navigate(location)
|
||||
}
|
||||
}}
|
||||
attachFile={async (file) => {
|
||||
return await createEmbedding(file, value)
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { afterUpdate } from 'svelte'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import documentsRes from '../../../plugin'
|
||||
|
||||
export let width: string | undefined = undefined
|
||||
export let height: string | undefined = undefined
|
||||
export let value: string | undefined = undefined
|
||||
export let placeholder: IntlString = documentsRes.string.EditDescription
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
let input: HTMLElement
|
||||
let phTranslate: string = ''
|
||||
|
||||
$: translate(placeholder, placeholderParam ?? {}).then((res) => {
|
||||
phTranslate = res
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if (value !== '') {
|
||||
input.style.height = 'auto'
|
||||
input.style.height = `${input.scrollHeight + 2}px`
|
||||
}
|
||||
})
|
||||
|
||||
$: if (!disabled && input) {
|
||||
input.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="textarea" style:width style:height>
|
||||
<textarea
|
||||
maxlength="240"
|
||||
rows="1"
|
||||
bind:value
|
||||
bind:this={input}
|
||||
placeholder={phTranslate}
|
||||
{disabled}
|
||||
on:keydown
|
||||
on:change
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
textarea {
|
||||
padding: 0.62rem 1rem;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--theme-docs-description-border-color);
|
||||
border-radius: 0.375rem;
|
||||
&:disabled {
|
||||
padding: 0.25rem 0.5rem 0.38rem 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background-color: var(--theme-docs-frozen-description-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { ModernDialog, IconClose } from '@hcengineering/ui'
|
||||
import { StyledTextArea } from '@hcengineering/text-editor-resources'
|
||||
import { MessageViewer, getClient } from '@hcengineering/presentation'
|
||||
|
||||
import documentsRes from '../../../plugin'
|
||||
import GuidanceEditing from '../../icons/GuidanceEditing.svelte'
|
||||
import { type GuidanceEditorMode } from '../../../utils'
|
||||
|
||||
export let section: DocumentSection
|
||||
export let index: number
|
||||
export let width: string | undefined = undefined
|
||||
export let mode: GuidanceEditorMode = 'readonly'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function handleCloseClick (): void {
|
||||
const reopenMode: GuidanceEditorMode | undefined = mode === 'canEdit' ? 'editing' : undefined
|
||||
close(reopenMode)
|
||||
}
|
||||
|
||||
function close (reopenMode?: GuidanceEditorMode): void {
|
||||
dispatch('close', { reopenMode, guidance: reopenMode == null ? guidance : undefined })
|
||||
}
|
||||
|
||||
export function onOutsideClick (): void {
|
||||
close()
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
|
||||
const title = `${index}. ${section.title}`
|
||||
let guidance = h.as(section, documentsRes.mixin.DocumentTemplateSection).guidance
|
||||
|
||||
let textArea: StyledTextArea
|
||||
</script>
|
||||
|
||||
<ModernDialog
|
||||
label={getEmbeddedLabel(title)}
|
||||
closeIcon={mode !== 'canEdit' ? IconClose : GuidanceEditing}
|
||||
withoutFooter
|
||||
on:close={handleCloseClick}
|
||||
{width}
|
||||
shadow={mode !== 'editing'}
|
||||
>
|
||||
{#if mode === 'editing'}
|
||||
<StyledTextArea bind:this={textArea} isScrollable={false} bind:content={guidance} showButtons={false} />
|
||||
{:else}
|
||||
<!-- TODO: use StyledTextArea.setEditable when it's fixed -->
|
||||
<MessageViewer message={guidance ?? ''} />
|
||||
{/if}
|
||||
</ModernDialog>
|
||||
+10
-18
@@ -5,35 +5,27 @@
|
||||
import { Ref, generateId } from '@hcengineering/core'
|
||||
import { ReferenceInput } from '@hcengineering/text-editor-resources'
|
||||
|
||||
import {
|
||||
addDocumentCommentFx,
|
||||
$controlledDocumentSections as controlledDocumentSections
|
||||
} from '../../../stores/editors/document'
|
||||
import { addDocumentCommentFx } from '../../../stores/editors/document'
|
||||
|
||||
export let sectionKey: string
|
||||
export let nodeId: string | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let messageId: Ref<ChatMessage> = generateId()
|
||||
|
||||
$: section = $controlledDocumentSections.find((section) => section.key === sectionKey)
|
||||
|
||||
async function handleMessage (event: CustomEvent<string>): Promise<void> {
|
||||
const comment = await addDocumentCommentFx({ content: event.detail, section, messageId, nodeId })
|
||||
const comment = await addDocumentCommentFx({ content: event.detail, messageId, nodeId })
|
||||
messageId = generateId()
|
||||
|
||||
dispatch('close', comment)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if section}
|
||||
<div class="text-editor-popup w-85">
|
||||
<ReferenceInput
|
||||
focusable
|
||||
kindSend="primary"
|
||||
placeholder={chunter.string.AddCommentPlaceholder}
|
||||
on:message={handleMessage}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-editor-popup w-85">
|
||||
<ReferenceInput
|
||||
focusable
|
||||
kindSend="primary"
|
||||
placeholder={chunter.string.AddCommentPlaceholder}
|
||||
on:message={handleMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+3
-4
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { $groupedDocumentComments as groupedDocumentComments } from '../../../stores/editors/document'
|
||||
import { $documentComments as documentComments } from '../../../stores/editors/document'
|
||||
import DocumentCommentThread from '../right-panel/DocumentCommentThread.svelte'
|
||||
|
||||
export let sectionKey: string
|
||||
export let nodeId: string | undefined
|
||||
|
||||
$: documentComments = $groupedDocumentComments.getDocumentComments(sectionKey, nodeId)
|
||||
$: commentsForNode = $documentComments.filter((c) => c.nodeId === nodeId)
|
||||
</script>
|
||||
|
||||
<div class="text-editor-popup max-h-80 min-w-100 overflow-y-auto">
|
||||
{#each documentComments as object}
|
||||
{#each commentsForNode as object}
|
||||
<DocumentCommentThread value={object} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
-36
@@ -1,36 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentsDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getPanelURI, Icon } from '@hcengineering/ui'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<AttachmentsDocumentSection> | undefined
|
||||
export let inline = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href="#{getPanelURI(documents.component.EditDoc, value.attachedTo, value.attachedToClass, 'content')}"
|
||||
class:inline-presenter={inline}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={documents.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap">{value.title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
-36
@@ -1,36 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getPanelURI, Icon } from '@hcengineering/ui'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<CollaborativeDocumentSection> | undefined
|
||||
export let inline = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href="#{getPanelURI(documents.component.EditDoc, value.attachedTo, value.attachedToClass, 'content')}"
|
||||
class:inline-presenter={inline}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={documents.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap">{value.title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
+2
-15
@@ -6,23 +6,15 @@
|
||||
<script lang="ts">
|
||||
import documents, { DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import { ThreadView } from '@hcengineering/chunter-resources'
|
||||
import { Button, IconCheckCircle } from '@hcengineering/ui'
|
||||
import { Button, IconCheckCircle, Label } from '@hcengineering/ui'
|
||||
import {
|
||||
$canAddDocumentCommentsFeedback as canAddDocumentCommentsFeedback,
|
||||
$controlledDocumentSectionIds as controlledDocumentSectionIds,
|
||||
$controlledDocumentSections as controlledDocumentSections,
|
||||
resolveCommentFx
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
export let value: DocumentComment | undefined
|
||||
export let highlighted = false
|
||||
|
||||
$: section =
|
||||
value !== undefined
|
||||
? $controlledDocumentSections.find((item) => value !== undefined && item.key === value.sectionKey)
|
||||
: null
|
||||
$: sectionIndex = section != null ? $controlledDocumentSectionIds.indexOf(section._id) + 1 : null
|
||||
|
||||
const handleResolveComment = async (): Promise<void> => {
|
||||
if (value === undefined) {
|
||||
return
|
||||
@@ -45,12 +37,7 @@
|
||||
<span>#{value.index}</span>
|
||||
<span>•</span>
|
||||
{/if}
|
||||
{#if sectionIndex !== null}
|
||||
<span>{sectionIndex}.</span>
|
||||
{/if}
|
||||
{#if section}
|
||||
<span>{section.title}</span>
|
||||
{/if}
|
||||
<Label label={resolved ? documents.string.Resolved : documents.string.Pending} />
|
||||
</div>
|
||||
{#if $canAddDocumentCommentsFeedback}
|
||||
<div class="tools">
|
||||
|
||||
+2
-5
@@ -54,12 +54,7 @@
|
||||
})
|
||||
|
||||
const handleItemHighlight = (item: DocumentComment) => () => {
|
||||
if (item.sectionKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsLocationNavigateRequested({
|
||||
sectionKey: item.sectionKey,
|
||||
nodeId: item.nodeId ?? null
|
||||
})
|
||||
}
|
||||
@@ -84,10 +79,12 @@
|
||||
</RightPanelTabHeader>
|
||||
{#if $documentComments.length > 0}
|
||||
{#each $documentComments as object}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={elements[object._id]}
|
||||
on:click={handleItemHighlight(object)}
|
||||
on:keydown={handleItemHighlight(object)}
|
||||
data-testid="comment"
|
||||
>
|
||||
<DocumentCommentThread
|
||||
value={object}
|
||||
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import documents, {
|
||||
DocumentTemplate,
|
||||
DocumentSection,
|
||||
DocumentTemplateSection
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { Class, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, getEventPopupPositionElement, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import Scroller from '@hcengineering/ui/src/components/Scroller.svelte'
|
||||
import SelectPopup from '@hcengineering/ui/src/components/SelectPopup.svelte'
|
||||
import SectionEditor from './SectionEditor.svelte'
|
||||
import document from '../../plugin'
|
||||
import { appendSection } from '../../utils'
|
||||
|
||||
export let documentObject: DocumentTemplate
|
||||
export let readonly = false
|
||||
export let withEditing = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let sections: DocumentTemplateSection[] = []
|
||||
const sectionsQuery = createQuery()
|
||||
$: sectionsQuery.query(
|
||||
documents.mixin.DocumentTemplateSection,
|
||||
{ attachedTo: documentObject._id, attachedToClass: documentObject._class },
|
||||
(res) => {
|
||||
sections = res
|
||||
},
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
async function onAddSection (evt: MouseEvent): Promise<void> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
const sectionTypes = hierarchy
|
||||
.getDescendants(documents.class.DocumentSection)
|
||||
.filter((sectionDcnt) => sectionDcnt !== documents.class.DocumentSection && !hierarchy.isMixin(sectionDcnt))
|
||||
.map((sectionClass) => hierarchy.getClass(sectionClass) as Class<DocumentSection>)
|
||||
|
||||
const pos = getEventPopupPositionElement(evt)
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{ value: sectionTypes.map((section) => ({ id: section._id, label: section.label, icon: section.icon })) },
|
||||
pos,
|
||||
async (res) => {
|
||||
if (res != null) {
|
||||
const s = sectionTypes.find((section) => section._id === res)
|
||||
if (s !== undefined) {
|
||||
await appendSection(documentObject, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller autoscroll>
|
||||
<div class="antiSection mr-3">
|
||||
<div class="antiSection-header">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={document.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title"><Label label={document.string.Sections} /></span>
|
||||
{#if !readonly}
|
||||
<div class="buttons-group small-gap">
|
||||
<Button icon={IconAdd} kind="ghost" shape={'circle'} on:click={onAddSection} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="antiAccordion mt-3">
|
||||
{#each sections as section, i}
|
||||
<SectionEditor value={section} index={i} document={documentObject} {readonly} {withEditing} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { DocumentTemplateSection } from '@hcengineering/controlled-documents'
|
||||
import { Card } from '@hcengineering/presentation'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import document from '../../plugin'
|
||||
|
||||
export let section: DocumentTemplateSection
|
||||
// const client = getClient()
|
||||
// const attrContext = client.getHierarchy().getAttribute(document.class.ContentSection, 'guidance')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let updatedValue = section.guidance ?? ''
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={document.string.Guidance}
|
||||
okLabel={textEditor.string.Save}
|
||||
okAction={() => {
|
||||
// updateAttribute(
|
||||
// client,
|
||||
// section,
|
||||
// document.class.ContentSection,
|
||||
// { key: 'guidance', attr: attrContext },
|
||||
// updatedValue
|
||||
// )
|
||||
}}
|
||||
canSave={updatedValue !== (section.guidance ?? '')}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="mt-4">
|
||||
<StyledTextBox
|
||||
bind:content={updatedValue}
|
||||
showButtons={true}
|
||||
placeholder={textEditor.string.EditorPlaceholder}
|
||||
alwaysEdit={true}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -1,124 +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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Mixin, Ref } from '@hcengineering/core'
|
||||
import presentation, {
|
||||
AttributesBar,
|
||||
KeyedAttribute,
|
||||
getClient,
|
||||
getFiltredKeys,
|
||||
isCollectionAttr
|
||||
} from '@hcengineering/presentation'
|
||||
import { Button, Component, EditBox, IconMoreH } from '@hcengineering/ui'
|
||||
import documents, { Document, DocumentTemplateSection } from '@hcengineering/controlled-documents'
|
||||
import documentsResources from '../../plugin'
|
||||
import {
|
||||
$collapsedDocumentSectionIds as collapsedSectionIds,
|
||||
documentSectionToggled
|
||||
} from '../../stores/editors/document'
|
||||
import FieldSectionEditor from '../FieldSectionEditor.svelte'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
|
||||
export let value: DocumentTemplateSection
|
||||
export let index: number
|
||||
export let document: Document
|
||||
export let readonly = false
|
||||
export let withEditing = false
|
||||
|
||||
let editing = !withEditing
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: editor = hierarchy.as(hierarchy.getClass(value._class), documents.mixin.DocumentSectionEditor)?.editor
|
||||
|
||||
$: classLabel = hierarchy.getClass(value._class).label
|
||||
|
||||
async function updateTitle () {
|
||||
await client.update(value, { title: value.title.trim() })
|
||||
}
|
||||
|
||||
function getSectionMixinKeys (mixin: Ref<Mixin<Doc>>, ignoreKeys?: string[]): KeyedAttribute[] {
|
||||
const filtredKeys = getFiltredKeys(hierarchy, mixin, [], documents.class.DocumentSection)
|
||||
|
||||
return filtredKeys.filter((ka) => !isCollectionAttr(hierarchy, ka) && !ignoreKeys?.includes(ka.key))
|
||||
}
|
||||
</script>
|
||||
|
||||
<FieldSectionEditor
|
||||
editable={!readonly}
|
||||
sectionType={classLabel}
|
||||
expanded={!$collapsedSectionIds.has(value._id)}
|
||||
on:toggle={() => documentSectionToggled(value._id)}
|
||||
>
|
||||
<svelte:fragment slot="index">
|
||||
{index + 1}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="flex">
|
||||
-
|
||||
<div class="mr-2" style="overflow: hidden">
|
||||
<EditBox
|
||||
bind:value={value.title}
|
||||
fullSize
|
||||
disabled={readonly || !editing}
|
||||
placeholder={documentsResources.string.TemplateSectionTitle}
|
||||
kind="editbox"
|
||||
on:blur={updateTitle}
|
||||
/>
|
||||
</div>
|
||||
{#if withEditing}
|
||||
<div class="mr-1">
|
||||
<Button
|
||||
kind="regular"
|
||||
label={editing ? presentation.string.Save : documentsResources.string.EditMode}
|
||||
on:click={() => (editing = !editing)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if editor}
|
||||
<div class="flex flex-grow">
|
||||
<div class="mr-2 flex-grow">
|
||||
<Component is={editor} props={{ value, document, editable: !readonly && editing }} />
|
||||
</div>
|
||||
{#if !readonly}
|
||||
<div class="mt-3 left-divider px-3 max-w-85">
|
||||
<AttributesBar
|
||||
object={value}
|
||||
_class={documents.mixin.DocumentTemplateSection}
|
||||
keys={getSectionMixinKeys(documents.mixin.DocumentTemplateSection, ['title'])}
|
||||
readonly={!editing}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="tools">
|
||||
{#if !readonly}
|
||||
<Button
|
||||
icon={IconMoreH}
|
||||
kind="ghost"
|
||||
size={'medium'}
|
||||
on:click={(evt) => {
|
||||
showMenu(evt, value)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</FieldSectionEditor>
|
||||
@@ -38,7 +38,6 @@ import documents, {
|
||||
type Project,
|
||||
DocumentState,
|
||||
getCollaborativeDocForDocument,
|
||||
createDocSections,
|
||||
createChangeControl
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import documentsRes from './plugin'
|
||||
@@ -95,7 +94,6 @@ export async function createNewDraftForControlledDoc (
|
||||
reviewInterval: document.reviewInterval,
|
||||
changeControl: newCCId,
|
||||
requests: 0,
|
||||
sections: 0,
|
||||
labels: 0,
|
||||
state: DocumentState.Draft,
|
||||
plannedEffectiveDate: 0,
|
||||
@@ -139,8 +137,6 @@ export async function createNewDraftForControlledDoc (
|
||||
await copyDocument(document.content, collaborativeDoc)
|
||||
}
|
||||
|
||||
await createDocSections(client, newDraftDocId, document._id, space, documents.class.ControlledDocument)
|
||||
|
||||
const documentTraining = getDocumentTraining(hierarchy, document)
|
||||
if (documentTraining !== undefined) {
|
||||
const newDraftDoc = await client.findOne(document._class, { _id: newDraftDocId })
|
||||
@@ -180,22 +176,13 @@ export async function createDocumentSnapshotAndEdit (client: TxOperations, docum
|
||||
name,
|
||||
state: document.state,
|
||||
controlledState: document.controlledState,
|
||||
content: snapshot,
|
||||
sections: 0
|
||||
content: snapshot
|
||||
},
|
||||
newSnapshotId
|
||||
)
|
||||
|
||||
await op.commit()
|
||||
|
||||
await createDocSections(
|
||||
client,
|
||||
newSnapshotId,
|
||||
document._id,
|
||||
document.space,
|
||||
documents.class.ControlledDocumentSnapshot
|
||||
)
|
||||
|
||||
await client.update(document, { controlledState: undefined })
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import contact from '@hcengineering/contact'
|
||||
import {
|
||||
type Class,
|
||||
type Client,
|
||||
@@ -24,18 +23,14 @@ import {
|
||||
type Doc
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
type CollaborativeDocumentSection,
|
||||
type Document,
|
||||
type DocumentSection,
|
||||
type DocumentSpace,
|
||||
DocumentState,
|
||||
type DocumentMeta
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import { type ObjectSearchResult, getClient, MessageBox, getCollaboratorClient } from '@hcengineering/presentation'
|
||||
import { EmptyMarkup } from '@hcengineering/text'
|
||||
import { type ObjectSearchResult, getClient, MessageBox } from '@hcengineering/presentation'
|
||||
import { showPopup } from '@hcengineering/ui'
|
||||
import { deleteObjects } from '@hcengineering/view-resources'
|
||||
|
||||
import CreateDocument from './components/CreateDocument.svelte'
|
||||
import QmsDocumentWizard from './components/create-doc/QmsDocumentWizard.svelte'
|
||||
@@ -64,22 +59,12 @@ import DocumentIcon from './components/icons/DocumentIcon.svelte'
|
||||
|
||||
import DocumentTemplates from './components/DocumentTemplates.svelte'
|
||||
|
||||
import EditTemplateSections from './components/template/EditTemplateSections.svelte'
|
||||
|
||||
import Categories from './components/Categories.svelte'
|
||||
import EditDocumentCategory from './components/EditDocumentCategory.svelte'
|
||||
|
||||
import DocumentTitle from './components/document/DocumentTitle.svelte'
|
||||
import EditDocContent from './components/document/EditDocContent.svelte'
|
||||
import CollaborativeSectionEditor from './components/document/editors/CollaborativeSectionEditor.svelte'
|
||||
import AttachmentsSectionEditor from './components/document/editors/AttachmentsSectionEditor.svelte'
|
||||
|
||||
import CollaborativeSectionPresenter from './components/document/presenters/CollaborativeSectionPresenter.svelte'
|
||||
import AttachmentsSectionPresenter from './components/document/presenters/AttachmentsSectionPresenter.svelte'
|
||||
import DocumentVersionPresenter from './components/document/presenters/DocumentVersionPresenter.svelte'
|
||||
|
||||
import DocumentSectionDeletePopup from './components/DocumentSectionDeletePopup.svelte'
|
||||
|
||||
import DocumentReviewRequest from './components/requests/DocumentReviewRequest.svelte'
|
||||
import DocumentReviewRequestPresenter from './components/requests/DocumentReviewRequestPresenter.svelte'
|
||||
import DocumentApprovalRequest from './components/requests/DocumentApprovalRequest.svelte'
|
||||
@@ -94,17 +79,12 @@ import ProjectPresenter from './components/project/ProjectPresenter.svelte'
|
||||
import ProjectRefPresenter from './components/project/ProjectRefPresenter.svelte'
|
||||
|
||||
import documents from './plugin'
|
||||
import { documentSectionDescriptionEditingRequested } from './stores/editors/document'
|
||||
import './styles/_colors.scss'
|
||||
import { resolveLocation } from './navigation'
|
||||
import {
|
||||
addSectionBetween,
|
||||
createAttachmentsSection,
|
||||
createCollaborativeSection,
|
||||
getVisibleFilters,
|
||||
sortDocumentStates,
|
||||
getAllDocumentStates,
|
||||
openGuidanceEditor,
|
||||
canChangeDocumentOwner,
|
||||
canDeleteDocumentCategory,
|
||||
canCreateChildTemplate,
|
||||
@@ -151,88 +131,6 @@ async function queryDocumentMeta (
|
||||
).map(toObjectSearchResult)
|
||||
}
|
||||
|
||||
async function addSection (
|
||||
section: CollaborativeDocumentSection,
|
||||
addAfter: boolean = true,
|
||||
copyFrom?: DocumentSection
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const doc = await client.findOne(section.attachedToClass, { _id: section.attachedTo })
|
||||
if (doc === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const sections = await client.findAll(
|
||||
documents.class.DocumentSection,
|
||||
{ attachedTo: section.attachedTo },
|
||||
{ sort: { rank: !addAfter ? SortingOrder.Ascending : SortingOrder.Descending } }
|
||||
)
|
||||
if (sections === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let prevSection: DocumentSection | undefined
|
||||
for (const s of sections) {
|
||||
if (s._id === section._id) {
|
||||
break
|
||||
}
|
||||
|
||||
prevSection = s
|
||||
}
|
||||
|
||||
const [first, second] = !addAfter ? [prevSection, section] : [section, prevSection]
|
||||
const sectionClass = client.getHierarchy().getClass(section._class)
|
||||
await addSectionBetween(client, doc as Document, sectionClass, first, second, copyFrom)
|
||||
}
|
||||
|
||||
async function addCollaborativeSectionAbove (section: CollaborativeDocumentSection): Promise<void> {
|
||||
await addSection(section, false)
|
||||
}
|
||||
|
||||
async function addCollaborativeSectionBelow (section: CollaborativeDocumentSection): Promise<void> {
|
||||
await addSection(section)
|
||||
}
|
||||
|
||||
async function deleteCollaborativeSection (section: CollaborativeDocumentSection): Promise<void> {
|
||||
showPopup(
|
||||
contact.component.DeleteConfirmationPopup,
|
||||
{
|
||||
object: section,
|
||||
deleteAction: async () => {
|
||||
const client = getClient()
|
||||
|
||||
const document = await client.findOne(documents.class.Document, { _id: section.attachedTo as Ref<Document> })
|
||||
await deleteObjects(client, [section], false)
|
||||
|
||||
if (document !== undefined) {
|
||||
await getCollaboratorClient().updateContent(document.content, section.collaboratorSectionId, EmptyMarkup)
|
||||
}
|
||||
}
|
||||
},
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
||||
async function duplicateSection (section: CollaborativeDocumentSection): Promise<void> {
|
||||
await addSection(section, true, section)
|
||||
}
|
||||
|
||||
async function editSectionDescription (section: CollaborativeDocumentSection): Promise<void> {
|
||||
documentSectionDescriptionEditingRequested(section._id)
|
||||
}
|
||||
|
||||
async function editSectionGuidance (section: CollaborativeDocumentSection): Promise<void> {
|
||||
const client = getClient()
|
||||
const sections = await client.findAll(
|
||||
documents.class.DocumentSection,
|
||||
{ attachedTo: section.attachedTo },
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
const sectionIndex = sections.findIndex((s) => s._id === section._id)
|
||||
openGuidanceEditor(client, section, sectionIndex + 1, 'editing')
|
||||
}
|
||||
|
||||
async function deleteDocuments (obj: Document | Document[]): Promise<void> {
|
||||
const docs = Array.isArray(obj) ? obj : [obj]
|
||||
const docNames = docs.map((d) => `${d.title} (${d.prefix}-${d.seqNumber})`).join(', ')
|
||||
@@ -322,16 +220,10 @@ export default async (): Promise<Resources> => ({
|
||||
NewDocumentHeader,
|
||||
MyDocuments,
|
||||
DocumentTemplates,
|
||||
EditTemplateSections,
|
||||
CategoryPresenter,
|
||||
Categories,
|
||||
EditDocumentCategory,
|
||||
EditDocContent,
|
||||
CollaborativeSectionEditor,
|
||||
AttachmentsSectionEditor,
|
||||
DocumentSectionDeletePopup,
|
||||
CollaborativeSectionPresenter,
|
||||
AttachmentsSectionPresenter,
|
||||
DocumentReviewRequest,
|
||||
DocumentReviewRequestPresenter,
|
||||
DocumentApprovalRequest,
|
||||
@@ -365,8 +257,6 @@ export default async (): Promise<Resources> => ({
|
||||
CanCreateChildTemplate: canCreateChildTemplate,
|
||||
CanCreateChildDocument: canCreateChildDocument,
|
||||
CanDeleteDocumentCategory: canDeleteDocumentCategory,
|
||||
CollaborativeSectionCreator: createCollaborativeSection,
|
||||
AttachmentsSectionCreator: createAttachmentsSection,
|
||||
GetVisibleFilters: getVisibleFilters,
|
||||
DocumentStateSort: sortDocumentStates,
|
||||
GetAllDocumentStates: getAllDocumentStates,
|
||||
@@ -378,12 +268,6 @@ export default async (): Promise<Resources> => ({
|
||||
IsCommentVisible: isCommentVisible
|
||||
},
|
||||
actionImpl: {
|
||||
AddCollaborativeSectionAbove: addCollaborativeSectionAbove,
|
||||
AddCollaborativeSectionBelow: addCollaborativeSectionBelow,
|
||||
DeleteCollaborativeSection: deleteCollaborativeSection,
|
||||
Duplicate: duplicateSection,
|
||||
EditDescription: editSectionDescription,
|
||||
EditGuidance: editSectionGuidance,
|
||||
CreateChildDocument: createChildDocument,
|
||||
CreateChildTemplate: createChildTemplate,
|
||||
CreateDocument: createDocument,
|
||||
|
||||
@@ -14,12 +14,11 @@
|
||||
//
|
||||
import documents, {
|
||||
type Document,
|
||||
type DocumentSection,
|
||||
type DocumentState,
|
||||
type DocumentSpace,
|
||||
documentsId
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Client, type AttachedData, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { type Client, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
|
||||
import { type AnyComponent, type Location } from '@hcengineering/ui'
|
||||
import type { KeyFilter, SortFunc, ViewActionAvailabilityFunction } from '@hcengineering/view'
|
||||
@@ -32,10 +31,6 @@ export default mergeIds(documentsId, documents, {
|
||||
ProjectPresenter: '' as AnyComponent,
|
||||
ProjectRefPresenter: '' as AnyComponent,
|
||||
Revision: '' as AnyComponent,
|
||||
EditTemplateSections: '' as AnyComponent,
|
||||
CollaborativeSectionEditor: '' as AnyComponent,
|
||||
AttachmentsSectionEditor: '' as AnyComponent,
|
||||
DocumentSectionDeletePopup: '' as AnyComponent,
|
||||
|
||||
// New model components
|
||||
EditDocumentCategory: '' as AnyComponent,
|
||||
@@ -86,7 +81,6 @@ export default mergeIds(documentsId, documents, {
|
||||
DocumentPrefix: '' as IntlString,
|
||||
DocumentCodePlaceholder: '' as IntlString,
|
||||
DocumentPrefixPlaceholder: '' as IntlString,
|
||||
TemplateSectionTitle: '' as IntlString,
|
||||
DocumentTemplateCreateLabel: '' as IntlString,
|
||||
DomainTitle: '' as IntlString,
|
||||
MetaAbstract: '' as IntlString,
|
||||
@@ -115,12 +109,6 @@ export default mergeIds(documentsId, documents, {
|
||||
MakeDocumentEffective: '' as IntlString,
|
||||
CreateDraftQmsTemplates: '' as IntlString,
|
||||
|
||||
DocTemplateDeleteSectionTitle: '' as IntlString,
|
||||
DocTemplateDeleteSectionConfirm: '' as IntlString,
|
||||
|
||||
AddSectionAbove: '' as IntlString,
|
||||
AddSectionBelow: '' as IntlString,
|
||||
|
||||
SelectReviewers: '' as IntlString,
|
||||
SelectApprovers: '' as IntlString,
|
||||
RequestsToReviewTheDoc: '' as IntlString,
|
||||
@@ -139,9 +127,6 @@ export default mergeIds(documentsId, documents, {
|
||||
NoApprovalsDescription: '' as IntlString,
|
||||
CurrentVersion: '' as IntlString,
|
||||
|
||||
EditDescription: '' as IntlString,
|
||||
EditGuidance: '' as IntlString,
|
||||
|
||||
NewDocument: '' as IntlString,
|
||||
NewDocumentCategory: '' as IntlString,
|
||||
NewDocumentTemplate: '' as IntlString,
|
||||
@@ -151,8 +136,6 @@ export default mergeIds(documentsId, documents, {
|
||||
InfoStepTitle: '' as IntlString,
|
||||
TeamStepTitle: '' as IntlString,
|
||||
|
||||
Duplicate: '' as IntlString,
|
||||
|
||||
TitleAndDescr: '' as IntlString,
|
||||
AbstractPlaceholder: '' as IntlString,
|
||||
NewDocCreation: '' as IntlString,
|
||||
@@ -236,13 +219,6 @@ export default mergeIds(documentsId, documents, {
|
||||
ToReview: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
AttachmentsSectionCreator: '' as Resource<
|
||||
(
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: DocumentSection
|
||||
) => AttachedData<DocumentSection>
|
||||
>,
|
||||
CanCreateTemplate: '' as Resource<ViewActionAvailabilityFunction<DocumentSpace>>,
|
||||
CanCreateDocument: '' as Resource<ViewActionAvailabilityFunction<DocumentSpace>>,
|
||||
CanCreateChildTemplate: '' as Resource<ViewActionAvailabilityFunction<Document>>,
|
||||
@@ -251,13 +227,6 @@ export default mergeIds(documentsId, documents, {
|
||||
CheckAreTemplatesDisabled: '' as Resource<() => Promise<boolean>>,
|
||||
CheckAreDomainsDisabled: '' as Resource<() => Promise<boolean>>,
|
||||
CheckIsLibraryDisabled: '' as Resource<() => Promise<boolean>>,
|
||||
CollaborativeSectionCreator: '' as Resource<
|
||||
(
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: DocumentSection
|
||||
) => AttachedData<DocumentSection>
|
||||
>,
|
||||
DocumentStateSort: '' as SortFunc,
|
||||
GetAllDocumentStates: '' as Resource<() => Promise<DocumentState[]>>,
|
||||
GetVisibleFilters: '' as Resource<(filters: KeyFilter[], space?: Ref<Space>) => Promise<KeyFilter[]>>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { attach, sample } from 'effector'
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { type ChatMessage } from '@hcengineering/chunter'
|
||||
import { type EditorMode, type DocumentSection, DocumentState } from '@hcengineering/controlled-documents'
|
||||
import { type EditorMode, DocumentState } from '@hcengineering/controlled-documents'
|
||||
import { isDocumentCommentAttachedTo } from '../../utils'
|
||||
import {
|
||||
RightPanelTab,
|
||||
@@ -41,15 +41,12 @@ export * from './document/documentComments'
|
||||
export * from './document/comparison'
|
||||
export * from './document/editor'
|
||||
export * from './document/query'
|
||||
export * from './document/section'
|
||||
export * from './document/types'
|
||||
|
||||
export const addDocumentCommentFx = attach({
|
||||
source: { document: $controlledDocument },
|
||||
effect: addCommentFx,
|
||||
mapParams: (
|
||||
payload: {
|
||||
section?: DocumentSection
|
||||
nodeId?: string
|
||||
content: string
|
||||
messageId?: Ref<ChatMessage>
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
type DocumentApprovalRequest,
|
||||
type ControlledDocumentSnapshot,
|
||||
type DocumentReviewRequest,
|
||||
type DocumentSection,
|
||||
type DocumentSnapshot,
|
||||
type EditorMode,
|
||||
type Project,
|
||||
@@ -55,10 +54,6 @@ export const controlledDocumentOpened = createEvent<{
|
||||
}>(generateActionName('controlledDocumentOpened'))
|
||||
export const controlledDocumentClosed = createEvent(generateActionName('controlledDocumentClosed'))
|
||||
|
||||
export const controlledDocumentSectionsUpdated = createEvent<DocumentSection[]>(
|
||||
generateActionName('controlledDocumentSectionsUpdated')
|
||||
)
|
||||
|
||||
export const controlledDocumentUpdated = createEvent<WithLookup<ControlledDocument>>(
|
||||
generateActionName('controlledDocumentUpdated')
|
||||
)
|
||||
@@ -83,19 +78,6 @@ export const documentCommentsShowResolvedToggled = createEvent(
|
||||
generateActionName('documentCommentsShowResolvedToggled')
|
||||
)
|
||||
|
||||
export const documentSectionToggled = createEvent<Ref<DocumentSection>>(generateActionName('documentSectionToggled'))
|
||||
export const documentSectionCollapsed = createEvent<Ref<DocumentSection>>(
|
||||
generateActionName('documentSectionCollapsed')
|
||||
)
|
||||
export const documentSectionExpanded = createEvent<Ref<DocumentSection>>(generateActionName('documentSectionExpanded'))
|
||||
|
||||
export const documentSectionDescriptionEditingRequested = createEvent<Ref<DocumentSection>>(
|
||||
generateActionName('documentSectionDescriptionEditingRequested')
|
||||
)
|
||||
export const documentSectionDescriptionEditingCompleted = createEvent(
|
||||
generateActionName('documentSectionDescriptionEditingCompleted')
|
||||
)
|
||||
|
||||
export const comparisonCleared = createEvent(generateActionName('comparisonCleared'))
|
||||
|
||||
export const comparisonRequested = createEvent<Document | DocumentSnapshot>(generateActionName('comparisonRequested'))
|
||||
@@ -112,7 +94,6 @@ export const savedAttachmentsUpdated = createEvent<Array<Ref<Attachment>>>(
|
||||
|
||||
export const documentCommentsDisplayRequested = createEvent<{
|
||||
nodeId?: string | null
|
||||
sectionKey: string
|
||||
element?: PopupAlignment
|
||||
}>(generateActionName('documentCommentsDisplayRequested'))
|
||||
|
||||
@@ -122,11 +103,9 @@ export const documentCommentsNavigateRequested = createEvent<{ value: DocumentCo
|
||||
|
||||
export const documentCommentsLocationNavigateRequested = createEvent<{
|
||||
nodeId?: string | null
|
||||
sectionKey: string
|
||||
}>(generateActionName('documentCommentsLocationNavigateRequested'))
|
||||
|
||||
export const documentCommentsHighlightUpdated = createEvent<{
|
||||
sectionKey: string | null
|
||||
nodeId?: string | null
|
||||
} | null>(generateActionName('documentCommentsHighlightUpdated'))
|
||||
|
||||
|
||||
@@ -11,38 +11,10 @@
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { createEffect, createStore, forward } from 'effector'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { SortingOrder } from '@hcengineering/core'
|
||||
import documents, {
|
||||
type Document,
|
||||
type DocumentSection,
|
||||
type DocumentSnapshot
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { createStore } from 'effector'
|
||||
import { type Document, type DocumentSnapshot } from '@hcengineering/controlled-documents'
|
||||
import { comparisonCleared, comparisonRequested, controlledDocumentClosed } from './actions'
|
||||
|
||||
export const loadComparedDocumentSectionsFx = createEffect(
|
||||
async (object: Document | DocumentSnapshot): Promise<DocumentSection[]> => {
|
||||
if (object === null || object === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return await getClient().findAll(
|
||||
documents.class.DocumentSection,
|
||||
{
|
||||
attachedTo: object._id
|
||||
},
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export const $comparedDocument = createStore<Document | DocumentSnapshot | null>(null)
|
||||
.on(comparisonRequested, (_, payload) => payload)
|
||||
.reset([controlledDocumentClosed, comparisonCleared])
|
||||
|
||||
export const $comparedDocumentSections = createStore<DocumentSection[]>([])
|
||||
.on(loadComparedDocumentSectionsFx.doneData, (_, payload) => payload)
|
||||
.reset([controlledDocumentClosed, comparisonCleared])
|
||||
|
||||
forward({ from: comparisonRequested, to: loadComparedDocumentSectionsFx })
|
||||
|
||||
+23
-121
@@ -11,16 +11,12 @@
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { createEffect, createStore, forward, sample } from 'effector'
|
||||
import { createEffect, createStore, forward } from 'effector'
|
||||
import { type Attachment } from '@hcengineering/attachment'
|
||||
import { type Ref, generateId } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type CompAndProps, type PopupAlignment, popupstore, showPopup } from '@hcengineering/ui'
|
||||
import documents, {
|
||||
type Document,
|
||||
type DocumentSection,
|
||||
type DocumentComment
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import documents, { type Document, type DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import { isDocumentCommentAttachedTo } from '../../../utils'
|
||||
import {
|
||||
DocumentCommentPopupCategory,
|
||||
@@ -35,75 +31,8 @@ import {
|
||||
documentCommentsSortByChanged,
|
||||
documentCommentsUpdated,
|
||||
controlledDocumentClosed,
|
||||
documentSectionExpanded,
|
||||
savedAttachmentsUpdated
|
||||
} from './actions'
|
||||
import { $controlledDocumentSections } from './editor'
|
||||
|
||||
// TODO: add support for Document-level attachments
|
||||
class SectionDocumentCommentsData {
|
||||
private readonly _headerComments: DocumentComment[] = []
|
||||
private readonly _nodeComments: Record<string, DocumentComment[]> = {}
|
||||
|
||||
public get sectionComments (): DocumentComment[] {
|
||||
return this._headerComments
|
||||
}
|
||||
|
||||
public get nodeComments (): Record<string, DocumentComment[]> {
|
||||
return this._nodeComments
|
||||
}
|
||||
|
||||
public addComment (item: DocumentComment): void {
|
||||
if (item.resolved === true) {
|
||||
return
|
||||
}
|
||||
|
||||
const nodeId = item.nodeId
|
||||
if (nodeId === null || nodeId === undefined) {
|
||||
this._headerComments.push(item)
|
||||
} else {
|
||||
if (this._nodeComments[nodeId] === undefined) {
|
||||
this._nodeComments[nodeId] = []
|
||||
}
|
||||
|
||||
this._nodeComments[nodeId].push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GroupedDocumentCommentsData {
|
||||
private readonly _data: Record<string, SectionDocumentCommentsData> = {}
|
||||
|
||||
constructor (data: DocumentComment[]) {
|
||||
data?.forEach((item) => {
|
||||
if (item.sectionKey !== undefined) {
|
||||
const sectionKey = item.sectionKey
|
||||
if (this._data[sectionKey] === undefined) {
|
||||
this._data[sectionKey] = new SectionDocumentCommentsData()
|
||||
}
|
||||
|
||||
this._data[sectionKey].addComment(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getDocumentComments (sectionKey: string, nodeId?: string | null): DocumentComment[] {
|
||||
const sectionData = this._data[sectionKey]
|
||||
if (sectionData === undefined || sectionData === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (nodeId === undefined || nodeId === null) {
|
||||
return sectionData.sectionComments
|
||||
}
|
||||
|
||||
return sectionData.nodeComments[nodeId] ?? []
|
||||
}
|
||||
|
||||
public hasDocumentComments (sectionKey: string, nodeId?: string | null): boolean {
|
||||
return this.getDocumentComments(sectionKey, nodeId).length > 0
|
||||
}
|
||||
}
|
||||
|
||||
export const $areDocumentCommentPopupsOpened = createStore(false).on(
|
||||
documentCommentPopupsOpened,
|
||||
@@ -118,12 +47,11 @@ popupstore.subscribe((popupProps: CompAndProps[]) => {
|
||||
export const addCommentFx = createEffect(
|
||||
async (payload: {
|
||||
document: Document | null
|
||||
section?: DocumentSection
|
||||
content: string
|
||||
nodeId?: string
|
||||
messageId?: Ref<DocumentComment>
|
||||
}) => {
|
||||
const { content, document, nodeId, section } = payload
|
||||
const { content, document, nodeId } = payload
|
||||
|
||||
if (document == null || content == null) {
|
||||
return
|
||||
@@ -146,7 +74,6 @@ export const addCommentFx = createEffect(
|
||||
{
|
||||
message: content,
|
||||
nodeId,
|
||||
sectionKey: section?.key,
|
||||
resolved: false,
|
||||
index
|
||||
},
|
||||
@@ -174,7 +101,7 @@ export const resolveCommentFx = createEffect(
|
||||
)
|
||||
|
||||
export const showDocumentCommentsPopupFx = createEffect(
|
||||
(payload: { element?: PopupAlignment, nodeId?: string | null, sectionKey: string }) => {
|
||||
(payload: { element?: PopupAlignment, nodeId?: string | null }) => {
|
||||
showPopup(
|
||||
documents.component.DocumentCommentsPopup,
|
||||
payload,
|
||||
@@ -186,24 +113,22 @@ export const showDocumentCommentsPopupFx = createEffect(
|
||||
}
|
||||
)
|
||||
|
||||
export const showAddCommentPopupFx = createEffect(
|
||||
(payload: { element?: PopupAlignment, nodeId?: string | null, sectionKey: string }) => {
|
||||
showPopup(
|
||||
documents.component.AddCommentPopup,
|
||||
payload,
|
||||
payload.element,
|
||||
(result) => {
|
||||
if (result === null || result === undefined) {
|
||||
documentCommentsHighlightCleared()
|
||||
} else {
|
||||
documentCommentsDisplayRequested(payload)
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{ category: DocumentCommentPopupCategory, overlay: true }
|
||||
)
|
||||
}
|
||||
)
|
||||
export const showAddCommentPopupFx = createEffect((payload: { element?: PopupAlignment, nodeId?: string | null }) => {
|
||||
showPopup(
|
||||
documents.component.AddCommentPopup,
|
||||
payload,
|
||||
payload.element,
|
||||
(result) => {
|
||||
if (result === null || result === undefined) {
|
||||
documentCommentsHighlightCleared()
|
||||
} else {
|
||||
documentCommentsDisplayRequested(payload)
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
{ category: DocumentCommentPopupCategory, overlay: true }
|
||||
)
|
||||
})
|
||||
|
||||
export const $documentCommentsFilter = createStore<DocumentCommentsFilter>({
|
||||
showResolved: false,
|
||||
@@ -233,8 +158,6 @@ export const $documentComments = createStore<DocumentComment[]>([])
|
||||
)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $groupedDocumentComments = $documentComments.map((comments) => new GroupedDocumentCommentsData(comments))
|
||||
|
||||
export const $documentCommentNavigateTo = createStore<DocumentComment | null>(null)
|
||||
.on(documentCommentsNavigateRequested, (_, { value }) => {
|
||||
return value
|
||||
@@ -251,19 +174,13 @@ export const $documentCommentNavigateTo = createStore<DocumentComment | null>(nu
|
||||
.reset([controlledDocumentClosed, documentCommentsHighlightCleared])
|
||||
|
||||
export const $documentCommentHighlightedLocation = createStore<{
|
||||
sectionKey: string | null
|
||||
nodeId: string | null
|
||||
} | null>(null)
|
||||
.on(documentCommentsHighlightUpdated, (_, payload) =>
|
||||
payload !== null ? { sectionKey: payload.sectionKey, nodeId: payload.nodeId ?? null } : null
|
||||
)
|
||||
.on([showDocumentCommentsPopupFx, showAddCommentPopupFx], (_, { sectionKey, nodeId }) => ({
|
||||
sectionKey,
|
||||
.on(documentCommentsHighlightUpdated, (_, payload) => (payload !== null ? { nodeId: payload.nodeId ?? null } : null))
|
||||
.on([showDocumentCommentsPopupFx, showAddCommentPopupFx], (_, { nodeId }) => ({
|
||||
nodeId: nodeId ?? null
|
||||
}))
|
||||
.on(documentCommentsNavigateRequested, (_, { value }) =>
|
||||
value !== null ? { sectionKey: value.sectionKey ?? null, nodeId: value.nodeId ?? null } : null
|
||||
)
|
||||
.on(documentCommentsNavigateRequested, (_, { value }) => (value !== null ? { nodeId: value.nodeId ?? null } : null))
|
||||
.reset([controlledDocumentClosed, documentCommentsHighlightCleared])
|
||||
|
||||
export const $savedAttachments = createStore<Array<Ref<Attachment>>>([])
|
||||
@@ -271,18 +188,3 @@ export const $savedAttachments = createStore<Array<Ref<Attachment>>>([])
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
forward({ from: documentCommentsLocationNavigateRequested, to: documentCommentsHighlightUpdated })
|
||||
|
||||
sample({
|
||||
clock: documentCommentsLocationNavigateRequested,
|
||||
source: {
|
||||
sections: $controlledDocumentSections
|
||||
},
|
||||
filter: ({ sections }, { nodeId, sectionKey }) =>
|
||||
nodeId !== null && nodeId !== undefined && sections.findIndex((s) => s.key === sectionKey) >= 0,
|
||||
fn: ({ sections }, { sectionKey }) => {
|
||||
const targetSection = sections.find((s) => s.key === sectionKey) as DocumentSection
|
||||
|
||||
return targetSection._id
|
||||
},
|
||||
target: documentSectionExpanded
|
||||
})
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
type ControlledDocument,
|
||||
ControlledDocumentState,
|
||||
type DocumentReviewRequest,
|
||||
type DocumentSection,
|
||||
DocumentState,
|
||||
type DocumentTemplate,
|
||||
type EditorMode,
|
||||
@@ -40,13 +39,7 @@ import {
|
||||
approvalRequestUpdated,
|
||||
controlledDocumentClosed,
|
||||
controlledDocumentOpened,
|
||||
controlledDocumentSectionsUpdated,
|
||||
controlledDocumentUpdated,
|
||||
documentSectionCollapsed,
|
||||
documentSectionDescriptionEditingCompleted,
|
||||
documentSectionDescriptionEditingRequested,
|
||||
documentSectionExpanded,
|
||||
documentSectionToggled,
|
||||
documentAllVersionsUpdated,
|
||||
editorModeUpdated,
|
||||
reviewRequestUpdated,
|
||||
@@ -66,14 +59,6 @@ export const $controlledDocumentTemplate = createStore<DocumentTemplate | null>(
|
||||
.on(controlledDocumentUpdated, (_, payload) => payload?.$lookup?.template ?? null)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $controlledDocumentSections = createStore<DocumentSection[]>([])
|
||||
.on(controlledDocumentSectionsUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $controlledDocumentSectionIds = $controlledDocumentSections.map(
|
||||
(sections) => sections?.map((section) => section._id) ?? []
|
||||
)
|
||||
|
||||
const $documentAllVersions = createStore<ControlledDocument[]>([])
|
||||
.on(documentAllVersionsUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
@@ -143,10 +128,6 @@ export const $editorMode = createStore<EditorMode>('viewing')
|
||||
.on(editorModeUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $documentSectionEditingDescription = createStore<Ref<DocumentSection> | null>(null)
|
||||
.on(documentSectionDescriptionEditingRequested, (_, payload) => payload)
|
||||
.reset([controlledDocumentClosed, documentSectionDescriptionEditingCompleted])
|
||||
|
||||
export const $activeRightPanelTab = createStore<RightPanelTab | null>(RightPanelTab.INFO)
|
||||
.on(rightPanelTabChanged, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
@@ -307,36 +288,6 @@ export const $isEditable = combine(
|
||||
(state, mode, isOwner, isCoAuthor) => (isOwner || isCoAuthor) && mode === 'editing' && state === DocumentState.Draft
|
||||
)
|
||||
|
||||
export const $collapsedDocumentSectionIds = createStore<Set<Ref<DocumentSection>>>(new Set())
|
||||
.on(documentSectionToggled, (collapsed, sectionId) => {
|
||||
if (collapsed.has(sectionId)) {
|
||||
collapsed.delete(sectionId)
|
||||
} else {
|
||||
collapsed.add(sectionId)
|
||||
}
|
||||
|
||||
return new Set(collapsed)
|
||||
})
|
||||
.on(documentSectionCollapsed, (collapsed, sectionId) => {
|
||||
if (collapsed.has(sectionId)) {
|
||||
return
|
||||
}
|
||||
|
||||
collapsed.add(sectionId)
|
||||
|
||||
return new Set(collapsed)
|
||||
})
|
||||
.on(documentSectionExpanded, (collapsed, sectionId) => {
|
||||
if (!collapsed.has(sectionId)) {
|
||||
return
|
||||
}
|
||||
|
||||
collapsed.delete(sectionId)
|
||||
|
||||
return new Set(collapsed)
|
||||
})
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
export const $canViewDocumentComments = combine(
|
||||
$editorMode,
|
||||
$isDocumentOwner,
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
approvalRequestUpdated,
|
||||
controlledDocumentClosed,
|
||||
controlledDocumentOpened,
|
||||
controlledDocumentSectionsUpdated,
|
||||
controlledDocumentUpdated,
|
||||
documentAllVersionsUpdated,
|
||||
reviewRequestUpdated,
|
||||
@@ -51,7 +50,6 @@ const documentSnapshotsQuery = createQuery(true)
|
||||
const reviewRequestQuery = createQuery(true)
|
||||
const approvalRequestQuery = createQuery(true)
|
||||
const documentCommentsQuery = createQuery(true)
|
||||
const sectionsQuery = createQuery(true)
|
||||
const workingCopyMetadataQuery = createQuery(true)
|
||||
const savedAttachmentsQuery = createQuery(true)
|
||||
const trainingQuery = createQuery(true)
|
||||
@@ -84,27 +82,6 @@ const queryControlledDocumentFx = createEffect(
|
||||
}
|
||||
)
|
||||
|
||||
const querySectionsFx = createEffect((payload: { _id: Ref<ControlledDocument> }) => {
|
||||
const { _id } = payload
|
||||
if (_id == null) {
|
||||
sectionsQuery.unsubscribe()
|
||||
return
|
||||
}
|
||||
|
||||
sectionsQuery.query(
|
||||
documents.class.DocumentSection,
|
||||
{ attachedTo: _id },
|
||||
(result) => {
|
||||
controlledDocumentSectionsUpdated(result ?? [])
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const queryDocumentVersionsFx = createEffect((payload: ControlledDocument) => {
|
||||
if (payload == null) {
|
||||
documentVersionsQuery.unsubscribe()
|
||||
@@ -269,7 +246,6 @@ const unsubscribeFx = createEffect(() => {
|
||||
reviewRequestQuery.unsubscribe()
|
||||
approvalRequestQuery.unsubscribe()
|
||||
documentCommentsQuery.unsubscribe()
|
||||
sectionsQuery.unsubscribe()
|
||||
workingCopyMetadataQuery.unsubscribe()
|
||||
documentVersionsQuery.unsubscribe()
|
||||
savedAttachmentsQuery.unsubscribe()
|
||||
@@ -282,7 +258,6 @@ forward({
|
||||
from: controlledDocumentOpened,
|
||||
to: [
|
||||
queryControlledDocumentFx,
|
||||
querySectionsFx,
|
||||
queryReviewRequestFx,
|
||||
queryApprovalRequestFx,
|
||||
querySavedAttachmentsFx,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
//
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// 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 documents, { type DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { createEffect, forward } from 'effector'
|
||||
import { documentSectionDescriptionEditingCompleted } from './actions'
|
||||
|
||||
export const updateDocumentSectionDescriptionFx = createEffect(
|
||||
async ({ section, description }: { section: DocumentSection, description: string }) => {
|
||||
if (section === null || section === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const currentDesc = client.getHierarchy().as(section, documents.mixin.DocumentTemplateSection).description
|
||||
|
||||
if (currentDesc === description) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateMixin(section._id, section._class, section.space, documents.mixin.DocumentTemplateSection, {
|
||||
description
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
forward({ from: updateDocumentSectionDescriptionFx, to: documentSectionDescriptionEditingCompleted })
|
||||
@@ -18,8 +18,7 @@ import documents, {
|
||||
DocumentState,
|
||||
type EditorMode,
|
||||
type ControlledDocument,
|
||||
type DocumentReviewRequest,
|
||||
type DocumentSection
|
||||
type DocumentReviewRequest
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { generateId, getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import { type PersonAccount } from '@hcengineering/contact'
|
||||
@@ -147,15 +146,6 @@ export async function comment (editor: Editor, event: MouseEvent, ctx: ActionCon
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const section = (await client.findOne(objectClass, {
|
||||
_id: objectId
|
||||
})) as DocumentSection
|
||||
|
||||
if (section === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let selectedNodeId = editor.extensionStorage[nodeUuidName].activeNodeUuid
|
||||
|
||||
if (selectedNodeId == null) {
|
||||
@@ -168,8 +158,7 @@ export async function comment (editor: Editor, event: MouseEvent, ctx: ActionCon
|
||||
|
||||
await showAddCommentPopupFx({
|
||||
element: getNodeElement(editor, selectedNodeId),
|
||||
nodeId: selectedNodeId,
|
||||
sectionKey: section.key
|
||||
nodeId: selectedNodeId
|
||||
})
|
||||
|
||||
selectNode(editor, selectedNodeId)
|
||||
@@ -182,20 +171,12 @@ export async function isCommentVisible (editor: Editor, ctx: ActionContext): Pro
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
if (!client.getHierarchy().isDerived(objectClass, documents.class.DocumentSection)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const section = (await client.findOne(objectClass, {
|
||||
_id: objectId
|
||||
})) as DocumentSection
|
||||
|
||||
if (section === undefined) {
|
||||
if (!client.getHierarchy().isDerived(objectClass, documents.class.ControlledDocument)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const doc = await client.findOne(documents.class.ControlledDocument, {
|
||||
_id: section.attachedTo as Ref<ControlledDocument>
|
||||
_id: objectId as Ref<ControlledDocument>
|
||||
})
|
||||
|
||||
if (doc === undefined) {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import core, {
|
||||
type AttachedData,
|
||||
type Class,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
@@ -25,26 +24,22 @@ import core, {
|
||||
type Client,
|
||||
type WithLookup,
|
||||
SortingOrder,
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
checkPermission
|
||||
} from '@hcengineering/core'
|
||||
import { type IntlString, getMetadata, getResource, translate } from '@hcengineering/platform'
|
||||
import presentation, { copyDocumentContent, getClient } from '@hcengineering/presentation'
|
||||
import { type IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import { type Person, type Employee, type PersonAccount } from '@hcengineering/contact'
|
||||
import request, { RequestStatus } from '@hcengineering/request'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { getEventPositionElement, showPopup, getUserTimezone, type Location } from '@hcengineering/ui'
|
||||
import { showPopup, getUserTimezone, type Location } from '@hcengineering/ui'
|
||||
import { type KeyFilter } from '@hcengineering/view'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import documents, {
|
||||
type AttachmentsDocumentSection,
|
||||
type CollaborativeDocumentSection,
|
||||
type ControlledDocument,
|
||||
type Document,
|
||||
type DocumentRequest,
|
||||
type DocumentSection,
|
||||
type DocumentTemplate,
|
||||
type DocumentSpace,
|
||||
type DocumentCategory,
|
||||
@@ -56,15 +51,12 @@ import documents, {
|
||||
type ProjectMeta,
|
||||
ControlledDocumentState,
|
||||
DocumentState,
|
||||
calcRank,
|
||||
genRanks,
|
||||
getDocumentName,
|
||||
getDocumentId
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Request } from '@hcengineering/request'
|
||||
|
||||
import documentsResources from './plugin'
|
||||
import GuidanceEditor from './components/document/editors/GuidanceEditor.svelte'
|
||||
import { getProjectDocumentLink } from './navigation'
|
||||
import { wizardOpened } from './stores/wizards/create-document'
|
||||
|
||||
@@ -75,16 +67,12 @@ export const COLLABORATOR_URL = getMetadata(textEditor.metadata.CollaboratorUrl)
|
||||
|
||||
export const isDocumentCommentAttachedTo = (
|
||||
value: DocumentComment | null | undefined,
|
||||
location: { sectionKey: string | null, nodeId?: string | null }
|
||||
location: { nodeId?: string | null }
|
||||
): boolean => {
|
||||
if (value === null || value === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.sectionKey !== location.sectionKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value.nodeId === location.nodeId) {
|
||||
return true
|
||||
}
|
||||
@@ -134,36 +122,6 @@ export function isProjectDocument (hierarchy: Hierarchy, doc: Doc): doc is Proje
|
||||
return hierarchy.isDerived(doc._class, documents.class.ProjectDocument)
|
||||
}
|
||||
|
||||
export function createCollaborativeSection (
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: CollaborativeDocumentSection
|
||||
): AttachedData<CollaborativeDocumentSection> {
|
||||
const collaboratorSectionId = generateId()
|
||||
if (copyFrom != null) {
|
||||
if (document.content !== undefined) {
|
||||
void copyDocumentContent(document.content, copyFrom.collaboratorSectionId, collaboratorSectionId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...section,
|
||||
collaboratorSectionId,
|
||||
attachments: 0
|
||||
}
|
||||
}
|
||||
|
||||
export function createAttachmentsSection (
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: DocumentSection
|
||||
): AttachedData<AttachmentsDocumentSection> {
|
||||
return {
|
||||
...section,
|
||||
attachments: 0
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVisibleFilters (filters: KeyFilter[], space?: Ref<Space>): Promise<KeyFilter[]> {
|
||||
// Removes the "Space" filter if a specific space is provided
|
||||
return space === undefined ? filters : filters.filter((f) => f.key !== 'space')
|
||||
@@ -229,80 +187,6 @@ export async function getDocumentMetaLinkFragment (document: Doc): Promise<Locat
|
||||
return getProjectDocumentLink(targetDocument, project)
|
||||
}
|
||||
|
||||
export async function appendSection (document: Document, sectionType: Class<DocumentSection>): Promise<void> {
|
||||
if (document === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const lastSection = await client.findOne(
|
||||
documents.class.DocumentSection,
|
||||
{ attachedTo: document._id, attachedToClass: document._class },
|
||||
{ sort: { rank: SortingOrder.Descending } }
|
||||
)
|
||||
|
||||
await addSectionBetween(client, document, sectionType, lastSection, undefined)
|
||||
}
|
||||
|
||||
export async function addSectionBetween (
|
||||
client: TxOperations,
|
||||
document: Document,
|
||||
sectionType: Class<DocumentSection>,
|
||||
first?: DocumentSection,
|
||||
second?: DocumentSection,
|
||||
copyFrom?: DocumentSection
|
||||
): Promise<void> {
|
||||
if (document === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const defaultTitle = await translate(documentsResources.string.Untitled, {})
|
||||
const copy = await translate(documentsResources.string.Copy, {})
|
||||
let sectionData: AttachedData<DocumentSection> = {
|
||||
title: copyFrom != null ? `${copyFrom.title} (${copy})` : defaultTitle,
|
||||
rank: calcRank(first, second),
|
||||
key: generateId()
|
||||
}
|
||||
|
||||
if (hierarchy.hasMixin(sectionType, documents.mixin.DocumentSectionCreator)) {
|
||||
const { creator } = hierarchy.as(sectionType, documents.mixin.DocumentSectionCreator)
|
||||
const create = await getResource(creator)
|
||||
|
||||
sectionData = create(document, sectionData, copyFrom)
|
||||
}
|
||||
|
||||
const sectionId = await client.addCollection(
|
||||
sectionType._id,
|
||||
document.space,
|
||||
document._id,
|
||||
document._class,
|
||||
'sections',
|
||||
sectionData
|
||||
)
|
||||
|
||||
const isDocumentTemplate = hierarchy.hasMixin(document, documents.mixin.DocumentTemplate)
|
||||
|
||||
if (isDocumentTemplate) {
|
||||
let description = ''
|
||||
let guidance = ''
|
||||
|
||||
if (copyFrom != null && hierarchy.hasMixin(copyFrom, documents.mixin.DocumentTemplateSection)) {
|
||||
const templateSection = hierarchy.as(copyFrom, documents.mixin.DocumentTemplateSection)
|
||||
description = templateSection.description ?? ''
|
||||
guidance = templateSection.guidance ?? ''
|
||||
}
|
||||
|
||||
await client.updateMixin(sectionId, sectionType._id, document.space, documents.mixin.DocumentTemplateSection, {
|
||||
mandatory: false,
|
||||
description,
|
||||
guidance
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export interface TeamPopupData {
|
||||
controlledDoc: ControlledDocument
|
||||
requestClass: Ref<Class<DocumentRequest>>
|
||||
@@ -472,35 +356,6 @@ export type StatesTags = {
|
||||
[K in DocumentState]: DocumentStateTagType
|
||||
}
|
||||
|
||||
export async function changeSectionIndex (
|
||||
client: TxOperations,
|
||||
section: DocumentSection,
|
||||
newIndex: number
|
||||
): Promise<void> {
|
||||
const sections = await client.findAll(
|
||||
documents.class.DocumentSection,
|
||||
{ attachedTo: section.attachedTo },
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
if (sections.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const oldIndex = sections.findIndex((s) => s._id === section._id)
|
||||
if (oldIndex === newIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
let newSections = [...sections.slice(0, oldIndex), ...sections.slice(oldIndex + 1)]
|
||||
newSections = [...newSections.slice(0, newIndex), section, ...newSections.slice(newIndex)]
|
||||
|
||||
const ranks = genRanks(sections.length)
|
||||
for (const i of Array(sections.length).keys()) {
|
||||
const rank = ranks.next().value as string
|
||||
await client.update(newSections[i], { rank })
|
||||
}
|
||||
}
|
||||
|
||||
export const statesTags: StatesTags = {
|
||||
[DocumentState.Draft]: 'draft',
|
||||
[DocumentState.Effective]: 'effective',
|
||||
@@ -536,34 +391,6 @@ export const loginIntlFieldNames: Readonly<{ [K in keyof LoginInfo]: IntlString
|
||||
|
||||
export type DocumentStateTagType = 'effective' | 'inProgress' | 'rejected' | 'draft' | 'obsolete'
|
||||
|
||||
export type GuidanceEditorMode = 'readonly' | 'canEdit' | 'editing'
|
||||
|
||||
export function openGuidanceEditor (
|
||||
client: TxOperations,
|
||||
section: DocumentSection,
|
||||
index: number,
|
||||
mode: GuidanceEditorMode,
|
||||
ev?: MouseEvent
|
||||
): void {
|
||||
showPopup(
|
||||
GuidanceEditor,
|
||||
{ section, index, mode, width: mode !== 'editing' ? '30rem' : undefined },
|
||||
mode === 'editing' || ev == null ? 'center' : getEventPositionElement(ev),
|
||||
async (res) => {
|
||||
if (res == null) return
|
||||
|
||||
const { reopenMode, guidance } = res
|
||||
if (reopenMode != null) {
|
||||
openGuidanceEditor(client, section, index, reopenMode)
|
||||
} else if (mode === 'editing' && guidance != null) {
|
||||
await client.updateMixin(section._id, section._class, section.space, documents.mixin.DocumentTemplateSection, {
|
||||
guidance
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function isDocOwner (ownableDocument: { owner?: Ref<Employee> }): boolean {
|
||||
const currentPerson = (getCurrentAccount() as PersonAccount)?.person
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import {
|
||||
type Document,
|
||||
type DocumentTemplate,
|
||||
type CollaborativeDocumentSection,
|
||||
type ControlledDocument,
|
||||
type DocumentCategory,
|
||||
type DocumentSpace,
|
||||
@@ -38,7 +37,7 @@ import {
|
||||
} from './types'
|
||||
|
||||
import documents from './plugin'
|
||||
import { TEMPLATE_PREFIX, calcRank, createDocSections } from './utils'
|
||||
import { TEMPLATE_PREFIX } from './utils'
|
||||
|
||||
async function getParentPath (client: TxOperations, parent: Ref<ProjectDocument>): Promise<Array<Ref<DocumentMeta>>> {
|
||||
const parentDocObj = await client.findOne(documents.class.ProjectDocument, {
|
||||
@@ -76,17 +75,9 @@ export async function createControlledDocFromTemplate (
|
||||
return { seqNumber: -1, success: false }
|
||||
}
|
||||
|
||||
const template = await client.findOne(
|
||||
documents.mixin.DocumentTemplate,
|
||||
{
|
||||
_id: templateId
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
_id: { sections: documents.class.DocumentSection }
|
||||
}
|
||||
}
|
||||
)
|
||||
const template = await client.findOne(documents.mixin.DocumentTemplate, {
|
||||
_id: templateId
|
||||
})
|
||||
|
||||
if (template === undefined) {
|
||||
return { seqNumber: -1, success: false }
|
||||
@@ -190,8 +181,6 @@ async function createControlledDoc (
|
||||
documentId
|
||||
)
|
||||
|
||||
await createDocSections(ops, documentId, templateId, space, documents.class.ControlledDocument)
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
return { seqNumber, success: success.result }
|
||||
@@ -208,8 +197,7 @@ export async function createDocumentTemplate (
|
||||
prefix: string,
|
||||
spec: Omit<AttachedData<ControlledDocument>, 'prefix'>,
|
||||
category: Ref<DocumentCategory>,
|
||||
author?: Ref<Employee>,
|
||||
defaultSection?: { title: string }
|
||||
author?: Ref<Employee>
|
||||
): Promise<{ seqNumber: number, success: boolean }> {
|
||||
const projectId = project ?? documents.ids.NoProject
|
||||
|
||||
@@ -297,36 +285,6 @@ export async function createDocumentTemplate (
|
||||
docPrefix: prefix
|
||||
})
|
||||
|
||||
if (defaultSection != null) {
|
||||
// adding the default first section
|
||||
const sectionId = generateId()
|
||||
const collaboratorSectionId = generateId()
|
||||
|
||||
await ops.addCollection(
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
space,
|
||||
templateId,
|
||||
documents.mixin.DocumentTemplate,
|
||||
'sections',
|
||||
{
|
||||
title: defaultSection.title,
|
||||
rank: calcRank(),
|
||||
key: sectionId,
|
||||
collaboratorSectionId,
|
||||
attachments: 0
|
||||
},
|
||||
sectionId as Ref<CollaborativeDocumentSection>
|
||||
)
|
||||
|
||||
await ops.updateMixin(
|
||||
sectionId,
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
space,
|
||||
documents.mixin.DocumentTemplateSection,
|
||||
{ description: '', guidance: '' }
|
||||
)
|
||||
}
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
return { seqNumber, success: success.result }
|
||||
|
||||
@@ -14,9 +14,7 @@ import type { AnyComponent, ResolvedLocation, Location } from '@hcengineering/ui
|
||||
import { Action } from '@hcengineering/view'
|
||||
|
||||
import type {
|
||||
AttachmentsDocumentSection,
|
||||
ChangeControl,
|
||||
CollaborativeDocumentSection,
|
||||
ControlledDocument,
|
||||
ControlledDocumentState,
|
||||
Document,
|
||||
@@ -27,16 +25,11 @@ import type {
|
||||
DocumentMeta,
|
||||
DocumentRequest,
|
||||
DocumentReviewRequest,
|
||||
DocumentSection,
|
||||
DocumentSectionCreator,
|
||||
DocumentSectionEditor,
|
||||
DocumentSectionPresenter,
|
||||
DocumentSpace,
|
||||
DocumentSpaceType,
|
||||
DocumentSpaceTypeDescriptor,
|
||||
DocumentState,
|
||||
DocumentTemplate,
|
||||
DocumentTemplateSection,
|
||||
DocumentTraining,
|
||||
HierarchyDocument,
|
||||
ExternalSpace,
|
||||
@@ -74,10 +67,7 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
DocumentCategory: '' as Ref<Class<DocumentCategory>>,
|
||||
ControlledDocument: '' as Ref<Class<ControlledDocument>>,
|
||||
ChangeControl: '' as Ref<Class<ChangeControl>>,
|
||||
DocumentSection: '' as Ref<Class<DocumentSection>>,
|
||||
DocumentComment: '' as Ref<Class<DocumentComment>>,
|
||||
CollaborativeDocumentSection: '' as Ref<Class<CollaborativeDocumentSection>>,
|
||||
AttachmentsDocumentSection: '' as Ref<Class<AttachmentsDocumentSection>>,
|
||||
|
||||
Sequence: '' as Ref<Class<Sequence>>,
|
||||
|
||||
@@ -91,10 +81,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
mixin: {
|
||||
DocumentTemplate: '' as Ref<Mixin<DocumentTemplate>>,
|
||||
DocumentTraining: '' as Ref<Mixin<DocumentTraining>>,
|
||||
DocumentSectionEditor: '' as Ref<Mixin<DocumentSectionEditor>>,
|
||||
DocumentSectionPresenter: '' as Ref<Mixin<DocumentSectionPresenter>>,
|
||||
DocumentSectionCreator: '' as Ref<Mixin<DocumentSectionCreator>>,
|
||||
DocumentTemplateSection: '' as Ref<Mixin<DocumentTemplateSection>>,
|
||||
DocumentSpaceTypeData: '' as Ref<Mixin<DocumentSpace>>
|
||||
// DocTemplateActions: '' as Ref<Mixin<DocTemplateActions>>
|
||||
},
|
||||
@@ -123,7 +109,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
},
|
||||
action: {
|
||||
ChangeDocumentOwner: '' as Ref<Action<Doc, any>>,
|
||||
DeleteDocumentTemplateSection: '' as Ref<Action>,
|
||||
CreateChildDocument: '' as Ref<Action<Document, any>>,
|
||||
CreateChildTemplate: '' as Ref<Action<Document, any>>,
|
||||
CreateDocument: '' as Ref<Action<DocumentSpace, any>>,
|
||||
@@ -151,9 +136,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
StateObsolete: '' as Asset,
|
||||
ArrowUp: '' as Asset,
|
||||
ArrowDown: '' as Asset,
|
||||
Duplicate: '' as Asset,
|
||||
EditDescription: '' as Asset,
|
||||
EditGuidance: '' as Asset,
|
||||
Configure: '' as Asset
|
||||
},
|
||||
space: {
|
||||
@@ -180,7 +162,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
Owner: '' as IntlString,
|
||||
Status: '' as IntlString,
|
||||
Labels: '' as IntlString,
|
||||
Sections: '' as IntlString,
|
||||
Description: '' as IntlString,
|
||||
Reason: '' as IntlString,
|
||||
CollaborativeDocument: '' as IntlString,
|
||||
@@ -194,11 +175,7 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
EffectiveDate: '' as IntlString,
|
||||
PlannedEffectiveDate: '' as IntlString,
|
||||
ChangeControl: '' as IntlString,
|
||||
Section: '' as IntlString,
|
||||
Rank: '' as IntlString,
|
||||
CollaborativeSection: '' as IntlString,
|
||||
CollaboratorSectionId: '' as IntlString,
|
||||
AttachmentsSection: '' as IntlString,
|
||||
DocumentRequest: '' as IntlString,
|
||||
DocumentReviewRequest: '' as IntlString,
|
||||
DocumentApprovalRequest: '' as IntlString,
|
||||
@@ -206,7 +183,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
Categories: '' as IntlString,
|
||||
Guidance: '' as IntlString,
|
||||
Required: '' as IntlString,
|
||||
SectionTemplate: '' as IntlString,
|
||||
Major: '' as IntlString,
|
||||
Minor: '' as IntlString,
|
||||
Patch: '' as IntlString,
|
||||
@@ -221,6 +197,7 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
InProgress: '' as IntlString,
|
||||
Resolve: '' as IntlString,
|
||||
Unresolve: '' as IntlString,
|
||||
Pending: '' as IntlString,
|
||||
Resolved: '' as IntlString,
|
||||
ShowResolved: '' as IntlString,
|
||||
Ordering: '' as IntlString,
|
||||
@@ -235,7 +212,6 @@ export const documentsPlugin = plugin(documentsId, {
|
||||
DeleteCategoryHint: '' as IntlString,
|
||||
DeleteCategoryWarning: '' as IntlString,
|
||||
Key: '' as IntlString,
|
||||
SectionKey: '' as IntlString,
|
||||
CommentsSequence: '' as IntlString,
|
||||
Index: '' as IntlString,
|
||||
GeneralDocumentation: '' as IntlString,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ChatMessage } from '@hcengineering/chunter'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import {
|
||||
CollaborativeDoc,
|
||||
type AttachedData,
|
||||
type AttachedDoc,
|
||||
type Class,
|
||||
type CollectionSize,
|
||||
@@ -19,9 +18,7 @@ import {
|
||||
SpaceType,
|
||||
SpaceTypeDescriptor
|
||||
} from '@hcengineering/core'
|
||||
import type { Resource } from '@hcengineering/platform'
|
||||
import { type TagReference } from '@hcengineering/tags'
|
||||
import type { AnyComponent } from '@hcengineering/ui'
|
||||
import { Request } from '@hcengineering/request'
|
||||
import type { Training, TrainingRequest } from '@hcengineering/training'
|
||||
|
||||
@@ -127,11 +124,11 @@ export interface Document extends Doc<DocumentSpace> {
|
||||
state: DocumentState
|
||||
content: CollaborativeDoc
|
||||
labels?: CollectionSize<TagReference> // A collection of attached tags(labels)
|
||||
sections: CollectionSize<DocumentSection> // A collections of document sections
|
||||
abstract?: string
|
||||
commentSequence: number // Used to enumerate the comments across revisions of the working copy of the document
|
||||
comments?: CollectionSize<DocumentComment> // A collection of document comments to the working copy of the document
|
||||
snapshots?: CollectionSize<DocumentSnapshot> // A collection of document snapshots
|
||||
attachments?: CollectionSize<Attachment> // A collection of attachments inlined into the document. E.g. pictures/documents.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +140,6 @@ export interface DocumentSnapshot extends AttachedDoc {
|
||||
name?: string
|
||||
content: CollaborativeDoc
|
||||
state?: DocumentState
|
||||
sections: CollectionSize<DocumentSection> // A collections of snapshot sections
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,49 +237,6 @@ export enum ControlledDocumentState {
|
||||
ToReview = 'toReview'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Base class for a document section
|
||||
*/
|
||||
export interface DocumentSection extends AttachedDoc {
|
||||
title: string
|
||||
rank: string // lexorank
|
||||
key: string // semantic key of a document section to track same sections between different version of the document
|
||||
templateSectionId?: Ref<DocumentTemplateSection>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* A document section mixin containing additional generic metadata for template sections
|
||||
*/
|
||||
export interface DocumentTemplateSection extends DocumentSection {
|
||||
mandatory?: boolean
|
||||
description?: string
|
||||
guidance?: Markup
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Section of a collaborative document
|
||||
*/
|
||||
export interface CollaborativeDocumentSection extends DocumentSection {
|
||||
collaboratorSectionId: string // field in a collaborator document
|
||||
attachments?: CollectionSize<Attachment> // A collection of attachments inlined into the document section. E.g. pictures/documents.
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Section only consisting of attachments
|
||||
*/
|
||||
export interface AttachmentsDocumentSection extends DocumentSection {
|
||||
attachments?: CollectionSize<Attachment>
|
||||
maximum?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Generic sequence attached to a class for cases when a single increment goes through all instances of the class.
|
||||
@@ -311,43 +264,14 @@ export interface DocumentReviewRequest extends DocumentRequest {}
|
||||
*/
|
||||
export interface DocumentApprovalRequest extends DocumentRequest {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocumentSectionEditor extends Class<Doc> {
|
||||
editor: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocumentSectionPresenter extends Class<Doc> {
|
||||
presenter: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DocumentSectionCreator extends Class<Doc> {
|
||||
creator: Resource<
|
||||
(
|
||||
document: Document,
|
||||
section: AttachedData<DocumentSection>,
|
||||
copyFrom?: DocumentSection
|
||||
) => AttachedData<DocumentSection>
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
* Attached to Document
|
||||
* Could be attached to the document section node (e.g. comment thread)
|
||||
* Could be attached to particular node or the whole section or the whole document
|
||||
* Could be attached to particular node or the whole document
|
||||
*/
|
||||
export interface DocumentComment extends ChatMessage {
|
||||
sectionKey?: string // If empty - attached to the document itself
|
||||
nodeId?: string // If empty - attached to the whole section
|
||||
nodeId?: string // If empty - attached to the whole document
|
||||
resolved?: boolean
|
||||
index?: number
|
||||
}
|
||||
|
||||
@@ -12,37 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import {
|
||||
ApplyOperations,
|
||||
Class,
|
||||
Data,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
TxOperations,
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import { ApplyOperations, Data, DocumentUpdate, Ref, TxOperations } from '@hcengineering/core'
|
||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||
|
||||
import documents from './plugin'
|
||||
|
||||
import {
|
||||
AttachmentsDocumentSection,
|
||||
ChangeControl,
|
||||
CollaborativeDocumentSection,
|
||||
ControlledDocument,
|
||||
Document,
|
||||
DocumentSection,
|
||||
DocumentSnapshot,
|
||||
DocumentSpace,
|
||||
DocumentState,
|
||||
DocumentTemplate,
|
||||
DocumentTemplateSection,
|
||||
Project,
|
||||
ProjectDocument,
|
||||
ProjectMeta
|
||||
@@ -87,107 +68,6 @@ export async function createChangeControl (
|
||||
await client.createDoc(documents.class.ChangeControl, space, ccSpec, ccId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createDocSections (
|
||||
client: TxOperations,
|
||||
documentId: Ref<Document> | Ref<DocumentSnapshot>,
|
||||
srcDocId: Ref<DocumentTemplate> | Ref<Document>,
|
||||
space: Ref<DocumentSpace>,
|
||||
attachedToClass: Ref<Class<Doc>> = documents.class.Document
|
||||
): Promise<void> {
|
||||
const docSections = await client.findAll<DocumentSection>(documents.class.DocumentSection, {
|
||||
attachedTo: srcDocId
|
||||
})
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
for (const section of docSections) {
|
||||
const sectionId: Ref<CollaborativeDocumentSection> = generateId()
|
||||
|
||||
// copying templateSectionId if srcDoc is another doc or _id - if srcDoc is template
|
||||
const templateSectionId = section.templateSectionId ?? section._id
|
||||
|
||||
// NOTE: ideally we should not assume section type here and allow for any types of sections
|
||||
// to be correctly created here. Add a mixin to section class with a function and invoke it here for
|
||||
// custom attributes/logic?
|
||||
if (hierarchy.isDerived(section._class, documents.class.CollaborativeDocumentSection)) {
|
||||
const collaborativeSection: CollaborativeDocumentSection = hierarchy.as(
|
||||
section,
|
||||
documents.class.CollaborativeDocumentSection
|
||||
)
|
||||
await client.addCollection<Document | DocumentSnapshot, CollaborativeDocumentSection>(
|
||||
documents.class.CollaborativeDocumentSection,
|
||||
space,
|
||||
documentId,
|
||||
attachedToClass,
|
||||
'sections',
|
||||
{
|
||||
title: collaborativeSection.title,
|
||||
rank: collaborativeSection.rank,
|
||||
key: collaborativeSection.key,
|
||||
collaboratorSectionId: collaborativeSection.collaboratorSectionId,
|
||||
attachments: collaborativeSection.attachments,
|
||||
templateSectionId
|
||||
},
|
||||
sectionId
|
||||
)
|
||||
} else if (hierarchy.isDerived(section._class, documents.class.AttachmentsDocumentSection)) {
|
||||
const attachmentsSection: AttachmentsDocumentSection = hierarchy.as(
|
||||
section,
|
||||
documents.class.AttachmentsDocumentSection
|
||||
)
|
||||
await client.addCollection<Document | DocumentSnapshot, AttachmentsDocumentSection>(
|
||||
documents.class.AttachmentsDocumentSection,
|
||||
space,
|
||||
documentId,
|
||||
attachedToClass,
|
||||
'sections',
|
||||
{
|
||||
title: attachmentsSection.title,
|
||||
rank: attachmentsSection.rank,
|
||||
key: attachmentsSection.key,
|
||||
attachments: attachmentsSection.attachments,
|
||||
templateSectionId
|
||||
},
|
||||
sectionId
|
||||
)
|
||||
|
||||
// Copying attachments metadata, not the files. It should only be detached on remove rather than deleted.
|
||||
const attachmentsMeta = await client.findAll(attachment.class.Attachment, {
|
||||
attachedTo: section._id
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
attachmentsMeta.map((a) =>
|
||||
client.addCollection(a._class, a.space, sectionId, section._class, 'attachments', {
|
||||
name: a.name,
|
||||
file: a.file,
|
||||
type: a.type,
|
||||
size: a.size,
|
||||
lastModified: a.lastModified
|
||||
})
|
||||
)
|
||||
)
|
||||
} else {
|
||||
await client.addCollection<Document | DocumentSnapshot, DocumentSection>(
|
||||
documents.class.DocumentSection,
|
||||
space,
|
||||
documentId,
|
||||
attachedToClass,
|
||||
'sections',
|
||||
{
|
||||
title: section.title,
|
||||
rank: section.rank,
|
||||
key: section.key,
|
||||
templateSectionId
|
||||
},
|
||||
sectionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -210,23 +90,6 @@ export function matchDocumentId (str: string): Pick<Document, 'prefix' | 'seqNum
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DraftDocumentTemplateSection = DocumentTemplateSection & {
|
||||
draftId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getDraftDocumentTemplateSectionId (
|
||||
draftId: string | undefined,
|
||||
sectionId: string
|
||||
): Ref<DocumentSection> {
|
||||
return `${sectionId}-${draftId ?? ''}` as Ref<DocumentSection>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -239,66 +102,6 @@ export function isControlledDocument (client: TxOperations, doc: Document): doc
|
||||
*/
|
||||
export type EditorMode = 'viewing' | 'editing' | 'comparing'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DocumentTemplateSectionData<T extends DocumentTemplateSection = DocumentTemplateSection> = Pick<
|
||||
Data<DocumentTemplateSection>,
|
||||
'title' | 'description' | 'mandatory'
|
||||
> & {
|
||||
id: Ref<DocumentTemplateSection>
|
||||
class: Ref<Class<T>>
|
||||
additional?: Omit<T, keyof DocumentTemplateSection>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function appendTemplateSection<T extends DocumentTemplateSection> (
|
||||
client: TxOperations,
|
||||
docId: Ref<Document>,
|
||||
data: DocumentTemplateSectionData<T>,
|
||||
space: Ref<Space>
|
||||
): Promise<void> {
|
||||
const lastOne = await client.findOne<DocumentTemplateSection>(
|
||||
documents.mixin.DocumentTemplateSection,
|
||||
{ attachedTo: docId, attachedToClass: documents.class.Document },
|
||||
{ sort: { rank: SortingOrder.Descending } }
|
||||
)
|
||||
|
||||
const ops = client.apply(docId)
|
||||
const sectionId = (data.id + '-' + generateId()) as Ref<DocumentTemplateSection>
|
||||
|
||||
await ops.addCollection(
|
||||
data.class,
|
||||
space,
|
||||
docId,
|
||||
documents.mixin.DocumentTemplate,
|
||||
'sections',
|
||||
{
|
||||
title: data.title,
|
||||
rank: calcRank(lastOne, undefined),
|
||||
key: docId,
|
||||
...(data.additional ?? {})
|
||||
},
|
||||
sectionId
|
||||
)
|
||||
|
||||
await ops.updateMixin<DocumentSection, DocumentTemplateSection>(
|
||||
sectionId,
|
||||
data.class,
|
||||
space,
|
||||
documents.mixin.DocumentTemplateSection,
|
||||
{
|
||||
description: data.description,
|
||||
guidance: '',
|
||||
mandatory: data.mandatory
|
||||
}
|
||||
)
|
||||
|
||||
await ops.commit()
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -362,11 +165,6 @@ export async function copyProjectDocuments (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const DEFAULT_SECTION_TITLE = 'Untitled'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Space,
|
||||
Timestamp,
|
||||
Tx,
|
||||
TxCUD,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform'
|
||||
@@ -286,6 +287,9 @@ export interface DocNotifyContext extends Doc<PersonSpace> {
|
||||
isPinned: boolean
|
||||
lastViewedTimestamp?: Timestamp
|
||||
lastUpdateTimestamp?: Timestamp
|
||||
|
||||
// Only for debug
|
||||
tx?: Ref<TxCUD<Doc>>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -183,4 +183,7 @@
|
||||
d="M18.2927 19.7071C17.9022 19.3166 17.9022 18.6834 18.2927 18.2929C18.6833 17.9024 19.3164 17.9024 19.707 18.2929L26 24.586V20H28V28H20V26H24.586L18.2927 19.7071Z"
|
||||
/>
|
||||
</symbol>
|
||||
<symbol id="note" viewBox="0 0 256 256">
|
||||
<rect width="256" height="256" fill="none"></rect><line x1="96" x2="160" y1="96" y2="96" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><line x1="96" x2="160" y1="128" y2="128" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><line x1="96" x2="128" y1="160" y2="160" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"></line><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8" d="M156.68629,216H48a8,8,0,0,1-8-8V48a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8V156.68629a8,8,0,0,1-2.34315,5.65686l-51.3137,51.3137A8,8,0,0,1,156.68629,216Z"></path><polyline fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="8" points="215.277 159.992 160 159.992 160 215.272"></polyline>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "Blockquote",
|
||||
"Code": "Code",
|
||||
"CodeBlock": "Code block",
|
||||
"Note": "Note",
|
||||
"ConfigureNote": "Configure note",
|
||||
"Set": "Set",
|
||||
"Update": "Update",
|
||||
"Remove": "Remove",
|
||||
"NotePlaceholder": "Add a note...",
|
||||
"SampleText": "Sample text",
|
||||
"Send": "Send",
|
||||
"Attach": "Attach",
|
||||
"TextStyle": "Text style",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "Cita",
|
||||
"Code": "Código",
|
||||
"CodeBlock": "Bloque de código",
|
||||
"Note": "Nota",
|
||||
"ConfigureNote": "Configurar nota",
|
||||
"Set": "Establecer",
|
||||
"Update": "Actualizar",
|
||||
"Remove": "Eliminar",
|
||||
"NotePlaceholder": "Añadir una nota...",
|
||||
"SampleText": "Texto de ejemplo",
|
||||
"Send": "Enviar",
|
||||
"Attach": "Adjuntar",
|
||||
"TextStyle": "Estilo de texto",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "Bloc de citation",
|
||||
"Code": "Code",
|
||||
"CodeBlock": "Bloc de code",
|
||||
"Note": "Note",
|
||||
"ConfigureNote": "Configurer la note",
|
||||
"Set": "Définir",
|
||||
"Update": "Mettre à jour",
|
||||
"Remove": "Supprimer",
|
||||
"NotePlaceholder": "Ajouter une note...",
|
||||
"SampleText": "Texte d'exemple",
|
||||
"Send": "Envoyer",
|
||||
"Attach": "Joindre",
|
||||
"TextStyle": "Style de texte",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "Citação",
|
||||
"Code": "Código",
|
||||
"CodeBlock": "Bloco de código",
|
||||
"Note": "Nota",
|
||||
"ConfigureNote": "Configurar nota",
|
||||
"Set": "Definir",
|
||||
"Update": "Atualizar",
|
||||
"Remove": "Remover",
|
||||
"NotePlaceholder": "Adicionar uma nota...",
|
||||
"SampleText": "Texto de exemplo",
|
||||
"Send": "Enviar",
|
||||
"Attach": "Anexar",
|
||||
"TextStyle": "Estilo do texto",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "Цитата",
|
||||
"Code": "Код",
|
||||
"CodeBlock": "Кодовый блок",
|
||||
"Note": "Заметка",
|
||||
"ConfigureNote": "Настроить заметку",
|
||||
"Set": "Установить",
|
||||
"Update": "Обновить",
|
||||
"Remove": "Удалить",
|
||||
"NotePlaceholder": "Добавить заметку...",
|
||||
"SampleText": "Пример текста",
|
||||
"Send": "Отправить",
|
||||
"Attach": "Прикреплять",
|
||||
"TextStyle": "Стиль текста",
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
"Blockquote": "引用",
|
||||
"Code": "代码",
|
||||
"CodeBlock": "代码块",
|
||||
"Note": "注释",
|
||||
"ConfigureNote": "配置注释",
|
||||
"Set": "设置",
|
||||
"Update": "更新",
|
||||
"Remove": "删除",
|
||||
"NotePlaceholder": "添加注释...",
|
||||
"SampleText": "示例文本",
|
||||
"Send": "发送",
|
||||
"Attach": "附件",
|
||||
"TextStyle": "文本样式",
|
||||
|
||||
@@ -37,5 +37,6 @@ loadMetadata(textEditor.icon, {
|
||||
AlignRight: `${icons}#alignRight`,
|
||||
MoreH: `${icons}#moreH`,
|
||||
Expand: `${icons}#expand`,
|
||||
ScaleOut: `${icons}#scaleOut`
|
||||
ScaleOut: `${icons}#scaleOut`,
|
||||
Note: `${icons}#note`
|
||||
})
|
||||
|
||||
@@ -119,6 +119,9 @@
|
||||
field={key.key}
|
||||
canEmbedFiles={false}
|
||||
withSideMenu={false}
|
||||
kitOptions={{
|
||||
note: false
|
||||
}}
|
||||
on:focus
|
||||
on:blur
|
||||
on:update
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import { Completion } from '../Completion'
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
import { textEditorCommandHandler } from '../commands'
|
||||
import { getEditorKit } from '../../src/kits/editor-kit'
|
||||
import { EditorKitOptions, getEditorKit } from '../../src/kits/editor-kit'
|
||||
import { DirectStorageProvider } from '../provider/storage'
|
||||
import { TiptapCollabProvider } from '../provider/tiptap'
|
||||
import { formatCollaborativeDocumentId, formatPlatformDocumentId } from '../provider/utils'
|
||||
@@ -99,6 +99,7 @@
|
||||
export let canEmbedImages = true
|
||||
export let withSideMenu = true
|
||||
export let withInlineCommands = true
|
||||
export let kitOptions: Partial<EditorKitOptions> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -419,7 +420,8 @@
|
||||
appendTo: () => boundary ?? element,
|
||||
isHidden: () => !showToolbar
|
||||
}
|
||||
}
|
||||
},
|
||||
...kitOptions
|
||||
}),
|
||||
...optionalExtensions,
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
import CollaborativeTextEditor from './CollaborativeTextEditor.svelte'
|
||||
import { FileAttachFunction } from './extension/types'
|
||||
import { EditorKitOptions } from '../kits/editor-kit'
|
||||
|
||||
export let collaborativeDoc: CollaborativeDoc
|
||||
export let initialCollaborativeDoc: CollaborativeDoc | undefined = undefined
|
||||
@@ -48,6 +49,7 @@
|
||||
|
||||
export let attachFile: FileAttachFunction | undefined = undefined
|
||||
export let canShowPopups = true
|
||||
export let kitOptions: Partial<EditorKitOptions> = {}
|
||||
|
||||
let element: HTMLElement
|
||||
|
||||
@@ -106,6 +108,7 @@
|
||||
extensions={[...onExtensions()]}
|
||||
{canShowPopups}
|
||||
{editorAttributes}
|
||||
{kitOptions}
|
||||
on:editor
|
||||
on:update
|
||||
on:open-document
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function isEditable (): boolean {
|
||||
return editor.isEditable
|
||||
return editor?.isEditable
|
||||
}
|
||||
export function setEditable (editable: boolean): void {
|
||||
if (editor !== undefined) {
|
||||
@@ -77,25 +77,27 @@
|
||||
return getMarkup(editor)
|
||||
}
|
||||
export function setContent (newContent: Markup): void {
|
||||
if (content !== newContent) {
|
||||
if (editor !== undefined && content !== newContent) {
|
||||
content = newContent
|
||||
editor.commands.setContent(markupToJSON(content))
|
||||
}
|
||||
}
|
||||
export function clear (): void {
|
||||
content = EmptyMarkup
|
||||
if (editor !== undefined) {
|
||||
content = EmptyMarkup
|
||||
|
||||
editor.commands.clearContent(true)
|
||||
editor.commands.clearContent(true)
|
||||
}
|
||||
}
|
||||
|
||||
export function insertText (text: string): void {
|
||||
editor.commands.insertContent(text)
|
||||
editor?.commands.insertContent(text)
|
||||
}
|
||||
export function insertTable (options: { rows?: number, cols?: number, withHeaderRow?: boolean }) {
|
||||
editor.commands.insertTable(options)
|
||||
editor?.commands.insertTable(options)
|
||||
}
|
||||
export function insertCodeBlock (pos?: number): void {
|
||||
editor.commands.insertContent(
|
||||
editor?.commands.insertContent(
|
||||
{
|
||||
type: 'codeBlock',
|
||||
content: [{ type: 'text', text: ' ' }]
|
||||
@@ -106,11 +108,11 @@
|
||||
)
|
||||
|
||||
if (pos !== undefined) {
|
||||
editor.commands.focus(pos, { scrollIntoView: false })
|
||||
editor?.commands.focus(pos, { scrollIntoView: false })
|
||||
}
|
||||
}
|
||||
export function insertSeparatorLine (): void {
|
||||
editor.commands.setHorizontalRule()
|
||||
editor?.commands.setHorizontalRule()
|
||||
}
|
||||
export function insertContent (
|
||||
value: Content,
|
||||
@@ -119,11 +121,11 @@
|
||||
updateSelection?: boolean
|
||||
}
|
||||
): void {
|
||||
editor.commands.insertContent(value, options)
|
||||
editor?.commands.insertContent(value, options)
|
||||
}
|
||||
|
||||
export function insertMarkup (markup: Markup): void {
|
||||
editor.commands.insertContent(markupToJSON(markup))
|
||||
editor?.commands.insertContent(markupToJSON(markup))
|
||||
}
|
||||
|
||||
let needFocus = false
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type Node as ProseMirrorNode, type MarkType } from '@tiptap/pm/model'
|
||||
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
|
||||
import { getEventPositionElement, showPopup } from '@hcengineering/ui'
|
||||
import { NoteBaseExtension, type NoteKind, name as noteName } from '@hcengineering/text'
|
||||
|
||||
import ConfigureNotePopup from '../note/ConfigureNotePopup.svelte'
|
||||
import DisplayNotePopup from '../note/DisplayNotePopup.svelte'
|
||||
|
||||
export interface NoteOptions {
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export const NoteExtension = NoteBaseExtension.extend({
|
||||
addOptions () {
|
||||
return {
|
||||
readonly: false
|
||||
}
|
||||
},
|
||||
|
||||
addCommands () {
|
||||
return {
|
||||
setNote:
|
||||
(text: string, kind: NoteKind) =>
|
||||
({ commands }) => {
|
||||
return commands.setMark(this.name, { title: text, kind })
|
||||
},
|
||||
unsetNote:
|
||||
() =>
|
||||
({ commands }) => {
|
||||
return commands.unsetMark(this.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-note-open'),
|
||||
props: {
|
||||
decorations (state) {
|
||||
// Consider moving to plugin's state to improve performance by skipping
|
||||
// creating decorations on transactions that do not affect notes
|
||||
const { doc, schema } = state
|
||||
const markType = schema.marks[noteName]
|
||||
|
||||
return createDecorations(doc, markType)
|
||||
},
|
||||
handleKeyDown (view: EditorView, event) {
|
||||
const markType = view.state.schema.marks[noteName]
|
||||
switch (event.key) {
|
||||
case 'ArrowRight':
|
||||
return onArrowRight(view, markType)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
function onArrowRight (view: EditorView, markType: MarkType): boolean {
|
||||
const { selection } = view.state
|
||||
if (!selection.empty) return false
|
||||
|
||||
const pos = selection.$from
|
||||
const inNote = markType.isInSet(pos.marks()) !== undefined
|
||||
|
||||
if (inNote && pos.nodeAfter == null) {
|
||||
view.dispatch(view.state.tr.removeStoredMark(markType))
|
||||
view.dispatch(view.state.tr.insertText(' '))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
interface NoteWidgetData {
|
||||
start: number
|
||||
end: number
|
||||
title: string
|
||||
kind: NoteKind
|
||||
}
|
||||
|
||||
function createDecorations (doc: ProseMirrorNode, markType: MarkType): DecorationSet {
|
||||
const decorations: Decoration[] = []
|
||||
const notes: NoteWidgetData[] = []
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
const noteMark = node.marks.find((mark) => mark.type.name === markType.name)
|
||||
|
||||
if (noteMark != null) {
|
||||
notes.push({
|
||||
start: pos,
|
||||
end: pos + node.nodeSize,
|
||||
title: noteMark.attrs.title,
|
||||
kind: noteMark.attrs.kind
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let currentNote: any
|
||||
notes.forEach((note) => {
|
||||
if (currentNote === undefined) {
|
||||
currentNote = note
|
||||
return
|
||||
}
|
||||
|
||||
if (currentNote.end === note.start && currentNote.kind === note.kind && currentNote.title === note.title) {
|
||||
currentNote.end = note.end
|
||||
return
|
||||
}
|
||||
|
||||
appendNoteWidget(currentNote, decorations)
|
||||
currentNote = note
|
||||
})
|
||||
|
||||
if (currentNote !== undefined) {
|
||||
appendNoteWidget(currentNote, decorations)
|
||||
}
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
}
|
||||
|
||||
function appendNoteWidget (note: NoteWidgetData, decorations: Decoration[]): void {
|
||||
const marker = document.createElement('span')
|
||||
marker.classList.add('text-editor-note-marker')
|
||||
marker.classList.add('theme-text-editor-note-anchor')
|
||||
marker.classList.add(note.kind)
|
||||
marker.textContent = '[]'
|
||||
marker.title = note.title
|
||||
marker.onmousedown = function (event) {
|
||||
displayNote(note.title, event)
|
||||
}
|
||||
|
||||
decorations.push(Decoration.widget(note.end, () => marker, { side: 0 }))
|
||||
}
|
||||
|
||||
export function displayNote (text: string, event: MouseEvent): void {
|
||||
showPopup(
|
||||
DisplayNotePopup,
|
||||
{
|
||||
text
|
||||
},
|
||||
getEventPositionElement(event)
|
||||
)
|
||||
}
|
||||
|
||||
export async function configureNote (editor: Editor, event: MouseEvent): Promise<void> {
|
||||
const attributes = editor.getAttributes(noteName)
|
||||
const text = attributes.title
|
||||
const kind = attributes.kind
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
showPopup(
|
||||
ConfigureNotePopup,
|
||||
{
|
||||
text,
|
||||
kind,
|
||||
isNew: !editor.isActive(noteName),
|
||||
handleUpdate: (text: string, kind: NoteKind) => {
|
||||
editor.commands.setNote(text, kind)
|
||||
},
|
||||
handleRemove: () => {
|
||||
editor.commands.unsetNote()
|
||||
}
|
||||
},
|
||||
getEventPositionElement(event),
|
||||
() => {
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export async function isEditableNote (editor: Editor): Promise<boolean> {
|
||||
if (!editor.isEditable) {
|
||||
return false
|
||||
}
|
||||
|
||||
const noteExt = editor.extensionManager.extensions.find((ext) => ext.name === noteName)
|
||||
|
||||
if (noteExt == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return noteExt.options?.readonly !== true
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { NoteKind } from '@hcengineering/text'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { Card } from '@hcengineering/presentation'
|
||||
import { Button, Label, PlainTextEditor } from '@hcengineering/ui'
|
||||
|
||||
export let text: string
|
||||
export let kind: NoteKind = NoteKind.Neutral
|
||||
export let isNew: boolean
|
||||
export let handleUpdate: (text: string, kind: NoteKind) => void
|
||||
export let handleRemove: () => void
|
||||
|
||||
const kindPalette = [
|
||||
NoteKind.Neutral,
|
||||
NoteKind.PrimaryLight,
|
||||
NoteKind.PositiveLight,
|
||||
NoteKind.WarningLight,
|
||||
NoteKind.DangerousLight,
|
||||
NoteKind.Primary,
|
||||
NoteKind.Positive,
|
||||
NoteKind.Warning,
|
||||
NoteKind.Dangerous
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function handleOk (): void {
|
||||
handleUpdate(text, kind)
|
||||
}
|
||||
|
||||
function handleRemoveClick (): void {
|
||||
handleRemove()
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={textEditor.string.ConfigureNote}
|
||||
okLabel={isNew ? textEditor.string.Set : textEditor.string.Update}
|
||||
okAction={handleOk}
|
||||
canSave
|
||||
width="medium"
|
||||
on:close
|
||||
on:changeContent
|
||||
>
|
||||
<div class="kindControl">
|
||||
<span class="sample theme-text-editor-note-anchor {kind}"><Label label={textEditor.string.SampleText} /></span>
|
||||
|
||||
<div class="kindPalette">
|
||||
{#each kindPalette as k}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="colorBox theme-text-editor-note-anchor {k}"
|
||||
on:click={() => (kind = k)}
|
||||
class:selected={kind === k}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlainTextEditor bind:value={text} placeholder={textEditor.string.NotePlaceholder} />
|
||||
<svelte:fragment slot="buttons">
|
||||
{#if !isNew}
|
||||
<Button label={textEditor.string.Remove} kind="ghost" on:click={handleRemoveClick} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.kindControl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.kindPalette {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.sample {
|
||||
color: var(--theme-text-primary-color);
|
||||
}
|
||||
|
||||
.colorBox {
|
||||
position: relative;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid var(--theme-button-border);
|
||||
cursor: pointer;
|
||||
|
||||
&.selected::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 32 32'%3E%3Cpath fill='%23333333' d='M23.6,10.9c-0.6-0.6-1.5-0.6-2.1,0l-6.9,6.9l-3.4-3.4c-0.6-0.6-1.5-0.6-2.1,0c-0.6,0.6-0.6,1.5,0,2.1l5.6,5.6l9.1-9.1C24.1,12.5,24.1,11.5,23.6,10.9z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark .colorBox) {
|
||||
&.selected::after {
|
||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 32 32'%3E%3Cpath fill='%23CCCCCC' d='M23.6,10.9c-0.6-0.6-1.5-0.6-2.1,0l-6.9,6.9l-3.4-3.4c-0.6-0.6-1.5-0.6-2.1,0c-0.6,0.6-0.6,1.5,0,2.1l5.6,5.6l9.1-9.1C24.1,12.5,24.1,11.5,23.6,10.9z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+17
-11
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
<!--
|
||||
// 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
|
||||
@@ -11,14 +11,20 @@
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { type DocumentSection } from '@hcengineering/controlled-documents'
|
||||
-->
|
||||
<script lang="ts">
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { Card } from '@hcengineering/presentation'
|
||||
|
||||
export interface ComparisonSectionData {
|
||||
section: DocumentSection
|
||||
index: number
|
||||
}
|
||||
export let text: string
|
||||
</script>
|
||||
|
||||
export type ComparisonSectionPair =
|
||||
| [ComparisonSectionData, null]
|
||||
| [null, ComparisonSectionData]
|
||||
| [ComparisonSectionData, ComparisonSectionData]
|
||||
<Card label={textEditor.string.Note} width="small" thinHeader={true} hideFooter={true} okAction={() => {}} on:close>
|
||||
<div class="note">{text}</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.note {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
export let items: Heading[] = []
|
||||
export let selected: Heading | undefined = undefined
|
||||
export let enumerated: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
|
||||
showPopup(
|
||||
TableofContentsPopup,
|
||||
{ items, selected },
|
||||
{ items, selected, enumerated },
|
||||
getPopupPositionElement(ev.target as HTMLElement, { v: 'top', h: 'right' }),
|
||||
(res) => {
|
||||
hovered = false
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
export let items: Heading[] = []
|
||||
export let selected: Heading | undefined = undefined
|
||||
export let enumerated: boolean = false
|
||||
|
||||
$: minLevel = items.reduce((p, v) => Math.min(p, v.level), Infinity)
|
||||
|
||||
@@ -43,18 +44,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
<div class="box" class:enumerated>
|
||||
{#each items as item}
|
||||
{@const level = getIndentLevel(item.level)}
|
||||
<button
|
||||
class="menu-item no-focus flex-row-center item"
|
||||
class="menu-item no-focus flex-row-center item enum{level + 1}"
|
||||
on:click={() => dispatch('close', item)}
|
||||
use:tooltip={{ label: getEmbeddedLabel(item.title) }}
|
||||
>
|
||||
<div class="label overflow-label flex-grow" class:selected={item.id === selected?.id}>
|
||||
<span style={`padding-left: ${level * 1.5}rem;`}>
|
||||
{item.title}
|
||||
</span>
|
||||
<div
|
||||
class="label overflow-label flex-grow"
|
||||
class:selected={item.id === selected?.id}
|
||||
style={`padding-left: ${level * 1.5}rem;`}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -79,4 +82,32 @@
|
||||
line-height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.enumerated {
|
||||
.enum1 {
|
||||
counter-increment: enum1;
|
||||
counter-reset: enum2;
|
||||
|
||||
.label::before {
|
||||
content: counter(enum1) '. ';
|
||||
}
|
||||
}
|
||||
|
||||
.enum2 {
|
||||
counter-increment: enum2;
|
||||
counter-reset: enum3;
|
||||
|
||||
.label::before {
|
||||
content: counter(enum1) '.' counter(enum2) '. ';
|
||||
}
|
||||
}
|
||||
|
||||
.enum3 {
|
||||
counter-increment: enum3;
|
||||
|
||||
.label::before {
|
||||
content: counter(enum1) '.' counter(enum2) '.' counter(enum3) '. ';
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
|
||||
export let items: Heading[] = []
|
||||
export let selected: Heading | undefined = undefined
|
||||
export let enumerated: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="menu-space" />
|
||||
<TableOfContentsContent {items} {selected} on:close />
|
||||
<TableOfContentsContent {items} {selected} {enumerated} on:close />
|
||||
<div class="menu-space" />
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { formatLink } from './kits/default-kit'
|
||||
import { isEditable, isHeadingVisible } from './kits/editor-kit'
|
||||
import { openTableOptions, isEditableTableActive } from './components/extension/table/table'
|
||||
import { openImage, expandImage, moreImageActions } from './components/extension/imageExt'
|
||||
import { configureNote, isEditableNote } from './components/extension/note'
|
||||
|
||||
export * from '@hcengineering/presentation/src/types'
|
||||
export type { EditorKitOptions } from './kits/editor-kit'
|
||||
@@ -86,7 +87,9 @@ export default async (): Promise<Resources> => ({
|
||||
OpenImage: openImage,
|
||||
ExpandImage: expandImage,
|
||||
MoreImageActions: moreImageActions,
|
||||
ConfigureNote: configureNote,
|
||||
IsEditableTableActive: isEditableTableActive,
|
||||
IsEditableNote: isEditableNote,
|
||||
IsEditable: isEditable,
|
||||
IsHeadingVisible: isHeadingVisible
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import ListKeymap from '@tiptap/extension-list-keymap'
|
||||
import TableHeader from '@tiptap/extension-table-header'
|
||||
import 'prosemirror-codemark/dist/codemark.css'
|
||||
|
||||
import { NoteExtension, type NoteOptions } from '../components/extension/note'
|
||||
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
||||
import { HardBreakExtension } from '../components/extension/hardBreak'
|
||||
import { ImageExtension, type ImageOptions } from '../components/extension/imageExt'
|
||||
@@ -47,6 +48,7 @@ export interface EditorKitOptions extends DefaultKitOptions {
|
||||
})
|
||||
| false
|
||||
mode?: 'full' | 'compact'
|
||||
note?: NoteOptions | false
|
||||
submit?: SubmitOptions | false
|
||||
objectId?: Ref<Doc>
|
||||
objectClass?: Ref<Class<Doc>>
|
||||
@@ -260,6 +262,10 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
||||
])
|
||||
}
|
||||
|
||||
if (mode !== 'compact' && this.options.note !== false) {
|
||||
staticKitExtensions.push([1000, NoteExtension.configure(this.options.note ?? {})])
|
||||
}
|
||||
|
||||
const allKitExtensions = [...tableKitExtensions, ...modelKitExtensions, ...staticKitExtensions]
|
||||
|
||||
allKitExtensions.sort((a, b) => a[0] - b[0])
|
||||
|
||||
@@ -55,6 +55,13 @@ export default plugin(textEditorId, {
|
||||
Blockquote: '' as IntlString,
|
||||
Code: '' as IntlString,
|
||||
CodeBlock: '' as IntlString,
|
||||
Note: '' as IntlString,
|
||||
ConfigureNote: '' as IntlString,
|
||||
Set: '' as IntlString,
|
||||
Update: '' as IntlString,
|
||||
Remove: '' as IntlString,
|
||||
NotePlaceholder: '' as IntlString,
|
||||
SampleText: '' as IntlString,
|
||||
Send: '' as IntlString,
|
||||
FullDescription: '' as IntlString,
|
||||
NoFullDescription: '' as IntlString,
|
||||
@@ -107,6 +114,7 @@ export default plugin(textEditorId, {
|
||||
AlignRight: '' as Asset,
|
||||
MoreH: '' as Asset,
|
||||
Expand: '' as Asset,
|
||||
ScaleOut: '' as Asset
|
||||
ScaleOut: '' as Asset,
|
||||
Note: '' as Asset
|
||||
}
|
||||
})
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "#platform.notification.timeout",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "#platform.notification.timeout",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "#platform.notification.timeout",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "#platform.notification.timeout",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
{
|
||||
"name": "flagOpenInDesktopApp",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"name": "#platform.notification.timeout",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../utils'
|
||||
import { allure } from 'allure-playwright'
|
||||
import { DocumentsPage } from '../model/documents/documents-page'
|
||||
import { Content, DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
|
||||
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
|
||||
import { DocumentContentPage } from '../model/documents/document-content-page'
|
||||
import { prepareDocumentStep } from './common-documents-steps'
|
||||
|
||||
@@ -49,15 +49,12 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContent: Content = {
|
||||
sectionTitle: `Overview-${generateId()}`,
|
||||
content: faker.lorem.paragraphs(10)
|
||||
}
|
||||
const newContent = faker.lorem.paragraphs(10)
|
||||
|
||||
await prepareDocumentStep(page, completeDocument)
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
await documentContentPage.updateSectionTitle('1', newContent.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContent)
|
||||
|
||||
await documentContentPage.addContent(newContent)
|
||||
|
||||
await test.step('2. Send for Approval', async () => {
|
||||
await documentContentPage.buttonSendForApproval.click()
|
||||
@@ -92,7 +89,7 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
|
||||
|
||||
await test.step('4. Check the content of the draft matches the approved version', async () => {
|
||||
const actualText = await page.locator('.tiptap').innerText()
|
||||
const expectedText = newContent.content.trim().replace(/\s+/g, ' ')
|
||||
const expectedText = newContent.trim().replace(/\s+/g, ' ')
|
||||
const cleanedActualText = actualText.trim().replace(/\s+/g, ' ')
|
||||
|
||||
expect(cleanedActualText).toContain(expectedText)
|
||||
@@ -121,15 +118,11 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContent: Content = {
|
||||
sectionTitle: `Overview-${generateId()}`,
|
||||
content: faker.lorem.paragraphs(10)
|
||||
}
|
||||
const newContent = faker.lorem.paragraphs(10)
|
||||
|
||||
await prepareDocumentStep(page, completeDocument)
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
await documentContentPage.updateSectionTitle('1', newContent.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContent)
|
||||
await documentContentPage.addContent(newContent)
|
||||
|
||||
await test.step('2. Send for Approval', async () => {
|
||||
await documentContentPage.buttonSendForApproval.click()
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '../utils'
|
||||
import { allure } from 'allure-playwright'
|
||||
import { DocumentsPage } from '../model/documents/documents-page'
|
||||
import { Content, DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
|
||||
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types'
|
||||
import { DocumentContentPage } from '../model/documents/document-content-page'
|
||||
import { DocumentCommentsPage } from '../model/documents/document-comments-page'
|
||||
import { prepareDocumentStep } from './common-documents-steps'
|
||||
@@ -64,21 +64,17 @@ test.describe('QMS. Documents tests', () => {
|
||||
title: `Edit Document-${generateId()}`,
|
||||
description: `Edit Document description-${generateId()}`
|
||||
}
|
||||
const newContent: Content = {
|
||||
sectionTitle: `Overview-${generateId()}`,
|
||||
content: `New content-${generateId()}!!!!`
|
||||
}
|
||||
const newContent = `New content-${generateId()}!!!!`
|
||||
await prepareDocumentStep(page, editDocument)
|
||||
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
await test.step('2. Update the created document content', async () => {
|
||||
await documentContentPage.checkDocumentTitle(editDocument.title)
|
||||
await documentContentPage.updateSectionTitle('1', newContent.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContent)
|
||||
await documentContentPage.addContent(newContent)
|
||||
})
|
||||
|
||||
await test.step('3. Check the updated document information', async () => {
|
||||
await documentContentPage.checkContentForTheSection(newContent)
|
||||
await documentContentPage.checkContent(newContent)
|
||||
})
|
||||
|
||||
await attachScreenshot('TESTS-124_edit_document.png', page)
|
||||
@@ -268,11 +264,7 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Overview-${generateId()}`,
|
||||
content: `New content-${generateId()}!!!!`
|
||||
}
|
||||
const messageToTitle: string = `Message to the title-${generateId()}`
|
||||
const newContentFirst = `New content-${generateId()}!!!!`
|
||||
const messageToContent: string = `Message to the content-${generateId()}`
|
||||
|
||||
await prepareDocumentStep(page, commentsDocument)
|
||||
@@ -284,8 +276,7 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.EDITING)
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
})
|
||||
|
||||
await test.step('3. Send to Review', async () => {
|
||||
@@ -299,13 +290,12 @@ test.describe('QMS. Documents tests', () => {
|
||||
})
|
||||
|
||||
await test.step('4. Add comments and Complete Review', async () => {
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentFirst.sectionTitle, messageToTitle)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst.content, messageToContent)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst, messageToContent)
|
||||
|
||||
await documentContentPage.buttonComments.click()
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
|
||||
await attachScreenshot('TESTS-136_add_comments.png', page)
|
||||
|
||||
@@ -317,9 +307,8 @@ test.describe('QMS. Documents tests', () => {
|
||||
|
||||
await test.step('5. Resolve the first comment', async () => {
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.resolveComments(newContentFirst.sectionTitle, '1')
|
||||
|
||||
await documentCommentsPage.checkCommentExist(newContentFirst.sectionTitle, 1)
|
||||
await documentCommentsPage.resolveComment(messageToContent)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContent)
|
||||
|
||||
await attachScreenshot('TESTS-136_add_and_done_comments.png', page)
|
||||
})
|
||||
@@ -454,38 +443,21 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Make Review. Overview-${generateId()}`,
|
||||
content: `Make Review. New content-${generateId()}!!!!`
|
||||
}
|
||||
const updateContentFirst: Content = {
|
||||
sectionTitle: `Make Review Updated. Updated Overview-${generateId()}`,
|
||||
content: `Make Review Updated. Updated content-${generateId()}!!!!`
|
||||
}
|
||||
const newContentSecond: Content = {
|
||||
sectionTitle: `Make Review. Description-${generateId()}`,
|
||||
content: `Make Review. New content Description-${generateId()}!!!!`
|
||||
}
|
||||
const messageToTitle: string = `Make Review. Message to the first title-${generateId()}`
|
||||
const messageToSecondTitle: string = `Make Review. Message to the second title-${generateId()}`
|
||||
const newContentFirst = `Make Review. New content-${generateId()}!!!!`
|
||||
const updateContentFirst = `Make Review Updated. Updated content-${generateId()}!!!!`
|
||||
const messageToContent: string = `Make Review. Message to the content-${generateId()}`
|
||||
|
||||
await prepareDocumentStep(page, makeReviewDocument)
|
||||
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
await test.step('2. Add section and content', async () => {
|
||||
await test.step('2. Add content', async () => {
|
||||
await documentContentPage.checkDocumentTitle(makeReviewDocument.title)
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.DRAFT)
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.EDITING)
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
|
||||
await documentContentPage.addNewSection('1', 'below')
|
||||
await documentContentPage.updateSectionTitle('2', newContentSecond.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentSecond)
|
||||
await documentContentPage.checkContentForTheSection(newContentSecond)
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
await documentContentPage.checkContent(newContentFirst)
|
||||
await attachScreenshot('TESTS-139_add_section_and_content.png', page)
|
||||
})
|
||||
|
||||
@@ -501,15 +473,12 @@ test.describe('QMS. Documents tests', () => {
|
||||
})
|
||||
|
||||
await test.step('4. Add comments and Complete Review', async () => {
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentFirst.sectionTitle, messageToTitle)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst.content, messageToContent)
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentSecond.sectionTitle, messageToSecondTitle)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst, messageToContent)
|
||||
|
||||
await documentContentPage.buttonComments.click()
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(newContentSecond.sectionTitle, 1)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
|
||||
await documentContentPage.completeReview()
|
||||
|
||||
@@ -519,19 +488,16 @@ test.describe('QMS. Documents tests', () => {
|
||||
})
|
||||
|
||||
await test.step('5. Update Document and fix reviews', async () => {
|
||||
await documentContentPage.buttonEditDocument.click()
|
||||
await documentContentPage.createNewDraft()
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', updateContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(updateContentFirst)
|
||||
await documentContentPage.checkContentForTheSection(updateContentFirst)
|
||||
await documentContentPage.replaceContent(updateContentFirst)
|
||||
await documentContentPage.checkContent(updateContentFirst)
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(updateContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(newContentSecond.sectionTitle, 1)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
await documentCommentsPage.resolveAllComments()
|
||||
|
||||
await documentCommentsPage.checkCommentNotExist(updateContentFirst.sectionTitle)
|
||||
await documentCommentsPage.checkCommentNotExist(newContentSecond.sectionTitle)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContent)
|
||||
await attachScreenshot('TESTS-136_fix_reviews.png', page)
|
||||
})
|
||||
|
||||
@@ -563,18 +529,8 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Comparing versions. Overview-${generateId()}`,
|
||||
content: `Comparing versions. New content-${generateId()}!!!!`
|
||||
}
|
||||
const updateContentFirst: Content = {
|
||||
sectionTitle: '',
|
||||
content: `Comparing versions Updated. Updated content-${generateId()}!!!!`
|
||||
}
|
||||
const newContentSecond: Content = {
|
||||
sectionTitle: `Comparing versions. Description-${generateId()}`,
|
||||
content: `Comparing versions. New content Description-${generateId()}!!!!`
|
||||
}
|
||||
const newContentFirst = `Comparing versions. New content-${generateId()}!!!!`
|
||||
const updateContentFirst = `Comparing versions Updated. Updated content-${generateId()}!!!!`
|
||||
|
||||
await prepareDocumentStep(page, makeReviewDocument)
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
@@ -584,8 +540,8 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.EDITING)
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
await documentContentPage.checkContent(newContentFirst)
|
||||
await attachScreenshot('TESTS-140_add_section_and_content.png', page)
|
||||
})
|
||||
|
||||
@@ -616,24 +572,12 @@ test.describe('QMS. Documents tests', () => {
|
||||
version: 'v0.1'
|
||||
})
|
||||
|
||||
await documentContentPage.addContentToTheSection({
|
||||
sectionTitle: newContentFirst.sectionTitle,
|
||||
content: updateContentFirst.content
|
||||
})
|
||||
await documentContentPage.checkContentForTheSection({
|
||||
sectionTitle: newContentFirst.sectionTitle,
|
||||
content: updateContentFirst.content
|
||||
})
|
||||
|
||||
await documentContentPage.addNewSection('1', 'below')
|
||||
await documentContentPage.updateSectionTitle('2', newContentSecond.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentSecond)
|
||||
await documentContentPage.replaceContent(updateContentFirst)
|
||||
await documentContentPage.checkContent(updateContentFirst)
|
||||
|
||||
await documentContentPage.changeCurrentRight(DocumentRights.COMPARING)
|
||||
await documentContentPage.checkComparingTextAdded('Updated')
|
||||
await documentContentPage.checkComparingTextDeleted('New')
|
||||
await documentContentPage.checkComparingTextAdded(newContentSecond.sectionTitle)
|
||||
await documentContentPage.checkComparingTextAdded(newContentSecond.content)
|
||||
|
||||
await attachScreenshot('TESTS-140_fix_reviews.png', page)
|
||||
})
|
||||
@@ -659,20 +603,11 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Complete document. Overview-${generateId()}`,
|
||||
content: `Complete document. New content-${generateId()}!!!!`
|
||||
}
|
||||
const updateContentFirst: Content = {
|
||||
sectionTitle: `Complete document Updated. Updated Overview-${generateId()}`,
|
||||
content: `Complete document Updated. Updated content-${generateId()}!!!!`
|
||||
}
|
||||
const newContentSecond: Content = {
|
||||
sectionTitle: `Complete document. Description-${generateId()}`,
|
||||
content: `Complete document. New content Description-${generateId()}!!!!`
|
||||
}
|
||||
const messageToTitle: string = `Complete document. Message to the first title-${generateId()}`
|
||||
const newContentFirst = `Complete document. New content-${generateId()}!!!!`
|
||||
const updateContentFirst = `Complete document Updated. Updated content-${generateId()}!!!!`
|
||||
const newContentSecond = `Complete document. New content Description-${generateId()}!!!!`
|
||||
const messageToContent: string = `Complete document. Message to the content-${generateId()}`
|
||||
const messageToContentSecond: string = `Complete document. Message to the content-second-${generateId()}`
|
||||
|
||||
await prepareDocumentStep(page, completeDocument)
|
||||
|
||||
@@ -683,12 +618,11 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.EDITING)
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
await documentContentPage.checkContent(newContentFirst)
|
||||
|
||||
await documentContentPage.addContent(newContentSecond, true, true)
|
||||
|
||||
await documentContentPage.addNewSection('1', 'below')
|
||||
await documentContentPage.updateSectionTitle('2', newContentSecond.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentSecond)
|
||||
await attachScreenshot('TESTS-141_add_section_and_content.png', page)
|
||||
})
|
||||
|
||||
@@ -703,35 +637,31 @@ test.describe('QMS. Documents tests', () => {
|
||||
await attachScreenshot('TESTS-141_send_for_review.png', page)
|
||||
})
|
||||
|
||||
await test.step('4. As author add comments to the first section', async () => {
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentFirst.sectionTitle, messageToTitle)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst.content, messageToContent)
|
||||
await test.step('4. As author add comment', async () => {
|
||||
await documentContentPage.addMessageToTheText(newContentFirst, messageToContent)
|
||||
|
||||
await documentContentPage.buttonComments.click()
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(newContentFirst.sectionTitle, 1)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(messageToContent)
|
||||
await attachScreenshot('TESTS-141_author_add_comments.png', page)
|
||||
})
|
||||
|
||||
await test.step('5. As reviewer add comments to the second section and Complete Review', async () => {
|
||||
await test.step('5. As reviewer add comment and Complete Review', async () => {
|
||||
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
|
||||
|
||||
const documentsPageSecond = new DocumentsPage(userSecondPage)
|
||||
await documentsPageSecond.openDocument(completeDocument.title)
|
||||
|
||||
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
|
||||
await documentContentPageSecond.addMessageToTheSectionTitle(newContentSecond.sectionTitle, messageToTitle)
|
||||
await documentContentPageSecond.addMessageToTheText(newContentSecond.content, messageToContent)
|
||||
await documentContentPageSecond.addMessageToTheText(newContentSecond, messageToContentSecond)
|
||||
|
||||
await documentContentPageSecond.buttonComments.click()
|
||||
|
||||
const documentCommentsPageSecond = new DocumentCommentsPage(userSecondPage)
|
||||
await documentCommentsPageSecond.checkCommentExist(newContentSecond.sectionTitle, 2)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(newContentSecond.sectionTitle, 3)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(newContentSecond.sectionTitle, 4)
|
||||
await documentCommentsPageSecond.checkCommentExist(messageToContentSecond)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(messageToContentSecond)
|
||||
|
||||
// // TODO uncomment after fix https://front.hc.engineering/workbench/platform/tracker/EZQMS-552
|
||||
// await documentCommentsPageSecond.checkCommentCanNotBeResolved(newContentFirst.sectionTitle, 1)
|
||||
@@ -745,17 +675,16 @@ test.describe('QMS. Documents tests', () => {
|
||||
})
|
||||
|
||||
await test.step('6. Update Document and fix reviews', async () => {
|
||||
await documentContentPage.buttonEditDocument.click()
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', updateContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(updateContentFirst)
|
||||
await documentContentPage.checkContentForTheSection(updateContentFirst)
|
||||
await documentContentPage.createNewDraft()
|
||||
await documentContentPage.replaceContent(updateContentFirst)
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(updateContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentExist(messageToContentSecond)
|
||||
await documentCommentsPage.resolveAllComments()
|
||||
|
||||
await documentCommentsPage.checkCommentNotExist(updateContentFirst.sectionTitle)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContentSecond)
|
||||
await attachScreenshot('TESTS-141_fix_reviews.png', page)
|
||||
|
||||
await documentContentPage.buttonDocumentInformation.click()
|
||||
@@ -887,11 +816,8 @@ test.describe('QMS. Documents tests', () => {
|
||||
description: `Check comment popup elements document description-${generateId()}`
|
||||
}
|
||||
const author = 'Appleseed John'
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Overview-${generateId()}`,
|
||||
content: `New content-${generateId()}!!!!`
|
||||
}
|
||||
const messageToTitle: string = `Make Review. Message to the first title-${generateId()}`
|
||||
const newContentFirst = `New content-${generateId()}!!!!`
|
||||
const messageToText: string = `Make Review. Message to the first title-${generateId()}`
|
||||
const replyCommentFirst = `Reply to first comment-${generateId(4)}`
|
||||
|
||||
await prepareDocumentStep(page, checkPopupDocument)
|
||||
@@ -899,38 +825,26 @@ test.describe('QMS. Documents tests', () => {
|
||||
const documentContentPage = new DocumentContentPage(page)
|
||||
await test.step('2. Add section and content', async () => {
|
||||
await documentContentPage.checkDocumentTitle(checkPopupDocument.title)
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
})
|
||||
|
||||
await test.step('3. Add comment and check popup', async () => {
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentFirst.sectionTitle, messageToTitle, false)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst, messageToText, false)
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.addReplyInPopupByCommentId(1, replyCommentFirst)
|
||||
await documentCommentsPage.checkCommentInPopupById(
|
||||
1,
|
||||
newContentFirst.sectionTitle,
|
||||
author,
|
||||
messageToTitle,
|
||||
replyCommentFirst
|
||||
)
|
||||
await documentCommentsPage.checkCommentInPopupById(1, 'Pending', author, messageToText, replyCommentFirst)
|
||||
|
||||
await attachScreenshot('TESTS-161_add_comment_and_check_popup.png', page)
|
||||
await documentContentPage.closeNewMessagePopup()
|
||||
})
|
||||
|
||||
await test.step('4. Add comment and check comment in the right panel', async () => {
|
||||
await test.step('4. Check the comment in the right panel', async () => {
|
||||
await documentContentPage.buttonComments.click()
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentInPanelById(
|
||||
1,
|
||||
newContentFirst.sectionTitle,
|
||||
author,
|
||||
messageToTitle,
|
||||
replyCommentFirst
|
||||
)
|
||||
await documentCommentsPage.checkCommentInPanelById(1, 'Pending', author, messageToText, replyCommentFirst)
|
||||
await attachScreenshot('TESTS-161_add_comment_and_check_comment_in_the_right_panel.png', page)
|
||||
})
|
||||
})
|
||||
@@ -1092,20 +1006,11 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const newContentFirst: Content = {
|
||||
sectionTitle: `Complete document. Overview-${generateId()}`,
|
||||
content: `Complete document. New content-${generateId()}!!!!`
|
||||
}
|
||||
const updateContentFirst: Content = {
|
||||
sectionTitle: `Complete document Updated. Updated Overview-${generateId()}`,
|
||||
content: `Complete document Updated. Updated content-${generateId()}!!!!`
|
||||
}
|
||||
const newContentSecond: Content = {
|
||||
sectionTitle: `Complete document. Description-${generateId()}`,
|
||||
content: `Complete document. New content Description-${generateId()}!!!!`
|
||||
}
|
||||
const messageToTitle: string = `Complete document. Message to the first title-${generateId()}`
|
||||
const newContentFirst = `Complete document. New content-${generateId()}!!!!`
|
||||
const updateContentFirst = `Complete document Updated. Updated content-${generateId()}!!!!`
|
||||
const newContentSecond = `Complete document. New content Description-${generateId()}!!!!`
|
||||
const messageToContent: string = `Complete document. Message to the content-${generateId()}`
|
||||
const messageToContentSecond: string = `Complete document. Message to the content-second-${generateId()}`
|
||||
|
||||
await prepareDocumentStep(page, completeDocument)
|
||||
|
||||
@@ -1116,12 +1021,8 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkCurrentRights(DocumentRights.EDITING)
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', newContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentFirst)
|
||||
|
||||
await documentContentPage.addNewSection('1', 'below')
|
||||
await documentContentPage.updateSectionTitle('2', newContentSecond.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(newContentSecond)
|
||||
await documentContentPage.addContent(newContentFirst)
|
||||
await documentContentPage.addContent(newContentSecond, true, true)
|
||||
await attachScreenshot('TESTS-206_add_section_and_content.png', page)
|
||||
})
|
||||
|
||||
@@ -1136,35 +1037,31 @@ test.describe('QMS. Documents tests', () => {
|
||||
await attachScreenshot('TESTS-206_send_for_review.png', page)
|
||||
})
|
||||
|
||||
await test.step('4. As author add comments to the first section', async () => {
|
||||
await documentContentPage.addMessageToTheSectionTitle(newContentFirst.sectionTitle, messageToTitle)
|
||||
await documentContentPage.addMessageToTheText(newContentFirst.content, messageToContent)
|
||||
await test.step('4. As author add a comment', async () => {
|
||||
await documentContentPage.addMessageToTheText(newContentFirst, messageToContent)
|
||||
|
||||
await documentContentPage.buttonComments.click()
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(newContentFirst.sectionTitle, 1)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(newContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentCanBeResolved(messageToContent)
|
||||
await attachScreenshot('TESTS-206_author_add_comments.png', page)
|
||||
})
|
||||
|
||||
await test.step('5. As reviewer add comments to the second section and Complete Review', async () => {
|
||||
await test.step('5. As a reviewer add a comment and Complete Review', async () => {
|
||||
await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished()
|
||||
|
||||
const documentsPageSecond = new DocumentsPage(userSecondPage)
|
||||
await documentsPageSecond.openDocument(completeDocument.title)
|
||||
|
||||
const documentContentPageSecond = new DocumentContentPage(userSecondPage)
|
||||
await documentContentPageSecond.addMessageToTheSectionTitle(newContentSecond.sectionTitle, messageToTitle)
|
||||
await documentContentPageSecond.addMessageToTheText(newContentSecond.content, messageToContent)
|
||||
await documentContentPageSecond.addMessageToTheText(newContentSecond, messageToContentSecond)
|
||||
|
||||
await documentContentPageSecond.buttonComments.click()
|
||||
|
||||
const documentCommentsPageSecond = new DocumentCommentsPage(userSecondPage)
|
||||
await documentCommentsPageSecond.checkCommentExist(newContentSecond.sectionTitle, 2)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(newContentSecond.sectionTitle, 3)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(newContentSecond.sectionTitle, 4)
|
||||
await documentCommentsPageSecond.checkCommentExist(messageToContentSecond)
|
||||
await documentCommentsPageSecond.checkCommentCanBeResolved(messageToContentSecond)
|
||||
|
||||
await documentContentPageSecond.completeReview()
|
||||
|
||||
@@ -1174,17 +1071,17 @@ test.describe('QMS. Documents tests', () => {
|
||||
})
|
||||
|
||||
await test.step('6. Update Document and fix reviews', async () => {
|
||||
await documentContentPage.buttonEditDocument.click()
|
||||
await documentContentPage.createNewDraft()
|
||||
|
||||
await documentContentPage.updateSectionTitle('1', updateContentFirst.sectionTitle)
|
||||
await documentContentPage.addContentToTheSection(updateContentFirst)
|
||||
await documentContentPage.checkContentForTheSection(updateContentFirst)
|
||||
await documentContentPage.replaceContent(updateContentFirst)
|
||||
|
||||
const documentCommentsPage = new DocumentCommentsPage(page)
|
||||
await documentCommentsPage.checkCommentExist(updateContentFirst.sectionTitle, 2)
|
||||
await documentCommentsPage.checkCommentExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentExist(messageToContentSecond)
|
||||
await documentCommentsPage.resolveAllComments()
|
||||
|
||||
await documentCommentsPage.checkCommentNotExist(updateContentFirst.sectionTitle)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContent)
|
||||
await documentCommentsPage.checkCommentDoesNotExist(messageToContentSecond)
|
||||
await attachScreenshot('TESTS-206_fix_reviews.png', page)
|
||||
|
||||
await documentContentPage.buttonDocumentInformation.click()
|
||||
@@ -1319,13 +1216,15 @@ test.describe('QMS. Documents tests', () => {
|
||||
owner: 'Appleseed John',
|
||||
author: 'Appleseed John'
|
||||
}
|
||||
const overviewContent: Content = {
|
||||
sectionTitle: 'Overview',
|
||||
|
||||
const overview = {
|
||||
heading: 'Overview',
|
||||
content:
|
||||
'In this section, we explore [Medical Topic], shedding light on its key aspects, causes, symptoms, and available treatments. Gain insights into the latest advancements in [Medical Field] and discover valuable information for a better understanding of managing and addressing [Medical Condition].'
|
||||
}
|
||||
const mainContent: Content = {
|
||||
sectionTitle: 'Main',
|
||||
|
||||
const main = {
|
||||
heading: 'Main',
|
||||
content:
|
||||
'[Medical Topic] is a prevalent [medical condition/issue] affecting a significant number of individuals worldwide. This condition is characterized by [brief description of symptoms or key features]. It can arise due to [common causes or triggers], leading to [impact on health or daily life].'
|
||||
}
|
||||
@@ -1343,8 +1242,12 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocumentTitle(existDocument.title)
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_REVIEW)
|
||||
await documentContentPage.checkContentForTheSection(overviewContent)
|
||||
await documentContentPage.checkContentForTheSection(mainContent)
|
||||
|
||||
await expect(documentContentPage.contentLocator.locator('h1:first-child')).toHaveText(overview.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:first-child + p')).toHaveText(overview.content)
|
||||
|
||||
await expect(documentContentPage.contentLocator.locator('h1:not(:first-child)')).toHaveText(main.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:not(:first-child) + p')).toHaveText(main.content)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,31 +4,34 @@ import { DocumentCommonPage } from './document-common-page'
|
||||
export class DocumentCommentsPage extends DocumentCommonPage {
|
||||
readonly page: Page
|
||||
readonly buttonDocumentTitle: Locator
|
||||
readonly comments: Locator
|
||||
|
||||
constructor (page: Page) {
|
||||
super(page)
|
||||
this.page = page
|
||||
this.buttonDocumentTitle = page.locator('button.version-item span.name')
|
||||
this.comments = page.locator('div[data-float="aside"]').getByTestId('comment')
|
||||
}
|
||||
|
||||
async checkCommentExist (message: string, count: number = 1): Promise<void> {
|
||||
await expect(this.page.locator('div[data-float="aside"] div.root span', { hasText: message })).toHaveCount(count)
|
||||
getCommentLocator (message: string): Locator {
|
||||
return this.comments.filter({
|
||||
has: this.page.locator('div.activityMessage p.p-inline', { hasText: message })
|
||||
})
|
||||
}
|
||||
|
||||
async resolveComments (message: string, position: string = '1'): Promise<void> {
|
||||
await this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: position })
|
||||
.hover()
|
||||
async checkCommentExist (message: string): Promise<void> {
|
||||
await expect(this.getCommentLocator(message)).toBeVisible()
|
||||
}
|
||||
|
||||
await this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: position })
|
||||
.locator('xpath=../..')
|
||||
.locator('div.tools button')
|
||||
.click()
|
||||
async checkCommentDoesNotExist (message: string): Promise<void> {
|
||||
await expect(this.getCommentLocator(message)).toHaveCount(0)
|
||||
}
|
||||
|
||||
async resolveComment (message: string): Promise<void> {
|
||||
const commentLocator = this.getCommentLocator(message)
|
||||
|
||||
await commentLocator.hover()
|
||||
await commentLocator.locator('div.tools button').click()
|
||||
}
|
||||
|
||||
async resolveAllComments (): Promise<void> {
|
||||
@@ -43,38 +46,18 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
||||
await expect(this.page.locator('div[data-float="aside"] div.root span', { hasText: message })).toHaveCount(0)
|
||||
}
|
||||
|
||||
async checkCommentCanBeResolved (message: string, position: number): Promise<void> {
|
||||
await this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: String(position) })
|
||||
.hover()
|
||||
async checkCommentCanBeResolved (message: string): Promise<void> {
|
||||
const commentLocator = this.getCommentLocator(message)
|
||||
|
||||
await expect(
|
||||
this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: String(position) })
|
||||
.locator('xpath=../..')
|
||||
.locator('div.tools button')
|
||||
).toBeEnabled()
|
||||
await commentLocator.hover()
|
||||
await expect(commentLocator.locator('div.tools button')).toBeEnabled()
|
||||
}
|
||||
|
||||
async checkCommentCanNotBeResolved (message: string, position: number): Promise<void> {
|
||||
await this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: String(position) })
|
||||
.hover()
|
||||
const commentLocator = this.getCommentLocator(message)
|
||||
|
||||
await expect(
|
||||
this.page
|
||||
.locator('div[data-float="aside"] div.root span', { hasText: message })
|
||||
.locator('xpath=..')
|
||||
.locator('span:first-child', { hasText: String(position) })
|
||||
.locator('xpath=../..')
|
||||
.locator('div.tools button')
|
||||
).not.toBeVisible()
|
||||
await commentLocator.hover()
|
||||
await expect(commentLocator.locator('div.tools button')).not.toBeVisible()
|
||||
}
|
||||
|
||||
async addReplyInPopupByCommentId (commentId: number, replyText: string): Promise<void> {
|
||||
@@ -96,7 +79,7 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
||||
.locator('div.popup div.root div.header > div > span:first-child', { hasText: String(commentId) })
|
||||
.locator('xpath=../../../..')
|
||||
// check header
|
||||
await expect(comment.locator('div.header > div > span:last-child')).toHaveText(header)
|
||||
await expect(comment.locator('div.header > div:first-child')).toContainText(header)
|
||||
// can be resolved
|
||||
await comment.locator('div.header div.tools button').hover()
|
||||
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
||||
@@ -119,7 +102,7 @@ export class DocumentCommentsPage extends DocumentCommonPage {
|
||||
.locator('div.box div.root div.header > div > span:first-child', { hasText: String(commentId) })
|
||||
.locator('xpath=../../../..')
|
||||
// check header
|
||||
await expect(comment.locator('div.header > div > span:last-child')).toHaveText(header)
|
||||
await expect(comment.locator('div.header > div:first-child')).toContainText(header)
|
||||
// can be resolved
|
||||
await comment.locator('div.header > div > span:last-child').hover()
|
||||
await expect(comment.locator('div.header div.tools button')).toBeEnabled()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, type Locator, type Page } from '@playwright/test'
|
||||
import { Content, DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../types'
|
||||
import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../types'
|
||||
import { DocumentCommonPage } from './document-common-page'
|
||||
import { iterateLocator, PlatformPassword } from '../../utils'
|
||||
import { PlatformPassword } from '../../utils'
|
||||
import { DocumentHistoryPage } from './document-history-page'
|
||||
|
||||
export class DocumentContentPage extends DocumentCommonPage {
|
||||
@@ -41,8 +41,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
readonly textPageHeader: Locator
|
||||
readonly buttonSelectNewOwnerChangeByQaraManager: Locator
|
||||
readonly textId: Locator
|
||||
readonly sectionsLocatorViewRight: Locator
|
||||
readonly sectionsLocatorEditRight: Locator
|
||||
readonly contentLocator: Locator
|
||||
readonly addSpaceButton: Locator
|
||||
readonly inputSpaceName: Locator
|
||||
readonly roleSelector: Locator
|
||||
@@ -123,8 +122,7 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
this.textPageHeader = page.locator('div.hulyNavPanel-header')
|
||||
this.buttonSelectNewOwnerChangeByQaraManager = page.locator('div.popup button[type="submit"]')
|
||||
this.textId = page.locator('div.flex:has(div.label:text("ID")) div.field')
|
||||
this.sectionsLocatorViewRight = page.locator('div.section span.label')
|
||||
this.sectionsLocatorEditRight = page.locator('div.section span.label input')
|
||||
this.contentLocator = page.locator('div.textInput div.tiptap')
|
||||
this.addSpaceButton = page.locator('#tree-orgspaces')
|
||||
this.inputSpaceName = page.getByPlaceholder('New documents space')
|
||||
this.roleSelector = page.getByRole('button', { name: 'Members' })
|
||||
@@ -164,14 +162,6 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.page.getByRole('button', { name }).click()
|
||||
}
|
||||
|
||||
async updateSectionTitle (sectionId: string, title: string): Promise<void> {
|
||||
await this.page
|
||||
.locator('span.hdr-alignment:not([class*="label"])', { hasText: sectionId })
|
||||
.locator('xpath=..')
|
||||
.locator('span.label input')
|
||||
.fill(title)
|
||||
}
|
||||
|
||||
async clickOnAddCategoryButton (): Promise<void> {
|
||||
await this.addCategoryButton.click()
|
||||
}
|
||||
@@ -245,29 +235,25 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
}
|
||||
}
|
||||
|
||||
async addContentToTheSection (content: Content): Promise<void> {
|
||||
const section = await this.getSectionLocator(content.sectionTitle)
|
||||
await section
|
||||
.locator('xpath=../../../../../../../..')
|
||||
.locator('div.content-container div.tiptap')
|
||||
.clear({ force: true })
|
||||
async addContent (content: string, append: boolean = false, newParagraph: boolean = false): Promise<void> {
|
||||
if (newParagraph) {
|
||||
await this.contentLocator.press('Enter')
|
||||
}
|
||||
|
||||
await section
|
||||
.locator('xpath=../../../../../../../..')
|
||||
.locator('div.content-container div.tiptap')
|
||||
.fill(content.content)
|
||||
if (append) {
|
||||
await this.contentLocator.pressSequentially(content)
|
||||
} else {
|
||||
await this.contentLocator.fill(content)
|
||||
}
|
||||
}
|
||||
|
||||
async checkContentForTheSection (content: Content): Promise<void> {
|
||||
const section = await this.getSectionLocator(content.sectionTitle)
|
||||
const parentLocator =
|
||||
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
|
||||
? '../../../../../../../..'
|
||||
: '../../../..'
|
||||
async replaceContent (content: string): Promise<void> {
|
||||
await this.contentLocator.clear({ force: true })
|
||||
await this.contentLocator.fill(content)
|
||||
}
|
||||
|
||||
await expect(section.locator(`xpath=${parentLocator}`).locator('div.content-container div.tiptap')).toHaveText(
|
||||
content.content
|
||||
)
|
||||
async checkContent (content: string): Promise<void> {
|
||||
await expect(this.contentLocator).toHaveText(content)
|
||||
}
|
||||
|
||||
async executeMoreActions (action: string): Promise<void> {
|
||||
@@ -351,6 +337,13 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.buttonHistoryTab.first().click()
|
||||
}
|
||||
|
||||
async createNewDraft (): Promise<void> {
|
||||
await this.buttonEditDocument.click()
|
||||
// It's important to wait for the draft status to make sure the content
|
||||
// is editable for the next steps
|
||||
await this.checkDocumentStatus(DocumentStatus.DRAFT)
|
||||
}
|
||||
|
||||
async checkIfHistoryVersionExists (description: string): Promise<void> {
|
||||
await this.page.waitForTimeout(200)
|
||||
await expect(this.page.getByText(description)).toBeVisible()
|
||||
@@ -464,20 +457,6 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await documentContentPageSecond.clickDocumentHeader(completeDocument.title + ' ' + prevVersion)
|
||||
}
|
||||
|
||||
async addMessageToTheSectionTitle (title: string, message: string, closePopup: boolean = true): Promise<void> {
|
||||
const locator = await this.getSectionLocator(title)
|
||||
const parentLocator =
|
||||
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING ? '../../../../..' : '../..'
|
||||
|
||||
await locator.locator(`xpath=${parentLocator}`).hover()
|
||||
await locator.locator(`xpath=${parentLocator}`).locator('div.tools button[type="button"]').click()
|
||||
await this.addMessage(message)
|
||||
|
||||
if (closePopup) {
|
||||
await this.closeNewMessagePopup()
|
||||
}
|
||||
}
|
||||
|
||||
async closeNewMessagePopup (): Promise<void> {
|
||||
await this.textPageHeader.press('Escape', { delay: 300 })
|
||||
await this.textPageHeader.click({ force: true, delay: 300, position: { x: 1, y: 1 } })
|
||||
@@ -502,18 +481,6 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.buttonSubmit.click()
|
||||
}
|
||||
|
||||
async addNewSection (startSectionId: string, direction: 'above' | 'below'): Promise<void> {
|
||||
await this.page.locator('span.hdr-alignment:not([class*="label"])', { hasText: startSectionId }).hover()
|
||||
|
||||
await this.page
|
||||
.locator('span.hdr-alignment:not([class*="label"])', { hasText: startSectionId })
|
||||
.locator('xpath=../../..')
|
||||
.locator('div.tools[draggable="true"]')
|
||||
.click()
|
||||
|
||||
await this.selectFromDropdown(this.page, `Add new section ${direction}`)
|
||||
}
|
||||
|
||||
async changeCurrentRight (newRight: DocumentRights): Promise<void> {
|
||||
await this.buttonCurrentRights.click()
|
||||
await this.selectMenuItem(this.page, newRight)
|
||||
@@ -537,29 +504,4 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.selectListItemWithSearch(this.page, newOwner)
|
||||
await this.buttonSelectNewOwnerChangeByQaraManager.click()
|
||||
}
|
||||
|
||||
private async getSectionLocator (title: string): Promise<Locator> {
|
||||
let result: Locator | null = null
|
||||
const sectionsLocator = iterateLocator(
|
||||
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
|
||||
? this.sectionsLocatorEditRight
|
||||
: this.sectionsLocatorViewRight
|
||||
)
|
||||
for await (const locator of sectionsLocator) {
|
||||
const value =
|
||||
(await this.buttonCurrentRights.textContent()) === DocumentRights.EDITING
|
||||
? await locator.inputValue()
|
||||
: ((await locator.textContent()) ?? '').trim()
|
||||
if (value === title) {
|
||||
result = locator
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
throw new Error('Section not found')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user