diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-04 12:09:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-04 12:09:25 +0000 |
commit | f1500a385abba1f03c0eadc3c6a95ea532030b4c (patch) | |
tree | 75888a5073d3a09a9181c3b61987f2787a68ef92 /app | |
parent | 72241c5e0a5ced1416fcf69ed1a6d8f57b9bf3e2 (diff) | |
download | gitlab-ce-f1500a385abba1f03c0eadc3c6a95ea532030b4c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
46 files changed, 526 insertions, 236 deletions
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js index 6ce5ebda241..bd7909bfa76 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js @@ -67,13 +67,23 @@ export const publishReview = ({ commit, dispatch, getters }) => { .catch(() => commit(types.RECEIVE_PUBLISH_REVIEW_ERROR)); }; -export const updateDiscussionsAfterPublish = ({ dispatch, getters, rootGetters }) => - dispatch('fetchDiscussions', { path: getters.getNotesData.discussionsPath }, { root: true }).then( - () => - dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, { - root: true, - }), - ); +export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => { + if (window.gon?.features?.paginatedNotes) { + await dispatch('stopPolling', null, { root: true }); + await dispatch('fetchData', null, { root: true }); + await dispatch('restartPolling', null, { root: true }); + } else { + await dispatch( + 'fetchDiscussions', + { path: getters.getNotesData.discussionsPath }, + { root: true }, + ); + } + + dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, { + root: true, + }); +}; export const updateDraft = ( { commit, getters }, diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 62454483ca1..c0468e5df0f 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -4,6 +4,7 @@ import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import { __ } from '~/locale'; import initUserPopovers from '~/user_popovers'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import { deprecatedCreateFlash as Flash } from '../../flash'; import * as constants from '../constants'; @@ -30,6 +31,7 @@ export default { discussionFilterNote, OrderedLayout, }, + mixins: [glFeatureFlagsMixin()], props: { noteableData: { type: Object, @@ -57,7 +59,6 @@ export default { }, data() { return { - isFetching: false, currentFilter: null, }; }, @@ -68,6 +69,7 @@ export default { 'convertedDisscussionIds', 'getNotesDataByProp', 'isLoading', + 'isFetching', 'commentsDisabled', 'getNoteableData', 'userCanReply', @@ -103,6 +105,13 @@ export default { }, }, watch: { + async isFetching() { + if (!this.isFetching) { + await this.$nextTick(); + await this.startTaskList(); + await this.checkLocationHash(); + } + }, shouldShow() { if (!this.isNotesFetched) { this.fetchNotes(); @@ -153,6 +162,7 @@ export default { }, methods: { ...mapActions([ + 'setFetchingState', 'setLoadingState', 'fetchDiscussions', 'poll', @@ -183,7 +193,11 @@ export default { fetchNotes() { if (this.isFetching) return null; - this.isFetching = true; + this.setFetchingState(true); + + if (this.glFeatures.paginatedNotes) { + return this.initPolling(); + } return this.fetchDiscussions(this.getFetchDiscussionsConfig()) .then(this.initPolling) @@ -191,11 +205,8 @@ export default { this.setLoadingState(false); this.setNotesFetchedState(true); eventHub.$emit('fetchedNotesData'); - this.isFetching = false; + this.setFetchingState(false); }) - .then(this.$nextTick) - .then(this.startTaskList) - .then(this.checkLocationHash) .catch(() => { this.setLoadingState(false); this.setNotesFetchedState(true); diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 057c97d3d1e..ddc6c44a4e5 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -15,6 +15,7 @@ import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import { mergeUrlParams } from '../../lib/utils/url_utility'; +import eventHub from '../event_hub'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import * as utils from './utils'; import * as types from './mutation_types'; @@ -420,14 +421,25 @@ export const saveNote = ({ commit, dispatch }, noteData) => { .catch(processErrors); }; -const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { +export const setFetchingState = ({ commit }, fetchingState) => + commit(types.SET_NOTES_FETCHING_STATE, fetchingState); + +const pollSuccessCallBack = async (resp, commit, state, getters, dispatch) => { if (state.isResolvingDiscussion) { return null; } + if (window.gon?.features?.paginatedNotes && !resp.more && state.isFetching) { + eventHub.$emit('fetchedNotesData'); + dispatch('setFetchingState', false); + dispatch('setNotesFetchedState', true); + dispatch('setLoadingState', false); + } + if (resp.notes?.length) { - dispatch('updateOrCreateNotes', resp.notes); + await dispatch('updateOrCreateNotes', resp.notes); dispatch('startTaskList'); + dispatch('updateResolvableDiscussionsCounts'); } commit(types.SET_LAST_FETCHED_AT, resp.last_fetched_at); diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5891a2e63e3..43d99937b8d 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -48,6 +48,8 @@ export const persistSortOrder = (state) => state.persistSortOrder; export const timelineEnabled = (state) => state.isTimelineEnabled; +export const isFetching = (state) => state.isFetching; + export const isLoading = (state) => state.isLoading; export const getNotesDataByProp = (state) => (prop) => state.notesData[prop]; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 144a3d7ba90..c1738eb20da 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -47,6 +47,7 @@ export default () => ({ unresolvedDiscussionsCount: 0, descriptionVersions: {}, isTimelineEnabled: false, + isFetching: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 5c4f62f4575..2e8b728e013 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; +export const SET_NOTES_FETCHING_STATE = 'SET_NOTES_FETCHING_STATE'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE'; export const DISABLE_COMMENTS = 'DISABLE_COMMENTS'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 4c8392f7220..536b47667c2 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -32,6 +32,20 @@ export default { } } + if (window.gon?.features?.paginatedNotes && note.base_discussion) { + if (discussion.diff_file) { + discussion.file_hash = discussion.diff_file.file_hash; + + discussion.truncated_diff_lines = utils.prepareDiffLines( + discussion.truncated_diff_lines || [], + ); + } + + discussion.resolvable = note.resolvable; + discussion.expanded = note.base_discussion.expanded; + discussion.resolved = note.resolved; + } + // note.base_discussion = undefined; // No point keeping a reference to this delete note.base_discussion; discussion.notes = [note]; @@ -323,6 +337,10 @@ export default { state.isLoading = value; }, + [types.SET_NOTES_FETCHING_STATE](state, value) { + state.isFetching = value; + }, + [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); diff --git a/app/assets/javascripts/pages/projects/compare/index/index.js b/app/assets/javascripts/pages/projects/compare/index/index.js new file mode 100644 index 00000000000..b86c9ec442f --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/index/index.js @@ -0,0 +1,3 @@ +import initCompareSelector from '~/projects/compare'; + +initCompareSelector(); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 8a7dbf890ab..d81c11ddbaf 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -224,11 +224,11 @@ export default { repositoryHelpText() { if (this.visibilityLevel === visibilityOptions.PRIVATE) { - return s__('ProjectSettings|View and edit files in this project'); + return s__('ProjectSettings|View and edit files in this project.'); } return s__( - 'ProjectSettings|View and edit files in this project. Non-project members will only have read access', + 'ProjectSettings|View and edit files in this project. Non-project members will only have read access.', ); }, }, @@ -400,7 +400,7 @@ export default { name="project[request_access_enabled]" /> <input v-model="requestAccessEnabled" type="checkbox" /> - {{ s__('ProjectSettings|Allow users to request access') }} + {{ s__('ProjectSettings|Users can request access') }} </label> </project-setting-row> </div> @@ -411,7 +411,7 @@ export default { <project-setting-row ref="issues-settings" :label="s__('ProjectSettings|Issues')" - :help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')" + :help-text="s__('ProjectSettings|Lightweight issue tracking system.')" > <project-feature-setting v-model="issuesAccessLevel" @@ -434,7 +434,7 @@ export default { <project-setting-row ref="merge-request-settings" :label="s__('ProjectSettings|Merge requests')" - :help-text="s__('ProjectSettings|Submit changes to be merged upstream')" + :help-text="s__('ProjectSettings|Submit changes to be merged upstream.')" > <project-feature-setting v-model="mergeRequestsAccessLevel" @@ -446,9 +446,7 @@ export default { <project-setting-row ref="fork-settings" :label="s__('ProjectSettings|Forks')" - :help-text=" - s__('ProjectSettings|Allow users to make copies of your repository to a new project') - " + :help-text="s__('ProjectSettings|Users can copy the repository to a new project.')" > <project-feature-setting v-model="forkingAccessLevel" @@ -460,7 +458,7 @@ export default { <project-setting-row ref="pipeline-settings" :label="s__('ProjectSettings|Pipelines')" - :help-text="s__('ProjectSettings|Build, test, and deploy your changes')" + :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')" > <project-feature-setting v-model="buildsAccessLevel" @@ -497,7 +495,7 @@ export default { :help-path="lfsHelpPath" :label="s__('ProjectSettings|Git Large File Storage (LFS)')" :help-text=" - s__('ProjectSettings|Manages large files such as audio, video, and graphics files') + s__('ProjectSettings|Manages large files such as audio, video, and graphics files.') " > <project-feature-toggle @@ -509,7 +507,7 @@ export default { <gl-sprintf :message=" s__( - 'ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}', + 'ProjectSettings|LFS objects from this repository are available to forks. %{linkStart}How do I remove them?%{linkEnd}', ) " > @@ -529,7 +527,7 @@ export default { :help-path="packagesHelpPath" :label="s__('ProjectSettings|Packages')" :help-text=" - s__('ProjectSettings|Every project can have its own space to store its packages') + s__('ProjectSettings|Every project can have its own space to store its packages.') " > <project-feature-toggle @@ -542,7 +540,7 @@ export default { <project-setting-row ref="analytics-settings" :label="s__('ProjectSettings|Analytics')" - :help-text="s__('ProjectSettings|View project analytics')" + :help-text="s__('ProjectSettings|View project analytics.')" > <project-feature-setting v-model="analyticsAccessLevel" @@ -554,7 +552,7 @@ export default { v-if="requirementsAvailable" ref="requirements-settings" :label="s__('ProjectSettings|Requirements')" - :help-text="s__('ProjectSettings|Requirements management system for this project')" + :help-text="s__('ProjectSettings|Requirements management system.')" > <project-feature-setting v-model="requirementsAccessLevel" @@ -576,7 +574,7 @@ export default { <project-setting-row ref="wiki-settings" :label="s__('ProjectSettings|Wiki')" - :help-text="s__('ProjectSettings|Pages for project documentation')" + :help-text="s__('ProjectSettings|Pages for project documentation.')" > <project-feature-setting v-model="wikiAccessLevel" @@ -587,7 +585,7 @@ export default { <project-setting-row ref="snippet-settings" :label="s__('ProjectSettings|Snippets')" - :help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')" + :help-text="s__('ProjectSettings|Share code with others outside the project.')" > <project-feature-setting v-model="snippetsAccessLevel" @@ -601,7 +599,7 @@ export default { :help-path="pagesHelpPath" :label="s__('ProjectSettings|Pages')" :help-text=" - s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab') + s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.') " > <project-feature-setting @@ -613,7 +611,7 @@ export default { <project-setting-row ref="operations-settings" :label="s__('ProjectSettings|Operations')" - :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more')" + :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more.')" > <project-feature-setting v-model="operationsAccessLevel" @@ -625,11 +623,7 @@ export default { <project-setting-row ref="metrics-visibility-settings" :label="__('Metrics Dashboard')" - :help-text=" - s__( - 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics', - ) - " + :help-text="s__('ProjectSettings|Visualize the project\'s performance metrics.')" > <project-feature-setting v-model="metricsDashboardAccessLevel" @@ -647,9 +641,7 @@ export default { {{ s__('ProjectSettings|Disable email notifications') }} </label> <span class="form-text text-muted">{{ - s__( - 'ProjectSettings|This setting will override user notification preferences for all project members.', - ) + s__('ProjectSettings|Override user notification preferences for all project members.') }}</span> </project-setting-row> <project-setting-row class="mb-3"> @@ -665,7 +657,7 @@ export default { {{ s__('ProjectSettings|Show default award emojis') }} <template #help>{{ s__( - 'ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons.', + 'ProjectSettings|Always show thumbs-up and thumbs-down award emoji buttons on issues, merge requests, and snippets.', ) }}</template> </gl-form-checkbox> @@ -683,9 +675,7 @@ export default { <gl-form-checkbox v-model="allowEditingCommitMessages"> {{ s__('ProjectSettings|Allow editing commit messages') }} <template #help>{{ - s__( - 'ProjectSettings|When enabled, commit authors will be able to edit commit messages on unprotected branches.', - ) + s__('ProjectSettings|Commit authors can edit commit messages on unprotected branches.') }}</template> </gl-form-checkbox> </project-setting-row> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue index 8021ff5a585..13f314a3a45 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -1,7 +1,7 @@ <script> -import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; -import { deprecatedCreateFlash as flash } from '~/flash'; +import createFlash from '~/flash'; import { s__, __, sprintf } from '~/locale'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import eventHub from '../../event_hub'; @@ -11,10 +11,10 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - GlIcon, GlCountdown, - GlButton, - GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlIcon, }, props: { actions: { @@ -61,7 +61,7 @@ export default { }) .catch(() => { this.isLoading = false; - flash(__('An error occurred while making the request.')); + createFlash({ message: __('An error occurred while making the request.') }); }); }, @@ -76,39 +76,27 @@ export default { }; </script> <template> - <div class="btn-group"> - <button - v-gl-tooltip - type="button" - :disabled="isLoading" - class="dropdown-new gl-button btn btn-default js-pipeline-dropdown-manual-actions" - :title="__('Run manual or delayed jobs')" - data-toggle="dropdown" - :aria-label="__('Run manual or delayed jobs')" + <gl-dropdown + v-gl-tooltip + :title="__('Run manual or delayed jobs')" + :loading="isLoading" + data-testid="pipelines-manual-actions-dropdown" + right + icon="play" + > + <gl-dropdown-item + v-for="action in actions" + :key="action.path" + :disabled="isActionDisabled(action)" + @click="onClickAction(action)" > - <gl-icon name="play" class="icon-play" /> - <gl-icon name="chevron-down" /> - <gl-loading-icon v-if="isLoading" /> - </button> - - <ul class="dropdown-menu dropdown-menu-right"> - <li v-for="action in actions" :key="action.path"> - <gl-button - category="tertiary" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" - class="js-pipeline-action-link" - @click="onClickAction(action)" - > - <div class="d-flex justify-content-between flex-wrap"> - {{ action.name }} - <span v-if="action.scheduled_at"> - <gl-icon name="clock" /> - <gl-countdown :end-date-string="action.scheduled_at" /> - </span> - </div> - </gl-button> - </li> - </ul> - </div> + <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"> + {{ action.name }} + <span v-if="action.scheduled_at"> + <gl-icon name="clock" /> + <gl-countdown :end-date-string="action.scheduled_at" /> + </span> + </div> + </gl-dropdown-item> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue new file mode 100644 index 00000000000..05bd0f1370b --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -0,0 +1,89 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import RevisionDropdown from './revision_dropdown.vue'; + +export default { + csrf, + components: { + RevisionDropdown, + GlButton, + }, + props: { + projectCompareIndexPath: { + type: String, + required: true, + }, + refsProjectPath: { + type: String, + required: true, + }, + paramsFrom: { + type: String, + required: false, + default: null, + }, + paramsTo: { + type: String, + required: false, + default: null, + }, + projectMergeRequestPath: { + type: String, + required: true, + }, + createMrPath: { + type: String, + required: true, + }, + }, + methods: { + onSubmit() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <form + ref="form" + class="form-inline js-requires-input js-signature-container" + method="POST" + :action="projectCompareIndexPath" + > + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Source" + params-name="to" + :params-branch="paramsTo" + /> + <div class="compare-ellipsis gl-display-inline" data-testid="ellipsis">...</div> + <revision-dropdown + :refs-project-path="refsProjectPath" + revision-text="Target" + params-name="from" + :params-branch="paramsFrom" + /> + <gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit"> + {{ s__('CompareRevisions|Compare') }} + </gl-button> + <a + v-if="projectMergeRequestPath" + :href="projectMergeRequestPath" + data-testid="projectMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|View open merge request') }} + </a> + <a + v-else-if="createMrPath" + :href="createMrPath" + data-testid="createMrButton" + class="btn btn-default gl-button gl-ml-3" + > + {{ s__('CompareRevisions|Create merge request') }} + </a> + </form> +</template> diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue new file mode 100644 index 00000000000..f657f36322d --- /dev/null +++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue @@ -0,0 +1,145 @@ +<script> +import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + }, + props: { + refsProjectPath: { + type: String, + required: true, + }, + revisionText: { + type: String, + required: true, + }, + paramsName: { + type: String, + required: true, + }, + paramsBranch: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + branches: [], + tags: [], + loading: true, + searchTerm: '', + selectedRevision: this.getDefaultBranch(), + }; + }, + computed: { + filteredBranches() { + return this.branches.filter((branch) => + branch.toLowerCase().includes(this.searchTerm.toLowerCase()), + ); + }, + hasFilteredBranches() { + return this.filteredBranches.length; + }, + filteredTags() { + return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase())); + }, + hasFilteredTags() { + return this.filteredTags.length; + }, + }, + mounted() { + this.fetchBranchesAndTags(); + }, + methods: { + fetchBranchesAndTags() { + const endpoint = this.refsProjectPath; + + return axios + .get(endpoint) + .then(({ data }) => { + this.branches = data.Branches; + this.tags = data.Tags; + }) + .catch(() => { + createFlash({ + message: `${s__( + 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.', + )}`, + }); + }) + .finally(() => { + this.loading = false; + }); + }, + getDefaultBranch() { + return this.paramsBranch || s__('CompareRevisions|Select branch/tag'); + }, + onClick(revision) { + this.selectedRevision = revision; + }, + onSearchEnter() { + this.selectedRevision = this.searchTerm; + }, + }, +}; +</script> + +<template> + <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`"> + <div class="input-group inline-input-group"> + <span class="input-group-prepend"> + <div class="input-group-text"> + {{ revisionText }} + </div> + </span> + <input type="hidden" :name="paramsName" :value="selectedRevision" /> + <gl-dropdown + class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace" + toggle-class="form-control compare-dropdown-toggle js-compare-dropdown gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!" + :text="selectedRevision" + header-text="Select Git revision" + :loading="loading" + > + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :placeholder="s__('CompareRevisions|Filter by Git revision')" + @keyup.enter="onSearchEnter" + /> + </template> + <gl-dropdown-section-header v-if="hasFilteredBranches"> + {{ s__('CompareRevisions|Branches') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(branch, index) in filteredBranches" + :key="`branch${index}`" + is-check-item + :is-checked="selectedRevision === branch" + @click="onClick(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-section-header v-if="hasFilteredTags"> + {{ s__('CompareRevisions|Tags') }} + </gl-dropdown-section-header> + <gl-dropdown-item + v-for="(tag, index) in filteredTags" + :key="`tag${index}`" + is-check-item + :is-checked="selectedRevision === tag" + @click="onClick(tag)" + > + {{ tag }} + </gl-dropdown-item> + </gl-dropdown> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js new file mode 100644 index 00000000000..4337eecb667 --- /dev/null +++ b/app/assets/javascripts/projects/compare/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import CompareApp from './components/app.vue'; + +export default function init() { + const el = document.getElementById('js-compare-selector'); + const { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + } = el.dataset; + + return new Vue({ + el, + components: { + CompareApp, + }, + render(createElement) { + return createElement(CompareApp, { + props: { + refsProjectPath, + paramsFrom, + paramsTo, + projectCompareIndexPath, + projectMergeRequestPath, + createMrPath, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index f46470418f8..951dc108c51 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -159,6 +159,7 @@ export default { .then((data) => { this.mr.setApprovals(data); eventHub.$emit('MRWidgetUpdateRequested'); + eventHub.$emit('ApprovalUpdated'); this.$emit('updated'); }) .catch(errFn) 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 3ff7d436c97..8084ad59f42 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 @@ -155,13 +155,14 @@ export default { > {{ $options.monitoringPipelineText }} <gl-link + v-gl-tooltip :href="ciTroubleshootingDocsPath" target="_blank" + :title="__('About this feature')" class="gl-display-flex gl-align-items-center gl-ml-2" > <gl-icon name="question" - :size="12" :aria-label="__('Link to go to GitLab pipeline documentation')" /> </gl-link> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 3e361ce487b..9c16bf78c93 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -4,6 +4,7 @@ import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merg import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { deprecatedCreateFlash as Flash } from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import MrWidgetAuthor from '../mr_widget_author.vue'; @@ -53,7 +54,11 @@ export default { }, computed: { loading() { - return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; + return ( + this.glFeatures.mergeRequestWidgetGraphql && + this.$apollo.queries.state.loading && + Object.keys(this.state).length === 0 + ); }, mergeUser() { if (this.glFeatures.mergeRequestWidgetGraphql) { @@ -78,7 +83,7 @@ export default { canRemoveSourceBranch() { const { currentUserId } = this.mr; const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql - ? this.state.mergeUser?.id + ? getIdFromGraphQLId(this.state.mergeUser?.id) : this.mr.mergeUserId; const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql ? this.state.userPermissions.removeSourceBranch @@ -96,7 +101,11 @@ export default { .cancelAutomaticMerge() .then((res) => res.data) .then((data) => { - eventHub.$emit('UpdateWidgetData', data); + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$emit('MRWidgetUpdateRequested'); + } else { + eventHub.$emit('UpdateWidgetData', data); + } }) .catch(() => { this.isCancellingAutoMerge = false; @@ -119,6 +128,11 @@ export default { eventHub.$emit('MRWidgetUpdateRequested'); } }) + .then(() => { + if (this.glFeatures.mergeRequestWidgetGraphql) { + this.$apollo.queries.state.refetch(); + } + }) .catch(() => { this.isRemovingSourceBranch = false; Flash(__('Something went wrong. Please try again.')); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index bf86e0d8b07..5127ab3d400 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -7,7 +7,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; -import rebaseQuery from '../../queries/states/ready_to_merge.query.graphql'; +import rebaseQuery from '../../queries/states/rebase.query.graphql'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import { deprecatedCreateFlash as Flash } from '../../../flash'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 89862dc09e6..58337ea8f67 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -53,8 +53,8 @@ export default { result({ data }) { this.state = { ...data.project.mergeRequest, - mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled, - onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds, + mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled, + onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds, }; this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch; this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage; @@ -277,7 +277,20 @@ export default { return this.mr.mergeRequestDiffsPath; }, }, + mounted() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$on('ApprovalUpdated', this.updateGraphqlState); + } + }, + beforeDestroy() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + eventHub.$off('ApprovalUpdated', this.updateGraphqlState); + } + }, methods: { + updateGraphqlState() { + return this.$apollo.queries.state.refetch(); + }, updateMergeCommitMessage(includeDescription) { const commitMessage = this.glFeatures.mergeRequestWidgetGraphql ? this.state.defaultMergeCommitMessage @@ -326,6 +339,10 @@ export default { } else if (hasError) { eventHub.$emit('FailedToMerge', data.merge_error); } + + if (this.glFeatures.mergeRequestWidgetGraphql) { + this.updateGraphqlState(); + } }) .catch(() => { this.isMakingRequest = false; @@ -532,7 +549,7 @@ export default { </div> <merge-train-helper-text v-if="shouldRenderMergeTrainHelperText" - :pipeline-id="pipeline.id" + :pipeline-id="pipelineId" :pipeline-link="pipeline.path" :merge-train-length="stateData.mergeTrainsCount" :merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath" diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index fe512d68ea2..23215982e6e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -35,5 +35,8 @@ export default { shouldRenderMergeTrainHelperText() { return false; }, + pipelineId() { + return this.pipeline.id; + }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 097dee52800..83370901cc3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -94,7 +94,6 @@ export default { state: { query: getStateQuery, manual: true, - pollInterval: 10 * 1000, skip() { return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql; }, @@ -286,6 +285,10 @@ export default { return new MRWidgetService(this.getServiceEndpoints(store)); }, checkStatus(cb, isRebased) { + if (window.gon?.features?.mergeRequestWidgetGraphql) { + this.$apollo.queries.state.refetch(); + } + return this.service .checkStatus() .then(({ data }) => { diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql index 44fc1cc7f23..b284bb23969 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -18,6 +18,7 @@ query getState($projectPath: ID!, $iid: String!) { } shouldBeRebased sourceBranchExists + state targetBranchExists userPermissions { canMerge diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql index 64cd70fcf42..ad715599eb1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql @@ -1,6 +1,7 @@ fragment autoMergeEnabled on MergeRequest { autoMergeStrategy mergeUser { + id name username webUrl diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql index bdcb7a8206b..daf21e75b3b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql @@ -4,7 +4,6 @@ query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { mergeRequest(iid: $iid) { ...autoMergeEnabled - mergeTrainsCount } } } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index a68989fdfed..78a17493d31 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -156,9 +156,9 @@ export default class MergeRequestStore { this.setState(); - mrEventHub.$emit('mr.state.updated', { - state: this.mergeRequestState, - }); + if (!window.gon?.features?.mergeRequestWidgetGraphql) { + this.emitUpdatedState(); + } } setGraphqlData(project) { @@ -182,7 +182,9 @@ export default class MergeRequestStore { this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha; this.shouldBeRebased = mergeRequest.shouldBeRebased; this.workInProgress = mergeRequest.workInProgress; + this.mergeRequestState = mergeRequest.state; + this.emitUpdatedState(); this.setState(); } @@ -208,6 +210,12 @@ export default class MergeRequestStore { } } + emitUpdatedState() { + mrEventHub.$emit('mr.state.updated', { + state: this.mergeRequestState, + }); + } + setPaths(data) { // Paths are set on the first load of the page and not auto-refreshed this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 1648c5e0a42..8251cdb9bbb 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -992,6 +992,20 @@ pre.light-well { width: auto; } } + + // Remove once gitlab/ui solution is implemented: + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1157 + // https://gitlab.com/gitlab-org/gitlab/-/issues/300405 + .gl-search-box-by-type-input { + width: 100%; + } + + // Remove once gitlab/ui solution is implemented + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158 + // https://gitlab.com/gitlab-org/gitlab/-/issues/300405 + .gl-new-dropdown-button-text { + @include str-truncated; + } } .clearable-input { diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index bfa7a30bc65..2cef43f19ab 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -31,9 +31,9 @@ module NotesActions # We know there's more data, so tell the frontend to poll again after 1ms set_polling_interval_header(interval: 1) if meta[:more] - # Only present an ETag for the empty response to ensure pagination works - # as expected - ::Gitlab::EtagCaching::Middleware.skip!(response) if notes.present? + # We might still want to investigate further adjusting ETag caching with paginated notes, but + # let's avoid ETag caching for now until we confirm the viability of paginated notes. + ::Gitlab::EtagCaching::Middleware.skip!(response) render json: meta.merge(notes: notes) end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 65d9b475d2a..88f59484cdd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -44,6 +44,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:codequality_mr_diff, @project) push_frontend_feature_flag(:suggestions_custom_commit, @project) push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml) + push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_b) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 5b7c040bcd7..40e6590d85c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -9,7 +9,7 @@ class SearchController < ApplicationController around_action :allow_gitaly_ref_name_caching - before_action :block_anonymous_global_searches + before_action :block_anonymous_global_searches, except: :opensearch skip_before_action :authenticate_user! requires_cross_project_access if: -> do search_term_present = params[:search].present? || params[:term].present? diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb index 92f52726ab3..c14b9f80a53 100644 --- a/app/graphql/types/merge_request_state_enum.rb +++ b/app/graphql/types/merge_request_state_enum.rb @@ -5,6 +5,6 @@ module Types graphql_name 'MergeRequestState' description 'State of a GitLab merge request' - value 'merged' + value 'merged', description: "Merge Request has been merged" end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 871d19c6a8c..62580124c0f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -175,7 +175,9 @@ module NotesHelper end end - def notes_data(issuable) + def notes_data(issuable, start_at_zero = false) + initial_last_fetched_at = start_at_zero ? 0 : Time.current.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + data = { discussionsPath: discussions_path(issuable), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), @@ -186,7 +188,7 @@ module NotesHelper reopenPath: reopen_issuable_path(issuable), notesPath: notes_url, prerenderedNotesCount: issuable.capped_notes_count(MAX_PRERENDERED_NOTES), - lastFetchedAt: Time.now.to_i * ::Gitlab::UpdatedNotesPaginator::MICROSECOND + lastFetchedAt: initial_last_fetched_at } if issuable.is_a?(MergeRequest) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index f24aa5d3bcb..c44a67d3e66 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -6,28 +6,6 @@ module TreeHelper FILE_LIMIT = 1_000 - # Sorts a repository's tree so that folders are before files and renders - # their corresponding partials - # - # tree - A `Tree` object for the current tree - # rubocop: disable CodeReuse/ActiveRecord - def render_tree(tree) - # Sort submodules and folders together by name ahead of files - folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = [] - items = (folders + submodules).sort_by(&:name) + files - - if items.size > FILE_LIMIT - tree << render(partial: 'projects/tree/truncated_notice_tree_row', - locals: { limit: FILE_LIMIT, total: items.size }) - items = items.take(FILE_LIMIT) - end - - tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? - tree.join.html_safe - end - # rubocop: enable CodeReuse/ActiveRecord - # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' @@ -37,20 +15,6 @@ module TreeHelper sprite_icon(file_type_icon_class(type, mode, name)) end - # Using Rails `*_path` methods can be slow, especially when generating - # many paths, as with a repository tree that has thousands of items. - def fast_project_blob_path(project, blob_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'blob', blob_path) - ) - end - - def fast_project_tree_path(project, tree_path) - ActionDispatch::Journey::Router::Utils.escape_path( - File.join(relative_url_root, project.path_with_namespace, '-', 'tree', tree_path) - ) - end - # Simple shortcut to File.join def tree_join(*args) File.join(*args) @@ -167,13 +131,6 @@ module TreeHelper Gitlab.config.gitlab.relative_url_root.presence || '/' end - # project and path are used on the EE version - def tree_content_data(logs_path, project, path) - { - "logs-path" => logs_path - } - end - def breadcrumb_data_attributes attrs = { can_collaborate: can_collaborate_with_project?(@project).to_s, diff --git a/app/serializers/base_discussion_entity.rb b/app/serializers/base_discussion_entity.rb index 5ca4d1d6cc9..8d4c3906847 100644 --- a/app/serializers/base_discussion_entity.rb +++ b/app/serializers/base_discussion_entity.rb @@ -15,6 +15,7 @@ class BaseDiscussionEntity < Grape::Entity expose :for_commit?, as: :for_commit expose :individual_note?, as: :individual_note expose :resolvable?, as: :resolvable + expose :resolved_by_push?, as: :resolved_by_push expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index 4edcff0e3d0..19b9b439fed 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -44,11 +44,7 @@ module Git def invalidated_file_types return super unless default_branch? && !creating_branch? - paths = limited_commits.each_with_object(Set.new) do |commit, set| - commit.raw_deltas.each do |diff| - set << diff.new_path - end - end + paths = commit_paths.values.reduce(&:merge) || Set.new Gitlab::FileDetector.types_in_paths(paths) end @@ -77,6 +73,7 @@ module Git enqueue_process_commit_messages enqueue_jira_connect_sync_messages enqueue_metrics_dashboard_sync + track_ci_config_change_event end def branch_remove_hooks @@ -89,6 +86,18 @@ module Git ::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id) end + def track_ci_config_change_event + return unless Gitlab::CurrentSettings.usage_ping_enabled? + return unless ::Feature.enabled?(:usage_data_unique_users_committing_ciconfigfile, project, default_enabled: :yaml) + return unless default_branch? + + commits_changing_ci_config.each do |commit| + Gitlab::UsageDataCounters::HLLRedisCounter.track_event( + 'o_pipeline_authoring_unique_users_committing_ciconfigfile', values: commit.author&.id + ) + end + end + # Schedules processing of commit messages def enqueue_process_commit_messages referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) @@ -190,6 +199,23 @@ module Git set end + + def commits_changing_ci_config + commit_paths.select do |commit, paths| + next if commit.merge_commit? + + paths.include?(project.ci_config_path_or_default) + end.keys + end + + def commit_paths + strong_memoize(:commit_paths) do + limited_commits.map do |commit| + paths = Set.new(commit.raw_deltas.map(&:new_path)) + [commit, paths] + end.to_h + end + end end end diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 505ee40182e..e8b2c5db4e6 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -84,8 +84,7 @@ = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') - elsif !current_user.can_remove_self? %p - - reset_link = reset_profile_password_path - = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "<a href='#{reset_link}'>".html_safe, closingTag: '</a>'.html_safe} + = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "<a href='#{reset_profile_password_path}' rel=\"nofollow\" data-method=\"put\">".html_safe, closingTag: '</a>'.html_safe} %p = s_('Profiles|If after setting a password, the option to delete your account is still not available, please email %{data_request} to begin the account deletion process.').html_safe % { data_request: mail_to('personal-data-request@gitlab.com') } - else diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 9ea4d3df631..fcf073e1e09 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -8,13 +8,13 @@ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do = branch.name - if branch.name == @repository.root_ref - %span.badge.badge-primary.gl-ml-2 default + %span.badge.gl-badge.sm.badge-pill.badge-primary.gl-ml-2 default - elsif merged - %span.badge.badge-info.has-tooltip.gl-ml-2{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } + %span.badge.gl-badge.sm.badge-pill.badge-info.has-tooltip.gl-ml-2{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } = s_('Branches|merged') - if protected_branch?(@project, branch) - %span.badge.badge-success.gl-ml-2 + %span.badge.gl-badge.sm.badge-pill.badge-success.gl-ml-2 = s_('Branches|protected') = render_if_exists 'projects/branches/diverged_from_upstream', branch: branch diff --git a/app/views/projects/buttons/_remove_tag.html.haml b/app/views/projects/buttons/_remove_tag.html.haml index ae776e93203..68a9d715674 100644 --- a/app/views/projects/buttons/_remove_tag.html.haml +++ b/app/views/projects/buttons/_remove_tag.html.haml @@ -2,5 +2,5 @@ - tag = local_assigns.fetch(:tag, nil) - return unless project && tag -%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-danger remove-row has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } } +%button{ type: "button", class: "js-remove-tag js-confirm-modal-button gl-button btn btn-danger btn-icon remove-row has-tooltip gl-ml-3 #{protected_tag?(project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), data: { container: 'body', path: project_tag_path(@project, tag.name), modal_attributes: delete_tag_modal_attributes(tag.name) } } = sprite_icon("remove") diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index e45ea209e8c..3f9aa24a569 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -13,4 +13,8 @@ = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } .prepend-top-20 - = render "form" + #js-compare-selector{ data: { project_compare_index_path: project_compare_index_path(@project), + refs_project_path: refs_project_path(@project), + params_from: params[:from], params_to: params[:to], + project_merge_request_path: @merge_request.present? ? project_merge_request_path(@project, @merge_request) : '', + create_mr_path: create_mr_button? ? create_mr_path : '' } } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 9e441eac602..962e1158118 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -16,7 +16,7 @@ .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions') %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') - %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.') + %p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.') .settings-content = form_for @project, remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 879365bffb4..f20a4094f8f 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -56,10 +56,13 @@ = render "projects/merge_requests/widget" = render "projects/merge_requests/awards_block" - if mr_action === "show" - - add_page_startup_api_call discussions_path(@merge_request) + - if Feature.enabled?(:paginated_notes, @project) + - add_page_startup_api_call notes_url + - else + - add_page_startup_api_call discussions_path(@merge_request) - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json) - add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json) - #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, + #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request, Feature.enabled?(:paginated_notes, @project)).to_json, noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'), noteable_type: 'MergeRequest', target_type: 'merge_request', diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 30ba22ba53c..99672ded6db 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -5,8 +5,8 @@ .project-network.gl-border-1.gl-border-solid.gl-border-gray-300 .controls.gl-bg-gray-50.gl-p-2.gl-font-base.gl-text-gray-400.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-300 = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control input-mx-250 search-sha' - = button_tag class: 'btn btn-success' do + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input input-mx-250 search-sha gl-mr-2' + = button_tag class: 'btn gl-button btn-success btn-icon' do = sprite_icon('search') .inline.gl-ml-5 .form-check.light diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 9d4e5d629f4..61b357831fd 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -41,6 +41,6 @@ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - if can?(current_user, :admin_tag, @project) - = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do + = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn gl-button btn-default btn-icon btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do = sprite_icon("pencil") = render 'projects/buttons/remove_tag', project: @project, tag: tag diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 2fe5c5888f5..04d8c1f42bc 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -24,9 +24,9 @@ %li = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) - if can?(current_user, :admin_tag, @project) - = link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn', data: { qa_selector: "new_tag_button" } do + = link_to new_project_tag_path(@project), class: 'btn gl-button btn-success', data: { qa_selector: "new_tag_button" } do = s_('TagsPage|New tag') - = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn btn-svg d-none d-sm-inline-block has-tooltip' do + = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-block has-tooltip' do = sprite_icon('rss', css_class: 'qa-rss-icon') = render_if_exists 'projects/commits/mirror_status' diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml deleted file mode 100644 index 6d2bdda8254..00000000000 --- a/app/views/projects/tree/_readme.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if readme.rich_viewer - %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout)] } - .js-file-title.file-title-flex-parent - .file-header-content - = blob_icon readme.mode, readme.name - = link_to project_blob_path(@project, tree_join(@ref, readme.path)) do - %strong - = readme.name - - = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: project_blob_path(@project, tree_join(@ref, readme.path), viewer: :rich, format: :json) diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml deleted file mode 100644 index a4427c6eedb..00000000000 --- a/app/views/projects/tree/_tree_content.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -.tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) } - .table-holder.bordered-box - %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } - %thead - %tr - %th= s_('ProjectFileTree|Name') - %th.d-none.d-sm-table-cell - .float-left= _('Last commit') - %th.text-right= _('Last update') - - if @path.present? - %tr.tree-item - %td.tree-item-file-name - = link_to "..", project_tree_path(@project, up_dir_path), class: 'gl-ml-3' - %td - %td.d-none.d-sm-table-cell - - = render_tree(tree) - - - if tree.readme - = render "projects/tree/readme", readme: tree.readme - -- if can_edit_tree? - = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post - = render 'projects/blob/new_dir' diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml deleted file mode 100644 index 04496914c02..00000000000 --- a/app/views/projects/tree/_tree_row.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -- tree_row_name = tree_row.name -- tree_row_type = tree_row.type - -%tr{ class: "tree-item file_#{hexdigest(tree_row_name)}" } - %td.tree-item-file-name - - if tree_row_type == :tree - = tree_icon('folder', tree_row.mode, tree_row.name) - - path = flatten_tree(@path, tree_row) - %a.str-truncated{ href: fast_project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path } - %span= path - - - elsif tree_row_type == :blob - = tree_icon('file', tree_row.mode, tree_row_name) - %a.str-truncated{ href: fast_project_blob_path(@project, tree_join(@id || @commit.id, tree_row_name)), title: tree_row_name } - %span= tree_row_name - - if @lfs_blob_ids.include?(tree_row.id) - %span.badge.label-lfs.gl-ml-2 LFS - - - elsif tree_row_type == :commit - = tree_icon('archive', tree_row.mode, tree_row.name) - = submodule_link(tree_row, @ref) - - %td.d-none.d-sm-table-cell.tree-commit - %td.tree-time-ago.text-right - %span.log_loading.hide - = loading_icon - Loading commit data... diff --git a/app/views/projects/tree/_truncated_notice_tree_row.html.haml b/app/views/projects/tree/_truncated_notice_tree_row.html.haml deleted file mode 100644 index a03e0a549ee..00000000000 --- a/app/views/projects/tree/_truncated_notice_tree_row.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%tr.tree-truncated-warning - %td{ colspan: '3' } - = sprite_icon('warning-solid') - %span - Too many items to show. To preserve performance only - %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)} - items are displayed. |