diff options
Diffstat (limited to 'app')
132 files changed, 1382 insertions, 345 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 743f11625bc..aaab217964c 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this */ +/* eslint-disable class-methods-use-this, @gitlab/i18n/no-non-i18n-strings */ import $ from 'jquery'; import _ from 'underscore'; diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index da82b52330a..d4f4df3ad75 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,4 +1,5 @@ const notImplemented = () => { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ throw new Error('Not implemented!'); }; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 77ba68be07e..09eb8bb9b98 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,6 +1,7 @@ import * as mutationTypes from './mutation_types'; const notImplemented = () => { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ throw new Error('Not implemented!'); }; diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index b62ec8a651b..9263e9b27e4 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -78,7 +78,7 @@ export default class ContextualSidebar { const dbp = ContextualSidebar.isDesktopBreakpoint(); if (this.$sidebar.length) { - this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); + this.$sidebar.toggleClass(`sidebar-collapsed-desktop ${SIDEBAR_COLLAPSED_CLASS}`, collapsed); this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false); this.$page.toggleClass( 'page-with-icon-sidebar', diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue deleted file mode 100644 index 6c256fa6736..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import iconCommit from '../svg/icon_commit.svg'; -import limitWarning from './limit_warning_component.vue'; -import totalTime from './total_time_component.vue'; - -export default { - components: { - userAvatarImage, - totalTime, - limitWarning, - }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, - }, - computed: { - iconCommit() { - return iconCommit; - }, - }, -}; -</script> -<template> - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="(commit, i) in items" :key="i" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl" /> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> {{ commit.title }} </a> - </h5> - <span> - {{ s__('FirstPushedBy|First') }} <span class="commit-icon" v-html="iconCommit"> </span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ - commit.shortSha - }}</a> - {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"><total-time :time="commit.totalTime" /></div> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 3f0a9f2602c..b56e08175cc 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -5,7 +5,6 @@ import Flash from '../flash'; import Translate from '../vue_shared/translate'; import banner from './components/banner.vue'; import stageCodeComponent from './components/stage_code_component.vue'; -import stagePlanComponent from './components/stage_plan_component.vue'; import stageComponent from './components/stage_component.vue'; import stageReviewComponent from './components/stage_review_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue'; @@ -26,7 +25,7 @@ export default () => { components: { banner, 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, + 'stage-plan-component': stageComponent, 'stage-code-component': stageCodeComponent, 'stage-test-component': stageTestComponent, 'stage-review-component': stageReviewComponent, diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 11d6672cacf..81da0754752 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -69,6 +69,16 @@ export default { required: false, default: false, }, + dismissEndpoint: { + type: String, + required: false, + default: '', + }, + showSuggestPopover: { + type: Boolean, + required: false, + default: false, + }, }, data() { const treeWidth = @@ -141,7 +151,12 @@ export default { showTreeList: 'adjustView', }, mounted() { - this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); + this.setBaseConfig({ + endpoint: this.endpoint, + projectPath: this.projectPath, + dismissEndpoint: this.dismissEndpoint, + showSuggestPopover: this.showSuggestPopover, + }); if (this.shouldShow) { this.fetchData(); diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index c209b857652..da0cdbe467b 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -42,6 +42,7 @@ export default { noteableData: state => state.notes.noteableData, diffViewType: state => state.diffs.diffViewType, }), + ...mapState('diffs', ['showSuggestPopover']), ...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters([ 'isLoggedIn', @@ -80,7 +81,12 @@ export default { } }, methods: { - ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']), + ...mapActions('diffs', [ + 'cancelCommentForm', + 'assignDiscussionsToDiff', + 'saveDiffDiscussion', + 'setSuggestPopoverDismissed', + ]), handleCancelCommentForm(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); @@ -125,11 +131,13 @@ export default { :line="line" :help-page-path="helpPagePath" :diff-file="diffFile" + :show-suggest-popover="showSuggestPopover" save-button-title="Comment" class="diff-comment-form" @handleFormUpdateAddToReview="addToReview" @cancelForm="handleCancelCommentForm" @handleFormUpdate="handleSaveNote" + @handleSuggestDismissed="setSuggestPopoverDismissed" /> </div> </template> diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 1d897bca1dd..1e57e9b8a30 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -72,6 +72,8 @@ export default function initDiffsApp(store) { currentUser: JSON.parse(dataset.currentUserData) || {}, changesEmptyStateIllustration: dataset.changesEmptyStateIllustration, isFluidLayout: parseBoolean(dataset.isFluidLayout), + dismissEndpoint: dataset.dismissEndpoint, + showSuggestPopover: parseBoolean(dataset.showSuggestPopover), }; }, computed: { @@ -99,6 +101,8 @@ export default function initDiffsApp(store) { shouldShow: this.activeTab === 'diffs', changesEmptyStateIllustration: this.changesEmptyStateIllustration, isFluidLayout: this.isFluidLayout, + dismissEndpoint: this.dismissEndpoint, + showSuggestPopover: this.showSuggestPopover, }, }); }, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 479afc50113..88d7b4bba63 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -36,8 +36,8 @@ import { import { diffViewerModes } from '~/ide/constants'; export const setBaseConfig = ({ commit }, options) => { - const { endpoint, projectPath } = options; - commit(types.SET_BASE_CONFIG, { endpoint, projectPath }); + const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options; + commit(types.SET_BASE_CONFIG, { endpoint, projectPath, dismissEndpoint, showSuggestPopover }); }; export const fetchDiffFiles = ({ state, commit }) => { @@ -455,5 +455,17 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => { export const setFileCollapsed = ({ commit }, { filePath, collapsed }) => commit(types.SET_FILE_COLLAPSED, { filePath, collapsed }); +export const setSuggestPopoverDismissed = ({ commit, state }) => + axios + .post(state.dismissEndpoint, { + feature_name: 'suggest_popover_dismissed', + }) + .then(() => { + commit(types.SET_SHOW_SUGGEST_POPOVER); + }) + .catch(() => { + createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.')); + }); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index cf4dd93dbfb..6821c8445ea 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -28,4 +28,6 @@ export default () => ({ renderTreeList: true, showWhitespace: true, fileFinderVisible: false, + dismissEndpoint: '', + showSuggestPopover: true, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 6bb24c97139..8d6111da500 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -33,3 +33,5 @@ export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES' export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES'; export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES'; export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE'; + +export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 67bc1724738..00181a63c43 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -11,8 +11,8 @@ import * as types from './mutation_types'; export default { [types.SET_BASE_CONFIG](state, options) { - const { endpoint, projectPath } = options; - Object.assign(state, { endpoint, projectPath }); + const { endpoint, projectPath, dismissEndpoint, showSuggestPopover } = options; + Object.assign(state, { endpoint, projectPath, dismissEndpoint, showSuggestPopover }); }, [types.SET_LOADING](state, isLoading) { @@ -302,4 +302,7 @@ export default { file.renderingLines = !file.renderingLines; }, + [types.SET_SHOW_SUGGEST_POPOVER](state) { + state.showSuggestPopover = false; + }, }; diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index be867a3838d..891086b4142 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -8,9 +8,19 @@ import DropdownUtils from './dropdown_utils'; import { mergeUrlParams } from '../lib/utils/url_utility'; export default class AvailableDropdownMappings { - constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) { + constructor( + container, + baseEndpoint, + labelsEndpoint, + milestonesEndpoint, + groupsOnly, + includeAncestorGroups, + includeDescendantGroups, + ) { this.container = container; this.baseEndpoint = baseEndpoint; + this.labelsEndpoint = labelsEndpoint; + this.milestonesEndpoint = milestonesEndpoint; this.groupsOnly = groupsOnly; this.includeAncestorGroups = includeAncestorGroups; this.includeDescendantGroups = includeDescendantGroups; @@ -117,11 +127,11 @@ export default class AvailableDropdownMappings { } getMilestoneEndpoint() { - return `${this.baseEndpoint}/milestones.json`; + return `${this.milestonesEndpoint}.json`; } getLabelsEndpoint() { - let endpoint = `${this.baseEndpoint}/labels.json?`; + let endpoint = `${this.labelsEndpoint}.json?`; if (this.groupsOnly) { endpoint = `${endpoint}only_group_labels=true&`; diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js new file mode 100644 index 00000000000..8ca0e94cfeb --- /dev/null +++ b/app/assets/javascripts/filtered_search/constants.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export const TOKEN_TYPES = ['author', 'assignee']; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index cb0a84b490b..1cbfd7f9bb9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -9,6 +9,8 @@ import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { constructor({ baseEndpoint = '', + labelsEndpoint = '', + milestonesEndpoint = '', tokenizer, page, isGroup, @@ -18,6 +20,8 @@ export default class FilteredSearchDropdownManager { }) { this.container = FilteredSearchContainer.container; this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); + this.labelsEndpoint = labelsEndpoint.replace(/\/$/, ''); + this.milestonesEndpoint = milestonesEndpoint.replace(/\/$/, ''); this.tokenizer = tokenizer; this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys; this.filteredSearchInput = this.container.querySelector('.filtered-search'); @@ -48,6 +52,8 @@ export default class FilteredSearchDropdownManager { const availableMappings = new AvailableDropdownMappings( this.container, this.baseEndpoint, + this.labelsEndpoint, + this.milestonesEndpoint, this.groupsOnly, this.includeAncestorGroups, this.includeDescendantGroups, diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 78fbb3696cc..450e0725f2e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -86,6 +86,8 @@ export default class FilteredSearchManager { this.tokenizer = FilteredSearchTokenizer; this.dropdownManager = new FilteredSearchDropdownManager({ baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '', + labelsEndpoint: this.filteredSearchInput.getAttribute('data-labels-endpoint') || '', + milestonesEndpoint: this.filteredSearchInput.getAttribute('data-milestones-endpoint') || '', tokenizer: this.tokenizer, page: this.page, isGroup: this.isGroup, diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 315cd6f64da..7f6457242ef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,4 +1,4 @@ -import VisualTokenValue from 'ee_else_ce/filtered_search/visual_token_value'; +import VisualTokenValue from './visual_token_value'; import { objectToQueryString } from '~/lib/utils/common_utils'; import FilteredSearchContainer from './container'; diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index 38327472cb3..018207541b3 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -6,6 +6,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils'; import Flash from '~/flash'; import UsersCache from '~/lib/utils/users_cache'; import { __ } from '~/locale'; +import { TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants'; export default class VisualTokenValue { constructor(tokenValue, tokenType) { @@ -22,7 +23,7 @@ export default class VisualTokenValue { if (tokenType === 'label') { this.updateLabelTokenColor(tokenValueContainer); - } else if (tokenType === 'author' || tokenType === 'assignee') { + } else if (TOKEN_TYPES.includes(tokenType)) { this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement); } else if (tokenType === 'my-reaction') { this.updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement); @@ -56,13 +57,13 @@ export default class VisualTokenValue { updateLabelTokenColor(tokenValueContainer) { const { tokenValue } = this; const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); - const { baseEndpoint } = filteredSearchInput.dataset; - const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( - `${baseEndpoint}/labels.json`, + const { labelsEndpoint } = filteredSearchInput.dataset; + const labelsEndpointWithParams = FilteredSearchVisualTokens.getEndpointWithQueryParams( + `${labelsEndpoint}.json`, filteredSearchInput.dataset.endpointQueryParams, ); - return AjaxCache.retrieve(labelsEndpoint) + return AjaxCache.retrieve(labelsEndpointWithParams) .then(labels => { const matchingLabel = (labels || []).find( label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e88ca4747c5..de2a9664cde 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -11,6 +11,7 @@ import titleComponent from './title.vue'; import descriptionComponent from './description.vue'; import editedComponent from './edited.vue'; import formComponent from './form.vue'; +import PinnedLinks from './pinned_links.vue'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { @@ -19,6 +20,7 @@ export default { titleComponent, editedComponent, formComponent, + PinnedLinks, }, mixins: [recaptchaModalImplementor], props: { @@ -340,6 +342,7 @@ export default { :title-text="state.titleText" :show-inline-edit-button="showInlineEditButton" /> + <pinned-links :description-html="state.descriptionHtml" /> <description-component v-if="state.descriptionHtml" :can-update="canUpdate" diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue new file mode 100644 index 00000000000..7a54b26bc2b --- /dev/null +++ b/app/assets/javascripts/issue_show/components/pinned_links.vue @@ -0,0 +1,52 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + GlLink, + }, + props: { + descriptionHtml: { + type: String, + required: true, + }, + }, + computed: { + linksInDescription() { + const el = document.createElement('div'); + el.innerHTML = this.descriptionHtml; + return [...el.querySelectorAll('a')].map(a => a.href); + }, + // Detect links matching the following formats: + // Zoom Start links: https://zoom.us/s/<meeting-id> + // Zoom Join links: https://zoom.us/j/<meeting-id> + // Personal Zoom links: https://zoom.us/my/<meeting-id> + // Vanity Zoom links: https://gitlab.zoom.us/j/<meeting-id> (also /s and /my) + zoomHref() { + const zoomRegex = /^https:\/\/([\w\d-]+\.)?zoom\.us\/(s|j|my)\/.+/; + return this.linksInDescription.reduce((acc, currentLink) => { + let lastLink = acc; + if (zoomRegex.test(currentLink)) { + lastLink = currentLink; + } + return lastLink; + }, ''); + }, + }, +}; +</script> + +<template> + <div v-if="zoomHref" class="border-bottom mb-3 mt-n2"> + <gl-link + :href="zoomHref" + target="_blank" + class="btn btn-inverted btn-secondary btn-sm text-dark mb-3" + > + <icon name="brand-zoom" :size="14" /> + <strong class="vertical-align-top">{{ __('Join Zoom meeting') }}</strong> + </gl-link> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 92e20e92d66..d611b370ab9 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -17,10 +17,19 @@ export default { ...mapState(['isScrolledToBottomBeforeReceivingTrace']), }, updated() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); }, mounted() { - this.$nextTick(() => this.handleScrollDown()); + this.$nextTick(() => { + this.handleScrollDown(); + this.handleCollapsibleRows(); + }); + }, + destroyed() { + this.removeEventListener(); }, methods: { ...mapActions(['scrollBottom']), @@ -38,21 +47,45 @@ export default { }, 0); } }, + removeEventListener() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.removeEventListener('click', this.handleSectionClick)); + }, + /** + * The collapsible rows are sent in HTML from the backend + * We need tos add a onclick handler for the divs that match `.js-section-start` + * + */ + handleCollapsibleRows() { + this.$el + .querySelectorAll('.js-section-start') + .forEach(el => el.addEventListener('click', this.handleSectionClick)); + }, + /** + * On click, we toggle the hidden class of + * all the rows that match the `data-section` selector + */ + handleSectionClick(evt) { + const clickedArrow = evt.currentTarget; + // toggle the arrow class + clickedArrow.classList.toggle('fa-caret-right'); + clickedArrow.classList.toggle('fa-caret-down'); + + const { section } = clickedArrow.dataset; + const sibilings = this.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`); + + sibilings.forEach(row => row.classList.toggle('hidden')); + }, }, }; </script> <template> <pre class="js-build-trace build-trace qa-build-trace"> - <code - class="bash" - v-html="trace" - > + <code class="bash" v-html="trace"> </code> - <div - v-if="!isComplete" - class="js-log-animation build-loader-animation" - > + <div v-if="!isComplete" class="js-log-animation build-loader-animation"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index cb073a9b04d..6e92b599b0a 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -2,7 +2,6 @@ import _ from 'underscore'; import { GlLink } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import Icon from '~/vue_shared/components/icon.vue'; export default { @@ -10,7 +9,6 @@ export default { CiIcon, Icon, GlLink, - PipelineLink, }, props: { pipeline: { @@ -50,12 +48,9 @@ export default { <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="js-pipeline-path link-commit qa-pipeline-path" - /> + <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') }} diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js index 023c336db02..37896626053 100644 --- a/app/assets/javascripts/lib/utils/autosave.js +++ b/app/assets/javascripts/lib/utils/autosave.js @@ -29,4 +29,5 @@ export const updateDraft = (autosaveKey, text) => { }; export const getDiscussionReplyKey = (noteableType, discussionId) => + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ ['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/'); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index d93873e0214..e7f6255e5f1 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -12,6 +12,7 @@ function notificationGranted(message, opts, onclick) { } function notifyPermissions() { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ if ('Notification' in window) { return Notification.requestPermission(); } @@ -24,6 +25,7 @@ function notifyMe(message, body, icon, onclick) { icon: icon, }; // Let's check if the browser supports notifications + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ if (!('Notification' in window)) { // do nothing } else if (Notification.permission === 'granted') { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index e5cf43e8289..b6868e63716 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -147,14 +147,14 @@ export default class MergeRequestTabs { e.stopImmediatePropagation(); e.preventDefault(); - const { action } = e.currentTarget.dataset; + const { action } = e.currentTarget.dataset || {}; - if (action) { - const href = e.currentTarget.getAttribute('href'); - this.tabShown(action, href); - } else if (isMetaClick(e)) { + if (isMetaClick(e)) { const targetLink = e.currentTarget.getAttribute('href'); window.open(targetLink, '_blank'); + } else if (action) { + const href = e.currentTarget.getAttribute('href'); + this.tabShown(action, href); } } } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index d716fc211ca..0a652329dfe 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -70,7 +70,7 @@ export default { type: String, required: true, }, - deploymentEndpoint: { + deploymentsEndpoint: { type: String, required: false, default: null, @@ -148,7 +148,7 @@ export default { this.setEndpoints({ metricsEndpoint: this.metricsEndpoint, environmentsEndpoint: this.environmentsEndpoint, - deploymentsEndpoint: this.deploymentEndpoint, + deploymentsEndpoint: this.deploymentsEndpoint, dashboardEndpoint: this.dashboardEndpoint, }); @@ -280,9 +280,8 @@ export default { <gl-button v-gl-modal-directive="$options.addMetric.modalId" class="js-add-metric-button text-success border-success" + >{{ $options.addMetric.title }}</gl-button > - {{ $options.addMetric.title }} - </gl-button> <gl-modal ref="addMetricModal" :modal-id="$options.addMetric.modalId" @@ -296,16 +295,13 @@ export default { /> </form> <div slot="modal-footer"> - <gl-button @click="hideAddMetricModal"> - {{ __('Cancel') }} - </gl-button> + <gl-button @click="hideAddMetricModal">{{ __('Cancel') }}</gl-button> <gl-button :disabled="!formIsValid" variant="success" @click="submitCustomMetricsForm" + >{{ __('Save changes') }}</gl-button > - {{ __('Save changes') }} - </gl-button> </div> </gl-modal> </div> diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 49ce722b838..f41e215cb5d 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -164,10 +164,10 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => { }; export const fetchDeploymentsData = ({ state, dispatch }) => { - if (!state.deploymentEndpoint) { + if (!state.deploymentsEndpoint) { return Promise.resolve([]); } - return backOffRequest(() => axios.get(state.deploymentEndpoint)) + return backOffRequest(() => axios.get(state.deploymentsEndpoint)) .then(resp => resp.data) .then(response => { if (!response || !response.deployments) { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index f338dbbb0a6..98522c67696 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, one-var, consistent-return, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return */ +/* eslint-disable func-names, no-var, one-var, consistent-return, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, @gitlab/i18n/no-non-i18n-strings */ import $ from 'jquery'; import RefSelectDropdown from './ref_select_dropdown'; diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 09ecb695214..042ed196933 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -77,6 +77,11 @@ export default { required: false, default: '', }, + showSuggestPopover: { + type: Boolean, + required: false, + default: false, + }, }, data() { let updatedNoteBody = this.noteBody; @@ -247,6 +252,8 @@ export default { :can-suggest="canSuggest" :add-spacing-classes="false" :help-page-path="helpPagePath" + :show-suggest-popover="showSuggestPopover" + @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" > <textarea id="note_note" @@ -303,7 +310,7 @@ export default { {{ __('Add comment now') }} </button> <button - class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" + class="btn note-edit-cancel js-close-discussion-note-form" type="button" @click="cancelHandler()" > diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js index 377dce6c746..506e6075d16 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js @@ -124,11 +124,14 @@ export const ContributorsGraph = (function() { }; ContributorsGraph.prototype.draw_x_axis = function() { - return this.svg - .append('g') - .attr('class', 'x axis') - .attr('transform', 'translate(0, ' + this.height + ')') - .call(this.x_axis); + return ( + this.svg + .append('g') + .attr('class', 'x axis') + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + .attr('transform', 'translate(0, ' + this.height + ')') + .call(this.x_axis) + ); }; ContributorsGraph.prototype.draw_y_axis = function() { @@ -205,6 +208,7 @@ export const ContributorsMasterGraph = (function(superClass) { .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) .attr('class', 'tint-box') .append('g') + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; @@ -354,6 +358,7 @@ export const ContributorsAuthorGraph = (function(superClass) { .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom) .attr('class', 'spark') .append('g') + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ .attr('transform', 'translate(' + this.MARGIN.left + ',' + this.MARGIN.top + ')'); return this.svg; }; diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index f3a71ee434c..b2e365e5cde 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -83,8 +83,6 @@ export default { v-if="shouldRenderContent" :status="status" :item-id="pipeline.id" - :item-iid="pipeline.iid" - :item-id-tooltip="__('Pipeline ID (IID)')" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 00c02e15562..c41ecab1294 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -2,7 +2,6 @@ import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import popover from '~/vue_shared/directives/popover'; @@ -20,7 +19,6 @@ export default { components: { UserAvatarLink, GlLink, - PipelineLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -61,13 +59,10 @@ export default { }; </script> <template> - <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap"> - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="js-pipeline-url-link" - /> + <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags"> + <gl-link :href="pipeline.path" class="js-pipeline-url-link"> + <span class="pipeline-id">#{{ pipeline.id }}</span> + </gl-link> <div class="label-container"> <span v-if="pipeline.flags.latest" diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 1e4dfe76b26..f535b2ae9f2 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -64,7 +64,7 @@ export default { <th>{{ s__('ContainerRegistry|Tag') }}</th> <th>{{ s__('ContainerRegistry|Tag ID') }}</th> <th>{{ s__('ContainerRegistry|Size') }}</th> - <th>{{ s__('ContainerRegistry|Created') }}</th> + <th>{{ s__('ContainerRegistry|Last Updated') }}</th> <th></th> </tr> </thead> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index e24a5e2c447..4519f82fc93 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -1,5 +1,6 @@ <script> import { GlBadge } from '@gitlab/ui'; +import { visitUrl } from '~/lib/utils/url_utility'; import { getIconName } from '../../utils/icon'; import getRefMixin from '../../mixins/get_ref'; @@ -63,6 +64,8 @@ export default { openRow() { if (this.isFolder) { this.$router.push(this.routerLinkTo); + } else { + visitUrl(this.url); } }, }, diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index c64d16ef02a..ef147ec15cb 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -18,6 +18,7 @@ const defaultClient = createDefaultClient( cacheConfig: { fragmentMatcher, dataIdFromObject: obj => { + /* eslint-disable @gitlab/i18n/no-non-i18n-strings */ // eslint-disable-next-line no-underscore-dangle switch (obj.__typename) { // We need to create a dynamic ID for each entry @@ -33,6 +34,7 @@ const defaultClient = createDefaultClient( // eslint-disable-next-line no-underscore-dangle return obj.id || obj._id; } + /* eslint-enable @gitlab/i18n/no-non-i18n-strings */ }, }, }, diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js index 4e194640e92..87d54c01200 100644 --- a/app/assets/javascripts/repository/utils/title.js +++ b/app/assets/javascripts/repository/utils/title.js @@ -5,5 +5,6 @@ export const setTitle = (pathMatch, ref, project) => { const path = pathMatch.replace(/^\//, ''); const isEmpty = path === ''; + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`; }; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 6aca4067ba7..842fb5e5b4f 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -447,9 +447,11 @@ export class SearchAutocomplete { onClick(item, $el, e) { if (window.location.pathname.indexOf(item.url) !== -1) { if (!e.metaKey) e.preventDefault(); + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ if (item.category === 'Projects') { this.projectInputEl.val(item.id); } + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ if (item.category === 'Groups') { this.groupInputEl.val(item.id); } diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 7e6f02b10af..33cedf78331 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -427,6 +427,7 @@ function UsersSelect(currentUser, els, options = {}) { const isActive = $el.hasClass('is-active'); const previouslySelected = $dropdown .closest('.selectbox') + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ .find("input[name='" + $dropdown.data('fieldName') + "'][value!=0]"); // Enables support for limiting the number of users selected diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js new file mode 100644 index 00000000000..2fec96d1435 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js @@ -0,0 +1,132 @@ +import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants'; +import { clearNote, note, postError } from './note'; +import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } from './utils'; + +const comment = ` + <div> + <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea> + ${note} + <p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p> + </div> + <div class="gitlab-button-wrapper"> + <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Logout </button> + <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button> + </div> +`; + +const resetCommentBox = () => { + const commentBox = selectCommentBox(); + const commentButton = selectCommentButton(); + + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + commentButton.innerText = 'Send feedback'; + commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); + commentButton.style.opacity = 1; + + commentBox.style.pointerEvents = 'auto'; + commentBox.style.color = BLACK; +}; + +const resetCommentButton = () => { + const commentBox = selectCommentBox(); + const currentNote = selectNote(); + + commentBox.value = ''; + currentNote.innerText = ''; +}; + +const resetComment = () => { + resetCommentBox(); + resetCommentButton(); +}; + +const confirmAndClear = mergeRequestId => { + const commentButton = selectCommentButton(); + const currentNote = selectNote(); + + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + commentButton.innerText = 'Feedback sent'; + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`; + setTimeout(resetComment, 2000); +}; + +const setInProgressState = () => { + const commentButton = selectCommentButton(); + const commentBox = selectCommentBox(); + + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + commentButton.innerText = 'Sending feedback'; + commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary'); + commentButton.style.opacity = 0.5; + commentBox.style.color = MUTED; + commentBox.style.pointerEvents = 'none'; +}; + +const postComment = ({ + href, + platform, + browser, + userAgent, + innerWidth, + innerHeight, + projectId, + mergeRequestId, + mrUrl, + token, +}) => { + // Clear any old errors + clearNote(COMMENT_BOX); + + setInProgressState(); + + const commentText = selectCommentBox().value.trim(); + + if (!commentText) { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + postError('Your comment appears to be empty.', COMMENT_BOX); + resetCommentBox(); + return; + } + + const detailText = ` + \n +<details> + <summary>Metadata</summary> + Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}. + <br /><br /> + <em>User agent: ${userAgent}</em> +</details> + `; + + const url = ` + ${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`; + + const body = `${commentText} ${detailText}`; + + fetch(url, { + method: 'POST', + headers: { + 'PRIVATE-TOKEN': token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ body }), + }) + .then(response => { + if (response.ok) { + confirmAndClear(mergeRequestId); + return; + } + + throw new Error(`${response.status}: ${response.statusText}`); + }) + .catch(err => { + postError( + `Your comment could not be sent. Please try again. Error: ${err.message}`, + COMMENT_BOX, + ); + resetCommentBox(); + }); +}; + +export { comment, postComment }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/components/constants.js new file mode 100644 index 00000000000..32ed1153515 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/constants.js @@ -0,0 +1,37 @@ +// component selectors +const COLLAPSE_BUTTON = 'gitlab-collapse'; +const COMMENT_BOX = 'gitlab-comment'; +const COMMENT_BUTTON = 'gitlab-comment-button'; +const FORM = 'gitlab-form-wrapper'; +const LOGIN = 'gitlab-login'; +const LOGOUT = 'gitlab-logout-button'; +const NOTE = 'gitlab-validation-note'; +const REMEMBER_TOKEN = 'gitlab-remember_token'; +const REVIEW_CONTAINER = 'gitlab-review-container'; +const TOKEN_BOX = 'gitlab-token'; + +// colors — these are applied programmatically +// rest of styles belong in ./styles +const BLACK = 'rgba(46, 46, 46, 1)'; +const CLEAR = 'rgba(255, 255, 255, 0)'; +const MUTED = 'rgba(223, 223, 223, 0.5)'; +const RED = 'rgba(219, 59, 33, 1)'; +const WHITE = 'rgba(255, 255, 255, 1)'; + +export { + COLLAPSE_BUTTON, + COMMENT_BOX, + COMMENT_BUTTON, + FORM, + LOGIN, + LOGOUT, + NOTE, + REMEMBER_TOKEN, + REVIEW_CONTAINER, + TOKEN_BOX, + BLACK, + CLEAR, + MUTED, + RED, + WHITE, +}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js new file mode 100644 index 00000000000..43581818152 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/index.js @@ -0,0 +1,23 @@ +import { comment, postComment } from './comment'; +import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants'; +import { authorizeUser, login } from './login'; +import { selectContainer } from './utils'; +import { form, logoutUser, toggleForm } from './wrapper'; +import { collapseButton } from './wrapper_icons'; + +export { + authorizeUser, + collapseButton, + comment, + form, + login, + logoutUser, + postComment, + selectContainer, + toggleForm, + COLLAPSE_BUTTON, + COMMENT_BUTTON, + LOGIN, + LOGOUT, + REVIEW_CONTAINER, +}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js new file mode 100644 index 00000000000..ce713cdc520 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/login.js @@ -0,0 +1,52 @@ +import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants'; +import { clearNote, note, postError } from './note'; +import { buttonClearStyles, selectRemember, selectToken } from './utils'; +import { addCommentForm } from './wrapper'; + +const login = ` + <div> + <label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label> + <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password"> + ${note} + </div> + <div class="gitlab-checkbox-wrapper"> + <input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember"> + <label for="${REMEMBER_TOKEN}" class="gitlab-checkbox-label">Remember me</label> + </div> + <div class="gitlab-button-wrapper"> + <button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${LOGIN}"> Submit </button> + </div> +`; + +const storeToken = (token, state) => { + const { localStorage } = window; + const rememberMe = selectRemember().checked; + + // All the browsers we support have localStorage, so let's silently fail + // and go on with the rest of the functionality. + try { + if (rememberMe) { + localStorage.setItem('token', token); + } + } finally { + state.token = token; + } +}; + +const authorizeUser = state => { + // Clear any old errors + clearNote(TOKEN_BOX); + + const token = selectToken().value; + + if (!token) { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + postError('Please enter your token.', TOKEN_BOX); + return; + } + + storeToken(token, state); + addCommentForm(); +}; + +export { authorizeUser, login }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js new file mode 100644 index 00000000000..dfebf58fd95 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/note.js @@ -0,0 +1,27 @@ +import { NOTE, RED } from './constants'; +import { selectById, selectNote } from './utils'; + +const note = ` + <p id=${NOTE} class='gitlab-message'></p> +`; + +const clearNote = inputId => { + const currentNote = selectNote(); + currentNote.innerText = ''; + currentNote.style.color = ''; + + if (inputId) { + const field = document.getElementById(inputId); + field.style.borderColor = ''; + } +}; + +const postError = (message, inputId) => { + const currentNote = selectNote(); + const field = selectById(inputId); + field.style.borderColor = RED; + currentNote.style.color = RED; + currentNote.innerText = message; +}; + +export { clearNote, note, postError }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js new file mode 100644 index 00000000000..7bc2e5a905b --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js @@ -0,0 +1,42 @@ +/* global document */ + +import { + COLLAPSE_BUTTON, + COMMENT_BOX, + COMMENT_BUTTON, + FORM, + NOTE, + REMEMBER_TOKEN, + REVIEW_CONTAINER, + TOKEN_BOX, +} from './constants'; + +// this style must be applied inline in a handful of components +/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ +const buttonClearStyles = ` + -webkit-appearance: none; +`; + +// selector functions to abstract out a little +const selectById = id => document.getElementById(id); +const selectCollapseButton = () => document.getElementById(COLLAPSE_BUTTON); +const selectCommentBox = () => document.getElementById(COMMENT_BOX); +const selectCommentButton = () => document.getElementById(COMMENT_BUTTON); +const selectContainer = () => document.getElementById(REVIEW_CONTAINER); +const selectForm = () => document.getElementById(FORM); +const selectNote = () => document.getElementById(NOTE); +const selectRemember = () => document.getElementById(REMEMBER_TOKEN); +const selectToken = () => document.getElementById(TOKEN_BOX); + +export { + buttonClearStyles, + selectById, + selectCollapseButton, + selectContainer, + selectCommentBox, + selectCommentButton, + selectForm, + selectNote, + selectRemember, + selectToken, +}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js new file mode 100644 index 00000000000..233b7ec496c --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js @@ -0,0 +1,82 @@ +import { comment } from './comment'; +import { CLEAR, FORM, WHITE } from './constants'; +import { login } from './login'; +import { selectCollapseButton, selectContainer, selectForm } from './utils'; +import { commentIcon, compressIcon } from './wrapper_icons'; + +const form = content => ` + <form id=${FORM}> + ${content} + </form> +`; + +const addCommentForm = () => { + const formWrapper = selectForm(); + formWrapper.innerHTML = comment; +}; + +const addLoginForm = () => { + const formWrapper = selectForm(); + formWrapper.innerHTML = login; +}; + +function logoutUser() { + const { localStorage } = window; + + // All the browsers we support have localStorage, so let's silently fail + // and go on with the rest of the functionality. + try { + localStorage.removeItem('token'); + } catch (err) { + return; + } + + addLoginForm(); +} + +function toggleForm() { + const container = selectContainer(); + const collapseButton = selectCollapseButton(); + const currentForm = selectForm(); + const OPEN = 'open'; + const CLOSED = 'closed'; + + /* + You may wonder why we spread the arrays before we reverse them. + In the immortal words of MDN, + Careful: reverse is destructive. It also changes the original array + */ + + const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open']; + const closedButtonClasses = [...openButtonClasses].reverse(); + const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper']; + const closedContainerClasses = [...openContainerClasses].reverse(); + + const stateVals = { + [OPEN]: { + buttonClasses: openButtonClasses, + containerClasses: openContainerClasses, + icon: compressIcon, + display: 'flex', + backgroundColor: WHITE, + }, + [CLOSED]: { + buttonClasses: closedButtonClasses, + containerClasses: closedContainerClasses, + icon: commentIcon, + display: 'none', + backgroundColor: CLEAR, + }, + }; + + const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; + const currentVals = stateVals[nextState]; + + container.classList.replace(...currentVals.containerClasses); + container.style.backgroundColor = currentVals.backgroundColor; + currentForm.style.display = currentVals.display; + collapseButton.classList.replace(...currentVals.buttonClasses); + collapseButton.innerHTML = currentVals.icon; +} + +export { addCommentForm, addLoginForm, form, logoutUser, toggleForm }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js new file mode 100644 index 00000000000..b686fd4f5c2 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js @@ -0,0 +1,15 @@ +import { buttonClearStyles } from './utils'; + +const commentIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg> +`; + +const compressIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg> +`; + +const collapseButton = ` + <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button> +`; + +export { commentIcon, compressIcon, collapseButton }; diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js index 91d0382feac..941d77e25b4 100644 --- a/app/assets/javascripts/visual_review_toolbar/index.js +++ b/app/assets/javascripts/visual_review_toolbar/index.js @@ -1,2 +1,37 @@ import './styles/toolbar.css'; -import 'vendor/visual_review_toolbar'; + +import { form, selectContainer, REVIEW_CONTAINER } from './components'; +import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store'; + +/* + + Welcome to the visual review toolbar files. A few useful notes: + + - These files build a static script that is served from our webpack + assets folder. (https://gitlab.com/assets/webpack/visual_review_toolbar.js) + + - To compile this file, run `yarn webpack-vrt`. + + - Vue is not used in these files because we do not want to ask users to + install another library at this time. It's all pure vanilla javascript. + +*/ + +window.addEventListener('load', () => { + initializeState(window, document); + + const { content, toggleButton } = getInitialView(window); + const container = document.createElement('div'); + + container.setAttribute('id', REVIEW_CONTAINER); + container.insertAdjacentHTML('beforeend', toggleButton); + container.insertAdjacentHTML('beforeend', form(content)); + + document.body.insertBefore(container, document.body.firstChild); + + selectContainer().addEventListener('click', event => { + eventLookup(event)(); + }); + + window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200)); +}); diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js new file mode 100644 index 00000000000..93996be8473 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/events.js @@ -0,0 +1,36 @@ +import { + authorizeUser, + logoutUser, + postComment, + toggleForm, + COLLAPSE_BUTTON, + COMMENT_BUTTON, + LOGIN, + LOGOUT, +} from '../components'; + +import { state } from './state'; + +const noop = () => {}; + +const eventLookup = ({ target: { id } }) => { + switch (id) { + case COLLAPSE_BUTTON: + return toggleForm; + case COMMENT_BUTTON: + return postComment.bind(null, state); + case LOGIN: + return authorizeUser.bind(null, state); + case LOGOUT: + return logoutUser; + default: + return noop; + } +}; + +const updateWindowSize = wind => { + state.innerWidth = wind.innerWidth; + state.innerHeight = wind.innerHeight; +}; + +export { eventLookup, updateWindowSize }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js new file mode 100644 index 00000000000..7143588c0bf --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/index.js @@ -0,0 +1,5 @@ +import { eventLookup, updateWindowSize } from './events'; +import { getInitialView, initializeState } from './state'; +import debounce from './utils'; + +export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js new file mode 100644 index 00000000000..f5ede6e85b2 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/state.js @@ -0,0 +1,77 @@ +import { comment, login, collapseButton } from '../components'; + +const state = { + browser: '', + href: '', + innerWidth: '', + innerHeight: '', + mergeRequestId: '', + mrUrl: '', + platform: '', + projectId: '', + userAgent: '', + token: '', +}; + +// adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator#Example_2_Browser_detect_and_return_an_index +const getBrowserId = sUsrAg => { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + const aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera']; + let nIdx = aKeys.length - 1; + + for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx -= 1); + return aKeys[nIdx]; +}; + +const initializeState = (wind, doc) => { + const { + innerWidth, + innerHeight, + location: { href }, + navigator: { platform, userAgent }, + } = wind; + + const browser = getBrowserId(userAgent); + + const scriptEl = doc.getElementById('review-app-toolbar-script'); + const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset; + + // This mutates our default state object above. It's weird but it makes the linter happy. + Object.assign(state, { + browser, + href, + innerWidth, + innerHeight, + mergeRequestId, + mrUrl, + platform, + projectId, + userAgent, + }); +}; + +function getInitialView({ localStorage }) { + const loginView = { + content: login, + toggleButton: collapseButton, + }; + + const commentView = { + content: comment, + toggleButton: collapseButton, + }; + + try { + const token = localStorage.getItem('token'); + + if (token) { + state.token = token; + return commentView; + } + return loginView; + } catch (err) { + return loginView; + } +} + +export { initializeState, getInitialView, state }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js new file mode 100644 index 00000000000..5cf145351b3 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/utils.js @@ -0,0 +1,15 @@ +const debounce = (fn, time) => { + let current; + + const debounced = () => { + if (current) { + clearTimeout(current); + } + + current = setTimeout(fn, time); + }; + + return debounced; +}; + +export default debounce; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index abe5bdd2901..34cdb70ce14 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -49,6 +49,7 @@ export default { required: false, default: () => ({ sourceProjectId: '', + sourceProjectPath: '', mergeRequestId: '', appUrl: '', }), @@ -184,11 +185,6 @@ export default { :link="deploymentExternalUrl" :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`" /> - <visual-review-app-link - v-if="showVisualReviewApp" - :link="deploymentExternalUrl" - :app-metadata="visualReviewAppMeta" - /> </template> <template slot="result" slot-scope="slotProps"> @@ -213,12 +209,12 @@ export default { :link="deploymentExternalUrl" css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline" /> - <visual-review-app-link - v-if="showVisualReviewApp" - :link="deploymentExternalUrl" - :app-metadata="visualReviewAppMeta" - /> </template> + <visual-review-app-link + v-if="showVisualReviewApp" + :link="deploymentExternalUrl" + :app-metadata="visualReviewAppMeta" + /> </template> <span v-if="deployment.stop_url" 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 c377c16fb13..f5fa68308bc 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 @@ -5,7 +5,6 @@ import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; -import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; @@ -17,7 +16,6 @@ export default { Icon, TooltipOnTruncate, GlLink, - PipelineLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, @@ -114,12 +112,9 @@ export default { <div class="media-body"> <div class="font-weight-bold js-pipeline-info-container"> {{ s__('Pipeline|Pipeline') }} - <pipeline-link - :href="pipeline.path" - :pipeline-id="pipeline.id" - :pipeline-iid="pipeline.iid" - class="pipeline-id pipeline-iid font-weight-normal" - /> + <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"> {{ s__('Pipeline|for') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 03a15ba81ed..17ac8ada32d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import Deployment from './deployment.vue'; import MrWidgetContainer from './mr_widget_container.vue'; import MrWidgetPipeline from './mr_widget_pipeline.vue'; @@ -17,6 +18,8 @@ export default { Deployment, MrWidgetContainer, MrWidgetPipeline, + MergeTrainInfo: () => + import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'), }, props: { mr: { @@ -50,6 +53,7 @@ export default { appUrl: this.mr.appUrl, mergeRequestId: this.mr.iid, sourceProjectId: this.mr.sourceProjectId, + sourceProjectPath: this.mr.sourceProjectFullPath, }; }, pipeline() { @@ -58,6 +62,9 @@ export default { showVisualReviewAppLink() { return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable); }, + showMergeTrainInfo() { + return _.isNumber(this.mr.mergeTrainIndex); + }, }, }; </script> @@ -79,10 +86,15 @@ export default { :class="deploymentClass" :deployment="deployment" :show-metrics="hasDeploymentMetrics" - :show-visual-review-app="true" + :show-visual-review-app="showVisualReviewAppLink" :visual-review-app-meta="visualReviewAppMeta" /> </div> + <merge-train-info + v-if="showMergeTrainInfo" + class="mr-widget-extension" + :merge-train-index="mr.mergeTrainIndex" + /> </template> </mr-widget-container> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue deleted file mode 100644 index eae4c06467c..00000000000 --- a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script> -import { GlLink, GlTooltipDirective } from '@gitlab/ui'; - -export default { - components: { - GlLink, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - href: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - pipelineIid: { - type: Number, - required: true, - }, - }, -}; -</script> -<template> - <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')"> - <span class="pipeline-id">#{{ pipelineId }}</span> - <span class="pipeline-iid">(#{{ pipelineIid }})</span> - </gl-link> -</template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 0bac63b1062..3f45dc7853b 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -37,16 +37,6 @@ export default { type: Number, required: true, }, - itemIid: { - type: Number, - required: false, - default: null, - }, - itemIdTooltip: { - type: String, - required: false, - default: '', - }, time: { type: String, required: true, @@ -95,12 +85,7 @@ export default { <section class="header-main-content"> <ci-icon-badge :status="status" /> - <strong v-gl-tooltip :title="itemIdTooltip"> - {{ itemName }} #{{ itemId }} - <template v-if="itemIid" - >(#{{ itemIid }})</template - > - </strong> + <strong> {{ itemName }} #{{ itemId }} </strong> <template v-if="shouldRenderTriggeredLabel"> triggered @@ -111,8 +96,9 @@ export default { <timeago-tooltip :time="time" /> + by + <template v-if="user"> - by <gl-link v-gl-tooltip :href="user.path" diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 0f3b3568414..3bdc0bb8ebd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -67,6 +67,11 @@ export default { required: false, default: '', }, + showSuggestPopover: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -194,8 +199,10 @@ export default { :preview-markdown="previewMarkdown" :line-content="lineContent" :can-suggest="canSuggest" + :show-suggest-popover="showSuggestPopover" @preview-markdown="showPreviewTab" @write-markdown="showWriteTab" + @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" /> <div v-show="!previewMarkdown" class="md-write-holder"> <div class="zen-backdrop"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index a5a5b2ef415..56a16c9e4d6 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; import ToolbarButton from './toolbar_button.vue'; import Icon from '../icon.vue'; @@ -8,6 +8,8 @@ export default { components: { ToolbarButton, Icon, + GlPopover, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -27,6 +29,11 @@ export default { required: false, default: true, }, + showSuggestPopover: { + type: Boolean, + required: false, + default: false, + }, }, computed: { mdTable() { @@ -70,6 +77,9 @@ export default { this.$emit('write-markdown'); }, + handleSuggestDismissed() { + this.$emit('handleSuggestDismissed'); + }, }, }; </script> @@ -93,66 +103,92 @@ export default { </button> </li> <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> - <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" /> - <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" /> - <toolbar-button - :prepend="true" - tag="> " - :button-title="__('Insert a quote')" - icon="quote" - /> - <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> - <toolbar-button - tag="[{text}](url)" - tag-select="url" - :button-title="__('Add a link')" - icon="link" - /> - <toolbar-button - :prepend="true" - tag="* " - :button-title="__('Add a bullet list')" - icon="list-bulleted" - /> - <toolbar-button - :prepend="true" - tag="1. " - :button-title="__('Add a numbered list')" - icon="list-numbered" - /> - <toolbar-button - :prepend="true" - tag="* [ ] " - :button-title="__('Add a task list')" - icon="task-done" - /> - <toolbar-button - :tag="mdTable" - :prepend="true" - :button-title="__('Add a table')" - icon="table" - /> - <toolbar-button - v-if="canSuggest" - :tag="mdSuggestion" - :prepend="true" - :button-title="__('Insert suggestion')" - :cursor-offset="4" - :tag-content="lineContent" - icon="doc-code" - class="qa-suggestion-btn" - /> - <button - v-gl-tooltip - :aria-label="__('Go full screen')" - class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" - data-container="body" - tabindex="-1" - :title="__('Go full screen')" - type="button" - > - <icon name="screen-full" /> - </button> + <div class="d-inline-block"> + <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" /> + <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" /> + <toolbar-button + :prepend="true" + tag="> " + :button-title="__('Insert a quote')" + icon="quote" + /> + </div> + <div class="d-inline-block ml-md-2 ml-0"> + <template v-if="canSuggest"> + <toolbar-button + ref="suggestButton" + :tag="mdSuggestion" + :prepend="true" + :button-title="__('Insert suggestion')" + :cursor-offset="4" + :tag-content="lineContent" + icon="doc-code" + class="qa-suggestion-btn" + @click="handleSuggestDismissed" + /> + <gl-popover + v-if="showSuggestPopover" + :target="() => $refs.suggestButton" + :css-classes="['diff-suggest-popover']" + placement="bottom" + :show="showSuggestPopover" + > + <strong>{{ __('New! Suggest changes directly') }}</strong> + <p class="mb-2"> + {{ __('Suggest code changes which are immediately applied. Try it out!') }} + </p> + <gl-button variant="primary" size="sm" @click="handleSuggestDismissed"> + {{ __('Got it') }} + </gl-button> + </gl-popover> + </template> + <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> + <toolbar-button + tag="[{text}](url)" + tag-select="url" + :button-title="__('Add a link')" + icon="link" + /> + </div> + <div class="d-inline-block ml-md-2 ml-0"> + <toolbar-button + :prepend="true" + tag="* " + :button-title="__('Add a bullet list')" + icon="list-bulleted" + /> + <toolbar-button + :prepend="true" + tag="1. " + :button-title="__('Add a numbered list')" + icon="list-numbered" + /> + <toolbar-button + :prepend="true" + tag="* [ ] " + :button-title="__('Add a task list')" + icon="task-done" + /> + <toolbar-button + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + /> + </div> + <div class="d-inline-block ml-md-2 ml-0"> + <button + v-gl-tooltip + :aria-label="__('Go full screen')" + class="toolbar-btn toolbar-fullscreen-btn js-zen-enter" + data-container="body" + tabindex="-1" + :title="__('Go full screen')" + type="button" + > + <icon name="screen-full" /> + </button> + </div> </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 4572caa907b..94f78c0c085 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -66,6 +66,7 @@ export default { class="toolbar-btn js-md" tabindex="-1" data-container="body" + @click="() => $emit('click')" > <icon :name="icon" /> </button> diff --git a/app/assets/javascripts/vue_shared/components/pagination/constants.js b/app/assets/javascripts/vue_shared/components/pagination/constants.js index c24b142ac7e..748ad178c70 100644 --- a/app/assets/javascripts/vue_shared/components/pagination/constants.js +++ b/app/assets/javascripts/vue_shared/components/pagination/constants.js @@ -7,3 +7,7 @@ export const PREV = s__('Pagination|Prev'); export const NEXT = s__('Pagination|Next'); export const FIRST = s__('Pagination|« First'); export const LAST = s__('Pagination|Last »'); +export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page'); +export const LABEL_PREV_PAGE = s__('Pagination|Go to previous page'); +export const LABEL_NEXT_PAGE = s__('Pagination|Go to next page'); +export const LABEL_LAST_PAGE = s__('Pagination|Go to last page'); diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue index 0b44f8578cb..06097913e91 100644 --- a/app/assets/javascripts/vue_shared/components/pagination_links.vue +++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue @@ -1,6 +1,13 @@ <script> import { GlPagination } from '@gitlab/ui'; -import { s__ } from '../../locale'; +import { + PREV, + NEXT, + LABEL_FIRST_PAGE, + LABEL_PREV_PAGE, + LABEL_NEXT_PAGE, + LABEL_LAST_PAGE, +} from '~/vue_shared/components/pagination/constants'; export default { components: { @@ -16,23 +23,27 @@ export default { required: true, }, }, - firstText: s__('Pagination|« First'), - prevText: s__('Pagination|Prev'), - nextText: s__('Pagination|Next'), - lastText: s__('Pagination|Last »'), + prevText: PREV, + nextText: NEXT, + labelFirstPage: LABEL_FIRST_PAGE, + labelPrevPage: LABEL_PREV_PAGE, + labelNextPage: LABEL_NEXT_PAGE, + labelLastPage: LABEL_LAST_PAGE, }; </script> <template> <gl-pagination v-bind="$attrs" - :change="change" - :page="pageInfo.page" + :value="pageInfo.page" :per-page="pageInfo.perPage" :total-items="pageInfo.total" - :first-text="$options.firstText" :prev-text="$options.prevText" :next-text="$options.nextText" - :last-text="$options.lastText" + :label-first-page="$options.labelFirstPage" + :label-prev-page="$options.labelPrevPage" + :label-next-page="$options.labelNextPage" + :label-last-page="$options.labelLastPage" + @input="change" /> </template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index db09118ba15..1bd5043ed10 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -416,6 +416,7 @@ img.emoji { .center { text-align: center; } .block { display: block; } .flex { display: flex; } +.vertical-align-top { vertical-align: top; } .vertical-align-middle { vertical-align: middle; } .vertical-align-sub { vertical-align: sub; } .flex-align-self-center { align-self: center; } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 2a601afff53..821e6691fe4 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -248,14 +248,24 @@ label { .gl-form-checkbox { align-items: baseline; + margin-right: 1rem; + margin-bottom: 0.25rem; + + .form-check-input { + margin-right: 0; + } + + .form-check-label { + padding-left: $gl-padding-8; + } &.form-check-inline .form-check-input { align-self: flex-start; - margin-right: $gl-padding-8; height: 1.5 * $gl-font-size; } - .help-text { - margin-bottom: 0; + .form-check-input:disabled, + .form-check-input:disabled ~ .form-check-label { + cursor: not-allowed; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index bfd96a4bc05..0bf911eec0a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -154,11 +154,9 @@ } .toolbar-fullscreen-btn { - margin-left: $gl-padding; margin-right: -5px; @include media-breakpoint-down(xs) { - margin-left: 0; margin-right: 0; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 07fc655307e..b6a24247d40 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -823,7 +823,7 @@ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15); /* Merge Requests */ -$mr-tabs-height: 51px; +$mr-tabs-height: 48px; $mr-version-controls-height: 56px; /* diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 6fc742871e7..6e98908eeed 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -124,6 +124,10 @@ float: left; padding-left: $gl-padding-8; } + + .section-header ~ .section.line { + margin-left: $gl-padding; + } } .build-header { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index c386493231c..62fc7311d94 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -9,7 +9,6 @@ color: $gl-text-color; } - .issue_created_ago, .author-link { white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 4ebf1019456..d2d35d91e0b 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,7 +14,7 @@ position: -webkit-sticky; position: sticky; top: $mr-file-header-top; - z-index: 102; + z-index: 220; &::before { content: ''; @@ -1122,3 +1122,15 @@ table.code { outline: 0; } } + +.diff-suggest-popover { + &.popover { + width: 250px; + min-width: 250px; + z-index: 210; + } + + .popover-header { + display: none; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 8cb3fab74e0..3917937f4af 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -904,7 +904,8 @@ margin-right: -5px; } -.deploy-heading { +.deploy-heading, +.merge-train-info { @include media-breakpoint-up(md) { padding: $gl-padding-8 $gl-padding; } diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb index ed7ea2f0e04..e4123d87137 100644 --- a/app/controllers/concerns/boards_actions.rb +++ b/app/controllers/concerns/boards_actions.rb @@ -35,4 +35,12 @@ module BoardsActions boards.find(params[:id]) end end + + def serializer + BoardSerializer.new(current_user: current_user) + end + + def serialize_as_json(resource) + serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) + end end diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index 8b191c86397..7625600e452 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -69,7 +69,7 @@ module BoardsResponses end def serialize_as_json(resource) - resource.as_json(only: [:id]) + serializer.represent(resource).as_json end def respond_with(resource) @@ -80,4 +80,8 @@ module BoardsResponses end end end + + def serializer + BoardSerializer.new + end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 08a0647f6b2..ae46a234aa6 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -164,7 +164,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project) result = dashboard_finder.find(project, current_user, environment, params[:dashboard]) - result[:all_dashboards] = project.repository.metrics_dashboard_paths + result[:all_dashboards] = dashboard_finder.find_all_paths(project) else result = dashboard_finder.find(project, current_user, environment) end diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index 567d750caae..bf1d8d8b5fc 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -3,7 +3,7 @@ module Projects module Registry class TagsController < ::Projects::Registry::ApplicationController - before_action :authorize_update_container_image!, only: [:destroy] + before_action :authorize_destroy_container_image!, only: [:destroy] def index respond_to do |format| diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index cb25548c83f..a80ab3bcd28 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -25,7 +25,6 @@ class SearchController < ApplicationController @show_snippets = search_service.show_snippets? @search_results = search_service.search_results @search_objects = search_service.search_objects - @display_options = search_service.display_options render_commits if @scope == 'commits' eager_load_user_status if @scope == 'users' diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 2e5bdbd79c8..5615909c4ec 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -7,8 +7,8 @@ class GitlabSchema < GraphQL::Schema AUTHENTICATED_COMPLEXITY = 250 ADMIN_COMPLEXITY = 300 - DEFAULT_MAX_DEPTH = 10 - AUTHENTICATED_MAX_DEPTH = 15 + DEFAULT_MAX_DEPTH = 15 + AUTHENTICATED_MAX_DEPTH = 20 use BatchLoader::GraphQL use Gitlab::Graphql::Authorize diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index dd5133189dc..f2365499eee 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -4,6 +4,8 @@ module Types class IssueType < BaseObject graphql_name 'Issue' + implements(Types::Notes::NoteableType) + authorize :read_issue expose_permissions Types::PermissionTypes::Issue @@ -49,5 +51,7 @@ module Types field :created_at, Types::TimeType, null: false field :updated_at, Types::TimeType, null: false + + field :task_completion_status, Types::TaskCompletionStatus, null: false end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 85ac3102442..dac4c24cf10 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -4,6 +4,8 @@ module Types class MergeRequestType < BaseObject graphql_name 'MergeRequest' + implements(Types::Notes::NoteableType) + authorize :read_merge_request expose_permissions Types::PermissionTypes::MergeRequest @@ -53,5 +55,7 @@ module Types field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline field :pipelines, Types::Ci::PipelineType.connection_type, resolver: Resolvers::MergeRequestPipelinesResolver + + field :task_completion_status, Types::TaskCompletionStatus, null: false end end diff --git a/app/graphql/types/notes/diff_position_type.rb b/app/graphql/types/notes/diff_position_type.rb new file mode 100644 index 00000000000..104ccb79bbb --- /dev/null +++ b/app/graphql/types/notes/diff_position_type.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Types + module Notes + class DiffPositionType < BaseObject + graphql_name 'DiffPosition' + + field :head_sha, GraphQL::STRING_TYPE, null: false, + description: "The sha of the head at the time the comment was made" + field :base_sha, GraphQL::STRING_TYPE, null: true, + description: "The merge base of the branch the comment was made on" + field :start_sha, GraphQL::STRING_TYPE, null: false, + description: "The sha of the branch being compared against" + + field :file_path, GraphQL::STRING_TYPE, null: false, + description: "The path of the file that was changed" + field :old_path, GraphQL::STRING_TYPE, null: true, + description: "The path of the file on the start sha." + field :new_path, GraphQL::STRING_TYPE, null: true, + description: "The path of the file on the head sha." + field :position_type, Types::Notes::PositionTypeEnum, null: false + + # Fields for text positions + field :old_line, GraphQL::INT_TYPE, null: true, + description: "The line on start sha that was changed", + resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? } + field :new_line, GraphQL::INT_TYPE, null: true, + description: "The line on head sha that was changed", + resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? } + + # Fields for image positions + field :x, GraphQL::INT_TYPE, null: true, + description: "The X postion on which the comment was made", + resolve: -> (position, _args, _ctx) { position.x if position.on_image? } + field :y, GraphQL::INT_TYPE, null: true, + description: "The Y position on which the comment was made", + resolve: -> (position, _args, _ctx) { position.y if position.on_image? } + field :width, GraphQL::INT_TYPE, null: true, + description: "The total width of the image", + resolve: -> (position, _args, _ctx) { position.width if position.on_image? } + field :height, GraphQL::INT_TYPE, null: true, + description: "The total height of the image", + resolve: -> (position, _args, _ctx) { position.height if position.on_image? } + end + end +end diff --git a/app/graphql/types/notes/discussion_type.rb b/app/graphql/types/notes/discussion_type.rb new file mode 100644 index 00000000000..c4691942f2d --- /dev/null +++ b/app/graphql/types/notes/discussion_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Notes + class DiscussionType < BaseObject + graphql_name 'Discussion' + + authorize :read_note + + field :id, GraphQL::ID_TYPE, null: false + field :created_at, Types::TimeType, null: false + field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes in the discussion" + end + end +end diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb new file mode 100644 index 00000000000..85c55d16ac2 --- /dev/null +++ b/app/graphql/types/notes/note_type.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Types + module Notes + class NoteType < BaseObject + graphql_name 'Note' + + authorize :read_note + + expose_permissions Types::PermissionTypes::Note + + field :id, GraphQL::ID_TYPE, null: false + + field :project, Types::ProjectType, + null: true, + description: "The project this note is associated to", + resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find } + + field :author, Types::UserType, + null: false, + description: "The user who wrote this note", + resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find } + + field :resolved_by, Types::UserType, + null: true, + description: "The user that resolved the discussion", + resolve: -> (note, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.resolved_by_id).find } + + field :system, GraphQL::BOOLEAN_TYPE, + null: false, + description: "Whether or not this note was created by the system or by a user" + + field :body, GraphQL::STRING_TYPE, + null: false, + method: :note, + description: "The content note itself" + + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + field :discussion, Types::Notes::DiscussionType, null: true, description: "The discussion this note is a part of" + field :resolvable, GraphQL::BOOLEAN_TYPE, null: false, method: :resolvable? + field :resolved_at, Types::TimeType, null: true, description: "The time the discussion was resolved" + field :position, Types::Notes::DiffPositionType, null: true, description: "The position of this note on a diff" + end + end +end diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb new file mode 100644 index 00000000000..9f126d67b0d --- /dev/null +++ b/app/graphql/types/notes/noteable_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Notes + module NoteableType + include Types::BaseInterface + + field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable" + field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable" + + definition_methods do + def resolve_type(object, context) + case object + when Issue + Types::IssueType + when MergeRequest + Types::MergeRequestType + else + raise "Unknown GraphQL type for #{object}" + end + end + end + end + end +end diff --git a/app/graphql/types/notes/position_type_enum.rb b/app/graphql/types/notes/position_type_enum.rb new file mode 100644 index 00000000000..abdb2cfc804 --- /dev/null +++ b/app/graphql/types/notes/position_type_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Notes + class PositionTypeEnum < BaseEnum + graphql_name 'DiffPositionType' + description 'Type of file the position refers to' + + value 'text' + value 'image' + end + end +end diff --git a/app/graphql/types/permission_types/note.rb b/app/graphql/types/permission_types/note.rb new file mode 100644 index 00000000000..a585d3daaa8 --- /dev/null +++ b/app/graphql/types/permission_types/note.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Note < BasePermissionType + graphql_name 'NotePermissions' + + abilities :read_note, :create_note, :admin_note, :resolve_note, :award_emoji + end + end +end diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb index 62537361918..4000c6db280 100644 --- a/app/graphql/types/project_statistics_type.rb +++ b/app/graphql/types/project_statistics_type.rb @@ -4,6 +4,8 @@ module Types class ProjectStatisticsType < BaseObject graphql_name 'ProjectStatistics' + authorize :read_statistics + field :commit_count, GraphQL::INT_TYPE, null: false field :storage_size, GraphQL::INT_TYPE, null: false diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 2236ffa394d..81914b70c7f 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -70,7 +70,7 @@ module Types field :group, Types::GroupType, null: true field :statistics, Types::ProjectStatisticsType, - null: false, + null: true, resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find } field :repository, Types::RepositoryType, null: false diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb new file mode 100644 index 00000000000..c289802509d --- /dev/null +++ b/app/graphql/types/task_completion_status.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class TaskCompletionStatus < BaseObject + graphql_name 'TaskCompletionStatus' + description 'Completion status of tasks' + + field :count, GraphQL::INT_TYPE, null: false + field :completed_count, GraphQL::INT_TYPE, null: false + end +end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 8d8c62f1291..64c5fae7d96 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -91,7 +91,7 @@ module DropdownsHelper def dropdown_filter(placeholder, search_id: nil) content_tag :div, class: "dropdown-input" do - filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off' + filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off' filter_output << icon('search', class: "dropdown-input-search") filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 855b243cc8a..0f118c235d8 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -27,7 +27,7 @@ module EnvironmentsHelper "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'), "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json), "dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json), - "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json), + "deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json), "environments-endpoint": project_environments_path(project, format: :json), "project-path" => project_path(project), "tags-path" => project_tags_path(project), diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index dce4168ad7b..bf894360a2e 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -263,6 +263,11 @@ module MarkupHelper end def asciidoc_unsafe(text, context = {}) + context.merge!( + commit: @commit, + ref: @ref, + requested_path: @path + ) Gitlab::Asciidoc.render(text, context) end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 4594f5a31b9..dfa34ad7020 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -172,11 +172,17 @@ module SearchHelper if @project.present? opts[:data]['project-id'] = @project.id opts[:data]['base-endpoint'] = project_path(@project) + opts[:data]['labels-endpoint'] = project_labels_path(@project) + opts[:data]['milestones-endpoint'] = project_milestones_path(@project) elsif @group.present? opts[:data]['group-id'] = @group.id opts[:data]['base-endpoint'] = group_canonical_path(@group) + opts[:data]['labels-endpoint'] = group_labels_path(@group) + opts[:data]['milestones-endpoint'] = group_milestones_path(@group) else opts[:data]['base-endpoint'] = root_dashboard_path + opts[:data]['labels-endpoint'] = dashboard_labels_path + opts[:data]['milestones-endpoint'] = dashboard_milestones_path end opts diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index 5d658d35107..d5e459311f7 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -3,6 +3,7 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze + SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -20,6 +21,10 @@ module UserCalloutsHelper def render_dashboard_gold_trial(user) end + def show_suggest_popover? + !user_dismissed?(SUGGEST_POPOVER_DISMISSED) + end + private def user_dismissed?(feature_name) diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index a24d3476d0e..aa56ba1828b 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -15,7 +15,7 @@ class RepositoryCheckMailer < BaseMailer end mail( - to: User.admins.pluck(:email), + to: User.admins.active.pluck(:email), subject: "GitLab Admin | #{@message}" ) end diff --git a/app/models/commit.rb b/app/models/commit.rb index fa0bf36ba49..be37fa2e76f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -94,7 +94,7 @@ class Commit end def lazy(project, oid) - BatchLoader.for({ project: project, oid: oid }).batch do |items, loader| + BatchLoader.for({ project: project, oid: oid }).batch(replace_methods: false) do |items, loader| items_by_project = items.group_by { |i| i[:project] } items_by_project.each do |project, commit_ids| diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb index b61bf29e6ad..2d09eff0111 100644 --- a/app/models/concerns/diff_positionable_note.rb +++ b/app/models/concerns/diff_positionable_note.rb @@ -3,6 +3,7 @@ module DiffPositionableNote extend ActiveSupport::Concern included do + delegate :on_text?, :on_image?, to: :position, allow_nil: true before_validation :set_original_position, on: :create before_validation :update_position, on: :create, if: :on_text? @@ -28,14 +29,6 @@ module DiffPositionableNote end end - def on_text? - position&.position_type == "text" - end - - def on_image? - position&.position_type == "image" - end - def supported? for_commit? || self.noteable.has_complete_diff_refs? end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 32529ebf71d..ae13cdfd85f 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -4,6 +4,7 @@ # # A discussion of this type can be resolvable. class Discussion + include GlobalID::Identification include ResolvableDiscussion attr_reader :notes, :context_noteable @@ -11,14 +12,19 @@ class Discussion delegate :created_at, :project, :author, - :noteable, :commit_id, :for_commit?, :for_merge_request?, + :to_ability_name, + :editable?, to: :first_note + def declarative_policy_delegate + first_note + end + def project_id project&.id end diff --git a/app/models/group.rb b/app/models/group.rb index cdb4e6e87f6..dbec211935d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -423,7 +423,7 @@ class Group < Namespace def update_two_factor_requirement return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period? - users.find_each(&:update_two_factor_requirement) + members_with_descendants.find_each(&:update_two_factor_requirement) end def path_changed_hook diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 5245dbc8d15..79a376ff0fd 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -5,7 +5,7 @@ class LfsObject < ApplicationRecord include ObjectStorage::BackgroundMove has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :projects, through: :lfs_objects_projects + has_many :projects, -> { distinct }, through: :lfs_objects_projects scope :with_files_stored_locally, -> { where(file_store: LfsObjectUploader::Store::LOCAL) } diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index f9afb18c1d7..e45c56b6394 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -5,11 +5,17 @@ class LfsObjectsProject < ApplicationRecord belongs_to :lfs_object validates :lfs_object_id, presence: true - validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } + validates :lfs_object_id, uniqueness: { scope: [:project_id, :repository_type], message: "already exists in repository" } validates :project_id, presence: true after_commit :update_project_statistics, on: [:create, :destroy] + enum repository_type: { + project: 0, + wiki: 1, + design: 2 ## EE-specific + } + private def update_project_statistics diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7481f1939ad..f07636e8f77 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -668,7 +668,7 @@ class MergeRequest < ApplicationRecord # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435 Gitlab::GitalyClient.allow_n_plus_1_calls do - merge_request_diffs.create + merge_request_diffs.create! reload_merge_request_diff end end diff --git a/app/models/note.rb b/app/models/note.rb index 081d6f91230..15271c68a9e 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -342,7 +342,7 @@ class Note < ApplicationRecord end def to_ability_name - for_snippet? ? noteable.class.name.underscore : noteable_type.underscore + for_snippet? ? noteable.class.name.underscore : noteable_type.demodulize.underscore end def can_be_discussion_note? diff --git a/app/models/project.rb b/app/models/project.rb index 9d17d68eee2..351d08eaf63 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -72,7 +72,6 @@ class Project < ApplicationRecord delegate :no_import?, to: :import_state, allow_nil: true default_value_for :archived, false - default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility } default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } @@ -223,7 +222,7 @@ class Project < ApplicationRecord has_many :starrers, through: :users_star_projects, source: :user has_many :releases has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :lfs_objects, through: :lfs_objects_projects + has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects has_many :lfs_file_locks has_many :project_group_links has_many :invited_groups, through: :project_group_links, source: :group @@ -613,6 +612,23 @@ class Project < ApplicationRecord end end + def initialize(attributes = {}) + # We can't use default_value_for because the database has a default + # value of 0 for visibility_level. If someone attempts to create a + # private project, default_value_for will assume that the + # visibility_level hasn't changed and will use the application + # setting default, which could be internal or public. For projects + # inside a private group, those levels are invalid. + # + # To fix the problem, we assign the actual default in the application if + # no explicit visibility has been initialized. + unless visibility_attribute_present?(attributes) + attributes[:visibility_level] = Gitlab::CurrentSettings.default_project_visibility + end + + super + end + def all_pipelines if builds_enabled? super diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 6bcb051bff6..0542581c6e0 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -72,8 +72,6 @@ class ProjectFeature < ApplicationRecord default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false - scope :for_project_id, -> (project) { where(project: project) } - def feature_available?(feature, user) # This feature might not be behind a feature flag at all, so default to true return false unless ::Feature.enabled?(feature, user, default_enabled: true) diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb new file mode 100644 index 00000000000..438d85098c8 --- /dev/null +++ b/app/models/project_services/data_fields.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module DataFields + extend ActiveSupport::Concern + + included do + has_one :issue_tracker_data + has_one :jira_tracker_data + end +end diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb new file mode 100644 index 00000000000..2c1d28ed421 --- /dev/null +++ b/app/models/project_services/issue_tracker_data.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class IssueTrackerData < ApplicationRecord + belongs_to :service + + delegate :activated?, to: :service, allow_nil: true + + validates :service, presence: true + validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated? + validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated? + + def self.encryption_options + { + key: Settings.attr_encrypted_db_key_base_32, + encode: true, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm' + } + end + + attr_encrypted :project_url, encryption_options + attr_encrypted :issues_url, encryption_options + attr_encrypted :new_issue_url, encryption_options +end diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb new file mode 100644 index 00000000000..4f528e3d81b --- /dev/null +++ b/app/models/project_services/jira_tracker_data.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class JiraTrackerData < ApplicationRecord + belongs_to :service + + delegate :activated?, to: :service, allow_nil: true + + validates :service, presence: true + validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated? + validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true + validates :username, presence: true, if: :activated? + validates :password, presence: true, if: :activated? + validates :jira_issue_transition_id, + format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") }, + allow_blank: true + + def self.encryption_options + { + key: Settings.attr_encrypted_db_key_base_32, + encode: true, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm' + } + end + + attr_encrypted :url, encryption_options + attr_encrypted :api_url, encryption_options + attr_encrypted :username, encryption_options + attr_encrypted :password, encryption_options +end diff --git a/app/models/service.rb b/app/models/service.rb index 9896aa12e90..40033003f3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -6,6 +6,7 @@ class Service < ApplicationRecord include Sortable include Importable include ProjectServicesLoggable + include DataFields serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize @@ -119,7 +120,7 @@ class Service < ApplicationRecord end def self.event_names - self.supported_events.map { |event| "#{event}_events" } + self.supported_events.map { |event| ServicesHelper.service_event_field_name(event) } end def event_field(event) @@ -151,7 +152,7 @@ class Service < ApplicationRecord end def self.supported_events - %w(push tag_push issue confidential_issue merge_request wiki_page) + %w(commit push tag_push issue confidential_issue merge_request wiki_page) end def execute(data) diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb index b9373ae6166..7b68e5076c7 100644 --- a/app/models/user_callout_enums.rb +++ b/app/models/user_callout_enums.rb @@ -6,11 +6,15 @@ module UserCalloutEnums # # This method is separate from the `UserCallout` model so that it can be # extended by EE. + # + # If you are going to add new items to this hash, check that you're not going + # to conflict with EE-only values: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/ee/user_callout_enums.rb def self.feature_names { gke_cluster_integration: 1, gcp_signup_offer: 2, - cluster_security_warning: 3 + cluster_security_warning: 3, + suggest_popover_dismissed: 9 } end end diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 5dd2279ef99..82bf9bf8bf6 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -7,6 +7,10 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:admin) { @user&.admin? } + desc "User is blocked" + with_options scope: :user, score: 0 + condition(:blocked) { @user&.blocked? } + desc "User has access to all private groups & projects" with_options scope: :user, score: 0 condition(:full_private_access) { @user&.full_private_access? } diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index e85397422e6..134de1c9ace 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class GlobalPolicy < BasePolicy - desc "User is blocked" - with_options scope: :user, score: 0 - condition(:blocked) { @user&.blocked? } - desc "User is an internal user" with_options scope: :user, score: 0 condition(:internal) { @user&.internal? } diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 728a3040227..b3e29e775fc 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -258,6 +258,7 @@ class ProjectPolicy < BasePolicy enable :resolve_note enable :create_container_image enable :update_container_image + enable :destroy_container_image enable :create_environment enable :create_deployment enable :create_release @@ -446,6 +447,10 @@ class ProjectPolicy < BasePolicy prevent :owner_access end + rule { blocked }.policy do + prevent :create_pipeline + end + private def team_member? diff --git a/app/policies/project_statistics_policy.rb b/app/policies/project_statistics_policy.rb new file mode 100644 index 00000000000..c0592f1ea13 --- /dev/null +++ b/app/policies/project_statistics_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectStatisticsPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/serializers/board_serializer.rb b/app/serializers/board_serializer.rb new file mode 100644 index 00000000000..70a4c9ae282 --- /dev/null +++ b/app/serializers/board_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BoardSerializer < BaseSerializer + entity BoardSimpleEntity +end diff --git a/app/serializers/board_simple_entity.rb b/app/serializers/board_simple_entity.rb new file mode 100644 index 00000000000..f297d993e27 --- /dev/null +++ b/app/serializers/board_simple_entity.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BoardSimpleEntity < Grape::Entity + expose :id +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index ec2698ecbe3..9ef93b2387f 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -4,7 +4,6 @@ class PipelineEntity < Grape::Entity include RequestAwareEntity expose :id - expose :iid expose :user, using: UserEntity expose :active?, as: :active diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index fd5442a6c28..f2cd51ef4d0 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,7 @@ module Files class CreateService < Files::BaseService def create_commit! - transformer = Lfs::FileTransformer.new(project, @branch_name) + transformer = Lfs::FileTransformer.new(project, repository, @branch_name) result = transformer.new_file(@file_path, @file_content) diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index c1bc26c330a..d8c4e5bc5e8 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -5,7 +5,7 @@ module Files UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze def create_commit! - transformer = Lfs::FileTransformer.new(project, @branch_name) + transformer = Lfs::FileTransformer.new(project, repository, @branch_name) actions = actions_after_lfs_transformation(transformer, params[:actions]) actions = transform_move_actions(actions) diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index d21a6bb1b9a..4aee48f22e7 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -20,8 +20,7 @@ module Git strong_memoize(:commits) do if creating_default_branch? # The most recent PROCESS_COMMIT_LIMIT commits in the default branch - offset = [count_commits_in_branch - PROCESS_COMMIT_LIMIT, 0].max - project.repository.commits(params[:newrev], offset: offset, limit: PROCESS_COMMIT_LIMIT) + project.repository.commits(params[:newrev], limit: PROCESS_COMMIT_LIMIT) elsif creating_branch? # Use the pushed commits that aren't reachable by the default branch # as a heuristic. This may include more commits than are actually @@ -84,9 +83,6 @@ module Git # Schedules processing of commit messages def enqueue_process_commit_messages - # don't process commits for the initial push to the default branch - return if creating_default_branch? - limited_commits.each do |commit| next unless commit.matches_cross_reference_regex? diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb index 5239fe1b6e3..d1746399908 100644 --- a/app/services/lfs/file_transformer.rb +++ b/app/services/lfs/file_transformer.rb @@ -8,17 +8,17 @@ module Lfs # pointer returned. If the file isn't in LFS the untransformed content # is returned to save in the commit. # - # transformer = Lfs::FileTransformer.new(project, @branch_name) + # transformer = Lfs::FileTransformer.new(project, repository, @branch_name) # content_or_lfs_pointer = transformer.new_file(file_path, content).content # create_transformed_commit(content_or_lfs_pointer) # class FileTransformer - attr_reader :project, :branch_name + attr_reader :project, :repository, :repository_type, :branch_name - delegate :repository, to: :project - - def initialize(project, branch_name) + def initialize(project, repository, branch_name) @project = project + @repository = repository + @repository_type = repository.repo_type.name @branch_name = branch_name end @@ -64,7 +64,11 @@ module Lfs # rubocop: enable CodeReuse/ActiveRecord def link_lfs_object!(lfs_object) - project.lfs_objects << lfs_object + LfsObjectsProject.safe_find_or_create_by!( + project: project, + lfs_object: lfs_object, + repository_type: repository_type + ) end def parse_file_content(file_content, encoding: nil) diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 302510341ac..e0cbfac2420 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -52,10 +52,6 @@ class SearchService @search_objects ||= search_results.objects(scope, params[:page]) end - def display_options - @display_options ||= search_results.display_options(scope) - end - private def search_service diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml index db24c9982f7..a2aa1687f80 100644 --- a/app/views/admin/application_settings/ci_cd.html.haml +++ b/app/views/admin/application_settings/ci_cd.html.haml @@ -13,6 +13,8 @@ .settings-content = render 'ci_cd' += render_if_exists 'admin/application_settings/required_instance_ci_setting', expanded: expanded_by_default? + - if Gitlab.config.registry.enabled %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded_by_default?) } .settings-header diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 8745a4e9d3e..4cd03be572f 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -3,4 +3,4 @@ %a.nav-link.qa-sign-in-tab.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if allow_signup? %li.nav-item{ role: 'presentation' } - %a.nav-link.qa-register-tab{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register + %a.nav-link.qa-register-tab{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: 'sign_in', track_event: 'click_button', track_value: 'register', toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 0a14830c666..0da1f1ba7f5 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -17,6 +17,7 @@ %br %span.descr.text-muted= share_with_group_lock_help_text(@group) + = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/two_factor_auth', f: f diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index c357207054b..7535aee83a3 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -78,3 +78,4 @@ = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render_if_exists 'layouts/snowplow' + = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test? diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 9b6551552c7..49ff976f8e8 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -9,7 +9,7 @@ = @project.name %ul.sidebar-top-level-items = nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do - = link_to project_path(@project), class: 'shortcuts-project' do + = link_to project_path(@project), class: 'shortcuts-project qa-link-project' do .nav-icon-container = sprite_icon('home') %span.nav-item-name diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index bdf7b933ab8..f4560404c03 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -53,10 +53,9 @@ %span.badge.badge-info= _('manual') - if pipeline_link - %td.pipeline-link - = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do + %td + = link_to pipeline_path(pipeline) do %span.pipeline-id ##{pipeline.id} - %span.pipeline-iid (##{pipeline.iid}) %span by - if pipeline.user = user_avatar(user: pipeline.user, size: 20) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 77ea2c04b28..a766dd51463 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -81,7 +81,7 @@ = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do = ci_icon_for_status(last_pipeline.status) #{ _('Pipeline') } - = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)') + = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id) = ci_label_for_status(last_pipeline.status) - if last_pipeline.stages_count.nonzero? #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index a201fafb949..f593f4e049e 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -80,7 +80,9 @@ current_user_data: UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json, project_path: project_path(@merge_request.project), changes_empty_state_illustration: image_path('illustrations/merge_request_changes_empty.svg'), - is_fluid_layout: fluid_layout.to_s } } + is_fluid_layout: fluid_layout.to_s, + dismiss_endpoint: user_callouts_path, + show_suggest_popover: show_suggest_popover?.to_s } } .mr-loading-status = spinner diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 12eb8d7fa81..cb8a8a24be8 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -21,7 +21,7 @@ .search-results - if @scope == 'projects' .term - = render 'shared/projects/list', { projects: @search_objects, pipeline_status: false }.merge(@display_options) + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - else - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope) = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml index d90a6d43761..d499bc0a253 100644 --- a/app/views/shared/_sidebar_toggle_button.html.haml +++ b/app/views/shared/_sidebar_toggle_button.html.haml @@ -1,4 +1,4 @@ -%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" } +%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" } = sprite_icon('angle-double-left', css_class: 'icon-angle-double-left') = sprite_icon('angle-double-right', css_class: 'icon-angle-double-right') %span.collapse-text= _("Collapse sidebar") diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 3a5adb34ad1..e87e560266f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -102,7 +102,7 @@ = _('Labels') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right' .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label_hash| @@ -118,7 +118,7 @@ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') - .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height + .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height = render partial: "shared/issuable/label_page_default" - if issuable_sidebar.dig(:current_user, :can_admin_label) = render partial: "shared/issuable/label_page_create" diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml index 7c7c0a363ac..4365e3f6877 100644 --- a/app/views/shared/projects/_search_form.html.haml +++ b/app/views/shared/projects/_search_form.html.haml @@ -1,7 +1,7 @@ - form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : '' - placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...' -= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| += form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f| = search_field_tag :name, params[:name], placeholder: placeholder, class: "project-filter-form-field form-control #{form_field_classes}", |