Skip to content
This repository was archived by the owner on Jan 6, 2024. It is now read-only.

Commit a075e8a

Browse files
authored
feat(assets): add rename and delete action (#183)
* feat(assets): add rename and delete action * fix(ui-kit): dialog closing and a11y
1 parent 6fdfca3 commit a075e8a

File tree

10 files changed

+194
-42
lines changed

10 files changed

+194
-42
lines changed

packages/client/components/AssetDetails.vue

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,29 @@ import { useDevToolsClient } from '~/logic/client'
33
import { rpc } from '~/logic/rpc'
44
55
const props = defineProps<{
6-
asset: AssetInfo
6+
modelValue: AssetInfo
77
}>()
8-
8+
const emit = defineEmits<{ (...args: any): void }>()
9+
const asset = useVModel(props, 'modelValue', emit, { passive: true })
10+
const showNotification = useNotification()
911
const origin = window.parent.location.origin
1012
1113
const imageMeta = computedAsync(() => {
12-
if (props.asset.type !== 'image')
14+
if (asset.value.type !== 'image')
1315
return undefined
14-
return rpc.getImageMeta(props.asset.filePath)
16+
return rpc.getImageMeta(asset.value.filePath)
1517
})
1618
1719
const textContent = computedAsync(() => {
18-
if (props.asset.type !== 'text')
20+
if (asset.value.type !== 'text')
1921
return undefined
20-
return rpc.getTextAssetContent(props.asset.filePath)
22+
return rpc.getTextAssetContent(asset.value.filePath)
2123
})
2224
2325
const copy = useCopy()
24-
const timeago = useTimeAgo(() => props.asset.mtime)
26+
const timeAgo = useTimeAgo(() => asset.value.mtime)
2527
const fileSize = computed(() => {
26-
const size = props.asset.size
28+
const size = asset.value.size
2729
if (size < 1024)
2830
return `${size} B`
2931
if (size < 1024 * 1024)
@@ -51,9 +53,65 @@ const supportsPreview = computed(() => {
5153
'text',
5254
'video',
5355
'font',
54-
].includes(props.asset.type)
56+
].includes(asset.value.type)
5557
})
5658
59+
const deleteDialog = ref(false)
60+
async function deleteAsset() {
61+
try {
62+
await rpc.deleteStaticAsset(asset.value.filePath)
63+
asset.value = undefined as any
64+
deleteDialog.value = false
65+
showNotification({
66+
text: 'Asset deleted',
67+
icon: 'carbon-checkmark',
68+
type: 'primary',
69+
})
70+
}
71+
catch (error) {
72+
deleteDialog.value = false
73+
showNotification({
74+
text: 'Something went wrong!',
75+
icon: 'carbon-warning',
76+
type: 'error',
77+
})
78+
}
79+
}
80+
81+
const renameDialog = ref(false)
82+
const newName = ref('')
83+
async function renameAsset() {
84+
const parts = asset.value.filePath.split('/')
85+
const oldName = parts.slice(-1)[0].split('.').slice(0, -1).join('.')
86+
if (!newName.value || newName.value === oldName) {
87+
return showNotification({
88+
text: 'Please enter a new name',
89+
icon: 'carbon-warning',
90+
type: 'error',
91+
})
92+
}
93+
try {
94+
const extension = parts.slice(-1)[0].split('.').slice(-1)[0]
95+
const fullPath = `${parts.slice(0, -1).join('/')}/${newName.value}.${extension}`
96+
await rpc.renameStaticAsset(asset.value.filePath, fullPath)
97+
98+
asset.value = undefined as any
99+
renameDialog.value = false
100+
showNotification({
101+
text: 'Asset renamed',
102+
icon: 'carbon-checkmark',
103+
type: 'primary',
104+
})
105+
}
106+
catch (error) {
107+
showNotification({
108+
text: 'Something went wrong!',
109+
icon: 'carbon-warning',
110+
type: 'error',
111+
})
112+
}
113+
}
114+
57115
const client = useDevToolsClient()
58116
</script>
59117

@@ -161,7 +219,7 @@ const client = useDevToolsClient()
161219
<td w-30 ws-nowrap pr5 text-right op50>
162220
Last modified
163221
</td>
164-
<td>{{ new Date(asset.mtime).toLocaleString() }} <span op70>({{ timeago }})</span></td>
222+
<td>{{ new Date(asset.mtime).toLocaleString() }} <span op70>({{ timeAgo }})</span></td>
165223
</tr>
166224
</tbody>
167225
</table>
@@ -174,11 +232,50 @@ const client = useDevToolsClient()
174232
<div x-divider />
175233
</div>
176234
<div flex="~ gap2 wrap">
177-
<VDButton :to="`${origin}${asset.publicPath}`" download target="_blank" icon="carbon-download">
235+
<VDButton :to="`${origin}${asset.publicPath}`" download target="_blank" icon="carbon-download" n="green">
178236
Download
179237
</VDButton>
238+
<VDButton icon="carbon-text-annotation-toggle" n="blue" @click="renameDialog = !renameDialog">
239+
Rename
240+
</VDButton>
241+
<VDButton icon="carbon-delete" n="red" @click="deleteDialog = !deleteDialog">
242+
Delete
243+
</VDButton>
180244
</div>
181245

182246
<div flex-auto />
247+
248+
<VDDialog
249+
v-model="deleteDialog" @close="deleteDialog = false"
250+
>
251+
<div flex="~ col gap-4" min-h-full w-full of-hidden p8>
252+
<span>
253+
Are you sure you want to delete this asset?
254+
</span>
255+
<div flex="~ gap2 wrap justify-center">
256+
<VDButton icon="carbon-close" @click="deleteDialog = false">
257+
Cancel
258+
</VDButton>
259+
<VDButton icon="carbon-delete" n="red" @click="deleteAsset">
260+
Delete
261+
</VDButton>
262+
</div>
263+
</div>
264+
</VDDialog>
265+
<VDDialog
266+
v-model="renameDialog" @close="deleteDialog = false"
267+
>
268+
<div flex="~ col gap-4" min-h-full w-full of-hidden p8>
269+
<VDTextInput v-model="newName" placeholder="New name" n="blue" />
270+
<div flex="~ gap2 wrap justify-center">
271+
<VDButton icon="carbon-close" @click="renameDialog = false">
272+
Cancel
273+
</VDButton>
274+
<VDButton icon="carbon-text-annotation-toggle" n="blue" @click="renameAsset">
275+
Rename
276+
</VDButton>
277+
</div>
278+
</div>
279+
</VDDialog>
183280
</div>
184281
</template>

packages/client/components/DrawerRight.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ onClickOutside(el, () => {
1717
if (props.modelValue && props.autoClose)
1818
emit('close')
1919
}, {
20-
ignore: ['a', 'button', 'summary'],
20+
ignore: ['a', 'button', 'summary', '[role="dialog"]'],
2121
})
2222
</script>
2323

packages/client/components/Notification.vue

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
const show = ref(false)
33
const icon = ref<string | undefined>()
44
const text = ref<string | undefined>()
5-
const type = ref<'primary' | 'error' | undefined>()
5+
const type = ref<'primary' | 'error' | 'warning' | undefined>()
66
const duration = ref<number>()
77
let timer: ReturnType<typeof setTimeout> | undefined
88
99
provideNotification((opt: {
1010
text: string
1111
icon?: string
12-
type?: 'primary' | 'error'
12+
type?: 'primary' | 'error' | 'warning'
1313
duration?: number
1414
}) => {
1515
text.value = opt.text
@@ -20,6 +20,17 @@ provideNotification((opt: {
2020
createTimer()
2121
})
2222
23+
const textColor = computed(() => {
24+
switch (type.value) {
25+
case 'warning':
26+
return 'text-orange'
27+
case 'error':
28+
return 'text-red'
29+
default:
30+
return 'text-primary'
31+
}
32+
})
33+
2334
function clearTimer() {
2435
if (timer) {
2536
clearTimeout(timer)
@@ -40,25 +51,11 @@ function createTimer() {
4051
:class="show ? '' : 'pointer-events-none overflow-hidden'"
4152
>
4253
<div
43-
v-if="type === 'error'"
44-
border="~ base"
45-
flex="~ inline gap2"
46-
m-3 inline-block items-center rounded px-4 py-1 text-red transition-all duration-300 bg-base
47-
:style="show ? {} : { transform: 'translateY(-300%)' }"
48-
:class="show ? 'shadow' : 'shadow-none'"
49-
@mouseenter="clearTimer"
50-
@mouseleave="createTimer"
51-
>
52-
<div v-if="icon" :class="`i-${icon}`" />
53-
<div>{{ text }}</div>
54-
</div>
55-
<div
56-
v-else
5754
border="~ base"
5855
flex="~ inline gap2"
59-
m-3 inline-block items-center rounded px-4 py-1 text-primary transition-all duration-300 bg-base
56+
m-3 inline-block items-center rounded px-4 py-1 transition-all duration-300 bg-base
6057
:style="show ? {} : { transform: 'translateY(-300%)' }"
61-
:class="show ? 'shadow' : 'shadow-none'"
58+
:class="[show ? 'shadow' : 'shadow-none', textColor]"
6259
@mouseenter="clearTimer"
6360
@mouseleave="createTimer"
6461
>

packages/client/composables/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const [
1717
] = useSingleton<(opt: {
1818
text: string
1919
icon?: string
20-
type?: 'primary' | 'error'
20+
type?: 'primary' | 'warning' | 'error'
2121
duration?: number
2222
}) => void>()
2323

packages/client/logic/rpc.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ export const rpc
1313
onTerminalExit({ data }: { id?: string; data: string }) {
1414
hookApi.hook.emit('__vue-devtools:terminal:exit__', data)
1515
},
16+
onFileWatch(data: { event: string; path: string }) {
17+
hookApi.hook.emit('__vue-devtools:file-watch', data)
18+
},
1619
})

packages/client/pages/assets.vue

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@
22
import { onKeyDown } from '@vueuse/core'
33
import Fuse from 'fuse.js'
44
import { rpc } from '~/logic/rpc'
5+
import { hookApi } from '~/logic/hook'
6+
import { rootPath } from '~/logic/global'
57
6-
const assets = ref<AssetInfo[]>([])
8+
function useAssets() {
9+
const assets = ref<AssetInfo[]>([])
710
8-
async function getAssets() {
9-
assets.value = await rpc.staticAssets()
11+
getAssets()
12+
const debounceAssets = useDebounceFn(() => {
13+
getAssets()
14+
}, 100)
15+
16+
async function getAssets() {
17+
assets.value = await rpc.staticAssets()
18+
}
19+
20+
hookApi.hook.on('__vue-devtools:file-watch', ({ event, path }) => {
21+
if (path.startsWith(rootPath) && ['add', 'unlink'].includes(event))
22+
debounceAssets()
23+
})
24+
25+
return { assets }
1026
}
1127
12-
getAssets()
28+
const { assets } = useAssets()
1329
1430
const search = ref('')
1531
@@ -125,7 +141,7 @@ const navbar = ref<HTMLElement>()
125141
:navbar="navbar"
126142
@close="selected = undefined"
127143
>
128-
<AssetDetails v-if="selected" :asset="selected" />
144+
<AssetDetails v-if="selected" v-model="selected" />
129145
</DrawerRight>
130146
</div>
131147
<VDPanelGrids v-else px5>

packages/node/client.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ declare interface RPCFunctions {
3737
staticAssets(): Promise<AssetInfo[]>
3838
getImageMeta(path: string): Promise<ImageMeta | undefined>
3939
getTextAssetContent(path: string): Promise<string | undefined>
40+
deleteStaticAsset(path: string): Promise<string | undefined>
41+
renameStaticAsset(oldPath: string, newPath: string): Promise<string | undefined>
4042
getPackages(): Promise<Record<string, Omit<PackageMeta, 'name'>>>
4143
getVueSFCList(): Promise<string[]>
4244
getComponentInfo(filename: string): Promise<Record<string, unknown>>
4345
onTerminalData(_: { id?: string; data: string }): void
4446
onTerminalExit(_: { id?: string; data?: string }): void
47+
onFileWatch(_: { event: string; path: string }): void
4548
installPackage(packages: string[], options?: ExecNpmScriptOptions): Promise<void>
4649
uninstallPackage(packages: string[], options?: ExecNpmScriptOptions): Promise<void>
4750
root(): string

packages/node/src/features/assets.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ResolvedConfig } from 'vite'
66
import { imageMeta } from 'image-meta'
77

88
const _imageMetaCache = new Map<string, ImageMeta | undefined>()
9+
let cache: AssetInfo[] | null = null
910

1011
function guessType(path: string): AssetType {
1112
if (/\.(a?png|jpe?g|jxl|gif|svg|webp|avif|ico|bmp|tiff?)$/i.test(path))
@@ -42,7 +43,7 @@ export async function getStaticAssets(config: ResolvedConfig): Promise<AssetInfo
4243
ignore: ['**/node_modules/**', '**/dist/**'],
4344
})
4445

45-
return await Promise.all(files.map(async (path) => {
46+
cache = await Promise.all(files.map(async (path) => {
4647
const filePath = resolve(dir, path)
4748
const stat = await fs.lstat(filePath)
4849
const publicDirname = p.relative(config.root, config.publicDir)
@@ -56,6 +57,8 @@ export async function getStaticAssets(config: ResolvedConfig): Promise<AssetInfo
5657
mtime: stat.mtimeMs,
5758
}
5859
}))
60+
61+
return cache
5962
}
6063

6164
export async function getImageMeta(filepath: string) {
@@ -83,3 +86,20 @@ export async function getTextAssetContent(filepath: string, limit = 300) {
8386
return undefined
8487
}
8588
}
89+
90+
export async function deleteStaticAsset(filepath: string) {
91+
try {
92+
return await fs.unlink(filepath)
93+
}
94+
catch (e) {
95+
console.error(e)
96+
throw e
97+
}
98+
}
99+
100+
export async function renameStaticAsset(oldPath: string, newPath: string) {
101+
const exist = cache?.find(asset => asset.filePath === newPath)
102+
if (exist)
103+
throw new Error(`File ${newPath} already exists`)
104+
return await fs.rename(oldPath, newPath)
105+
}

packages/node/src/vite.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AnalyzeOptions, DeepRequired } from '@vite-plugin-vue-devtools/cor
1010
import { analyzeCode, analyzeOptionsDefault } from '@vite-plugin-vue-devtools/core/compiler'
1111
import { DIR_CLIENT } from './dir'
1212
import {
13+
deleteStaticAsset,
1314
execNpmScript,
1415
getComponentInfo,
1516
getComponentsRelationships,
@@ -18,6 +19,7 @@ import {
1819
getStaticAssets,
1920
getTextAssetContent,
2021
getVueSFCList,
22+
renameStaticAsset,
2123
} from './features'
2224

2325
function getVueDevtoolsPath() {
@@ -84,6 +86,8 @@ export default function VitePluginVueDevTools(options?: VitePluginVueDevToolsOpt
8486
staticAssets: () => getStaticAssets(config),
8587
getImageMeta,
8688
getTextAssetContent,
89+
deleteStaticAsset,
90+
renameStaticAsset,
8791
getPackages: () => getPackages(config.root),
8892
getVueSFCList: () => getVueSFCList(config.root),
8993
getComponentInfo: (filename: string) => getComponentInfo(config.root, filename),
@@ -112,6 +116,10 @@ export default function VitePluginVueDevTools(options?: VitePluginVueDevToolsOpt
112116
},
113117
}),
114118
})
119+
120+
server.watcher.on('all', (event, path) => {
121+
rpc.onFileWatch({ event, path })
122+
})
115123
}
116124
const plugin = <PluginOption>{
117125
name: PLUGIN_NAME,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy