diff options
Diffstat (limited to 'app/assets/javascripts')
53 files changed, 698 insertions, 223 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index aeb88715c11..3826ecd1ac1 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -8,6 +8,7 @@ import { updateTooltipTitle } from './lib/utils/common_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; import flash from './flash'; import axios from './lib/utils/axios_utils'; +import bp from './breakpoints'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -264,7 +265,10 @@ export class AwardsHandler { const css = { top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, }; - if (position === 'right') { + // for xs screen we position the element on center + if (bp.getBreakpointSize() === 'xs') { + css.left = '5%'; + } else if (position === 'right') { css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`; $menu.addClass('is-aligned-right'); } else { diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 58cf057b2c2..318b7f77c7b 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -10,10 +10,10 @@ export class CopyAsGFM { const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); if (isIOS) return; - $(document).on('copy', '.md, .wiki', e => { + $(document).on('copy', '.md', e => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); - $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', e => { + $(document).on('copy', 'pre.code.highlight, table.code td.line_content', e => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); @@ -99,7 +99,7 @@ export class CopyAsGFM { } static transformGFMSelection(documentFragment) { - const gfmElements = documentFragment.querySelectorAll('.md, .wiki'); + const gfmElements = documentFragment.querySelectorAll('.md'); switch (gfmElements.length) { case 0: { return documentFragment; diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index fc9286d15e6..bfb073fdcdc 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -4,6 +4,7 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; +import initMRPopovers from '../../mr_popover'; // Render GitLab flavoured Markdown // @@ -15,6 +16,7 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); + initMRPopovers(this.find('.gfm-merge_request').get()); return this; }; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 680f2031409..670f66b005e 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -37,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts { } // Sanity check: Make sure the selected text comes from a discussion : it can either contain a message... - let foundMessage = !!documentFragment.querySelector('.md, .wiki'); + let foundMessage = !!documentFragment.querySelector('.md'); // ... Or come from a message if (!foundMessage) { @@ -46,7 +46,7 @@ export default class ShortcutsIssuable extends Shortcuts { let node = e; do { // Text nodes don't define the `matches` method - if (node.matches && node.matches('.md, .wiki')) { + if (node.matches && node.matches('.md')) { foundMessage = true; } node = node.parentNode; diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 388f674f643..c95d7608e37 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -12,6 +12,8 @@ import { REQUEST_FAILURE, UPGRADE_REQUESTED, UPGRADE_REQUEST_FAILURE, + INGRESS, + INGRESS_DOMAIN_SUFFIX, } from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; @@ -76,6 +78,10 @@ export default class Clusters { this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); + this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text'); + this.ingressDomainSnippet = this.ingressDomainHelpText.querySelector( + '.js-ingress-domain-snippet', + ); Clusters.initDismissableCallout(); initSettingsPanels(); @@ -182,6 +188,10 @@ export default class Clusters { this.checkForNewInstalls(prevApplicationMap, this.store.state.applications); this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); + this.toggleIngressDomainHelpText( + prevApplicationMap[INGRESS], + this.store.state.applications[INGRESS], + ); } showToken() { @@ -277,6 +287,16 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestStatus', null); } + toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { + const helpTextHidden = ingressNewState.status !== APPLICATION_STATUS.INSTALLED; + const domainSnippetText = `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`; + + if (ingressPreviousState.status !== ingressNewState.status) { + this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden); + this.ingressDomainSnippet.textContent = domainSnippetText; + } + } + saveKnativeDomain(data) { const appId = data.id; this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 5b206b82fe0..d54f9ce552c 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -393,7 +393,6 @@ export default { <div slot="description" v-html="prometheusDescription"></div> </application-row> <application-row - v-if="isProjectCluster" id="runner" :logo-url="gitlabLogo" :title="applications.runner.title" @@ -409,9 +408,9 @@ export default { > <div slot="description"> {{ - s__(`ClusterIntegration|GitLab Runner connects to this - project's repository and executes CI/CD jobs, - pushing results back and deploying, + s__(`ClusterIntegration|GitLab Runner connects to the + repository and executes CI/CD jobs, + pushing results back and deploying applications to production.`) }} </div> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 39022879d91..67f481f2afb 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -28,3 +28,4 @@ export const JUPYTER = 'jupyter'; export const KNATIVE = 'knative'; export const RUNNER = 'runner'; export const CERT_MANAGER = 'cert_manager'; +export const INGRESS_DOMAIN_SUFFIX = '.nip.io'; diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index fba30aea9ae..e5e1cbb1e62 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -16,3 +16,63 @@ $.fn.extend({ .removeClass('disabled'); }, }); + +/* + Starting with bootstrap 4.3.1, bootstrap sanitizes html used for tooltips / popovers. + This extends the default whitelists with more elements / attributes: + https://getbootstrap.com/docs/4.3/getting-started/javascript/#sanitizer + */ +const whitelist = $.fn.tooltip.Constructor.Default.whiteList; + +const inputAttributes = ['value', 'type']; + +const dataAttributes = [ + 'data-toggle', + 'data-placement', + 'data-container', + 'data-title', + 'data-class', + 'data-clipboard-text', + 'data-placement', +]; + +// Whitelisting data attributes +whitelist['*'] = [ + ...whitelist['*'], + ...dataAttributes, + 'title', + 'width height', + 'abbr', + 'datetime', + 'name', + 'width', + 'height', +]; + +// Whitelist missing elements: +whitelist.label = ['for']; +whitelist.button = [...inputAttributes]; +whitelist.input = [...inputAttributes]; + +whitelist.tt = []; +whitelist.samp = []; +whitelist.kbd = []; +whitelist.var = []; +whitelist.dfn = []; +whitelist.cite = []; +whitelist.big = []; +whitelist.address = []; +whitelist.dl = []; +whitelist.dt = []; +whitelist.dd = []; +whitelist.abbr = []; +whitelist.acronym = []; +whitelist.blockquote = []; +whitelist.del = []; +whitelist.ins = []; +whitelist['gl-emoji'] = []; + +// Whitelisting SVG tags and attributes +whitelist.svg = ['viewBox']; +whitelist.use = ['xlink:href']; +whitelist.path = ['d']; diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index a5125c3d077..d41d1464166 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -128,9 +128,6 @@ export default { isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; }, - showExpandDiffToFullFileEnabled() { - return gon.features.expandDiffFullFile && !this.diffFile.is_fully_expanded; - }, }, mounted() { polyfillSticky(this.$refs.header); @@ -258,7 +255,7 @@ export default { <icon name="external-link" /> </gl-button> <gl-button - v-if="showExpandDiffToFullFileEnabled" + v-if="!diffFile.is_fully_expanded" class="expand-file js-expand-file" @click="toggleFullDiff(diffFile.file_path)" > diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index caf0df8a4e3..c60246bf8ef 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -140,7 +140,7 @@ export default { :id="line.left.line_code" :class="parallelViewLeftLineType" class="line_content parallel left-side" - @mousedown.native="handleParallelLineMouseDown" + @mousedown="handleParallelLineMouseDown" v-html="line.left.rich_text" ></td> </template> @@ -171,7 +171,7 @@ export default { }, ]" class="line_content parallel right-side" - @mousedown.native="handleParallelLineMouseDown" + @mousedown="handleParallelLineMouseDown" v-html="line.right.rich_text" ></td> </template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 93e754fa896..41a80d99850 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -38,36 +38,34 @@ export default { </script> <template> - <div + <table :class="$options.userColorScheme" :data-commit-id="commitId" class="code diff-wrap-lines js-syntax-highlight text-file" > - <table> - <tbody> - <template v-for="(line, index) in diffLines"> - <parallel-diff-table-row - :key="line.line_code" - :file-hash="diffFile.file_hash" - :context-lines-path="diffFile.context_lines_path" - :line="line" - :is-bottom="index + 1 === diffLinesLength" - /> - <parallel-diff-comment-row - :key="`dcr-${line.line_code || index}`" - :line="line" - :diff-file-hash="diffFile.file_hash" - :line-index="index" - :help-page-path="helpPagePath" - /> - <parallel-draft-comment-row - v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" - :key="`drafts-${index}`" - :line="line" - :diff-file-content-sha="diffFile.file_hash" - /> - </template> - </tbody> - </table> - </div> + <tbody> + <template v-for="(line, index) in diffLines"> + <parallel-diff-table-row + :key="line.line_code" + :file-hash="diffFile.file_hash" + :context-lines-path="diffFile.context_lines_path" + :line="line" + :is-bottom="index + 1 === diffLinesLength" + /> + <parallel-diff-comment-row + :key="`dcr-${line.line_code || index}`" + :line="line" + :diff-file-hash="diffFile.file_hash" + :line-index="index" + :help-page-path="helpPagePath" + /> + <parallel-draft-comment-row + v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)" + :key="`drafts-${index}`" + :line="line" + :diff-file-content-sha="diffFile.file_hash" + /> + </template> + </tbody> + </table> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index a092bdfbc6c..c541ea3445b 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -459,12 +459,7 @@ export default { class="gl-responsive-table-row" role="row" > - <div - v-gl-tooltip - :title="model.name" - class="table-section section-wrap section-15 text-truncate" - role="gridcell" - > + <div class="table-section section-wrap section-15 text-truncate" role="gridcell"> <div v-if="!model.isFolder" class="table-mobile-header" role="rowheader"> {{ s__('Environments|Environment') }} </div> @@ -473,14 +468,28 @@ export default { <icon :name="deployIconName" /> </span> - <span v-if="!model.isFolder" class="environment-name table-mobile-content"> - <a class="qa-environment-link" :href="environmentPath"> {{ model.name }} </a> + <span + v-if="!model.isFolder" + v-gl-tooltip + :title="model.name" + class="environment-name table-mobile-content" + > + <a class="qa-environment-link" :href="environmentPath"> + <span v-if="model.size === 1">{{ model.name }}</span> + <span v-else>{{ model.name_without_type }}</span> + </a> <span v-if="isProtected" class="badge badge-success"> {{ s__('Environments|protected') }} </span> </span> - - <span v-else class="folder-name" role="button" @click="onClickFolder"> + <span + v-else + v-gl-tooltip + :title="model.folderName" + class="folder-name" + role="button" + @click="onClickFolder" + > <icon :name="folderIconName" class="folder-icon" /> <icon name="folder" class="folder-icon" /> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index ff4e16178e8..55613d815ce 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -99,7 +99,7 @@ export default { /> <div - v-if="shouldRenderDeployBoard" + v-if="shouldRenderDeployBoard(model)" :key="`deploy-board-row-${i}`" class="js-deploy-board-row" > diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index d42e4f145dc..1e754a4f54f 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -20,7 +20,7 @@ export function startPolling({ commit, dispatch }, endpoint) { commit(types.SET_LOADING, false); dispatch('stopPolling'); }, - errorCallback: response => { + errorCallback: ({ response }) => { let errorMessage = ''; if (response && response.data && response.data.message) { errorMessage = response.data.message; diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 29dc2d6a8a3..aa50fd8ff62 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -244,7 +244,7 @@ export default { <gl-loading-icon v-if="isLoading" :label="s__('GroupsTree|Loading groups')" - :size="2" + size="md" class="loading-animation prepend-top-20" /> <groups-component diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 2b44438f849..9161eb3d9b1 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -38,8 +38,8 @@ export default { }, }, computed: { - ...mapState('commit', ['commitAction']), - ...mapGetters('commit', ['newBranchName']), + ...mapState('commit', ['commitAction', 'newBranchName']), + ...mapGetters('commit', ['placeholderBranchName']), tooltipTitle() { return this.disabled ? this.title : ''; }, @@ -73,7 +73,8 @@ export default { </label> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> <input - :placeholder="newBranchName" + :placeholder="placeholderBranchName" + :value="newBranchName" type="text" class="form-control monospace" @input="updateBranchName($event.target.value)" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index ba6bbdfef4b..412b07553dc 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -29,7 +29,7 @@ export default { return this.name || (entryPath ? `${entryPath}/` : ''); }, set(val) { - this.name = val; + this.name = val.trim(); }, }, modalTitle() { diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 03777e6c10b..bbe40b2ec2f 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -14,7 +14,7 @@ const createTranslatedTextForFiles = (files, text) => { export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; -export const newBranchName = (state, _, rootState) => +export const placeholderBranchName = (state, _, rootState) => `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( -BRANCH_SUFFIX_COUNT, )}`; @@ -25,7 +25,7 @@ export const branchName = (state, getters, rootState) => { state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR ) { if (state.newBranchName === '') { - return getters.newBranchName; + return getters.placeholderBranchName; } return state.newBranchName; diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 58f14bac8c8..732184dc782 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -140,7 +140,7 @@ export default { 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation, }" - class="wiki" + class="md" v-html="descriptionHtml" ></div> <textarea diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 7076a79dd5d..b651a6e4bfb 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -39,7 +39,7 @@ export default { </gl-link> <clipboard-button - :text="commit.short_id" + :text="commit.id" :title="__('Copy commit SHA to clipboard')" css-class="btn btn-clipboard btn-transparent" /> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index c5076d65ff9..6e92b599b0a 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,5 +1,6 @@ <script> import _ from 'underscore'; +import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -7,6 +8,7 @@ export default { components: { CiIcon, Icon, + GlLink, }, props: { pipeline: { @@ -26,6 +28,12 @@ export default { hasRef() { return !_.isEmpty(this.pipeline.ref); }, + isTriggeredByMergeRequest() { + return Boolean(this.pipeline.merge_request); + }, + isMergeRequestPipeline() { + return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); + }, }, methods: { onStageClick(stage) { @@ -36,16 +44,41 @@ export default { </script> <template> <div class="block-last dropdown"> - <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> + <div class="js-pipeline-info"> + <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> - <span class="font-weight-bold">{{ __('Pipeline') }}</span> - <a :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" - >#{{ pipeline.id }}</a - > - <template v-if="hasRef"> - {{ __('from') }} - <a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a> - </template> + <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> + <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" + >#{{ pipeline.id }}</gl-link + > + <template v-if="hasRef"> + {{ s__('Job|for') }} + + <template v-if="isTriggeredByMergeRequest"> + <gl-link :href="pipeline.merge_request.path" class="link-commit ref-name js-mr-link" + >!{{ pipeline.merge_request.iid }}</gl-link + > + {{ s__('Job|with') }} + <gl-link + :href="pipeline.merge_request.source_branch_path" + class="link-commit ref-name js-source-branch-link" + >{{ pipeline.merge_request.source_branch }}</gl-link + > + + <template v-if="isMergeRequestPipeline"> + {{ s__('Job|into') }} + <gl-link + :href="pipeline.merge_request.target_branch_path" + class="link-commit ref-name js-target-branch-link" + >{{ pipeline.merge_request.target_branch }}</gl-link + > + </template> + </template> + <gl-link v-else :href="pipeline.ref.path" class="link-commit ref-name">{{ + pipeline.ref.name + }}</gl-link> + </template> + </div> <button type="button" diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js new file mode 100644 index 00000000000..023c336db02 --- /dev/null +++ b/app/assets/javascripts/lib/utils/autosave.js @@ -0,0 +1,32 @@ +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +export const clearDraft = autosaveKey => { + try { + window.localStorage.removeItem(`autosave/${autosaveKey}`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +export const getDraft = autosaveKey => { + try { + return window.localStorage.getItem(`autosave/${autosaveKey}`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return null; + } +}; + +export const updateDraft = (autosaveKey, text) => { + try { + window.localStorage.setItem(`autosave/${autosaveKey}`, text); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +export const getDiscussionReplyKey = (noteableType, discussionId) => + ['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/'); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index c2de0379d23..3cb406b819d 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -16,7 +16,7 @@ import utilsMixin from '../mixins/line_conflict_utils'; }, }, template: ` - <table> + <table class="diff-wrap-lines code js-syntax-highlight"> <tr class="line_holder parallel" v-for="section in file.parallelLines"> <template v-for="line in section"> <td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td> diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 41783d311ef..d453dc1fdb7 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -69,17 +69,17 @@ export default { appearance && appearance.line && appearance.line.type ? appearance.line.type : lineTypes.default; - const lineColor = lineType === lineTypes.threshold ? this.primaryColor : undefined; + const lineWidth = + appearance && appearance.line && appearance.line.width + ? appearance.line.width + : undefined; return { name: this.formatLegendLabel(query), data: this.concatenateResults(query.result), lineStyle: { type: lineType, - color: lineColor, - }, - itemStyle: { - color: lineColor, + width: lineWidth, }, areaStyle: { opacity: diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 895a57785bc..ba6a17827f7 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,6 +1,8 @@ <script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; +import '~/vue_shared/mixins/is_ee'; import Flash from '../../flash'; import MonitoringService from '../services/monitoring_service'; import MonitorAreaChart from './charts/area.vue'; @@ -17,7 +19,10 @@ export default { GraphGroup, EmptyState, Icon, + GlDropdown, + GlDropdownItem, }, + props: { hasMetrics: { type: Boolean, @@ -104,9 +109,29 @@ export default { } }, mounted() { + this.servicePromises = [ + this.service + .getGraphsData() + .then(data => this.store.storeMetrics(data)) + .catch(() => Flash(s__('Metrics|There was an error while retrieving metrics'))), + this.service + .getDeploymentData() + .then(data => this.store.storeDeploymentData(data)) + .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), + ]; if (!this.hasMetrics) { this.state = 'gettingStarted'; } else { + if (this.environmentsEndpoint) { + this.servicePromises.push( + this.service + .getEnvironmentsData() + .then(data => this.store.storeEnvironmentsData(data)) + .catch(() => + Flash(s__('Metrics|There was an error getting environments information.')), + ), + ); + } this.getGraphsData(); sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver.observe(document.querySelector('.layout-page'), { @@ -122,17 +147,7 @@ export default { }, getGraphsData() { this.state = 'loading'; - Promise.all([ - this.service.getGraphsData().then(data => this.store.storeMetrics(data)), - this.service - .getDeploymentData() - .then(data => this.store.storeDeploymentData(data)) - .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))), - this.service - .getEnvironmentsData() - .then(data => this.store.storeEnvironmentsData(data)) - .catch(() => Flash(s__('Metrics|There was an error getting environments information.'))), - ]) + Promise.all(this.servicePromises) .then(() => { if (this.store.groups.length < 1) { this.state = 'noData'; @@ -156,29 +171,22 @@ export default { <template> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> - <div class="environments d-flex align-items-center"> - {{ s__('Metrics|Environment') }} - <div class="dropdown prepend-left-10"> - <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span>{{ currentEnvironmentName }}</span> - <icon name="chevron-down" /> - </button> - <div - v-if="store.environmentsData.length > 0" - class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" + <div v-if="environmentsEndpoint" class="environments d-flex align-items-center"> + <strong>{{ s__('Metrics|Environment') }}</strong> + <gl-dropdown + class="prepend-left-10 js-environments-dropdown" + toggle-class="dropdown-menu-toggle" + :text="currentEnvironmentName" + :disabled="store.environmentsData.length === 0" + > + <gl-dropdown-item + v-for="environment in store.environmentsData" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + >{{ environment.name }}</gl-dropdown-item > - <ul> - <li v-for="environment in store.environmentsData" :key="environment.id"> - <a - :href="environment.metrics_path" - :class="{ 'is-active': environment.name == currentEnvironmentName }" - class="dropdown-item" - >{{ environment.name }}</a - > - </li> - </ul> - </div> - </div> + </gl-dropdown> </div> <graph-group v-for="(groupData, index) in store.groups" @@ -194,7 +202,17 @@ export default { :alert-data="getGraphAlerts(graphData.id)" :container-width="elWidth" group-id="monitor-area-chart" - /> + > + <alert-widget + v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData.id" + :alerts-endpoint="alertsEndpoint" + :label="getGraphLabel(graphData)" + :current-alerts="getQueryAlerts(graphData)" + :custom-metric-id="graphData.id" + :alert-data="alertData[graphData.id]" + @setAlerts="setAlerts" + /> + </monitor-area-chart> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 869173b6572..55ecf3b5334 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -6,5 +6,4 @@ export const graphTypes = { export const lineTypes = { default: 'solid', - threshold: 'dashed', }; diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue new file mode 100644 index 00000000000..8e2d8fa816a --- /dev/null +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -0,0 +1,110 @@ +<script> +import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import Icon from '../../vue_shared/components/icon.vue'; +import CiIcon from '../../vue_shared/components/ci_icon.vue'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import query from '../queries/merge_request.graphql'; +import { mrStates, humanMRStates } from '../constants'; + +export default { + name: 'MRPopover', + components: { + GlPopover, + GlSkeletonLoading, + Icon, + CiIcon, + }, + mixins: [timeagoMixin], + props: { + target: { + type: HTMLAnchorElement, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + mergeRequestIID: { + type: String, + required: true, + }, + mergeRequestTitle: { + type: String, + required: true, + }, + }, + data() { + return { + mergeRequest: {}, + }; + }, + computed: { + detailedStatus() { + return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus; + }, + formattedTime() { + return this.timeFormated(this.mergeRequest.createdAt); + }, + statusBoxClass() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return 'status-box-mr-merged'; + case mrStates.closed: + return 'status-box-closed'; + default: + return 'status-box-open'; + } + }, + stateHumanName() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return humanMRStates.merged; + case mrStates.closed: + return humanMRStates.closed; + default: + return humanMRStates.open; + } + }, + showDetails() { + return Object.keys(this.mergeRequest).length > 0; + }, + }, + apollo: { + mergeRequest: { + query, + update: data => data.project.mergeRequest, + variables() { + const { projectPath, mergeRequestIID } = this; + + return { + projectPath, + mergeRequestIID, + }; + }, + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" boundary="viewport" placement="top" show> + <div class="mr-popover"> + <div v-if="$apollo.loading"> + <gl-skeleton-loading :lines="1" class="animation-container-small mt-1" /> + </div> + <div v-else-if="showDetails" class="d-flex align-items-center justify-content-between"> + <div class="d-inline-flex align-items-center"> + <div :class="`issuable-status-box status-box ${statusBoxClass}`"> + {{ stateHumanName }} + </div> + <span class="text-secondary">Opened <time v-text="formattedTime"></time></span> + </div> + <ci-icon v-if="detailedStatus" :status="detailedStatus" /> + </div> + <h5 class="my-2">{{ mergeRequestTitle }}</h5> + <div class="text-secondary"> + {{ `${projectPath}!${mergeRequestIID}` }} + </div> + </div> + </gl-popover> +</template> diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js new file mode 100644 index 00000000000..433df844c80 --- /dev/null +++ b/app/assets/javascripts/mr_popover/constants.js @@ -0,0 +1,10 @@ +export const mrStates = { + merged: 'merged', + closed: 'closed', +}; + +export const humanMRStates = { + merged: 'Merged', + closed: 'Closed', + open: 'Open', +}; diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js new file mode 100644 index 00000000000..cc686b401d2 --- /dev/null +++ b/app/assets/javascripts/mr_popover/index.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import MRPopover from './components/mr_popover.vue'; +import createDefaultClient from '~/lib/graphql'; + +let renderedPopover; +let renderFn; + +const handleUserPopoverMouseOut = ({ target }) => { + target.removeEventListener('mouseleave', handleUserPopoverMouseOut); + + if (renderFn) { + clearTimeout(renderFn); + } + if (renderedPopover) { + renderedPopover.$destroy(); + renderedPopover = null; + } +}; + +/** + * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes. + * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover + */ +const handleMRPopoverMount = apolloProvider => ({ target }) => { + // Add listener to actually remove it again + target.addEventListener('mouseleave', handleUserPopoverMouseOut); + + const { projectPath, mrTitle, iid } = target.dataset; + const mergeRequest = {}; + + renderFn = setTimeout(() => { + const MRPopoverComponent = Vue.extend(MRPopover); + renderedPopover = new MRPopoverComponent({ + propsData: { + target, + projectPath, + mergeRequestIID: iid, + mergeRequest, + mergeRequestTitle: mrTitle, + }, + apolloProvider, + }); + + renderedPopover.$mount(); + }, 200); // 200ms delay so not every mouseover triggers Popover + API Call +}; + +export default elements => { + const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')]; + if (mrLinks.length > 0) { + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + mrLinks.forEach(el => { + el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider)); + }); + } +}; diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.graphql new file mode 100644 index 00000000000..0bb9bc03bc7 --- /dev/null +++ b/app/assets/javascripts/mr_popover/queries/merge_request.graphql @@ -0,0 +1,14 @@ +query mergeRequest($projectPath: ID!, $mergeRequestIID: ID!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $mergeRequestIID) { + createdAt + state + headPipeline { + detailedStatus { + icon + group + } + } + } + } +} diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index c5ae7e7ee10..b59ddd0d57a 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -20,12 +20,20 @@ export default { required: true, }, }, - data() { - return { - outputType: '', - }; - }, methods: { + outputType(output) { + if (output.text) { + return 'text/plain'; + } else if (output.data['image/png']) { + return 'image/png'; + } else if (output.data['text/html']) { + return 'text/html'; + } else if (output.data['image/svg+xml']) { + return 'image/svg+xml'; + } + + return 'text/plain'; + }, dataForType(output, type) { let data = output.data[type]; @@ -39,20 +47,13 @@ export default { if (output.text) { return CodeOutput; } else if (output.data['image/png']) { - this.outputType = 'image/png'; - return ImageOutput; } else if (output.data['text/html']) { - this.outputType = 'text/html'; - return HtmlOutput; } else if (output.data['image/svg+xml']) { - this.outputType = 'image/svg+xml'; - return HtmlOutput; } - this.outputType = 'text/plain'; return CodeOutput; }, rawCode(output) { @@ -60,7 +61,7 @@ export default { return output.text.join(''); } - return this.dataForType(output, this.outputType); + return this.dataForType(output, this.outputType(output)); }, }, }; @@ -73,7 +74,7 @@ export default { v-for="(output, index) in outputs" :key="index" type="output" - :output-type="outputType" + :output-type="outputType(output)" :count="count" :index="index" :raw-code="rawCode(output)" diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index d8947e8ca50..ab758a9e922 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -72,8 +72,8 @@ export default { :can-current-user-fork="false" :expanded="!discussion.diff_file.viewer.collapsed" /> - <div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code"> - <table> + <div v-if="isTextFile" class="diff-content"> + <table class="code js-syntax-highlight" :class="$options.userColorSchemeClass"> <template v-if="hasTruncatedDiffLines"> <tr v-for="line in discussion.truncated_diff_lines" @@ -81,8 +81,8 @@ export default { :key="line.line_code" class="line_holder" > - <td class="diff-line-num old_line">{{ line.old_line }}</td> - <td class="diff-line-num new_line">{{ line.new_line }}</td> + <td :class="line.type" class="diff-line-num old_line">{{ line.old_line }}</td> + <td :class="line.type" class="diff-line-num new_line">{{ line.new_line }}</td> <td :class="line.type" class="line_content" v-html="line.rich_text"></td> </tr> </template> diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index f50cab81efe..be8e42af9ea 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -18,7 +18,7 @@ export default { <div class="note-actions-item"> <gl-button ref="button" - v-gl-tooltip.bottom + v-gl-tooltip class="note-action-button" variant="transparent" :title="__('Reply to comment')" diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index fb1d98355b3..ff303d0f55a 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -95,7 +95,6 @@ export default { <div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body"> <suggestions v-if="hasSuggestion && !isEditing" - class="note-text md" :suggestions="note.suggestions" :note-html="note.note_html" :line-type="lineType" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 92258a25438..57d6b181bd7 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -7,6 +7,7 @@ import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __ } from '~/locale'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; export default { name: 'NoteForm', @@ -65,10 +66,21 @@ export default { required: false, default: '', }, + autosaveKey: { + type: String, + required: false, + default: '', + }, }, data() { + let updatedNoteBody = this.noteBody; + + if (!updatedNoteBody && this.autosaveKey) { + updatedNoteBody = getDraft(this.autosaveKey) || ''; + } + return { - updatedNoteBody: this.noteBody, + updatedNoteBody, conflictWhileEditing: false, isSubmitting: false, isResolving: this.resolveDiscussion, @@ -175,6 +187,12 @@ export default { // Sends information about confirm message and if the textarea has changed this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, + onInput() { + if (this.autosaveKey) { + const { autosaveKey, updatedNoteBody: text } = this; + updateDraft(autosaveKey, text); + } + }, }, }; </script> @@ -218,6 +236,7 @@ export default { @keydown.ctrl.enter="handleKeySubmit()" @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)" + @input="onInput" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index fc51998935d..a3d664a738f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, __, sprintf } from '~/locale'; +import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; import icon from '~/vue_shared/components/icon.vue'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; @@ -21,7 +22,6 @@ import noteForm from './note_form.vue'; import diffWithNote from './diff_with_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; -import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; @@ -54,7 +54,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [autosave, noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], + mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], props: { discussion: { type: Object, @@ -87,13 +87,10 @@ export default { }, }, data() { - const { diff_discussion: isDiffDiscussion, resolved } = this.discussion; - return { isReplying: false, isResolving: false, resolveAsThread: true, - isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved), }; }, computed: { @@ -106,7 +103,10 @@ export default { 'showJumpToNextDiscussion', ]), author() { - return this.initialDiscussion.author; + return this.firstNote.author; + }, + autosaveKey() { + return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id); }, canReply() { return this.getNoteableData.current_user.can_create_note; @@ -117,7 +117,7 @@ export default { hasReplies() { return this.discussion.notes.length > 1; }, - initialDiscussion() { + firstNote() { return this.discussion.notes.slice(0, 1)[0]; }, replies() { @@ -175,11 +175,11 @@ export default { return ''; }, - shouldShowDiscussions() { - const { expanded, resolved } = this.discussion; - const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved; - - return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; + isExpanded() { + return this.discussion.expanded || this.alwaysExpanded; + }, + shouldHideDiscussionBody() { + return this.shouldRenderDiffs && !this.isExpanded; }, actionText() { const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; @@ -242,18 +242,6 @@ export default { return !this.discussionResolved && this.discussion.resolve_with_issue_path; }, }, - watch: { - isReplying() { - if (this.isReplying) { - this.$nextTick(() => { - // Pass an extra key to separate reply and note edit forms - this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']); - }); - } else { - this.disposeAutoSave(); - } - }, - }, created() { eventHub.$on('startReplying', this.onStartReplying); }, @@ -291,9 +279,6 @@ export default { toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); }, - toggleReplies() { - this.isRepliesCollapsed = !this.isRepliesCollapsed; - }, showReplyForm() { this.isReplying = true; }, @@ -312,7 +297,7 @@ export default { } this.isReplying = false; - this.resetAutoSave(); + clearDraft(this.autosaveKey); }, saveReply(noteText, form, callback) { const postData = { @@ -338,7 +323,7 @@ export default { this.isReplying = false; this.saveNote(replyData) .then(() => { - this.resetAutoSave(); + clearDraft(this.autosaveKey); callback(); }) .catch(err => { @@ -390,8 +375,8 @@ Please check your network connection and try again.`; <div class="timeline-content"> <note-header :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" + :created-at="firstNote.created_at" + :note-id="firstNote.id" :include-toggle="true" :expanded="discussion.expanded" @toggleHandler="toggleDiscussionHandler" @@ -414,7 +399,7 @@ Please check your network connection and try again.`; /> </div> </div> - <div v-if="shouldShowDiscussions" class="discussion-body"> + <div v-if="!shouldHideDiscussionBody" class="discussion-body"> <component :is="wrapperComponent" v-bind="wrapperComponentProps" @@ -424,8 +409,8 @@ Please check your network connection and try again.`; <ul class="notes"> <template v-if="shouldGroupReplies"> <component - :is="componentName(initialDiscussion)" - :note="componentData(initialDiscussion)" + :is="componentName(firstNote)" + :note="componentData(firstNote)" :line="line" :commit="commit" :help-page-path="helpPagePath" @@ -445,11 +430,11 @@ Please check your network connection and try again.`; </component> <toggle-replies-widget v-if="hasReplies" - :collapsed="isRepliesCollapsed" + :collapsed="!isExpanded" :replies="replies" - @toggle="toggleReplies" + @toggle="toggleDiscussionHandler" /> - <template v-if="!isRepliesCollapsed"> + <template v-if="isExpanded"> <component :is="componentName(note)" v-for="note in replies" @@ -476,7 +461,7 @@ Please check your network connection and try again.`; </template> </ul> <div - v-if="!isRepliesCollapsed || !hasReplies" + v-if="isExpanded || !hasReplies" :class="{ 'is-replying': isReplying }" class="discussion-reply-holder" > @@ -512,6 +497,7 @@ Please check your network connection and try again.`; :is-editing="false" :line="diffLine" save-button-title="Comment" + :autosave-key="autosaveKey" @handleFormUpdateAddToReview="addReplyToReview" @handleFormUpdate="saveReply" @cancelForm="cancelReplyForm" diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index e7fa05faa8a..6aed2492084 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -4,6 +4,7 @@ import Flash from './flash'; export default function notificationsDropdown() { $(document).on('click', '.update-notification', function updateNotificationCallback(e) { e.preventDefault(); + if ($(this).is('.is-active') && $(this).data('notificationLevel') === 'custom') { return; } diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 918622ef8dc..3e7bf20470c 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -110,12 +110,12 @@ export default { {{ __('stuck') }} </span> <span - v-if="pipeline.flags.merge_request" + v-if="pipeline.flags.detached_merge_request_pipeline" v-gl-tooltip - :title="__('This pipeline is run in a merge request context')" - class="js-pipeline-url-mergerequest badge badge-info" + :title="__('This pipeline is run on the source branch')" + class="js-pipeline-url-detached badge badge-info" > - {{ __('merge request') }} + {{ __('detached') }} </span> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index f6454a84ea5..1c44427e720 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -272,10 +272,11 @@ export default { :tag="commitTag" :commit-ref="commitRef" :commit-url="commitUrl" + :merge-request-ref="pipeline.merge_request" :short-sha="commitShortSha" :title="commitTitle" :author="commitAuthor" - :show-branch="!isChildView" + :show-ref-info="!isChildView" /> </div> </div> diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js index 9177943f88a..dd79ade5bc9 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js @@ -1,6 +1,16 @@ +import Flash from '~/flash'; +import { __ } from '~/locale'; + export default { methods: { clickTriggeredByPipeline() {}, clickTriggeredPipeline() {}, + requestRefreshPipelineGraph() { + // When an action is clicked + // (wether in the dropdown or in the main nodes, we refresh the big graph) + this.mediator + .refreshPipeline() + .catch(() => Flash(__('An error occurred while making the request.'))); + }, }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 8adbd39edd4..6660f8120f8 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -29,15 +29,6 @@ export default () => { mediator, }; }, - methods: { - requestRefreshPipelineGraph() { - // When an action is clicked - // (wether in the dropdown or in the main nodes, we refresh the big graph) - this.mediator - .refreshPipeline() - .catch(() => Flash(__('An error occurred while making the request.'))); - }, - }, render(createElement) { return createElement('pipeline-graph', { props: { diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 052e34a8aef..259278b6410 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -1,7 +1,6 @@ export default class PipelineStore { constructor() { this.state = {}; - this.state.pipeline = {}; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 9e63aa00341..f5a1ff2f6fd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable vue/require-default-prop */ +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -14,9 +15,13 @@ export default { CiIcon, Icon, TooltipOnTruncate, + GlLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, + directives: { + GlTooltip: GlTooltipDirective, + }, mixins: [mrWidgetPipelineMixin], props: { pipeline: { @@ -78,12 +83,18 @@ export default { false, ); }, + isTriggeredByMergeRequest() { + return Boolean(this.pipeline.merge_request); + }, + isMergeRequestPipeline() { + return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); + }, }, }; </script> <template> - <div v-if="hasPipeline || hasCIError" class="ci-widget media"> + <div v-if="hasPipeline || hasCIError" class="ci-widget media js-ci-widget"> <template v-if="hasCIError"> <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default" @@ -99,21 +110,58 @@ export default { <div class="ci-widget-container d-flex"> <div class="ci-widget-content"> <div class="media-body"> - <div class="font-weight-bold"> - Pipeline - <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" - >#{{ pipeline.id }}</a + <div class="font-weight-bold js-pipeline-info-container"> + {{ s__('Pipeline|Pipeline') }} + <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" + >#{{ pipeline.id }}</gl-link > {{ pipeline.details.status.label }} <template v-if="hasCommitInfo"> - for - <a + {{ s__('Pipeline|for') }} + <gl-link :href="pipeline.commit.commit_path" class="commit-sha js-commit-link font-weight-normal" - >{{ pipeline.commit.short_id }}</a + >{{ pipeline.commit.short_id }}</gl-link > - on + {{ s__('Pipeline|on') }} + <template v-if="isTriggeredByMergeRequest"> + <gl-link + v-gl-tooltip + :href="pipeline.merge_request.path" + :title="pipeline.merge_request.title" + class="font-weight-normal" + >!{{ pipeline.merge_request.iid }}</gl-link + > + {{ s__('Pipeline|with') }} + <tooltip-on-truncate + :title="pipeline.merge_request.source_branch" + truncate-target="child" + class="label-branch label-truncate" + > + <gl-link + :href="pipeline.merge_request.source_branch_path" + class="font-weight-normal" + >{{ pipeline.merge_request.source_branch }}</gl-link + > + </tooltip-on-truncate> + + <template v-if="isMergeRequestPipeline"> + {{ s__('Pipeline|into') }} + <tooltip-on-truncate + :title="pipeline.merge_request.target_branch" + truncate-target="child" + class="label-branch label-truncate" + > + <gl-link + :href="pipeline.merge_request.target_branch_path" + class="font-weight-normal" + >{{ pipeline.merge_request.target_branch }}</gl-link + > + </tooltip-on-truncate> + </template> + </template> <tooltip-on-truncate + v-else :title="sourceBranch" truncate-target="child" class="label-branch label-truncate" @@ -121,7 +169,9 @@ export default { /> </template> </div> - <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div> + <div v-if="pipeline.coverage" class="coverage"> + {{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}% + </div> </div> </div> <div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 2a4dff71d9b..11bc8c73ee9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -80,7 +80,7 @@ export default { <status-icon :show-disabled-button="true" status="warning" /> <div class="media-body space-children"> <span class="bold"> - <span v-if="mr.mergeError" class="has-error-message"> {{ mergeError }}. </span> + <span v-if="mr.mergeError" class="has-error-message"> {{ mergeError }} </span> <span v-else> {{ s__('mrWidget|Merge failed.') }} </span> <span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span> </span> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index e6f0a1c69cd..25f80219993 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -22,6 +22,7 @@ import Icon from '../../vue_shared/components/icon.vue'; * - Jobs show view header * - Jobs show view sidebar * - Linked pipelines + * - Extended MR Popover */ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index ee685a4b8cd..3f282138bdf 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,5 +1,6 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import _ from 'underscore'; +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import UserAvatarLink from './user_avatar/user_avatar_link.vue'; import Icon from '../../vue_shared/components/icon.vue'; @@ -10,6 +11,7 @@ export default { components: { UserAvatarLink, Icon, + GlLink, }, props: { /** @@ -33,6 +35,27 @@ export default { required: false, default: () => ({}), }, + + /** + * If provided, is used the render the MR IID and link + * in place of the branch name. Must contains the + * following properties: + * - iid (number) + * - path (non-empty string) + * + * May optionally contain the following properties: + * - title (string): used in a tooltip if provided + * + * Any additional properties are ignored. + */ + mergeRequestRef: { + type: Object, + required: false, + default: undefined, + validator: ref => + _.isUndefined(ref) || (_.isFinite(ref.iid) && _.isString(ref.path) && !_.isEmpty(ref.path)), + }, + /** * Used to link to the commit sha. */ @@ -70,7 +93,11 @@ export default { required: false, default: () => ({}), }, - showBranch: { + + /** + * Indicates whether or not to show the branch/MR ref info + */ + showRefInfo: { type: Boolean, required: false, default: true, @@ -78,14 +105,12 @@ export default { }, computed: { /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * @returns {Boolean} + * Determines if we shoud render the ref info section based */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + shouldShowRefInfo() { + return this.showRefInfo && (this.commitRef || this.mergeRequestRef); }, + /** * Used to verify if all the properties needed to render the commit * author section were provided. @@ -109,18 +134,35 @@ export default { </script> <template> <div class="branch-commit"> - <template v-if="hasCommitRef && showBranch"> + <template v-if="shouldShowRefInfo"> <div class="icon-container"> - <i v-if="tag" class="fa fa-tag" aria-hidden="true"> </i> <icon v-if="!tag" name="fork" /> + <icon v-if="tag" name="tag" /> + <icon v-else-if="mergeRequestRef" name="git-merge" /> + <icon v-else name="branch" /> </div> - <a v-gl-tooltip :href="commitRef.ref_url" :title="commitRef.name" class="ref-name"> + <gl-link + v-if="mergeRequestRef" + v-gl-tooltip + :href="mergeRequestRef.path" + :title="mergeRequestRef.title" + class="ref-name" + > + {{ mergeRequestRef.iid }} + </gl-link> + <gl-link + v-else + v-gl-tooltip + :href="commitRef.ref_url" + :title="commitRef.name" + class="ref-name" + > {{ commitRef.name }} - </a> + </gl-link> </template> <icon name="commit" class="commit-icon js-commit-icon" /> - <a :href="commitUrl" class="commit-sha"> {{ shortSha }} </a> + <gl-link :href="commitUrl" class="commit-sha"> {{ shortSha }} </gl-link> <div class="commit-title flex-truncate-parent"> <span v-if="title" class="flex-truncate-child"> @@ -132,7 +174,7 @@ export default { :tooltip-text="author.username" class="avatar-image-container" /> - <a :href="commitUrl" class="commit-row-message"> {{ title }} </a> + <gl-link :href="commitUrl" class="commit-row-message"> {{ title }} </gl-link> </span> <span v-else> Can't find HEAD commit for this branch </span> </div> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index c9915f7d685..5fdc915fffb 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -78,8 +78,8 @@ export default { </script> <template> - <div ref="markdown-preview" class="md md-previewer"> + <div ref="markdown-preview" class="md-previewer"> <gl-skeleton-loading v-if="isLoading" /> - <div v-else v-html="previewContent"></div> + <div v-else class="md" v-html="previewContent"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 7a53d053eec..216f6c62e69 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -53,7 +53,7 @@ export default { <template> <button :class="containerClass" :disabled="loading || disabled" type="button" @click="onClick"> - <transition name="fade"> + <transition name="fade-in"> <gl-loading-icon v-if="loading" :inline="true" @@ -63,7 +63,7 @@ export default { class="js-loading-button-icon" /> </transition> - <transition name="fade"> + <transition name="fade-in"> <slot> <span v-if="label" class="js-loading-button-label"> {{ label }} </span> </slot> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 3f607aa2a0a..a4b3131c8e4 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -215,7 +215,7 @@ export default { <div v-show="previewMarkdown" ref="markdown-preview" - class="md-preview js-vue-md-preview md md-preview-holder" + class="js-vue-md-preview md-preview-holder" > <suggestions v-if="hasSuggestion" @@ -233,7 +233,7 @@ export default { <div v-show="previewMarkdown" ref="markdown-preview" - class="md-preview js-vue-md-preview md md-preview-holder" + class="js-vue-md-preview md md-preview-holder" v-html="markdownPreview" ></div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index dcda701f049..177d78cb904 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -130,6 +130,6 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" v-html="noteHtml"></div> + <div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 8d3a3009c55..a50f49c1279 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -57,7 +57,7 @@ export default { </div> </div> <div class="note-body"> - <div class="note-text"> + <div class="note-text md"> <p>{{ note.body }}</p> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index b0af8399955..acc179b3834 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -93,7 +93,7 @@ export default { 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded, }" - class="note-text" + class="note-text md" v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> diff --git a/app/assets/javascripts/vue_shared/components/svg_gradient.vue b/app/assets/javascripts/vue_shared/components/svg_gradient.vue index cca90af275e..5ce45d492f9 100644 --- a/app/assets/javascripts/vue_shared/components/svg_gradient.vue +++ b/app/assets/javascripts/vue_shared/components/svg_gradient.vue @@ -4,10 +4,16 @@ export default { colors: { type: Array, required: true, + validator(value) { + return value.length === 2; + }, }, opacity: { type: Array, required: true, + validator(value) { + return value.length === 2; + }, }, identifierName: { type: String, |