From 2be60e80ee756e3e6b0354b21335c280660dcb64 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Mon, 10 Nov 2025 16:08:14 +0100 Subject: [PATCH 01/15] fix(collection): handle prefix/include source mismatch --- src/module/src/runtime/utils/collection.ts | 44 +++- src/module/test/utils/collection.test.ts | 245 ++++++++++++++++++++- 2 files changed, 282 insertions(+), 7 deletions(-) diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts index 3f4b6ec9..563c8b70 100644 --- a/src/module/src/runtime/utils/collection.ts +++ b/src/module/src/runtime/utils/collection.ts @@ -4,7 +4,7 @@ import { pathMetaTransform } from './path-meta' import { minimatch } from 'minimatch' import { join, dirname, parse } from 'pathe' import type { DatabaseItem } from 'nuxt-studio/app' -import { withoutLeadingSlash } from 'ufo' +import { withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo' export const getCollectionByFilePath = (path: string, collections: Record): CollectionInfo | undefined => { let matchedSource: ResolvedCollectionSource | undefined @@ -62,13 +62,39 @@ export function getCollection(collectionName: string, collections: Record { - const include = minimatch(path, source.include, { dot: true }) - const exclude = source.exclude?.some(exclude => minimatch(path, exclude)) + const prefix = source.prefix + if (!prefix) { + return false + } + + if (!withLeadingSlash(prefixAndPath).startsWith(prefix)) { + return false + } + + let fsPath + const [fixPart] = source.include.includes('*') ? source.include.split('*') : ['', source.include] + const fixed = withoutTrailingSlash(withoutLeadingSlash(fixPart || '/')) + if (fixed === prefix) { + fsPath = prefixAndPath + } + else { + const path = prefixAndPath.replace(prefix, '') + fsPath = join(fixed, path) + } + + const include = minimatch(fsPath, source.include, { dot: true }) + const exclude = source.exclude?.some(exclude => minimatch(fsPath, exclude)) return include && !exclude }) @@ -81,9 +107,15 @@ export function generateFsPathFromId(id: string, source: CollectionInfo['source' const path = rest.join('/') const { fixed } = parseSourceBase(source) + const normalizedFixed = withoutTrailingSlash(fixed) - const pathWithoutFixed = path.substring(fixed.length) - return join(fixed, pathWithoutFixed) + // If path already starts with the fixed part, return as is + if (normalizedFixed && path.startsWith(normalizedFixed)) { + return path + } + + // Otherwise, join fixed part with path + return join(fixed, path) } export function getCollectionInfo(id: string, collections: Record) { diff --git a/src/module/test/utils/collection.test.ts b/src/module/test/utils/collection.test.ts index f7635605..5f82903c 100644 --- a/src/module/test/utils/collection.test.ts +++ b/src/module/test/utils/collection.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { getCollectionByFilePath, generateFsPathFromId } from '../../src/runtime/utils/collection' +import { getCollectionByFilePath, generateFsPathFromId, getCollectionSource, generateIdFromFsPath } from '../../src/runtime/utils/collection' import type { CollectionInfo, ResolvedCollectionSource } from '@nuxt/content' import { collections } from '../mocks/collection' @@ -78,4 +78,247 @@ describe('generateFsPathFromId', () => { const result = generateFsPathFromId(id, source) expect(result).toBe('en/1.getting-started/2.introduction.md') }) + + it('Custom pattern with root prefix and fixed part', () => { + const id = 'pages/about.md' + const source: ResolvedCollectionSource = { + prefix: '/', + include: 'pages/**/*', + cwd: '', + _resolved: true, + } + + const result = generateFsPathFromId(id, source) + expect(result).toBe('pages/about.md') + }) +}) + +describe('generateIdFromFsPath', () => { + it('should generate id for single file with no prefix', () => { + const path = 'index.md' + const result = generateIdFromFsPath(path, collections.landing!) + expect(result).toBe('landing/index.md') + }) + + it('should generate id for nested file with global pattern', () => { + const path = '1.getting-started/2.introduction.md' + const result = generateIdFromFsPath(path, collections.docs!) + expect(result).toBe('docs/1.getting-started/2.introduction.md') + }) + + it('should handle deeply nested paths', () => { + const path = '2.essentials/1.nested/3.components.md' + const result = generateIdFromFsPath(path, collections.docs!) + expect(result).toBe('docs/2.essentials/1.nested/3.components.md') + }) + + it('should handle collection with custom prefix', () => { + const customCollection: CollectionInfo = { + name: 'docs_en', + pascalName: 'DocsEn', + tableName: '_content_docs_en', + source: [ + { + _resolved: true, + prefix: '/en', + cwd: '', + include: 'en/**/*', + exclude: ['en/index.md'], + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/docs_en', + definitions: {}, + }, + tableDefinition: '', + } + + const path = 'en/1.getting-started/2.introduction.md' + const result = generateIdFromFsPath(path, customCollection) + expect(result).toBe('docs_en/en/1.getting-started/2.introduction.md') + }) + + it('should handle empty prefix correctly', () => { + const customCollection: CollectionInfo = { + name: 'pages', + pascalName: 'Pages', + tableName: '_content_pages', + source: [ + { + _resolved: true, + prefix: '', + cwd: '', + include: 'content/**/*.md', + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/pages', + definitions: {}, + }, + tableDefinition: '', + } + + const path = 'content/about.md' + const result = generateIdFromFsPath(path, customCollection) + expect(result).toBe('pages/about.md') + }) +}) + +describe('getCollectionSource', () => { + it('should return matching source for root docs collection', () => { + const id = 'docs/1.getting-started/2.introduction.md' + const source = getCollectionSource(id, collections.docs!) + + expect(source).toEqual(collections.docs!.source[0]) + }) + + it('should return matching source for root index file in landing collection', () => { + const id = 'landing/index.md' + const source = getCollectionSource(id, collections.landing!) + + expect(source).toEqual(collections.landing!.source[0]) + }) + + it('should handle root dot files correctly', () => { + const id = 'docs/.navigation.yml' + const source = getCollectionSource(id, collections.docs!) + + expect(source).toEqual(collections.docs!.source[0]) + }) + + it('should return undefined when path matches exclude pattern', () => { + const id = 'landing/index.md' + const source = getCollectionSource(id, collections.docs!) + + expect(source).toBeUndefined() + }) + + it('should return correct source when collection has prefix with dynamic include pattern', () => { + const collectionWithPrefix: CollectionInfo = { + name: 'blog', + pascalName: 'Blog', + tableName: '_content_blog', + source: [ + { + _resolved: true, + prefix: '/blog', + include: 'blog/**/*.md', + cwd: '', + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/blog', + definitions: {}, + }, + tableDefinition: '', + } + + const id = 'blog/blog/my-post.md' + const source = getCollectionSource(id, collectionWithPrefix) + expect(source).toEqual(collectionWithPrefix.source[0]) + }) + + it('should return correct source when collection has multiple sources', () => { + const multiSourceCollection: CollectionInfo = { + name: 'content', + pascalName: 'Content', + tableName: '_content', + source: [ + { + _resolved: true, + prefix: '/blog', + cwd: '', + include: 'blog/**/*.md', + }, + { + _resolved: true, + prefix: '/docs', + cwd: '', + include: 'docs/**/*.md', + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/content', + definitions: {}, + }, + tableDefinition: '', + } + + const blogId = 'content/blog/my-post.md' + const blogResult = getCollectionSource(blogId, multiSourceCollection) + expect(blogResult).toEqual(multiSourceCollection.source[0]) + + const docsId = 'content/docs/guide.md' + const docsResult = getCollectionSource(docsId, multiSourceCollection) + expect(docsResult).toEqual(multiSourceCollection.source[1]) + }) + + it('should return correct source when collection has root prefix with custom dynamic include pattern', () => { + const rootPrefixCollection: CollectionInfo = { + name: 'pages', + pascalName: 'Pages', + tableName: '_content_pages', + source: [ + { + _resolved: true, + prefix: '/', + include: 'pages/**/*.md', + cwd: '', + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/__SCHEMA__', + definitions: { }, + }, + tableDefinition: '', + } + + // {collection.name}/{source.prefix}/{name} + const id = 'pages/about.md' + const source = getCollectionSource(id, rootPrefixCollection) + expect(source).toEqual(rootPrefixCollection.source[0]) + }) + + it('should return correct source when collection has custom prefix with custom dynamic include pattern', () => { + const customPrefixCollection: CollectionInfo = { + name: 'edge_case', + pascalName: 'EdgeCase', + tableName: '_content_edge_case', + source: [ + { + _resolved: true, + prefix: '/prefix', + include: 'path/**/*.md', + cwd: '', + }, + ], + type: 'page', + fields: {}, + schema: { + $schema: 'http://json-schema.org/draft-07/schema#', + $ref: '#/definitions/edge_case', + definitions: {}, + }, + tableDefinition: '', + } + + const id = 'edge_case/prefix/file.md' + const source = getCollectionSource(id, customPrefixCollection) + expect(source).toEqual(customPrefixCollection.source[0]) + }) }) From cfc56f07d31c573a04a948d143e36f7c0c471040 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Mon, 10 Nov 2025 17:49:01 +0100 Subject: [PATCH 02/15] refactor(test): adapt test to new logic --- src/app/src/composables/useTree.ts | 2 - src/app/src/types/draft.ts | 1 - src/app/src/types/index.ts | 17 +- src/app/src/types/tree.ts | 1 - src/app/src/utils/tree.ts | 26 +- src/app/test/integration/actions.test.ts | 322 ++++++++++----------- src/app/test/mocks/draft.ts | 21 +- src/app/test/mocks/host.ts | 42 ++- src/app/test/mocks/tree.ts | 6 - src/app/test/unit/utils/context.test.ts | 18 +- src/app/test/unit/utils/draft.test.ts | 37 ++- src/app/test/unit/utils/tree.test.ts | 31 -- src/app/test/utils/index.ts | 16 +- src/module/src/runtime/utils/collection.ts | 1 - 14 files changed, 237 insertions(+), 304 deletions(-) diff --git a/src/app/src/composables/useTree.ts b/src/app/src/composables/useTree.ts index 8b374400..d3e5a54d 100644 --- a/src/app/src/composables/useTree.ts +++ b/src/app/src/composables/useTree.ts @@ -6,7 +6,6 @@ import { buildTree, findItemFromFsPath, findItemFromRoute, findParentFromFsPath, import type { RouteLocationNormalized } from 'vue-router' import { useHooks } from './useHooks' import { useStudioState } from './useStudioState' -import { TreeRootId } from '../types/tree' export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType) => { const hooks = useHooks() @@ -22,7 +21,6 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType fsPath: '/', children: tree.value, status: draftedTreeItems.length > 0 ? TreeStatus.Updated : null, - collections: [type === StudioFeature.Content ? TreeRootId.Content : TreeRootId.Media], prefix: null, } as TreeItem }) diff --git a/src/app/src/types/draft.ts b/src/app/src/types/draft.ts index b03c2b77..a7b00e5e 100644 --- a/src/app/src/types/draft.ts +++ b/src/app/src/types/draft.ts @@ -15,7 +15,6 @@ export interface ContentConflict { } export interface DraftItem { - id: string // nuxt/content id (with collection prefix) fsPath: string // file path in content directory status: DraftStatus // status diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts index 7439da58..f9fe116f 100644 --- a/src/app/src/types/index.ts +++ b/src/app/src/types/index.ts @@ -41,20 +41,19 @@ export interface StudioHost { } repository: Repository document: { - get: (id: string) => Promise - getFileSystemPath: (id: string) => string + get: (fsPath: string) => Promise list: () => Promise - upsert: (id: string, document: DatabaseItem) => Promise + upsert: (fsPath: string, document: DatabaseItem) => Promise create: (fsPath: string, content: string) => Promise - delete: (id: string) => Promise - detectActives: () => Array<{ id: string, title: string }> + delete: (fsPath: string) => Promise + detectActives: () => Array<{ fsPath: string, title: string }> } media: { - get: (id: string) => Promise - getFileSystemPath: (id: string) => string + get: (fsPath: string) => Promise + getFileSystemPath: (fsPath: string) => string list: () => Promise - upsert: (id: string, media: MediaItem) => Promise - delete: (id: string) => Promise + upsert: (fsPath: string, media: MediaItem) => Promise + delete: (fsPath: string) => Promise } user: { get: () => StudioUser diff --git a/src/app/src/types/tree.ts b/src/app/src/types/tree.ts index aa4a6fd8..e52e666b 100644 --- a/src/app/src/types/tree.ts +++ b/src/app/src/types/tree.ts @@ -16,7 +16,6 @@ export interface TreeItem { fsPath: string // can be used as id type: 'file' | 'directory' | 'root' prefix: number | null - collections: string[] status?: TreeStatus routePath?: string children?: TreeItem[] diff --git a/src/app/src/utils/tree.ts b/src/app/src/utils/tree.ts index d2ba7fca..1939317e 100644 --- a/src/app/src/utils/tree.ts +++ b/src/app/src/utils/tree.ts @@ -40,22 +40,23 @@ TreeItem[] { function addDeletedDraftItemsInDbItems(dbItems: ((BaseItem) & { fsPath: string })[], deletedItems: DraftItem[]) { dbItems = [...dbItems] for (const deletedItem of deletedItems) { + // TODO: createdDraftItem.original?.fsPath is null ATM // Files in both deleted and original created draft are considered as renamed // We don't want to add them to the tree and duplicate them - const renamedDraftItem = createdDraftItems.find(createdDraftItem => createdDraftItem.original?.id === deletedItem.id) + const renamedDraftItem = createdDraftItems.find(createdDraftItem => createdDraftItem.original?.fsPath === deletedItem.fsPath) if (renamedDraftItem) { continue } - const virtualDbItems: BaseItem & { fsPath: string } = { - id: deletedItem.id, - extension: getFileExtension(deletedItem.id), - stem: '', + const virtualDbItem: BaseItem & { fsPath: string } = { + id: 'N/A', fsPath: deletedItem.fsPath, + extension: getFileExtension(deletedItem.fsPath), + stem: '', path: deletedItem.original?.path, } - dbItems.push(virtualDbItems) + dbItems.push(virtualDbItem) } return dbItems @@ -80,7 +81,6 @@ TreeItem[] { fsPath: dbItem.fsPath, type: 'file', prefix, - collections: [dbItem.id.split('/')[0]], } if (dbItem.fsPath.endsWith('.gitkeep')) { @@ -91,7 +91,7 @@ TreeItem[] { fileItem.routePath = dbItem.path as string } - const draftFileItem = draftList?.find(draft => draft.id === dbItem.id) + const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!) } @@ -121,7 +121,6 @@ TreeItem[] { type: 'directory', children: [], prefix: dirPrefix, - collections: [dbItem.id.split('/')[0]], } directoryMap.set(dirFsPath, directory) @@ -130,12 +129,6 @@ TreeItem[] { directoryChildren.push(directory) } } - else { - const collection = dbItem.id.split('/')[0] - if (!directory.collections.includes(collection)) { - directory.collections.push(collection) - } - } directoryChildren = directory.children! } @@ -149,14 +142,13 @@ TreeItem[] { fsPath: dbItem.fsPath, type: 'file', prefix, - collections: [dbItem.id.split('/')[0]], } if (dbItem.fsPath.endsWith('.gitkeep')) { fileItem.hide = true } - const draftFileItem = draftList?.find(draft => draft.id === dbItem.id) + const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!) } diff --git a/src/app/test/integration/actions.test.ts b/src/app/test/integration/actions.test.ts index 5813870f..ab725851 100644 --- a/src/app/test/integration/actions.test.ts +++ b/src/app/test/integration/actions.test.ts @@ -1,11 +1,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { joinURL } from 'ufo' -import { DraftStatus, StudioItemActionId, TreeRootId, StudioFeature, type StudioHost, type TreeItem } from '../../src/types' -import { normalizeKey, generateUniqueDocumentId, generateUniqueMediaId, generateUniqueMediaName } from '../utils' -import { createMockHost, clearMockHost } from '../mocks/host' +import { DraftStatus, StudioItemActionId, StudioFeature, type StudioHost, type TreeItem, type DatabaseItem } from '../../src/types' +import { normalizeKey, generateUniqueDocumentFsPath, generateUniqueMediaFsPath } from '../utils' +import { createMockHost, clearMockHost, fsPathToId } from '../mocks/host' import { createMockGit } from '../mocks/git' import { createMockFile, createMockMedia, setupMediaMocks } from '../mocks/media' -import { createMockDocument } from '../mocks/document' import { createMockStorage } from '../mocks/composables' import type { useGit } from '../../src/composables/useGit' import { findItemFromFsPath } from '../../src/utils/tree' @@ -73,17 +72,15 @@ const cleanAndSetupContext = async (mockedHost: StudioHost, mockedGit: ReturnTyp describe('Document - Action Chains Integration Tests', () => { let filename: string - let documentId: string let documentFsPath: string - let collection: string + let documentId: string let context: Awaited> beforeEach(async () => { currentRouteName = 'content' - collection = 'docs' filename = 'document' - documentId = generateUniqueDocumentId(filename, collection) - documentFsPath = mockHost.document.getFileSystemPath(documentId) + documentFsPath = generateUniqueDocumentFsPath(filename) + documentId = fsPathToId(documentFsPath, 'document') context = await cleanAndSetupContext(mockHost, mockGit) }) @@ -98,9 +95,9 @@ describe('Document - Action Chains Integration Tests', () => { // Draft in Storage expect(mockStorageDraft.size).toEqual(1) - const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(storedDraft).toHaveProperty('status', DraftStatus.Created) - expect(storedDraft).toHaveProperty('id', documentId) + expect(storedDraft).toHaveProperty('fsPath', documentFsPath) expect(storedDraft.modified).toHaveProperty('id', documentId) expect(storedDraft.modified).toHaveProperty('body', { type: 'minimark', @@ -110,7 +107,7 @@ describe('Document - Action Chains Integration Tests', () => { // Draft in Memory expect(context.activeTree.value.draft.list.value).toHaveLength(1) - expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('fsPath', documentFsPath) expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', documentId) expect(context.activeTree.value.draft.list.value[0].original).toBeUndefined() @@ -145,32 +142,30 @@ describe('Document - Action Chains Integration Tests', () => { }) /* STEP 2: RENAME */ - const newId = generateUniqueDocumentId() - const newFsPath = mockHost.document.getFileSystemPath(newId) + const newFsPath = generateUniqueDocumentFsPath('document-renamed') await context.itemActionHandler[StudioItemActionId.RenameItem]({ newFsPath, item: { type: 'file', fsPath: documentFsPath, - collections: [collection], } as TreeItem, }) // Draft in Storage expect(mockStorageDraft.size).toEqual(1) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath) + expect(createdDraftStorage.modified).toHaveProperty('id', fsPathToId(newFsPath, 'document')) expect(createdDraftStorage.original).toBeUndefined() - expect(createdDraftStorage.modified).toHaveProperty('id', newId) // Draft in Memory const list = context.activeTree.value.draft.list.value expect(list).toHaveLength(1) expect(list[0].status).toEqual(DraftStatus.Created) - expect(list[0].id).toEqual(newId) + expect(list[0]).toHaveProperty('fsPath', newFsPath) + expect(list[0].modified).toHaveProperty('id', fsPathToId(newFsPath, 'document')) expect(list[0].original).toBeUndefined() - expect(list[0].modified).toHaveProperty('id', newId) // Tree expect(context.activeTree.value.root.value[0]).toHaveProperty('fsPath', newFsPath) @@ -192,19 +187,21 @@ describe('Document - Action Chains Integration Tests', () => { }) /* STEP 2: UPDATE */ - const updatedDocument = createMockDocument(documentId, { + const currentDraft = context.activeTree.value.draft.list.value[0] + const updatedDocument = { + ...currentDraft.modified!, body: { type: 'minimark', value: ['Updated content'], }, - }) - await context.activeTree.value.draft.update(documentId, updatedDocument) + } as DatabaseItem + await context.activeTree.value.draft.update(documentFsPath, updatedDocument as DatabaseItem) // Storage expect(mockStorageDraft.size).toEqual(1) - const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(storedDraft).toHaveProperty('status', DraftStatus.Created) - expect(storedDraft).toHaveProperty('id', documentId) + expect(storedDraft).toHaveProperty('fsPath', documentFsPath) expect(storedDraft.modified).toHaveProperty('id', documentId) expect(storedDraft.modified).toHaveProperty('body', updatedDocument.body) expect(storedDraft.original).toBeUndefined() @@ -212,9 +209,9 @@ describe('Document - Action Chains Integration Tests', () => { // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(1) expect(context.activeTree.value.draft.list.value[0].status).toEqual(DraftStatus.Created) - expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('fsPath', documentFsPath) + expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', documentId) expect(context.activeTree.value.draft.list.value[0].original).toBeUndefined() - expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', updatedDocument.id) // Tree expect(context.activeTree.value.root.value).toHaveLength(1) @@ -250,16 +247,16 @@ describe('Document - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const selectedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const selectedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(selectedDraft).toHaveProperty('status', DraftStatus.Pristine) - expect(selectedDraft).toHaveProperty('id', documentId) + expect(selectedDraft).toHaveProperty('fsPath', documentFsPath) expect(selectedDraft.modified).toHaveProperty('id', documentId) expect(selectedDraft.original).toHaveProperty('id', documentId) // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(1) expect(context.activeTree.value.draft.list.value[0].status).toEqual(DraftStatus.Pristine) - expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('fsPath', documentFsPath) expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', documentId) expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId) @@ -268,19 +265,21 @@ describe('Document - Action Chains Integration Tests', () => { expect(context.activeTree.value.root.value[0]).toHaveProperty('fsPath', documentFsPath) /* STEP 2: UPDATE */ - const updatedDocument = createMockDocument(documentId, { + const currentDraft = context.activeTree.value.draft.list.value[0] + const updatedDocument = { + ...currentDraft.modified!, body: { type: 'minimark', value: ['Updated content'], }, - }) - await context.activeTree.value.draft.update(documentId, updatedDocument) + } as DatabaseItem + await context.activeTree.value.draft.update(documentFsPath, updatedDocument) // Storage expect(mockStorageDraft.size).toEqual(1) - const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const storedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(storedDraft).toHaveProperty('status', DraftStatus.Updated) - expect(storedDraft).toHaveProperty('id', documentId) + expect(storedDraft).toHaveProperty('fsPath', documentFsPath) expect(storedDraft.modified).toHaveProperty('id', documentId) expect(storedDraft.modified).toHaveProperty('body', updatedDocument.body) expect(storedDraft.original).toHaveProperty('id', documentId) @@ -288,7 +287,9 @@ describe('Document - Action Chains Integration Tests', () => { // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(1) expect(context.activeTree.value.draft.list.value[0].status).toEqual(DraftStatus.Updated) - expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('fsPath', documentFsPath) + expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId) // Tree expect(context.activeTree.value.root.value).toHaveLength(1) @@ -300,16 +301,18 @@ describe('Document - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const revertedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const revertedDraft = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(revertedDraft).toHaveProperty('status', DraftStatus.Pristine) - expect(revertedDraft).toHaveProperty('id', documentId) + expect(revertedDraft).toHaveProperty('fsPath', documentFsPath) expect(revertedDraft.modified).toHaveProperty('id', documentId) expect(revertedDraft.original).toHaveProperty('id', documentId) // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(1) expect(context.activeTree.value.draft.list.value[0].status).toEqual(DraftStatus.Pristine) - expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0]).toHaveProperty('fsPath', documentFsPath) + expect(context.activeTree.value.draft.list.value[0].modified).toHaveProperty('id', documentId) + expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId) // Tree expect(context.activeTree.value.root.value).toHaveLength(1) @@ -334,22 +337,23 @@ describe('Document - Action Chains Integration Tests', () => { await context.activeTree.value.selectItemByFsPath(documentFsPath) /* STEP 2: UPDATE */ - const updatedDocument = createMockDocument(documentId, { + const currentDraft = context.activeTree.value.draft.list.value[0] + const updatedDocument = { + ...currentDraft.modified!, body: { type: 'minimark', value: ['Updated content'], }, - }) - await context.activeTree.value.draft.update(documentId, updatedDocument) + } as DatabaseItem + await context.activeTree.value.draft.update(documentFsPath, updatedDocument) /* STEP 3: RENAME */ - const newId = generateUniqueDocumentId() - const newFsPath = mockHost.document.getFileSystemPath(newId) + const newFsPath = generateUniqueDocumentFsPath('document-renamed') + const newId = fsPathToId(newFsPath, 'document') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: documentFsPath, - collections: [collection], } as TreeItem, newFsPath, }) @@ -358,17 +362,17 @@ describe('Document - Action Chains Integration Tests', () => { expect(mockStorageDraft.size).toEqual(2) // Created renamed draft - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath) expect(createdDraftStorage.original).toHaveProperty('id', documentId) expect(createdDraftStorage.modified).toHaveProperty('id', newId) expect(createdDraftStorage.modified).toHaveProperty('body', updatedDocument.body) // Deleted original draft - const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', documentId) + expect(deletedDraftStorage).toHaveProperty('fsPath', documentFsPath) expect(deletedDraftStorage.original).toHaveProperty('id', documentId) expect(deletedDraftStorage.modified).toBeUndefined() @@ -377,12 +381,12 @@ describe('Document - Action Chains Integration Tests', () => { expect(list).toHaveLength(2) expect(list[0].status).toEqual(DraftStatus.Deleted) - expect(list[0].id).toEqual(documentId) + expect(list[0]).toHaveProperty('fsPath', documentFsPath) expect(list[0].original).toHaveProperty('id', documentId) expect(list[0].modified).toBeUndefined() expect(list[1].status).toEqual(DraftStatus.Created) - expect(list[1].id).toEqual(newId) + expect(list[1]).toHaveProperty('fsPath', newFsPath) expect(list[1].original).toHaveProperty('id', documentId) expect(list[1].modified).toHaveProperty('id', newId) expect(list[1].modified).toHaveProperty('body', updatedDocument.body) @@ -410,13 +414,12 @@ describe('Document - Action Chains Integration Tests', () => { await context.activeTree.value.selectItemByFsPath(documentFsPath) /* STEP 2: RENAME */ - const newId = generateUniqueDocumentId() - const newFsPath = mockHost.document.getFileSystemPath(newId) + const newFsPath = generateUniqueDocumentFsPath('document-renamed') + const newId = fsPathToId(newFsPath, 'document') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: documentFsPath, - collections: [collection], } as TreeItem, newFsPath, }) @@ -425,16 +428,16 @@ describe('Document - Action Chains Integration Tests', () => { expect(mockStorageDraft.size).toEqual(2) // Created renamed draft - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath) expect(createdDraftStorage.original).toHaveProperty('id', documentId) expect(createdDraftStorage.modified).toHaveProperty('id', newId) // Deleted original draft - let deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + let deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', documentId) + expect(deletedDraftStorage).toHaveProperty('fsPath', documentFsPath) expect(deletedDraftStorage.original).toHaveProperty('id', documentId) expect(deletedDraftStorage.modified).toBeUndefined() @@ -442,15 +445,15 @@ describe('Document - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(2) // Deleted original draft - let deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === documentId) + let deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === documentFsPath) expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) expect(deletedDraftMemory!.original).toHaveProperty('id', documentId) expect(deletedDraftMemory!.modified).toBeUndefined() // Created renamed draft - const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === newId) + const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === newFsPath) expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', newId) + expect(createdDraftMemory).toHaveProperty('fsPath', newFsPath) expect(createdDraftMemory!.original).toHaveProperty('id', documentId) expect(createdDraftMemory!.modified).toHaveProperty('id', newId) @@ -459,44 +462,46 @@ describe('Document - Action Chains Integration Tests', () => { expect(context.activeTree.value.root.value[0]).toHaveProperty('fsPath', newFsPath) /* STEP 3: UPDATE */ - const updatedDocument = createMockDocument(newId, { + const currentDraft = context.activeTree.value.draft.list.value.find(item => item.fsPath === newFsPath)! + const updatedDocument = { + ...currentDraft.modified!, body: { type: 'minimark', value: ['Updated content'], }, - }) - await context.activeTree.value.draft.update(newId, updatedDocument) + } as DatabaseItem + await context.activeTree.value.draft.update(newFsPath, updatedDocument) // Storage expect(mockStorageDraft.size).toEqual(2) // Updated renamed draft - const updatedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const updatedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(updatedDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(updatedDraftStorage).toHaveProperty('id', newId) + expect(updatedDraftStorage).toHaveProperty('fsPath', newFsPath) expect(updatedDraftStorage.original).toHaveProperty('id', documentId) expect(updatedDraftStorage.modified).toHaveProperty('id', newId) expect(updatedDraftStorage.modified).toHaveProperty('body', updatedDocument.body) // Deleted original draft - deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', documentId) + expect(deletedDraftStorage).toHaveProperty('fsPath', documentFsPath) expect(deletedDraftStorage.original).toHaveProperty('id', documentId) // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(2) // Deleted original draft - deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === documentId) + deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === documentFsPath) expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) expect(deletedDraftMemory!.original).toHaveProperty('id', documentId) expect(deletedDraftMemory!.modified).toBeUndefined() // Renamed original draft - const updatedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === newId)! + const updatedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === newFsPath)! expect(updatedDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(updatedDraftMemory).toHaveProperty('id', newId) + expect(updatedDraftMemory).toHaveProperty('fsPath', newFsPath) expect(updatedDraftMemory!.original).toHaveProperty('id', documentId) expect(updatedDraftMemory!.modified).toHaveProperty('id', newId) expect(updatedDraftMemory!.modified).toHaveProperty('body', updatedDocument.body) @@ -525,13 +530,12 @@ describe('Document - Action Chains Integration Tests', () => { await context.activeTree.value.selectItemByFsPath(documentFsPath) /* STEP 2: RENAME */ - const newId = generateUniqueDocumentId() - const newFsPath = mockHost.document.getFileSystemPath(newId) + const newFsPath = generateUniqueDocumentFsPath('document-renamed') + const _newId = fsPathToId(newFsPath, 'document') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: documentFsPath, - collections: [collection], } as TreeItem, newFsPath, }) @@ -544,9 +548,9 @@ describe('Document - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const openedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + const openedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(openedDraftStorage).toHaveProperty('status', DraftStatus.Pristine) - expect(openedDraftStorage).toHaveProperty('id', documentId) + expect(openedDraftStorage).toHaveProperty('fsPath', documentFsPath) expect(openedDraftStorage.modified).toHaveProperty('id', documentId) expect(openedDraftStorage.original).toHaveProperty('id', documentId) @@ -554,7 +558,7 @@ describe('Document - Action Chains Integration Tests', () => { const list = context.activeTree.value.draft.list.value expect(list).toHaveLength(1) expect(list[0]).toHaveProperty('status', DraftStatus.Pristine) - expect(list[0]).toHaveProperty('id', documentId) + expect(list[0]).toHaveProperty('fsPath', documentFsPath) expect(list[0].modified).toHaveProperty('id', documentId) expect(list[0].original).toHaveProperty('id', documentId) @@ -581,25 +585,23 @@ describe('Document - Action Chains Integration Tests', () => { await context.activeTree.value.selectItemByFsPath(documentFsPath) /* STEP 2: RENAME */ - const newId = generateUniqueDocumentId() - const newFsPath = mockHost.document.getFileSystemPath(newId) + const newFsPath = generateUniqueDocumentFsPath('document-renamed') + const _newId = fsPathToId(newFsPath, 'document') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: documentFsPath, - collections: [collection], } as TreeItem, newFsPath, }) /* STEP 3: RENAME */ - const newId2 = generateUniqueDocumentId() - const newFsPath2 = mockHost.document.getFileSystemPath(newId2) + const newFsPath2 = generateUniqueDocumentFsPath('document-renamed-again') + const newId2 = fsPathToId(newFsPath2, 'document') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: newFsPath, - collections: [collection], } as TreeItem, newFsPath: newFsPath2, }) @@ -607,34 +609,34 @@ describe('Document - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(2) - // Created renamed draft (newId2) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId2))!) + // Created renamed draft (newFsPath2) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath2))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId2) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath2) expect(createdDraftStorage.original).toHaveProperty('id', documentId) expect(createdDraftStorage.modified).toHaveProperty('id', newId2) - // Deleted original draft (documentId) - const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentId))!) + // Deleted original draft (documentFsPath) + const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(documentFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', documentId) + expect(deletedDraftStorage).toHaveProperty('fsPath', documentFsPath) expect(deletedDraftStorage.original).toHaveProperty('id', documentId) expect(deletedDraftStorage.modified).toBeUndefined() // Memory expect(context.activeTree.value.draft.list.value).toHaveLength(2) - // Created renamed draft (newId2) - const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === newId2)! + // Created renamed draft (newFsPath2) + const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === newFsPath2)! expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', newId2) + expect(createdDraftMemory).toHaveProperty('fsPath', newFsPath2) expect(createdDraftMemory.original).toHaveProperty('id', documentId) expect(createdDraftMemory.modified).toHaveProperty('id', newId2) - // Deleted original draft (documentId) - const deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === documentId)! + // Deleted original draft (documentFsPath) + const deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === documentFsPath)! expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftMemory).toHaveProperty('id', documentId) + expect(deletedDraftMemory).toHaveProperty('fsPath', documentFsPath) expect(deletedDraftMemory.original).toHaveProperty('id', documentId) expect(deletedDraftMemory.modified).toBeUndefined() @@ -654,17 +656,17 @@ describe('Document - Action Chains Integration Tests', () => { describe('Media - Action Chains Integration Tests', () => { let context: Awaited> let mediaName: string - let mediaId: string let mediaFsPath: string - const parentPath = '/' + let mediaId: string + const parentPath = '' beforeEach(async () => { setupMediaMocks() currentRouteName = 'media' - mediaName = generateUniqueMediaName() - mediaId = joinURL(TreeRootId.Media, mediaName) - mediaFsPath = mockHost.media.getFileSystemPath(mediaId) + mediaFsPath = generateUniqueMediaFsPath('media', 'png') + mediaId = fsPathToId(mediaFsPath, 'media') + mediaName = mediaFsPath.split('/').pop()! // Extract filename from fsPath context = await cleanAndSetupContext(mockHost, mockGit) }) @@ -680,9 +682,9 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', mediaId) + expect(createdDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(createdDraftStorage.original).toBeUndefined() expect(createdDraftStorage.modified).toHaveProperty('id', mediaId) @@ -690,7 +692,7 @@ describe('Media - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(1) const createdDraftMemory = context.activeTree.value.draft.list.value[0] expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', mediaId) + expect(createdDraftMemory).toHaveProperty('fsPath', mediaFsPath) expect(createdDraftMemory.original).toBeUndefined() expect(createdDraftMemory.modified).toHaveProperty('id', mediaId) @@ -728,22 +730,21 @@ describe('Media - Action Chains Integration Tests', () => { }) /* STEP 2: RENAME */ - const newId = generateUniqueMediaId() - const newFsPath = mockHost.media.getFileSystemPath(newId) + const newFsPath = generateUniqueMediaFsPath('media-renamed', 'png') + const newId = fsPathToId(newFsPath, 'media') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: mediaFsPath, - collections: [TreeRootId.Media], } as TreeItem, newFsPath, }) // Storage expect(mockStorageDraft.size).toEqual(1) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath) expect(createdDraftStorage.original).toBeUndefined() expect(createdDraftStorage.modified).toHaveProperty('id', newId) @@ -751,7 +752,7 @@ describe('Media - Action Chains Integration Tests', () => { const list = context.activeTree.value.draft.list.value expect(list).toHaveLength(1) expect(list[0].status).toEqual(DraftStatus.Created) - expect(list[0].id).toEqual(newId) + expect(list[0]).toHaveProperty('fsPath', newFsPath) expect(list[0].original).toBeUndefined() expect(list[0].modified).toHaveProperty('id', newId) @@ -770,7 +771,7 @@ describe('Media - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create media in db and load tree - await mockHost.media.upsert(mediaId, createMockMedia(mediaId)) + await mockHost.media.upsert(mediaFsPath, createMockMedia(mediaId)) await context.activeTree.value.draft.load() /* STEP 1: SELECT */ @@ -778,9 +779,9 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Pristine) - expect(createdDraftStorage).toHaveProperty('id', mediaId) + expect(createdDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(createdDraftStorage.original).toHaveProperty('id', mediaId) expect(createdDraftStorage.modified).toHaveProperty('id', mediaId) @@ -788,7 +789,7 @@ describe('Media - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(1) const createdDraftMemory = context.activeTree.value.draft.list.value[0] expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Pristine) - expect(createdDraftMemory).toHaveProperty('id', mediaId) + expect(createdDraftMemory).toHaveProperty('fsPath', mediaFsPath) expect(createdDraftMemory.original).toHaveProperty('id', mediaId) expect(createdDraftMemory.modified).toHaveProperty('id', mediaId) @@ -802,9 +803,9 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', mediaId) + expect(deletedDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftStorage.modified).toBeUndefined() expect(deletedDraftStorage.original).toHaveProperty('id', mediaId) @@ -812,7 +813,7 @@ describe('Media - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(1) const deletedDraftMemory = context.activeTree.value.draft.list.value[0] expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftMemory).toHaveProperty('id', mediaId) + expect(deletedDraftMemory).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftMemory.modified).toBeUndefined() expect(deletedDraftMemory.original).toHaveProperty('id', mediaId) @@ -826,18 +827,18 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const revertedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const revertedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(revertedDraftStorage).toHaveProperty('status', DraftStatus.Pristine) - expect(revertedDraftStorage).toHaveProperty('id', mediaId) - expect(revertedDraftStorage.modified).toBeDefined() + expect(revertedDraftStorage).toHaveProperty('fsPath', mediaFsPath) + expect(revertedDraftStorage.modified).toHaveProperty('id', mediaId) expect(revertedDraftStorage.original).toHaveProperty('id', mediaId) // Memory const list = context.activeTree.value.draft.list.value expect(list).toHaveLength(1) expect(list[0]).toHaveProperty('status', DraftStatus.Pristine) - expect(list[0]).toHaveProperty('id', mediaId) - expect(list[0].modified).toBeDefined() + expect(list[0]).toHaveProperty('fsPath', mediaFsPath) + expect(list[0].modified).toHaveProperty('id', mediaId) expect(list[0].original).toHaveProperty('id', mediaId) // Tree @@ -856,19 +857,18 @@ describe('Media - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create media in db and load tree - await mockHost.media.upsert(mediaId, { id: mediaId, stem: mediaName.split('.')[0], extension: mediaName.split('.')[1] }) + await mockHost.media.upsert(mediaFsPath, { id: mediaId, stem: mediaName.split('.')[0], extension: mediaName.split('.')[1] }) await context.activeTree.value.draft.load() /* STEP 1: RENAME */ await context.activeTree.value.selectItemByFsPath(mediaFsPath) - const newId = generateUniqueMediaId() - const newFsPath = mockHost.media.getFileSystemPath(newId) + const newFsPath = generateUniqueMediaFsPath('media-renamed', 'png') + const newId = fsPathToId(newFsPath, 'media') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: mediaFsPath, - collections: [TreeRootId.Media], } as TreeItem, newFsPath, }) @@ -877,16 +877,16 @@ describe('Media - Action Chains Integration Tests', () => { expect(mockStorageDraft.size).toEqual(2) // Created renamed draft - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath) expect(createdDraftStorage.original).toHaveProperty('id', mediaId) expect(createdDraftStorage.modified).toHaveProperty('id', newId) // Deleted original draft - const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', mediaId) + expect(deletedDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftStorage.modified).toBeUndefined() expect(deletedDraftStorage.original).toHaveProperty('id', mediaId) @@ -894,16 +894,16 @@ describe('Media - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(2) // Created renamed draft - const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === newId)! + const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === newFsPath)! expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', newId) + expect(createdDraftMemory).toHaveProperty('fsPath', newFsPath) expect(createdDraftMemory.modified).toHaveProperty('id', newId) expect(createdDraftMemory.original).toHaveProperty('id', mediaId) // Deleted original draft - const deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === mediaId)! + const deletedDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === mediaFsPath)! expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftMemory).toHaveProperty('id', mediaId) + expect(deletedDraftMemory).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftMemory.modified).toBeUndefined() expect(deletedDraftMemory.original).toHaveProperty('id', mediaId) @@ -918,9 +918,9 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const revertedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const revertedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(revertedDraftStorage).toHaveProperty('status', DraftStatus.Pristine) - expect(revertedDraftStorage).toHaveProperty('id', mediaId) + expect(revertedDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(revertedDraftStorage.modified).toHaveProperty('id', mediaId) expect(revertedDraftStorage.original).toHaveProperty('id', mediaId) @@ -928,7 +928,7 @@ describe('Media - Action Chains Integration Tests', () => { const list = context.activeTree.value.draft.list.value expect(list).toHaveLength(1) expect(list[0]).toHaveProperty('status', DraftStatus.Pristine) - expect(list[0]).toHaveProperty('id', mediaId) + expect(list[0]).toHaveProperty('fsPath', mediaFsPath) expect(list[0].modified).toHaveProperty('id', mediaId) expect(list[0].original).toHaveProperty('id', mediaId) @@ -948,31 +948,29 @@ describe('Media - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create media in db and load tree - await mockHost.media.upsert(mediaId, { id: mediaId, stem: mediaName.split('.')[0], extension: mediaName.split('.')[1] }) + await mockHost.media.upsert(mediaFsPath, { id: mediaId, stem: mediaName.split('.')[0], extension: mediaName.split('.')[1] }) await context.activeTree.value.draft.load() /* STEP 1: RENAME */ await context.activeTree.value.selectItemByFsPath(mediaFsPath) - const newId = generateUniqueMediaId() - const newFsPath = mockHost.media.getFileSystemPath(newId) + const newFsPath = generateUniqueMediaFsPath('media-renamed', 'png') + const _newId = fsPathToId(newFsPath, 'media') await context.itemActionHandler[StudioItemActionId.RenameItem]({ newFsPath, item: { type: 'file', fsPath: mediaFsPath, - collections: [TreeRootId.Media], } as TreeItem, }) /* STEP 2: RENAME */ - const newId2 = generateUniqueMediaId() - const newFsPath2 = mockHost.media.getFileSystemPath(newId2) + const newFsPath2 = generateUniqueMediaFsPath('media-renamed-again', 'png') + const newId2 = fsPathToId(newFsPath2, 'media') await context.itemActionHandler[StudioItemActionId.RenameItem]({ item: { type: 'file', fsPath: newFsPath, - collections: [TreeRootId.Media], } as TreeItem, newFsPath: newFsPath2, }) @@ -981,16 +979,16 @@ describe('Media - Action Chains Integration Tests', () => { expect(mockStorageDraft.size).toEqual(2) // Created renamed draft - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newId2))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(newFsPath2))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', newId2) + expect(createdDraftStorage).toHaveProperty('fsPath', newFsPath2) expect(createdDraftStorage.original).toHaveProperty('id', mediaId) expect(createdDraftStorage.modified).toHaveProperty('id', newId2) // Deleted original draft - const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaId))!) + const deletedDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(mediaFsPath))!) expect(deletedDraftStorage).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftStorage).toHaveProperty('id', mediaId) + expect(deletedDraftStorage).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftStorage.modified).toBeUndefined() expect(deletedDraftStorage.original).toHaveProperty('id', mediaId) @@ -999,16 +997,16 @@ describe('Media - Action Chains Integration Tests', () => { expect(list).toHaveLength(2) // Created renamed draft - const createdDraftMemory = list.find(item => item.id === newId2)! + const createdDraftMemory = list.find(item => item.fsPath === newFsPath2)! expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', newId2) + expect(createdDraftMemory).toHaveProperty('fsPath', newFsPath2) expect(createdDraftMemory.modified).toHaveProperty('id', newId2) expect(createdDraftMemory.original).toHaveProperty('id', mediaId) // Deleted original draft - const deletedDraftMemory = list.find(item => item.id === mediaId)! + const deletedDraftMemory = list.find(item => item.fsPath === mediaFsPath)! expect(deletedDraftMemory).toHaveProperty('status', DraftStatus.Deleted) - expect(deletedDraftMemory).toHaveProperty('id', mediaId) + expect(deletedDraftMemory).toHaveProperty('fsPath', mediaFsPath) expect(deletedDraftMemory.original).toHaveProperty('id', mediaId) expect(deletedDraftMemory.modified).toBeUndefined() @@ -1028,8 +1026,8 @@ describe('Media - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') const folderName = 'media-folder' const folderPath = `/${folderName}` - const gitkeepId = joinURL(TreeRootId.Media, folderPath, '.gitkeep') - const gitkeepFsPath = mockHost.media.getFileSystemPath(gitkeepId) + const gitkeepFsPath = joinURL(folderPath, '.gitkeep') + const gitkeepId = fsPathToId(gitkeepFsPath, 'media') /* STEP 1: CREATE FOLDER */ await context.itemActionHandler[StudioItemActionId.CreateMediaFolder]({ @@ -1038,9 +1036,9 @@ describe('Media - Action Chains Integration Tests', () => { // Storage expect(mockStorageDraft.size).toEqual(1) - const gitkeepDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(gitkeepId))!) + const gitkeepDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(gitkeepFsPath))!) expect(gitkeepDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(gitkeepDraftStorage).toHaveProperty('id', gitkeepId) + expect(gitkeepDraftStorage).toHaveProperty('fsPath', gitkeepFsPath) expect(gitkeepDraftStorage.modified).toHaveProperty('id', gitkeepId) expect(gitkeepDraftStorage.original).toBeUndefined() @@ -1048,7 +1046,7 @@ describe('Media - Action Chains Integration Tests', () => { expect(context.activeTree.value.draft.list.value).toHaveLength(1) const gitkeepDraftMemory = context.activeTree.value.draft.list.value[0] expect(gitkeepDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(gitkeepDraftMemory).toHaveProperty('id', gitkeepId) + expect(gitkeepDraftMemory).toHaveProperty('fsPath', gitkeepFsPath) expect(gitkeepDraftMemory.modified).toHaveProperty('id', gitkeepId) expect(gitkeepDraftMemory.original).toBeUndefined() @@ -1063,8 +1061,8 @@ describe('Media - Action Chains Integration Tests', () => { /* STEP 2: UPLOAD MEDIA IN FOLDER */ const file = createMockFile(mediaName) - const uploadedMediaId = joinURL(TreeRootId.Media, folderPath, mediaName) - const uploadedMediaFsPath = mockHost.media.getFileSystemPath(uploadedMediaId) + const uploadedMediaFsPath = joinURL(folderPath, mediaName) + const uploadedMediaId = fsPathToId(uploadedMediaFsPath, 'media') await context.itemActionHandler[StudioItemActionId.UploadMedia]({ parentFsPath: folderPath, files: [file], @@ -1072,17 +1070,17 @@ describe('Media - Action Chains Integration Tests', () => { // Storage - .gitkeep has been removed expect(mockStorageDraft.size).toEqual(1) - const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(uploadedMediaId))!) + const createdDraftStorage = JSON.parse(mockStorageDraft.get(normalizeKey(uploadedMediaFsPath))!) expect(createdDraftStorage).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftStorage).toHaveProperty('id', uploadedMediaId) + expect(createdDraftStorage).toHaveProperty('fsPath', uploadedMediaFsPath) expect(createdDraftStorage.original).toBeUndefined() expect(createdDraftStorage.modified).toHaveProperty('id', uploadedMediaId) // Memory - .gitkeep has been removed expect(context.activeTree.value.draft.list.value).toHaveLength(1) - const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.id === uploadedMediaId)! + const createdDraftMemory = context.activeTree.value.draft.list.value.find(item => item.fsPath === uploadedMediaFsPath)! expect(createdDraftMemory).toHaveProperty('status', DraftStatus.Created) - expect(createdDraftMemory).toHaveProperty('id', uploadedMediaId) + expect(createdDraftMemory).toHaveProperty('fsPath', uploadedMediaFsPath) expect(createdDraftMemory.modified).toHaveProperty('id', uploadedMediaId) expect(createdDraftMemory.original).toBeUndefined() diff --git a/src/app/test/mocks/draft.ts b/src/app/test/mocks/draft.ts index 820b3c0e..ffb7dd62 100644 --- a/src/app/test/mocks/draft.ts +++ b/src/app/test/mocks/draft.ts @@ -4,8 +4,7 @@ import { DraftStatus } from '../../src/types/draft' export const draftItemsList: DraftItem[] = [ // Root files { - id: 'landing/index.md', - fsPath: '/index.md', + fsPath: 'index.md', status: DraftStatus.Updated, original: { id: 'landing/index.md', @@ -29,8 +28,7 @@ export const draftItemsList: DraftItem[] = [ }, }, { - id: 'docs/root-file.md', - fsPath: '/root-file.md', + fsPath: 'root-file.md', status: DraftStatus.Created, original: { id: 'docs/root-file.md', @@ -56,8 +54,7 @@ export const draftItemsList: DraftItem[] = [ // Files in getting-started directory { - id: 'docs/1.getting-started/2.introduction.md', - fsPath: '/1.getting-started/2.introduction.md', + fsPath: '1.getting-started/2.introduction.md', status: DraftStatus.Updated, original: { id: 'docs/1.getting-started/2.introduction.md', @@ -81,8 +78,7 @@ export const draftItemsList: DraftItem[] = [ }, }, { - id: 'docs/1.getting-started/3.installation.md', - fsPath: '/1.getting-started/3.installation.md', + fsPath: '1.getting-started/3.installation.md', status: DraftStatus.Created, original: { id: 'docs/1.getting-started/3.installation.md', @@ -106,8 +102,7 @@ export const draftItemsList: DraftItem[] = [ }, }, { - id: 'docs/1.getting-started/4.configuration.md', - fsPath: '/1.getting-started/4.configuration.md', + fsPath: '1.getting-started/4.configuration.md', status: DraftStatus.Deleted, modified: undefined, original: { @@ -124,8 +119,7 @@ export const draftItemsList: DraftItem[] = [ // Files in advanced subdirectory { - id: 'docs/1.getting-started/1.advanced/1.studio.md', - fsPath: '/1.getting-started/1.advanced/1.studio.md', + fsPath: '1.getting-started/1.advanced/1.studio.md', status: DraftStatus.Updated, original: { id: 'docs/1.getting-started/1.advanced/1.studio.md', @@ -149,8 +143,7 @@ export const draftItemsList: DraftItem[] = [ }, }, { - id: 'docs/1.getting-started/1.advanced/2.deployment.md', - fsPath: '/1.getting-started/1.advanced/2.deployment.md', + fsPath: '1.getting-started/1.advanced/2.deployment.md', status: DraftStatus.Created, original: { id: 'docs/1.getting-started/1.advanced/2.deployment.md', diff --git a/src/app/test/mocks/host.ts b/src/app/test/mocks/host.ts index a44956e9..66b64a8b 100644 --- a/src/app/test/mocks/host.ts +++ b/src/app/test/mocks/host.ts @@ -5,9 +5,18 @@ import { createMockMedia } from './media' import { joinURL } from 'ufo' import type { MediaItem } from '../../src/types/media' -// Simple implementation that mimics the real getFileSystemPath logic -const getFileSystemPath = (id: string) => { - return `${id.split('/').slice(1).join('/')}` +// Helper to convert fsPath to id (simulates module's internal mapping) +export const fsPathToId = (fsPath: string, type: 'document' | 'media') => { + if (type === 'media') { + return joinURL(TreeRootId.Media, fsPath) + } + // For documents, prefix with a collection name + return joinURL('docs', fsPath) +} + +// Helper to convert id back to fsPath (simulates module's internal mapping) +const idToFsPath = (id: string) => { + return id.split('/').slice(1).join('/') } const documentDb = new Map() @@ -15,7 +24,8 @@ const mediaDb = new Map() export const createMockHost = (): StudioHost => ({ document: { - get: vi.fn().mockImplementation(async (id: string) => { + get: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'document') if (documentDb.has(id)) { return documentDb.get(id) } @@ -24,8 +34,7 @@ export const createMockHost = (): StudioHost => ({ return document }), create: vi.fn().mockImplementation(async (fsPath: string, content: string) => { - // Add dummy collection prefix - const id = joinURL('docs', fsPath) + const id = fsPathToId(fsPath, 'document') const document = createMockDocument(id, { body: { type: 'minimark', @@ -35,19 +44,22 @@ export const createMockHost = (): StudioHost => ({ documentDb.set(id, document) return document }), - upsert: vi.fn().mockImplementation(async (id: string, document: DatabaseItem) => { + upsert: vi.fn().mockImplementation(async (fsPath: string, document: DatabaseItem) => { + const id = fsPathToId(fsPath, 'document') documentDb.set(id, document) }), - delete: vi.fn().mockImplementation(async (id: string) => { + delete: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'document') documentDb.delete(id) }), list: vi.fn().mockImplementation(async () => { return Array.from(documentDb.values()) }), - getFileSystemPath, + getFileSystemPath: vi.fn().mockImplementation(idToFsPath), }, media: { - get: vi.fn().mockImplementation(async (id: string) => { + get: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'media') if (mediaDb.has(id)) { return mediaDb.get(id) } @@ -56,18 +68,20 @@ export const createMockHost = (): StudioHost => ({ return media }), create: vi.fn().mockImplementation(async (fsPath: string, _routePath: string, _content: string) => { - const id = joinURL(TreeRootId.Media, fsPath) + const id = fsPathToId(fsPath, 'media') const media = createMockMedia(id) mediaDb.set(id, media) return media }), - upsert: vi.fn().mockImplementation(async (id: string, media: MediaItem) => { + upsert: vi.fn().mockImplementation(async (fsPath: string, media: MediaItem) => { + const id = fsPathToId(fsPath, 'media') mediaDb.set(id, media) }), - delete: vi.fn().mockImplementation(async (id: string) => { + delete: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'media') mediaDb.delete(id) }), - getFileSystemPath, + getFileSystemPath: vi.fn().mockImplementation(idToFsPath), list: vi.fn().mockImplementation(async () => { return Array.from(mediaDb.values()) }), diff --git a/src/app/test/mocks/tree.ts b/src/app/test/mocks/tree.ts index 29c8130f..1b305e0a 100644 --- a/src/app/test/mocks/tree.ts +++ b/src/app/test/mocks/tree.ts @@ -6,14 +6,12 @@ export const tree: TreeItem[] = [ fsPath: 'index.md', type: 'file', routePath: '/', - collections: ['landing'], prefix: null, }, { name: 'getting-started', fsPath: '1.getting-started', type: 'directory', - collections: ['docs'], prefix: 1, children: [ { @@ -21,7 +19,6 @@ export const tree: TreeItem[] = [ fsPath: '1.getting-started/2.introduction.md', type: 'file', routePath: '/getting-started/introduction', - collections: ['docs'], prefix: 2, }, { @@ -29,14 +26,12 @@ export const tree: TreeItem[] = [ fsPath: '1.getting-started/3.installation.md', type: 'file', routePath: '/getting-started/installation', - collections: ['docs'], prefix: 3, }, { name: 'advanced', fsPath: '1.getting-started/1.advanced', type: 'directory', - collections: ['docs'], prefix: 1, children: [ { @@ -44,7 +39,6 @@ export const tree: TreeItem[] = [ fsPath: '1.getting-started/1.advanced/1.studio.md', type: 'file', routePath: '/getting-started/installation/advanced/studio', - collections: ['docs'], prefix: 1, }, ], diff --git a/src/app/test/unit/utils/context.test.ts b/src/app/test/unit/utils/context.test.ts index c937c691..45d574c9 100644 --- a/src/app/test/unit/utils/context.test.ts +++ b/src/app/test/unit/utils/context.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { computeItemActions, STUDIO_ITEM_ACTION_DEFINITIONS } from '../../../src/utils/context' -import { StudioItemActionId, type TreeItem, TreeRootId } from '../../../src/types' +import { StudioItemActionId, type TreeItem } from '../../../src/types' import { TreeStatus } from '../../../src/types' describe('computeItemActions', () => { @@ -18,7 +18,6 @@ describe('computeItemActions', () => { name: 'content', fsPath: '/', prefix: null, - collections: [TreeRootId.Content], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) @@ -40,7 +39,6 @@ describe('computeItemActions', () => { name: 'media', fsPath: '/', prefix: null, - collections: [TreeRootId.Media], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) @@ -64,7 +62,6 @@ describe('computeItemActions', () => { fsPath: '/', prefix: null, status: TreeStatus.Updated, - collections: [TreeRootId.Media], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) @@ -89,7 +86,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) @@ -110,7 +106,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Opened, } @@ -132,7 +127,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Updated, } @@ -153,7 +147,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Created, } @@ -174,7 +167,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Deleted, } @@ -198,7 +190,6 @@ describe('computeItemActions', () => { name: 'test.md', fsPath: 'test.md', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Renamed, } @@ -223,7 +214,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) @@ -244,7 +234,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Opened, } @@ -265,7 +254,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Updated, } @@ -285,7 +273,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Created, } @@ -305,7 +292,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Deleted, } @@ -327,7 +313,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Content], status: TreeStatus.Renamed, } as TreeItem @@ -347,7 +332,6 @@ describe('computeItemActions', () => { name: 'folder', fsPath: 'folder', prefix: null, - collections: [TreeRootId.Media], } const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) diff --git a/src/app/test/unit/utils/draft.test.ts b/src/app/test/unit/utils/draft.test.ts index 1afd9e29..84cd3901 100644 --- a/src/app/test/unit/utils/draft.test.ts +++ b/src/app/test/unit/utils/draft.test.ts @@ -1,54 +1,53 @@ import { describe, it, expect } from 'vitest' -import { findDescendantsFromId, getDraftStatus } from '../../../src/utils/draft' +import { findDescendantsFromFsPath, getDraftStatus } from '../../../src/utils/draft' import { draftItemsList } from '../../../test/mocks/draft' import { dbItemsList } from '../../../test/mocks/database' import { DraftStatus, TreeRootId } from '../../../src/types' -describe('findDescendantsFromId', () => { +describe('findDescendantsFromFsPath', () => { it('returns exact match for a root level file', () => { - const descendants = findDescendantsFromId(draftItemsList, 'landing/index.md') + const descendants = findDescendantsFromFsPath(draftItemsList, 'index.md') expect(descendants).toHaveLength(1) - expect(descendants[0].id).toBe('landing/index.md') - expect(descendants[0].fsPath).toBe('/index.md') + expect(descendants[0].fsPath).toBe('index.md') }) - it('returns empty array for non-existent id', () => { - const descendants = findDescendantsFromId(draftItemsList, 'non-existent/file.md') + it('returns empty array for non-existent fsPath', () => { + const descendants = findDescendantsFromFsPath(draftItemsList, 'non-existent/file.md') expect(descendants).toHaveLength(0) }) it('returns all descendants files for a directory path', () => { - const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started') + const descendants = findDescendantsFromFsPath(draftItemsList, '1.getting-started') expect(descendants).toHaveLength(5) - expect(descendants.some(item => item.id === 'docs/1.getting-started/2.introduction.md')).toBe(true) - expect(descendants.some(item => item.id === 'docs/1.getting-started/3.installation.md')).toBe(true) - expect(descendants.some(item => item.id === 'docs/1.getting-started/4.configuration.md')).toBe(true) - expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/1.studio.md')).toBe(true) - expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/2.deployment.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/2.introduction.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/3.installation.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/4.configuration.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/1.advanced/1.studio.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/1.advanced/2.deployment.md')).toBe(true) }) it('returns all descendants for a nested directory path', () => { - const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started/1.advanced') + const descendants = findDescendantsFromFsPath(draftItemsList, '1.getting-started/1.advanced') expect(descendants).toHaveLength(2) - expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/1.studio.md')).toBe(true) - expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/2.deployment.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/1.advanced/1.studio.md')).toBe(true) + expect(descendants.some(item => item.fsPath === '1.getting-started/1.advanced/2.deployment.md')).toBe(true) }) it('returns all descendants for root item', () => { - const descendants = findDescendantsFromId(draftItemsList, TreeRootId.Content) + const descendants = findDescendantsFromFsPath(draftItemsList, TreeRootId.Content) expect(descendants).toHaveLength(draftItemsList.length) }) it('returns only the file itself when searching for a specific file', () => { - const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started/1.advanced/1.studio.md') + const descendants = findDescendantsFromFsPath(draftItemsList, '1.getting-started/1.advanced/1.studio.md') expect(descendants).toHaveLength(1) - expect(descendants[0].id).toBe('docs/1.getting-started/1.advanced/1.studio.md') + expect(descendants[0].fsPath).toBe('1.getting-started/1.advanced/1.studio.md') }) }) diff --git a/src/app/test/unit/utils/tree.test.ts b/src/app/test/unit/utils/tree.test.ts index 3a20a5c1..5f4c66cd 100644 --- a/src/app/test/unit/utils/tree.test.ts +++ b/src/app/test/unit/utils/tree.test.ts @@ -19,14 +19,12 @@ describe('buildTree of documents with one level of depth', () => { type: 'file', routePath: '/', prefix: null, - collections: ['landing'], }, { name: 'getting-started', fsPath: '1.getting-started', type: 'directory', prefix: 1, - collections: ['docs'], children: [ { name: 'introduction', @@ -34,7 +32,6 @@ describe('buildTree of documents with one level of depth', () => { type: 'file', routePath: '/getting-started/introduction', prefix: 2, - collections: ['docs'], }, { name: 'installation', @@ -42,7 +39,6 @@ describe('buildTree of documents with one level of depth', () => { type: 'file', routePath: '/getting-started/installation', prefix: 3, - collections: ['docs'], }, ], }, @@ -57,7 +53,6 @@ describe('buildTree of documents with one level of depth', () => { const createdDbItem: DatabaseItem & { fsPath: string } = dbItemsList[0] const draftList: DraftItem[] = [{ - id: createdDbItem.id, fsPath: createdDbItem.fsPath, status: DraftStatus.Created, original: undefined, @@ -78,7 +73,6 @@ describe('buildTree of documents with one level of depth', () => { const deletedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md const draftList: DraftItem[] = [{ - id: deletedDbItem.id, fsPath: deletedDbItem.fsPath, status: DraftStatus.Deleted, modified: undefined, @@ -103,7 +97,6 @@ describe('buildTree of documents with one level of depth', () => { routePath: deletedDbItem.path, status: TreeStatus.Deleted, prefix: 2, - collections: ['docs'], }, ], }, @@ -114,7 +107,6 @@ describe('buildTree of documents with one level of depth', () => { const deletedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - id: deletedDbItem.id, fsPath: deletedDbItem.fsPath, status: DraftStatus.Deleted, modified: undefined, @@ -139,7 +131,6 @@ describe('buildTree of documents with one level of depth', () => { routePath: deletedDbItem.path, status: TreeStatus.Deleted, prefix: 3, - collections: ['docs'], }, ], }, @@ -150,7 +141,6 @@ describe('buildTree of documents with one level of depth', () => { const updatedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md const draftList: DraftItem[] = [{ - id: updatedDbItem.id, fsPath: updatedDbItem.fsPath, status: DraftStatus.Updated, original: updatedDbItem, @@ -188,13 +178,11 @@ describe('buildTree of documents with one level of depth', () => { const openedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - id: createdDbItem.id, fsPath: createdDbItem.fsPath, status: DraftStatus.Created, original: undefined, modified: createdDbItem, }, { - id: openedDbItem.id, fsPath: openedDbItem.fsPath, status: DraftStatus.Pristine, original: openedDbItem, @@ -223,13 +211,11 @@ describe('buildTree of documents with one level of depth', () => { const openedDbItem2: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - id: openedDbItem1.id, fsPath: openedDbItem1.fsPath, status: DraftStatus.Pristine, original: openedDbItem1, modified: openedDbItem1, }, { - id: openedDbItem2.id, fsPath: openedDbItem2.fsPath, status: DraftStatus.Pristine, original: openedDbItem2, @@ -265,13 +251,11 @@ describe('buildTree of documents with one level of depth', () => { } const draftList: DraftItem[] = [{ - id: deletedDbItem.id, fsPath: deletedDbItem.fsPath, status: DraftStatus.Deleted, modified: undefined, original: deletedDbItem, }, { - id: createdDbItem.id, fsPath: createdDbItem.fsPath, status: DraftStatus.Created, modified: createdDbItem, @@ -298,7 +282,6 @@ describe('buildTree of documents with one level of depth', () => { type: 'file', status: TreeStatus.Renamed, prefix: 2, - collections: ['docs'], }, ], }, @@ -313,7 +296,6 @@ describe('buildTree of documents with two levels of depth', () => { fsPath: '1.essentials', type: 'directory', prefix: 1, - collections: ['docs'], children: [ { name: 'configuration', @@ -321,14 +303,12 @@ describe('buildTree of documents with two levels of depth', () => { type: 'file', routePath: '/essentials/configuration', prefix: 2, - collections: ['docs'], }, { name: 'nested', fsPath: '1.essentials/1.nested', type: 'directory', prefix: 1, - collections: ['docs'], children: [ { name: 'advanced', @@ -336,7 +316,6 @@ describe('buildTree of documents with two levels of depth', () => { type: 'file', routePath: '/essentials/nested/advanced', prefix: 2, - collections: ['docs'], }, ], }, @@ -353,7 +332,6 @@ describe('buildTree of documents with two levels of depth', () => { const updatedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[0] // 1.essentials/2.configuration.md const draftList: DraftItem[] = [{ - id: updatedDbItem.id, fsPath: updatedDbItem.fsPath, status: DraftStatus.Updated, original: updatedDbItem, @@ -382,7 +360,6 @@ describe('buildTree of documents with two levels of depth', () => { const updatedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md const draftList: DraftItem[] = [{ - id: updatedDbItem.id, fsPath: updatedDbItem.fsPath, status: DraftStatus.Updated, original: updatedDbItem, @@ -420,7 +397,6 @@ describe('buildTree of documents with two levels of depth', () => { const deletedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md const draftList: DraftItem[] = [{ - id: deletedDbItem.id, fsPath: deletedDbItem.fsPath, status: DraftStatus.Deleted, modified: undefined, @@ -448,7 +424,6 @@ describe('buildTree of documents with two levels of depth', () => { type: 'file', status: TreeStatus.Deleted, prefix: 2, - collections: ['docs'], }, ], }, @@ -464,7 +439,6 @@ describe('buildTree of documents with language prefixed', () => { fsPath: 'en', type: 'directory', prefix: null, - collections: ['landing_en', 'docs_en'], children: [ { name: 'index', @@ -472,14 +446,12 @@ describe('buildTree of documents with language prefixed', () => { prefix: null, type: 'file', routePath: '/en', - collections: ['landing_en'], }, { name: 'getting-started', fsPath: 'en/1.getting-started', type: 'directory', prefix: 1, - collections: ['docs_en'], children: [ { name: 'introduction', @@ -487,7 +459,6 @@ describe('buildTree of documents with language prefixed', () => { type: 'file', routePath: '/en/getting-started/introduction', prefix: 2, - collections: ['docs_en'], }, { name: 'installation', @@ -495,7 +466,6 @@ describe('buildTree of documents with language prefixed', () => { type: 'file', routePath: '/en/getting-started/installation', prefix: 3, - collections: ['docs_en'], }, ], }, @@ -534,7 +504,6 @@ describe('buildTree of medias', () => { } const draftList: DraftItem[] = [{ - id: gitkeepDbItem.id, fsPath: gitkeepDbItem.fsPath, status: DraftStatus.Created, original: undefined, diff --git a/src/app/test/utils/index.ts b/src/app/test/utils/index.ts index b19118b0..dbeb1f0e 100644 --- a/src/app/test/utils/index.ts +++ b/src/app/test/utils/index.ts @@ -17,18 +17,14 @@ export function normalizeKey(key: string): string { || '' } -export function generateUniqueDocumentId(filename = 'document', collection = 'docs'): string { +export function generateUniqueDocumentFsPath(filename = 'document', subdirectory = ''): string { const uniqueId = Math.random().toString(36).substr(2, 9) - // Add dummy collection prefix - return joinURL(collection, `${filename}-${uniqueId}.md`) + const file = `${filename}-${uniqueId}.md` + return subdirectory ? joinURL(subdirectory, file) : file } -export function generateUniqueMediaId(filename = 'media'): string { +export function generateUniqueMediaFsPath(filename = 'media', extension = 'png', subdirectory = ''): string { const uniqueId = Math.random().toString(36).substr(2, 9) - return `${TreeRootId.Media}/${filename}-${uniqueId}.md` -} - -export function generateUniqueMediaName(filename = 'media', extension = 'png'): string { - const uniqueId = Math.random().toString(36).substr(2, 9) - return `${filename}-${uniqueId}.${extension}` + const file = `${filename}-${uniqueId}.${extension}` + return subdirectory ? joinURL(subdirectory, file) : file } diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts index 563c8b70..e46c2a0a 100644 --- a/src/module/src/runtime/utils/collection.ts +++ b/src/module/src/runtime/utils/collection.ts @@ -70,7 +70,6 @@ export function getCollection(collectionName: string, collections: Record { const prefix = source.prefix From 34d88836c19102735077ccfc45f58ce8093783ef Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Mon, 10 Nov 2025 21:40:32 +0100 Subject: [PATCH 03/15] fix(context): compute actions --- .../shared/item/ItemActionsDropdown.vue | 2 +- .../shared/item/ItemActionsToolbar.vue | 2 +- src/app/src/composables/useContext.ts | 17 +++++- src/app/src/types/tree.ts | 2 +- src/app/src/utils/context.ts | 11 ++-- src/app/test/unit/utils/context.test.ts | 52 ++++++++++++------- 6 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/app/src/components/shared/item/ItemActionsDropdown.vue b/src/app/src/components/shared/item/ItemActionsDropdown.vue index e0eedb9f..063c39a8 100644 --- a/src/app/src/components/shared/item/ItemActionsDropdown.vue +++ b/src/app/src/components/shared/item/ItemActionsDropdown.vue @@ -40,7 +40,7 @@ const actions = computed(() => { const hasPendingAction = pendingAction.value !== null const hasLoadingAction = loadingAction.value !== null - return computeItemActions(context.itemActions.value, props.item).map((action) => { + return computeItemActions(context.itemActions.value, props.item, context.currentFeature.value).map((action) => { const isOneStepAction = oneStepActions.includes(action.id) const isPending = pendingAction.value?.id === action.id const isLoading = loadingAction.value?.id === action.id diff --git a/src/app/src/components/shared/item/ItemActionsToolbar.vue b/src/app/src/components/shared/item/ItemActionsToolbar.vue index 8efd06c7..45351b41 100644 --- a/src/app/src/components/shared/item/ItemActionsToolbar.vue +++ b/src/app/src/components/shared/item/ItemActionsToolbar.vue @@ -24,7 +24,7 @@ const actions = computed(() => { const hasPendingAction = pendingAction.value !== null const hasLoadingAction = loadingAction.value !== null - return computeItemActions(context.itemActions.value, item.value).map((action) => { + return computeItemActions(context.itemActions.value, item.value, context.currentFeature.value).map((action) => { const isOneStepAction = oneStepActions.includes(action.id) const isPending = pendingAction.value?.id === action.id const isLoading = loadingAction.value?.id === action.id diff --git a/src/app/src/composables/useContext.ts b/src/app/src/composables/useContext.ts index 329e91a5..ed8ddde1 100644 --- a/src/app/src/composables/useContext.ts +++ b/src/app/src/composables/useContext.ts @@ -1,6 +1,6 @@ import { createSharedComposable } from '@vueuse/core' import { computed, ref } from 'vue' -import { StudioItemActionId, DraftStatus, StudioBranchActionId, TreeRootId } from '../types' +import { StudioItemActionId, DraftStatus, StudioBranchActionId, TreeRootId, StudioFeature } from '../types' import type { PublishBranchParams, RenameFileParams, @@ -34,6 +34,20 @@ export const useContext = createSharedComposable(( const route = useRoute() const router = useRouter() + /** + * Current feature + */ + const currentFeature = computed(() => { + switch (route.name) { + case 'media': + return StudioFeature.Media + case 'content': + return StudioFeature.Content + default: + return null + } + }) + /** * Drafts */ @@ -230,6 +244,7 @@ export const useContext = createSharedComposable(( } return { + currentFeature, activeTree, itemActions, itemActionHandler, diff --git a/src/app/src/types/tree.ts b/src/app/src/types/tree.ts index e52e666b..93b26d99 100644 --- a/src/app/src/types/tree.ts +++ b/src/app/src/types/tree.ts @@ -13,7 +13,7 @@ export enum TreeStatus { export interface TreeItem { name: string - fsPath: string // can be used as id + fsPath: string // unique identifier type: 'file' | 'directory' | 'root' prefix: number | null status?: TreeStatus diff --git a/src/app/src/utils/context.ts b/src/app/src/utils/context.ts index 3f5af2f8..7ddb181c 100644 --- a/src/app/src/utils/context.ts +++ b/src/app/src/utils/context.ts @@ -1,5 +1,4 @@ -import { type StudioAction, type TreeItem, TreeStatus, StudioItemActionId, StudioBranchActionId, TreeRootId } from '../types' - +import { type StudioAction, type TreeItem, TreeStatus, StudioItemActionId, StudioBranchActionId, StudioFeature } from '../types' export const oneStepActions: StudioItemActionId[] = [StudioItemActionId.RevertItem, StudioItemActionId.DeleteItem, StudioItemActionId.DuplicateItem] export const twoStepActions: StudioItemActionId[] = [StudioItemActionId.CreateDocument, StudioItemActionId.CreateDocumentFolder, StudioItemActionId.CreateMediaFolder, StudioItemActionId.RenameItem] @@ -61,14 +60,14 @@ export const STUDIO_BRANCH_ACTION_DEFINITIONS: StudioAction[], item?: TreeItem | null): StudioAction[] { - if (!item) { - return itemActions +export function computeItemActions(itemActions: StudioAction[], item: TreeItem | null, feature: StudioFeature | null): StudioAction[] { + if (!item || !feature) { + return [] } const forbiddenActions: StudioItemActionId[] = [] - if (item.collections.includes(TreeRootId.Media)) { + if (feature === StudioFeature.Media) { forbiddenActions.push(StudioItemActionId.DuplicateItem, StudioItemActionId.CreateDocumentFolder, StudioItemActionId.CreateDocument) } else { diff --git a/src/app/test/unit/utils/context.test.ts b/src/app/test/unit/utils/context.test.ts index 45d574c9..2d06bf74 100644 --- a/src/app/test/unit/utils/context.test.ts +++ b/src/app/test/unit/utils/context.test.ts @@ -1,12 +1,24 @@ import { describe, it, expect } from 'vitest' import { computeItemActions, STUDIO_ITEM_ACTION_DEFINITIONS } from '../../../src/utils/context' -import { StudioItemActionId, type TreeItem } from '../../../src/types' +import { StudioItemActionId, StudioFeature, type TreeItem } from '../../../src/types' import { TreeStatus } from '../../../src/types' describe('computeItemActions', () => { - it('should return all actions when item is undefined', () => { - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, undefined) - expect(result).toEqual(STUDIO_ITEM_ACTION_DEFINITIONS) + it('should return no actions when item is null', () => { + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, null, StudioFeature.Content) + expect(result).toEqual([]) + }) + + it('should return no actions when feature is undefined', () => { + const rootItem: TreeItem = { + type: 'root', + name: 'content', + fsPath: '/', + prefix: null, + } + + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem, null) + expect(result).toEqual([]) }) /************************************************** @@ -20,7 +32,7 @@ describe('computeItemActions', () => { prefix: null, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.RenameItem @@ -41,7 +53,7 @@ describe('computeItemActions', () => { prefix: null, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem, StudioFeature.Media) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.RevertItem @@ -64,7 +76,7 @@ describe('computeItemActions', () => { status: TreeStatus.Updated, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem, StudioFeature.Media) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DeleteItem @@ -88,7 +100,7 @@ describe('computeItemActions', () => { prefix: null, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -109,7 +121,7 @@ describe('computeItemActions', () => { status: TreeStatus.Opened, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -130,7 +142,7 @@ describe('computeItemActions', () => { status: TreeStatus.Updated, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -150,7 +162,7 @@ describe('computeItemActions', () => { status: TreeStatus.Created, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -170,7 +182,7 @@ describe('computeItemActions', () => { status: TreeStatus.Deleted, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -193,7 +205,7 @@ describe('computeItemActions', () => { status: TreeStatus.Renamed, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.CreateDocumentFolder @@ -216,7 +228,7 @@ describe('computeItemActions', () => { prefix: null, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -237,7 +249,7 @@ describe('computeItemActions', () => { status: TreeStatus.Opened, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -257,7 +269,7 @@ describe('computeItemActions', () => { status: TreeStatus.Updated, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -276,7 +288,7 @@ describe('computeItemActions', () => { status: TreeStatus.Created, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -295,7 +307,7 @@ describe('computeItemActions', () => { status: TreeStatus.Deleted, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -316,7 +328,7 @@ describe('computeItemActions', () => { status: TreeStatus.Renamed, } as TreeItem - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Content) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.DuplicateItem @@ -334,7 +346,7 @@ describe('computeItemActions', () => { prefix: null, } - const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem) + const result = computeItemActions(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem, StudioFeature.Media) const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action => action.id !== StudioItemActionId.RevertItem From 6af655c0e8389fe9c04c40adacf5f4121ad18b47 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Tue, 11 Nov 2025 01:15:29 +0100 Subject: [PATCH 04/15] refactor(app): remove id logic from app --- src/app/src/app.vue | 9 +- .../src/components/content/ContentCard.vue | 10 - .../src/components/content/ContentEditor.vue | 2 +- .../src/components/shared/item/ItemCard.vue | 6 +- .../components/shared/item/ItemCardReview.vue | 9 +- src/app/src/composables/useContext.ts | 49 ++-- src/app/src/composables/useDraftBase.ts | 75 +++--- src/app/src/composables/useDraftDocuments.ts | 42 +-- src/app/src/composables/useDraftMedias.ts | 36 +-- src/app/src/composables/useStudio.ts | 18 +- src/app/src/composables/useTree.ts | 15 +- src/app/src/pages/review.vue | 2 +- src/app/src/shared.ts | 1 + src/app/src/types/database.ts | 3 + src/app/src/types/draft.ts | 4 +- src/app/src/types/index.ts | 5 +- src/app/src/types/item.ts | 1 + src/app/src/types/media.ts | 2 + src/app/src/types/tree.ts | 5 - src/app/src/utils/content.ts | 9 +- src/app/src/utils/context.ts | 1 + src/app/src/utils/draft.ts | 15 +- src/app/src/utils/tree.ts | 9 +- src/app/test/integration/actions.test.ts | 102 ++++++- src/app/test/mocks/document.ts | 30 ++- src/app/test/mocks/host.ts | 17 +- src/app/test/mocks/media.ts | 2 + src/app/test/unit/utils/draft.test.ts | 4 +- src/app/test/unit/utils/tree.test.ts | 9 +- src/app/test/utils/index.ts | 1 - src/module/src/runtime/host.dev.ts | 36 ++- src/module/src/runtime/host.ts | 63 +++-- src/module/src/runtime/utils/collection.ts | 192 +++---------- src/module/src/runtime/utils/document.ts | 63 +++++ src/module/src/runtime/utils/media.ts | 6 + src/module/src/runtime/utils/source.ts | 51 ++++ src/module/test/utils/collection.test.ts | 255 ++++-------------- src/module/test/utils/source.test.ts | 105 ++++++++ 38 files changed, 655 insertions(+), 609 deletions(-) create mode 100644 src/module/src/runtime/utils/document.ts create mode 100644 src/module/src/runtime/utils/media.ts create mode 100644 src/module/src/runtime/utils/source.ts create mode 100644 src/module/test/utils/source.test.ts diff --git a/src/app/src/app.vue b/src/app/src/app.vue index 99f99933..ed1cfb6a 100644 --- a/src/app/src/app.vue +++ b/src/app/src/app.vue @@ -25,18 +25,17 @@ watch(ui.sidebar.sidebarWidth, () => { // Nuxt UI Portal element const appPortal = ref() -const activeDocuments = ref<{ id: string, title: string }[]>([]) +const activeDocuments = ref<{ fsPath: string, title: string }[]>([]) function detectActiveDocuments() { activeDocuments.value = host.document.detectActives().map((content) => { return { - id: content.id, + fsPath: content.fsPath, title: content.title, } }) } -async function editContentFile(id: string) { - const fsPath = host.document.getFileSystemPath(id) +async function editContentFile(fsPath: string) { await context.activeTree.value.selectItemByFsPath(fsPath) ui.open() } @@ -140,7 +139,7 @@ router.beforeEach((to, from) => { variant="outline" class="bg-transparent backdrop-blur-md px-2" label="Edit this page" - @click="editContentFile(activeDocuments[0].id)" + @click="editContentFile(activeDocuments[0].fsPath)" /> diff --git a/src/app/src/components/content/ContentCard.vue b/src/app/src/components/content/ContentCard.vue index adea679b..a3b835c0 100644 --- a/src/app/src/components/content/ContentCard.vue +++ b/src/app/src/components/content/ContentCard.vue @@ -21,8 +21,6 @@ const props = defineProps({ }) const itemExtensionIcon = computed(() => getFileIcon(props.item.fsPath)) -const collectionName = computed(() => props.item.collections[0]) -const isDirectory = computed(() => props.item.type === 'directory') - diff --git a/src/app/src/components/content/ContentEditor.vue b/src/app/src/components/content/ContentEditor.vue index 69f73b3b..e213f14a 100644 --- a/src/app/src/components/content/ContentEditor.vue +++ b/src/app/src/components/content/ContentEditor.vue @@ -50,7 +50,7 @@ const document = computed({ return } - context.activeTree.value.draft.update(props.draftItem.id, { + context.activeTree.value.draft.update(props.draftItem.fsPath, { ...toRaw(document.value as DatabasePageItem), ...toRaw(value), }) diff --git a/src/app/src/components/shared/item/ItemCard.vue b/src/app/src/components/shared/item/ItemCard.vue index 700e6510..49cb2c52 100644 --- a/src/app/src/components/shared/item/ItemCard.vue +++ b/src/app/src/components/shared/item/ItemCard.vue @@ -22,8 +22,7 @@ const statusRingColor = computed(() => props.item.status && props.item.status != const displayInfo = computed(() => { if (isDirectory.value) { const itemcount = props.item.children?.filter(child => !child.hide).length || 0 - const collectionCount = props.item.collections.length - return `${itemcount} ${itemcount === 1 ? 'item' : 'items'} from ${collectionCount} ${collectionCount === 1 ? 'collection' : 'collections'}` + return `${itemcount} ${itemcount === 1 ? 'item' : 'items'}` } return props.item.routePath || props.item.fsPath }) @@ -70,9 +69,8 @@ const displayInfo = computed(() => { -
+
-
diff --git a/src/app/src/components/shared/item/ItemCardReview.vue b/src/app/src/components/shared/item/ItemCardReview.vue index 7a5219d5..4a6a9e88 100644 --- a/src/app/src/components/shared/item/ItemCardReview.vue +++ b/src/app/src/components/shared/item/ItemCardReview.vue @@ -2,12 +2,9 @@ import type { DraftItem } from '../../../types' import type { PropType } from 'vue' import { computed } from 'vue' -import { DraftStatus, TreeRootId } from '../../../types' +import { DraftStatus } from '../../../types' import { getFileIcon } from '../../../utils/file' import { COLOR_UI_STATUS_MAP } from '../../../utils/tree' -import { useStudio } from '../../../composables/useStudio' - -const { host } = useStudio() const props = defineProps({ draftItem: { @@ -28,9 +25,7 @@ const originalPath = computed(() => { return null } - const isMedia = props.draftItem.original.id.startsWith(TreeRootId.Media) - const hostApi = isMedia ? host.media : host.document - return hostApi.getFileSystemPath(props.draftItem.original.id) + return props.draftItem.original.fsPath }) function toggleOpen() { diff --git a/src/app/src/composables/useContext.ts b/src/app/src/composables/useContext.ts index ed8ddde1..43bb77bd 100644 --- a/src/app/src/composables/useContext.ts +++ b/src/app/src/composables/useContext.ts @@ -1,6 +1,6 @@ import { createSharedComposable } from '@vueuse/core' import { computed, ref } from 'vue' -import { StudioItemActionId, DraftStatus, StudioBranchActionId, TreeRootId, StudioFeature } from '../types' +import { StudioItemActionId, DraftStatus, StudioBranchActionId, StudioFeature, VirtualMediaCollectionName } from '../types' import type { PublishBranchParams, RenameFileParams, @@ -20,7 +20,7 @@ import type { useTree } from './useTree' import type { useGit } from './useGit' import type { useDraftMedias } from './useDraftMedias' import { useRoute, useRouter } from 'vue-router' -import { findDescendantsFileItemsFromFsPath, generateIdFromFsPath } from '../utils/tree' +import { findDescendantsFileItemsFromFsPath } from '../utils/tree' import { joinURL } from 'ufo' import { upperFirst } from 'scule' import { generateStemFromFsPath } from '../utils/media' @@ -108,44 +108,44 @@ export const useContext = createSharedComposable(( const navigationDocument = await host.document.create(navigationDocumentFsPath, `title: ${folderName}`) const rootDocument = await host.document.create(rootDocumentFsPath, `# ${upperFirst(folderName)} root file`) - await activeTree.value.draft.create(navigationDocument) + await activeTree.value.draft.create(fsPath, navigationDocument) unsetActionInProgress() - const rootDocumentDraftItem = await activeTree.value.draft.create(rootDocument) + const rootDocumentDraftItem = await activeTree.value.draft.create(rootDocumentFsPath, rootDocument) await activeTree.value.selectItemByFsPath(rootDocumentDraftItem.fsPath) }, [StudioItemActionId.CreateMediaFolder]: async (params: CreateFolderParams) => { const { fsPath } = params const gitkeepFsPath = joinURL(fsPath, '.gitkeep') + const gitKeepId = joinURL(VirtualMediaCollectionName, gitkeepFsPath) const gitKeepMedia: MediaItem = { - id: generateIdFromFsPath(gitkeepFsPath, TreeRootId.Media), + id: gitKeepId, fsPath: gitkeepFsPath, stem: generateStemFromFsPath(gitkeepFsPath), extension: '', } - await host.media.upsert(gitKeepMedia.id, gitKeepMedia) - await (activeTree.value.draft as ReturnType).create(gitKeepMedia) + await host.media.upsert(gitkeepFsPath, gitKeepMedia) + await (activeTree.value.draft as ReturnType).create(gitkeepFsPath, gitKeepMedia) unsetActionInProgress() - await activeTree.value.selectParentByFsPath(gitKeepMedia.id) + await activeTree.value.selectParentByFsPath(gitkeepFsPath) }, [StudioItemActionId.CreateDocument]: async (params: CreateFileParams) => { const { fsPath, content } = params const document = await host.document.create(fsPath, content) - const draftItem = await activeTree.value.draft.create(document as DatabaseItem) + const draftItem = await activeTree.value.draft.create(fsPath, document as DatabaseItem) await activeTree.value.selectItemByFsPath(draftItem.fsPath) }, [StudioItemActionId.UploadMedia]: async ({ parentFsPath, files }: UploadMediaParams) => { // Remove .gitkeep draft in folder if exists const gitkeepFsPath = parentFsPath === '/' ? '.gitkeep' : joinURL(parentFsPath, '.gitkeep') - const gitkeepId = generateIdFromFsPath(gitkeepFsPath, TreeRootId.Media) - const gitkeepDraft = await activeTree.value.draft.get(gitkeepId) + const gitkeepDraft = await activeTree.value.draft.get(gitkeepFsPath) if (gitkeepDraft) { - await activeTree.value.draft.remove([gitkeepId], { rerender: false }) + await activeTree.value.draft.remove([gitkeepFsPath], { rerender: false }) } for (const file of files) { @@ -153,19 +153,14 @@ export const useContext = createSharedComposable(( } }, [StudioItemActionId.RevertItem]: async (item: TreeItem) => { - // Get collections from document item or use default media collection - for (const collection of item.collections) { - const id = generateIdFromFsPath(item.fsPath, collection) - await activeTree.value.draft.revert(id) - } + await activeTree.value.draft.revert(item.fsPath) }, [StudioItemActionId.RenameItem]: async (params: TreeItem | RenameFileParams) => { const { item, newFsPath } = params as RenameFileParams // Revert file if (item.type === 'file') { - const id = generateIdFromFsPath(item.fsPath, item.collections[0]) - await activeTree.value.draft.rename([{ id, newFsPath }]) + await activeTree.value.draft.rename([{ fsPath: item.fsPath, newFsPath }]) return } @@ -174,7 +169,7 @@ export const useContext = createSharedComposable(( if (descendants.length > 0) { const itemsToRename = descendants.map((descendant) => { return { - id: generateIdFromFsPath(descendant.fsPath, descendant.collections[0]), + fsPath: descendant.fsPath, newFsPath: descendant.fsPath.replace(item.fsPath, newFsPath), } }) @@ -185,26 +180,22 @@ export const useContext = createSharedComposable(( [StudioItemActionId.DeleteItem]: async (item: TreeItem) => { // Delete file if (item.type === 'file') { - const id = generateIdFromFsPath(item.fsPath, item.collections![0]) - await activeTree.value.draft.remove([id]) + await activeTree.value.draft.remove([item.fsPath]) return } // Delete folder const descendants = findDescendantsFileItemsFromFsPath(activeTree.value.root.value, item.fsPath) if (descendants.length > 0) { - const ids: string[] = descendants.map((descendant) => { - return generateIdFromFsPath(descendant.fsPath, descendant.collections![0]) - }) - await activeTree.value.draft.remove(ids) + const fsPaths: string[] = descendants.map(descendant => descendant.fsPath) + await activeTree.value.draft.remove(fsPaths) } }, [StudioItemActionId.DuplicateItem]: async (item: TreeItem) => { // Duplicate file if (item.type === 'file') { - const id = generateIdFromFsPath(item.fsPath, item.collections![0]) - const draftItem = await activeTree.value.draft.duplicate(id) - await activeTree.value.selectItemByFsPath(draftItem!.id) + const draftItem = await activeTree.value.draft.duplicate(item.fsPath) + await activeTree.value.selectItemByFsPath(draftItem!.fsPath) return } }, diff --git a/src/app/src/composables/useDraftBase.ts b/src/app/src/composables/useDraftBase.ts index a217bdbe..a2b68235 100644 --- a/src/app/src/composables/useDraftBase.ts +++ b/src/app/src/composables/useDraftBase.ts @@ -2,12 +2,12 @@ import type { Storage } from 'unstorage' import { joinURL } from 'ufo' import type { DraftItem, StudioHost, GithubFile, DatabaseItem, MediaItem } from '../types' import { DraftStatus } from '../types/draft' -import { checkConflict, findDescendantsFromId, getDraftStatus } from '../utils/draft' +import { checkConflict, findDescendantsFromFsPath, getDraftStatus } from '../utils/draft' import type { useGit } from './useGit' import { useHooks } from './useHooks' import { ref } from 'vue' -export function useDraftBase( +export function useDraftBase( type: 'media' | 'document', host: StudioHost, git: ReturnType, @@ -23,21 +23,19 @@ export function useDraftBase( const hooks = useHooks() - async function get(id: string): Promise | undefined> { - return list.value.find(item => item.id === id) as DraftItem + async function get(fsPath: string): Promise | undefined> { + return list.value.find(item => item.fsPath === fsPath) as DraftItem } - async function create(item: T, original?: T, { rerender = true }: { rerender?: boolean } = {}): Promise> { - const existingItem = list.value?.find(draft => draft.id === item.id) + async function create(fsPath: string, item: T, original?: T, { rerender = true }: { rerender?: boolean } = {}): Promise> { + const existingItem = list.value?.find(draft => draft.fsPath === fsPath) if (existingItem) { - throw new Error(`Draft file already exists for document ${item.id}`) + throw new Error(`Draft file already exists for document at ${fsPath}`) } - const fsPath = hostDb.getFileSystemPath(item.id) const githubFile = await git.fetchFile(joinURL(ghPathPrefix, fsPath), { cached: true }) as GithubFile const draftItem: DraftItem = { - id: item.id, fsPath, githubFile, status: getDraftStatus(item, original), @@ -53,7 +51,7 @@ export function useDraftBase( draftItem.conflict = conflict } - await storage.setItem(item.id, draftItem) + await storage.setItem(fsPath, draftItem) list.value.push(draftItem) @@ -64,32 +62,30 @@ export function useDraftBase( return draftItem } - async function remove(ids: string[], { rerender = true }: { rerender?: boolean } = {}) { - for (const id of ids) { - const existingDraftItem = list.value.find(item => item.id === id) as DraftItem | undefined - const fsPath = hostDb.getFileSystemPath(id) - const originalDbItem = await hostDb.get(id) as T + async function remove(fsPaths: string[], { rerender = true }: { rerender?: boolean } = {}) { + for (const fsPath of fsPaths) { + const existingDraftItem = list.value.find(item => item.fsPath === fsPath) as DraftItem | undefined + const originalDbItem = await hostDb.get(fsPath) as T - await storage.removeItem(id) - await hostDb.delete(id) + await storage.removeItem(fsPath) + await hostDb.delete(fsPath) let deleteDraftItem: DraftItem | null = null if (existingDraftItem) { if (existingDraftItem.status === DraftStatus.Deleted) return if (existingDraftItem.status === DraftStatus.Created) { - list.value = list.value.filter(item => item.id !== id) + list.value = list.value.filter(item => item.fsPath !== fsPath) } else { deleteDraftItem = { - id, fsPath: existingDraftItem.fsPath, status: DraftStatus.Deleted, original: existingDraftItem.original, githubFile: existingDraftItem.githubFile, } - list.value = list.value.map(item => item.id === id ? deleteDraftItem! : item) as DraftItem[] + list.value = list.value.map(item => item.fsPath === fsPath ? deleteDraftItem! : item) as DraftItem[] } } else { @@ -97,7 +93,6 @@ export function useDraftBase( const githubFile = await git.fetchFile(joinURL('content', fsPath), { cached: true }) as GithubFile deleteDraftItem = { - id, fsPath, status: DraftStatus.Deleted, original: originalDbItem, @@ -108,7 +103,7 @@ export function useDraftBase( } if (deleteDraftItem) { - await storage.setItem(id, deleteDraftItem) + await storage.setItem(fsPath, deleteDraftItem) } if (rerender) { @@ -117,31 +112,31 @@ export function useDraftBase( } } - async function revert(id: string, { rerender = true }: { rerender?: boolean } = {}) { - const draftItems = findDescendantsFromId(list.value, id) + async function revert(fsPath: string, { rerender = true }: { rerender?: boolean } = {}) { + const draftItems = findDescendantsFromFsPath(list.value, fsPath) for (const draftItem of draftItems) { - const existingItem = list.value.find(item => item.id === draftItem.id) as DraftItem + const existingItem = list.value.find(item => item.fsPath === draftItem.fsPath) as DraftItem if (!existingItem) { return } if (existingItem.status === DraftStatus.Created) { - await hostDb.delete(draftItem.id) - await storage.removeItem(draftItem.id) - list.value = list.value.filter(item => item.id !== draftItem.id) + await hostDb.delete(draftItem.fsPath) + await storage.removeItem(draftItem.fsPath) + list.value = list.value.filter(item => item.fsPath !== draftItem.fsPath) // Renamed draft if (existingItem.original) { - await revert(existingItem.original.id, { rerender: false }) + await revert(existingItem.original.fsPath, { rerender: false }) } } else { // @ts-expect-error upsert type is wrong, second param should be DatabaseItem | MediaItem - await hostDb.upsert(draftItem.id, existingItem.original) + await hostDb.upsert(draftItem.fsPath, existingItem.original) existingItem.modified = existingItem.original existingItem.status = getDraftStatus(existingItem.modified, existingItem.original) - await storage.setItem(draftItem.id, existingItem) + await storage.setItem(draftItem.fsPath, existingItem) } } @@ -154,7 +149,7 @@ export function useDraftBase( const itemsToRevert = [...list.value] for (const draftItem of itemsToRevert) { - await revert(draftItem.id, { rerender: false }) + await revert(draftItem.fsPath, { rerender: false }) } await hooks.callHook(hookName, { caller: 'useDraftBase.revertAll' }) @@ -164,22 +159,22 @@ export function useDraftBase( current.value = null } - async function selectById(id: string) { + async function selectByFsPath(fsPath: string) { isLoading.value = true try { - const existingItem = list.value?.find(item => item.id === id) as DraftItem + const existingItem = list.value?.find(item => item.fsPath === fsPath) as DraftItem if (existingItem) { current.value = existingItem return } - const dbItem = await hostDb.get(id) as T + const dbItem = await hostDb.get(fsPath) as T if (!dbItem) { - throw new Error(`Cannot select item: no corresponding database entry found for id ${id}`) + throw new Error(`Cannot select item: no corresponding database entry found for fsPath ${fsPath}`) } - const draftItem = await create(dbItem, dbItem) + const draftItem = await create(fsPath, dbItem, dbItem) current.value = draftItem } @@ -205,11 +200,11 @@ export function useDraftBase( // Upsert/Delete draft files in database await Promise.all(list.value.map(async (draftItem) => { if (draftItem.status === DraftStatus.Deleted) { - await hostDb.delete(draftItem.id) + await hostDb.delete(draftItem.fsPath) } else { // @ts-expect-error upsert type is wrong, second param should be DatabaseItem | MediaItem - await hostDb.upsert(draftItem.id, draftItem.modified) + await hostDb.upsert(draftItem.fsPath, draftItem.modified) } })) @@ -225,7 +220,7 @@ export function useDraftBase( remove, revert, revertAll, - selectById, + selectByFsPath, unselect, load, checkConflict, diff --git a/src/app/src/composables/useDraftDocuments.ts b/src/app/src/composables/useDraftDocuments.ts index e35561be..7933cf7f 100644 --- a/src/app/src/composables/useDraftDocuments.ts +++ b/src/app/src/composables/useDraftDocuments.ts @@ -20,29 +20,29 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: remove, revert, revertAll, - selectById, + selectByFsPath, unselect, load, } = useDraftBase('document', host, git, storage) const hooks = useHooks() - async function update(id: string, document: DatabaseItem): Promise> { - const existingItem = list.value.find(item => item.id === id) as DraftItem + async function update(fsPath: string, document: DatabaseItem): Promise> { + const existingItem = list.value.find(item => item.fsPath === fsPath) as DraftItem if (!existingItem) { - throw new Error(`Draft file not found for document ${id}`) + throw new Error(`Draft file not found for document fsPath: ${fsPath}`) } const oldStatus = existingItem.status existingItem.status = getDraftStatus(document, existingItem.original) existingItem.modified = document - await storage.setItem(id, existingItem) + await storage.setItem(fsPath, existingItem) - list.value = list.value.map(item => item.id === id ? existingItem : item) + list.value = list.value.map(item => item.fsPath === fsPath ? existingItem : item) // Upsert document in database - await host.document.upsert(id, existingItem.modified) + await host.document.upsert(fsPath, existingItem.modified) // Trigger hook to warn that draft list has changed if (existingItem.status !== oldStatus) { @@ -56,14 +56,14 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: return existingItem } - async function rename(items: { id: string, newFsPath: string }[]) { + async function rename(items: { fsPath: string, newFsPath: string }[]) { for (const item of items) { - const { id, newFsPath } = item + const { fsPath, newFsPath } = item - const existingDraftToRename = list.value.find(draftItem => draftItem.id === id) as DraftItem - const dbItemToRename: DatabaseItem = await host.document.get(id) + const existingDraftToRename = list.value.find(draftItem => draftItem.fsPath === fsPath) as DraftItem + const dbItemToRename: DatabaseItem = await host.document.get(fsPath) if (!dbItemToRename) { - throw new Error(`Database item not found for document ${id}`) + throw new Error(`Database item not found for document fsPath: ${fsPath}`) } const modifiedDbItem = existingDraftToRename?.modified || dbItemToRename @@ -74,28 +74,28 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: const content = await generateContentFromDocument(modifiedDbItem) - await remove([id], { rerender: false }) + await remove([fsPath], { rerender: false }) const newDbItem = await host.document.create(newFsPath, content!) - await create(newDbItem, originalDbItem, { rerender: false }) + await create(newFsPath, newDbItem, originalDbItem, { rerender: false }) } await hooks.callHook('studio:draft:document:updated', { caller: 'useDraftDocuments.rename' }) } - async function duplicate(id: string): Promise> { - let currentDbItem = await host.document.get(id) + async function duplicate(fsPath: string): Promise> { + let currentDbItem = await host.document.get(fsPath) if (!currentDbItem) { - throw new Error(`Database item not found for document ${id}`) + throw new Error(`Database item not found for document fsPath: ${fsPath}`) } - const currentDraftItem = list.value.find(item => item.id === id) + const currentDraftItem = list.value.find(item => item.fsPath === fsPath) if (currentDraftItem) { currentDbItem = currentDraftItem.modified as DatabaseItem } - const currentFsPath = currentDraftItem?.fsPath || host.document.getFileSystemPath(id) + const currentFsPath = currentDraftItem?.fsPath || fsPath const currentContent = await generateContentFromDocument(currentDbItem) || '' const currentName = currentFsPath.split('/').pop()! const currentExtension = getFileExtension(currentName) @@ -105,7 +105,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: const newDbItem = await host.document.create(newFsPath, currentContent) - return await create(newDbItem) + return await create(newFsPath, newDbItem) } async function listAsRawFiles(): Promise { @@ -142,7 +142,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: duplicate, listAsRawFiles, load, - selectById, + selectByFsPath, unselect, } }) diff --git a/src/app/src/composables/useDraftMedias.ts b/src/app/src/composables/useDraftMedias.ts index 7d01e4e5..dcfec8dc 100644 --- a/src/app/src/composables/useDraftMedias.ts +++ b/src/app/src/composables/useDraftMedias.ts @@ -1,6 +1,6 @@ import { joinURL, withLeadingSlash } from 'ufo' import type { DraftItem, StudioHost, MediaItem, RawFile } from '../types' -import { TreeRootId } from '../types' +import { VirtualMediaCollectionName } from '../types/media' import { DraftStatus } from '../types/draft' import type { useGit } from './useGit' import { createSharedComposable } from '@vueuse/core' @@ -22,15 +22,15 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret remove, revert, revertAll, - selectById, + selectByFsPath, unselect, load, } = useDraftBase('media', host, git, storage) async function upload(parentFsPath: string, file: File) { const draftItem = await fileToDraftItem(parentFsPath, file) - host.media.upsert(draftItem.id, draftItem.modified!) - await create(draftItem.modified!) + host.media.upsert(draftItem.fsPath, draftItem.modified!) + await create(draftItem.fsPath, draftItem.modified!) } async function fileToDraftItem(parentFsPath: string, file: File): Promise> { @@ -39,49 +39,49 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret const fsPath = parentFsPath !== '/' ? joinURL(parentFsPath, slugifiedFileName) : slugifiedFileName return { - id: joinURL(TreeRootId.Media, fsPath), fsPath, githubFile: undefined, status: DraftStatus.Created, modified: { - id: joinURL(TreeRootId.Media, fsPath), + id: joinURL(VirtualMediaCollectionName, fsPath), fsPath, extension: getFileExtension(fsPath), - stem: fsPath.split('.').join('.'), + stem: generateStemFromFsPath(fsPath), path: withLeadingSlash(fsPath), raw: rawData, }, } } - async function rename(items: { id: string, newFsPath: string }[]) { + async function rename(items: { fsPath: string, newFsPath: string }[]) { for (const item of items) { - const { id, newFsPath } = item + const { fsPath, newFsPath } = item - const existingDraftToRename = list.value.find(draftItem => draftItem.id === id) as DraftItem + const existingDraftToRename = list.value.find(draftItem => draftItem.fsPath === fsPath) as DraftItem - const currentDbItem = await host.media.get(id) + const currentDbItem = await host.media.get(fsPath) if (!currentDbItem) { - throw new Error(`Database item not found for document ${id}`) + throw new Error(`Database item not found for document fsPath: ${fsPath}`) } - await remove([id], { rerender: false }) + await remove([fsPath], { rerender: false }) - const newDbItem: MediaItem = { + const newDbItem: MediaItem & { fsPath: string } = { ...currentDbItem, - id: joinURL(TreeRootId.Media, newFsPath), + fsPath: newFsPath, + id: joinURL(VirtualMediaCollectionName, newFsPath), stem: generateStemFromFsPath(newFsPath), path: withLeadingSlash(newFsPath), } - await host.media.upsert(newDbItem.id, newDbItem) + await host.media.upsert(newFsPath, newDbItem) let originalDbItem: MediaItem | undefined = currentDbItem if (existingDraftToRename) { originalDbItem = existingDraftToRename.original } - await create(newDbItem, originalDbItem, { rerender: false }) + await create(newFsPath, newDbItem, originalDbItem, { rerender: false }) } await hooks.callHook('studio:draft:media:updated', { caller: 'useDraftMedias.rename' }) @@ -128,7 +128,7 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret revertAll, rename, load, - selectById, + selectByFsPath, unselect, upload, listAsRawFiles, diff --git a/src/app/src/composables/useStudio.ts b/src/app/src/composables/useStudio.ts index 2183ea82..4b7cc535 100644 --- a/src/app/src/composables/useStudio.ts +++ b/src/app/src/composables/useStudio.ts @@ -88,19 +88,18 @@ function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType { - const item = draftDocuments.list.value.find(item => item.id === id) + host.on.documentUpdate(async (fsPath: string, type: 'remove' | 'update') => { + const item = draftDocuments.list.value.find(item => item.fsPath === fsPath) if (type === 'remove') { if (item) { - await draftDocuments.remove([id]) + await draftDocuments.remove([fsPath]) } } else if (item) { - const fsPath = host.document.getFileSystemPath(id) // Update draft if the document is not focused or the current item is not the one that was updated if (!window.document.hasFocus() || documentTree.currentItem.value?.fsPath !== fsPath) { - const document = await host.document.get(id) + const document = await host.document.get(fsPath) item.modified = document item.original = document item.status = getDraftStatus(document, item.original) @@ -111,18 +110,17 @@ function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType { - const item = draftMedias.list.value.find(item => item.id === id) + host.on.mediaUpdate(async (fsPath: string, type: 'remove' | 'update') => { + const item = draftMedias.list.value.find(item => item.fsPath === fsPath) if (type === 'remove') { if (item) { - await draftMedias.remove([id]) + await draftMedias.remove([fsPath]) } } else if (item) { - const fsPath = host.media.getFileSystemPath(id) if (!window.document.hasFocus() || mediaTree.currentItem.value?.fsPath !== fsPath) { - const media = await host.media.get(id) + const media = await host.media.get(fsPath) item.modified = media item.original = media item.status = getDraftStatus(media, item.original) diff --git a/src/app/src/composables/useTree.ts b/src/app/src/composables/useTree.ts index d3e5a54d..6f885672 100644 --- a/src/app/src/composables/useTree.ts +++ b/src/app/src/composables/useTree.ts @@ -1,8 +1,9 @@ -import { StudioFeature, TreeStatus, type StudioHost, type TreeItem, DraftStatus } from '../types' +import type { DatabaseItem, StudioHost, TreeItem } from '../types' +import { StudioFeature, TreeStatus, DraftStatus } from '../types' import { ref, computed } from 'vue' import type { useDraftDocuments } from './useDraftDocuments' import type { useDraftMedias } from './useDraftMedias' -import { buildTree, findItemFromFsPath, findItemFromRoute, findParentFromFsPath, generateIdFromFsPath } from '../utils/tree' +import { buildTree, findItemFromFsPath, findItemFromRoute, findParentFromFsPath } from '../utils/tree' import type { RouteLocationNormalized } from 'vue-router' import { useHooks } from './useHooks' import { useStudioState } from './useStudioState' @@ -51,7 +52,7 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType setLocation(type, currentItem.value.fsPath) if (item?.type === 'file') { - await draft.selectById(generateIdFromFsPath(item.fsPath, item.collections![0])) + await draft.selectByFsPath(item.fsPath) if ( !preferences.value.syncEditorAndRoute @@ -98,13 +99,9 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType // Trigger tree rebuild to update files status async function handleDraftUpdate(selectItem: boolean = true) { const api = type === StudioFeature.Content ? host.document : host.media - const list = await api.list() - const listWithFsPath = list.map((item) => { - const fsPath = api.getFileSystemPath(item.id) - return { ...item, fsPath } - }) + const list = await api.list() as DatabaseItem[] - tree.value = buildTree(listWithFsPath, draft.list.value) + tree.value = buildTree(list, draft.list.value) // Reselect current item to update status if (selectItem) { diff --git a/src/app/src/pages/review.vue b/src/app/src/pages/review.vue index e3199618..588f7a67 100644 --- a/src/app/src/pages/review.vue +++ b/src/app/src/pages/review.vue @@ -79,7 +79,7 @@ const statusConfig = {