diff options
Diffstat (limited to 'app/assets')
8 files changed, 188 insertions, 73 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index b5e17a0587d..fe63ebd470d 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,6 +1,7 @@ import flash from '~/flash'; import $ from 'jquery'; -import { sprintf, __ } from '../../locale'; +import { __, sprintf } from '~/locale'; +import { once } from 'lodash'; // Renders diagrams and flowcharts from text using Mermaid in any element with the // `js-render-mermaid` class. @@ -18,14 +19,10 @@ import { sprintf, __ } from '../../locale'; // This is an arbitrary number; Can be iterated upon when suitable. const MAX_CHAR_LIMIT = 5000; +let mermaidModule = {}; -function renderMermaids($els) { - if (!$els.length) return; - - // A diagram may have been truncated in search results which will cause errors, so abort the render. - if (document.querySelector('body').dataset.page === 'search:show') return; - - import(/* webpackChunkName: 'mermaid' */ 'mermaid') +function importMermaidModule() { + return import(/* webpackChunkName: 'mermaid' */ 'mermaid') .then(mermaid => { mermaid.initialize({ // mermaid core options @@ -41,63 +38,127 @@ function renderMermaids($els) { securityLevel: 'strict', }); + mermaidModule = mermaid; + + return mermaid; + }) + .catch(err => { + flash(sprintf(__("Can't load mermaid module: %{err}"), { err })); + // eslint-disable-next-line no-console + console.error(err); + }); +} + +function fixElementSource(el) { + // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly. + const source = el.textContent.replace(/<br\s*\/>/g, '<br>'); + + // Remove any extra spans added by the backend syntax highlighting. + Object.assign(el, { textContent: source }); + + return { source }; +} + +function renderMermaidEl(el) { + mermaidModule.init(undefined, el, id => { + const source = el.textContent; + const svg = document.getElementById(id); + + // As of https://github.com/knsv/mermaid/commit/57b780a0d, + // Mermaid will make two init callbacks:one to initialize the + // flow charts, and another to initialize the Gannt charts. + // Guard against an error caused by double initialization. + if (svg.classList.contains('mermaid')) { + return; + } + + svg.classList.add('mermaid'); + + // pre > code > svg + svg.closest('pre').replaceWith(svg); + + // We need to add the original source into the DOM to allow Copy-as-GFM + // to access it. + const sourceEl = document.createElement('text'); + sourceEl.classList.add('source'); + sourceEl.setAttribute('display', 'none'); + sourceEl.textContent = source; + + svg.appendChild(sourceEl); + }); +} + +function renderMermaids($els) { + if (!$els.length) return; + + // A diagram may have been truncated in search results which will cause errors, so abort the render. + if (document.querySelector('body').dataset.page === 'search:show') return; + + importMermaidModule() + .then(() => { let renderedChars = 0; $els.each((i, el) => { - // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly. - const source = el.textContent.replace(/<br\s*\/>/g, '<br>'); - + const { source } = fixElementSource(el); /** * Restrict the rendering to a certain amount of character to * prevent mermaidjs from hanging up the entire thread and * causing a DoS. */ if ((source && source.length > MAX_CHAR_LIMIT) || renderedChars > MAX_CHAR_LIMIT) { - el.textContent = sprintf( - __( - 'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.', - ), - { charLimit: MAX_CHAR_LIMIT }, - ); + const html = ` + <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert"> + <div> + <div class="display-flex"> + <div>${__( + 'Warning: Displaying this diagram might cause performance issues on this page.', + )}</div> + <div class="gl-alert-actions"> + <button class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md new-gl-button">Display</button> + </div> + </div> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + </div> + `; + + const $parent = $(el).parent(); + + if (!$parent.hasClass('lazy-alert-shown')) { + $parent.after(html); + $parent.addClass('lazy-alert-shown'); + } + return; } renderedChars += source.length; - // Remove any extra spans added by the backend syntax highlighting. - Object.assign(el, { textContent: source }); - - mermaid.init(undefined, el, id => { - const svg = document.getElementById(id); - - // As of https://github.com/knsv/mermaid/commit/57b780a0d, - // Mermaid will make two init callbacks:one to initialize the - // flow charts, and another to initialize the Gannt charts. - // Guard against an error caused by double initialization. - if (svg.classList.contains('mermaid')) { - return; - } - - svg.classList.add('mermaid'); - - // pre > code > svg - svg.closest('pre').replaceWith(svg); - // We need to add the original source into the DOM to allow Copy-as-GFM - // to access it. - const sourceEl = document.createElement('text'); - sourceEl.classList.add('source'); - sourceEl.setAttribute('display', 'none'); - sourceEl.textContent = source; - - svg.appendChild(sourceEl); - }); + renderMermaidEl(el); }); }) .catch(err => { - flash(`Can't load mermaid module: ${err}`); + flash(sprintf(__('Encountered an error while rendering: %{err}'), { err })); + // eslint-disable-next-line no-console + console.error(err); }); } +const hookLazyRenderMermaidEvent = once(() => { + $(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() { + const parent = $(this).closest('.js-lazy-render-mermaid-container'); + const pre = parent.prev(); + + const el = pre.find('.js-render-mermaid'); + + parent.remove(); + + renderMermaidEl(el); + }); +}); + export default function renderMermaid($els) { if (!$els.length) return; @@ -112,4 +173,6 @@ export default function renderMermaid($els) { renderMermaids($(this).find('.js-render-mermaid')); } }); + + hookLazyRenderMermaidEvent(); } diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index bfb760f3579..c93a95e490a 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -308,6 +308,7 @@ export default { 'is-added': file.tempFile, }" class="multi-file-editor-holder" + data-qa-selector="editor_container" @focusout="triggerFilesChange" ></div> <content-viewer diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 5cc22f62262..f8c1c3634c2 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -39,6 +39,7 @@ const populateUserInfo = user => { location: userData.location, bio: userData.bio, organization: userData.organization, + jobTitle: userData.job_title, loaded: true, }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index b85be8b9652..c38272ab239 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -121,6 +121,7 @@ export default { data-placement="bottom" tabindex="0" role="button" + data-qa-selector="open_in_web_ide_button" > {{ s__('mrWidget|Open in Web IDE') }} </a> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index ca25d9ee738..602d4ab89e1 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -1,8 +1,10 @@ <script> -import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import { GlPopover, GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import { glEmojiTag } from '../../../emoji'; +import { s__ } from '~/locale'; +import { isString } from 'lodash'; export default { name: 'UserPopover', @@ -10,6 +12,7 @@ export default { Icon, GlPopover, GlSkeletonLoading, + GlSprintf, UserAvatarImage, }, props: { @@ -45,8 +48,27 @@ export default { nameIsLoading() { return !this.user.name; }, - jobInfoIsLoading() { - return !this.user.loaded && this.user.organization === null; + workInformationIsLoading() { + return !this.user.loaded && this.workInformation === null; + }, + workInformation() { + const { jobTitle, organization } = this.user; + + if (organization && jobTitle) { + return { + message: s__('Profile|%{job_title} at %{organization}'), + placeholders: { job_title: jobTitle, organization }, + }; + } else if (organization) { + return organization; + } else if (jobTitle) { + return jobTitle; + } + + return null; + }, + workInformationShouldUseSprintf() { + return !isString(this.workInformation); }, locationIsLoading() { return !this.user.loaded && this.user.location === null; @@ -72,16 +94,30 @@ export default { <gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" /> </div> <div class="text-secondary"> - <div v-if="user.bio" class="js-bio d-flex mb-1"> + <div v-if="user.bio" class="d-flex mb-1"> <icon name="profile" class="category-icon flex-shrink-0" /> - <span class="ml-1">{{ user.bio }}</span> + <span ref="bio" class="ml-1">{{ user.bio }}</span> </div> - <div v-if="user.organization" class="js-organization d-flex mb-1"> - <icon v-show="!jobInfoIsLoading" name="work" class="category-icon flex-shrink-0" /> - <span class="ml-1">{{ user.organization }}</span> + <div v-if="workInformation" class="d-flex mb-1"> + <icon + v-show="!workInformationIsLoading" + name="work" + class="category-icon flex-shrink-0" + /> + <span ref="workInformation" class="ml-1"> + <gl-sprintf v-if="workInformationShouldUseSprintf" :message="workInformation.message"> + <template + v-for="(placeholder, slotName) in workInformation.placeholders" + v-slot:[slotName] + > + <span :key="slotName">{{ placeholder }}</span> + </template> + </gl-sprintf> + <span v-else>{{ workInformation }}</span> + </span> </div> <gl-skeleton-loading - v-if="jobInfoIsLoading" + v-if="workInformationIsLoading" :lines="1" class="animation-container-small mb-1" /> diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 0e4080ce201..f922d8bcaab 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -161,13 +161,17 @@ } .cover-controls { - position: absolute; - top: 10px; - right: 10px; + @include media-breakpoint-up(sm) { + position: absolute; + top: 1rem; + right: 1.25rem; + } &.left { - left: 10px; - right: auto; + @include media-breakpoint-up(sm) { + left: 1.25rem; + right: auto; + } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index fd448ee24ed..621a4eddc34 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -401,3 +401,21 @@ line-height: 16px; text-align: center; } + +@mixin middle-dot-divider { + &::after { + // Duplicate `content` property used as a fallback + // scss-lint:disable DuplicateProperty + content: '\00B7'; // middle dot fallback if browser does not support alternative content + content: '\00B7' / ''; // tell screen readers to ignore the content https://www.w3.org/TR/css-content-3/#accessibility + padding: 0 0.375rem; + font-weight: $gl-font-weight-bold; + } + + &:last-child { + &::after { + content: ''; + padding: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 43cf0d4bd70..82b3698287c 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -74,17 +74,12 @@ // Middle dot divider between each element in a list of items. .middle-dot-divider { - &::after { - content: '\00B7'; // Middle Dot - padding: 0 6px; - font-weight: $gl-font-weight-bold; - } + @include middle-dot-divider; +} - &:last-child { - &::after { - content: ''; - padding: 0; - } +.middle-dot-divider-sm { + @include media-breakpoint-up(sm) { + @include middle-dot-divider; } } @@ -202,10 +197,6 @@ } .user-profile { - .cover-controls a { - margin-left: 5px; - } - .profile-header { margin: 0 $gl-padding; |