Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/content_editor/components/wrappers')
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue207
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/image.vue103
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/reference.vue15
3 files changed, 310 insertions, 15 deletions
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index 4a3dfe3656c..efd0926d7ed 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -1,20 +1,33 @@
<script>
import { debounce } from 'lodash';
+import { GlButton, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
-import { __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
import codeBlockLanguageLoader from '../../services/code_block_language_loader';
import EditorStateObserver from '../editor_state_observer.vue';
+import { memoizedGet } from '../../services/utils';
+import {
+ lineOffsetToLangParams,
+ langParamsToLineOffset,
+ toAbsoluteLineOffset,
+ getLines,
+ appendNewlines,
+} from '../../services/code_suggestion_utils';
export default {
name: 'CodeBlock',
components: {
+ GlButton,
+ GlSprintf,
NodeViewWrapper,
NodeViewContent,
EditorStateObserver,
SandboxedMermaid,
},
+ directives: {
+ GlTooltip,
+ },
inject: ['contentEditor'],
props: {
editor: {
@@ -39,13 +52,54 @@ export default {
return {
diagramUrl: '',
diagramSource: '',
+
+ allLines: [],
+ deletedLines: [],
+ addedLines: [],
};
},
+ computed: {
+ isCodeSuggestion() {
+ return (
+ this.node.attrs.isCodeSuggestion &&
+ this.contentEditor.codeSuggestionsConfig?.canSuggest &&
+ this.contentEditor.codeSuggestionsConfig?.diffFile
+ );
+ },
+ classList() {
+ return this.isCodeSuggestion
+ ? 'gl-p-0! suggestion-added-input'
+ : `gl-p-3 code highlight ${this.$options.userColorScheme}`;
+ },
+ lineOffset() {
+ return langParamsToLineOffset(this.node.attrs.langParams);
+ },
+ absoluteLineOffset() {
+ if (!this.contentEditor.codeSuggestionsConfig) return [0, 0];
+
+ const { new_line: n } = this.contentEditor.codeSuggestionsConfig.line;
+ return toAbsoluteLineOffset(this.lineOffset, n);
+ },
+ disableDecrementLineStart() {
+ return this.absoluteLineOffset[0] <= 1;
+ },
+ disableIncrementLineStart() {
+ return this.lineOffset[0] >= 0;
+ },
+ disableDecrementLineEnd() {
+ return this.lineOffset[1] <= 0;
+ },
+ disableIncrementLineEnd() {
+ return this.absoluteLineOffset[1] >= this.allLines.length - 1;
+ },
+ },
async mounted() {
- this.updateDiagramPreview = debounce(
- this.updateDiagramPreview,
- DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
- );
+ if (this.isCodeSuggestion) {
+ await this.updateAllLines();
+ this.updateCodeSuggestion();
+ }
+
+ this.updateCodeBlock = debounce(this.updateCodeBlock, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
const lang = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(this.node.attrs.language);
await codeBlockLanguageLoader.loadLanguage(lang.syntax);
@@ -53,7 +107,26 @@ export default {
this.updateAttributes({ language: this.node.attrs.language });
},
methods: {
- async updateDiagramPreview() {
+ async updateAllLines() {
+ const { diffFile } = this.contentEditor.codeSuggestionsConfig;
+ this.allLines = (await memoizedGet(diffFile.view_path.replace('/blob/', '/raw/'))).split(
+ '\n',
+ );
+ },
+ updateCodeSuggestion() {
+ this.deletedLines = appendNewlines(getLines(this.absoluteLineOffset, this.allLines));
+ this.addedLines = appendNewlines(
+ this.$refs.nodeViewContent?.$el.textContent.split('\n') || [],
+ );
+ },
+ updateNodeView() {
+ if (this.isCodeSuggestion) {
+ this.updateCodeSuggestion();
+ } else {
+ this.updateCodeBlock();
+ }
+ },
+ async updateCodeBlock() {
if (!this.node.attrs.showPreview) {
this.diagramSource = '';
return;
@@ -70,22 +143,34 @@ export default {
);
}
},
- },
- i18n: {
- frontmatter: __('frontmatter'),
+ updateLineOffset(deltaStart = 0, deltaEnd = 0) {
+ const { lineOffset } = this;
+
+ this.editor
+ .chain()
+ .updateAttributes('codeSuggestion', {
+ langParams: lineOffsetToLangParams([
+ lineOffset[0] + deltaStart,
+ lineOffset[1] + deltaEnd,
+ ]),
+ })
+ .run();
+ },
},
userColorScheme: gon.user_color_scheme,
};
</script>
<template>
- <editor-state-observer @transaction="updateDiagramPreview">
+ <editor-state-observer :debounce="0" @transaction="updateNodeView">
<node-view-wrapper
- :class="`content-editor-code-block gl-relative code highlight gl-p-3 ${$options.userColorScheme}`"
+ :class="classList"
+ class="content-editor-code-block gl-relative"
as="pre"
dir="auto"
>
<div
v-if="node.attrs.showPreview"
+ contenteditable="false"
class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" />
@@ -93,12 +178,108 @@ export default {
</div>
<span
v-if="node.attrs.isFrontmatter"
+ contenteditable="false"
data-testid="frontmatter-label"
class="gl-absolute gl-top-0 gl-right-3"
+ >{{ __('frontmatter') }}:{{ node.attrs.language }}</span
+ >
+ <div
+ v-if="isCodeSuggestion"
contenteditable="false"
- >{{ $options.i18n.frontmatter }}:{{ node.attrs.language }}</span
+ class="gl-relative gl-z-index-0"
+ data-testid="code-suggestion-box"
>
- <node-view-content ref="nodeViewContent" as="code" />
+ <div
+ class="md-suggestion-header gl-flex-wrap gl-z-index-1 gl-w-full gl-border-none! gl-font-regular gl-px-4 gl-py-3 gl-border-b-1! gl-border-b-solid! gl-mr-n10!"
+ >
+ <div class="gl-font-weight-bold gl-pr-3">
+ {{ __('Suggested change') }}
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-pl-3 gl-gap-2 gl-white-space-nowrap"
+ >
+ <gl-sprintf :message="__('From line %{line1} to %{line2}')">
+ <template #line1>
+ <div class="gl-display-flex gl-bg-gray-50 gl-rounded-base gl-mx-1">
+ <gl-button
+ size="small"
+ icon="dash"
+ variant="confirm"
+ category="tertiary"
+ data-testid="decrement-line-start"
+ :aria-label="__('Decrement suggestion line start')"
+ :disabled="disableDecrementLineStart"
+ @click="updateLineOffset(-1, 0)"
+ />
+ <div
+ class="flex gl-align-items-center gl-justify-content-center gl-px-3 monospace"
+ >
+ <strong>{{ absoluteLineOffset[0] }}</strong>
+ </div>
+ <gl-button
+ size="small"
+ icon="plus"
+ variant="confirm"
+ category="tertiary"
+ data-testid="increment-line-start"
+ :aria-label="__('Increment suggestion line start')"
+ :disabled="disableIncrementLineStart"
+ @click="updateLineOffset(1, 0)"
+ />
+ </div>
+ </template>
+ <template #line2>
+ <div class="gl-display-flex gl-bg-gray-50 gl-rounded-base gl-ml-1">
+ <gl-button
+ size="small"
+ icon="dash"
+ variant="confirm"
+ category="tertiary"
+ data-testid="decrement-line-end"
+ :aria-label="__('Decrement suggestion line end')"
+ :disabled="disableDecrementLineEnd"
+ @click="updateLineOffset(0, -1)"
+ />
+ <div
+ class="flex gl-align-items-center gl-justify-content-center gl-px-3 monospace"
+ >
+ <strong>{{ absoluteLineOffset[1] }}</strong>
+ </div>
+ <gl-button
+ size="small"
+ icon="plus"
+ variant="confirm"
+ category="tertiary"
+ data-testid="increment-line-end"
+ :aria-label="__('Increment suggestion line end')"
+ :disabled="disableIncrementLineEnd"
+ @click="updateLineOffset(0, 1)"
+ />
+ </div>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+
+ <div class="suggestion-deleted" data-testid="suggestion-deleted">
+ <code
+ v-for="(line, i) in deletedLines"
+ :key="i"
+ :data-line-number="absoluteLineOffset[0] + i"
+ >{{ line }}</code
+ >
+ </div>
+ <div class="suggestion-added gl-absolute" data-testid="suggestion-added">
+ <code
+ v-for="(line, i) in addedLines"
+ :key="i"
+ :data-line-number="absoluteLineOffset[0] + i"
+ >{{ line }}</code
+ >
+ </div>
+ </div>
+ <node-view-content ref="nodeViewContent" as="code" class="gl-relative gl-z-index-1" />
</node-view-wrapper>
</editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
new file mode 100644
index 00000000000..0b80802d993
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue
@@ -0,0 +1,103 @@
+<script>
+import { NodeViewWrapper } from '@tiptap/vue-2';
+
+export default {
+ name: 'ImageWrapper',
+ components: {
+ NodeViewWrapper,
+ },
+ props: {
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ editor: {
+ type: Object,
+ required: true,
+ },
+ node: {
+ type: Object,
+ required: true,
+ },
+ selected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ dragData: {},
+ };
+ },
+ mounted() {
+ document.addEventListener('mousemove', this.onDrag);
+ document.addEventListener('mouseup', this.onDragEnd);
+ },
+ destroyed() {
+ document.removeEventListener('mousemove', this.onDrag);
+ document.removeEventListener('mouseup', this.onDragEnd);
+ },
+ methods: {
+ onDragStart(handle, event) {
+ this.dragData = {
+ handle,
+ startX: event.screenX,
+ startY: event.screenY,
+ width: this.$refs.image.width,
+ height: this.$refs.image.height,
+ };
+ },
+ onDrag(event) {
+ const { handle, startX, width, height } = this.dragData;
+ if (!handle) return;
+
+ const deltaX = event.screenX - startX;
+ const newWidth = handle.includes('w') ? width - deltaX : width + deltaX;
+ const newHeight = (height / width) * newWidth;
+
+ this.$refs.image.setAttribute('width', newWidth);
+ this.$refs.image.setAttribute('height', newHeight);
+ },
+ onDragEnd() {
+ const { handle } = this.dragData;
+ if (!handle) return;
+
+ this.dragData = {};
+
+ this.editor
+ .chain()
+ .focus()
+ .updateAttributes(this.node.type, {
+ width: this.$refs.image.width,
+ height: this.$refs.image.height,
+ })
+ .setNodeSelection(this.getPos())
+ .run();
+ },
+ },
+ resizeHandles: ['ne', 'nw', 'se', 'sw'],
+};
+</script>
+<template>
+ <node-view-wrapper as="span" class="gl-relative gl-display-inline-block">
+ <span
+ v-for="handle in $options.resizeHandles"
+ v-show="selected"
+ :key="handle"
+ class="image-resize"
+ :class="`image-resize-${handle}`"
+ :data-testid="`image-resize-${handle}`"
+ @mousedown="onDragStart(handle, $event)"
+ ></span>
+ <img
+ ref="image"
+ :src="node.attrs.src"
+ :alt="node.attrs.alt"
+ :title="node.attrs.title"
+ :width="node.attrs.width || 'auto'"
+ :height="node.attrs.height || 'auto'"
+ :class="{ 'ProseMirror-selectednode': selected }"
+ />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/reference.vue b/app/assets/javascripts/content_editor/components/wrappers/reference.vue
index 2b4b9891c77..4ec477232d4 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/reference.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/reference.vue
@@ -3,11 +3,12 @@ import { NodeViewWrapper } from '@tiptap/vue-2';
import { GlLink } from '@gitlab/ui';
export default {
- name: 'DetailsWrapper',
+ name: 'ReferenceWrapper',
components: {
NodeViewWrapper,
GlLink,
},
+ inject: ['contentEditor'],
props: {
node: {
type: Object,
@@ -19,6 +20,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ href: '#',
+ };
+ },
computed: {
text() {
return this.node.attrs.text;
@@ -33,6 +39,11 @@ export default {
return gon.current_username === this.text.substring(1);
},
},
+ async mounted() {
+ const text = this.node.attrs.originalText || this.node.attrs.text;
+ const { href } = await this.contentEditor.resolveReference(text);
+ this.href = href || '';
+ },
};
</script>
<template>
@@ -40,7 +51,7 @@ export default {
<span v-if="isCommand">{{ text }}</span>
<gl-link
v-else
- href="#"
+ :href="href"
tabindex="-1"
class="gfm gl-cursor-text"
:class="{