) => {
const hooks = useHooks()
@@ -22,7 +22,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
})
@@ -53,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
@@ -100,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 = {
{
})
function fetchFromIndexedDB(event, url) {
- const dbKey = ['public-assets:', url.replace(/^\\//g, '').replace(/\\//g, ':')].join('')
+ const dbKey = url.replace(/^\\//g, '').replace(/\\//g, ':')
return getData(dbKey).then(data => {
if (!data) {
return fetch(event.request);
diff --git a/src/app/src/shared.ts b/src/app/src/shared.ts
index e25b309b..69035383 100644
--- a/src/app/src/shared.ts
+++ b/src/app/src/shared.ts
@@ -1 +1,2 @@
export { generateContentFromDocument, generateDocumentFromContent, removeReservedKeysFromDocument } from './utils/content'
+export { VirtualMediaCollectionName } from './utils/media'
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..17cc98ff 100644
--- a/src/app/src/types/index.ts
+++ b/src/app/src/types/index.ts
@@ -28,8 +28,8 @@ export interface StudioHost {
beforeUnload: (fn: (event: BeforeUnloadEvent) => void) => void
colorModeChange: (fn: (colorMode: 'light' | 'dark') => void) => void
manifestUpdate: (fn: (id: string) => void) => void
- documentUpdate: (fn: (id: string, type: 'remove' | 'update') => void) => void
- mediaUpdate: (fn: (id: string, type: 'remove' | 'update') => void) => void
+ documentUpdate: (fn: (fsPath: string, type: 'remove' | 'update') => void) => void
+ mediaUpdate: (fn: (fsPath: string, type: 'remove' | 'update') => void) => void
}
ui: {
colorMode: 'light' | 'dark'
@@ -41,20 +41,18 @@ 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
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/item.ts b/src/app/src/types/item.ts
index 8c0325fb..b804abea 100644
--- a/src/app/src/types/item.ts
+++ b/src/app/src/types/item.ts
@@ -1,5 +1,6 @@
export interface BaseItem {
id: string
+ fsPath: string
extension: string
stem: string
path?: string
diff --git a/src/app/src/types/tree.ts b/src/app/src/types/tree.ts
index aa4a6fd8..acd95d7e 100644
--- a/src/app/src/types/tree.ts
+++ b/src/app/src/types/tree.ts
@@ -1,8 +1,3 @@
-export enum TreeRootId {
- Content = 'content',
- Media = 'public-assets',
-}
-
export enum TreeStatus {
Deleted = 'deleted',
Created = 'created',
@@ -13,10 +8,9 @@ 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
- collections: string[]
status?: TreeStatus
routePath?: string
children?: TreeItem[]
diff --git a/src/app/src/utils/content.ts b/src/app/src/utils/content.ts
index e37d6500..fe0bcca6 100644
--- a/src/app/src/utils/content.ts
+++ b/src/app/src/utils/content.ts
@@ -11,7 +11,7 @@ import type { MarkdownRoot } from '@nuxt/content'
import { destr } from 'destr'
import { getFileExtension } from './file'
-const reservedKeys = ['id', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody']
+const reservedKeys = ['id', 'fsPath', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody']
export function generateStemFromId(id: string) {
return id.split('/').slice(1).join('/').split('.').slice(0, -1).join('.')
@@ -105,7 +105,7 @@ async function generateDocumentFromYAMLContent(id: string, content: string): Pro
meta: {},
...parsed,
body: parsed.body || parsed,
- } as DatabaseItem
+ } as never as DatabaseItem
}
async function generateDocumentFromJSONContent(id: string, content: string): Promise {
@@ -119,6 +119,7 @@ async function generateDocumentFromJSONContent(id: string, content: string): Pro
}
}
+ // fsPath will be overridden by host
return {
id,
extension: ContentFileExtension.JSON,
@@ -126,7 +127,7 @@ async function generateDocumentFromJSONContent(id: string, content: string): Pro
meta: {},
...parsed,
body: parsed.body || parsed,
- } as DatabaseItem
+ } as never as DatabaseItem
}
async function generateDocumentFromMarkdownContent(id: string, content: string): Promise {
@@ -161,7 +162,7 @@ async function generateDocumentFromMarkdownContent(id: string, content: string):
toc: document.toc,
},
...document.data,
- } as DatabaseItem
+ } as never as DatabaseItem
}
export async function generateContentFromDocument(document: DatabaseItem): Promise {
diff --git a/src/app/src/utils/context.ts b/src/app/src/utils/context.ts
index 3f5af2f8..e7825b98 100644
--- a/src/app/src/utils/context.ts
+++ b/src/app/src/utils/context.ts
@@ -1,4 +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 +61,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/src/utils/draft.ts b/src/app/src/utils/draft.ts
index 47693b19..3728641f 100644
--- a/src/app/src/utils/draft.ts
+++ b/src/app/src/utils/draft.ts
@@ -1,12 +1,13 @@
import type { DatabaseItem, MediaItem, DatabasePageItem, DraftItem, BaseItem, ContentConflict } from '../types'
-import { DraftStatus, ContentFileExtension, TreeRootId } from '../types'
+import { DraftStatus, ContentFileExtension } from '../types'
import { isEqual } from './database'
import { studioFlags } from '../composables/useStudio'
import { generateContentFromDocument, generateDocumentFromContent } from './content'
import { fromBase64ToUTF8 } from '../utils/string'
+import { isMediaFile } from './file'
export async function checkConflict(draftItem: DraftItem): Promise {
- if (draftItem.id.startsWith(TreeRootId.Media)) {
+ if (isMediaFile(draftItem.fsPath) || draftItem.fsPath.endsWith('.gitkeep')) {
return
}
@@ -28,7 +29,7 @@ export async function checkConflict(draftItem: DraftItem()
@@ -37,25 +36,26 @@ TreeItem[] {
const deletedDraftItems = draftList?.filter(draft => draft.status === DraftStatus.Deleted) || []
const createdDraftItems = draftList?.filter(draft => draft.status === DraftStatus.Created) || []
- function addDeletedDraftItemsInDbItems(dbItems: ((BaseItem) & { fsPath: string })[], deletedItems: DraftItem[]) {
+ function addDeletedDraftItemsInDbItems(dbItems: BaseItem[], 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 = {
+ 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 +80,6 @@ TreeItem[] {
fsPath: dbItem.fsPath,
type: 'file',
prefix,
- collections: [dbItem.id.split('/')[0]],
}
if (dbItem.fsPath.endsWith('.gitkeep')) {
@@ -91,7 +90,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 +120,6 @@ TreeItem[] {
type: 'directory',
children: [],
prefix: dirPrefix,
- collections: [dbItem.id.split('/')[0]],
}
directoryMap.set(dirFsPath, directory)
@@ -130,12 +128,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 +141,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!)
}
@@ -173,10 +164,6 @@ TreeItem[] {
return tree
}
-export function generateIdFromFsPath(fsPath: string, collectionName: string): string {
- return joinURL(collectionName, fsPath)
-}
-
export function getTreeStatus(modified?: BaseItem, original?: BaseItem): TreeStatus {
if (studioFlags.dev) {
return TreeStatus.Opened
diff --git a/src/app/test/integration/actions.test.ts b/src/app/test/integration/actions.test.ts
index 5813870f..4d10aa4c 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,20 +95,19 @@ 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',
- value: ['Test content'],
- })
+ expect(storedDraft.modified).toHaveProperty('fsPath', documentFsPath)
+ expect(JSON.stringify(storedDraft.modified.body)).toContain('Test content')
expect(storedDraft.original).toBeUndefined()
// 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].modified).toHaveProperty('fsPath', documentFsPath)
expect(context.activeTree.value.draft.list.value[0].original).toBeUndefined()
// Tree
@@ -145,32 +141,32 @@ 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.modified).toHaveProperty('fsPath', newFsPath)
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].modified).toHaveProperty('fsPath', newFsPath)
expect(list[0].original).toBeUndefined()
- expect(list[0].modified).toHaveProperty('id', newId)
// Tree
expect(context.activeTree.value.root.value[0]).toHaveProperty('fsPath', newFsPath)
@@ -192,29 +188,33 @@ 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('fsPath', documentFsPath)
expect(storedDraft.modified).toHaveProperty('body', updatedDocument.body)
expect(storedDraft.original).toBeUndefined()
// 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].modified).toHaveProperty('fsPath', documentFsPath)
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,45 +250,57 @@ 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.modified).toHaveProperty('fsPath', documentFsPath)
expect(selectedDraft.original).toHaveProperty('id', documentId)
+ expect(selectedDraft.original).toHaveProperty('fsPath', documentFsPath)
// 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].modified).toHaveProperty('fsPath', documentFsPath)
expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId)
+ expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('fsPath', documentFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
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('fsPath', documentFsPath)
expect(storedDraft.modified).toHaveProperty('body', updatedDocument.body)
expect(storedDraft.original).toHaveProperty('id', documentId)
+ expect(storedDraft.original).toHaveProperty('fsPath', documentFsPath)
// 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].modified).toHaveProperty('fsPath', documentFsPath)
+ expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId)
+ expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('fsPath', documentFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -300,16 +312,22 @@ 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.modified).toHaveProperty('fsPath', documentFsPath)
expect(revertedDraft.original).toHaveProperty('id', documentId)
+ expect(revertedDraft.original).toHaveProperty('fsPath', documentFsPath)
// 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].modified).toHaveProperty('fsPath', documentFsPath)
+ expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('id', documentId)
+ expect(context.activeTree.value.draft.list.value[0].original).toHaveProperty('fsPath', documentFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -334,22 +352,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,18 +377,21 @@ 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.original).toHaveProperty('fsPath', documentFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', newId)
- expect(createdDraftStorage.modified).toHaveProperty('body', updatedDocument.body)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
+ expect(JSON.stringify(createdDraftStorage.modified.body)).toContain('Updated content')
// 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.original).toHaveProperty('fsPath', documentFsPath)
expect(deletedDraftStorage.modified).toBeUndefined()
// Memory
@@ -377,15 +399,18 @@ 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].original).toHaveProperty('fsPath', documentFsPath)
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].original).toHaveProperty('fsPath', documentFsPath)
expect(list[1].modified).toHaveProperty('id', newId)
- expect(list[1].modified).toHaveProperty('body', updatedDocument.body)
+ expect(list[1].modified).toHaveProperty('fsPath', newFsPath)
+ expect(JSON.stringify(list[1].modified!.body)).toContain('Updated content')
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -410,13 +435,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,80 +449,94 @@ 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.original).toHaveProperty('fsPath', documentFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', newId)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
// 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.original).toHaveProperty('fsPath', documentFsPath)
expect(deletedDraftStorage.modified).toBeUndefined()
// Memory
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!.original).toHaveProperty('fsPath', documentFsPath)
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!.original).toHaveProperty('fsPath', documentFsPath)
expect(createdDraftMemory!.modified).toHaveProperty('id', newId)
+ expect(createdDraftMemory!.modified).toHaveProperty('fsPath', newFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
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.original).toHaveProperty('fsPath', documentFsPath)
expect(updatedDraftStorage.modified).toHaveProperty('id', newId)
+ expect(updatedDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
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)
+ expect(deletedDraftStorage.original).toHaveProperty('fsPath', documentFsPath)
// 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!.original).toHaveProperty('fsPath', documentFsPath)
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!.original).toHaveProperty('fsPath', documentFsPath)
expect(updatedDraftMemory!.modified).toHaveProperty('id', newId)
+ expect(updatedDraftMemory!.modified).toHaveProperty('fsPath', newFsPath)
expect(updatedDraftMemory!.modified).toHaveProperty('body', updatedDocument.body)
// Tree
@@ -525,13 +563,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,19 +581,24 @@ 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.modified).toHaveProperty('fsPath', documentFsPath)
expect(openedDraftStorage.original).toHaveProperty('id', documentId)
+ expect(openedDraftStorage.original).toHaveProperty('fsPath', documentFsPath)
// 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', documentId)
+ expect(list[0]).toHaveProperty('fsPath', documentFsPath)
expect(list[0].modified).toHaveProperty('id', documentId)
+ expect(list[0].modified).toHaveProperty('fsPath', documentFsPath)
expect(list[0].original).toHaveProperty('id', documentId)
+ expect(list[0].original).toHaveProperty('fsPath', documentFsPath)
+ expect(list[0].original).toHaveProperty('fsPath', documentFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -581,25 +623,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 +647,39 @@ 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.original).toHaveProperty('fsPath', documentFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', newId2)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath2)
- // 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.original).toHaveProperty('fsPath', documentFsPath)
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.original).toHaveProperty('fsPath', documentFsPath)
expect(createdDraftMemory.modified).toHaveProperty('id', newId2)
+ expect(createdDraftMemory.modified).toHaveProperty('fsPath', newFsPath2)
- // 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 +699,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,19 +725,21 @@ 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)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', mediaFsPath)
// Memory
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)
+ expect(createdDraftMemory.modified).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -728,32 +775,33 @@ 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)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
// 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].original).toBeUndefined()
expect(list[0].modified).toHaveProperty('id', newId)
+ expect(list[0].modified).toHaveProperty('fsPath', newFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -770,7 +818,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,19 +826,23 @@ 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.original).toHaveProperty('fsPath', mediaFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', mediaId)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', mediaFsPath)
// Memory
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.original).toHaveProperty('fsPath', mediaFsPath)
expect(createdDraftMemory.modified).toHaveProperty('id', mediaId)
+ expect(createdDraftMemory.modified).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -802,19 +854,21 @@ 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)
+ expect(deletedDraftStorage.original).toHaveProperty('fsPath', mediaFsPath)
// Memory
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)
+ expect(deletedDraftMemory.original).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -826,19 +880,23 @@ 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.modified).toHaveProperty('fsPath', mediaFsPath)
expect(revertedDraftStorage.original).toHaveProperty('id', mediaId)
+ expect(revertedDraftStorage.original).toHaveProperty('fsPath', mediaFsPath)
// 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].modified).toHaveProperty('fsPath', mediaFsPath)
expect(list[0].original).toHaveProperty('id', mediaId)
+ expect(list[0].original).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -856,19 +914,19 @@ 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] })
+ const mediaDbItem = createMockMedia(mediaId)
+ await mockHost.media.upsert(mediaFsPath, mediaDbItem)
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,35 +935,42 @@ 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.original).toHaveProperty('fsPath', mediaFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', newId)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath)
// 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)
+ expect(deletedDraftStorage.original).toHaveProperty('fsPath', mediaFsPath)
// Memory
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.modified).toHaveProperty('fsPath', newFsPath)
expect(createdDraftMemory.original).toHaveProperty('id', mediaId)
+ expect(createdDraftMemory.original).toHaveProperty('fsPath', mediaFsPath)
// 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)
+ expect(deletedDraftMemory.original).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -918,19 +983,23 @@ 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.modified).toHaveProperty('fsPath', mediaFsPath)
expect(revertedDraftStorage.original).toHaveProperty('id', mediaId)
+ expect(revertedDraftStorage.original).toHaveProperty('fsPath', mediaFsPath)
// 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]).toHaveProperty('fsPath', mediaFsPath)
expect(list[0].modified).toHaveProperty('id', mediaId)
+ expect(list[0].modified).toHaveProperty('fsPath', mediaFsPath)
expect(list[0].original).toHaveProperty('id', mediaId)
+ expect(list[0].original).toHaveProperty('fsPath', mediaFsPath)
// Tree
expect(context.activeTree.value.root.value).toHaveLength(1)
@@ -948,31 +1017,30 @@ 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] })
+ const mediaDbItem = createMockMedia(mediaId)
+ await mockHost.media.upsert(mediaFsPath, mediaDbItem)
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,35 +1049,41 @@ 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.original).toHaveProperty('fsPath', mediaFsPath)
expect(createdDraftStorage.modified).toHaveProperty('id', newId2)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', newFsPath2)
// 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)
+ expect(deletedDraftStorage.original).toHaveProperty('fsPath', mediaFsPath)
// Memory
const list = context.activeTree.value.draft.list.value
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.modified).toHaveProperty('fsPath', newFsPath2)
expect(createdDraftMemory.original).toHaveProperty('id', mediaId)
+ expect(createdDraftMemory.original).toHaveProperty('fsPath', mediaFsPath)
// 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.original).toHaveProperty('fsPath', mediaFsPath)
expect(deletedDraftMemory.modified).toBeUndefined()
// Tree
@@ -1024,12 +1098,12 @@ describe('Media - Action Chains Integration Tests', () => {
expect(consoleInfoSpy).toHaveBeenCalledWith('studio:draft:media:updated have been called by', 'useDraftMedias.rename')
})
- it('CreateFolder > Upload > Revert Media', async () => {
+ it('CreateMediaFolder > Upload > Revert Media', async () => {
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 folderPath = folderName
+ const gitkeepFsPath = joinURL(folderPath, '.gitkeep')
+ const gitkeepId = fsPathToId(gitkeepFsPath, 'media')
/* STEP 1: CREATE FOLDER */
await context.itemActionHandler[StudioItemActionId.CreateMediaFolder]({
@@ -1038,18 +1112,20 @@ 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.modified).toHaveProperty('fsPath', gitkeepFsPath)
expect(gitkeepDraftStorage.original).toBeUndefined()
// Memory
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.modified).toHaveProperty('fsPath', gitkeepFsPath)
expect(gitkeepDraftMemory.original).toBeUndefined()
// Tree - .gitkeep file exists but is hidden
@@ -1063,8 +1139,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,18 +1148,20 @@ 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)
+ expect(createdDraftStorage.modified).toHaveProperty('fsPath', uploadedMediaFsPath)
// 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.modified).toHaveProperty('fsPath', uploadedMediaFsPath)
expect(createdDraftMemory.original).toBeUndefined()
// Tree - .gitkeep has been removed
@@ -1117,7 +1195,7 @@ describe('Media - Action Chains Integration Tests', () => {
it('CreateMediaFolder > Upload > Revert Media', async () => {
const consoleInfoSpy = vi.spyOn(console, 'info')
const folderName = 'media-folder'
- const folderPath = `/${folderName}`
+ const folderPath = folderName
/* STEP 1: CREATE MEDIA FOLDER */
await context.itemActionHandler[StudioItemActionId.CreateMediaFolder]({
diff --git a/src/app/test/mocks/database.ts b/src/app/test/mocks/database.ts
index 7ff73213..7b8a83ba 100644
--- a/src/app/test/mocks/database.ts
+++ b/src/app/test/mocks/database.ts
@@ -1,6 +1,6 @@
import type { DatabaseItem } from '../../src/types/database'
-export const dbItemsList: (DatabaseItem & { fsPath: string })[] = [
+export const dbItemsList: DatabaseItem[] = [
{
id: 'landing/index.md',
title: '',
@@ -60,7 +60,7 @@ export const dbItemsList: (DatabaseItem & { fsPath: string })[] = [
},
]
-export const nestedDbItemsList: (DatabaseItem & { fsPath: string })[] = [
+export const nestedDbItemsList: DatabaseItem[] = [
{
id: 'docs/1.essentials/2.configuration.md',
title: 'Configuration',
@@ -101,7 +101,7 @@ export const nestedDbItemsList: (DatabaseItem & { fsPath: string })[] = [
},
]
-export const languagePrefixedDbItemsList: (DatabaseItem & { fsPath: string })[] = [
+export const languagePrefixedDbItemsList: DatabaseItem[] = [
{
id: 'landing_en/en/index.md',
title: '',
diff --git a/src/app/test/mocks/document.ts b/src/app/test/mocks/document.ts
index 93c6efbe..ef222f22 100644
--- a/src/app/test/mocks/document.ts
+++ b/src/app/test/mocks/document.ts
@@ -1,14 +1,20 @@
import type { DatabasePageItem } from '../../src/types'
+import { idToFsPath } from './host'
-export const createMockDocument = (id: string, overrides?: Partial) => ({
- id,
- path: `${id.split('/').pop()?.replace('.md', '')}`,
- stem: id.split('/').pop()?.replace('.md', '') || 'document',
- extension: 'md',
- body: {
- type: 'minimark',
- value: ['Test content'],
- },
- meta: {},
- ...overrides,
-})
+export const createMockDocument = (id: string, overrides?: Partial) => {
+ const fsPath = idToFsPath(id)
+ const path = fsPath.replace('.md', '')
+ return {
+ id,
+ fsPath,
+ path,
+ stem: path,
+ extension: 'md',
+ body: {
+ type: 'minimark',
+ value: ['Test content'],
+ },
+ meta: {},
+ ...overrides,
+ }
+}
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..cf9e9963 100644
--- a/src/app/test/mocks/host.ts
+++ b/src/app/test/mocks/host.ts
@@ -1,13 +1,23 @@
-import { type StudioHost, TreeRootId, type DatabaseItem } from '../../src/types'
+import type { StudioHost, DatabaseItem } from '../../src/types'
+import { VirtualMediaCollectionName } from '../../src/utils/media'
import { vi } from 'vitest'
import { createMockDocument } from './document'
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(VirtualMediaCollectionName, fsPath)
+ }
+ // For documents, prefix with a collection name
+ return joinURL('docs', fsPath)
+}
+
+// Helper to convert id back to fsPath (simulates module's internal mapping)
+export const idToFsPath = (id: string) => {
+ return id.split('/').slice(1).join('/')
}
const documentDb = new Map()
@@ -15,7 +25,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,30 +35,26 @@ 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 document = createMockDocument(id, {
- body: {
- type: 'minimark',
- value: [content?.trim() || 'Test content'],
- },
- })
+ const id = fsPathToId(fsPath, 'document')
+ const document = createMockDocument(id, { body: { type: 'minimark', value: [content?.trim() || 'Test content'] }, fsPath })
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,
},
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 +63,19 @@ 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,
list: vi.fn().mockImplementation(async () => {
return Array.from(mediaDb.values())
}),
diff --git a/src/app/test/mocks/media.ts b/src/app/test/mocks/media.ts
index 682b45e0..8a08531b 100644
--- a/src/app/test/mocks/media.ts
+++ b/src/app/test/mocks/media.ts
@@ -16,11 +16,13 @@ export const createMockFile = (name: string, overrides?: Partial): File =>
}
export const createMockMedia = (id: string, overrides?: Partial): MediaItem => {
+ const fsPath = id.split('/').slice(1).join('/')
const extension = id.split('.').pop()!
const stem = id.split('.').slice(0, -1).join('.')
return {
id,
+ fsPath,
stem,
extension,
...overrides,
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..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, TreeRootId } 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([])
})
/**************************************************
@@ -18,10 +30,9 @@ describe('computeItemActions', () => {
name: 'content',
fsPath: '/',
prefix: null,
- collections: [TreeRootId.Content],
}
- 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
@@ -40,10 +51,9 @@ describe('computeItemActions', () => {
name: 'media',
fsPath: '/',
prefix: null,
- collections: [TreeRootId.Media],
}
- 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,10 +74,9 @@ describe('computeItemActions', () => {
fsPath: '/',
prefix: null,
status: TreeStatus.Updated,
- collections: [TreeRootId.Media],
}
- 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
@@ -89,10 +98,9 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
}
- 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
@@ -110,11 +118,10 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -132,11 +139,10 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -153,11 +159,10 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -174,11 +179,10 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -198,11 +202,10 @@ describe('computeItemActions', () => {
name: 'test.md',
fsPath: 'test.md',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -223,10 +226,9 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
}
- 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
@@ -244,11 +246,10 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -265,11 +266,10 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -285,11 +285,10 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -305,11 +304,10 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -327,11 +325,10 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Content],
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
@@ -347,10 +344,9 @@ describe('computeItemActions', () => {
name: 'folder',
fsPath: 'folder',
prefix: null,
- collections: [TreeRootId.Media],
}
- 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
diff --git a/src/app/test/unit/utils/draft.test.ts b/src/app/test/unit/utils/draft.test.ts
index 1afd9e29..8975ffd8 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'
+import { DraftStatus } 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, '/')
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..b923ce3b 100644
--- a/src/app/test/unit/utils/tree.test.ts
+++ b/src/app/test/unit/utils/tree.test.ts
@@ -1,14 +1,15 @@
import { describe, it, expect } from 'vitest'
-import { buildTree, findParentFromFsPath, findItemFromRoute, findItemFromFsPath, findDescendantsFileItemsFromFsPath, getTreeStatus, generateIdFromFsPath } from '../../../src/utils/tree'
+import { buildTree, findParentFromFsPath, findItemFromRoute, findItemFromFsPath, findDescendantsFileItemsFromFsPath, getTreeStatus } from '../../../src/utils/tree'
import { tree } from '../../../test/mocks/tree'
import type { TreeItem } from '../../../src/types/tree'
import { dbItemsList, languagePrefixedDbItemsList, nestedDbItemsList } from '../../../test/mocks/database'
import type { DraftItem } from '../../../src/types/draft'
import type { MediaItem } from '../../../src/types'
-import { DraftStatus, TreeStatus, TreeRootId } from '../../../src/types'
+import { DraftStatus, TreeStatus } from '../../../src/types'
import type { RouteLocationNormalized } from 'vue-router'
import type { DatabaseItem } from '../../../src/types/database'
import { joinURL, withLeadingSlash } from 'ufo'
+import { VirtualMediaCollectionName } from '../../../src/utils/media'
describe('buildTree of documents with one level of depth', () => {
// Result based on dbItemsList mock
@@ -19,14 +20,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 +33,6 @@ describe('buildTree of documents with one level of depth', () => {
type: 'file',
routePath: '/getting-started/introduction',
prefix: 2,
- collections: ['docs'],
},
{
name: 'installation',
@@ -42,7 +40,6 @@ describe('buildTree of documents with one level of depth', () => {
type: 'file',
routePath: '/getting-started/installation',
prefix: 3,
- collections: ['docs'],
},
],
},
@@ -54,10 +51,9 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With draft', () => {
- const createdDbItem: DatabaseItem & { fsPath: string } = dbItemsList[0]
+ const createdDbItem: DatabaseItem = dbItemsList[0]
const draftList: DraftItem[] = [{
- id: createdDbItem.id,
fsPath: createdDbItem.fsPath,
status: DraftStatus.Created,
original: undefined,
@@ -75,10 +71,9 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With DELETED draft file in existing directory', () => {
- const deletedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md
+ const deletedDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md
const draftList: DraftItem[] = [{
- id: deletedDbItem.id,
fsPath: deletedDbItem.fsPath,
status: DraftStatus.Deleted,
modified: undefined,
@@ -103,7 +98,6 @@ describe('buildTree of documents with one level of depth', () => {
routePath: deletedDbItem.path,
status: TreeStatus.Deleted,
prefix: 2,
- collections: ['docs'],
},
],
},
@@ -111,10 +105,9 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With DELETED draft file in non existing directory', () => {
- const deletedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md
+ const deletedDbItem: DatabaseItem = dbItemsList[2] // 3.installation.md
const draftList: DraftItem[] = [{
- id: deletedDbItem.id,
fsPath: deletedDbItem.fsPath,
status: DraftStatus.Deleted,
modified: undefined,
@@ -139,7 +132,6 @@ describe('buildTree of documents with one level of depth', () => {
routePath: deletedDbItem.path,
status: TreeStatus.Deleted,
prefix: 3,
- collections: ['docs'],
},
],
},
@@ -147,10 +139,9 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With UPDATED draft file in existing directory (directory status is set)', () => {
- const updatedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md
+ const updatedDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md
const draftList: DraftItem[] = [{
- id: updatedDbItem.id,
fsPath: updatedDbItem.fsPath,
status: DraftStatus.Updated,
original: updatedDbItem,
@@ -184,17 +175,15 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With CREATED and OPENED draft files in exsiting directory (directory status is set)', () => {
- const createdDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md
- const openedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md
+ const createdDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md
+ const openedDbItem: DatabaseItem = 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,
@@ -219,17 +208,15 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With OPENED draft files in existing directory (directory status is not set)', () => {
- const openedDbItem1: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md
- const openedDbItem2: DatabaseItem & { fsPath: string } = dbItemsList[2] // 3.installation.md
+ const openedDbItem1: DatabaseItem = dbItemsList[1] // 2.introduction.md
+ const openedDbItem2: DatabaseItem = 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,
@@ -255,8 +242,8 @@ describe('buildTree of documents with one level of depth', () => {
})
it('With same id DELETED and CREATED draft file resulting in RENAMED', () => {
- const deletedDbItem: DatabaseItem & { fsPath: string } = dbItemsList[1] // 2.introduction.md
- const createdDbItem: DatabaseItem & { fsPath: string } = { // 2.renamed.md
+ const deletedDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md
+ const createdDbItem: DatabaseItem = { // 2.renamed.md
...dbItemsList[1],
id: 'docs/1.getting-started/2.renamed.md',
path: '/getting-started/renamed',
@@ -265,13 +252,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 +283,6 @@ describe('buildTree of documents with one level of depth', () => {
type: 'file',
status: TreeStatus.Renamed,
prefix: 2,
- collections: ['docs'],
},
],
},
@@ -313,7 +297,6 @@ describe('buildTree of documents with two levels of depth', () => {
fsPath: '1.essentials',
type: 'directory',
prefix: 1,
- collections: ['docs'],
children: [
{
name: 'configuration',
@@ -321,14 +304,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 +317,6 @@ describe('buildTree of documents with two levels of depth', () => {
type: 'file',
routePath: '/essentials/nested/advanced',
prefix: 2,
- collections: ['docs'],
},
],
},
@@ -350,10 +330,9 @@ describe('buildTree of documents with two levels of depth', () => {
})
it('With one level of depth draft files', () => {
- const updatedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[0] // 1.essentials/2.configuration.md
+ const updatedDbItem: DatabaseItem = nestedDbItemsList[0] // 1.essentials/2.configuration.md
const draftList: DraftItem[] = [{
- id: updatedDbItem.id,
fsPath: updatedDbItem.fsPath,
status: DraftStatus.Updated,
original: updatedDbItem,
@@ -379,10 +358,9 @@ describe('buildTree of documents with two levels of depth', () => {
})
it('With nested levels of depth draft files', () => {
- const updatedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md
+ const updatedDbItem: DatabaseItem = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md
const draftList: DraftItem[] = [{
- id: updatedDbItem.id,
fsPath: updatedDbItem.fsPath,
status: DraftStatus.Updated,
original: updatedDbItem,
@@ -417,10 +395,9 @@ describe('buildTree of documents with two levels of depth', () => {
})
it ('With DELETED draft file in nested non existing directory (directory status is set)', () => {
- const deletedDbItem: DatabaseItem & { fsPath: string } = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md
+ const deletedDbItem: DatabaseItem = 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 +425,6 @@ describe('buildTree of documents with two levels of depth', () => {
type: 'file',
status: TreeStatus.Deleted,
prefix: 2,
- collections: ['docs'],
},
],
},
@@ -464,7 +440,6 @@ describe('buildTree of documents with language prefixed', () => {
fsPath: 'en',
type: 'directory',
prefix: null,
- collections: ['landing_en', 'docs_en'],
children: [
{
name: 'index',
@@ -472,14 +447,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 +460,6 @@ describe('buildTree of documents with language prefixed', () => {
type: 'file',
routePath: '/en/getting-started/introduction',
prefix: 2,
- collections: ['docs_en'],
},
{
name: 'installation',
@@ -495,7 +467,6 @@ describe('buildTree of documents with language prefixed', () => {
type: 'file',
routePath: '/en/getting-started/installation',
prefix: 3,
- collections: ['docs_en'],
},
],
},
@@ -513,11 +484,11 @@ describe('buildTree of medias', () => {
it('With .gitkeep file in directory (file is marked as hidden)', () => {
const mediaFolderName = 'media-folder'
const gitKeepFsPath = joinURL(mediaFolderName, '.gitkeep')
- const gitKeepId = generateIdFromFsPath(gitKeepFsPath, TreeRootId.Media)
const mediaFsPath = joinURL(mediaFolderName, 'image.jpg')
- const mediaId = generateIdFromFsPath(mediaFsPath, TreeRootId.Media)
+ const mediaId = joinURL(VirtualMediaCollectionName, mediaFsPath)
+ const gitKeepId = joinURL(VirtualMediaCollectionName, gitKeepFsPath)
- const gitkeepDbItem: MediaItem & { fsPath: string } = {
+ const gitkeepDbItem: MediaItem = {
id: gitKeepId,
fsPath: gitKeepFsPath,
stem: '.gitkeep',
@@ -525,7 +496,7 @@ describe('buildTree of medias', () => {
path: withLeadingSlash(gitKeepFsPath),
}
- const mediaDbItem: MediaItem & { fsPath: string } = {
+ const mediaDbItem: MediaItem = {
id: mediaId,
fsPath: mediaFsPath,
stem: 'image',
@@ -534,7 +505,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..c5c1dafe 100644
--- a/src/app/test/utils/index.ts
+++ b/src/app/test/utils/index.ts
@@ -1,4 +1,3 @@
-import { TreeRootId } from '../../src/types'
import { joinURL } from 'ufo'
/**
@@ -17,18 +16,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/build.config.ts b/src/module/build.config.ts
index e49164fd..7118f2d5 100644
--- a/src/module/build.config.ts
+++ b/src/module/build.config.ts
@@ -3,6 +3,7 @@ import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
outDir: '../../dist/module',
externals: [
+ 'ufo',
'defu',
'destr',
'unstorage',
diff --git a/src/module/src/runtime/host.dev.ts b/src/module/src/runtime/host.dev.ts
index 12ba30cc..c50ff3e6 100644
--- a/src/module/src/runtime/host.dev.ts
+++ b/src/module/src/runtime/host.dev.ts
@@ -1,12 +1,14 @@
import { useStudioHost as useStudioHostBase } from './host'
import type { StudioUser, DatabaseItem, Repository } from 'nuxt-studio/app'
import { generateContentFromDocument } from 'nuxt-studio/app/utils'
-import { createCollectionDocument, getCollectionInfo } from './utils/collection'
+import { getCollectionByFilePath, generateIdFromFsPath, generateFsPathFromId, getCollectionById } from './utils/collection'
+import { createCollectionDocument } from './utils/document'
import { createStorage } from 'unstorage'
import httpDriver from 'unstorage/drivers/http'
import { useRuntimeConfig } from '#imports'
import { collections } from '#content/preview'
import { debounce } from 'perfect-debounce'
+import { getCollectionSourceById } from './utils/source'
export function useStudioHost(user: StudioUser, repository: Repository) {
const host = useStudioHostBase(user, repository)
@@ -26,33 +28,44 @@ export function useStudioHost(user: StudioUser, repository: Repository) {
// no operation let hmr do the job
}
- host.document.upsert = debounce(async (id: string, upsertedDocument: DatabaseItem) => {
- id = id.replace(/:/g, '/')
+ // TODO @farnabaz to check
+ host.document.upsert = debounce(async (fsPath: string, upsertedDocument: DatabaseItem) => {
+ const collectionInfo = getCollectionByFilePath(fsPath, collections)
+ if (!collectionInfo) {
+ throw new Error(`Collection not found for fsPath: ${fsPath}`)
+ }
- const collection = getCollectionInfo(id, collections).collection
- const doc = createCollectionDocument(collection, id, upsertedDocument)
+ const id = generateIdFromFsPath(fsPath, collectionInfo)
+ const doc = createCollectionDocument(id, collectionInfo, upsertedDocument)
const content = await generateContentFromDocument(doc)
- await devStorage.setItem(host.document.getFileSystemPath(id), content, {
+ await devStorage.setItem(fsPath, content, {
headers: {
'content-type': 'text/plain',
},
})
}, 100)
- host.document.delete = async (id: string) => {
- await devStorage.removeItem(host.document.getFileSystemPath(id.replace(/:/g, '/')))
+ // TODO @farnabaz to check
+ host.document.delete = async (fsPath: string) => {
+ await devStorage.removeItem(fsPath)
}
+ // TODO @farnabaz
host.on.documentUpdate = (fn: (id: string, type: 'remove' | 'update') => void) => {
// @ts-expect-error import.meta.hot is not defined in types
import.meta.hot.on('nuxt-content:update', (data: { key: string, queries: string[] }) => {
+ const collection = getCollectionById(data.key, collections)
+ const source = getCollectionSourceById(data.key, collection.source)
+ const fsPath = generateFsPathFromId(data.key, source!)
+
const isRemoved = data.queries.length === 0 // In case of update there is one remove and one insert query
- fn(data.key, isRemoved ? 'remove' : 'update')
+ fn(fsPath, isRemoved ? 'remove' : 'update')
})
}
+ // TODO @farnabaz
host.on.mediaUpdate = (fn: (id: string, type: 'remove' | 'update') => void) => {
// @ts-expect-error import.meta.hot is not defined in types
import.meta.hot.on('nuxt-studio:media:update', (data: { type: string, id: string }) => {
diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts
index 826a3dd0..3c77baef 100644
--- a/src/module/src/runtime/host.ts
+++ b/src/module/src/runtime/host.ts
@@ -1,8 +1,9 @@
import { ref } from 'vue'
import { ensure } from './utils/ensure'
-import type { CollectionItemBase, CollectionSource, DatabaseAdapter } from '@nuxt/content'
+import type { CollectionInfo, CollectionItemBase, CollectionSource, DatabaseAdapter } from '@nuxt/content'
import type { ContentDatabaseAdapter } from '../types/content'
-import { getCollectionByFilePath, generateIdFromFsPath, createCollectionDocument, generateRecordDeletion, generateRecordInsert, getCollectionInfo, normalizeDocument } from './utils/collection'
+import { getCollectionByFilePath, generateIdFromFsPath, generateRecordDeletion, generateRecordInsert, generateFsPathFromId, getCollectionById } from './utils/collection'
+import { createCollectionDocument, normalizeDocument } from './utils/document'
import { kebabCase } from 'scule'
import type { StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app'
import type { RouteLocationNormalized, Router } from 'vue-router'
@@ -12,8 +13,10 @@ import { clearError, getAppManifest, queryCollection, queryCollectionItemSurroun
import { collections } from '#content/preview'
import { publicAssetsStorage } from '#build/studio-public-assets'
import { useHostMeta } from './composables/useMeta'
+import { generateIdFromFsPath as generateMediaIdFromFsPath } from './utils/media'
+import { getCollectionSourceById } from './utils/source'
-const serviceWorkerVersion = 'v0.0.1'
+const serviceWorkerVersion = 'v0.0.2'
function getSidebarWidth(): number {
let sidebarWidth = 440
@@ -86,7 +89,7 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
return localDatabaseAdapter!(collection)
}
- function useContentCollections() {
+ function useContentCollections(): Record {
return Object.fromEntries(
Object.entries(useContent().collections).filter(([, collection]) => {
if (!collection.source.length || collection.source.some((source: CollectionSource) => source.repository || (source as unknown as { _custom: boolean })._custom)) {
@@ -182,26 +185,37 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
},
document: {
- get: async (id: string): Promise => {
- const item = await useContentCollectionQuery(id.split('/')[0] as string).where('id', '=', id).first()
+ get: async (fsPath: string): Promise => {
+ const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections())
+ if (!collectionInfo) {
+ throw new Error(`Collection not found for fsPath: ${fsPath}`)
+ }
- return normalizeDocument(item as DatabaseItem)
- },
- getFileSystemPath: (id: string) => {
- return getCollectionInfo(id, useContentCollections()).fsPath
+ const id = generateIdFromFsPath(fsPath, collectionInfo)
+ const item = await useContentCollectionQuery(collectionInfo.name).where('id', '=', id).first()
+
+ return item ? normalizeDocument(fsPath, item as DatabaseItem) : undefined
},
list: async (): Promise => {
- const collections = Object.keys(useContentCollections()).filter(c => c !== 'info')
- const contents = await Promise.all(collections.map(async (collection) => {
- return await useContentCollectionQuery(collection).all() as DatabaseItem[]
+ const collections = Object.values(useContentCollections()).filter(collection => collection.name !== 'info')
+ const documentsByCollection = await Promise.all(collections.map(async (collection) => {
+ const documents = await useContentCollectionQuery(collection.name).all() as DatabaseItem[]
+
+ return documents.map((document) => {
+ const source = getCollectionSourceById(document.id, collection.source)
+ const fsPath = generateFsPathFromId(document.id, source!)
+
+ return normalizeDocument(fsPath, document)
+ })
}))
- return contents.flat()
+ return documentsByCollection.flat()
},
create: async (fsPath: string, content: string) => {
- const collections = useContentCollections()
-
- const collectionInfo = getCollectionByFilePath(fsPath, collections)
+ const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections())
+ if (!collectionInfo) {
+ throw new Error(`Collection not found for fsPath: ${fsPath}`)
+ }
const id = generateIdFromFsPath(fsPath, collectionInfo!)
@@ -211,25 +225,33 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
}
const document = await generateDocumentFromContent(id, content)
- const collectionDocument = createCollectionDocument(collectionInfo!, id, document!)
+ const collectionDocument = createCollectionDocument(id, collectionInfo, document!)
- await host.document.upsert(id, collectionDocument!)
+ await host.document.upsert(fsPath, collectionDocument)
- return collectionDocument!
+ return normalizeDocument(fsPath, collectionDocument!)
},
- upsert: async (id: string, document: CollectionItemBase) => {
- id = id.replace(/:/g, '/')
+ upsert: async (fsPath: string, document: CollectionItemBase) => {
+ const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections())
+ if (!collectionInfo) {
+ throw new Error(`Collection not found for fsPath: ${fsPath}`)
+ }
- const collection = getCollectionInfo(id, useContentCollections()).collection
- const doc = createCollectionDocument(collection, id, document)
+ const id = generateIdFromFsPath(fsPath, collectionInfo)
- await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id))
- await useContentDatabaseAdapter(collection.name).exec(generateRecordInsert(collection, doc))
+ const doc = createCollectionDocument(id, collectionInfo, document)
+
+ await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordDeletion(collectionInfo, id))
+ await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, doc))
},
- delete: async (id: string) => {
- id = id.replace(/:/g, '/')
+ delete: async (fsPath: string) => {
+ const collection = getCollectionByFilePath(fsPath, useContentCollections())
+ if (!collection) {
+ throw new Error(`Collection not found for fsPath: ${fsPath}`)
+ }
+
+ const id = generateIdFromFsPath(fsPath, collection)
- const collection = getCollectionInfo(id, useContentCollections()).collection
await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id))
},
detectActives: () => {
@@ -238,8 +260,15 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
return Array.from(wrappers).map((wrapper) => {
const id = wrapper.getAttribute('data-content-id')!
const title = id.split(/[/:]/).pop() || id
+
+ const collection = getCollectionById(id, useContentCollections())
+
+ const source = getCollectionSourceById(id, collection.source)
+
+ const fsPath = generateFsPathFromId(id, source!)
+
return {
- id,
+ fsPath,
title,
}
})
@@ -247,20 +276,18 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH
},
media: {
- get: async (id: string): Promise => {
- return await publicAssetsStorage.getItem(id) as MediaItem
- },
- getFileSystemPath: (id: string) => {
- return id.split('/').slice(1).join('/')
+ get: async (fsPath: string): Promise => {
+ return await publicAssetsStorage.getItem(generateMediaIdFromFsPath(fsPath)) as MediaItem
},
list: async (): Promise => {
return await Promise.all(await publicAssetsStorage.getKeys().then(keys => keys.map(key => publicAssetsStorage.getItem(key)))) as MediaItem[]
},
- upsert: async (id: string, media: MediaItem) => {
- await publicAssetsStorage.setItem(id, media)
+ upsert: async (fsPath: string, media: MediaItem) => {
+ const id = generateMediaIdFromFsPath(fsPath)
+ await publicAssetsStorage.setItem(generateMediaIdFromFsPath(fsPath), { ...media, id })
},
- delete: async (id: string) => {
- await publicAssetsStorage.removeItem(id)
+ delete: async (fsPath: string) => {
+ await publicAssetsStorage.removeItem(generateMediaIdFromFsPath(fsPath))
},
},
diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts
index 3f4b6ec9..8924caed 100644
--- a/src/module/src/runtime/utils/collection.ts
+++ b/src/module/src/runtime/utils/collection.ts
@@ -1,40 +1,17 @@
-import type { CollectionInfo, CollectionSource, Draft07, CollectionItemBase, PageCollectionItemBase, ResolvedCollectionSource, Draft07DefinitionProperty } from '@nuxt/content'
+import type { CollectionInfo, Draft07, ResolvedCollectionSource, Draft07DefinitionProperty } from '@nuxt/content'
import { hash } from 'ohash'
-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'
-
-export const getCollectionByFilePath = (path: string, collections: Record): CollectionInfo | undefined => {
- let matchedSource: ResolvedCollectionSource | undefined
- const collection = Object.values(collections).find((collection) => {
- if (!collection.source || collection.source.length === 0) {
- return
- }
-
- // const pathWithoutRoot = withoutRoot(path)
- const paths = path === '/' ? ['index.yml', 'index.yaml', 'index.md', 'index.json'] : [path]
- return paths.some((p) => {
- matchedSource = collection.source.find((source) => {
- const include = minimatch(p, source.include, { dot: true })
- const exclude = source.exclude?.some(exclude => minimatch(p, exclude))
-
- return include && !exclude
- })
-
- return matchedSource
- })
- })
-
- return collection
-}
+import { withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
+import { parseSourceBase } from './source'
+/**
+ * Generation methods
+ */
export function generateStemFromFsPath(path: string) {
return withoutLeadingSlash(join(dirname(path), parse(path).name))
}
-// TODO handle several sources case
export function generateIdFromFsPath(path: string, collectionInfo: CollectionInfo) {
const { fixed } = parseSourceBase(collectionInfo.source[0]!)
@@ -43,126 +20,74 @@ export function generateIdFromFsPath(path: string, collectionInfo: CollectionInf
return join(collectionInfo.name, collectionInfo.source[0]?.prefix || '', pathWithoutFixed)
}
-export function getOrderedSchemaKeys(schema: Draft07) {
- const shape = Object.values(schema.definitions)[0]?.properties || {}
- const keys = new Set([
- shape.id ? 'id' : undefined,
- shape.title ? 'title' : undefined,
- ...Object.keys(shape).sort(),
- ].filter(Boolean))
-
- return Array.from(keys) as string[]
-}
-
-export function getCollection(collectionName: string, collections: Record): CollectionInfo {
- const collection = collections[collectionName as keyof typeof collections]
- if (!collection) {
- throw new Error(`Collection ${collectionName} not found`)
- }
- return collection
-}
-
-export function getCollectionSource(id: string, collection: CollectionInfo) {
- const [_, ...rest] = id.split(/[/:]/)
- const path = rest.join('/')
-
- const matchedSource = collection.source.find((source) => {
- const include = minimatch(path, source.include, { dot: true })
- const exclude = source.exclude?.some(exclude => minimatch(path, exclude))
-
- return include && !exclude
- })
-
- return matchedSource
-}
-
-export function generateFsPathFromId(id: string, source: CollectionInfo['source'][0]) {
+export function generateFsPathFromId(id: string, source: ResolvedCollectionSource) {
const [_, ...rest] = id.split(/[/:]/)
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
+ }
-export function getCollectionInfo(id: string, collections: Record) {
- const collection = getCollection(id.split(/[/:]/)[0]!, collections)
- const source = getCollectionSource(id, collection)
+ // Otherwise, join fixed part with path
+ return join(fixed, path)
+}
- const fsPath = generateFsPathFromId(id, source!)
+/**
+ * Utils methods
+ */
- return {
- collection,
- source,
- fsPath,
- }
-}
+export function getOrderedSchemaKeys(schema: Draft07) {
+ const shape = Object.values(schema.definitions)[0]?.properties || {}
+ const keys = new Set([
+ shape.id ? 'id' : undefined,
+ shape.title ? 'title' : undefined,
+ ...Object.keys(shape).sort(),
+ ].filter(Boolean))
-export function parseSourceBase(source: CollectionSource) {
- const [fixPart, ...rest] = source.include.includes('*') ? source.include.split('*') : ['', source.include]
- return {
- fixed: fixPart || '',
- dynamic: '*' + rest.join('*'),
- }
+ return Array.from(keys) as string[]
}
-export function createCollectionDocument(collection: CollectionInfo, id: string, document: CollectionItemBase) {
- const parsedContent = [
- pathMetaTransform,
- ].reduce((acc, fn) => collection.type === 'page' ? fn(acc as PageCollectionItemBase) : acc, { ...document, id } as PageCollectionItemBase)
- const result = { id } as DatabaseItem
- const meta = parsedContent.meta as Record
-
- const collectionKeys = getOrderedSchemaKeys(collection.schema)
- for (const key of Object.keys(parsedContent)) {
- if (collectionKeys.includes(key)) {
- result[key] = parsedContent[key as keyof PageCollectionItemBase]
- }
- else {
- meta[key] = parsedContent[key as keyof PageCollectionItemBase]
+export function getCollectionByFilePath(path: string, collections: Record): CollectionInfo | undefined {
+ let matchedSource: ResolvedCollectionSource | undefined
+ const collection = Object.values(collections).find((collection) => {
+ if (!collection.source || collection.source.length === 0) {
+ return
}
- }
- result.meta = meta
+ const paths = path === '/' ? ['index.yml', 'index.yaml', 'index.md', 'index.json'] : [path]
+ return paths.some((p) => {
+ matchedSource = collection.source.find((source) => {
+ const include = minimatch(p, source.include, { dot: true })
+ const exclude = source.exclude?.some(exclude => minimatch(p, exclude))
- // Storing `content` into `rawbody` field
- // TODO: handle rawbody
- // if (collectionKeys.includes('rawbody')) {
- // result.rawbody = result.rawbody ?? file.body
- // }
+ return include && !exclude
+ })
- if (collectionKeys.includes('seo')) {
- const seo = result.seo = (result.seo || {}) as PageCollectionItemBase['seo']
- seo.title = seo.title || result.title as string
- seo.description = seo.description || result.description as string
- }
+ return matchedSource
+ })
+ })
- return result
+ return collection
}
-export function normalizeDocument(document: DatabaseItem) {
- // `seo` is an auto-generated field in content module
- // if `seo.title` and `seo.description` are same as `title` and `description`
- // we can remove it to avoid duplication
- if (document?.seo) {
- const seo = document.seo as Record
-
- if (!seo.title || seo.title === document.title) {
- Reflect.deleteProperty(document.seo, 'title')
- }
- if (!seo.description || seo.description === document.description) {
- Reflect.deleteProperty(document.seo, 'description')
- }
+export function getCollectionById(id: string, collections: Record): CollectionInfo {
+ const collectionName = id.split(/[/:]/)[0]!
- if (Object.keys(seo).length === 0) {
- Reflect.deleteProperty(document, 'seo')
- }
+ const collection = collections[collectionName as keyof typeof collections]
+ if (!collection) {
+ throw new Error(`Collection ${collectionName} not found`)
}
- return document
+ return collection
}
+/**
+ * SQL query generation methods
+ */
function computeValuesBasedOnCollectionSchema(collection: CollectionInfo, data: Record) {
const fields: string[] = []
const values: Array = []
diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts
new file mode 100644
index 00000000..96ea59bd
--- /dev/null
+++ b/src/module/src/runtime/utils/document.ts
@@ -0,0 +1,63 @@
+import type { CollectionInfo, CollectionItemBase, PageCollectionItemBase } from '@nuxt/content'
+import { getOrderedSchemaKeys } from './collection'
+import { pathMetaTransform } from './path-meta'
+import type { DatabaseItem } from 'nuxt-studio/app'
+
+export function createCollectionDocument(id: string, collectionInfo: CollectionInfo, document: CollectionItemBase) {
+ const parsedContent = [
+ pathMetaTransform,
+ ].reduce((acc, fn) => collectionInfo.type === 'page' ? fn(acc as PageCollectionItemBase) : acc, { ...document, id } as PageCollectionItemBase)
+ const result = { id } as DatabaseItem
+ const meta = parsedContent.meta as Record
+
+ const collectionKeys = getOrderedSchemaKeys(collectionInfo.schema)
+ for (const key of Object.keys(parsedContent)) {
+ if (collectionKeys.includes(key)) {
+ result[key] = parsedContent[key as keyof PageCollectionItemBase]
+ }
+ else {
+ meta[key] = parsedContent[key as keyof PageCollectionItemBase]
+ }
+ }
+
+ result.meta = meta
+
+ // Storing `content` into `rawbody` field
+ // TODO: handle rawbody
+ // if (collectionKeys.includes('rawbody')) {
+ // result.rawbody = result.rawbody ?? file.body
+ // }
+
+ if (collectionKeys.includes('seo')) {
+ const seo = result.seo = (result.seo || {}) as PageCollectionItemBase['seo']
+ seo.title = seo.title || result.title as string
+ seo.description = seo.description || result.description as string
+ }
+
+ return result
+}
+
+export function normalizeDocument(fsPath: string, document: DatabaseItem): DatabaseItem {
+ // `seo` is an auto-generated field in content module
+ // if `seo.title` and `seo.description` are same as `title` and `description`
+ // we can remove it to avoid duplication
+ if (document?.seo) {
+ const seo = document.seo as Record
+
+ if (!seo.title || seo.title === document.title) {
+ Reflect.deleteProperty(document.seo, 'title')
+ }
+ if (!seo.description || seo.description === document.description) {
+ Reflect.deleteProperty(document.seo, 'description')
+ }
+
+ if (Object.keys(seo).length === 0) {
+ Reflect.deleteProperty(document, 'seo')
+ }
+ }
+
+ return {
+ ...document,
+ fsPath,
+ }
+}
diff --git a/src/module/src/runtime/utils/media.ts b/src/module/src/runtime/utils/media.ts
new file mode 100644
index 00000000..de655ec4
--- /dev/null
+++ b/src/module/src/runtime/utils/media.ts
@@ -0,0 +1,6 @@
+import { join } from 'pathe'
+import { VirtualMediaCollectionName } from 'nuxt-studio/app/utils'
+
+export function generateIdFromFsPath(fsPath: string) {
+ return join(VirtualMediaCollectionName, fsPath)
+}
diff --git a/src/module/src/runtime/utils/source.ts b/src/module/src/runtime/utils/source.ts
new file mode 100644
index 00000000..622bdfd1
--- /dev/null
+++ b/src/module/src/runtime/utils/source.ts
@@ -0,0 +1,51 @@
+import type { CollectionSource, ResolvedCollectionSource } from '@nuxt/content'
+import { withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from 'ufo'
+import { join } from 'pathe'
+import { minimatch } from 'minimatch'
+
+export function parseSourceBase(source: CollectionSource) {
+ const [fixPart, ...rest] = source.include.includes('*') ? source.include.split('*') : ['', source.include]
+ return {
+ fixed: fixPart || '',
+ dynamic: '*' + rest.join('*'),
+ }
+}
+
+/**
+ * On Nuxt Content, Id is built like this: {collection.name}/{source.prefix}/{path}
+ * But 'source.prefix' can be different from the fixed part of 'source.include'
+ * We need to remove the 'source.prefix' from the path and add the fixed part of the 'source.include' to get the fsPath (used to match the source)
+ */
+export function getCollectionSourceById(id: string, sources: ResolvedCollectionSource[]) {
+ const [_, ...rest] = id.split(/[/:]/)
+ const prefixAndPath = rest.join('/')
+
+ const matchedSource = sources.find((source) => {
+ 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
+ })
+
+ return matchedSource
+}
diff --git a/src/module/src/templates.ts b/src/module/src/templates.ts
index 51747a60..78f10ea8 100644
--- a/src/module/src/templates.ts
+++ b/src/module/src/templates.ts
@@ -1,5 +1,6 @@
import type { Storage } from 'unstorage'
import type { Nuxt } from '@nuxt/schema'
+import { withLeadingSlash } from 'ufo'
export async function getAssetsStorageDevTemplate(_assetsStorage: Storage, _nuxt: Nuxt) {
return [
@@ -19,11 +20,13 @@ export async function getAssetsStorageTemplate(assetsStorage: Storage, _nuxt: Nu
'const storage = createStorage({})',
'',
...keys.map((key) => {
+ const path = withLeadingSlash(key.replace(/:/g, '/'))
const value = {
id: `public-assets/${key.replace(/:/g, '/')}`,
extension: key.split('.').pop(),
stem: key.split('.').join('.'),
- path: '/' + key.replace(/:/g, '/'),
+ path,
+ fsPath: path,
}
return `storage.setItem('${value.id}', ${JSON.stringify(value)})`
}),
diff --git a/src/module/test/utils/collection.test.ts b/src/module/test/utils/collection.test.ts
index f7635605..6f72134c 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, generateIdFromFsPath, generateFsPathFromId } from '../../src/runtime/utils/collection'
import type { CollectionInfo, ResolvedCollectionSource } from '@nuxt/content'
import { collections } from '../mocks/collection'
@@ -78,4 +78,94 @@ 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')
+ })
})
diff --git a/src/module/test/utils/source.test.ts b/src/module/test/utils/source.test.ts
new file mode 100644
index 00000000..dbd1bd6d
--- /dev/null
+++ b/src/module/test/utils/source.test.ts
@@ -0,0 +1,105 @@
+import { describe, it, expect } from 'vitest'
+import { getCollectionSourceById } from '../../src/runtime/utils/source'
+import { collections } from '../mocks/collection'
+import type { ResolvedCollectionSource } from '@nuxt/content'
+
+describe('getCollectionSourceById', () => {
+ it('should return matching source for root docs collection', () => {
+ const id = 'collectionName/1.getting-started/2.introduction.md'
+ const source = getCollectionSourceById(id, collections.docs!.source)
+
+ expect(source).toEqual(collections.docs!.source[0])
+ })
+
+ it('should return matching source for root index file in landing collection', () => {
+ const id = 'collectionName/index.md'
+ const source = getCollectionSourceById(id, collections.landing!.source)
+
+ expect(source).toEqual(collections.landing!.source[0])
+ })
+
+ it('should handle root dot files correctly', () => {
+ const id = 'collectionName/.navigation.yml'
+ const source = getCollectionSourceById(id, collections.docs!.source)
+
+ expect(source).toEqual(collections.docs!.source[0])
+ })
+
+ it('should return undefined when path matches exclude pattern', () => {
+ const id = 'collectionName/index.md'
+ const source = getCollectionSourceById(id, collections.docs!.source)
+
+ expect(source).toBeUndefined()
+ })
+
+ it('should return correct source when source has prefix with dynamic include pattern', () => {
+ const sourceWithPrefix: ResolvedCollectionSource[] = [
+ {
+ _resolved: true,
+ prefix: '/blog',
+ include: 'blog/**/*.md',
+ cwd: '',
+ },
+ ]
+
+ const id = 'collectionName/blog/my-post.md'
+ const source = getCollectionSourceById(id, sourceWithPrefix)
+ expect(source).toEqual(sourceWithPrefix[0])
+ })
+
+ it('should return correct source when source has multiple sources', () => {
+ const multipleSources: ResolvedCollectionSource[] = [
+ {
+ _resolved: true,
+ prefix: '/blog',
+ cwd: '',
+ include: 'blog/**/*.md',
+ },
+ {
+ _resolved: true,
+ prefix: '/docs',
+ cwd: '',
+ include: 'docs/**/*.md',
+ },
+ ]
+
+ const blogId = 'collectionName/blog/my-post.md'
+ const blogResult = getCollectionSourceById(blogId, multipleSources)
+ expect(blogResult).toEqual(multipleSources[0])
+
+ const docsId = 'collectionName/docs/guide.md'
+ const docsResult = getCollectionSourceById(docsId, multipleSources)
+ expect(docsResult).toEqual(multipleSources[1])
+ })
+
+ it('should return correct source when source has root prefix with custom dynamic include pattern', () => {
+ const rootPrefixSource: ResolvedCollectionSource[] = [
+ {
+ _resolved: true,
+ prefix: '/',
+ include: 'pages/**/*.md',
+ cwd: '',
+ },
+ ]
+
+ // {collection.name}/{source.prefix}/{name}
+ const id = 'collectionName/about.md'
+ const source = getCollectionSourceById(id, rootPrefixSource)
+ expect(source).toEqual(rootPrefixSource[0])
+ })
+
+ it('should return correct source when source has custom prefix with custom dynamic include pattern', () => {
+ const customPrefixSource: ResolvedCollectionSource[] = [
+ {
+ _resolved: true,
+ prefix: '/prefix',
+ include: 'path/**/*.md',
+ cwd: '',
+ },
+ ]
+
+ const id = 'collectionName/prefix/file.md'
+ const source = getCollectionSourceById(id, customPrefixSource)
+ expect(source).toEqual(customPrefixSource[0])
+ })
+})