Skip to content

Commit c5af5a6

Browse files
authored
feat: adds json sytanx to template (#4414)
1 parent b8f224c commit c5af5a6

File tree

5 files changed

+126
-5
lines changed

5 files changed

+126
-5
lines changed

assets/auto-imports.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ declare global {
3939
const createRef: typeof import('@vueuse/core').createRef
4040
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
4141
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
42+
const createTemplateEditor: typeof import('./composable/templateEditor').createTemplateEditor
4243
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
4344
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
4445
const customRef: typeof import('vue').customRef
@@ -403,6 +404,9 @@ declare global {
403404
export type { ExprEditorOptions } from './composable/exprEditor'
404405
import('./composable/exprEditor')
405406
// @ts-ignore
407+
export type { TemplateEditorOptions } from './composable/templateEditor'
408+
import('./composable/templateEditor')
409+
// @ts-ignore
406410
export type { Config, Profile } from './stores/config'
407411
import('./stores/config')
408412
// @ts-ignore
@@ -454,6 +458,7 @@ declare module 'vue' {
454458
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
455459
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
456460
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
461+
readonly createTemplateEditor: UnwrapRef<typeof import('./composable/templateEditor')['createTemplateEditor']>
457462
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
458463
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
459464
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>

assets/components/Notification/DestinationForm.vue

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,10 @@
154154
$t("notifications.destination-form.template-hint")
155155
}}</span>
156156
</legend>
157-
<textarea
158-
v-model="template"
159-
class="textarea focus:textarea-primary min-h-48 w-full font-mono text-sm"
160-
:class="{ 'textarea-primary': template.trim().length > 0 }"
161-
></textarea>
157+
<div
158+
ref="templateEditorRef"
159+
class="border-base-content/20 focus-within:border-primary min-h-48 w-full overflow-auto rounded-lg border"
160+
></div>
162161
</fieldset>
163162

164163
<!-- Error -->
@@ -204,6 +203,7 @@
204203

205204
<script lang="ts" setup>
206205
import type { Dispatcher, TestWebhookResult } from "@/types/notifications";
206+
import { createTemplateEditor } from "@/composable/templateEditor";
207207
208208
type PayloadFormat = "slack" | "discord" | "ntfy" | "custom";
209209
@@ -277,6 +277,7 @@ const hasExistingCloudDestination = computed(() => {
277277
const isEditing = !!destination;
278278
279279
const nameInput = ref<HTMLInputElement>();
280+
const templateEditorRef = ref<HTMLElement>();
280281
const name = ref(destination?.name ?? "");
281282
useFocus(nameInput, { initialValue: true });
282283
const type = ref<"webhook" | "cloud">((destination?.type as "webhook" | "cloud") ?? "webhook");
@@ -292,11 +293,34 @@ const callbackUrl = `${window.location.origin}${withBase("/")}`;
292293
const cloudLinkUrl = `${__CLOUD_URL__}/link?appUrl=${encodeURIComponent(callbackUrl)}`;
293294
const cloudSettingsUrl = `${__CLOUD_URL__}/settings`;
294295
296+
let templateEditorView: Awaited<ReturnType<typeof createTemplateEditor>> | undefined;
297+
295298
function selectPayloadFormat(format: PayloadFormat) {
296299
payloadFormat.value = format;
297300
template.value = PAYLOAD_TEMPLATES[format];
301+
setEditorContent(template.value);
302+
}
303+
304+
function setEditorContent(value: string) {
305+
if (!templateEditorView) return;
306+
templateEditorView.dispatch({
307+
changes: { from: 0, to: templateEditorView.state.doc.length, insert: value },
308+
});
298309
}
299310
311+
onMounted(async () => {
312+
if (!templateEditorRef.value) return;
313+
templateEditorView = await createTemplateEditor({
314+
parent: templateEditorRef.value,
315+
initialValue: template.value,
316+
onChange: (v) => (template.value = v),
317+
});
318+
});
319+
320+
onScopeDispose(() => {
321+
templateEditorView?.destroy();
322+
});
323+
300324
const canTest = computed(() => {
301325
if (type.value === "webhook") {
302326
return webhookUrl.value.trim().length > 0;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
export interface TemplateEditorOptions {
2+
parent: HTMLElement;
3+
initialValue: string;
4+
onChange?: (value: string) => void;
5+
}
6+
7+
export async function createTemplateEditor(options: TemplateEditorOptions) {
8+
const [{ EditorView }, { EditorState }, { json }, { HighlightStyle, syntaxHighlighting }, { tags }] =
9+
await Promise.all([
10+
import("@codemirror/view"),
11+
import("@codemirror/state"),
12+
import("@codemirror/lang-json"),
13+
import("@codemirror/language"),
14+
import("@lezer/highlight"),
15+
]);
16+
17+
const editorTheme = EditorView.theme({
18+
"&": {
19+
backgroundColor: "var(--color-base-100)",
20+
color: "var(--color-base-content)",
21+
fontSize: "0.875rem",
22+
},
23+
".cm-content": {
24+
caretColor: "var(--color-primary)",
25+
fontFamily: "ui-monospace, monospace",
26+
},
27+
".cm-cursor": {
28+
borderLeftColor: "var(--color-primary)",
29+
},
30+
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground": {
31+
backgroundColor: "var(--color-base-300)",
32+
},
33+
".cm-activeLine": {
34+
backgroundColor: "color-mix(in oklch, var(--color-base-200) 50%, transparent)",
35+
},
36+
".cm-gutters": {
37+
backgroundColor: "var(--color-base-200)",
38+
color: "color-mix(in oklch, var(--color-base-content) 50%, transparent)",
39+
border: "none",
40+
},
41+
".cm-activeLineGutter": {
42+
backgroundColor: "var(--color-base-300)",
43+
},
44+
});
45+
46+
const highlightStyle = HighlightStyle.define([
47+
{ tag: tags.propertyName, color: "var(--color-info)" },
48+
{ tag: tags.string, color: "var(--color-success)" },
49+
{ tag: tags.number, color: "var(--color-warning)" },
50+
{ tag: tags.bool, color: "var(--color-warning)" },
51+
{ tag: tags.null, color: "var(--color-secondary)" },
52+
{ tag: tags.punctuation, color: "var(--color-base-content)" },
53+
]);
54+
55+
const state = EditorState.create({
56+
doc: options.initialValue,
57+
extensions: [
58+
EditorView.lineWrapping,
59+
json(),
60+
editorTheme,
61+
syntaxHighlighting(highlightStyle),
62+
EditorView.updateListener.of((update) => {
63+
if (update.docChanged && options.onChange) {
64+
options.onChange(update.view.state.doc.toString());
65+
}
66+
}),
67+
],
68+
});
69+
70+
return new EditorView({ state, parent: options.parent });
71+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dependencies": {
3232
"@codemirror/autocomplete": "^6.20.0",
3333
"@codemirror/lang-javascript": "^6.2.4",
34+
"@codemirror/lang-json": "^6.0.2",
3435
"@codemirror/language": "^6.12.1",
3536
"@codemirror/state": "^6.5.4",
3637
"@codemirror/theme-one-dark": "^6.1.3",

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)