Skip to content

Commit

Permalink
feat(server-routes): add global default inputs (#321)
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 Jul 20, 2023
1 parent bde6a6a commit 80a284f
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 68 deletions.
4 changes: 2 additions & 2 deletions packages/devtools-kit/src/_types/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { VitePluginInspectorOptions } from 'vite-plugin-vue-inspector'
import type { Import } from 'unimport'
import type { ModuleCustomTab } from './custom-tabs'
import type { ServerRouteInfo } from './integrations'
import type { ServerRouteInfo, ServerRouteInput } from './integrations'

export interface ModuleOptions {
/**
Expand Down Expand Up @@ -156,6 +156,6 @@ export interface NuxtDevToolsOptions {
serverRoutes: {
selectedRoute: ServerRouteInfo | null
view: 'tree' | 'list'
// TODO: add global inputs
inputDefaults: Record<string, ServerRouteInput[]>
}
}
95 changes: 48 additions & 47 deletions packages/devtools/client/components/ServerRouteDetails.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<script setup lang="ts">
import JsonEditorVue from 'json-editor-vue'
import type { CodeSnippet, ServerRouteInfo, ServerRouteInput, ServerRouteInputType } from '~/../src/types'
import { createReusableTemplate } from '@vueuse/core'
import type { CodeSnippet, ServerRouteInfo, ServerRouteInput } from '~/../src/types'
const props = defineProps<{
route: ServerRouteInfo
}>()
const emit = defineEmits<{
(event: 'open-default-input'): void
}>()
const [DefineDefaultInputs, UseDefaultInputs] = createReusableTemplate()
const config = useServerConfig()
const response = reactive({
Expand Down Expand Up @@ -60,9 +67,10 @@ const routeInputs = reactive({
headers: [{ key: 'Content-Type', value: 'application/json', type: 'string' }] as ServerRouteInput[],
})
const routeInputBodyJSON = ref({})
const { inputDefaults } = useDevToolsOptions('serverRoutes')
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
// https://github.com/unjs/h3/blob/main/src/utils/body.ts#L12
// https://github.com/unjs/h3/blob/main/src/utils/body.ts#L19
const bodyPayloadMethods = ['PATCH', 'POST', 'PUT', 'DELETE']
const hasBody = computed(() => bodyPayloadMethods.includes(routeMethod.value.toUpperCase()))
Expand All @@ -80,22 +88,27 @@ const currentParams = computed({
},
})
// TODO: add global inputs
const parsedQuery = computed(() => {
return {
...parseInputs(inputDefaults.value.query),
...parseInputs(routeInputs.query),
}
})
const parsedHeader = computed(() => {
return {
...parseInputs(inputDefaults.value.headers),
...parseInputs(routeInputs.headers),
}
})
const parsedBody = computed(() => {
return hasBody.value
? selectedTabInput.value === 'json'
? routeInputBodyJSON.value
? {
...parseInputs(inputDefaults.value.body),
...routeInputBodyJSON.value,
}
: {
...parseInputs(inputDefaults.value.body),
...parseInputs(routeInputs.body),
}
: undefined
Expand Down Expand Up @@ -170,9 +183,9 @@ const codeSnippets = computed(() => {
const snippets: CodeSnippet[] = []
const items: string[] = []
const headers = routeInputs.headers
.filter(({ key, value }) => key && value && !(key === 'Content-Type' && value === 'application/json'))
.map(({ key, value }) => ` '${key}': '${value}'`).join(',\n')
const headers = Object.entries(parsedHeader.value)
.filter(([key, value]) => key && value && !(key === 'Content-Type' && value === 'application/json'))
.map(([key, value]) => ` '${key}': '${value}'`).join(',\n')
if (routeMethod.value.toUpperCase() !== 'GET')
items.push(`method: '${routeMethod.value.toUpperCase()}'`)
Expand Down Expand Up @@ -210,34 +223,29 @@ const tabs = computed(() => {
items.push({
name: 'Params',
slug: 'params',
icon: 'carbon-text-selection',
length: paramNames.value.length,
})
}
items.push({
name: 'Query',
slug: 'query',
icon: 'carbon-help',
length: routeInputs.query.length,
})
if (hasBody.value) {
items.push({
name: 'Body',
slug: 'body',
icon: 'carbon-document',
length: routeInputs.body.length,
})
}
items.push({
name: 'Headers',
slug: 'headers',
icon: 'carbon-html-reference',
length: routeInputs.headers.length,
})
items.push({
name: 'Snippets',
slug: 'snippet',
icon: 'carbon-code',
})
return items
})
Expand All @@ -248,34 +256,6 @@ watchEffect(() => {
routeInputBodyJSON.value = JSON.parse(routeInputBodyJSON.value)
}
})
const types: ServerRouteInputType[] = []
watch(currentParams, (value) => {
if (!value)
return
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, deep: true })
</script>

<template>
Expand Down Expand Up @@ -309,8 +289,12 @@ watch(currentParams, (value) => {
:class="activeTab === tab.slug ? 'text-primary n-primary' : 'border-transparent shadow-none'"
@click="activeTab = tab.slug"
>
<NIcon :icon="tab.icon" />
{{ tab.name }} {{ tab?.length ? `(${tab.length})` : '' }}
<NIcon :icon="ServerRouteTabIcons[tab.slug]" />
{{ tab.name }}
{{ tab?.length ? `(${tab.length})` : '' }}
<span>
{{ inputDefaults[tab.slug]?.length ? `(${inputDefaults[tab.slug].length})` : '' }}
</span>
</NButton>
<div flex-auto />
<NButton
Expand All @@ -336,14 +320,31 @@ watch(currentParams, (value) => {
/>
</template>
</div>
<div v-if="activeTab === 'snippet'" relative>
<DefineDefaultInputs>
<ServerRouteInputs v-model="currentParams" :default="{ type: 'string' }" max-h-xs of-auto>
<template v-if="inputDefaults[activeTab]?.length">
<div flex="~ gap2" mb--2 items-center op50>
<div w-5 x-divider />
<div flex-none>
Default Inputs
</div>
<NIconButton
icon="i-carbon-edit"
@click="emit('open-default-input')"
/>
<div x-divider />
</div>
<ServerRouteInputs v-model="inputDefaults[activeTab]" disabled p0 />
</template>
</ServerRouteInputs>
</DefineDefaultInputs>
<div v-if="activeTab === 'snippet'">
<CodeSnippets
v-if="codeSnippets.length"
border="b base"
:code-snippets="codeSnippets"
/>
</div>
<div v-else-if="currentParams" relative n-code-block border="b base">
<div v-else-if="currentParams" border="b base" relative n-code-block>
<template v-if="activeTab === 'body'">
<div flex="~ wrap" w-full>
<template v-for="item of tabInputs" :key="item">
Expand All @@ -361,7 +362,7 @@ watch(currentParams, (value) => {
<div border="b base" flex-auto />
</div>

<ServerRouteInputs v-if="selectedTabInput === 'input'" v-model="currentParams" :default="{ type: 'string' }" />
<UseDefaultInputs v-if="selectedTabInput === 'input'" />
<JsonEditorVue
v-else-if="selectedTabInput === 'json'"
v-model="routeInputBodyJSON"
Expand All @@ -370,7 +371,7 @@ watch(currentParams, (value) => {
v-bind="$attrs" mode="text" :navigation-bar="false" :indentation="2" :tab-size="2"
/>
</template>
<ServerRouteInputs v-else v-model="currentParams" :default="{ type: 'string' }" />
<UseDefaultInputs v-else />
</div>

<NPanelGrids v-if="!started">
Expand Down
81 changes: 66 additions & 15 deletions packages/devtools/client/components/ServerRouteInputs.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: any
keys?: string[]
default?: any
exclude?: string[]
disabled?: boolean
}>(), {
keys: () => [],
disabled: false,
default: () => ({}),
exclude: () => [],
})
const emit = defineEmits<{ (...args: any): void }>()
const params = useVModel(props, 'modelValue', emit, { passive: true }) as any
const params = useVModel(props, 'modelValue', emit, { passive: true })
const filteredKeys = computed(() => {
const keys = [...props.keys, 'key', 'value', 'type']
return [...keys.filter(i => !props.exclude.includes(i))]
return [...props.keys, 'key', 'value', 'type']
})
const keysObject = computed(() => {
Expand All @@ -25,7 +25,6 @@ const keysObject = computed(() => {
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) {
Expand All @@ -35,10 +34,38 @@ function onFileInputChange(index: number, event: Event) {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
params[index].value = reader.result
params.value[index].value = reader.result
}
}
}
watch(() => params, (items) => {
items.value.forEach((item: any) => {
if (item.type === 'number' && typeof item.value !== 'number') {
const parsed = Number.parseFloat(item.value)
item.value = Number.isNaN(parsed) ? 0 : parsed
}
else if (item.type === 'boolean' && typeof item.value !== 'boolean') {
item.value = true
}
else if (item.type === 'file' && typeof item.value !== 'object') {
item.value = ''
}
else if (item.type === 'date' && typeof item.value === 'string' && !item.value.match(/^\d{4}-\d{2}-\d{2}$/)) {
item.value = new Date().toISOString().slice(0, 10)
console.log('date', item.value)
}
else if (item.type === 'time' && typeof item.value === 'string' && !item.value.match(/^\d{2}:\d{2}$/)) {
item.value = new Date().toISOString().slice(11, 16)
}
else if (item.type === 'datetime-local' && typeof item.value === 'string' && !item.value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)) {
item.value = new Date().toISOString().slice(0, 16)
}
else if (item.type === 'string') {
item.value = item.value.toString()
}
})
}, { deep: true, immediate: true, flush: 'sync' })
</script>

<template>
Expand All @@ -47,28 +74,51 @@ function onFileInputChange(index: number, event: Event) {
<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)" />
<NTextInput
v-if="item.type !== null && key === 'key'"
v-model="item[key]"
:placeholder="key" flex-1 font-mono n="sm primary"
:disabled="disabled"
:class="disabled ? 'op50' : ''"
/>
<template v-else-if="key === 'value'">
<NTextInput
v-if="item.type === 'file'" type="file"
:disabled="disabled"
:class="disabled ? 'op75' : ''"
@change="onFileInputChange(index, $event)"
/>
<div v-else-if="item.type === 'boolean'" ml2 flex>
<NCheckbox v-model="item.value" placeholder="Value" n="green lg" />
<NCheckbox v-model="item.value" placeholder="Value" n="green lg" :disabled="disabled" />
</div>
<NTextInput v-else v-model="item.value" :type="item.type" placeholder="Value" flex-1 font-mono n="sm" />
<NTextInput
v-else
v-model="item.value"
:type="item.type" placeholder="Value"
flex-1 font-mono n="sm primary"
:disabled="disabled"
:class="disabled ? 'op75' : ''"
/>
</template>
<NSelect v-else-if="key === 'type'" v-model="item.type" n="sm green">
<NSelect
v-else-if="key === 'type'"
v-model="item.type" n="sm green"
:class="disabled ? 'op75' : ''"
:disabled="disabled"
>
<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)">
<NButton n="red" :disabled="disabled" :class="disabled ? 'op0!' : ''" @click="params.splice(index, 1)">
<NIcon icon="carbon:delete" />
</NButton>
</slot>
</div>
<div flex gap-4>
<div v-if="!disabled" flex gap-4>
<slot name="actions" :params="params">
<NButton
icon="carbon-add" n="sm primary"
Expand All @@ -86,5 +136,6 @@ function onFileInputChange(index: number, event: Event) {
</NButton>
</slot>
</div>
<slot />
</div>
</template>
8 changes: 8 additions & 0 deletions packages/devtools/client/composables/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ export const ComposablesDocs = {
nextTick: 'https://vuejs.org/api/general.html#nexttick',
},
}

export const ServerRouteTabIcons: Record<string, string> = {
snippet: 'i-carbon-code',
headers: 'i-carbon-html-reference',
params: 'i-carbon-text-selection',
query: 'i-carbon-help',
body: 'i-carbon-document',
}
6 changes: 3 additions & 3 deletions packages/devtools/client/composables/storage-options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toRefs } from '@vueuse/core'
import { toRefs, watchDebounced } from '@vueuse/core'

// Server Routes
const serverRouteOptions = ref(await rpc.getOptions('serverRoutes'))
Expand All @@ -13,6 +13,6 @@ export function useDevToolsOptions<T extends keyof typeof list>(tab: T) {
}

// Server Routes
watch(serverRouteOptions, async (options) => {
watchDebounced(serverRouteOptions, async (options) => {
rpc.updateOptions('serverRoutes', options)
}, { deep: true, flush: 'post' })
}, { deep: true, flush: 'post', debounce: 500, maxWait: 1000 })

0 comments on commit 80a284f

Please sign in to comment.