mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 21:27:46 +02:00
Export product version with controlled docs (#10586)
* Export product version with controlled docs Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix tests Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -150,6 +150,21 @@ export interface Relation extends Doc {
|
||||
association: Ref<Association>
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an existing class field with reference to other document: which relations to follow when building documents graph.
|
||||
* @public
|
||||
*/
|
||||
export interface RelationMetadata extends Doc {
|
||||
/** Class (source) */
|
||||
sourceClass: Ref<Class<Doc>>
|
||||
/** Class referenced by the field (target) */
|
||||
targetClass: Ref<Class<Doc>>
|
||||
/** Field on the source class */
|
||||
field: string
|
||||
/** Whether this is a forward (source→target) or inverse (target→source) relation */
|
||||
direction?: 'forward' | 'inverse'
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
RefTo,
|
||||
RelatedDocument,
|
||||
Relation,
|
||||
RelationMetadata,
|
||||
Role,
|
||||
Sequence,
|
||||
Space,
|
||||
@@ -175,6 +176,7 @@ export default plugin(coreId, {
|
||||
FullTextSearchContext: '' as Ref<Mixin<FullTextSearchContext>>,
|
||||
Association: '' as Ref<Class<Association>>,
|
||||
Relation: '' as Ref<Class<Relation>>,
|
||||
RelationMetadata: '' as Ref<Class<RelationMetadata>>,
|
||||
Sequence: '' as Ref<Class<Sequence>>,
|
||||
CustomSequence: '' as Ref<Class<CustomSequence>>,
|
||||
ClassCollaborators: '' as Ref<Class<ClassCollaborators<Doc>>>,
|
||||
|
||||
@@ -26,7 +26,7 @@ import documentsPlugin, {
|
||||
type ChangeControl,
|
||||
type DocumentRequest
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import exportPlugin, { type RelationDefinition } from '@hcengineering/export'
|
||||
import exportPlugin from '@hcengineering/export'
|
||||
import { type Builder } from '@hcengineering/model'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
import core from '@hcengineering/model-core'
|
||||
@@ -79,6 +79,31 @@ import {
|
||||
export { documentsId } from '@hcengineering/controlled-documents/src/index'
|
||||
export * from './types'
|
||||
|
||||
function defineRelationMetadata (builder: Builder): void {
|
||||
const rel = (
|
||||
ref: Ref<Class<Doc>>,
|
||||
field: string,
|
||||
targetClass: Ref<Class<Doc>>,
|
||||
direction: 'forward' | 'inverse' = 'forward'
|
||||
): void => {
|
||||
builder.createDoc(core.class.RelationMetadata, core.space.Model, {
|
||||
sourceClass: ref,
|
||||
field,
|
||||
targetClass,
|
||||
direction
|
||||
})
|
||||
}
|
||||
|
||||
rel(documents.class.Document, 'category', documents.class.DocumentCategory, 'forward')
|
||||
rel(documents.class.Document, 'template', documents.class.Document, 'forward')
|
||||
rel(documents.class.Document, 'meta', documents.class.ProjectMeta, 'inverse')
|
||||
rel(documents.class.Document, 'document', documents.class.ProjectDocument, 'inverse')
|
||||
rel(documents.class.HierarchyDocument, 'attachedTo', documents.class.DocumentMeta, 'forward')
|
||||
rel(documents.class.ControlledDocument, 'changeControl', documents.class.ChangeControl, 'forward')
|
||||
rel(documents.class.ProjectMeta, 'meta', documents.class.ProjectMeta, 'inverse')
|
||||
rel(documents.class.ProjectMeta, 'document', documents.class.ProjectDocument, 'inverse')
|
||||
}
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TDocumentSpace,
|
||||
@@ -109,6 +134,8 @@ export function createModel (builder: Builder): void {
|
||||
TDocumentComment
|
||||
)
|
||||
|
||||
defineRelationMetadata(builder)
|
||||
|
||||
builder.mixin(documents.class.ControlledDocument, core.class.Class, view.mixin.ObjectTitle, {
|
||||
titleProvider: documents.function.ControlledDocumentTitleProvider
|
||||
})
|
||||
@@ -847,19 +874,6 @@ export function createModel (builder: Builder): void {
|
||||
documents.action.TransferDocument
|
||||
)
|
||||
|
||||
const relations: RelationDefinition[] = [
|
||||
// Forward relations - migrate referenced documents first
|
||||
{ field: 'attachedTo', class: documents.class.DocumentMeta },
|
||||
{ field: 'changeControl', class: documents.class.ChangeControl },
|
||||
{ field: 'category', class: documents.class.DocumentCategory },
|
||||
{ field: 'template', class: documents.class.Document },
|
||||
// Inverse relations - find documents that reference this one
|
||||
// ProjectMeta references DocumentMeta via 'meta' field - must be migrated before ProjectDocument
|
||||
{ field: 'meta', class: documents.class.ProjectMeta, direction: 'inverse' },
|
||||
// ProjectDocument references ControlledDocument via 'document' field
|
||||
{ field: 'document', class: documents.class.ProjectDocument, direction: 'inverse' }
|
||||
]
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
@@ -868,9 +882,6 @@ export function createModel (builder: Builder): void {
|
||||
component: exportPlugin.component.ExportToWorkspaceModal,
|
||||
fillProps: {
|
||||
_objects: 'value'
|
||||
},
|
||||
props: {
|
||||
relations
|
||||
}
|
||||
},
|
||||
label: exportPlugin.string.ExportToWorkspace,
|
||||
@@ -896,7 +907,6 @@ export function createModel (builder: Builder): void {
|
||||
_object: 'value'
|
||||
},
|
||||
props: {
|
||||
relations,
|
||||
spaceExport: true,
|
||||
docClass: documents.class.ControlledDocument
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
type Ref,
|
||||
type RefTo,
|
||||
type Relation,
|
||||
type RelationMetadata,
|
||||
type Sequence,
|
||||
type Space,
|
||||
type Timestamp,
|
||||
@@ -161,6 +162,22 @@ export class TRelation extends TDoc implements Relation {
|
||||
association!: Ref<Association>
|
||||
}
|
||||
|
||||
@Model(core.class.RelationMetadata, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TRelationMetadata extends TDoc implements RelationMetadata {
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
sourceClass!: Ref<Class<Doc>>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.TargetClass)
|
||||
targetClass!: Ref<Class<Doc>>
|
||||
|
||||
@Prop(TypeString(), core.string.ClassPropertyLabel)
|
||||
field!: string
|
||||
|
||||
@Prop(TypeString(), core.string.Description)
|
||||
direction?: 'forward' | 'inverse'
|
||||
}
|
||||
|
||||
@Model(core.class.Blob, core.class.Doc, DOMAIN_BLOB)
|
||||
@UX(core.string.Object)
|
||||
export class TBlob extends TDoc implements Blob {
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
TPluginConfiguration,
|
||||
TRefTo,
|
||||
TRelation,
|
||||
TRelationMetadata,
|
||||
TSequence,
|
||||
TTransientConfiguration,
|
||||
TType,
|
||||
@@ -177,6 +178,7 @@ export function createModel (builder: Builder): void {
|
||||
TMigrationState,
|
||||
TBlob,
|
||||
TRelation,
|
||||
TRelationMetadata,
|
||||
TAssociation,
|
||||
TDomainIndexConfiguration,
|
||||
TBenchmarkDoc,
|
||||
|
||||
@@ -22,7 +22,17 @@ import { type Attachment } from '@hcengineering/attachment'
|
||||
import contact from '@hcengineering/contact'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { getRoleAttributeProps } from '@hcengineering/setting'
|
||||
import type { Type, Ref, CollectionSize, Markup, RolesAssignment, Permission, Role } from '@hcengineering/core'
|
||||
import type {
|
||||
Type,
|
||||
Ref,
|
||||
CollectionSize,
|
||||
Markup,
|
||||
RolesAssignment,
|
||||
Permission,
|
||||
Role,
|
||||
Class,
|
||||
Doc
|
||||
} from '@hcengineering/core'
|
||||
import { IndexKind, AccountUuid } from '@hcengineering/core'
|
||||
import {
|
||||
type Builder,
|
||||
@@ -418,6 +428,25 @@ function defineProductVersionState (builder: Builder): void {
|
||||
})
|
||||
}
|
||||
|
||||
function defineRelationMetadata (builder: Builder): void {
|
||||
const rel = (
|
||||
sourceClass: Ref<Class<Doc>>,
|
||||
field: string,
|
||||
targetClass: Ref<Class<Doc>>,
|
||||
direction: 'forward' | 'inverse' = 'forward'
|
||||
): void => {
|
||||
builder.createDoc(core.class.RelationMetadata, core.space.Model, {
|
||||
sourceClass,
|
||||
field,
|
||||
targetClass,
|
||||
direction
|
||||
})
|
||||
}
|
||||
|
||||
// Product → ProductVersion via `space` (inverse: versions that belong to this product)
|
||||
rel(products.class.Product, 'space', products.class.ProductVersion, 'inverse')
|
||||
}
|
||||
|
||||
function defineApplication (builder: Builder): void {
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
@@ -467,6 +496,7 @@ export function createModel (builder: Builder): void {
|
||||
defineProduct(builder)
|
||||
defineProductVersion(builder)
|
||||
defineProductVersionState(builder)
|
||||
defineRelationMetadata(builder)
|
||||
defineApplication(builder)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import { DropdownLabels, DropdownLabelsIntl, Label } from '@hcengineering/ui'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import login from '@hcengineering/login'
|
||||
import { type RelationDefinition, shouldSkipDocument, isEffectiveDocument } from '@hcengineering/export'
|
||||
import { shouldSkipDocument, isEffectiveDocument } from '@hcengineering/export'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
|
||||
export let query: DocumentQuery<Doc> | undefined = undefined
|
||||
export let value: Doc | Doc[] | Space
|
||||
export let relations: RelationDefinition[] | undefined = undefined
|
||||
export let docClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let spaceExport: boolean | undefined = false
|
||||
|
||||
@@ -127,7 +126,7 @@
|
||||
exportQuery,
|
||||
filteredSelectedDocs,
|
||||
targetWorkspace,
|
||||
relations,
|
||||
undefined,
|
||||
exportFilterMode === 'skipArchivedObsolete',
|
||||
exportFilterMode === 'effectiveOnly'
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function exportToWorkspace (
|
||||
query: DocumentQuery<Doc> | undefined,
|
||||
selectedDocs: Doc[],
|
||||
targetWorkspace: string | undefined,
|
||||
relations: RelationDefinition[] | undefined,
|
||||
relations?: RelationDefinition[] | undefined,
|
||||
skipDeletedObsolete?: boolean,
|
||||
exportOnlyEffective?: boolean
|
||||
): Promise<void> {
|
||||
@@ -66,11 +66,13 @@ export async function exportToWorkspace (
|
||||
const body: any = {
|
||||
targetWorkspace,
|
||||
_class,
|
||||
relations,
|
||||
fieldMappers,
|
||||
skipDeletedObsolete,
|
||||
exportOnlyEffective
|
||||
}
|
||||
if (relations != null) {
|
||||
body.relations = relations
|
||||
}
|
||||
|
||||
body.query =
|
||||
selectedDocs.length > 0
|
||||
|
||||
@@ -135,6 +135,15 @@ export class DocumentExporter {
|
||||
sourceHierarchy,
|
||||
sourceLowLevel
|
||||
)
|
||||
await this.exportSpaceRelations(
|
||||
doc,
|
||||
doc.space,
|
||||
conflictStrategy,
|
||||
includeAttachments,
|
||||
sourceHierarchy,
|
||||
sourceLowLevel,
|
||||
relations
|
||||
)
|
||||
|
||||
// Handle attachments
|
||||
if (includeAttachments) {
|
||||
@@ -157,6 +166,49 @@ export class DocumentExporter {
|
||||
}
|
||||
}
|
||||
|
||||
private async exportSpaceRelations (
|
||||
doc: Doc,
|
||||
space: Ref<Space>,
|
||||
conflictStrategy: 'skip' | 'duplicate',
|
||||
includeAttachments: boolean,
|
||||
sourceHierarchy: Hierarchy,
|
||||
sourceLowLevel: LowLevelStorage,
|
||||
relations: RelationDefinition[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (this.relationExporter === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const spaceDomain = sourceHierarchy.findDomain(core.class.Space)
|
||||
if (spaceDomain === undefined) {
|
||||
this.context.warn('Space domain not found')
|
||||
return
|
||||
}
|
||||
|
||||
const sourceSpaces = await sourceLowLevel.rawFindAll<Space>(spaceDomain, { _id: space })
|
||||
const spaceDoc = sourceSpaces[0]
|
||||
if (spaceDoc == null) {
|
||||
this.context.warn(`Source space ${space} not found for exporting relations`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.relationExporter.exportAllRelations(
|
||||
spaceDoc,
|
||||
relations,
|
||||
conflictStrategy,
|
||||
includeAttachments,
|
||||
sourceHierarchy,
|
||||
sourceLowLevel
|
||||
)
|
||||
} catch (err: any) {
|
||||
this.context.error(`Failed to export relations for space ${space}:`, {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
docId: doc._id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async createDocument (
|
||||
sourceDoc: Doc,
|
||||
data: any,
|
||||
|
||||
@@ -119,6 +119,32 @@ export class RelationExporter {
|
||||
}
|
||||
}
|
||||
|
||||
async exportAllRelations (
|
||||
doc: Doc,
|
||||
relations: RelationDefinition[],
|
||||
conflictStrategy: 'skip' | 'duplicate',
|
||||
includeAttachments: boolean,
|
||||
sourceHierarchy: Hierarchy,
|
||||
sourceLowLevel: LowLevelStorage
|
||||
): Promise<void> {
|
||||
await this.exportForwardRelations(
|
||||
doc,
|
||||
relations,
|
||||
conflictStrategy,
|
||||
includeAttachments,
|
||||
sourceHierarchy,
|
||||
sourceLowLevel
|
||||
)
|
||||
await this.exportInverseRelations(
|
||||
doc,
|
||||
relations,
|
||||
conflictStrategy,
|
||||
includeAttachments,
|
||||
sourceHierarchy,
|
||||
sourceLowLevel
|
||||
)
|
||||
}
|
||||
|
||||
private async exportForwardRelation (
|
||||
doc: Doc,
|
||||
relation: RelationDefinition,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
import core, {
|
||||
type AccountUuid,
|
||||
type Doc,
|
||||
type MeasureContext,
|
||||
@@ -178,6 +178,25 @@ export class CrossWorkspaceExporter {
|
||||
throw new Error('Low level storage not available')
|
||||
}
|
||||
|
||||
// Resolve relations from RelationMetadata when not provided
|
||||
let resolvedRelations = relations
|
||||
try {
|
||||
if (resolvedRelations.length === 0) {
|
||||
const relations = await sourcePipeline.findAll(this.context, core.class.RelationMetadata, {})
|
||||
resolvedRelations = relations.map((doc) => ({
|
||||
field: doc.field,
|
||||
class: doc.targetClass,
|
||||
direction: (doc.direction ?? 'forward') as 'forward' | 'inverse'
|
||||
}))
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.context.error('Failed to get relations:', {
|
||||
error: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
}
|
||||
const relationsCount = resolvedRelations != null ? resolvedRelations.length : 0
|
||||
this.context.info(`Number of document relations: ${relationsCount}`)
|
||||
|
||||
// Get domain for the class
|
||||
const domain = hierarchy.findDomain(_class)
|
||||
if (domain === undefined) {
|
||||
@@ -235,7 +254,7 @@ export class CrossWorkspaceExporter {
|
||||
hierarchy,
|
||||
lowLevelStorage,
|
||||
existingDocsMap,
|
||||
relations
|
||||
resolvedRelations
|
||||
)
|
||||
if (exported) {
|
||||
result.exportedCount++
|
||||
|
||||
Reference in New Issue
Block a user