-
Notifications
You must be signed in to change notification settings - Fork 26
Adding CodeMirror editor for code highlighting and editing #2279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<script setup lang="ts"> | ||
import CodeMirror from "vue-codemirror6"; | ||
import { json } from "@codemirror/lang-json"; | ||
import { xml } from "@codemirror/lang-xml"; | ||
import { StreamLanguage } from "@codemirror/language"; | ||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell"; | ||
import { shell } from "@codemirror/legacy-modes/mode/shell"; | ||
import { csharp } from "@codemirror/legacy-modes/mode/clike"; | ||
import { Extension } from "@codemirror/state"; | ||
import { CodeLanguage } from "@/components/codeEditorTypes"; | ||
import CopyToClipboard from "@/components/CopyToClipboard.vue"; | ||
import { computed } from "vue"; | ||
const code = defineModel<string>({ required: true }); | ||
const props = withDefaults( | ||
defineProps<{ | ||
language?: CodeLanguage; | ||
readOnly?: boolean; | ||
showGutter?: boolean; | ||
showCopyToClipboard?: boolean; | ||
ariaLabel?: string; | ||
}>(), | ||
{ readOnly: true, showGutter: true, showCopyToClipboard: true } | ||
); | ||
const extensions = computed(() => { | ||
const extensions: Extension[] = []; | ||
switch (props.language) { | ||
case "json": | ||
extensions.push(json()); | ||
break; | ||
case "xml": | ||
extensions.push(xml()); | ||
break; | ||
case "shell": | ||
extensions.push(StreamLanguage.define(shell)); | ||
break; | ||
case "powershell": | ||
extensions.push(StreamLanguage.define(powerShell)); | ||
break; | ||
case "csharp": | ||
extensions.push(StreamLanguage.define(csharp)); | ||
break; | ||
} | ||
return extensions; | ||
}); | ||
</script> | ||
|
||
<template> | ||
<div class="wrapper" :aria-label="ariaLabel"> | ||
<div v-if="props.showCopyToClipboard" class="toolbar"> | ||
<CopyToClipboard :value="code" /> | ||
</div> | ||
<CodeMirror v-model="code" :extensions="extensions" :basic="props.showGutter" :minimal="!props.showGutter" :readonly="props.readOnly" :gutter="!props.readOnly"></CodeMirror> | ||
</div> | ||
</template> | ||
|
||
<style scoped> | ||
.wrapper { | ||
border-radius: 0.5rem; | ||
padding: 0.5rem; | ||
border: 1px solid #ccc; | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
.toolbar { | ||
border-bottom: 1px solid #ccc; | ||
padding-bottom: 0.5rem; | ||
margin-bottom: 0.5rem; | ||
display: flex; | ||
flex-direction: row; | ||
justify-content: end; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<script setup lang="ts"> | ||
import { Tippy, TippyComponent } from "vue-tippy"; | ||
import { useTemplateRef } from "vue"; | ||
|
||
const props = defineProps<{ | ||
value: string; | ||
}>(); | ||
|
||
const tippyRef = useTemplateRef<TippyComponent | null>("tippyRef"); | ||
let timeoutId: number; | ||
|
||
async function copyToClipboard() { | ||
await navigator.clipboard.writeText(props.value); | ||
window.clearTimeout(timeoutId); | ||
tippyRef.value?.show(); | ||
timeoutId = window.setTimeout(() => tippyRef.value?.hide(), 3000); | ||
} | ||
</script> | ||
|
||
<template> | ||
<Tippy content="Copied" ref="tippyRef" trigger="manual"> | ||
<button type="button" class="btn btn-secondary btn-sm" @click="copyToClipboard"><i class="fa fa-copy"></i> Copy to clipboard</button> | ||
</Tippy> | ||
</template> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type CodeLanguage = "json" | "xml" | "shell" | "powershell" | "csharp"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the moment these are the only languages supported, we can add more later if needed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enum? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. works the same |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,9 @@ import MessageHeader from "./EditMessageHeader.vue"; | |
import { EditAndRetryConfig } from "@/resources/Configuration"; | ||
import type Header from "@/resources/Header"; | ||
import { ExtendedFailedMessage } from "@/resources/FailedMessage"; | ||
import parseContentType from "@/composables/contentTypeParser"; | ||
import { CodeLanguage } from "@/components/codeEditorTypes"; | ||
import CodeEditor from "@/components/CodeEditor.vue"; | ||
|
||
interface HeaderWithEditing extends Header { | ||
isLocked: boolean; | ||
|
@@ -28,6 +31,7 @@ interface LocalMessageState { | |
isBodyChanged: boolean; | ||
isBodyEmpty: boolean; | ||
isContentTypeSupported: boolean; | ||
language?: CodeLanguage; | ||
bodyContentType: string | undefined; | ||
bodyUnavailable: boolean; | ||
isEvent: boolean; | ||
|
@@ -104,30 +108,6 @@ function getContentType() { | |
return header?.value; | ||
} | ||
|
||
function isContentTypeSupported(contentType: string | undefined) { | ||
if (contentType === undefined) return false; | ||
|
||
if (contentType.startsWith("text/")) return true; | ||
|
||
const charsetUtf8 = "; charset=utf-8"; | ||
|
||
if (contentType.endsWith(charsetUtf8)) { | ||
contentType = contentType.substring(0, contentType.length - charsetUtf8.length); | ||
} | ||
|
||
if (contentType === "application/json") return true; | ||
|
||
if (contentType.startsWith("application/")) { | ||
// Some examples: | ||
// application/atom+xml | ||
// application/ld+json | ||
// application/vnd.masstransit+json | ||
if (contentType.endsWith("+json") || contentType.endsWith("+xml")) return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
Comment on lines
-107
to
-130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactor so it can be reused |
||
function getMessageIntent() { | ||
const intent = findHeadersByKey("NServiceBus.MessageIntent"); | ||
return intent?.value; | ||
|
@@ -167,7 +147,9 @@ function initializeMessageBodyAndHeaders() { | |
|
||
const contentType = getContentType(); | ||
localMessage.value.bodyContentType = contentType; | ||
localMessage.value.isContentTypeSupported = isContentTypeSupported(contentType); | ||
const parsedContentType = parseContentType(contentType); | ||
localMessage.value.isContentTypeSupported = parsedContentType.isSupported; | ||
localMessage.value.language = parsedContentType.language; | ||
|
||
const messageIntent = getMessageIntent(); | ||
localMessage.value.isEvent = messageIntent === "Publish"; | ||
|
@@ -248,7 +230,9 @@ onMounted(() => { | |
</tbody> | ||
</table> | ||
<div role="tabpanel" v-if="panel === 2 && !localMessage.bodyUnavailable" style="height: calc(100% - 260px)"> | ||
<textarea aria-label="message body" class="form-control" :disabled="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody"></textarea> | ||
<div style="margin-top: 1.25rem"> | ||
<CodeEditor aria-label="message body" :read-only="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody" :language="localMessage.language" :show-gutter="true"></CodeEditor> | ||
</div> | ||
<span class="empty-error" v-if="localMessage.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span> | ||
<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> | ||
<div class="alert alert-info" v-if="panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div> | ||
|
@@ -369,9 +353,4 @@ onMounted(() => { | |
overflow-y: auto; | ||
padding-right: 15px; | ||
} | ||
|
||
.modal-msg-editor :deep(textarea) { | ||
height: 100%; | ||
margin-top: 20px; | ||
} | ||
</style> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,20 @@ | ||
<script setup lang="ts"> | ||
import { ExtendedFailedMessage } from "@/resources/FailedMessage"; | ||
|
||
import { computed } from "vue"; | ||
import CodeEditor from "@/components/CodeEditor.vue"; | ||
import parseContentType from "@/composables/contentTypeParser"; | ||
const props = defineProps<{ | ||
message: ExtendedFailedMessage; | ||
}>(); | ||
|
||
const contentType = computed(() => parseContentType(props.message.contentType)); | ||
</script> | ||
|
||
<template> | ||
<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> | ||
<div v-else-if="props.message.bodyUnavailable" class="alert alert-info">Message body unavailable.</div> | ||
<pre v-else>{{ props.message.messageBody }}</pre> | ||
<CodeEditor v-else-if="contentType.isSupported" :model-value="props.message.messageBody" :language="contentType.language" :read-only="true" :show-gutter="true"></CodeEditor> | ||
<div v-else class="alert alert-warning">Message body cannot be displayed because content type "{{ props.message.contentType }}" is not supported.</div> | ||
</template> | ||
|
||
<style scoped></style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment here to explain that the copy on CodeMirror doesn't seem to work so we rolled our own
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, out of the box, there is no copy button.
The last time I looked at a plugin option, I can't remember which one.
So, we don't need a comment here to spell that out.