Skip to content

Commit 59c57d8

Browse files
authored
feat(context): create folder action (#25)
1 parent 1a1fd8e commit 59c57d8

File tree

19 files changed

+249
-91
lines changed

19 files changed

+249
-91
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@nuxt/ui": "https://pkg.pr.new/@nuxt/ui@5111",
5151
"@octokit/types": "^15.0.0",
5252
"@tailwindcss/typography": "latest",
53+
"@types/js-yaml": "^4.0.9",
5354
"@vitejs/plugin-vue": "^6.0.1",
5455
"eslint": "^9.36.0",
5556
"modern-monaco": "^0.2.2",
@@ -79,6 +80,7 @@
7980
"defu": "^6.1.4",
8081
"destr": "^2.0.5",
8182
"idb-keyval": "^6.2.2",
83+
"js-yaml": "^4.1.0",
8284
"minimark": "^0.2.0",
8385
"ofetch": "^1.4.1",
8486
"unstorage": "^1.17.1"

pnpm-lock.yaml

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/src/components/shared/item/ItemCard.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<script setup lang="ts">
2-
import type { TreeItem } from '../../../types'
2+
import { type TreeItem, TreeStatus } from '../../../types'
33
import type { PropType } from 'vue'
44
import { computed } from 'vue'
55
import { Image } from '@unpic/vue'
66
import { titleCase } from 'scule'
77
import { COLOR_UI_STATUS_MAP } from '../../../utils/tree'
8-
import { DraftStatus } from '../../../types/draft'
98
109
const props = defineProps({
1110
item: {
@@ -62,7 +61,7 @@ const statusRingColor = computed(() => props.item.status ? `ring-(--ui-${COLOR_U
6261
/>
6362
<div
6463
v-else
65-
class="z-[-1] aspect-video bg-muted"
64+
class="z-[-1] aspect-video bg-elevated"
6665
/>
6766
<div
6867
v-if="itemExtensionIcon"
@@ -75,7 +74,7 @@ const statusRingColor = computed(() => props.item.status ? `ring-(--ui-${COLOR_U
7574
</div>
7675
</div>
7776
<ItemBadge
78-
v-if="item.status && item.status !== DraftStatus.Opened"
77+
v-if="item.status && item.status !== TreeStatus.Opened"
7978
:status="item.status"
8079
class="absolute top-2 right-2"
8180
/>

src/app/src/components/shared/item/ItemCardForm.vue

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
<script setup lang="ts">
22
import { computed, reactive, type PropType } from 'vue'
3-
import { Image } from '@unpic/vue'
43
import * as z from 'zod'
54
import type { FormSubmitEvent } from '@nuxt/ui'
65
import { type CreateFileParams, type CreateFolderParams, type RenameFileParams, type StudioAction, type TreeItem, ContentFileExtension } from '../../../types'
7-
import { joinURL, withLeadingSlash } from 'ufo'
6+
import { joinURL, withLeadingSlash, withoutLeadingSlash } from 'ufo'
87
import { contentFileExtensions } from '../../../utils/content'
98
import { useStudio } from '../../../composables/useStudio'
109
import { StudioItemActionId } from '../../../types'
1110
import { stripNumericPrefix } from '../../../utils/string'
11+
import { defineShortcuts } from '#imports'
12+
import { upperFirst } from 'scule'
1213
1314
const { context } = useStudio()
1415
16+
defineShortcuts({
17+
escape: () => {
18+
context.unsetActionInProgress()
19+
},
20+
})
21+
1522
const props = defineProps({
1623
actionId: {
1724
type: String as PropType<StudioItemActionId.CreateDocument | StudioItemActionId.CreateFolder | StudioItemActionId.RenameItem>,
@@ -48,6 +55,14 @@ const action = computed<StudioAction>(() => {
4855
return context.itemActions.value.find(action => action.id === props.actionId)!
4956
})
5057
58+
const isFolderAction = computed(() => {
59+
return props.actionId === StudioItemActionId.CreateFolder
60+
|| (
61+
props.actionId === StudioItemActionId.RenameItem
62+
&& props.renamedItem.type === 'directory'
63+
)
64+
})
65+
5166
const itemExtensionIcon = computed<string>(() => {
5267
return {
5368
md: 'i-ph-markdown-logo',
@@ -74,26 +89,23 @@ const tooltipText = computed(() => {
7489
})
7590
7691
function onSubmit(_event: FormSubmitEvent<Schema>) {
77-
const fsPath = joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)
78-
7992
let params: CreateFileParams | CreateFolderParams | RenameFileParams
8093
switch (props.actionId) {
8194
case StudioItemActionId.CreateDocument:
8295
params = {
83-
routePath: routePath.value,
84-
fsPath,
85-
content: `New ${state.name} file`,
96+
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
97+
content: `# ${upperFirst(state.name)} file`,
8698
}
8799
break
88100
case StudioItemActionId.CreateFolder:
89101
params = {
90-
fsPath,
102+
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, state.name)),
91103
}
92104
break
93105
case StudioItemActionId.RenameItem:
94106
params = {
107+
newFsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
95108
id: props.renamedItem.id,
96-
newFsPath: joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`),
97109
}
98110
break
99111
}
@@ -113,14 +125,11 @@ function onSubmit(_event: FormSubmitEvent<Schema>) {
113125
reverse
114126
class="hover:bg-white relative w-full min-w-0"
115127
>
116-
<div class="relative">
117-
<Image
118-
src="https://placehold.co/1920x1080/f9fafc/f9fafc"
119-
width="426"
120-
height="240"
121-
alt="Card placeholder"
122-
class="z-[-1] rounded-t-lg"
123-
/>
128+
<div
129+
v-if="!isFolderAction"
130+
class="relative"
131+
>
132+
<div class="z-[-1] aspect-video rounded-lg bg-elevated" />
124133
<div class="absolute inset-0 flex items-center justify-center">
125134
<UIcon
126135
:name="itemExtensionIcon"
@@ -170,15 +179,23 @@ function onSubmit(_event: FormSubmitEvent<Schema>) {
170179
<template #error>
171180
<span />
172181
</template>
173-
<UInput
174-
v-model="state.name"
175-
variant="soft"
176-
autofocus
177-
placeholder="File name"
178-
class="w-full"
179-
/>
182+
<div class="flex items-center gap-1">
183+
<UIcon
184+
v-if="isFolderAction"
185+
name="i-lucide-folder"
186+
class="h-4 w-4 shrink-0 text-muted"
187+
/>
188+
<UInput
189+
v-model="state.name"
190+
variant="soft"
191+
autofocus
192+
:placeholder="isFolderAction ? 'Folder name' : 'File name'"
193+
class="w-full"
194+
/>
195+
</div>
180196
</UFormField>
181197
<UFormField
198+
v-if="!isFolderAction"
182199
name="extension"
183200
:ui="{ error: 'hidden' }"
184201
>

src/app/src/composables/useContext.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type { useTree } from './useTree'
1818
import { useRoute } from 'vue-router'
1919
import { findDescendantsFileItemsFromId } from '../utils/tree'
2020
import type { useDraftMedias } from './useDraftMedias'
21+
import { joinURL } from 'ufo'
22+
import { upperFirst } from 'scule'
2123

2224
export const useContext = createSharedComposable((
2325
host: StudioHost,
@@ -73,11 +75,25 @@ export const useContext = createSharedComposable((
7375

7476
const itemActionHandler: { [K in StudioItemActionId]: (args: ActionHandlerParams[K]) => Promise<void> } = {
7577
[StudioItemActionId.CreateFolder]: async (params: CreateFolderParams) => {
76-
alert(`create folder in ${params.fsPath}`)
78+
const { fsPath } = params
79+
const folderName = fsPath.split('/').pop()!
80+
const rootDocumentFsPath = joinURL(fsPath, 'index.md')
81+
const navigationDocumentFsPath = joinURL(fsPath, '.navigation.yml')
82+
83+
const navigationDocument = await host.document.create(navigationDocumentFsPath, `title: ${folderName}`)
84+
const rootDocument = await host.document.create(rootDocumentFsPath, `# ${upperFirst(folderName)} root file`)
85+
86+
await activeTree.value.draft.create(navigationDocument)
87+
88+
unsetActionInProgress()
89+
90+
const rootDocumentDraftItem = await activeTree.value.draft.create(rootDocument)
91+
92+
await activeTree.value.selectItemById(rootDocumentDraftItem.id)
7793
},
7894
[StudioItemActionId.CreateDocument]: async (params: CreateFileParams) => {
79-
const { fsPath, routePath, content } = params
80-
const document = await host.document.create(fsPath, routePath, content)
95+
const { fsPath, content } = params
96+
const document = await host.document.create(fsPath, content)
8197
const draftItem = await activeTree.value.draft.create(document)
8298
await activeTree.value.selectItemById(draftItem.id)
8399
},

src/app/src/composables/useDraftDocuments.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { generateContentFromDocument } from '../utils/content'
88
import { findDescendantsFromId, getDraftStatus } from '../utils/draft'
99
import { createSharedComposable } from '@vueuse/core'
1010
import { useHooks } from './useHooks'
11-
import { stripNumericPrefix } from '../utils/string'
1211
import { joinURL } from 'ufo'
1312

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

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

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

188185
// Create new draft item
189-
const newDbItem = await host.document.create(newFsPath, newRoutePath, content!)
186+
const newDbItem = await host.document.create(newFsPath, content!)
190187
return await create(newDbItem, currentDbItem)
191188
}
192189

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

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

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

214-
const newDbItem = await host.document.create(newFsPath, newRoutePath, currentContent)
209+
const newDbItem = await host.document.create(newFsPath, currentContent)
215210

216211
return await create(newDbItem)
217212
}

src/app/src/composables/useStudio.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export const useStudio = createSharedComposable(() => {
2424
const isReady = ref(false)
2525
const ui = useUI(host)
2626
const draftDocuments = useDraftDocuments(host, git)
27-
const documentTree = useTree(StudioFeature.Content, host, draftDocuments)
27+
const documentTree = useTree(StudioFeature.Content, host, ui, draftDocuments)
2828

2929
const draftMedias = useDraftMedias(host, git)
30-
const mediaTree = useTree(StudioFeature.Media, host, draftMedias)
30+
const mediaTree = useTree(StudioFeature.Media, host, ui, draftMedias)
3131

3232
const context = useContext(host, documentTree, mediaTree)
3333

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

4141
host.on.routeChange(async (to: RouteLocationNormalized, _from: RouteLocationNormalized) => {
4242
if (ui.isOpen.value && ui.config.value.syncEditorAndRoute) {
43+
if (documentTree.currentItem.value.routePath === to.path) {
44+
return
45+
}
46+
4347
await documentTree.selectByRoute(to)
4448
}
4549
// setTimeout(() => {

src/app/src/composables/useTree.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import type { useDraftMedias } from './useDraftMedias'
55
import { buildTree, findItemFromId, findItemFromRoute, ROOT_ITEM, findParentFromId } from '../utils/tree'
66
import type { RouteLocationNormalized } from 'vue-router'
77
import { useHooks } from './useHooks'
8+
import type { useUI } from './useUI'
89

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

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

File renamed without changes.

src/app/src/types/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ export interface StudioAction<K extends StudioItemActionId = StudioItemActionId>
3131
export interface CreateFolderParams {
3232
fsPath: string
3333
}
34+
3435
export interface CreateFileParams {
3536
fsPath: string
36-
routePath: string
3737
content: string
3838
}
3939

0 commit comments

Comments
 (0)