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

Commit dbdd5c4

Browse files
authored
Merge pull request #25 from vue-email/feat/props-editor
feat: props editor
2 parents aeb9720 + f4e857c commit dbdd5c4

File tree

20 files changed

+839
-107
lines changed

20 files changed

+839
-107
lines changed

client/components/CodeContainer.vue

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<script lang="ts" setup>
22
import { camelCase } from 'scule'
3+
import JsonEditorVue from 'json-editor-vue'
34
import { copyTextToClipboard } from '@/util/copy-text-to-clipboard'
5+
import 'vanilla-jsoneditor/themes/jse-theme-dark.css'
46
57
defineEmits(['setlang'])
68
79
const toast = useToast()
810
const { editorCode } = useTool()
9-
const { template, email } = useEmail()
11+
const { template, email, renderEmail } = useEmail()
12+
13+
const emailProps = ref(JSON.parse(JSON.stringify(email.value.props)))
1014
1115
function handleDownload(lang: 'html' | 'txt' | 'vue') {
1216
const content = template.value[lang]
@@ -85,14 +89,25 @@ const items = computed(() => {
8589
})
8690
}
8791
92+
if (emailProps.value.length) {
93+
arr.push({
94+
key: 'props',
95+
label: 'Props',
96+
icon: 'i-ph-code-duotone',
97+
} as any)
98+
}
99+
88100
return arr
89101
})
90102
91103
const tab = ref(0)
104+
105+
watchEffect(() => {
106+
emailProps.value = JSON.parse(JSON.stringify(email.value.props))
107+
})
92108
</script>
93109

94110
<template>
95-
{{ email.props }}
96111
<UTabs
97112
v-model="tab" :items="items" :ui="{
98113
wrapper: 'relative space-y-0',
@@ -103,7 +118,7 @@ const tab = ref(0)
103118
<UIcon :name="item.icon" class="w-7 h-7 flex-shrink-0" />
104119

105120
<span class="truncate">{{ item.label }}</span>
106-
<template v-if="selected">
121+
<template v-if="selected && item.code">
107122
<UTooltip text="Copy to clipboard">
108123
<UButton class="ml-6" icon="i-ph-copy-duotone" size="xs" square color="gray" variant="solid" @click="handleClipboard(item.key)" />
109124
</UTooltip>
@@ -112,12 +127,49 @@ const tab = ref(0)
112127
</UTooltip>
113128
</template>
114129

130+
<UBadge v-if="item.key === 'props'" size="xs" label="Beta" variant="subtle" />
131+
115132
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
116133
</div>
117134
</template>
118135

119136
<template #item="{ item }">
120-
<div class="w-full h-full" v-html="highlight(item.code, item.key)" />
137+
<div v-if="item.code" class="w-full h-full" v-html="highlight(item.code, item.key)" />
138+
<div v-else-if="item.key === 'props' && email.props && email.props.length" class="w-full h-full">
139+
<UContainer class="py-5 flex flex-col gap-y-4 relative">
140+
<template v-for="prop in email.props" :key="prop.label">
141+
<UFormGroup v-if="prop.type === 'string'" size="lg" :label="prop.label" :description="prop.description">
142+
<UInput v-model="prop.value" type="text" />
143+
</UFormGroup>
144+
<UFormGroup v-if="prop.type === 'number'" size="lg" :label="prop.label" :description="prop.description">
145+
<UInput v-model.number="prop.value" type="number" />
146+
</UFormGroup>
147+
<UFormGroup v-if="prop.type === 'date'" size="lg" :label="prop.label" :description="prop.description">
148+
<UInput v-model="prop.value" type="datetime-local" :value="prop.value" />
149+
</UFormGroup>
150+
<UFormGroup v-if="prop.type === 'boolean'" size="lg" :label="prop.label" :description="prop.description">
151+
<UToggle v-model="prop.value" />
152+
</UFormGroup>
153+
<UFormGroup v-if="prop.type === 'object'" size="lg" :label="prop.label" :description="prop.description">
154+
<JsonEditorVue
155+
v-model="prop.value"
156+
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
157+
class="json-editor-vue of-auto text-sm outline-none"
158+
mode="tree" :navigation-bar="false" :indentation="2" :tab-size="2"
159+
/>
160+
</UFormGroup>
161+
<UFormGroup v-if="prop.type === 'array'" size="lg" :label="prop.label" :description="prop.description">
162+
<JsonEditorVue
163+
v-model="prop.value"
164+
:class="[$colorMode.value === 'dark' ? 'jse-theme-dark' : 'light']"
165+
class="json-editor-vue of-auto text-sm outline-none"
166+
mode="tree" :navigation-bar="false" :indentation="2" :tab-size="2"
167+
/>
168+
</UFormGroup>
169+
</template>
170+
<UButton size="lg" icon="i-ph-floppy-disk" block label="Update Props" @click="renderEmail(emailProps)" />
171+
</UContainer>
172+
</div>
121173
</template>
122174
</UTabs>
123175
</template>
@@ -135,4 +187,25 @@ const tab = ref(0)
135187
overflow: auto;
136188
white-space: break-spaces;
137189
}
190+
191+
.dark,
192+
.jse-theme-dark {
193+
--jse-panel-background: #111 !important;
194+
--jse-theme-color: #111 !important;
195+
--jse-text-color-inverse: #fff !important;
196+
--jse-main-border: none !important;
197+
}
198+
199+
.json-editor-vue .no-main-menu {
200+
border: none !important;
201+
}
202+
203+
.json-editor-vue .jse-main {
204+
min-height: 1em !important;
205+
}
206+
207+
.json-editor-vue .jse-contents {
208+
border-width: 0 !important;
209+
border-radius: 5px !important;
210+
}
138211
</style>

client/composables/shiki.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Highlighter } from 'shikiji'
2-
import { getHighlighter } from 'shikiji'
1+
import type { Highlighter } from 'shiki'
2+
import { getHighlighter } from 'shiki'
33
import { ref } from 'vue'
44

55
export const shiki = ref<Highlighter>()

client/composables/useEmail.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import pretty from 'pretty'
22
import type { Result } from '@vue-email/compiler'
3-
import type { Email } from '@/types/email'
3+
import type { Email, Template } from '@/types/email'
44

55
export function useEmail() {
66
const emails = useState<Email[]>('emails')
77
const email = useState<Email>('email')
88
const sending = useState<boolean>('sending', () => false)
99
const refresh = useState<boolean>('refresh', () => false)
10-
const template = useState<{
11-
vue: string
12-
html: string
13-
txt: string
14-
}>('template')
10+
const template = useState<Template>('template')
1511

1612
const { host } = useWindow()
1713

@@ -29,23 +25,25 @@ export function useEmail() {
2925
emails.value = data.value
3026
}
3127

32-
const renderEmail = async () => {
28+
const renderEmail = async (props?: Email['props']) => {
3329
if (!email.value)
3430
return null
3531

3632
const { data } = await useFetch<Result>(`/api/render/${email.value.filename}`, {
33+
method: 'POST',
3734
baseURL: host.value,
35+
body: {
36+
props,
37+
},
3838
})
3939

4040
if (data.value) {
41-
return {
41+
template.value = {
4242
vue: email.value.content,
4343
html: pretty(data.value.html),
4444
txt: data.value.text,
45-
}
45+
} as Template
4646
}
47-
48-
return null
4947
}
5048

5149
const getEmail = async (filename: string) => {
@@ -55,10 +53,7 @@ export function useEmail() {
5553
if (found) {
5654
email.value = found
5755

58-
await renderEmail().then((value) => {
59-
if (value)
60-
template.value = value
61-
})
56+
await renderEmail()
6257
}
6358
}
6459
}

client/emails/code-components.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const box = {
1515
padding: '0 48px',
1616
}
1717
18-
const code = `import { codeToThemedTokens } from 'shikiji'
18+
const code = `import { codeToThemedTokens } from 'shiki'
1919
2020
const tokens = await codeToThemedTokens('<div class="foo">bar</div>', {
2121
lang: 'html',

client/emails/github-access-token.vue

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,31 @@ defineProps({
66
type: String,
77
default: 'John Doe',
88
},
9+
string: {
10+
type: String,
11+
},
12+
number: {
13+
type: Number,
14+
default: 0,
15+
},
16+
boolean: {
17+
type: Boolean,
18+
default: true,
19+
},
20+
array: {
21+
type: Array,
22+
default: () => [
23+
{
24+
key: 'value',
25+
},
26+
],
27+
},
28+
object: {
29+
type: Object,
30+
default: () => ({
31+
key: 'value',
32+
}),
33+
},
934
})
1035
1136
const main = {
@@ -74,6 +99,14 @@ const footer = {
7499
<strong>@{{ username }}</strong>, a personal access was created on your account.
75100
</EText>
76101

102+
<p>
103+
{{ string }}
104+
{{ number }}
105+
{{ boolean }}
106+
{{ array }}
107+
{{ object }}
108+
</p>
109+
77110
<ESection :style="section">
78111
<EText :style="text">
79112
Hey <strong>{{ username }}</strong>!

client/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222
"@types/splitpanes": "^2.2.6",
2323
"@vueuse/core": "^10.7.2",
2424
"@vueuse/nuxt": "^10.7.2",
25+
"destr": "^2.0.2",
2526
"html-to-text": "^9.0.5",
27+
"json-editor-vue": "^0.12.0",
28+
"json5": "^2.2.3",
2629
"nuxt": "^3.9.3",
2730
"pretty": "^2.0.0",
2831
"scule": "^1.2.0",
29-
"shikiji": "^0.9.19",
32+
"shiki": "^1.0.0-beta.3",
3033
"splitpanes": "^3.1.5",
3134
"vue-component-meta": "^1.8.27"
3235
}

client/pages/email/[file].vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ const route = useRoute()
44
const { getEmail, template } = useEmail()
55
const { horizontalSplit, previewMode } = useTool({
66
async onReload() {
7-
await getEmail(`${route.params.file}`)
7+
await getEmail(route.params.file as string)
88
},
99
})
1010
1111
onMounted(async () => {
12-
await getEmail(`${route.params.file}`)
12+
await getEmail(route.params.file as string)
1313
})
1414
1515
const showBoth = computed(() => previewMode.value.id === 'both')

client/server/api/emails.get.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import path from 'node:path'
22
import { kebabCase, pascalCase } from 'scule'
33
import { createComponentMetaCheckerByJsonConfig } from 'vue-component-meta'
4+
import { destr } from 'destr'
5+
import JSON5 from 'json5'
46
import type { Email } from '~/types/email'
57
import { createError, defineEventHandler, useStorage } from '#imports'
68

@@ -82,6 +84,65 @@ export default defineEventHandler(async () => {
8284
return 0
8385
})
8486
emailProps = emailProps.map(stripeTypeScriptInternalTypesSchema)
87+
const destructuredProps = emailProps.map((prop) => {
88+
const destructuredType = prop.type.split('|').map((type) => {
89+
type = type.trim()
90+
const value = prop.default
91+
92+
if (type === 'string') {
93+
return {
94+
type: 'string',
95+
value: destr(value) ?? '',
96+
}
97+
}
98+
99+
if (type === 'number') {
100+
return {
101+
type: 'number',
102+
value: destr(value) || 0,
103+
}
104+
}
105+
106+
if (type === 'boolean') {
107+
return {
108+
type: 'boolean',
109+
value: destr(value) || false,
110+
}
111+
}
112+
113+
if (type === 'object' || type.includes('Record') || type.includes('Record<')) {
114+
return {
115+
type: 'object',
116+
value: value ? JSON5.parse(value) : {},
117+
}
118+
}
119+
120+
if (type === 'array' || type.includes('[]') || type.includes('Array') || type.includes('Array<')) {
121+
return {
122+
type: 'array',
123+
value: value ? JSON5.parse(value) : [],
124+
}
125+
}
126+
127+
if (type === 'Date') {
128+
return {
129+
type: 'date',
130+
value: value ? eval(value) : new Date().toISOString(),
131+
}
132+
}
133+
134+
return {
135+
type: 'string',
136+
value: value ?? '',
137+
}
138+
})
139+
140+
return {
141+
label: prop.name,
142+
type: destructuredType[0].type,
143+
value: destructuredType[0].value,
144+
}
145+
})
85146

86147
const content = (await useStorage('assets:emails').getItem(
87148
email,
@@ -99,7 +160,7 @@ export default defineEventHandler(async () => {
99160
size: emailData.size,
100161
created: emailData.birthtime,
101162
modified: emailData.mtime,
102-
props: emailProps,
163+
props: destructuredProps,
103164
}
104165
}),
105166
)
@@ -114,6 +175,8 @@ export default defineEventHandler(async () => {
114175
return emails
115176
}
116177
catch (error) {
178+
console.error(error)
179+
117180
throw createError({
118181
statusCode: 500,
119182
statusMessage: 'Internal Server Error',

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy