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: 1 addition & 1 deletion src/app/src/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const userMenuItems = computed(() => [
>
<USwitch
v-model="showTechnicalMode"
label="Technical view"
label="Developer view"
size="xs"
:ui="{ root: 'w-full flex-row-reverse justify-between', wrapper: 'ms-0' }"
/>
Expand Down
14 changes: 8 additions & 6 deletions src/app/src/components/shared/item/ItemBreadcrumb.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,26 @@ import type { DropdownMenuItem } from '@nuxt/ui/components/DropdownMenu.vue.d.ts
import { computed, unref } from 'vue'
import { type TreeItem, TreeStatus } from '../../../types'
import { useStudio } from '../../../composables/useStudio'
import { findParentFromId, ROOT_ITEM } from '../../../utils/tree'
import { findParentFromId } from '../../../utils/tree'

const { context } = useStudio()

const currentItem = computed(() => context.activeTree.value.currentItem.value)
const tree = computed(() => context.activeTree.value.root.value)

const items = computed<BreadcrumbItem[]>(() => {
const rootItem = {
const rootTreeItem = context.activeTree.value.rootItem.value
const rootBreadcrumbItem = {
icon: 'i-lucide-folder-git',
label: rootTreeItem.name,
onClick: () => {
// TODO: update for ROOT_DOCUMENT_ITEM and ROOT_MEDIA_ITEM
context.activeTree.value.select(ROOT_ITEM)
context.activeTree.value.select(rootTreeItem)
},
}

if (currentItem.value.id === ROOT_ITEM.id) {
return [rootItem]
if (currentItem.value.id === rootTreeItem.id) {
return [rootBreadcrumbItem]
}

const breadcrumbItems: BreadcrumbItem[] = []
Expand All @@ -39,7 +41,7 @@ const items = computed<BreadcrumbItem[]>(() => {
currentTreeItem = findParentFromId(tree.value, currentTreeItem.id)
}

const allItems = [rootItem, ...breadcrumbItems]
const allItems = [rootBreadcrumbItem, ...breadcrumbItems]

// Handle ellipsis dropdown
if (allItems.length > 3) {
Expand Down
26 changes: 18 additions & 8 deletions src/app/src/components/shared/item/ItemCardForm.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed, reactive, type PropType } from '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, withoutLeadingSlash } from 'ufo'
import { contentFileExtensions } from '../../../utils/content'
Expand Down Expand Up @@ -35,14 +34,21 @@ const props = defineProps({
})

const originalName = computed(() => props.renamedItem?.name || '')
const originalExtension = computed(() => props.renamedItem?.id.split('.').pop() as ContentFileExtension || ContentFileExtension.Markdown)
const originalExtension = computed(() => {
const ext = props.renamedItem?.id.split('.').pop()
if (ext && contentFileExtensions.includes(ext as ContentFileExtension)) {
return ext as ContentFileExtension
}

return ContentFileExtension.Markdown
})

const schema = z.object({
name: z.string()
.min(1, 'Name cannot be empty')
.refine((name: string) => !name.endsWith('.'), 'Name cannot end with "."')
.refine((name: string) => !name.startsWith('/'), 'Name cannot start with "/"'),
extension: z.enum(ContentFileExtension),
extension: z.optional(z.enum(ContentFileExtension)),
})

type Schema = z.output<typeof schema>
Expand Down Expand Up @@ -88,23 +94,27 @@ const tooltipText = computed(() => {
}
})

function onSubmit(_event: FormSubmitEvent<Schema>) {
function onSubmit() {
let params: CreateFileParams | CreateFolderParams | RenameFileParams
const newFsPath = isFolderAction.value
? joinURL(props.parentItem.fsPath, state.name)
: joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)

switch (props.actionId) {
case StudioItemActionId.CreateDocument:
params = {
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
fsPath: withoutLeadingSlash(newFsPath),
content: `# ${upperFirst(state.name)} file`,
}
break
case StudioItemActionId.CreateFolder:
params = {
fsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, state.name)),
fsPath: withoutLeadingSlash(newFsPath),
}
break
case StudioItemActionId.RenameItem:
params = {
newFsPath: withoutLeadingSlash(joinURL(props.parentItem.fsPath, `${state.name}.${state.extension}`)),
newFsPath: withoutLeadingSlash(newFsPath),
id: props.renamedItem.id,
}
break
Expand All @@ -126,7 +136,7 @@ function onSubmit(_event: FormSubmitEvent<Schema>) {
class="hover:bg-white relative w-full min-w-0"
>
<div
v-if="!isFolderAction"
v-show="!isFolderAction"
class="relative"
>
<div class="z-[-1] aspect-video rounded-lg bg-elevated" />
Expand Down
13 changes: 11 additions & 2 deletions src/app/src/composables/useContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { oneStepActions, STUDIO_ITEM_ACTION_DEFINITIONS, twoStepActions } from '
import { useModal } from './useModal'
import type { useTree } from './useTree'
import { useRoute } from 'vue-router'
import { findDescendantsFileItemsFromId } from '../utils/tree'
import { findDescendantsFileItemsFromId, findItemFromId } from '../utils/tree'
import type { useDraftMedias } from './useDraftMedias'
import { joinURL } from 'ufo'
import { upperFirst } from 'scule'
Expand Down Expand Up @@ -109,7 +109,16 @@ export const useContext = createSharedComposable((
},
[StudioItemActionId.RenameItem]: async (params: TreeItem | RenameFileParams) => {
const { id, newFsPath } = params as RenameFileParams
await activeTree.value.draft.rename(id, newFsPath)

const descendants = findDescendantsFileItemsFromId(activeTree.value.root.value, id)
if (descendants.length > 0) {
const parent = findItemFromId(activeTree.value.root.value, id)!
const itemsToRename = descendants.map(item => ({ id: item.id, newFsPath: item.fsPath.replace(parent.fsPath, newFsPath) }))
await activeTree.value.draft.rename(itemsToRename)
}
else {
await activeTree.value.draft.rename([{ id, newFsPath }])
}
},
[StudioItemActionId.DeleteItem]: async (item: TreeItem) => {
modal.openConfirmActionModal(item.id, StudioItemActionId.DeleteItem, async () => {
Expand Down
26 changes: 15 additions & 11 deletions src/app/src/composables/useDraftDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,24 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git:
host.app.requestRerender()
}

async function rename(id: string, newFsPath: string) {
const currentDbItem: DatabaseItem = await host.document.get(id)
if (!currentDbItem) {
throw new Error(`Database item not found for document ${id}`)
}
async function rename(items: { id: string, newFsPath: string }[]) {
for (const item of items) {
const { id, newFsPath } = item

const content = await generateContentFromDocument(currentDbItem)
const currentDbItem: DatabaseItem = await host.document.get(id)
if (!currentDbItem) {
throw new Error(`Database item not found for document ${id}`)
}

const content = await generateContentFromDocument(currentDbItem)

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

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

async function duplicate(id: string): Promise<DraftItem<DatabaseItem>> {
Expand Down
7 changes: 4 additions & 3 deletions src/app/src/composables/useDraftMedias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { useGit } from './useGit'
import { getDraftStatus } from '../utils/draft'
import { createSharedComposable } from '@vueuse/core'
import { useHooks } from './useHooks'
import { TreeRootId } from '../utils/tree'

const storage = createStorage({
driver: indexedDbDriver({
Expand Down Expand Up @@ -153,7 +154,7 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret
// host.app.requestRerender()
// }

async function rename(_id: string, _newFsPath: string) {
async function rename(_items: { id: string, newFsPath: string }[]) {
// let currentDbItem: MediaItem = await host.document.get(id)
// if (!currentDbItem) {
// throw new Error(`Database item not found for document ${id}`)
Expand Down Expand Up @@ -267,12 +268,12 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret
const fsPath = directory && directory !== '/' ? joinURL(directory, file.name) : file.name

return {
id: `public-assets/${fsPath}`,
id: `${TreeRootId.Media}/${fsPath}`,
fsPath,
githubFile: undefined,
status: DraftStatus.Created,
modified: {
id: `public-assets/${fsPath}`,
id: `${TreeRootId.Media}/${fsPath}`,
fsPath,
extension: fsPath.split('.').pop()!,
stem: fsPath.split('.').join('.'),
Expand Down
25 changes: 14 additions & 11 deletions src/app/src/composables/useTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@ import { StudioFeature, type StudioHost, type TreeItem } from '../types'
import { ref, computed } from 'vue'
import type { useDraftDocuments } from './useDraftDocuments'
import type { useDraftMedias } from './useDraftMedias'
import { buildTree, findItemFromId, findItemFromRoute, ROOT_ITEM, findParentFromId } from '../utils/tree'
import { buildTree, findItemFromId, findItemFromRoute, findParentFromId, TreeRootId } 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, ui: ReturnType<typeof useUI>, draft: ReturnType<typeof useDraftDocuments | typeof useDraftMedias>) => {
const hooks = useHooks()

const rootItem = computed<TreeItem>(() => {
return {
id: type === StudioFeature.Content ? TreeRootId.Content : TreeRootId.Media,
name: type === StudioFeature.Content ? 'content' : 'media',
type: 'root',
fsPath: '/',
} as TreeItem
})

const tree = ref<TreeItem[]>([])
const currentItem = ref<TreeItem>(ROOT_ITEM)
const currentItem = ref<TreeItem>(rootItem.value)

const currentTree = computed<TreeItem[]>(() => {
if (currentItem.value.id === ROOT_ITEM.id) {
if (currentItem.value.id === rootItem.value.id) {
return tree.value
}

Expand All @@ -31,15 +40,8 @@ export const useTree = (type: StudioFeature, host: StudioHost, ui: ReturnType<ty
return subTree
})

// const parentItem = computed<TreeItem | null>(() => {
// 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
currentItem.value = item || rootItem.value
if (item?.type === 'file') {
if (type === StudioFeature.Content && ui.config.value.syncEditorAndRoute) {
host.app.navigateTo(item.routePath!)
Expand Down Expand Up @@ -103,6 +105,7 @@ export const useTree = (type: StudioFeature, host: StudioHost, ui: ReturnType<ty

return {
root: tree,
rootItem,
current: currentTree,
currentItem,
// parentItem,
Expand Down
2 changes: 1 addition & 1 deletion src/app/src/pages/content.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const showFileForm = computed(() => {
</script>

<template>
<div class="flex flex-col">
<div>
<div class="flex items-center justify-between gap-2 px-4 py-1 border-b-[0.5px] border-default bg-muted/70">
<ItemBreadcrumb />
<ItemActionsToolbar />
Expand Down
14 changes: 10 additions & 4 deletions src/app/src/pages/media.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const isFileCreationInProgress = computed(() => context.actionInProgress.value?.
const isFolderCreationInProgress = computed(() => context.actionInProgress.value?.id === StudioItemActionId.CreateFolder)

async function onFileDrop(event: DragEvent) {
if (mediaTree.draft.current.value) {
return
}

if (event.dataTransfer?.files) {
await context.itemActionHandler[StudioItemActionId.UploadMedia]({
directory: mediaTree.currentItem.value.fsPath,
Expand All @@ -22,7 +26,11 @@ async function onFileDrop(event: DragEvent) {
</script>

<template>
<div class="flex flex-col">
<div
class="flex flex-col"
@drop.prevent.stop="onFileDrop"
@dragover.prevent.stop
>
<div class="flex items-center justify-between gap-2 px-4 py-1 border-b-[0.5px] border-default bg-muted/70">
<ItemBreadcrumb />
<ItemActionsToolbar />
Expand All @@ -33,9 +41,7 @@ async function onFileDrop(event: DragEvent) {
/>
<div
v-else
class="flex flex-col p-4"
@drop.prevent.stop="onFileDrop"
@dragover.prevent.stop
class="flex flex-col p-4 min-h-[200px]"
>
<ItemTree
v-if="folderTree?.length > 0 || isFolderCreationInProgress"
Expand Down
18 changes: 14 additions & 4 deletions src/app/src/utils/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type StudioAction, type TreeItem, TreeStatus, StudioItemActionId } from '../types'
import { TreeRootId } from './tree'

export const oneStepActions: StudioItemActionId[] = [StudioItemActionId.RevertItem, StudioItemActionId.DeleteItem, StudioItemActionId.DuplicateItem]
export const twoStepActions: StudioItemActionId[] = [StudioItemActionId.CreateDocument, StudioItemActionId.CreateFolder, StudioItemActionId.RenameItem]
Expand All @@ -16,6 +17,12 @@ export const STUDIO_ITEM_ACTION_DEFINITIONS: StudioAction[] = [
icon: 'i-lucide-file-plus',
tooltip: 'Create a new file',
},
{
id: StudioItemActionId.UploadMedia,
label: 'Upload media',
icon: 'i-lucide-upload',
tooltip: 'Upload media',
},
{
id: StudioItemActionId.CreateFolder,
label: 'Create folder',
Expand Down Expand Up @@ -49,14 +56,18 @@ export function computeActionItems(itemActions: StudioAction[], item?: TreeItem

const forbiddenActions: StudioItemActionId[] = []

if (item.type === 'root') {
return itemActions.filter(action => ![StudioItemActionId.RenameItem, StudioItemActionId.DeleteItem, StudioItemActionId.DuplicateItem].includes(action.id))
// Upload only available for medias
if (!item.id.startsWith(TreeRootId.Media)) {
forbiddenActions.push(StudioItemActionId.UploadMedia)
}

// Item type filtering
switch (item.type) {
case 'root':
forbiddenActions.push(StudioItemActionId.RenameItem, StudioItemActionId.DeleteItem, StudioItemActionId.DuplicateItem)
break
case 'file':
forbiddenActions.push(StudioItemActionId.CreateFolder, StudioItemActionId.CreateDocument)
forbiddenActions.push(StudioItemActionId.CreateFolder, StudioItemActionId.CreateDocument, StudioItemActionId.UploadMedia)
break
case 'directory':
forbiddenActions.push(StudioItemActionId.DuplicateItem)
Expand All @@ -72,7 +83,6 @@ export function computeActionItems(itemActions: StudioAction[], item?: TreeItem
forbiddenActions.push(StudioItemActionId.DuplicateItem, StudioItemActionId.RenameItem, StudioItemActionId.DeleteItem)
break
case TreeStatus.Renamed:
forbiddenActions.push(StudioItemActionId.RenameItem)
break
default:
forbiddenActions.push(StudioItemActionId.RevertItem)
Expand Down
4 changes: 2 additions & 2 deletions src/app/src/utils/draft.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type DatabasePageItem, type DraftItem, type BaseItem, ContentFileExtension } from '../types'
import { DraftStatus } from '../types'
import { ROOT_ITEM } from './tree'
import { isEqual } from './database'
import { TreeRootId } from './tree'

export function getDraftStatus(modified?: BaseItem, original?: BaseItem): DraftStatus {
if (!modified && !original) {
Expand Down Expand Up @@ -31,7 +31,7 @@ export function getDraftStatus(modified?: BaseItem, original?: BaseItem): DraftS
}

export function findDescendantsFromId(list: DraftItem[], id: string): DraftItem[] {
if (id === ROOT_ITEM.id) {
if ([TreeRootId.Content, TreeRootId.Media].includes(id as TreeRootId)) {
return list
}

Expand Down
Loading
Loading