Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@5111",
"@octokit/types": "^15.0.0",
"@tailwindcss/typography": "latest",
"@types/js-yaml": "^4.0.9",
"@vitejs/plugin-vue": "^6.0.1",
"eslint": "^9.36.0",
"modern-monaco": "^0.2.2",
Expand Down Expand Up @@ -79,6 +80,7 @@
"defu": "^6.1.4",
"destr": "^2.0.5",
"idb-keyval": "^6.2.2",
"js-yaml": "^4.1.0",
"minimark": "^0.2.0",
"ofetch": "^1.4.1",
"unstorage": "^1.17.1"
Expand Down
15 changes: 13 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions src/app/src/components/shared/item/ItemCard.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script setup lang="ts">
import type { TreeItem } from '../../../types'
import { type TreeItem, TreeStatus } from '../../../types'
import type { PropType } from 'vue'
import { computed } from 'vue'
import { Image } from '@unpic/vue'
import { titleCase } from 'scule'
import { COLOR_UI_STATUS_MAP } from '../../../utils/tree'
import { DraftStatus } from '../../../types/draft'

const props = defineProps({
item: {
Expand Down Expand Up @@ -62,7 +61,7 @@ const statusRingColor = computed(() => props.item.status ? `ring-(--ui-${COLOR_U
/>
<div
v-else
class="z-[-1] aspect-video bg-muted"
class="z-[-1] aspect-video bg-elevated"
/>
<div
v-if="itemExtensionIcon"
Expand All @@ -75,7 +74,7 @@ const statusRingColor = computed(() => props.item.status ? `ring-(--ui-${COLOR_U
</div>
</div>
<ItemBadge
v-if="item.status && item.status !== DraftStatus.Opened"
v-if="item.status && item.status !== TreeStatus.Opened"
:status="item.status"
class="absolute top-2 right-2"
/>
Expand Down
65 changes: 41 additions & 24 deletions src/app/src/components/shared/item/ItemCardForm.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
<script setup lang="ts">
import { computed, reactive, type PropType } from 'vue'
import { Image } from '@unpic/vue'
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import { type CreateFileParams, type CreateFolderParams, type RenameFileParams, type StudioAction, type TreeItem, ContentFileExtension } from '../../../types'
import { joinURL, withLeadingSlash } from 'ufo'
import { joinURL, withLeadingSlash, withoutLeadingSlash } from 'ufo'
import { contentFileExtensions } from '../../../utils/content'
import { useStudio } from '../../../composables/useStudio'
import { StudioItemActionId } from '../../../types'
import { stripNumericPrefix } from '../../../utils/string'
import { defineShortcuts } from '#imports'
import { upperFirst } from 'scule'

const { context } = useStudio()

defineShortcuts({
escape: () => {
context.unsetActionInProgress()
},
})

const props = defineProps({
actionId: {
type: String as PropType<StudioItemActionId.CreateDocument | StudioItemActionId.CreateFolder | StudioItemActionId.RenameItem>,
Expand Down Expand Up @@ -48,6 +55,14 @@ const action = computed<StudioAction>(() => {
return context.itemActions.value.find(action => action.id === props.actionId)!
})

const isFolderAction = computed(() => {
return props.actionId === StudioItemActionId.CreateFolder
|| (
props.actionId === StudioItemActionId.RenameItem
&& props.renamedItem.type === 'directory'
)
})

const itemExtensionIcon = computed<string>(() => {
return {
md: 'i-ph-markdown-logo',
Expand All @@ -74,26 +89,23 @@ const tooltipText = computed(() => {
})

function onSubmit(_event: FormSubmitEvent<Schema>) {
const fsPath = joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)

let params: CreateFileParams | CreateFolderParams | RenameFileParams
switch (props.actionId) {
case StudioItemActionId.CreateDocument:
params = {
routePath: routePath.value,
fsPath,
content: `New ${state.name} file`,
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
content: `# ${upperFirst(state.name)} file`,
}
break
case StudioItemActionId.CreateFolder:
params = {
fsPath,
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, state.name)),
}
break
case StudioItemActionId.RenameItem:
params = {
newFsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
id: props.renamedItem.id,
newFsPath: joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`),
}
break
}
Expand All @@ -113,14 +125,11 @@ function onSubmit(_event: FormSubmitEvent<Schema>) {
reverse
class="hover:bg-white relative w-full min-w-0"
>
<div class="relative">
<Image
src="https://placehold.co/1920x1080/f9fafc/f9fafc"
width="426"
height="240"
alt="Card placeholder"
class="z-[-1] rounded-t-lg"
/>
<div
v-if="!isFolderAction"
class="relative"
>
<div class="z-[-1] aspect-video rounded-lg bg-elevated" />
<div class="absolute inset-0 flex items-center justify-center">
<UIcon
:name="itemExtensionIcon"
Expand Down Expand Up @@ -170,15 +179,23 @@ function onSubmit(_event: FormSubmitEvent<Schema>) {
<template #error>
<span />
</template>
<UInput
v-model="state.name"
variant="soft"
autofocus
placeholder="File name"
class="w-full"
/>
<div class="flex items-center gap-1">
<UIcon
v-if="isFolderAction"
name="i-lucide-folder"
class="h-4 w-4 shrink-0 text-muted"
/>
<UInput
v-model="state.name"
variant="soft"
autofocus
:placeholder="isFolderAction ? 'Folder name' : 'File name'"
class="w-full"
/>
</div>
</UFormField>
<UFormField
v-if="!isFolderAction"
name="extension"
:ui="{ error: 'hidden' }"
>
Expand Down
22 changes: 19 additions & 3 deletions src/app/src/composables/useContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type { useTree } from './useTree'
import { useRoute } from 'vue-router'
import { findDescendantsFileItemsFromId } from '../utils/tree'
import type { useDraftMedias } from './useDraftMedias'
import { joinURL } from 'ufo'
import { upperFirst } from 'scule'

export const useContext = createSharedComposable((
host: StudioHost,
Expand Down Expand Up @@ -73,11 +75,25 @@ export const useContext = createSharedComposable((

const itemActionHandler: { [K in StudioItemActionId]: (args: ActionHandlerParams[K]) => Promise<void> } = {
[StudioItemActionId.CreateFolder]: async (params: CreateFolderParams) => {
alert(`create folder in ${params.fsPath}`)
const { fsPath } = params
const folderName = fsPath.split('/').pop()!
const rootDocumentFsPath = joinURL(fsPath, 'index.md')
const navigationDocumentFsPath = joinURL(fsPath, '.navigation.yml')

const navigationDocument = await host.document.create(navigationDocumentFsPath, `title: ${folderName}`)
const rootDocument = await host.document.create(rootDocumentFsPath, `# ${upperFirst(folderName)} root file`)

await activeTree.value.draft.create(navigationDocument)

unsetActionInProgress()

const rootDocumentDraftItem = await activeTree.value.draft.create(rootDocument)

await activeTree.value.selectItemById(rootDocumentDraftItem.id)
},
[StudioItemActionId.CreateDocument]: async (params: CreateFileParams) => {
const { fsPath, routePath, content } = params
const document = await host.document.create(fsPath, routePath, content)
const { fsPath, content } = params
const document = await host.document.create(fsPath, content)
const draftItem = await activeTree.value.draft.create(document)
await activeTree.value.selectItemById(draftItem.id)
},
Expand Down
9 changes: 2 additions & 7 deletions src/app/src/composables/useDraftDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { generateContentFromDocument } from '../utils/content'
import { findDescendantsFromId, getDraftStatus } from '../utils/draft'
import { createSharedComposable } from '@vueuse/core'
import { useHooks } from './useHooks'
import { stripNumericPrefix } from '../utils/string'
import { joinURL } from 'ufo'

const storage = createStorage({
Expand Down Expand Up @@ -178,15 +177,13 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git:
throw new Error(`Database item not found for document ${id}`)
}

const nameWithoutExtension = newFsPath.split('/').pop()!.split('.').slice(0, -1).join('.')
const newRoutePath = `${currentDbItem.path!.split('/').slice(0, -1).join('/')}/${nameWithoutExtension}`
const content = await generateContentFromDocument(currentDbItem)

// Delete renamed draft item
await remove([id])

// Create new draft item
const newDbItem = await host.document.create(newFsPath, newRoutePath, content!)
const newDbItem = await host.document.create(newFsPath, content!)
return await create(newDbItem, currentDbItem)
}

Expand All @@ -202,16 +199,14 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git:
}

const currentFsPath = currentDraftItem?.fsPath || host.document.getFileSystemPath(id)
const currentRoutePath = currentDbItem.path!
const currentContent = await generateContentFromDocument(currentDbItem) || ''
const currentName = currentFsPath.split('/').pop()!
const currentExtension = currentName.split('.').pop()!
const currentNameWithoutExtension = currentName.split('.').slice(0, -1).join('.')

const newFsPath = `${currentFsPath.split('/').slice(0, -1).join('/')}/${currentNameWithoutExtension}-copy.${currentExtension}`
const newRoutePath = `${currentRoutePath.split('/').slice(0, -1).join('/')}/${stripNumericPrefix(currentNameWithoutExtension)}-copy`

const newDbItem = await host.document.create(newFsPath, newRoutePath, currentContent)
const newDbItem = await host.document.create(newFsPath, currentContent)

return await create(newDbItem)
}
Expand Down
8 changes: 6 additions & 2 deletions src/app/src/composables/useStudio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export const useStudio = createSharedComposable(() => {
const isReady = ref(false)
const ui = useUI(host)
const draftDocuments = useDraftDocuments(host, git)
const documentTree = useTree(StudioFeature.Content, host, draftDocuments)
const documentTree = useTree(StudioFeature.Content, host, ui, draftDocuments)

const draftMedias = useDraftMedias(host, git)
const mediaTree = useTree(StudioFeature.Media, host, draftMedias)
const mediaTree = useTree(StudioFeature.Media, host, ui, draftMedias)

const context = useContext(host, documentTree, mediaTree)

Expand All @@ -40,6 +40,10 @@ export const useStudio = createSharedComposable(() => {

host.on.routeChange(async (to: RouteLocationNormalized, _from: RouteLocationNormalized) => {
if (ui.isOpen.value && ui.config.value.syncEditorAndRoute) {
if (documentTree.currentItem.value.routePath === to.path) {
return
}

await documentTree.selectByRoute(to)
}
// setTimeout(() => {
Expand Down
5 changes: 3 additions & 2 deletions src/app/src/composables/useTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import type { useDraftMedias } from './useDraftMedias'
import { buildTree, findItemFromId, findItemFromRoute, ROOT_ITEM, findParentFromId } from '../utils/tree'
import type { RouteLocationNormalized } from 'vue-router'
import { useHooks } from './useHooks'
import type { useUI } from './useUI'

export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType<typeof useDraftDocuments | typeof useDraftMedias>) => {
export const useTree = (type: StudioFeature, host: StudioHost, ui: ReturnType<typeof useUI>, draft: ReturnType<typeof useDraftDocuments | typeof useDraftMedias>) => {
const hooks = useHooks()

const tree = ref<TreeItem[]>([])
Expand Down Expand Up @@ -40,7 +41,7 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType
async function select(item: TreeItem) {
currentItem.value = item || ROOT_ITEM
if (item?.type === 'file') {
if (type === StudioFeature.Content) {
if (type === StudioFeature.Content && ui.config.value.syncEditorAndRoute) {
host.app.navigateTo(item.routePath!)
}

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/app/src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export interface StudioAction<K extends StudioItemActionId = StudioItemActionId>
export interface CreateFolderParams {
fsPath: string
}

export interface CreateFileParams {
fsPath: string
routePath: string
content: string
}

Expand Down
6 changes: 3 additions & 3 deletions src/app/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DatabaseItem } from './database'
import type { RouteLocationNormalized } from 'vue-router'
import type { MediaItem } from './media'
import type { Repository } from './git'
import type { ComponentMeta } from './components'
import type { ComponentMeta } from './component'

export * from './item'
export * from './draft'
Expand All @@ -14,7 +14,7 @@ export * from './tree'
export * from './git'
export * from './context'
export * from './content'
export * from './components'
export * from './component'

export interface StudioHost {
meta: {
Expand All @@ -40,7 +40,7 @@ export interface StudioHost {
getFileSystemPath: (id: string) => string
list: () => Promise<DatabaseItem[]>
upsert: (id: string, upsertedDocument: DatabaseItem) => Promise<void>
create: (fsPath: string, routePath: string, content: string) => Promise<DatabaseItem>
create: (fsPath: string, content: string) => Promise<DatabaseItem>
delete: (id: string) => Promise<void>
detectActives: () => Array<{ id: string, title: string }>
}
Expand Down
Loading
Loading