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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-01 21:08:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-01 21:08:50 +0300
commit2ea5aa8bd11544524b4f5da1e4c750f67bf5fc7d (patch)
tree7771ce1234dc66bb59eb5128561d485f91330056 /app/assets
parentae1efa2e1d32dee59d8f509ba17b623b5ffe4ba6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js30
-rw-r--r--app/assets/javascripts/editor/editor_markdown_ext.js99
-rw-r--r--app/assets/javascripts/issue_show/components/issuable_header_warnings.vue28
-rw-r--r--app/assets/javascripts/issue_show/index.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js43
-rw-r--r--app/assets/javascripts/notes/components/sort_discussion.vue5
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js3
-rw-r--r--app/assets/stylesheets/components/rich_content_editor.scss53
9 files changed, 238 insertions, 36 deletions
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index a725b3fe5d6..7e5be8454fe 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -8,15 +8,16 @@ import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
-const monacoEnabled = window?.gon?.features?.monacoBlobs;
+const monacoEnabledGlobally = window.gon.features?.monacoBlobs;
export default class EditBlob {
// The options object has:
// assetsPath, filePath, currentAction, projectId, isMarkdown
constructor(options) {
this.options = options;
- const { isMarkdown } = this.options;
- Promise.resolve()
+ this.options.monacoEnabled = this.options.monacoEnabled ?? monacoEnabledGlobally;
+ const { isMarkdown, monacoEnabled } = this.options;
+ return Promise.resolve()
.then(() => {
return monacoEnabled ? this.configureMonacoEditor() : this.configureAceEditor();
})
@@ -33,8 +34,15 @@ export default class EditBlob {
}
configureMonacoEditor() {
- return import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite').then(
- EditorModule => {
+ const EditorPromise = import(
+ /* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite'
+ );
+ const MarkdownExtensionPromise = this.options.isMarkdown
+ ? import('~/editor/editor_markdown_ext')
+ : Promise.resolve(false);
+
+ return Promise.all([EditorPromise, MarkdownExtensionPromise])
+ .then(([EditorModule, MarkdownExtension]) => {
const EditorLite = EditorModule.default;
const editorEl = document.getElementById('editor');
const fileNameEl =
@@ -44,6 +52,10 @@ export default class EditBlob {
this.editor = new EditorLite();
+ if (MarkdownExtension) {
+ this.editor.use(MarkdownExtension.default);
+ }
+
this.editor.createInstance({
el: editorEl,
blobPath: fileNameEl.value,
@@ -57,8 +69,8 @@ export default class EditBlob {
form.addEventListener('submit', () => {
fileContentEl.value = this.editor.getValue();
});
- },
- );
+ })
+ .catch(() => createFlash(BLOB_EDITOR_ERROR));
}
configureAceEditor() {
@@ -126,7 +138,7 @@ export default class EditBlob {
}
initSoftWrap() {
- this.isSoftWrapped = Boolean(monacoEnabled);
+ this.isSoftWrapped = Boolean(this.options.monacoEnabled);
this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.$toggleButton.on('click', () => this.toggleSoftWrap());
@@ -135,7 +147,7 @@ export default class EditBlob {
toggleSoftWrap() {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
- if (monacoEnabled) {
+ if (this.options.monacoEnabled) {
this.editor.updateOptions({ wordWrap: this.isSoftWrapped ? 'on' : 'off' });
} else {
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/editor_markdown_ext.js
new file mode 100644
index 00000000000..9d09663e643
--- /dev/null
+++ b/app/assets/javascripts/editor/editor_markdown_ext.js
@@ -0,0 +1,99 @@
+export default {
+ getSelectedText(selection = this.getSelection()) {
+ const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
+ const valArray = this.instance.getValue().split('\n');
+ let text = '';
+ if (startLineNumber === endLineNumber) {
+ text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1);
+ } else {
+ const startLineText = valArray[startLineNumber - 1].slice(startColumn - 1);
+ const endLineText = valArray[endLineNumber - 1].slice(0, endColumn - 1);
+
+ for (let i = startLineNumber, k = endLineNumber - 1; i < k; i += 1) {
+ text += `${valArray[i]}`;
+ if (i !== k - 1) text += `\n`;
+ }
+ text = text
+ ? [startLineText, text, endLineText].join('\n')
+ : [startLineText, endLineText].join('\n');
+ }
+ return text;
+ },
+
+ getSelection() {
+ return this.instance.getSelection();
+ },
+
+ replaceSelectedText(text, select = undefined) {
+ const forceMoveMarkers = !select;
+ this.instance.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]);
+ },
+
+ moveCursor(dx = 0, dy = 0) {
+ const pos = this.instance.getPosition();
+ pos.column += dx;
+ pos.lineNumber += dy;
+ this.instance.setPosition(pos);
+ },
+
+ /**
+ * Adjust existing selection to select text within the original selection.
+ * - If `selectedText` is not supplied, we fetch selected text with
+ *
+ * ALGORITHM:
+ *
+ * MULTI-LINE SELECTION
+ * 1. Find line that contains `toSelect` text.
+ * 2. Using the index of this line and the position of `toSelect` text in it,
+ * construct:
+ * * newStartLineNumber
+ * * newStartColumn
+ *
+ * SINGLE-LINE SELECTION
+ * 1. Use `startLineNumber` from the current selection as `newStartLineNumber`
+ * 2. Find the position of `toSelect` text in it to get `newStartColumn`
+ *
+ * 3. `newEndLineNumber` — Since this method is supposed to be used with
+ * markdown decorators that are pretty short, the `newEndLineNumber` is
+ * suggested to be assumed the same as the startLine.
+ * 4. `newEndColumn` — pretty obvious
+ * 5. Adjust the start and end positions of the current selection
+ * 6. Re-set selection on the instance
+ *
+ * @param {string} toSelect - New text to select within current selection.
+ * @param {string} selectedText - Currently selected text. It's just a
+ * shortcut: If it's not supplied, we fetch selected text from the instance
+ */
+ selectWithinSelection(toSelect, selectedText) {
+ const currentSelection = this.getSelection();
+ if (currentSelection.isEmpty() || !toSelect) {
+ return;
+ }
+ const text = selectedText || this.getSelectedText(currentSelection);
+ let lineShift;
+ let newStartLineNumber;
+ let newStartColumn;
+
+ const textLines = text.split('\n');
+
+ if (textLines.length > 1) {
+ // Multi-line selection
+ lineShift = textLines.findIndex(line => line.indexOf(toSelect) !== -1);
+ newStartLineNumber = currentSelection.startLineNumber + lineShift;
+ newStartColumn = textLines[lineShift].indexOf(toSelect) + 1;
+ } else {
+ // Single-line selection
+ newStartLineNumber = currentSelection.startLineNumber;
+ newStartColumn = currentSelection.startColumn + text.indexOf(toSelect);
+ }
+
+ const newEndLineNumber = newStartLineNumber;
+ const newEndColumn = newStartColumn + toSelect.length;
+
+ const newSelection = currentSelection
+ .setStartPosition(newStartLineNumber, newStartColumn)
+ .setEndPosition(newEndLineNumber, newEndColumn);
+
+ this.instance.setSelection(newSelection);
+ },
+};
diff --git a/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue b/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue
new file mode 100644
index 00000000000..b6816be9eb8
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue
@@ -0,0 +1,28 @@
+<script>
+import { mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ Icon,
+ },
+ computed: {
+ ...mapState({
+ confidential: ({ noteableData }) => noteableData.confidential,
+ dicussionLocked: ({ noteableData }) => noteableData.discussion_locked,
+ }),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-inline-block">
+ <div v-if="confidential" class="issuable-warning-icon inline">
+ <icon class="icon" name="eye-slash" data-testid="confidential" />
+ </div>
+
+ <div v-if="dicussionLocked" class="issuable-warning-icon inline">
+ <icon class="icon" name="lock" data-testid="locked" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index e170d338408..fe4ff133145 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
import issuableApp from './components/app.vue';
+import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
import { parseIssuableData } from './utils/parse_data';
+import { store } from '~/notes/stores';
export default function initIssueableApp() {
return new Vue({
@@ -15,3 +17,13 @@ export default function initIssueableApp() {
},
});
}
+
+export function issuableHeaderWarnings() {
+ return new Vue({
+ el: document.getElementById('js-issuable-header-warnings'),
+ store,
+ render(createElement) {
+ return createElement(IssuableHeaderWarnings);
+ },
+ });
+}
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 0dfc144c363..4d25ee9e4bd 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -27,9 +27,28 @@ function lineAfter(text, textarea) {
.split('\n')[0];
}
+function convertMonacoSelectionToAceFormat(sel) {
+ return {
+ start: {
+ row: sel.startLineNumber,
+ column: sel.startColumn,
+ },
+ end: {
+ row: sel.endLineNumber,
+ column: sel.endColumn,
+ },
+ };
+}
+
+function getEditorSelectionRange(editor) {
+ return window.gon.features?.monacoBlobs
+ ? convertMonacoSelectionToAceFormat(editor.getSelection())
+ : editor.getSelectionRange();
+}
+
function editorBlockTagText(text, blockTag, selected, editor) {
const lines = text.split('\n');
- const selectionRange = editor.getSelectionRange();
+ const selectionRange = getEditorSelectionRange(editor);
const shouldRemoveBlock =
lines[selectionRange.start.row - 1] === blockTag &&
lines[selectionRange.end.row + 1] === blockTag;
@@ -90,8 +109,12 @@ function moveCursor({
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
} else if (editor) {
- editor.navigateLeft(tag.length - tag.indexOf(select));
- editor.getSelection().selectAWord();
+ if (window.gon.features?.monacoBlobs) {
+ editor.selectWithinSelection(select, tag);
+ } else {
+ editor.navigateLeft(tag.length - tag.indexOf(select));
+ editor.getSelection().selectAWord();
+ }
return;
}
}
@@ -115,7 +138,11 @@ function moveCursor({
}
} else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
if (positionBetweenTags) {
- editor.navigateLeft(tag.length);
+ if (window.gon.features?.monacoBlobs) {
+ editor.moveCursor(tag.length * -1);
+ } else {
+ editor.navigateLeft(tag.length);
+ }
}
}
}
@@ -140,7 +167,7 @@ export function insertMarkdownText({
let textToInsert;
if (editor) {
- const selectionRange = editor.getSelectionRange();
+ const selectionRange = getEditorSelectionRange(editor);
editorSelectionStart = selectionRange.start;
editorSelectionEnd = selectionRange.end;
@@ -237,7 +264,11 @@ export function insertMarkdownText({
}
if (editor) {
- editor.insert(textToInsert);
+ if (window.gon.features?.monacoBlobs) {
+ editor.replaceSelectedText(textToInsert, select);
+ } else {
+ editor.insert(textToInsert);
+ }
} else {
insertText(textArea, textToInsert);
}
diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue
index 4a7543819eb..60b531d7597 100644
--- a/app/assets/javascripts/notes/components/sort_discussion.vue
+++ b/app/assets/javascripts/notes/components/sort_discussion.vue
@@ -49,7 +49,10 @@ export default {
</script>
<template>
- <div class="mr-2 d-inline-block align-bottom full-width-mobile">
+ <div
+ data-testid="sort-discussion-filter"
+ class="gl-mr-2 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"
+ >
<local-storage-sync
:value="sortDirection"
:storage-key="storageKey"
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 329bf5e147e..c71e62cb551 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -26,6 +26,7 @@ export default () => ({
},
userData: {},
noteableData: {
+ discussion_locked: false,
confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes.
current_user: {},
preview_note_path: 'path/to/preview',
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index e700d2bef47..32f77465347 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,7 +3,7 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
-import initIssueableApp from '~/issue_show';
+import initIssueableApp, { issuableHeaderWarnings } from '~/issue_show';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
@@ -12,6 +12,7 @@ export default function() {
initIssueableApp();
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
+ issuableHeaderWarnings();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then(module => module.default())
diff --git a/app/assets/stylesheets/components/rich_content_editor.scss b/app/assets/stylesheets/components/rich_content_editor.scss
index bedd06ec9a1..8d31b386d9e 100644
--- a/app/assets/stylesheets/components/rich_content_editor.scss
+++ b/app/assets/stylesheets/components/rich_content_editor.scss
@@ -2,30 +2,45 @@
* Overrides styles from ToastUI editor
*/
-// Toolbar buttons
-.tui-editor-defaultUI-toolbar .toolbar-button {
- color: $gl-gray-600;
- border: 0;
-
- &:hover,
- &:active {
- color: $blue-500;
+.tui-editor-defaultUI {
+
+ // Toolbar buttons
+ .tui-editor-defaultUI-toolbar .toolbar-button {
+ color: $gl-gray-600;
border: 0;
+
+ &:hover,
+ &:active {
+ color: $blue-500;
+ border: 0;
+ }
}
-}
-// Contextual menu's & popups
-.tui-editor-defaultUI .tui-popup-wrapper {
- @include gl-overflow-hidden;
- @include gl-rounded-base;
- @include gl-border-gray-400;
+ // Contextual menu's & popups
+ .tui-popup-wrapper {
+ @include gl-overflow-hidden;
+ @include gl-rounded-base;
+ @include gl-border-gray-400;
- hr {
- @include gl-m-0;
- @include gl-bg-gray-400;
+ hr {
+ @include gl-m-0;
+ @include gl-bg-gray-400;
+ }
+
+ button {
+ @include gl-text-gray-800;
+ }
}
- button {
- @include gl-text-gray-800;
+ /**
+ * Overrides styles from ToastUI's Code Mirror (markdown mode) editor.
+ * Toast UI internally overrides some of these using the `.tui-md-` prefix.
+ * https://codemirror.net/doc/manual.html#styling
+ */
+
+ .te-md-container .CodeMirror * {
+ @include gl-font-monospace;
+ @include gl-font-size-monospace;
+ @include gl-line-height-20;
}
}