diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-01 21:08:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-01 21:08:50 +0300 |
commit | 2ea5aa8bd11544524b4f5da1e4c750f67bf5fc7d (patch) | |
tree | 7771ce1234dc66bb59eb5128561d485f91330056 /app/assets | |
parent | ae1efa2e1d32dee59d8f509ba17b623b5ffe4ba6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
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; } } |