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

feat(assets): add rename and delete action #183

Merged
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
119 changes: 108 additions & 11 deletions packages/client/components/AssetDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,29 @@ import { useDevToolsClient } from '~/logic/client'
import { rpc } from '~/logic/rpc'

const props = defineProps<{
asset: AssetInfo
modelValue: AssetInfo
}>()

const emit = defineEmits<{ (...args: any): void }>()
const asset = useVModel(props, 'modelValue', emit, { passive: true })
const showNotification = useNotification()
const origin = window.parent.location.origin

const imageMeta = computedAsync(() => {
if (props.asset.type !== 'image')
if (asset.value.type !== 'image')
return undefined
return rpc.getImageMeta(props.asset.filePath)
return rpc.getImageMeta(asset.value.filePath)
})

const textContent = computedAsync(() => {
if (props.asset.type !== 'text')
if (asset.value.type !== 'text')
return undefined
return rpc.getTextAssetContent(props.asset.filePath)
return rpc.getTextAssetContent(asset.value.filePath)
})

const copy = useCopy()
const timeago = useTimeAgo(() => props.asset.mtime)
const timeAgo = useTimeAgo(() => asset.value.mtime)
const fileSize = computed(() => {
const size = props.asset.size
const size = asset.value.size
if (size < 1024)
return `${size} B`
if (size < 1024 * 1024)
Expand Down Expand Up @@ -51,9 +53,65 @@ const supportsPreview = computed(() => {
'text',
'video',
'font',
].includes(props.asset.type)
].includes(asset.value.type)
})

const deleteDialog = ref(false)
async function deleteAsset() {
try {
await rpc.deleteStaticAsset(asset.value.filePath)
asset.value = undefined as any
deleteDialog.value = false
showNotification({
text: 'Asset deleted',
icon: 'carbon-checkmark',
type: 'primary',
})
}
catch (error) {
deleteDialog.value = false
showNotification({
text: 'Something went wrong!',
icon: 'carbon-warning',
type: 'error',
})
}
}

const renameDialog = ref(false)
const newName = ref('')
async function renameAsset() {
const parts = asset.value.filePath.split('/')
const oldName = parts.slice(-1)[0].split('.').slice(0, -1).join('.')
if (!newName.value || newName.value === oldName) {
return showNotification({
text: 'Please enter a new name',
icon: 'carbon-warning',
type: 'error',
})
}
try {
const extension = parts.slice(-1)[0].split('.').slice(-1)[0]
const fullPath = `${parts.slice(0, -1).join('/')}/${newName.value}.${extension}`
await rpc.renameStaticAsset(asset.value.filePath, fullPath)

asset.value = undefined as any
renameDialog.value = false
showNotification({
text: 'Asset renamed',
icon: 'carbon-checkmark',
type: 'primary',
})
}
catch (error) {
showNotification({
text: 'Something went wrong!',
icon: 'carbon-warning',
type: 'error',
})
}
}

const client = useDevToolsClient()
</script>

Expand Down Expand Up @@ -161,7 +219,7 @@ const client = useDevToolsClient()
<td w-30 ws-nowrap pr5 text-right op50>
Last modified
</td>
<td>{{ new Date(asset.mtime).toLocaleString() }} <span op70>({{ timeago }})</span></td>
<td>{{ new Date(asset.mtime).toLocaleString() }} <span op70>({{ timeAgo }})</span></td>
</tr>
</tbody>
</table>
Expand All @@ -174,11 +232,50 @@ const client = useDevToolsClient()
<div x-divider />
</div>
<div flex="~ gap2 wrap">
<VDButton :to="`${origin}${asset.publicPath}`" download target="_blank" icon="carbon-download">
<VDButton :to="`${origin}${asset.publicPath}`" download target="_blank" icon="carbon-download" n="green">
Download
</VDButton>
<VDButton icon="carbon-text-annotation-toggle" n="blue" @click="renameDialog = !renameDialog">
Rename
</VDButton>
<VDButton icon="carbon-delete" n="red" @click="deleteDialog = !deleteDialog">
Delete
</VDButton>
</div>

<div flex-auto />

<VDDialog
v-model="deleteDialog" @close="deleteDialog = false"
>
<div flex="~ col gap-4" min-h-full w-full of-hidden p8>
<span>
Are you sure you want to delete this asset?
</span>
<div flex="~ gap2 wrap justify-center">
<VDButton icon="carbon-close" @click="deleteDialog = false">
Cancel
</VDButton>
<VDButton icon="carbon-delete" n="red" @click="deleteAsset">
Delete
</VDButton>
</div>
</div>
</VDDialog>
<VDDialog
v-model="renameDialog" @close="deleteDialog = false"
>
<div flex="~ col gap-4" min-h-full w-full of-hidden p8>
<VDTextInput v-model="newName" placeholder="New name" n="blue" />
<div flex="~ gap2 wrap justify-center">
<VDButton icon="carbon-close" @click="renameDialog = false">
Cancel
</VDButton>
<VDButton icon="carbon-text-annotation-toggle" n="blue" @click="renameAsset">
Rename
</VDButton>
</div>
</div>
</VDDialog>
</div>
</template>
2 changes: 1 addition & 1 deletion packages/client/components/DrawerRight.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ onClickOutside(el, () => {
if (props.modelValue && props.autoClose)
emit('close')
}, {
ignore: ['a', 'button', 'summary'],
ignore: ['a', 'button', 'summary', '[role="dialog"]'],
})
</script>

Expand Down
33 changes: 15 additions & 18 deletions packages/client/components/Notification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
const show = ref(false)
const icon = ref<string | undefined>()
const text = ref<string | undefined>()
const type = ref<'primary' | 'error' | undefined>()
const type = ref<'primary' | 'error' | 'warning' | undefined>()
const duration = ref<number>()
let timer: ReturnType<typeof setTimeout> | undefined

provideNotification((opt: {
text: string
icon?: string
type?: 'primary' | 'error'
type?: 'primary' | 'error' | 'warning'
duration?: number
}) => {
text.value = opt.text
Expand All @@ -20,6 +20,17 @@ provideNotification((opt: {
createTimer()
})

const textColor = computed(() => {
switch (type.value) {
case 'warning':
return 'text-orange'
case 'error':
return 'text-red'
default:
return 'text-primary'
}
})

function clearTimer() {
if (timer) {
clearTimeout(timer)
Expand All @@ -40,25 +51,11 @@ function createTimer() {
:class="show ? '' : 'pointer-events-none overflow-hidden'"
>
<div
v-if="type === 'error'"
border="~ base"
flex="~ inline gap2"
m-3 inline-block items-center rounded px-4 py-1 text-red transition-all duration-300 bg-base
:style="show ? {} : { transform: 'translateY(-300%)' }"
:class="show ? 'shadow' : 'shadow-none'"
@mouseenter="clearTimer"
@mouseleave="createTimer"
>
<div v-if="icon" :class="`i-${icon}`" />
<div>{{ text }}</div>
</div>
<div
v-else
border="~ base"
flex="~ inline gap2"
m-3 inline-block items-center rounded px-4 py-1 text-primary transition-all duration-300 bg-base
m-3 inline-block items-center rounded px-4 py-1 transition-all duration-300 bg-base
:style="show ? {} : { transform: 'translateY(-300%)' }"
:class="show ? 'shadow' : 'shadow-none'"
:class="[show ? 'shadow' : 'shadow-none', textColor]"
@mouseenter="clearTimer"
@mouseleave="createTimer"
>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/composables/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const [
] = useSingleton<(opt: {
text: string
icon?: string
type?: 'primary' | 'error'
type?: 'primary' | 'warning' | 'error'
duration?: number
}) => void>()

Expand Down
3 changes: 3 additions & 0 deletions packages/client/logic/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ export const rpc
onTerminalExit({ data }: { id?: string; data: string }) {
hookApi.hook.emit('__vue-devtools:terminal:exit__', data)
},
onFileWatch(data: { event: string; path: string }) {
hookApi.hook.emit('__vue-devtools:file-watch', data)
},
})
26 changes: 21 additions & 5 deletions packages/client/pages/assets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,30 @@
import { onKeyDown } from '@vueuse/core'
import Fuse from 'fuse.js'
import { rpc } from '~/logic/rpc'
import { hookApi } from '~/logic/hook'
import { rootPath } from '~/logic/global'

const assets = ref<AssetInfo[]>([])
function useAssets() {
const assets = ref<AssetInfo[]>([])

async function getAssets() {
assets.value = await rpc.staticAssets()
getAssets()
const debounceAssets = useDebounceFn(() => {
getAssets()
}, 100)

async function getAssets() {
assets.value = await rpc.staticAssets()
}

hookApi.hook.on('__vue-devtools:file-watch', ({ event, path }) => {
if (path.startsWith(rootPath) && ['add', 'unlink'].includes(event))
debounceAssets()
})

return { assets }
}

getAssets()
const { assets } = useAssets()

const search = ref('')

Expand Down Expand Up @@ -125,7 +141,7 @@ const navbar = ref<HTMLElement>()
:navbar="navbar"
@close="selected = undefined"
>
<AssetDetails v-if="selected" :asset="selected" />
<AssetDetails v-if="selected" v-model="selected" />
</DrawerRight>
</div>
<VDPanelGrids v-else px5>
Expand Down
3 changes: 3 additions & 0 deletions packages/node/client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ declare interface RPCFunctions {
staticAssets(): Promise<AssetInfo[]>
getImageMeta(path: string): Promise<ImageMeta | undefined>
getTextAssetContent(path: string): Promise<string | undefined>
deleteStaticAsset(path: string): Promise<string | undefined>
renameStaticAsset(oldPath: string, newPath: string): Promise<string | undefined>
getPackages(): Promise<Record<string, Omit<PackageMeta, 'name'>>>
getVueSFCList(): Promise<string[]>
getComponentInfo(filename: string): Promise<Record<string, unknown>>
onTerminalData(_: { id?: string; data: string }): void
onTerminalExit(_: { id?: string; data?: string }): void
onFileWatch(_: { event: string; path: string }): void
installPackage(packages: string[], options?: ExecNpmScriptOptions): Promise<void>
uninstallPackage(packages: string[], options?: ExecNpmScriptOptions): Promise<void>
root(): string
Expand Down
22 changes: 21 additions & 1 deletion packages/node/src/features/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ResolvedConfig } from 'vite'
import { imageMeta } from 'image-meta'

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

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

return await Promise.all(files.map(async (path) => {
cache = await Promise.all(files.map(async (path) => {
const filePath = resolve(dir, path)
const stat = await fs.lstat(filePath)
const publicDirname = p.relative(config.root, config.publicDir)
Expand All @@ -56,6 +57,8 @@ export async function getStaticAssets(config: ResolvedConfig): Promise<AssetInfo
mtime: stat.mtimeMs,
}
}))

return cache
}

export async function getImageMeta(filepath: string) {
Expand Down Expand Up @@ -83,3 +86,20 @@ export async function getTextAssetContent(filepath: string, limit = 300) {
return undefined
}
}

export async function deleteStaticAsset(filepath: string) {
try {
return await fs.unlink(filepath)
}
catch (e) {
console.error(e)
throw e
}
}

export async function renameStaticAsset(oldPath: string, newPath: string) {
const exist = cache?.find(asset => asset.filePath === newPath)
if (exist)
throw new Error(`File ${newPath} already exists`)
return await fs.rename(oldPath, newPath)
}
8 changes: 8 additions & 0 deletions packages/node/src/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { AnalyzeOptions, DeepRequired } from '@vite-plugin-vue-devtools/cor
import { analyzeCode, analyzeOptionsDefault } from '@vite-plugin-vue-devtools/core/compiler'
import { DIR_CLIENT } from './dir'
import {
deleteStaticAsset,
execNpmScript,
getComponentInfo,
getComponentsRelationships,
Expand All @@ -18,6 +19,7 @@ import {
getStaticAssets,
getTextAssetContent,
getVueSFCList,
renameStaticAsset,
} from './features'

function getVueDevtoolsPath() {
Expand Down Expand Up @@ -84,6 +86,8 @@ export default function VitePluginVueDevTools(options?: VitePluginVueDevToolsOpt
staticAssets: () => getStaticAssets(config),
getImageMeta,
getTextAssetContent,
deleteStaticAsset,
renameStaticAsset,
getPackages: () => getPackages(config.root),
getVueSFCList: () => getVueSFCList(config.root),
getComponentInfo: (filename: string) => getComponentInfo(config.root, filename),
Expand Down Expand Up @@ -112,6 +116,10 @@ export default function VitePluginVueDevTools(options?: VitePluginVueDevToolsOpt
},
}),
})

server.watcher.on('all', (event, path) => {
rpc.onFileWatch({ event, path })
})
}
const plugin = <PluginOption>{
name: PLUGIN_NAME,
Expand Down
Loading