Skip to content

Commit

Permalink
feat(server-routes): json-editor for tab inputs (#297)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
arashsheyda and antfu committed Jun 30, 2023
1 parent 45dc415 commit ee3b446
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 31 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 @@ -52,6 +52,13 @@ export interface ServerRouteInfo {
routes?: ServerRouteInfo[]
}

export type ServerRouteInputType = 'string' | 'number' | 'boolean' | 'file' | 'date' | 'time' | 'datetime-local'
export interface ServerRouteInput {
key: string
value: any
type?: ServerRouteInputType
}

export interface Payload {
url: string
time: number
Expand Down
100 changes: 75 additions & 25 deletions packages/devtools/client/components/ServerRouteDetails.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { CodeSnippet, ServerRouteInfo } from '~/../src/types'
import JsonEditorVue from 'json-editor-vue'
import type { CodeSnippet, ServerRouteInfo, ServerRouteInput, ServerRouteInputType } from '~/../src/types'
const props = defineProps<{
route: ServerRouteInfo
Expand Down Expand Up @@ -55,12 +56,12 @@ const paramNames = computed(() => parsedRoute.value?.filter(i => i.startsWith(':
const routeMethod = ref(props.route.method || 'GET')
const routeParams = ref<{ [key: string]: string }>({})
// TODO: add type to switch between json and input
const routeInputs = reactive({
query: [{ key: '', value: '' }],
body: [{ key: '', value: '' }],
headers: [{ key: 'Content-Type', value: 'application/json' }],
query: [{ key: '', value: '', type: 'string' }] as ServerRouteInput[],
body: [{ key: '', value: '', type: 'string' }] as ServerRouteInput[],
headers: [{ key: 'Content-Type', value: 'application/json', type: 'string' }] as ServerRouteInput[],
})
const routeInputBodyJSON = ref({})
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
// https://github.com/unjs/h3/blob/main/src/utils/body.ts#L12
Expand All @@ -69,11 +70,14 @@ const hasBody = computed(() => bodyPayloadMethods.includes(routeMethod.value.toU
const activeTab = ref(currentRoute.query.tab ? currentRoute.query.tab : paramNames.value.length ? 'params' : 'query')
const tabInputs = ['input', 'json']
const selectedTabInput = ref(tabInputs[0])
// TODO: fix routeInputs[activeTab.value] type
type RouteInputs = keyof typeof routeInputs
const currentParams = computed({
get: () => routeInputs[activeTab.value as RouteInputs],
set: (value) => {
set: (value: any) => {
routeInputs[activeTab.value as RouteInputs] = value
},
})
Expand All @@ -91,9 +95,11 @@ const parsedHeader = computed(() => {
})
const parsedBody = computed(() => {
return hasBody.value
? {
...parseInputs(routeInputs.body),
}
? selectedTabInput.value === 'json'
? routeInputBodyJSON.value
: {
...parseInputs(routeInputs.body),
}
: undefined
})
Expand Down Expand Up @@ -241,6 +247,38 @@ const tabs = computed(() => {
})
return items
})
watchEffect(() => {
if (selectedTabInput.value === 'json') {
if (typeof routeInputBodyJSON.value === 'string')
routeInputBodyJSON.value = JSON.parse(routeInputBodyJSON.value)
}
})
const types: ServerRouteInputType[] = []
watch(currentParams.value, () => {
currentParams.value?.forEach((input, index) => {
if (types.length) {
if (types[index] !== input.type && input.type !== undefined) {
types[index] = input.type
if (input.type !== 'string') {
if (input.type === 'boolean' && typeof input.value !== 'boolean')
input.value = true
else if (input.type === 'number' && typeof input.value !== 'number')
input.value = 0
else
input.value = ''
}
else if (input.type === 'string') {
input.value = input.value.toString()
}
}
}
else {
types[index] = input.type ?? 'string'
}
})
}, { immediate: true })
</script>

<template>
Expand Down Expand Up @@ -308,22 +346,34 @@ const tabs = computed(() => {
:code-snippets="codeSnippets"
/>
</div>
<div v-else-if="currentParams" px4 py2 flex="~ col gap-2" border="b base">
<div v-for="(item, index) in currentParams" :key="index" flex="~ gap-2" justify-around>
<NTextInput v-model="item.key" placeholder="Key" flex-1 font-mono n="sm" />
<NTextInput v-model="item.value" placeholder="Value" flex-1 font-mono n="sm" />
<NButton n="red" @click="currentParams!.splice(index, 1)">
<NIcon icon="carbon:delete" />
</NButton>
</div>
<div>
<NButton
icon="carbon-add" n="sm primary"
my1 px-3 @click="currentParams!.push({ key: '', value: '' })"
>
Add
</NButton>
</div>
<div v-else-if="currentParams" relative n-code-block border="b base">
<template v-if="activeTab === 'body'">
<div flex="~ wrap" w-full>
<template v-for="item of tabInputs" :key="item">
<button
px4 py2 border="r base"
hover="bg-active"
:class="{ 'border-b': item !== selectedTabInput }"
@click="selectedTabInput = item"
>
<div :class="{ op30: item !== selectedTabInput } " font-mono>
{{ item }}
</div>
</button>
</template>
<div border="b base" flex-auto />
</div>

<ServerRouteInputs v-if="selectedTabInput === 'input'" v-model="currentParams" :default="{ type: 'string' }" />
<JsonEditorVue
v-else-if="selectedTabInput === 'json'"
v-model="routeInputBodyJSON"
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
class="json-editor-vue of-auto text-sm outline-none"
v-bind="$attrs" mode="text" :navigation-bar="false" :indentation="2" :tab-size="2"
/>
</template>
<ServerRouteInputs v-else v-model="currentParams" :default="{ type: 'string' }" />
</div>

<NPanelGrids v-if="!started">
Expand Down
90 changes: 90 additions & 0 deletions packages/devtools/client/components/ServerRouteInputs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: any
keys?: string[]
default?: any
exclude?: string[]
}>(), {
keys: () => [],
default: () => ({}),
exclude: () => [],
})
const emit = defineEmits<{ (...args: any): void }>()
const params = useVModel(props, 'modelValue', emit, { passive: true }) as any
const filteredKeys = computed(() => {
const keys = [...props.keys, 'key', 'value', 'type']
return [...keys.filter(i => !props.exclude.includes(i))]
})
const keysObject = computed(() => {
const obj: any = {}
for (const key of filteredKeys.value)
obj[key] = props.default[key] || ''
return obj
})
// TODO: add better support for file, color, etc
const inputTypes = ['string', 'number', 'boolean', 'file', 'date', 'time', 'datetime-local']
function onFileInputChange(index: number, event: Event) {
const target = event.target as HTMLInputElement
if (target.files && target.files[0]) {
const file = target.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
params[index].value = reader.result
}
}
}
</script>

<template>
<div p4 flex="~ col gap-4">
<div v-for="(item, index) in params" :key="index" flex="~ gap-2" justify-around>
<slot name="input" :item="item" />

<template v-for="key of filteredKeys" :key="key">
<NTextInput v-if="item?.type !== null && key === 'key'" v-model="item[key]" :placeholder="key" flex-1 font-mono n="sm" />
<template v-else-if="key !== 'type'">
<NTextInput v-if="item.type === 'file'" type="file" @change="onFileInputChange(index, $event)" />
<div v-else-if="item.type === 'boolean'" ml2 flex>
<NCheckbox v-model="item.value" placeholder="Value" n="green lg" />
</div>
<NTextInput v-else v-model="item.value" :type="item.type" placeholder="Value" flex-1 font-mono n="sm" />
</template>
<NSelect v-else-if="key === 'type'" v-model="item.type" n="sm green">
<option v-for="typeItem of inputTypes" :key="typeItem" :value="typeItem">
{{ typeItem }}
</option>
</NSelect>
</template>
<slot name="input-actions">
<NButton n="red" @click="params.splice(index, 1)">
<NIcon icon="carbon:delete" />
</NButton>
</slot>
</div>
<div flex gap-4>
<slot name="actions" :params="params">
<NButton
icon="carbon-add" n="sm primary"
my1 px-3 @click="params.push({ ...keysObject })"
>
Add
</NButton>
<div flex-auto />
<NButton
v-if="params.length"
icon="carbon-trash-can" n="sm red"
my1 px-3 @click="params = []"
>
Remove All
</NButton>
</slot>
</div>
</div>
</template>
7 changes: 5 additions & 2 deletions packages/devtools/client/server/api/echo.post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export default defineEventHandler((ctx) => {
return ctx.node.req.read()
export default defineEventHandler(async (ctx) => {
const body = await readBody(ctx)
return {
...body,
}
})
19 changes: 15 additions & 4 deletions packages/devtools/client/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,21 @@ textarea {
background: #8881
}

.json-editor-vue {
--jse-theme-color: #8886 !important;
--jse-theme-color-highlight: #8889 !important;
--jse-background-color: #8888880A !important;
:root {
--jse-theme-color: #fff !important;
--jse-text-color-inverse: #777 !important;
--jse-theme-color-highlight: #eee !important;
--jse-panel-background: #fff !important;
--jse-background-color: var(--jse-panel-background) !important;
--jse-error-color: #ee534150 !important;
--jse-main-border: none !important;
}

.dark, .jse-theme-dark {
--jse-panel-background: #111 !important;
--jse-theme-color: #111 !important;
--jse-text-color-inverse: #fff !important;
--jse-main-border: none !important;
}

.json-editor-vue .no-main-menu {
Expand Down

0 comments on commit ee3b446

Please sign in to comment.