diff --git a/src/app/src/App.vue b/src/app/src/App.vue
index 137fe7c4..fcdbf8a1 100644
--- a/src/app/src/App.vue
+++ b/src/app/src/App.vue
@@ -1,14 +1,11 @@
+
+
+
+
+
diff --git a/src/app/src/components/panel/base/PanelBaseBodyEditor.vue b/src/app/src/components/panel/base/PanelBaseBodyEditor.vue
new file mode 100644
index 00000000..88327984
--- /dev/null
+++ b/src/app/src/components/panel/base/PanelBaseBodyEditor.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/src/app/src/components/panel/content/PanelContentTree.vue b/src/app/src/components/panel/base/PanelBaseBodyTree.vue
similarity index 77%
rename from src/app/src/components/panel/content/PanelContentTree.vue
rename to src/app/src/components/panel/base/PanelBaseBodyTree.vue
index 3e608a78..6a5a0a66 100644
--- a/src/app/src/components/panel/content/PanelContentTree.vue
+++ b/src/app/src/components/panel/base/PanelBaseBodyTree.vue
@@ -1,9 +1,11 @@
diff --git a/src/app/src/components/panel/base/PanelBaseSubHeader.vue b/src/app/src/components/panel/base/PanelBaseSubHeader.vue
new file mode 100644
index 00000000..07938fb5
--- /dev/null
+++ b/src/app/src/components/panel/base/PanelBaseSubHeader.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/src/app/src/components/panel/content/PanelContent.vue b/src/app/src/components/panel/content/PanelContent.vue
deleted file mode 100644
index 713bc86d..00000000
--- a/src/app/src/components/panel/content/PanelContent.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
diff --git a/src/app/src/components/panel/content/editor/PanelContentEditor.vue b/src/app/src/components/panel/content/editor/PanelContentEditor.vue
index ba8833fc..c5d214c3 100644
--- a/src/app/src/components/panel/content/editor/PanelContentEditor.vue
+++ b/src/app/src/components/panel/content/editor/PanelContentEditor.vue
@@ -2,17 +2,17 @@
import { computed, type PropType, toRaw } from 'vue'
import { decompressTree } from '@nuxt/content/runtime'
import type { MarkdownRoot } from '@nuxt/content'
-import type { DatabasePageItem, DraftFileItem } from '../../../../types'
+import type { DatabasePageItem, DraftItem } from '../../../../types'
import { useStudio } from '../../../../composables/useStudio'
const props = defineProps({
draftItem: {
- type: Object as PropType,
+ type: Object as PropType,
required: true,
},
})
-const { draftFiles } = useStudio()
+const { draftDocuments } = useStudio()
const document = computed({
get() {
@@ -20,13 +20,13 @@ const document = computed({
return {} as DatabasePageItem
}
- const dbItem = props.draftItem.document as DatabasePageItem
+ const dbItem = props.draftItem.modified as DatabasePageItem
let result: DatabasePageItem
// TODO: check mdcRoot and markdownRoot types with Ahad
if (dbItem.body?.type === 'minimark') {
result = {
- ...props.draftItem.document as DatabasePageItem,
+ ...props.draftItem.modified as DatabasePageItem,
// @ts-expect-error todo fix MarkdownRoot/MDCRoot conversion in MDC module
body: decompressTree(dbItem.body) as MarkdownRoot,
}
@@ -38,7 +38,7 @@ const document = computed({
return result
},
set(value) {
- draftFiles.update(props.draftItem.id, {
+ draftDocuments.update(props.draftItem.id, {
...toRaw(document.value as DatabasePageItem),
...toRaw(value),
})
diff --git a/src/app/src/components/panel/content/editor/PanelContentEditorCode.vue b/src/app/src/components/panel/content/editor/PanelContentEditorCode.vue
index 7b5a335f..2575ccd1 100644
--- a/src/app/src/components/panel/content/editor/PanelContentEditorCode.vue
+++ b/src/app/src/components/panel/content/editor/PanelContentEditorCode.vue
@@ -1,13 +1,13 @@
+
+
+
+
![]()
+
+
diff --git a/src/app/src/components/shared/item/ItemBreadcrumb.vue b/src/app/src/components/shared/item/ItemBreadcrumb.vue
index 6890482c..651129dc 100644
--- a/src/app/src/components/shared/item/ItemBreadcrumb.vue
+++ b/src/app/src/components/shared/item/ItemBreadcrumb.vue
@@ -2,13 +2,13 @@
import type { BreadcrumbItem } from '@nuxt/ui/components/Breadcrumb.vue.d.ts'
import type { DropdownMenuItem } from '@nuxt/ui/components/DropdownMenu.vue.d.ts'
import { computed, type PropType, unref } from 'vue'
-import type { TreeItem } from '../../../types'
+import { StudioFeature, type TreeItem } from '../../../types'
import { useStudio } from '../../../composables/useStudio'
import { findParentFromId, ROOT_ITEM } from '../../../utils/tree'
import { FEATURE_DISPLAY_MAP } from '../../../utils/context'
import { DraftStatus } from '../../../types/draft'
-const { tree: treeApi, context } = useStudio()
+const { documentTree, mediaTree, context } = useStudio()
const props = defineProps({
currentItem: {
@@ -21,11 +21,14 @@ const props = defineProps({
},
})
+const treeApi = computed(() => context.feature.value === StudioFeature.Content ? documentTree : mediaTree)
+
const items = computed(() => {
const rootItem = {
label: FEATURE_DISPLAY_MAP[context.feature.value as keyof typeof FEATURE_DISPLAY_MAP],
onClick: () => {
- treeApi.select(ROOT_ITEM)
+ // TODO: update for ROOT_DOCUMENT_ITEM and ROOT_MEDIA_ITEM
+ treeApi.value.select(ROOT_ITEM)
},
}
@@ -41,7 +44,7 @@ const items = computed(() => {
breadcrumbItems.unshift({
label: currentTreeItem.name,
onClick: async () => {
- await treeApi.select(itemToSelect)
+ await treeApi.value.select(itemToSelect)
},
})
diff --git a/src/app/src/composables/useContext.ts b/src/app/src/composables/useContext.ts
index cc608e52..445c09b3 100644
--- a/src/app/src/composables/useContext.ts
+++ b/src/app/src/composables/useContext.ts
@@ -3,14 +3,16 @@ import { computed, ref } from 'vue'
import type { useUi } from './useUi'
import { type CreateFileParams, type StudioHost, type StudioAction, type TreeItem, StudioItemActionId, type ActionHandlerParams } from '../types'
import { oneStepActions, STUDIO_ITEM_ACTION_DEFINITIONS, twoStepActions } from '../utils/context'
-import type { useDraftFiles } from './useDraftFiles'
+import type { useDraftDocuments } from './useDraftDocuments'
import { useModal } from './useModal'
import type { useTree } from './useTree'
+import type { useDraftMedias } from './useDraftMedias'
export const useContext = createSharedComposable((
host: StudioHost,
ui: ReturnType,
- draftFiles: ReturnType,
+ draftDocuments: ReturnType,
+ draftMedias: ReturnType,
tree: ReturnType,
) => {
const modal = useModal()
@@ -52,13 +54,13 @@ export const useContext = createSharedComposable((
[StudioItemActionId.CreateFolder]: async (args: string) => {
alert(`create folder ${args}`)
},
- [StudioItemActionId.CreateFile]: async ({ fsPath, routePath, content }: CreateFileParams) => {
+ [StudioItemActionId.CreateDocument]: async ({ fsPath, routePath, content }: CreateFileParams) => {
const document = await host.document.create(fsPath, routePath, content)
- const draftItem = await draftFiles.create(document)
+ const draftItem = await draftDocuments.create(document)
tree.selectItemById(draftItem.id)
},
[StudioItemActionId.RevertItem]: async (id: string) => {
- modal.openConfirmActionModal(id, StudioItemActionId.RevertItem, () => draftFiles.revert(id))
+ modal.openConfirmActionModal(id, StudioItemActionId.RevertItem, () => draftDocuments.revert(id))
},
[StudioItemActionId.RenameItem]: async ({ path, file }: { path: string, file: TreeItem }) => {
alert(`rename file ${path} ${file.name}`)
diff --git a/src/app/src/composables/useDraftFiles.ts b/src/app/src/composables/useDraftDocuments.ts
similarity index 71%
rename from src/app/src/composables/useDraftFiles.ts
rename to src/app/src/composables/useDraftDocuments.ts
index 35ac31fe..ad80ac30 100644
--- a/src/app/src/composables/useDraftFiles.ts
+++ b/src/app/src/composables/useDraftDocuments.ts
@@ -1,6 +1,7 @@
+import { createStorage } from 'unstorage'
+import indexedDbDriver from 'unstorage/drivers/indexedb'
import { ref } from 'vue'
-import type { StorageValue, Storage } from 'unstorage'
-import type { DatabaseItem, DraftFileItem, StudioHost, GithubFile, DatabasePageItem } from '../types'
+import type { DatabaseItem, DraftItem, StudioHost, GithubFile, DatabasePageItem } from '../types'
import { DraftStatus } from '../types/draft'
import type { useGit } from './useGit'
import { generateContentFromDocument } from '../utils/content'
@@ -8,9 +9,15 @@ import { getDraftStatus } from '../utils/draft'
import { createSharedComposable } from '@vueuse/core'
import { useHooks } from './useHooks'
-export const useDraftFiles = createSharedComposable((host: StudioHost, git: ReturnType, storage: Storage) => {
- const list = ref([])
- const current = ref(null)
+const storage = createStorage({
+ driver: indexedDbDriver({
+ storeName: 'nuxt-content-studio-documents',
+ }),
+})
+
+export const useDraftDocuments = createSharedComposable((host: StudioHost, git: ReturnType) => {
+ const list = ref[]>([])
+ const current = ref | null>(null)
const hooks = useHooks()
@@ -19,7 +26,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
if (item && generateContent) {
return {
...item,
- content: await generateContentFromDocument(item!.document as DatabasePageItem) || '',
+ content: await generateContentFromDocument(item!.modified as DatabasePageItem) || '',
}
}
return item
@@ -32,15 +39,15 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
}
const fsPath = host.document.getFileSystemPath(document.id)
- const originalGithubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
+ const githubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
- const item: DraftFileItem = {
+ const item: DraftItem = {
id: document.id,
fsPath,
- originalDatabaseItem: document,
- originalGithubFile,
+ original: document,
+ githubFile,
status,
- document,
+ modified: document,
}
await storage.setItem(document.id, item)
@@ -59,15 +66,15 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
}
const oldStatus = existingItem.status
- existingItem.status = getDraftStatus(document, existingItem.originalDatabaseItem)
- existingItem.document = document
+ existingItem.status = getDraftStatus(document, existingItem.original)
+ existingItem.modified = document
await storage.setItem(id, existingItem)
list.value = list.value.map(item => item.id === id ? existingItem : item)
// Upsert document in database
- await host.document.upsert(id, existingItem.document)
+ await host.document.upsert(id, existingItem.modified)
// Rerender host app
host.app.requestRerender()
@@ -81,7 +88,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
}
async function remove(id: string) {
- const item = await storage.getItem(id) as DraftFileItem
+ const item = await storage.getItem(id) as DraftItem
const fsPath = host.document.getFileSystemPath(id)
if (item) {
@@ -90,30 +97,30 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
await storage.removeItem(id)
await host.document.delete(id)
- if (item.originalDatabaseItem) {
- const deleteDraft: DraftFileItem = {
+ if (item.original) {
+ const deleteDraft: DraftItem = {
id,
fsPath: item.fsPath,
status: DraftStatus.Deleted,
- originalDatabaseItem: item.originalDatabaseItem,
- originalGithubFile: item.originalGithubFile,
+ original: item.original,
+ githubFile: item.githubFile,
}
await storage.setItem(id, deleteDraft)
- await host.document.upsert(id, item.originalDatabaseItem!)
+ await host.document.upsert(id, item.original!)
}
}
else {
// Fetch github file before creating draft to detect non deployed changes
- const originalGithubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
- const originalDatabaseItem = await host.document.get(id)
+ const githubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
+ const original = await host.document.get(id)
- const deleteItem: DraftFileItem = {
+ const deleteItem: DraftItem = {
id,
fsPath,
status: DraftStatus.Deleted,
- originalDatabaseItem,
- originalGithubFile,
+ original,
+ githubFile,
}
await storage.setItem(id, deleteItem)
@@ -137,9 +144,9 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
list.value = list.value.filter(item => item.id !== id)
}
else {
- await host.document.upsert(id, existingItem.originalDatabaseItem!)
+ await host.document.upsert(id, existingItem.original!)
existingItem.status = DraftStatus.Opened
- existingItem.document = existingItem.originalDatabaseItem
+ existingItem.modified = existingItem.original
await storage.setItem(id, existingItem)
}
@@ -151,8 +158,8 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
async function revertAll() {
await storage.clear()
for (const item of list.value) {
- if (item.originalDatabaseItem) {
- await host.document.upsert(item.id, item.originalDatabaseItem)
+ if (item.original) {
+ await host.document.upsert(item.id, item.original)
}
else if (item.status === DraftStatus.Created) {
await host.document.delete(item.id)
@@ -165,7 +172,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
async function load() {
const storedList = await storage.getKeys().then(async (keys) => {
return Promise.all(keys.map(async (key) => {
- const item = await storage.getItem(key) as DraftFileItem
+ const item = await storage.getItem(key) as DraftItem
if (item.status === DraftStatus.Opened) {
await storage.removeItem(key)
return null
@@ -174,7 +181,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
}))
})
- list.value = storedList.filter(Boolean) as DraftFileItem[]
+ list.value = storedList.filter(Boolean) as DraftItem[]
// Upsert/Delete draft files in database
await Promise.all(list.value.map(async (draftItem) => {
@@ -182,7 +189,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
await host.document.delete(draftItem.id)
}
else {
- await host.document.upsert(draftItem.id, draftItem.document!)
+ await host.document.upsert(draftItem.id, draftItem.modified!)
}
}))
@@ -191,7 +198,7 @@ export const useDraftFiles = createSharedComposable((host: StudioHost, git: Retu
await hooks.callHook('studio:draft:updated')
}
- function select(draftItem: DraftFileItem | null) {
+ function select(draftItem: DraftItem | null) {
current.value = draftItem
}
diff --git a/src/app/src/composables/useDraftMedias.ts b/src/app/src/composables/useDraftMedias.ts
new file mode 100644
index 00000000..afb5f6d7
--- /dev/null
+++ b/src/app/src/composables/useDraftMedias.ts
@@ -0,0 +1,215 @@
+import { ref } from 'vue'
+import { createStorage } from 'unstorage'
+import indexedDbDriver from 'unstorage/drivers/indexedb'
+import type { DraftItem, StudioHost, GithubFile, MediaItem } from '../types'
+import { DraftStatus } from '../types/draft'
+import type { useGit } from './useGit'
+import { getDraftStatus } from '../utils/draft'
+import { createSharedComposable } from '@vueuse/core'
+import { useHooks } from './useHooks'
+
+const storage = createStorage({
+ driver: indexedDbDriver({
+ storeName: 'nuxt-content-studio-medias',
+ }),
+})
+
+export const useDraftMedias = createSharedComposable((host: StudioHost, git: ReturnType) => {
+ const list = ref([])
+ const current = ref(null)
+
+ const hooks = useHooks()
+
+ async function get(id: string) {
+ const item = list.value.find(item => item.id === id)
+ return item
+ }
+
+ async function create(media: MediaItem, status: DraftStatus = DraftStatus.Created) {
+ const existingItem = list.value.find(item => item.id === media.id)
+ if (existingItem) {
+ throw new Error(`Draft file already exists for document ${media.id}`)
+ }
+
+ const fsPath = host.media.getFileSystemPath(media.id)
+ const githubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
+
+ const item: DraftItem = {
+ id: media.id,
+ fsPath,
+ original: media,
+ githubFile,
+ status,
+ modified: media,
+ }
+
+ await storage.setItem(media.id, item)
+
+ list.value.push(item)
+
+ await hooks.callHook('studio:draft:updated')
+
+ return item
+ }
+
+ async function update(id: string, media: MediaItem) {
+ const existingItem = list.value.find(item => item.id === id)
+ if (!existingItem) {
+ throw new Error(`Draft file not found for document ${id}`)
+ }
+
+ const oldStatus = existingItem.status
+ existingItem.status = getDraftStatus(media, existingItem.original)
+ existingItem.modified = media
+
+ await storage.setItem(id, existingItem)
+
+ list.value = list.value.map(item => item.id === id ? existingItem : item)
+
+ // Upsert document in database
+ await host.media.upsert(id, existingItem.modified!)
+
+ // Rerender host app
+ host.app.requestRerender()
+
+ // Trigger hook to warn that draft list has changed
+ if (existingItem.status !== oldStatus) {
+ await hooks.callHook('studio:draft:updated')
+ }
+
+ return existingItem
+ }
+
+ async function remove(id: string) {
+ const item = await storage.getItem(id) as DraftItem
+ const fsPath = host.media.getFileSystemPath(id)
+
+ if (item) {
+ if (item.status === DraftStatus.Deleted) return
+
+ await storage.removeItem(id)
+ await host.media.delete(id)
+ }
+ else {
+ // Fetch github file before creating draft to detect non deployed changes
+ const githubFile = await git.fetchFile(fsPath, { cached: true }) as GithubFile
+ const original = await host.media.get(id)
+
+ const deleteItem: DraftItem = {
+ id,
+ fsPath,
+ status: DraftStatus.Deleted,
+ original,
+ githubFile,
+ }
+
+ await storage.setItem(id, deleteItem)
+
+ await host.media.delete(id)
+ }
+
+ list.value = list.value.filter(item => item.id !== id)
+ host.app.requestRerender()
+ }
+
+ async function revert(id: string) {
+ const existingItem = list.value.find(item => item.id === id)
+ if (!existingItem) {
+ return
+ }
+
+ if (existingItem.status === DraftStatus.Created) {
+ await host.media.delete(id)
+ await storage.removeItem(id)
+ list.value = list.value.filter(item => item.id !== id)
+ }
+ else {
+ await host.media.upsert(id, existingItem.original!)
+ existingItem.status = DraftStatus.Opened
+ existingItem.modified = existingItem.original
+ await storage.setItem(id, existingItem)
+ }
+
+ await hooks.callHook('studio:draft:updated')
+
+ host.app.requestRerender()
+ }
+
+ async function revertAll() {
+ await storage.clear()
+ for (const item of list.value) {
+ if (item.original) {
+ await host.media.upsert(item.id, item.original)
+ }
+ else if (item.status === DraftStatus.Created) {
+ await host.media.delete(item.id)
+ }
+ }
+ list.value = []
+ host.app.requestRerender()
+ }
+
+ async function load() {
+ const storedList = await storage.getKeys().then(async (keys) => {
+ return Promise.all(keys.map(async (key) => {
+ const item = await storage.getItem(key) as DraftItem
+ if (item.status === DraftStatus.Opened) {
+ await storage.removeItem(key)
+ return null
+ }
+ return item
+ }))
+ })
+
+ list.value = storedList.filter(Boolean) as DraftItem[]
+
+ // Upsert/Delete draft files in database
+ await Promise.all(list.value.map(async (draftItem) => {
+ if (draftItem.status === DraftStatus.Deleted) {
+ await host.media.delete(draftItem.id)
+ }
+ else {
+ await host.media.upsert(draftItem.id, draftItem.modified!)
+ }
+ }))
+
+ host.app.requestRerender()
+
+ await hooks.callHook('studio:draft:updated')
+ }
+
+ function select(draftItem: DraftItem | null) {
+ current.value = draftItem
+ console.log('select', draftItem)
+ }
+
+ async function selectById(id: string) {
+ const existingItem = list.value.find(item => item.id === id)
+ if (existingItem) {
+ select(existingItem)
+ return
+ }
+
+ const dbItem = await host.media.get(id)
+ if (!dbItem) {
+ throw new Error(`Cannot select item: no corresponding database entry found for id ${id}`)
+ }
+
+ const draftItem = await create(dbItem, DraftStatus.Opened)
+ select(draftItem)
+ }
+
+ return {
+ get,
+ create,
+ update,
+ remove,
+ revert,
+ revertAll,
+ list,
+ load,
+ current,
+ select,
+ selectById,
+ }
+})
diff --git a/src/app/src/composables/useMediaTree.ts b/src/app/src/composables/useMediaTree.ts
new file mode 100644
index 00000000..74956c5a
--- /dev/null
+++ b/src/app/src/composables/useMediaTree.ts
@@ -0,0 +1,93 @@
+import type { StudioHost, TreeItem } from '../types'
+import { ref, computed } from 'vue'
+import type { useDraftDocuments } from './useDraftDocuments'
+import { buildTree, findItemFromId, findItemFromRoute, ROOT_ITEM } from '../utils/tree'
+import type { RouteLocationNormalized } from 'vue-router'
+import { createSharedComposable } from '@vueuse/core'
+import { useHooks } from './useHooks'
+
+export const useTree = createSharedComposable((host: StudioHost, draftDocuments: ReturnType) => {
+ const hooks = useHooks()
+
+ const tree = ref([])
+ const currentItem = ref(ROOT_ITEM)
+
+ const currentTree = computed(() => {
+ if (currentItem.value.id === ROOT_ITEM.id) {
+ return tree.value
+ }
+
+ let subTree = tree.value
+ const idSegments = currentItem.value.id.split('/').filter(Boolean)
+ for (let i = 0; i < idSegments.length; i++) {
+ const id = idSegments.slice(0, i + 1).join('/')
+ const file = subTree.find(item => item.id === id) as TreeItem
+ if (file) {
+ subTree = file.children!
+ }
+ }
+
+ return subTree
+ })
+
+ // const parentItem = computed(() => {
+ // if (currentItem.value.id === ROOT_ITEM.id) return null
+
+ // const parent = findParentFromId(tree.value, currentItem.value.id)
+ // return parent || ROOT_ITEM
+ // })
+
+ async function select(item: TreeItem) {
+ currentItem.value = item || ROOT_ITEM
+ if (item?.type === 'file') {
+ host.app.navigateTo(item.routePath!)
+ await draftDocuments.selectById(item.id)
+ }
+ else {
+ draftDocuments.select(null)
+ }
+ }
+
+ async function selectByRoute(route: RouteLocationNormalized) {
+ const item = findItemFromRoute(tree.value, route)
+
+ if (!item || item.id === currentItem.value.id) return
+
+ select(item)
+ }
+
+ async function selectItemById(id: string) {
+ const treeItem = findItemFromId(tree.value, id)
+
+ if (!treeItem || treeItem.id === currentItem.value.id) return
+
+ select(treeItem)
+ }
+
+ hooks.hook('studio:draft:updated', async () => {
+ const list = await host.document.list()
+ const listWithFsPath = list.map((item) => {
+ const fsPath = host.document.getFileSystemPath(item.id)
+ return {
+ ...item,
+ fsPath,
+ }
+ })
+
+ // Trigger tree rebuild to update files status
+ tree.value = buildTree(listWithFsPath, draftDocuments.list.value)
+
+ // Reselect current item to update status
+ select(findItemFromId(tree.value, currentItem.value.id)!)
+ })
+
+ return {
+ root: tree,
+ current: currentTree,
+ currentItem,
+ // parentItem,
+ select,
+ selectByRoute,
+ selectItemById,
+ }
+})
diff --git a/src/app/src/composables/useStudio.ts b/src/app/src/composables/useStudio.ts
index 7ca94c2f..47422c55 100644
--- a/src/app/src/composables/useStudio.ts
+++ b/src/app/src/composables/useStudio.ts
@@ -1,19 +1,13 @@
-import { createStorage } from 'unstorage'
-import indexedDbDriver from 'unstorage/drivers/indexedb'
import { useGit } from './useGit'
import { useUi } from './useUi'
import { useContext } from './useContext'
-import { useDraftFiles } from './useDraftFiles'
+import { useDraftDocuments } from './useDraftDocuments'
+import { useDraftMedias } from './useDraftMedias'
import { ref } from 'vue'
import { useTree } from './useTree'
import { createSharedComposable } from '@vueuse/core'
import type { RouteLocationNormalized } from 'vue-router'
-
-const storage = createStorage({
- driver: indexedDbDriver({
- storeName: 'nuxt-content-preview',
- }),
-})
+import { StudioFeature } from '../types'
export const useStudio = createSharedComposable(() => {
const host = window.useStudioHost()
@@ -28,18 +22,22 @@ export const useStudio = createSharedComposable(() => {
const isReady = ref(false)
const ui = useUi(host)
- const draftFiles = useDraftFiles(host, git, storage)
- const tree = useTree(host, draftFiles)
- const context = useContext(host, ui, draftFiles, tree)
+ const draftDocuments = useDraftDocuments(host, git)
+ const documentTree = useTree(StudioFeature.Content, host, draftDocuments)
+
+ const draftMedias = useDraftMedias(host, git)
+ const mediaTree = useTree(StudioFeature.Media, host, draftMedias)
+
+ const context = useContext(host, ui, draftDocuments, draftMedias, documentTree)
host.on.mounted(async () => {
- await draftFiles.load()
+ await draftDocuments.load()
host.app.requestRerender()
isReady.value = true
host.on.routeChange((to: RouteLocationNormalized, _from: RouteLocationNormalized) => {
if (ui.isPanelOpen.value && ui.config.value.syncEditorAndRoute) {
- tree.selectByRoute(to)
+ documentTree.selectByRoute(to)
}
// setTimeout(() => {
// host.document.detectActives()
@@ -71,15 +69,17 @@ export const useStudio = createSharedComposable(() => {
git,
ui,
context,
- draftFiles,
- tree,
+ draftDocuments,
+ draftMedias,
+ documentTree,
+ mediaTree,
// draftMedia: {
- // get -> DraftMediaItem
+ // get -> DraftItem
// upsert
// remove
// revert
// move
- // list -> DraftMediaItem[]
+ // list -> DraftItem[]
// revertAll
// }
// media: {
diff --git a/src/app/src/composables/useTree.ts b/src/app/src/composables/useTree.ts
index 9a6de6e1..457059e7 100644
--- a/src/app/src/composables/useTree.ts
+++ b/src/app/src/composables/useTree.ts
@@ -1,12 +1,12 @@
-import type { StudioHost, TreeItem } from '../types'
+import { StudioFeature, type StudioHost, type TreeItem } from '../types'
import { ref, computed } from 'vue'
-import type { useDraftFiles } from './useDraftFiles'
+import type { useDraftDocuments } from './useDraftDocuments'
+import type { useDraftMedias } from './useDraftMedias'
import { buildTree, findItemFromId, findItemFromRoute, ROOT_ITEM } from '../utils/tree'
import type { RouteLocationNormalized } from 'vue-router'
-import { createSharedComposable } from '@vueuse/core'
import { useHooks } from './useHooks'
-export const useTree = createSharedComposable((host: StudioHost, draftFiles: ReturnType) => {
+export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType) => {
const hooks = useHooks()
const tree = ref([])
@@ -40,11 +40,14 @@ export const useTree = createSharedComposable((host: StudioHost, draftFiles: Ret
async function select(item: TreeItem) {
currentItem.value = item || ROOT_ITEM
if (item?.type === 'file') {
- host.app.navigateTo(item.routePath!)
- await draftFiles.selectById(item.id)
+ if (type === StudioFeature.Content) {
+ host.app.navigateTo(item.routePath!)
+ }
+
+ await draft.selectById(item.id)
}
else {
- draftFiles.select(null)
+ draft.select(null)
}
}
@@ -65,9 +68,12 @@ export const useTree = createSharedComposable((host: StudioHost, draftFiles: Ret
}
hooks.hook('studio:draft:updated', async () => {
- const list = await host.document.list()
+ const hostList = type === 'content' ? host.document : host.media
+ const hostGetFileSystemPath = type === 'content' ? host.document.getFileSystemPath : host.media.getFileSystemPath
+ const list = await hostList.list()
+
const listWithFsPath = list.map((item) => {
- const fsPath = host.document.getFileSystemPath(item.id)
+ const fsPath = hostGetFileSystemPath(item.id)
return {
...item,
fsPath,
@@ -75,7 +81,7 @@ export const useTree = createSharedComposable((host: StudioHost, draftFiles: Ret
})
// Trigger tree rebuild to update files status
- tree.value = buildTree(listWithFsPath, draftFiles.list.value)
+ tree.value = buildTree(listWithFsPath, draft.list.value)
// Reselect current item to update status
select(findItemFromId(tree.value, currentItem.value.id)!)
@@ -90,4 +96,4 @@ export const useTree = createSharedComposable((host: StudioHost, draftFiles: Ret
selectByRoute,
selectItemById,
}
-})
+}
diff --git a/src/app/src/composables/useUi.ts b/src/app/src/composables/useUi.ts
index 294bfdee..10239a12 100644
--- a/src/app/src/composables/useUi.ts
+++ b/src/app/src/composables/useUi.ts
@@ -6,7 +6,6 @@ export const useUi = createSharedComposable((host: StudioHost) => {
const panels = reactive({
[StudioFeature.Content]: false,
[StudioFeature.Media]: false,
- [StudioFeature.Config]: false,
})
const config = useStorage('studio-ui-config', { syncEditorAndRoute: true })
diff --git a/src/app/src/types/context.ts b/src/app/src/types/context.ts
index f41e1df3..e31b836f 100644
--- a/src/app/src/types/context.ts
+++ b/src/app/src/types/context.ts
@@ -3,12 +3,11 @@ import type { TreeItem } from './tree'
export enum StudioFeature {
Content = 'content',
Media = 'media',
- Config = 'config',
}
export enum StudioItemActionId {
CreateFolder = 'create-folder',
- CreateFile = 'create-file',
+ CreateDocument = 'create-document',
RevertItem = 'revert-item',
RenameItem = 'rename-item',
DeleteItem = 'delete-item',
@@ -36,7 +35,7 @@ export interface RenameFileParams {
export type ActionHandlerParams = {
[StudioItemActionId.CreateFolder]: string
- [StudioItemActionId.CreateFile]: CreateFileParams
+ [StudioItemActionId.CreateDocument]: CreateFileParams
[StudioItemActionId.RevertItem]: string
[StudioItemActionId.RenameItem]: RenameFileParams
[StudioItemActionId.DeleteItem]: string
diff --git a/src/app/src/types/database.ts b/src/app/src/types/database.ts
index d26fda29..b3076383 100644
--- a/src/app/src/types/database.ts
+++ b/src/app/src/types/database.ts
@@ -1,13 +1,15 @@
import type { CollectionItemBase, PageCollectionItemBase, DataCollectionItemBase } from '@nuxt/content'
+import type { BaseItem } from './item'
-export interface DatabaseItem extends CollectionItemBase {
+export interface DatabaseItem extends CollectionItemBase, BaseItem {
[key: string]: unknown
}
-export interface DatabasePageItem extends PageCollectionItemBase {
+export interface DatabasePageItem extends PageCollectionItemBase, BaseItem {
+ path: string
[key: string]: unknown
}
-export interface DatabaseDataItem extends DataCollectionItemBase {
+export interface DatabaseDataItem extends DataCollectionItemBase, BaseItem {
[key: string]: unknown
}
diff --git a/src/app/src/types/draft.ts b/src/app/src/types/draft.ts
index ab4f05e3..edadf53f 100644
--- a/src/app/src/types/draft.ts
+++ b/src/app/src/types/draft.ts
@@ -1,5 +1,6 @@
import type { GithubFile } from './github'
import type { DatabaseItem } from './database'
+import type { MediaItem } from './media'
export enum DraftStatus {
Deleted = 'deleted',
@@ -9,27 +10,42 @@ export enum DraftStatus {
Opened = 'opened',
}
-export interface DraftItem {
+export interface DraftItem {
id: string // nuxt/content id
fsPath: string // file path in content directory
status: DraftStatus // status
-}
-export interface DraftFileItem extends DraftItem {
- originalDatabaseItem?: DatabaseItem // original collection document saved in db
- originalGithubFile?: GithubFile // file fetched on gh
- content?: string // Drafted raw markdown content
- document?: DatabaseItem // Drafted parsed AST (body, frontmatter...)
+ githubFile: GithubFile // file fetched on gh
+ original?: T
+ modified?: T
+ /**
+ * - String: Markdown for docuemnts
+ * - Buffer: Media content
+ */
+ raw?: string | Buffer
}
-export interface DraftMediaItem extends DraftItem {
- oldPath?: string // Old path in public directory (used for revert a renamed file)
- content?: string // Base64 value
- url?: string // Public gh url
+// export interface DraftItem extends DraftItem {
+// originalDatabaseItem?: DatabaseItem // original collection document saved in db
+// originalGithubFile?: GithubFile // file fetched on gh
+// content?: string // Drafted raw markdown content
+// document?: DatabaseItem // Drafted parsed AST (body, frontmatter...)
+// }
- // Image metas
- width?: number
- height?: number
- size?: number
- mimeType?: string
-}
+// export interface DraftItem extends DraftItem {
+// originalMediaItem?: MediaItem // original collection document saved in db
+// originalGithubFile?: GithubFile // file fetched on gh
+// content?: Buffer // Drafted raw media content
+// media?: MediaItem // Drafted parsed AST (body, frontmatter...)
+
+// //
+// oldPath?: string // Old path in public directory (used for revert a renamed file)
+// // content?: string // Base64 value
+// url?: string // Public gh url
+
+// // Image metas
+// width?: number
+// height?: number
+// size?: number
+// mimeType?: string
+// }
diff --git a/src/app/src/types/index.ts b/src/app/src/types/index.ts
index c1ec5712..1f1bdb5c 100644
--- a/src/app/src/types/index.ts
+++ b/src/app/src/types/index.ts
@@ -1,9 +1,12 @@
import type { StudioUser } from './user'
import type { DatabaseItem } from './database'
import type { RouteLocationNormalized } from 'vue-router'
+import type { MediaItem } from './media'
+export * from './item'
export * from './draft'
export * from './database'
+export * from './media'
export * from './user'
export * from './tree'
export * from './github'
@@ -32,6 +35,14 @@ export interface StudioHost {
delete: (id: string) => Promise
detectActives: () => Array<{ id: string, title: string }>
}
+ media: {
+ get: (id: string) => Promise
+ getFileSystemPath: (id: string) => string
+ list: () => Promise
+ upsert: (id: string, upsertedDocument: MediaItem) => Promise
+ create: (fsPath: string, routePath: string, content: string) => Promise
+ delete: (id: string) => Promise
+ }
user: {
get: () => StudioUser
}
diff --git a/src/app/src/types/item.ts b/src/app/src/types/item.ts
new file mode 100644
index 00000000..8c0325fb
--- /dev/null
+++ b/src/app/src/types/item.ts
@@ -0,0 +1,6 @@
+export interface BaseItem {
+ id: string
+ extension: string
+ stem: string
+ path?: string
+}
diff --git a/src/app/src/types/media.ts b/src/app/src/types/media.ts
new file mode 100644
index 00000000..99d7109b
--- /dev/null
+++ b/src/app/src/types/media.ts
@@ -0,0 +1,5 @@
+import type { BaseItem } from './item'
+
+export interface MediaItem extends BaseItem {
+ [key: string]: unknown
+}
diff --git a/src/app/src/utils/context.ts b/src/app/src/utils/context.ts
index 35dd7983..1ef08053 100644
--- a/src/app/src/utils/context.ts
+++ b/src/app/src/utils/context.ts
@@ -5,11 +5,10 @@ import { StudioItemActionId } from '../types/context'
export const FEATURE_DISPLAY_MAP = {
[StudioFeature.Content]: 'Content files',
[StudioFeature.Media]: 'Media library',
- [StudioFeature.Config]: 'Application configuration',
} as const
export const oneStepActions: StudioItemActionId[] = [StudioItemActionId.RevertItem, StudioItemActionId.DeleteItem, StudioItemActionId.DuplicateItem]
-export const twoStepActions: StudioItemActionId[] = [StudioItemActionId.CreateFile, StudioItemActionId.CreateFolder, StudioItemActionId.RenameItem]
+export const twoStepActions: StudioItemActionId[] = [StudioItemActionId.CreateDocument, StudioItemActionId.CreateFolder, StudioItemActionId.RenameItem]
export const STUDIO_ITEM_ACTION_DEFINITIONS: StudioAction[] = [
{
@@ -19,7 +18,7 @@ export const STUDIO_ITEM_ACTION_DEFINITIONS: StudioAction[] = [
tooltip: 'Create a new folder',
},
{
- id: StudioItemActionId.CreateFile,
+ id: StudioItemActionId.CreateDocument,
label: 'Create file',
icon: 'i-lucide-file-plus',
tooltip: 'Create a new file',
@@ -64,7 +63,7 @@ export function computeActionItems(itemActions: StudioAction[], item?: TreeItem
// Item type filtering
switch (item.type) {
case 'file':
- forbiddenActions.push(StudioItemActionId.CreateFolder, StudioItemActionId.CreateFile)
+ forbiddenActions.push(StudioItemActionId.CreateFolder, StudioItemActionId.CreateDocument)
break
case 'directory':
forbiddenActions.push(StudioItemActionId.DuplicateItem)
diff --git a/src/app/src/utils/draft.ts b/src/app/src/utils/draft.ts
index 99d67051..1231fcf3 100644
--- a/src/app/src/utils/draft.ts
+++ b/src/app/src/utils/draft.ts
@@ -1,6 +1,4 @@
-import type { DatabaseItem, DatabasePageItem } from '../types/database'
-import { DraftStatus } from '../types/draft'
-import { ContentFileExtension } from '../types/content'
+import { type BaseItem, type DatabasePageItem, ContentFileExtension, DraftStatus } from '../types'
import { stringify } from 'minimark/stringify'
export const COLOR_STATUS_MAP: { [key in DraftStatus]?: string } = {
@@ -19,18 +17,18 @@ export const COLOR_UI_STATUS_MAP: { [key in DraftStatus]?: string } = {
[DraftStatus.Opened]: 'neutral',
} as const
-export function getDraftStatus(draftedDocument: DatabaseItem, originalDatabaseItem: DatabaseItem | undefined) {
- if (!originalDatabaseItem) {
+export function getDraftStatus(modified: BaseItem, original: BaseItem | undefined) {
+ if (!original) {
return DraftStatus.Created
}
- if (originalDatabaseItem.extension === ContentFileExtension.Markdown) {
- if (!isEqual(originalDatabaseItem as DatabasePageItem, draftedDocument as DatabasePageItem)) {
+ if (original.extension === ContentFileExtension.Markdown) {
+ if (!isEqual(original as DatabasePageItem, modified as DatabasePageItem)) {
return DraftStatus.Updated
}
}
else {
- if (JSON.stringify(originalDatabaseItem) !== JSON.stringify(draftedDocument)) {
+ if (JSON.stringify(original) !== JSON.stringify(modified)) {
return DraftStatus.Updated
}
}
@@ -40,7 +38,7 @@ export function getDraftStatus(draftedDocument: DatabaseItem, originalDatabaseIt
function isEqual(document1: DatabasePageItem, document2: DatabasePageItem) {
function removeLastStyle(document: DatabasePageItem) {
- if (document.body?.value[document.body?.value.length - 1][0] === 'style') {
+ if (document.body?.value[document.body?.value.length - 1]?.[0] === 'style') {
return { ...document, body: { ...document.body, value: document.body?.value.slice(0, -1) } }
}
return document
diff --git a/src/app/src/utils/tree.ts b/src/app/src/utils/tree.ts
index 1a129b92..ac02e53c 100644
--- a/src/app/src/utils/tree.ts
+++ b/src/app/src/utils/tree.ts
@@ -1,23 +1,24 @@
-import { DraftStatus, type DatabaseItem, type DraftFileItem, type TreeItem } from '../types'
+import { DraftStatus, type DraftItem, type TreeItem } from '../types'
import { withLeadingSlash } from 'ufo'
import { stripNumericPrefix } from './string'
import type { RouteLocationNormalized } from 'vue-router'
+import type { BaseItem } from '../types/item'
export const ROOT_ITEM: TreeItem = { id: 'root', name: 'content', fsPath: '/', type: 'root' }
-export function buildTree(dbItems: (DatabaseItem & { fsPath: string })[], draftList: DraftFileItem[] | null):
+export function buildTree(dbItems: ((BaseItem) & { fsPath: string })[], draftList: DraftItem[] | null):
TreeItem[] {
const tree: TreeItem[] = []
const directoryMap = new Map()
for (const dbItem of dbItems) {
- const collectionType = dbItem.path ? 'page' : 'data'
+ const itemHasPathField = 'path' in dbItem && dbItem.path
const fsPathSegments = dbItem.fsPath.split('/')
const directorySegments = fsPathSegments.slice(0, -1)
let fileName = fsPathSegments[fsPathSegments.length - 1].replace(/\.[^/.]+$/, '')
let routePathSegments: string[] | undefined
- if (collectionType === 'page') {
+ if (itemHasPathField) {
routePathSegments = (dbItem.path as string).split('/').slice(0, -1).filter(Boolean)
}
@@ -34,7 +35,7 @@ TreeItem[] {
type: 'file',
}
- if (collectionType === 'page') {
+ if (itemHasPathField) {
fileItem.routePath = dbItem.path as string
}
@@ -81,7 +82,7 @@ TreeItem[] {
children: [],
}
- if (collectionType === 'page') {
+ if (itemHasPathField) {
directory.routePath = dirRoutePathBuilder(i)
}
diff --git a/src/app/test/mocks/database.ts b/src/app/test/mocks/database.ts
index f5e0dbce..dc9e9184 100644
--- a/src/app/test/mocks/database.ts
+++ b/src/app/test/mocks/database.ts
@@ -4,7 +4,7 @@ export const dbItemsList: (DatabaseItem & { fsPath: string })[] = [
{
id: 'landing/index.md',
title: '',
- body: {},
+ body: { type: 'minimark', value: [] },
description: '',
extension: 'md',
meta: {},
@@ -21,7 +21,7 @@ export const dbItemsList: (DatabaseItem & { fsPath: string })[] = [
{
id: 'docs/1.getting-started/2.introduction.md',
title: 'Introduction',
- body: {},
+ body: { type: 'minimark', value: [] },
description: 'Welcome to Docus theme documentation.',
extension: 'md',
layout: null,
@@ -42,7 +42,7 @@ export const dbItemsList: (DatabaseItem & { fsPath: string })[] = [
{
id: 'docs/1.getting-started/3.installation.md',
title: 'Installation',
- body: {},
+ body: { type: 'minimark', value: [] },
description: 'Learn how to install Docus.',
extension: 'md',
layout: null,
diff --git a/src/app/test/utils/context.test.ts b/src/app/test/utils/context.test.ts
index 8323ba14..a65cebd4 100644
--- a/src/app/test/utils/context.test.ts
+++ b/src/app/test/utils/context.test.ts
@@ -16,7 +16,6 @@ describe('computeActionItems', () => {
const rootItem: TreeItem = {
type: 'root',
name: 'content',
- path: '/',
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, rootItem)
@@ -39,19 +38,18 @@ describe('computeActionItems', () => {
it('should filter out actions for file items without draft status', () => {
const fileItem: TreeItem = {
type: 'file',
- name: 'test.md',
- path: '/test.md',
+ name: 'test.md'
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.RevertItem)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile
+ && action.id !== StudioItemActionId.CreateDocument
&& action.id !== StudioItemActionId.RevertItem,
)
expect(result).toEqual(expectedActions)
@@ -61,19 +59,18 @@ describe('computeActionItems', () => {
const fileItem: TreeItem = {
type: 'file',
name: 'test.md',
- path: '/test.md',
status: DraftStatus.Opened,
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.RevertItem)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile
+ && action.id !== StudioItemActionId.CreateDocument
&& action.id !== StudioItemActionId.RevertItem,
)
expect(result).toEqual(expectedActions)
@@ -83,18 +80,17 @@ describe('computeActionItems', () => {
const fileItem: TreeItem = {
type: 'file',
name: 'test.md',
- path: '/test.md',
status: DraftStatus.Updated,
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile,
+ && action.id !== StudioItemActionId.CreateDocument,
)
expect(result).toEqual(expectedActions)
})
@@ -103,18 +99,17 @@ describe('computeActionItems', () => {
const fileItem: TreeItem = {
type: 'file',
name: 'test.md',
- path: '/test.md',
status: DraftStatus.Created,
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile,
+ && action.id !== StudioItemActionId.CreateDocument,
)
expect(result).toEqual(expectedActions)
})
@@ -123,21 +118,20 @@ describe('computeActionItems', () => {
const fileItem: TreeItem = {
type: 'file',
name: 'test.md',
- path: '/test.md',
status: DraftStatus.Deleted,
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.DuplicateItem)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.RenameItem)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.DeleteItem)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile
+ && action.id !== StudioItemActionId.CreateDocument
&& action.id !== StudioItemActionId.DuplicateItem
&& action.id !== StudioItemActionId.RenameItem
&& action.id !== StudioItemActionId.DeleteItem,
@@ -149,19 +143,18 @@ describe('computeActionItems', () => {
const fileItem: TreeItem = {
type: 'file',
name: 'test.md',
- path: '/test.md',
status: DraftStatus.Renamed,
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, fileItem)
expect(result.find(action => action.id === StudioItemActionId.CreateFolder)).toBeUndefined()
- expect(result.find(action => action.id === StudioItemActionId.CreateFile)).toBeUndefined()
+ expect(result.find(action => action.id === StudioItemActionId.CreateDocument)).toBeUndefined()
expect(result.find(action => action.id === StudioItemActionId.RenameItem)).toBeUndefined()
const expectedActions = STUDIO_ITEM_ACTION_DEFINITIONS.filter(action =>
action.id !== StudioItemActionId.CreateFolder
- && action.id !== StudioItemActionId.CreateFile
+ && action.id !== StudioItemActionId.CreateDocument
&& action.id !== StudioItemActionId.RenameItem,
)
expect(result).toEqual(expectedActions)
@@ -175,7 +168,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
} as TreeItem
const result = computeActionItems(STUDIO_ITEM_ACTION_DEFINITIONS, directoryItem)
@@ -194,7 +186,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
status: DraftStatus.Opened,
} as TreeItem
@@ -214,7 +205,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
status: DraftStatus.Updated,
} as TreeItem
@@ -232,7 +222,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
status: DraftStatus.Created,
} as TreeItem
@@ -250,7 +239,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
status: DraftStatus.Deleted,
} as TreeItem
@@ -272,7 +260,6 @@ describe('computeActionItems', () => {
const directoryItem: TreeItem = {
type: 'directory',
name: 'folder',
- path: '/folder',
status: DraftStatus.Renamed,
} as TreeItem
diff --git a/src/app/test/utils/draft.test.ts b/src/app/test/utils/draft.test.ts
index 1cf0211b..1fcd420d 100644
--- a/src/app/test/utils/draft.test.ts
+++ b/src/app/test/utils/draft.test.ts
@@ -10,7 +10,10 @@ describe('getDraftStatus', () => {
const draft: DatabaseItem = {
id: 'landing/index.md',
title: 'Home',
- body: {},
+ body: {
+ type: 'minimark',
+ value: [],
+ },
description: 'Home page',
extension: 'md',
stem: 'index',
diff --git a/src/app/test/utils/tree.test.ts b/src/app/test/utils/tree.test.ts
index 5d519f4b..7efb523c 100644
--- a/src/app/test/utils/tree.test.ts
+++ b/src/app/test/utils/tree.test.ts
@@ -3,7 +3,7 @@ import { buildTree, findParentFromId, findItemFromRoute, findItemFromId } from '
import { tree } from '../mocks/tree'
import type { TreeItem } from '../../src/types/tree'
import { dbItemsList } from '../mocks/database'
-import type { DraftFileItem } from '../../src/types/draft'
+import type { DraftItem } from '../../src/types/draft'
import { DraftStatus } from '../../src/types/draft'
import type { RouteLocationNormalized } from 'vue-router'
@@ -48,7 +48,7 @@ describe('buildTree', () => {
})
it('should build a tree from a list of database items and set file status for root file based on draft', () => {
- const draftList: DraftFileItem[] = [{
+ const draftList: DraftItem[] = [{
id: dbItemsList[0].id,
fsPath: 'index.md',
status: DraftStatus.Created,
@@ -62,7 +62,7 @@ describe('buildTree', () => {
})
it('should build a tree from a list of database items and set file status for nestedfile and parent directory based on draft', () => {
- const draftList: DraftFileItem[] = [{
+ const draftList: DraftItem[] = [{
id: dbItemsList[1].id,
fsPath: '1.getting-started/2.introduction.md',
status: DraftStatus.Updated,
@@ -85,7 +85,7 @@ describe('buildTree', () => {
})
it('should build a tree from a list of database items and set file status for nestedfile and parent directory based on draft (status is always updated in directory)', () => {
- const draftList: DraftFileItem[] = [{
+ const draftList: DraftItem[] = [{
id: dbItemsList[1].id,
fsPath: '1.getting-started/2.introduction.md',
status: DraftStatus.Created,
diff --git a/src/module/src/module.ts b/src/module/src/module.ts
index fb41c608..351459bf 100644
--- a/src/module/src/module.ts
+++ b/src/module/src/module.ts
@@ -1,7 +1,9 @@
-import { defineNuxtModule, createResolver, addPlugin, extendViteConfig, useLogger, addServerHandler } from '@nuxt/kit'
+import { defineNuxtModule, createResolver, addPlugin, extendViteConfig, useLogger, addServerHandler, addTemplate } from '@nuxt/kit'
import { createHash } from 'node:crypto'
import { defu } from 'defu'
import { resolve } from 'node:path'
+import fsDriver from 'unstorage/drivers/fs'
+import { createStorage } from 'unstorage'
interface ModuleOptions {
devStorage?: boolean
@@ -99,6 +101,40 @@ export default defineNuxtModule({
})
}
+ const assetsStorage = createStorage({
+ driver: fsDriver({
+ base: resolve(nuxt.options.rootDir, 'public'),
+ }),
+ })
+ assetsStorage.getKeys()
+ .then((keys) => {
+ addTemplate({
+ filename: 'content-studio-public-assets.mjs',
+ getContents: () => {
+ return [
+ 'import { createStorage } from \'unstorage\'',
+ 'const storage = createStorage({})',
+ '',
+ ...keys.map((key) => {
+ key = `public-assets:${key}`
+ const value = {
+ id: key,
+ extension: key.split('.').pop(),
+ stem: key.split('.').slice(0, -1).join('.'),
+ path: '/' + key.replace(/:/g, '/'),
+ }
+ return `storage.setItem('${key}', ${JSON.stringify(value)})`
+ }),
+ '',
+ 'export const publicAssetsStorage = storage',
+ ].join('\n')
+ },
+ })
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
addServerHandler({
route: '/__nuxt_content/studio/auth/github',
handler: runtime('./server/routes/auth/github.get'),
diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts
index cadaea79..bb89b511 100644
--- a/src/module/src/runtime/host.ts
+++ b/src/module/src/runtime/host.ts
@@ -4,12 +4,13 @@ import type { CollectionItemBase, DatabaseAdapter } from '@nuxt/content'
import type { ContentDatabaseAdapter } from '../types/content'
import { getCollectionByFilePath, generateIdFromFsPath, createCollectionDocument, generateRecordDeletion, generateRecordInsert, getCollectionInfo } from './utils/collections'
import { kebabCase } from 'lodash'
-import type { UseStudioHost, StudioHost, StudioUser, DatabaseItem } from 'nuxt-studio/app'
+import type { UseStudioHost, StudioHost, StudioUser, DatabaseItem, MediaItem } from 'nuxt-studio/app'
import type { RouteLocationNormalized, Router } from 'vue-router'
import { generateDocumentFromContent } from './utils/content'
// @ts-expect-error queryCollection is not defined in .nuxt/imports.d.ts
import { queryCollection, queryCollectionItemSurroundings, queryCollectionNavigation, queryCollectionSearchSections } from '#imports'
import { collections } from '#content/preview'
+import { publicAssetsStorage } from '#build/content-studio-public-assets'
function getSidebarWidth(): number {
let sidebarWidth = 440
@@ -205,6 +206,40 @@ export function useStudioHost(user: StudioUser): StudioHost {
},
},
+ media: {
+ get: async (id: string): Promise => {
+ return await publicAssetsStorage.getItem(id) as MediaItem
+ },
+ getFileSystemPath: (id: string) => {
+ return id.split(':').slice(1).join('/')
+ },
+ list: async (): Promise => {
+ return await Promise.all(await publicAssetsStorage.getKeys().then(keys => keys.map(key => publicAssetsStorage.getItem(key)))) as MediaItem[]
+ },
+ upsert: async (id: string, upsertedDocument: MediaItem) => {
+ await publicAssetsStorage.setItem(id, upsertedDocument)
+ },
+ create: async (fsPath: string, routePath: string, content: string) => {
+ await publicAssetsStorage.setItem(fsPath, {
+ id: fsPath,
+ fsPath,
+ routePath,
+ content,
+ })
+ return {
+ id: fsPath,
+ extension: fsPath.split('.').pop(),
+ stem: fsPath.split('.').slice(0, -1).join('.'),
+ fsPath,
+ routePath,
+ content,
+ } as MediaItem
+ },
+ delete: async (id: string) => {
+ await publicAssetsStorage.removeItem(id)
+ },
+ },
+
app: {
requestRerender: () => {
useNuxtApp().hooks.callHookParallel('app:data:refresh')
diff --git a/src/module/src/types/global.d.ts b/src/module/src/types/global.d.ts
index b03e8ee5..82b881e1 100644
--- a/src/module/src/types/global.d.ts
+++ b/src/module/src/types/global.d.ts
@@ -5,3 +5,9 @@ declare module '#content/preview' {
export const gitInfo: GitInfo
export const appConfigSchema: Record
}
+
+declare module '#build/content-studio-public-assets' {
+ import type { Storage } from 'unstorage'
+
+ export const publicAssetsStorage: Storage
+}