Skip to content

Commit

Permalink
feat(assets): able to edit text content (#366)
Browse files Browse the repository at this point in the history
  • Loading branch information
arashsheyda committed Aug 8, 2023
1 parent 3867814 commit 1e56198
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 29 deletions.
7 changes: 7 additions & 0 deletions packages/devtools-kit/src/_types/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ export interface AssetInfo {
mtime: number
}

export interface AssetEntry {
path: string
content: string
encoding?: BufferEncoding
override?: boolean
}

export interface CodeSnippet {
code: string
lang: string
Expand Down
6 changes: 3 additions & 3 deletions packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { StorageMounts } from 'nitropack'
import type { StorageValue } from 'unstorage'
import type { ModuleOptions, NuxtDevToolsOptions } from './options'
import type { ModuleCustomTab } from './custom-tabs'
import type { AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageManagerName, PackageUpdateInfo, ServerRouteInfo } from './integrations'
import type { AssetEntry, AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageManagerName, PackageUpdateInfo, ServerRouteInfo } from './integrations'
import type { TerminalAction, TerminalInfo } from './terminals'
import type { GetWizardArgs, WizardActions } from './wizard'
import type { AnalyzeBuildsInfo } from './analyze-build'
Expand Down Expand Up @@ -55,8 +55,8 @@ export interface ServerFunctions {
// Queries
getImageMeta(filepath: string): Promise<ImageMeta | undefined>
getTextAssetContent(filepath: string, limit?: number): Promise<string | undefined>
writeStaticAssets(token: string, file: { name: string; data: string }[], path: string): Promise<string[]>
deleteStaticAsset(token: string, file: string): Promise<void>
writeStaticAssets(token: string, file: AssetEntry[], folder: string): Promise<string[]>
deleteStaticAsset(token: string, filepath: string): Promise<void>
renameStaticAsset(token: string, oldPath: string, newPath: string): Promise<void>

// Actions
Expand Down
60 changes: 58 additions & 2 deletions packages/devtools/client/components/AssetDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,47 @@ const imageMeta = computedAsync(() => {
return rpc.getImageMeta(asset.value.filePath)
})
const textContent = computedAsync(() => {
const editDialog = ref(false)
const newTextContent = ref()
const textContentCounter = ref(0)
const textContent = computedAsync(async () => {
if (asset.value.type !== 'text')
return undefined
return rpc.getTextAssetContent(asset.value.filePath)
// eslint-disable-next-line no-unused-expressions
textContentCounter.value
const content = await rpc.getTextAssetContent(asset.value.filePath)
newTextContent.value = content
return content
})
async function saveTextContent() {
if (textContent.value !== newTextContent.value) {
try {
await rpc.writeStaticAssets(await ensureDevAuthToken(), [{
path: asset.value.path,
content: newTextContent.value,
override: true,
}], '')
editDialog.value = false
textContentCounter.value++
showNotification({
message: 'Updated',
icon: 'i-carbon-checkmark',
classes: 'text-green',
})
}
catch (error) {
showNotification({
message: 'Something went wrong!',
icon: 'i-carbon-warning',
classes: 'text-red',
})
}
}
}
const config = useServerConfig()
const hasNuxtImage = computed(() => {
const modules = config.value?._installedModules || []
Expand Down Expand Up @@ -267,6 +302,9 @@ async function renameAsset() {
<NButton :to="asset.publicPath" download target="_blank" icon="carbon-download" n="green">
Download
</NButton>
<NButton v-if="asset.type === 'text'" icon="carbon-edit" n="cyan" @click="editDialog = !editDialog">
Edit
</NButton>
<NButton icon="carbon-text-annotation-toggle" n="blue" @click="renameDialog = !renameDialog">
Rename
</NButton>
Expand Down Expand Up @@ -315,4 +353,22 @@ async function renameAsset() {
</div>
</div>
</NDialog>
<NDialog v-if="asset.type === 'text'" v-model="editDialog">
<div flex="~ col gap-4" min-h-full w-full of-hidden p4>
<textarea
v-model="newTextContent"
placeholder="Item value..."
class="h-lg w-xl of-auto rounded-lg p-4 text-sm font-mono outline-none"
@keydown.enter="saveTextContent"
/>
<div flex justify-end gap-4>
<NButton icon="carbon-close" @click="editDialog = false">
Cancel
</NButton>
<NButton icon="carbon:save" n="primary" @click="saveTextContent">
save
</NButton>
</div>
</div>
</NDialog>
</template>
2 changes: 1 addition & 1 deletion packages/devtools/client/components/AssetListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const icon = computed(() => {
flex="~ gap-2" w-full items-center hover="bg-active" px4 py1
:style="{ paddingLeft: `calc(1rem + ${index * 1.5}em)` }"
:class="{ 'bg-active': !isCollection && model?.filePath === item?.filePath }"
border="b base"
@click="isCollection ? open = !open : model = item"
>
<div :class="icon" />
Expand All @@ -49,7 +50,6 @@ const icon = computed(() => {
</span>
<NIcon v-if="isCollection" icon="carbon:chevron-right" :transform-rotate="open ? 90 : 0" transition />
</button>
<div x-divider />
<slot v-if="open">
<AssetListItem
v-for="subItem in item?.children"
Expand Down
15 changes: 9 additions & 6 deletions packages/devtools/client/components/DropZone.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts" setup>
import type { AssetEntry } from '~/../src/types'
const props = defineProps({
folder: {
type: String,
Expand Down Expand Up @@ -75,21 +77,22 @@ async function uploadFiles() {
if (wsConnecting.value || wsError.value)
return
const readyFiles = []
const uploadFiles: AssetEntry[] = []
for (const file of files.value) {
const reader = new FileReader()
reader.readAsDataURL(file)
const result = await new Promise((resolve) => {
reader.onload = () => resolve(reader.result as string)
}) as string
// TODO: add validation
const data = result.split(';base64,').pop() as string
readyFiles.push({
name: file.name,
data,
const content = result.split(';base64,').pop() as string
uploadFiles.push({
path: file.name,
encoding: 'base64',
content,
})
}
await rpc.writeStaticAssets(await ensureDevAuthToken(), [...readyFiles], props.folder).then(() => {
await rpc.writeStaticAssets(await ensureDevAuthToken(), [...uploadFiles], props.folder).then(() => {
close()
showNotification({
message: 'Files uploaded successfully!',
Expand Down
38 changes: 21 additions & 17 deletions packages/devtools/src/server-rpc/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join, resolve } from 'pathe'
import { imageMeta } from 'image-meta'
import { debounce } from 'perfect-debounce'
import fg from 'fast-glob'
import type { AssetInfo, AssetType, ImageMeta, NuxtDevtoolsServerContext, ServerFunctions } from '../types'
import type { AssetEntry, AssetInfo, AssetType, ImageMeta, NuxtDevtoolsServerContext, ServerFunctions } from '../types'

export function setupAssetsRPC({ nuxt, ensureDevAuthToken, refresh }: NuxtDevtoolsServerContext) {
const _imageMetaCache = new Map<string, ImageMeta | undefined>()
Expand Down Expand Up @@ -89,27 +89,31 @@ export function setupAssetsRPC({ nuxt, ensureDevAuthToken, refresh }: NuxtDevtoo
return undefined
}
},
async writeStaticAssets(token: string, files: { name: string; data: string }[], path: string) {
async writeStaticAssets(token: string, files: AssetEntry[], folder: string) {
await ensureDevAuthToken(token)

const baseDir = resolve(nuxt.options.srcDir, nuxt.options.dir.public + path)
const baseDir = resolve(nuxt.options.srcDir, nuxt.options.dir.public + folder)

return await Promise.all(
files.map(async ({ name, data }) => {
let dir = resolve(baseDir, name)
try {
await fsp.stat(dir)
const ext = dir.split('.').pop() as string
const base = dir.slice(0, dir.length - ext.length - 1)
let i = 1
while (await fsp.access(`${base}-${i}.${ext}`).then(() => true).catch(() => false))
i++
dir = `${base}-${i}.${ext}`
files.map(async ({ path, content, encoding, override }) => {
let dir = resolve(baseDir, path)
if (!override) {
try {
await fsp.stat(dir)
const ext = dir.split('.').pop() as string
const base = dir.slice(0, dir.length - ext.length - 1)
let i = 1
while (await fsp.access(`${base}-${i}.${ext}`).then(() => true).catch(() => false))
i++
dir = `${base}-${i}.${ext}`
}
catch (err) {
// Ignore error if file doesn't exist
}
}
catch (err) {
// Ignore error if file doesn't exist
}
await fsp.writeFile(dir, data, 'base64')
await fsp.writeFile(dir, content, {
encoding: encoding ?? 'utf-8',
})
return dir
}),
)
Expand Down

0 comments on commit 1e56198

Please sign in to comment.