diff --git a/.changeset/pretty-numbers-bow.md b/.changeset/pretty-numbers-bow.md new file mode 100644 index 0000000..c2e1733 --- /dev/null +++ b/.changeset/pretty-numbers-bow.md @@ -0,0 +1,5 @@ +--- +"coda-mover": patch +--- + +Fix order of sub docs on Outline after imports diff --git a/src/modules/simple-mover/Mover.ts b/src/modules/simple-mover/Mover.ts index 03d01f1..a622cfa 100644 --- a/src/modules/simple-mover/Mover.ts +++ b/src/modules/simple-mover/Mover.ts @@ -255,17 +255,17 @@ export class Mover implements IMover { } setStatus (id: string, status: IStatus, message?: string) { - const itemId = id.replace('import::', '') - const itemName = this.items[itemId]?.name + const itemId = id.replace(/^.+::/, '') // remove prefixes + const itemName = this.items[itemId]?.name || '' const itemStatus: IItemStatus = { id, status, message, name: itemName } this._itemStatuses[id] = itemStatus if (status === ITEM_STATUS_ERROR) { - log.error(`[mover] ${id} ${itemName}`, status, message) + log.error(`[mover] ${id} ${itemName}`, `[${status}]`, message) } else if (message) { - log.info(`[mover] ${id} ${itemName}`, status, message) + log.info(`[mover] ${id} ${itemName}`, `[${status}]`, message) } else { - log.info(`[mover] ${id} ${itemName}`, status) + log.info(`[mover] ${id} ${itemName}`, `[${status}]`) } /** @@ -292,6 +292,14 @@ export class Mover implements IMover { return this._itemStatuses[id].status || '' } + getInnerPages (item: ICodaItem) { + const innerCodaTreePath = `${item.treePath}${item.id}/` + + return Object.values(this.items).filter(page => ( + page.treePath === innerCodaTreePath + )) + } + private cancelExports () { if (this._exporter) { log.info('[mover] cancel exports') diff --git a/src/modules/simple-mover/apis/OutlineApis.ts b/src/modules/simple-mover/apis/OutlineApis.ts index c44dc81..87ddd5a 100644 --- a/src/modules/simple-mover/apis/OutlineApis.ts +++ b/src/modules/simple-mover/apis/OutlineApis.ts @@ -7,6 +7,7 @@ import type { IOutlineSearchDocumentResult, IOutlineDocumentUpdateInput, IOutlineDocumentCreateInput, + IOutlineDocumentMoveInput, } from './interfaces' import { createReadStream, pathExists } from 'fs-extra' import FormData from 'form-data' @@ -97,4 +98,8 @@ export class OutlineApis implements IOutlineApis { return data.data } + + async moveDocument (input: IOutlineDocumentMoveInput) { + await this.apis.post('/documents.move', input) + } } diff --git a/src/modules/simple-mover/apis/interfaces.ts b/src/modules/simple-mover/apis/interfaces.ts index 5b9d511..de2584c 100644 --- a/src/modules/simple-mover/apis/interfaces.ts +++ b/src/modules/simple-mover/apis/interfaces.ts @@ -100,6 +100,13 @@ export interface IOutlineDocumentUpdateInput { title?: string } +export interface IOutlineDocumentMoveInput { + id: string + collectionId: string + index: number + parentDocumentId?: string +} + export interface IOutlineDocumentTreeItem { id: string title: string @@ -110,6 +117,7 @@ export interface IOutlineItem { id: string name: string treePath: string + index?: number // index of sub document under parent document } export interface IOutlineApis { @@ -122,4 +130,5 @@ export interface IOutlineApis { importDocumentByFile: (collectionId: string, filePath: string, parentDocumentId?: string) => Promise updateDocument: (document: IOutlineDocumentUpdateInput) => Promise getCollectionTree: (collectionId: string) => Promise + moveDocument: (input: IOutlineDocumentMoveInput) => Promise } diff --git a/src/modules/simple-mover/interfaces.ts b/src/modules/simple-mover/interfaces.ts index d83baec..4c11c17 100644 --- a/src/modules/simple-mover/interfaces.ts +++ b/src/modules/simple-mover/interfaces.ts @@ -48,6 +48,8 @@ export interface IMover { saveItems: () => Promise dispose: () => void + + getInnerPages: (item: ICodaItem) => ICodaPage[] } export type IStatusUpdateHandler = (itemStatus: IItemStatus) => void diff --git a/src/modules/simple-mover/transfers/OutlineImporter.ts b/src/modules/simple-mover/transfers/OutlineImporter.ts index 60dac10..63eb421 100644 --- a/src/modules/simple-mover/transfers/OutlineImporter.ts +++ b/src/modules/simple-mover/transfers/OutlineImporter.ts @@ -1,14 +1,12 @@ import { TaskEmitter, TaskPriority } from '@abxvn/tasks' import { CLIENT_IMPORT_OUTLINE, - ITEM_STATUS_ARCHIVING, ITEM_STATUS_CONFIRMING, ITEM_STATUS_DONE, ITEM_STATUS_ERROR, ITEM_STATUS_IMPORTING, ITEM_STATUS_LISTING, ITEM_STATUS_PENDING, - ITEM_STATUS_SKIPPED, ITEM_STATUS_VALIDATING, ITEM_STATUS_WAITING, } from '../events' @@ -50,6 +48,8 @@ export class OutlineImporter implements IImporter { private collectionId: string | undefined private documentTreeItems: IOutlineItem[] = [] private waitingExports: Array<{ id: string, outlineTreePath: string }> = [] + // coda id => corresponding outline index (for ordering) + private readonly codaOrderingIndexes: Record = {} constructor ( private readonly mover: IMover, @@ -136,8 +136,13 @@ export class OutlineImporter implements IImporter { private receiveDocumentTreeItems (items: IOutlineDocumentTreeItem[], parentPath = '/') { if (!items.length) return - items.forEach(item => { - this.documentTreeItems.push({ id: item.id, name: item.title, treePath: parentPath }) + items.forEach((item, idx) => { + this.documentTreeItems.push({ + id: item.id, + name: item.title, + treePath: parentPath, + index: idx, + }) this.receiveDocumentTreeItems(item.children, `${parentPath}${item.id}/`) }) } @@ -169,13 +174,11 @@ export class OutlineImporter implements IImporter { this.documentTreeItems.push(docTreeItem) } - const innerCodaTreePath = `${doc.treePath}${doc.id}/` const innerOutlineTreePath = `${outlineTreePath}${docTreeItem.id}/` - const innerPages = Object.values(this.mover.items).filter(item => ( - item.treePath === innerCodaTreePath - )) + const innerPages = this.mover.getInnerPages(doc) - innerPages.forEach(innerPage => { + innerPages.forEach((innerPage, index) => { + this.codaOrderingIndexes[innerPage.id] = index this.setStatus(innerPage.id, ITEM_STATUS_PENDING) this.tasks.add({ id: innerPage.id, @@ -220,13 +223,15 @@ export class OutlineImporter implements IImporter { if (!importedPage) throw Error('Failed to import page') - const innerCodaTreePath = `${page.treePath}${page.id}/` + const outlineId = importedPage.id const innerOutlineTreePath = `${outlineTreePath}${importedPage.id}/` - const innerPages = Object.values(this.mover.items).filter(item => ( - item.treePath === innerCodaTreePath - )) + const shouldArchiveOutdatedPage = docTreeItem && isPageOutOfSync + const orderingIndex = this.codaOrderingIndexes[page.id] + const shouldFixDocumentOrder = orderingIndex !== undefined && docTreeItem?.index !== orderingIndex + const innerPages = this.mover.getInnerPages(page) - innerPages.forEach(innerPage => { + innerPages.forEach((innerPage, index) => { + this.codaOrderingIndexes[innerPage.id] = index this.setStatus(innerPage.id, ITEM_STATUS_PENDING) this.tasks.add({ id: innerPage.id, @@ -234,16 +239,10 @@ export class OutlineImporter implements IImporter { }) }) - if (docTreeItem && isPageOutOfSync) { - this.setStatus(page.id, ITEM_STATUS_ARCHIVING, `Archiving outdated page ${page.name}`) - await this.archiveOutdatedPage(docTreeItem.id) - } + if (shouldArchiveOutdatedPage) this.queueArchivingOutdatedPage(page.id, docTreeItem.id) + if (shouldFixDocumentOrder) this.queueFixingDocumentOrder(page.id, outlineId, outlineParentId, orderingIndex) - if (isPageOutOfSync) { - this.setStatus(page.id, ITEM_STATUS_DONE, `Imported ${page.name}`) - } else { - this.setStatus(page.id, ITEM_STATUS_SKIPPED, `Skipped ${page.name}`) - } + this.setStatus(page.id, ITEM_STATUS_DONE, isPageOutOfSync ? `Imported ${page.name}` : `Skipped ${page.name}`) } async createAndPublishDocIfNotExists (doc: ICodaDoc) { @@ -254,6 +253,19 @@ export class OutlineImporter implements IImporter { }) } + private queueArchivingOutdatedPage (pageId: string, outlineId: string) { + this.tasks.add({ + id: `archive::${pageId}`, + priority: TaskPriority.IDLE, + execute: async () => { + await this.archiveOutdatedPage(outlineId) + this.setStatus(`archive::${pageId}`, ITEM_STATUS_DONE) + }, + }) + + this.tasks.next() + } + async archiveOutdatedPage (outlineId: string) { await this.apis.archiveDocument(outlineId) } @@ -279,6 +291,30 @@ export class OutlineImporter implements IImporter { return importedPage } + private queueFixingDocumentOrder ( + pageId: string, + documentId: string, + parentDocumentId: string, + orderingIndex: number, + ) { + this.tasks.add({ + id: `order::${pageId}`, + priority: TaskPriority.IDLE, + execute: async () => { + await this.apis.moveDocument({ + id: documentId, + collectionId: this.collectionId!, + parentDocumentId, + index: orderingIndex, + }) + + this.setStatus(`order::${pageId}`, ITEM_STATUS_DONE) + }, + }) + + this.tasks.next() + } + private setStatus (id: string, status: IStatus, message?: string) { const itemId = id === CLIENT_IMPORT_OUTLINE ? id : `import::${id}`