Skip to content

Commit 221f84f

Browse files
committed
feat(actions): revert directory
1 parent b8fc257 commit 221f84f

File tree

9 files changed

+211
-40
lines changed

9 files changed

+211
-40
lines changed

src/app/src/components/modal/ModalConfirmAction.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const titleMap = {
3030
} as Record<StudioItemActionId, string>
3131
3232
const descriptionMap = {
33-
[StudioItemActionId.RevertItem]: `Are you sure you want to revert this file back to the original version?`,
33+
[StudioItemActionId.RevertItem]: `Are you sure you want to revert ${name.value} back to its original version?`,
3434
} as Record<StudioItemActionId, string>
3535
3636
const successLabelMap = {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Image } from '@unpic/vue'
44
import * as z from 'zod'
55
import type { FormSubmitEvent } from '@nuxt/ui'
66
import { type StudioAction, type TreeItem, ContentFileExtension } from '../../../types'
7-
import { joinURL } from 'ufo'
7+
import { joinURL, withLeadingSlash } from 'ufo'
88
import { contentFileExtensions } from '../../../utils/content'
99
import { useStudio } from '../../../composables/useStudio'
1010
import { StudioItemActionId } from '../../../types'
@@ -51,7 +51,7 @@ const itemExtensionIcon = computed<string>(() => {
5151
})
5252
5353
const routePath = computed(() => {
54-
return joinURL(props.parentItem.routePath!, stripNumericPrefix(state.name))
54+
return withLeadingSlash(joinURL(props.parentItem.routePath!, stripNumericPrefix(state.name)))
5555
})
5656
5757
const tooltipText = computed(() => {

src/app/src/composables/useContext.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const useContext = createSharedComposable((
2121
const currentFeature = computed<keyof typeof ui.panels | null>(() =>
2222
Object.keys(ui.panels).find(key => ui.panels[key as keyof typeof ui.panels]) as keyof typeof ui.panels,
2323
)
24+
const draft = computed(() => currentFeature.value === StudioFeature.Content ? draftDocuments : draftMedias)
2425

2526
const itemActions = computed<StudioAction[]>(() => {
2627
return STUDIO_ITEM_ACTION_DEFINITIONS.map(action => ({
@@ -29,7 +30,7 @@ export const useContext = createSharedComposable((
2930
if (actionInProgress.value === action.id) {
3031
// Two steps actions need to be already in progress to be executed
3132
if (twoStepActions.includes(action.id)) {
32-
await itemActionHandler[action.id](args)
33+
await itemActionHandler[action.id](args as never)
3334
unsetActionInProgress()
3435
return
3536
}
@@ -43,7 +44,7 @@ export const useContext = createSharedComposable((
4344

4445
// One step actions can be executed immediately
4546
if (oneStepActions.includes(action.id)) {
46-
await itemActionHandler[action.id](args)
47+
await itemActionHandler[action.id](args as never)
4748
unsetActionInProgress()
4849
}
4950
},
@@ -56,22 +57,18 @@ export const useContext = createSharedComposable((
5657
},
5758
[StudioItemActionId.CreateDocument]: async ({ fsPath, routePath, content }: CreateFileParams) => {
5859
const document = await host.document.create(fsPath, routePath, content)
59-
const draftItem = await draftDocuments.create(document)
60+
const draftItem = await draft.value.create(document)
6061
tree.selectItemById(draftItem.id)
6162
},
6263
[StudioItemActionId.UploadMedia]: async ({ directory, files }: UploadMediaParams) => {
6364
for (const file of files) {
64-
await draftMedias.upload(directory, file)
65+
await (draft.value as ReturnType<typeof useDraftMedias>).upload(directory, file)
6566
}
6667
},
6768
[StudioItemActionId.RevertItem]: async (id: string) => {
69+
console.log('revert item', id)
6870
modal.openConfirmActionModal(id, StudioItemActionId.RevertItem, async () => {
69-
if (currentFeature.value === StudioFeature.Content) {
70-
await draftDocuments.revert(id)
71-
}
72-
else {
73-
await draftMedias.revert(id)
74-
}
71+
await draft.value.revert(id)
7572
})
7673
},
7774
[StudioItemActionId.RenameItem]: async ({ path, file }: { path: string, file: TreeItem }) => {

src/app/src/composables/useDraftDocuments.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { DatabaseItem, DraftItem, StudioHost, GithubFile, DatabasePageItem
55
import { DraftStatus } from '../types/draft'
66
import type { useGit } from './useGit'
77
import { generateContentFromDocument } from '../utils/content'
8-
import { getDraftStatus } from '../utils/draft'
8+
import { getDraftStatus, findDescendantsFromId } from '../utils/draft'
99
import { createSharedComposable } from '@vueuse/core'
1010
import { useHooks } from './useHooks'
1111

@@ -134,21 +134,27 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git:
134134
}
135135

136136
async function revert(id: string) {
137-
const existingItem = list.value.find(item => item.id === id)
138-
if (!existingItem) {
139-
return
140-
}
137+
const draftItems = findDescendantsFromId(list.value, id)
141138

142-
if (existingItem.status === DraftStatus.Created) {
143-
await host.document.delete(id)
144-
await storage.removeItem(id)
145-
list.value = list.value.filter(item => item.id !== id)
146-
}
147-
else {
148-
await host.document.upsert(id, existingItem.original!)
149-
existingItem.status = DraftStatus.Opened
150-
existingItem.modified = existingItem.original
151-
await storage.setItem(id, existingItem)
139+
console.log('draftItems', draftItems)
140+
141+
for (const draftItem of draftItems) {
142+
const existingItem = list.value.find(item => item.id === draftItem.id)
143+
if (!existingItem) {
144+
return
145+
}
146+
147+
if (existingItem.status === DraftStatus.Created) {
148+
await host.document.delete(draftItem.id)
149+
await storage.removeItem(draftItem.id)
150+
list.value = list.value.filter(item => item.id !== draftItem.id)
151+
}
152+
else {
153+
await host.document.upsert(draftItem.id, existingItem.original!)
154+
existingItem.status = DraftStatus.Opened
155+
existingItem.modified = existingItem.original
156+
await storage.setItem(draftItem.id, existingItem)
157+
}
152158
}
153159

154160
await hooks.callHook('studio:draft:document:updated')

src/app/src/utils/draft.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { type BaseItem, type DatabasePageItem, ContentFileExtension, DraftStatus } from '../types'
1+
import { type BaseItem, type DatabasePageItem, ContentFileExtension, DraftStatus, type DraftItem } from '../types'
22
import { stringify } from 'minimark/stringify'
3+
import { ROOT_ITEM } from './tree'
34

45
export const COLOR_STATUS_MAP: { [key in DraftStatus]?: string } = {
56
[DraftStatus.Created]: 'green',
@@ -36,6 +37,21 @@ export function getDraftStatus(modified: BaseItem, original: BaseItem | undefine
3637
return DraftStatus.Opened
3738
}
3839

40+
export function findDescendantsFromId(list: DraftItem[], id: string): DraftItem[] {
41+
if (id === ROOT_ITEM.id) {
42+
return list
43+
}
44+
45+
const descendants: DraftItem[] = []
46+
for (const item of list) {
47+
if (item.id === id || item.id.startsWith(id + '/')) {
48+
descendants.push(item)
49+
}
50+
}
51+
52+
return descendants
53+
}
54+
3955
function isEqual(document1: DatabasePageItem, document2: DatabasePageItem) {
4056
function removeLastStyle(document: DatabasePageItem) {
4157
if (document.body?.value[document.body?.value.length - 1]?.[0] === 'style') {

src/app/src/utils/tree.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ TreeItem[] {
4343
name: fileName,
4444
fsPath: dbItem.fsPath,
4545
type: 'file',
46-
preview: EXTENSIONS_WITH_PREVIEW.has(dbItem.extension) ? dbItem.path : undefined,
46+
}
47+
48+
// Public assets
49+
if (dbItem.id.startsWith('public-assets/')) {
50+
fileItem.preview = EXTENSIONS_WITH_PREVIEW.has(dbItem.extension) ? dbItem.path : undefined
4751
}
4852

4953
if (itemHasPathField) {
@@ -188,13 +192,24 @@ export function findItemFromRoute(tree: TreeItem[], route: RouteLocationNormaliz
188192
return null
189193
}
190194

195+
export function findDescendantsFromId(tree: TreeItem[], id: string): TreeItem[] {
196+
const descendants: TreeItem[] = []
197+
for (const item of tree) {
198+
if (item.id === id) {
199+
descendants.push(item)
200+
}
201+
}
202+
203+
return descendants
204+
}
205+
191206
function calculateDirectoryStatuses(items: TreeItem[]) {
192207
for (const item of items) {
193208
if (item.type === 'directory' && item.children) {
194209
calculateDirectoryStatuses(item.children)
195210

196211
for (const child of item.children) {
197-
if (child.status) {
212+
if (child.status && child.status !== DraftStatus.Opened) {
198213
item.status = DraftStatus.Updated
199214
break
200215
}

src/app/test/mocks/draft.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { DraftItem } from '../../src/types/draft'
2+
import { DraftStatus } from '../../src/types/draft'
3+
4+
const draftItemsList: DraftItem[] = [
5+
// Root files
6+
{
7+
id: 'landing/index.md',
8+
fsPath: '/index.md',
9+
status: DraftStatus.Updated,
10+
},
11+
{
12+
id: 'docs/root-file.md',
13+
fsPath: '/root-file.md',
14+
status: DraftStatus.Created,
15+
},
16+
17+
// Files in getting-started directory
18+
{
19+
id: 'docs/1.getting-started/2.introduction.md',
20+
fsPath: '/1.getting-started/2.introduction.md',
21+
status: DraftStatus.Updated,
22+
},
23+
{
24+
id: 'docs/1.getting-started/3.installation.md',
25+
fsPath: '/1.getting-started/3.installation.md',
26+
status: DraftStatus.Created,
27+
},
28+
{
29+
id: 'docs/1.getting-started/4.configuration.md',
30+
fsPath: '/1.getting-started/4.configuration.md',
31+
status: DraftStatus.Deleted,
32+
},
33+
34+
// Files in advanced subdirectory
35+
{
36+
id: 'docs/1.getting-started/1.advanced/1.studio.md',
37+
fsPath: '/1.getting-started/1.advanced/1.studio.md',
38+
status: DraftStatus.Updated,
39+
},
40+
{
41+
id: 'docs/1.getting-started/1.advanced/2.deployment.md',
42+
fsPath: '/1.getting-started/1.advanced/2.deployment.md',
43+
status: DraftStatus.Created,
44+
},
45+
]
46+
47+
export { draftItemsList }

src/app/test/utils/draft.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { describe, it, expect } from 'vitest'
22
import type { DatabaseItem } from '../../src/types/database'
3-
import { getDraftStatus } from '../../src/utils/draft'
3+
import { getDraftStatus, findDescendantsFromId } from '../../src/utils/draft'
44
import { DraftStatus } from '../../src/types/draft'
55
import { dbItemsList } from '../mocks/database'
6+
import { draftItemsList } from '../mocks/draft'
7+
import { ROOT_ITEM } from '../../src/utils/tree'
68

79
describe('getDraftStatus', () => {
810
it('draft is CREATED if originalDatabaseItem is not defined', () => {
@@ -43,3 +45,51 @@ describe('getDraftStatus', () => {
4345
expect(status).toBe(DraftStatus.Updated)
4446
})
4547
})
48+
49+
describe('findDescendantsFromId', () => {
50+
it('returns exact match for a root level file', () => {
51+
const descendants = findDescendantsFromId(draftItemsList, 'landing/index.md')
52+
expect(descendants).toHaveLength(1)
53+
expect(descendants[0].id).toBe('landing/index.md')
54+
expect(descendants[0].fsPath).toBe('/index.md')
55+
})
56+
57+
it('returns empty array for non-existent id', () => {
58+
const descendants = findDescendantsFromId(draftItemsList, 'non-existent/file.md')
59+
expect(descendants).toHaveLength(0)
60+
})
61+
62+
it('returns all descendants files for a directory path', () => {
63+
const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started')
64+
65+
expect(descendants).toHaveLength(5)
66+
67+
expect(descendants.some(item => item.id === 'docs/1.getting-started/2.introduction.md')).toBe(true)
68+
expect(descendants.some(item => item.id === 'docs/1.getting-started/3.installation.md')).toBe(true)
69+
expect(descendants.some(item => item.id === 'docs/1.getting-started/4.configuration.md')).toBe(true)
70+
expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/1.studio.md')).toBe(true)
71+
expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/2.deployment.md')).toBe(true)
72+
})
73+
74+
it('returns all descendants for a nested directory path', () => {
75+
const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started/1.advanced')
76+
77+
expect(descendants).toHaveLength(2)
78+
79+
expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/1.studio.md')).toBe(true)
80+
expect(descendants.some(item => item.id === 'docs/1.getting-started/1.advanced/2.deployment.md')).toBe(true)
81+
})
82+
83+
it('returns all descendants for root item', () => {
84+
const descendants = findDescendantsFromId(draftItemsList, ROOT_ITEM.id)
85+
86+
expect(descendants).toHaveLength(draftItemsList.length)
87+
})
88+
89+
it('returns only the file itself when searching for a specific file', () => {
90+
const descendants = findDescendantsFromId(draftItemsList, 'docs/1.getting-started/1.advanced/1.studio.md')
91+
92+
expect(descendants).toHaveLength(1)
93+
expect(descendants[0].id).toBe('docs/1.getting-started/1.advanced/1.studio.md')
94+
})
95+
})

0 commit comments

Comments
 (0)