Skip to content

Commit e0e6d18

Browse files
committed
Adding CodeMirror editor for code highlighting and editing
Removed previous code highlighter
1 parent 90c1f69 commit e0e6d18

File tree

18 files changed

+517
-330
lines changed

18 files changed

+517
-330
lines changed

src/Frontend/package-lock.json

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

src/Frontend/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,26 @@
1717
"test:application": "npm run test:application:vitest"
1818
},
1919
"dependencies": {
20+
"@codemirror/lang-json": "^6.0.1",
21+
"@codemirror/lang-xml": "^6.1.0",
22+
"@codemirror/legacy-modes": "^6.5.0",
2023
"@tinyhttp/content-disposition": "^2.2.2",
2124
"@vue-flow/core": "^1.41.5",
22-
"@wdns/vue-code-block": "^2.3.3",
2325
"bootstrap": "^5.3.3",
2426
"bootstrap-icons": "^1.11.3",
25-
"highlight.js": "^11.10.0",
27+
"codemirror": "^6.0.1",
2628
"lossless-json": "^4.0.2",
2729
"memoize-one": "^6.0.0",
2830
"moment": "^2.30.1",
2931
"pinia": "^2.2.8",
3032
"vue": "^3.5.13",
33+
"vue-codemirror6": "^1.3.12",
3134
"vue-router": "^4.4.5",
3235
"vue-tippy": "^6.5.0",
3336
"vue-toastification": "^2.0.0-rc.5",
3437
"vue3-cookies": "^1.0.6",
35-
"vue3-simple-typeahead": "^1.0.11"
38+
"vue3-simple-typeahead": "^1.0.11",
39+
"xml-formatter": "^3.6.4"
3640
},
3741
"devDependencies": {
3842
"@eslint/js": "^9.13.0",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script setup lang="ts">
2+
import CodeMirror from "vue-codemirror6";
3+
import { json } from "@codemirror/lang-json";
4+
import { xml } from "@codemirror/lang-xml";
5+
import { StreamLanguage } from "@codemirror/language";
6+
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
7+
import { shell } from "@codemirror/legacy-modes/mode/shell";
8+
import { csharp } from "@codemirror/legacy-modes/mode/clike";
9+
import { Extension } from "@codemirror/state";
10+
import { CodeLanguage } from "@/components/codeEditorTypes";
11+
import CopyToClipboard from "@/components/CopyToClipboard.vue";
12+
import { computed } from "vue";
13+
14+
const code = defineModel<string>({ required: true });
15+
const props = withDefaults(
16+
defineProps<{
17+
language?: CodeLanguage;
18+
readOnly?: boolean;
19+
showGutter?: boolean;
20+
showCopyToClipboard?: boolean;
21+
}>(),
22+
{ readOnly: true, showGutter: true, showCopyToClipboard: true }
23+
);
24+
25+
const extensions = computed(() => {
26+
const extensions: Extension[] = [];
27+
28+
switch (props.language) {
29+
case "json":
30+
extensions.push(json());
31+
break;
32+
case "xml":
33+
extensions.push(xml());
34+
break;
35+
case "shell":
36+
extensions.push(StreamLanguage.define(shell));
37+
break;
38+
case "powershell":
39+
extensions.push(StreamLanguage.define(powerShell));
40+
break;
41+
case "csharp":
42+
extensions.push(StreamLanguage.define(csharp));
43+
break;
44+
}
45+
46+
return extensions;
47+
});
48+
</script>
49+
50+
<template>
51+
<div class="wrapper">
52+
<div v-if="props.showCopyToClipboard" class="toolbar">
53+
<CopyToClipboard :value="code" />
54+
</div>
55+
<CodeMirror v-model="code" :extensions="extensions" :basic="props.showGutter" :minimal="!props.showGutter" :readonly="props.readOnly" :gutter="!props.readOnly"></CodeMirror>
56+
</div>
57+
</template>
58+
59+
<style scoped>
60+
.wrapper {
61+
border-radius: 0.5rem;
62+
padding: 0.5rem;
63+
border: 1px solid #ccc;
64+
display: flex;
65+
flex-direction: column;
66+
}
67+
.toolbar {
68+
border-bottom: 1px solid #ccc;
69+
padding-bottom: 0.5rem;
70+
margin-bottom: 0.5rem;
71+
display: flex;
72+
flex-direction: row;
73+
justify-content: end;
74+
}
75+
</style>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
import { Tippy, TippyComponent } from "vue-tippy";
3+
import { useTemplateRef } from "vue";
4+
5+
const props = defineProps<{
6+
value: string;
7+
}>();
8+
9+
const tippyRef = useTemplateRef<TippyComponent | null>("tippyRef");
10+
let timeoutId: number;
11+
12+
async function copyToClipboard() {
13+
await navigator.clipboard.writeText(props.value);
14+
window.clearTimeout(timeoutId);
15+
tippyRef.value?.show();
16+
timeoutId = window.setTimeout(() => tippyRef.value?.hide(), 3000);
17+
}
18+
</script>
19+
20+
<template>
21+
<Tippy content="Copied" ref="tippyRef" trigger="manual">
22+
<button type="button" class="btn btn-sm" @click="copyToClipboard"><i class="fa fa-copy"></i> Copy to clipboard</button>
23+
</Tippy>
24+
</template>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export type CodeLanguage = "json" | "xml" | "shell" | "powershell" | "csharp";

src/Frontend/src/components/configuration/EndpointConnection.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ServiceControlNotAvailable from "../ServiceControlNotAvailable.vue";
55
import { licenseStatus } from "@/composables/serviceLicense";
66
import { connectionState, useServiceControlConnections } from "@/composables/serviceServiceControl";
77
import BusyIndicator from "../BusyIndicator.vue";
8-
import VCodeBlock from "@wdns/vue-code-block";
8+
import CodeEditor from "@/components/CodeEditor.vue";
99
1010
const isExpired = licenseStatus.isExpired;
1111
@@ -109,7 +109,7 @@ function switchJsonTab() {
109109
<section v-if="showCodeOnlyTab && !loading">
110110
<div class="row">
111111
<div class="col-12 h-100">
112-
<VCodeBlock :code="inlineSnippet" lang="csharp"></VCodeBlock>
112+
<CodeEditor :model-value="inlineSnippet" language="csharp" :show-gutter="false"></CodeEditor>
113113
</div>
114114
</div>
115115
</section>
@@ -119,11 +119,11 @@ function switchJsonTab() {
119119
<div class="col-12 h-100">
120120
<p>Note that when using JSON for configuration, you also need to change the endpoint configuration as shown below.</p>
121121
<p><strong>Endpoint configuration:</strong></p>
122-
<VCodeBlock :code="jsonSnippet" lang="csharp"></VCodeBlock>
122+
<CodeEditor :model-value="jsonSnippet" language="csharp" :show-gutter="false"></CodeEditor>
123123
<p style="margin-top: 15px">
124124
<strong>JSON configuration file:</strong>
125125
</p>
126-
<VCodeBlock :code="jsonConfig" lang="json"></VCodeBlock>
126+
<CodeEditor :model-value="jsonConfig" language="json" :show-gutter="false"></CodeEditor>
127127
</div>
128128
</div>
129129
</section>

src/Frontend/src/components/failedmessages/EditRetryDialog.vue

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import MessageHeader from "./EditMessageHeader.vue";
55
import { EditAndRetryConfig } from "@/resources/Configuration";
66
import type Header from "@/resources/Header";
77
import { ExtendedFailedMessage } from "@/resources/FailedMessage";
8+
import parseContentType from "@/composables/contentTypeParser";
9+
import { CodeLanguage } from "@/components/codeEditorTypes";
10+
import CodeEditor from "@/components/CodeEditor.vue";
811
912
interface HeaderWithEditing extends Header {
1013
isLocked: boolean;
@@ -28,6 +31,7 @@ interface LocalMessageState {
2831
isBodyChanged: boolean;
2932
isBodyEmpty: boolean;
3033
isContentTypeSupported: boolean;
34+
language?: CodeLanguage;
3135
bodyContentType: string | undefined;
3236
bodyUnavailable: boolean;
3337
isEvent: boolean;
@@ -104,30 +108,6 @@ function getContentType() {
104108
return header?.value;
105109
}
106110
107-
function isContentTypeSupported(contentType: string | undefined) {
108-
if (contentType === undefined) return false;
109-
110-
if (contentType.startsWith("text/")) return true;
111-
112-
const charsetUtf8 = "; charset=utf-8";
113-
114-
if (contentType.endsWith(charsetUtf8)) {
115-
contentType = contentType.substring(0, contentType.length - charsetUtf8.length);
116-
}
117-
118-
if (contentType === "application/json") return true;
119-
120-
if (contentType.startsWith("application/")) {
121-
// Some examples:
122-
// application/atom+xml
123-
// application/ld+json
124-
// application/vnd.masstransit+json
125-
if (contentType.endsWith("+json") || contentType.endsWith("+xml")) return true;
126-
}
127-
128-
return false;
129-
}
130-
131111
function getMessageIntent() {
132112
const intent = findHeadersByKey("NServiceBus.MessageIntent");
133113
return intent?.value;
@@ -167,7 +147,9 @@ function initializeMessageBodyAndHeaders() {
167147
168148
const contentType = getContentType();
169149
localMessage.value.bodyContentType = contentType;
170-
localMessage.value.isContentTypeSupported = isContentTypeSupported(contentType);
150+
const parsedContentType = parseContentType(contentType);
151+
localMessage.value.isContentTypeSupported = parsedContentType.isSupported;
152+
localMessage.value.language = parsedContentType.language;
171153
172154
const messageIntent = getMessageIntent();
173155
localMessage.value.isEvent = messageIntent === "Publish";
@@ -248,7 +230,9 @@ onMounted(() => {
248230
</tbody>
249231
</table>
250232
<div role="tabpanel" v-if="panel === 2 && !localMessage.bodyUnavailable" style="height: calc(100% - 260px)">
251-
<textarea aria-label="message body" class="form-control" :disabled="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
233+
<div style="margin-top: 20px">
234+
<CodeEditor role="textbox" aria-label="message body" :read-only="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody" :language="localMessage.language" :show-gutter="true"></CodeEditor>
235+
</div>
252236
<span class="empty-error" v-if="localMessage.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
253237
<span class="reset-body" v-if="localMessage.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
254238
<div class="alert alert-info" v-if="panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
@@ -369,9 +353,4 @@ onMounted(() => {
369353
overflow-y: auto;
370354
padding-right: 15px;
371355
}
372-
373-
.modal-msg-editor :deep(textarea) {
374-
height: 100%;
375-
margin-top: 20px;
376-
}
377356
</style>

src/Frontend/src/components/failedmessages/MessageRedirectForBackwardsCompatibility.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ onMounted(async () => {
1111
await router.push({ path: routeLinks.messages.message.link(id.toString()), query: { back: routeLinks.failedMessage.failedMessages.link } });
1212
});
1313
</script>
14+
<template>
15+
<template></template>
16+
</template>
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
<script setup lang="ts">
22
import { ExtendedFailedMessage } from "@/resources/FailedMessage";
3-
3+
import { computed } from "vue";
4+
import CodeEditor from "@/components/CodeEditor.vue";
5+
import parseContentType from "@/composables/contentTypeParser";
46
const props = defineProps<{
57
message: ExtendedFailedMessage;
68
}>();
9+
10+
const contentType = computed(() => parseContentType(props.message.contentType));
711
</script>
812

913
<template>
1014
<div v-if="props.message.messageBodyNotFound" class="alert alert-info">Could not find message body. This could be because the message URL is invalid or the corresponding message was processed and is no longer tracked by ServiceControl.</div>
1115
<div v-else-if="props.message.bodyUnavailable" class="alert alert-info">Message body unavailable.</div>
12-
<pre v-else>{{ props.message.messageBody }}</pre>
16+
<CodeEditor v-else-if="contentType.isSupported" :model-value="props.message.messageBody" :language="contentType.language" :read-only="true" :show-gutter="true"></CodeEditor>
17+
<div v-else class="alert alert-warning">Message body cannot be displayed because content type "{{ props.message.contentType }}" is not supported.</div>
1318
</template>
1419

1520
<style scoped></style>

0 commit comments

Comments
 (0)