summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js153
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue54
-rw-r--r--app/assets/stylesheets/framework/blocks.scss14
-rw-r--r--app/assets/stylesheets/framework/mixins.scss18
-rw-r--r--app/assets/stylesheets/pages/profile.scss19
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">&times;</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;