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

Commit a075e8a

Browse files
authoredAug 7, 2023
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

+108-11
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

+1-1
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

+15-18
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

+1-1
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

+3
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

+21-5
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

+3
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

+21-1
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

+8
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,

‎packages/ui-kit/src/components/Dialog.vue

+13-5
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ const props = withDefaults(
88
defineProps<{
99
modelValue?: boolean
1010
dim?: boolean
11+
autoClose?: boolean
1112
}>(),
1213
{
1314
modelValue: false,
1415
dim: true,
16+
autoClose: true,
1517
},
1618
)
1719
@@ -38,10 +40,14 @@ watchEffect(
3840
},
3941
)
4042
41-
function close() {
42-
show.value = false
43-
emit('close')
44-
}
43+
onClickOutside(card, () => {
44+
if (props.modelValue && props.autoClose) {
45+
show.value = false
46+
emit('close')
47+
}
48+
}, {
49+
ignore: ['a', 'button', 'summary'],
50+
})
4551
</script>
4652

4753
<script lang="ts">
@@ -56,11 +62,13 @@ export default {
5662
v-show="show" class="n-dialog fixed inset-0 z-100 flex items-center justify-center n-transition" :class="[
5763
show ? '' : 'op0 pointer-events-none visibility-none',
5864
]"
65+
role="dialog"
66+
aria-modal="true"
5967
>
6068
<div
6169
class="absolute inset-0 -z-1" :class="[
6270
dim ? 'bg-black/50' : '',
63-
]" @click="close()"
71+
]"
6472
/>
6573
<Card v-bind="$attrs" ref="card" class="max-h-screen of-auto">
6674
<slot />

0 commit comments

Comments
 (0)
This repository has been archived.