Skip to content

Commit 7be55ef

Browse files
committed
working state after refactoring
Signed-off-by: Anna Khismatullina <[email protected]>
1 parent b2c808b commit 7be55ef

File tree

3 files changed

+225
-41
lines changed

3 files changed

+225
-41
lines changed

packages/importer/src/huly/huly.ts

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import core, {
2727
Attribute,
2828
type Class,
2929
type Doc,
30+
isId,
3031
generateId,
3132
PersonId,
3233
type Ref,
@@ -63,6 +64,8 @@ import { type Logger } from '../importer/logger'
6364
import { BaseMarkdownPreprocessor } from '../importer/preprocessor'
6465
import { type FileUploader } from '../importer/uploader'
6566
import { Props, UnifiedDoc } from '../types'
67+
import { readMarkdownContent, readYamlHeader } from './parsing'
68+
import { UnifiedDocProcessor } from './unified'
6669

6770
export interface HulyComment {
6871
author: string
@@ -350,13 +353,16 @@ export class HulyFormatImporter {
350353

351354
private readonly personIdByEmail = new Map<string, PersonId>()
352355

356+
private readonly unifiedDocImporter: UnifiedDocProcessor
357+
353358
constructor (
354359
private readonly client: TxOperations,
355360
private readonly fileUploader: FileUploader,
356361
private readonly logger: Logger,
357362
private readonly importerSocialId?: PersonId,
358363
private readonly importerPerson?: Ref<Person>
359364
) {
365+
this.unifiedDocImporter = new UnifiedDocProcessor(this.client, this.logger)
360366
}
361367

362368
private async initCaches (): Promise<void> {
@@ -482,6 +488,29 @@ export class HulyFormatImporter {
482488
}
483489
}
484490

491+
// Импортируем UnifiedDoc сущности
492+
const unifiedResult = await this.unifiedDocImporter.importFromDirectory(folderPath)
493+
494+
// Разбираем и добавляем в билдер по классу
495+
for (const [path, docs] of unifiedResult.entries()) {
496+
for (const doc of docs) {
497+
switch (doc._class) {
498+
case card.class.MasterTag:
499+
builder.addMasterTag(path, doc as UnifiedDoc<MasterTag>)
500+
break
501+
case core.class.Attribute:
502+
builder.addMasterTagAttributes(path, [doc as UnifiedDoc<Attribute<MasterTag>>])
503+
break
504+
default:
505+
if (isId(doc._class)) {
506+
builder.addCard(path, doc as UnifiedDoc<Card>)
507+
} else {
508+
this.logger.error(`Unknown doc class ${String(doc._class)} for path ${path}`)
509+
}
510+
}
511+
}
512+
}
513+
485514
// Process all yaml files first
486515
const yamlFiles = fs.readdirSync(folderPath).filter((f) => f.endsWith('.yaml') && f !== 'settings.yaml')
487516

@@ -528,22 +557,7 @@ export class HulyFormatImporter {
528557
}
529558

530559
case card.class.MasterTag: {
531-
const masterTag = await this.processMasterTag(spaceConfig)
532-
const masterTagId = masterTag.props._id as Ref<MasterTag>
533-
if (masterTagId === undefined) {
534-
throw new Error('Master tag ID is undefined')
535-
}
536-
builder.addMasterTag(spacePath, masterTag)
537-
538-
const attributesByLabel = await this.processMasterTagAttributes(spaceConfig, masterTagId)
539-
builder.addMasterTagAttributes(spacePath, Array.from(attributesByLabel.values()))
540-
541-
if (fs.existsSync(spacePath) && fs.statSync(spacePath).isDirectory()) {
542-
// Сначала обрабатываем дочерние мастер-теги
543-
await this.processSubTagsRecursively(builder, spacePath, spacePath, masterTagId, attributesByLabel)
544-
// Затем обрабатываем карточки
545-
await this.processCardsRecursively(builder, spacePath, spacePath, masterTagId, attributesByLabel)
546-
}
560+
this.logger.log(`Skipping ${spaceName}: master tag already processed`)
547561
break
548562
}
549563

@@ -669,7 +683,7 @@ export class HulyFormatImporter {
669683

670684
for (const cardFile of cardFiles) {
671685
const cardPath = path.join(currentPath, cardFile)
672-
const cardHeader = (await this.readYamlHeader(cardPath)) as Record<string, any>
686+
const cardHeader = await readYamlHeader(cardPath)
673687

674688
if (cardHeader.class === undefined) { // means it's a card of class MasterTag
675689
const card = await this.processCard(cardHeader, cardPath, masterTagId, attributesByLabel)
@@ -710,7 +724,7 @@ export class HulyFormatImporter {
710724
return {
711725
_class: masterTagId,
712726
collabField: 'content',
713-
contentProvider: () => this.readMarkdownContent(cardPath),
727+
contentProvider: () => readMarkdownContent(cardPath),
714728
props: props as Props<Card> // todo: what is the correct props type?
715729
}
716730
}
@@ -726,7 +740,7 @@ export class HulyFormatImporter {
726740

727741
for (const issueFile of issueFiles) {
728742
const issuePath = path.join(currentPath, issueFile)
729-
const issueHeader = (await this.readYamlHeader(issuePath)) as HulyIssueHeader
743+
const issueHeader = (await readYamlHeader(issuePath)) as HulyIssueHeader
730744

731745
if (issueHeader.class === undefined) {
732746
this.logger.error(`Skipping ${issueFile}: not an issue`)
@@ -750,7 +764,7 @@ export class HulyFormatImporter {
750764
class: tracker.class.Issue,
751765
title: issueHeader.title,
752766
number: parseInt(issueNumber ?? 'NaN'),
753-
descrProvider: async () => await this.readMarkdownContent(issuePath),
767+
descrProvider: async () => await readMarkdownContent(issuePath),
754768
status: { name: issueHeader.status },
755769
priority: issueHeader.priority,
756770
estimation: issueHeader.estimation,
@@ -838,7 +852,7 @@ export class HulyFormatImporter {
838852

839853
for (const docFile of docFiles) {
840854
const docPath = path.join(currentPath, docFile)
841-
const docHeader = (await this.readYamlHeader(docPath)) as HulyDocumentHeader
855+
const docHeader = (await readYamlHeader(docPath)) as HulyDocumentHeader
842856

843857
if (docHeader.class === undefined) {
844858
this.logger.error(`Skipping ${docFile}: not a document`)
@@ -859,7 +873,7 @@ export class HulyFormatImporter {
859873
id: docMeta.id as Ref<Document>,
860874
class: document.class.Document,
861875
title: docHeader.title,
862-
descrProvider: async () => await this.readMarkdownContent(docPath),
876+
descrProvider: async () => await readMarkdownContent(docPath),
863877
subdocs: [] // Will be added via builder
864878
}
865879

@@ -886,7 +900,7 @@ export class HulyFormatImporter {
886900

887901
for (const docFile of docFiles) {
888902
const docPath = path.join(currentPath, docFile)
889-
const docHeader = (await this.readYamlHeader(docPath)) as
903+
const docHeader = (await readYamlHeader(docPath)) as
890904
| HulyControlledDocumentHeader
891905
| HulyDocumentTemplateHeader
892906

@@ -1087,7 +1101,7 @@ export class HulyFormatImporter {
10871101
reviewers: header.reviewers?.map((email) => this.findEmployeeByName(email)) ?? [],
10881102
approvers: header.approvers?.map((email) => this.findEmployeeByName(email)) ?? [],
10891103
coAuthors: header.coAuthors?.map((email) => this.findEmployeeByName(email)) ?? [],
1090-
descrProvider: async () => await this.readMarkdownContent(docPath),
1104+
descrProvider: async () => await readMarkdownContent(docPath),
10911105
ccReason: header.changeControl?.reason,
10921106
ccImpact: header.changeControl?.impact,
10931107
ccDescription: header.changeControl?.description,
@@ -1125,30 +1139,14 @@ export class HulyFormatImporter {
11251139
reviewers: header.reviewers?.map((email) => this.findEmployeeByName(email)) ?? [],
11261140
approvers: header.approvers?.map((email) => this.findEmployeeByName(email)) ?? [],
11271141
coAuthors: header.coAuthors?.map((email) => this.findEmployeeByName(email)) ?? [],
1128-
descrProvider: async () => await this.readMarkdownContent(docPath),
1142+
descrProvider: async () => await readMarkdownContent(docPath),
11291143
ccReason: header.changeControl?.reason,
11301144
ccImpact: header.changeControl?.impact,
11311145
ccDescription: header.changeControl?.description,
11321146
subdocs: []
11331147
}
11341148
}
11351149

1136-
private async readYamlHeader (filePath: string): Promise<any> {
1137-
this.logger.log('Read YAML header from: ' + filePath)
1138-
const content = fs.readFileSync(filePath, 'utf8')
1139-
const match = content.match(/^---\n([\s\S]*?)\n---/)
1140-
if (match != null) {
1141-
return yaml.load(match[1])
1142-
}
1143-
return {}
1144-
}
1145-
1146-
private async readMarkdownContent (filePath: string): Promise<string> {
1147-
const content = fs.readFileSync(filePath, 'utf8')
1148-
const match = content.match(/^---\n[\s\S]*?\n---\n(.*)$/s)
1149-
return match != null ? match[1] : content
1150-
}
1151-
11521150
private async cacheAccountsByEmails (): Promise<void> {
11531151
const employees = await this.client.findAll(
11541152
contact.mixin.Employee,

packages/importer/src/huly/parsing.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as fs from 'fs'
2+
import * as yaml from 'js-yaml'
3+
4+
export async function readYamlHeader (filePath: string): Promise<any> {
5+
const content = fs.readFileSync(filePath, 'utf8')
6+
const match = content.match(/^---\n([\s\S]*?)\n---/)
7+
if (match != null) {
8+
return yaml.load(match[1])
9+
}
10+
return {}
11+
}
12+
13+
export async function readMarkdownContent (filePath: string): Promise<string> {
14+
const content = fs.readFileSync(filePath, 'utf8')
15+
const match = content.match(/^---\n[\s\S]*?\n---\n(.*)$/s)
16+
return match != null ? match[1] : content
17+
}

packages/importer/src/huly/unified.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// unified.ts
2+
import { UnifiedDoc, Props } from '../types'
3+
import card, { Card, MasterTag } from '@hcengineering/card'
4+
import core, {
5+
Attribute,
6+
type Doc,
7+
generateId,
8+
type Ref,
9+
type TxOperations
10+
} from '@hcengineering/core'
11+
import * as fs from 'fs'
12+
import * as path from 'path'
13+
import * as yaml from 'js-yaml'
14+
import { Logger } from '../importer/logger'
15+
import { IntlString } from '../../../platform/types'
16+
import { readMarkdownContent, readYamlHeader } from './parsing'
17+
18+
export type UnifiedDocProcessResult = Map<string, Array<UnifiedDoc<Doc>>>
19+
20+
export class UnifiedDocProcessor {
21+
constructor (
22+
private readonly client: TxOperations,
23+
private readonly logger: Logger
24+
) {}
25+
26+
async importFromDirectory (directoryPath: string): Promise<UnifiedDocProcessResult> {
27+
const unifiedDocs: UnifiedDocProcessResult = new Map()
28+
await this.processDirectory(directoryPath, unifiedDocs)
29+
return unifiedDocs
30+
}
31+
32+
private async processDirectory (
33+
currentPath: string,
34+
result: UnifiedDocProcessResult,
35+
parentMasterTagId?: Ref<MasterTag>,
36+
parentAttributesByLabel?: Map<string, UnifiedDoc<Attribute<MasterTag>>>
37+
): Promise<void> {
38+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
39+
40+
// Сначала обрабатываем YAML файлы (потенциальные мастер-теги)
41+
for (const entry of entries) {
42+
if (!entry.isFile() || !entry.name.endsWith('.yaml')) continue // todo: filter entries by extension
43+
44+
const yamlPath = path.join(currentPath, entry.name)
45+
const yamlConfig = yaml.load(fs.readFileSync(yamlPath, 'utf8')) as Record<string, any>
46+
47+
if (yamlConfig?.class === card.class.MasterTag) {
48+
const masterTag = await this.createMasterTag(yamlConfig, parentMasterTagId)
49+
const masterTagId = masterTag.props._id as Ref<MasterTag>
50+
const attributesByLabel = await this.createMasterTagAttributes(yamlConfig, masterTagId)
51+
52+
// Добавляем мастер-тег и его атрибуты
53+
const docs = result.get(yamlPath) ?? []
54+
docs.push(
55+
masterTag,
56+
...Array.from(attributesByLabel.values())
57+
)
58+
result.set(yamlPath, docs)
59+
60+
// Рекурсивно обрабатываем содержимое директории мастер-тега
61+
const tagDir = path.join(currentPath, path.basename(yamlPath, '.yaml'))
62+
if (fs.existsSync(tagDir) && fs.statSync(tagDir).isDirectory()) {
63+
await this.processDirectory(tagDir, result, masterTagId, attributesByLabel)
64+
}
65+
}
66+
}
67+
68+
if (parentMasterTagId === undefined || parentAttributesByLabel === undefined) {
69+
// means we are in the root directory
70+
return
71+
}
72+
73+
// Затем обрабатываем markdown файлы (карточки)
74+
for (const entry of entries) {
75+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue
76+
77+
const cardPath = path.join(currentPath, entry.name)
78+
const cardHeader = await readYamlHeader(cardPath)
79+
const unifiedDoc = await this.createCard(cardHeader, cardPath, parentMasterTagId, parentAttributesByLabel)
80+
81+
if (unifiedDoc != null) {
82+
const docs = result.get(cardPath) ?? []
83+
docs.push(unifiedDoc)
84+
result.set(cardPath, docs)
85+
}
86+
}
87+
}
88+
89+
private async createMasterTag (
90+
data: Record<string, any>,
91+
parentMasterTagId?: Ref<MasterTag>
92+
): Promise<UnifiedDoc<MasterTag>> {
93+
const { class: _class, title } = data
94+
if (_class !== card.class.MasterTag) {
95+
throw new Error('Invalid master tag data')
96+
}
97+
98+
return {
99+
_class: card.class.MasterTag,
100+
props: {
101+
_id: generateId<MasterTag>(),
102+
space: core.space.Model,
103+
extends: parentMasterTagId ?? card.class.Card,
104+
label: 'embedded:embedded:' + title as IntlString, // todo: check if it's correct
105+
kind: 0,
106+
icon: card.icon.MasterTag
107+
}
108+
}
109+
}
110+
111+
private async createMasterTagAttributes (
112+
data: Record<string, any>,
113+
masterTagId: Ref<MasterTag>
114+
): Promise<Map<string, UnifiedDoc<Attribute<MasterTag>>>> {
115+
if (data.properties === undefined) {
116+
return new Map()
117+
}
118+
119+
const attributesByLabel = new Map<string, UnifiedDoc<Attribute<MasterTag>>>()
120+
for (const property of data.properties) {
121+
const attr: UnifiedDoc<Attribute<MasterTag>> = {
122+
_class: core.class.Attribute,
123+
props: {
124+
space: core.space.Model,
125+
attributeOf: masterTagId,
126+
name: generateId<Attribute<MasterTag>>(),
127+
label: 'embedded:embedded:' + property.label as IntlString, // todo: check if it's correct
128+
isCustom: true,
129+
type: {
130+
_class: 'core:class:' + property.type
131+
},
132+
defaultValue: property.defaultValue ?? null
133+
}
134+
}
135+
attributesByLabel.set(property.label, attr)
136+
}
137+
return attributesByLabel
138+
}
139+
140+
private async createCard (
141+
cardHeader: Record<string, any>,
142+
cardPath: string,
143+
masterTagId: Ref<MasterTag>,
144+
attributesByLabel: Map<string, UnifiedDoc<Attribute<MasterTag>>>
145+
): Promise<UnifiedDoc<Card>> {
146+
const { _class, title, ...customProperties } = cardHeader
147+
148+
const props: Record<string, any> = {
149+
_id: generateId(),
150+
space: core.space.Workspace,
151+
title
152+
}
153+
154+
for (const [key, value] of Object.entries(customProperties)) {
155+
const attributeName = attributesByLabel.get(key)?.props.name
156+
if (attributeName === undefined) {
157+
throw new Error(`Attribute not found: ${key}`) // todo: keep the error till builder validation
158+
}
159+
props[attributeName] = value
160+
}
161+
162+
return {
163+
_class: masterTagId,
164+
collabField: 'content',
165+
contentProvider: () => readMarkdownContent(cardPath),
166+
props: props as Props<Card> // todo: what is the correct props type?
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)