diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-09 21:08:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-09 21:08:33 +0000 |
commit | b296ffa543e23f57fa2692539e6f0028c59e2203 (patch) | |
tree | f5224a37cac088506d1c8fd53925ee1d9fd2a02c /app | |
parent | c172bb9967f280e05bd904188d60a959dff10f00 (diff) | |
download | gitlab-ce-b296ffa543e23f57fa2692539e6f0028c59e2203.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
44 files changed, 361 insertions, 213 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index c1ebd234088..c6605452616 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -10,7 +10,6 @@ import { GlTabs, GlTab, GlButton, - GlTable, } from '@gitlab/ui'; import { s__ } from '~/locale'; import alertQuery from '../graphql/queries/details.query.graphql'; @@ -28,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import SystemNote from './system_notes/system_note.vue'; import AlertSidebar from './alert_sidebar.vue'; import AlertMetrics from './alert_metrics.vue'; +import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; const containerEl = document.querySelector('.page-with-contextual-sidebar'); @@ -55,6 +55,7 @@ export default { }, ], components: { + AlertDetailsTable, GlBadge, GlAlert, GlIcon, @@ -63,7 +64,6 @@ export default { GlTab, GlTabs, GlButton, - GlTable, TimeAgoTooltip, AlertSidebar, SystemNote, @@ -331,20 +331,7 @@ export default { </div> <div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div> </div> - <gl-table - class="alert-management-details-table" - :items="[{ 'Full Alert Payload': 'Value', ...alert }]" - :show-empty="true" - :busy="loading" - stacked - > - <template #empty> - {{ s__('AlertManagement|No alert data to display.') }} - </template> - <template #table-busy> - <gl-loading-icon size="lg" color="dark" class="mt-3" /> - </template> - </gl-table> + <alert-details-table :alert="alert" :loading="loading" /> </gl-tab> <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title"> <alert-metrics :dashboard-url="alert.metricsDashboardUrl" /> diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 8060938c72a..fd12c282b62 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,7 +1,7 @@ +import $ from 'jquery'; import './autosize'; import './bind_in_out'; import './markdown/render_gfm'; -import initGFMInput from './markdown/gfm_auto_complete'; import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; @@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi import initSelect2Dropdowns from './select2'; installGlEmojiElement(); -initGFMInput(); + initCopyAsGFM(); initCopyToClipboard(); + initPageShortcuts(); initCollapseSidebarOnWindowResize(); initSelect2Dropdowns(); + +document.addEventListener('DOMContentLoaded', () => { + window.requestIdleCallback( + () => { + // Check if we have to Load GFM Input + const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)'); + if ($gfmInputs.length) { + import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete') + .then(({ default: initGFMInput }) => { + initGFMInput($gfmInputs); + }) + .catch(() => {}); + } + }, + { timeout: 500 }, + ); +}); diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js index 6bbd2133344..83f2ca0bdc2 100644 --- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js +++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import { parseBoolean } from '~/lib/utils/common_utils'; -export default function initGFMInput() { - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { +export default function initGFMInput($els) { + $els.each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const enableGFM = parseBoolean(el.dataset.supportsAutocomplete); diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue index 9251af01aff..06f436adb8e 100644 --- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue +++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue @@ -1,5 +1,4 @@ <script> -/* eslint-disable vue/no-v-html */ import { GlPopover, GlSprintf, GlButton } from '@gitlab/ui'; import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; @@ -114,7 +113,7 @@ export default { :css-classes="['suggest-gitlab-ci-yml', 'ml-4']" > <template #title> - <span v-html="suggestTitle"></span> + <span>{{ suggestTitle }}</span> <span class="ml-auto"> <gl-button :aria-label="__('Close')" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 34e8438ba4c..2817f9cb13d 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,11 +1,13 @@ <script> import $ from 'jquery'; +import { mapActions, mapGetters } from 'vuex'; import { GlButton } from '@gitlab/ui'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; import ListIssue from 'ee_else_ce/boards/models/issue'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import boardsStore from '../stores/boards_store'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'BoardNewIssue', @@ -13,6 +15,7 @@ export default { ProjectSelect, GlButton, }, + mixins: [glFeatureFlagMixin()], props: { groupId: { type: Number, @@ -32,6 +35,7 @@ export default { }; }, computed: { + ...mapGetters(['isSwimlanesOn']), disabled() { if (this.groupId) { return this.title === '' || !this.selectedProject.name; @@ -44,6 +48,7 @@ export default { eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { + ...mapActions(['addListIssue', 'addListIssueFailure']), submit(e) { e.preventDefault(); if (this.title.trim() === '') return Promise.resolve(); @@ -70,21 +75,31 @@ export default { eventHub.$emit(`scroll-board-list-${this.list.id}`); this.cancel(); + if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) { + this.addListIssue({ list: this.list, issue, position: 0 }); + } + return this.list .newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); - boardsStore.setIssueDetail(issue); - boardsStore.setListDetail(this.list); + if (!this.glFeatures.boardsWithSwimlanes || !this.isSwimlanesOn) { + boardsStore.setIssueDetail(issue); + boardsStore.setListDetail(this.list); + } }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); // Remove the issue - this.list.removeIssue(issue); + if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) { + this.addListIssueFailure({ list: this.list, issue }); + } else { + this.list.removeIssue(issue); + } // Show error message this.error = true; diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 4e808c809fb..8d9b58f71cb 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -235,6 +235,14 @@ export default { notImplemented(); }, + addListIssue: ({ commit }, { list, issue, position }) => { + commit(types.ADD_ISSUE_TO_LIST, { list, issue, position }); + }, + + addListIssueFailure: ({ commit }, { list, issue }) => { + commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue }); + }, + fetchBacklog: () => { notImplemented(); }, diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index f336d6d03c9..4fdbfbc36c5 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -15,6 +15,7 @@ import { import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import eventHub from '../eventhub'; import { ListType } from '../constants'; import IssueProject from '../models/project'; @@ -303,7 +304,7 @@ const boardsStore = { onNewListIssueResponse(list, issue, data) { issue.refreshData(data); - if (list.issuesSize > 1) { + if (!gon.features.boardsWithSwimlanes && list.issuesSize > 1) { const moveBeforeId = list.issues[1].id; this.moveIssue(issue.id, null, null, null, moveBeforeId); } @@ -710,6 +711,10 @@ const boardsStore = { }, newIssue(id, issue) { + if (typeof id === 'string') { + id = getIdFromGraphQLId(id); + } + return axios.post(this.generateIssuesPath(id), { issue, }); diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 12dd96380f6..a3b84108cb3 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -24,6 +24,8 @@ export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR'; export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; +export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST'; +export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE'; export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 837bccce091..f25c339836f 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { sortBy } from 'lodash'; +import { sortBy, pull } from 'lodash'; import * as mutationTypes from './mutation_types'; import { __ } from '~/locale'; @@ -8,6 +8,10 @@ const notImplemented = () => { throw new Error('Not implemented!'); }; +const removeIssueFromList = (state, listId, issueId) => { + Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); +}; + export default { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { const { boardType, disabled, showPromotion, ...endpoints } = data; @@ -131,6 +135,18 @@ export default { notImplemented(); }, + [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => { + const listIssues = state.issuesByListId[list.id]; + listIssues.splice(position, 0, issue.id); + Vue.set(state.issuesByListId, list.id, listIssues); + Vue.set(state.issues, issue.id, issue); + }, + + [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => { + state.error = __('An error occurred while creating the issue. Please try again.'); + removeIssueFromList(state, list.id, issue.id); + }, + [mutationTypes.SET_CURRENT_PAGE]: () => { notImplemented(); }, diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue index f87bd695560..845f1aec8cf 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue @@ -1,6 +1,6 @@ <script> import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui'; import { s__ } from '~/locale'; import createFlash from '~/flash'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; @@ -27,6 +27,7 @@ export default { GlLink, ToggleRepliesWidget, TimeAgoTooltip, + GlBadge, }, directives: { GlTooltip: GlTooltipDirective, @@ -148,14 +149,14 @@ export default { } }, onCreateNoteError(err) { - this.$emit('createNoteError', err); + this.$emit('create-note-error', err); }, hideForm() { this.isFormRendered = false; this.discussionComment = ''; }, showForm() { - this.$emit('openForm', this.discussion.id); + this.$emit('open-form', this.discussion.id); this.isFormRendered = true; }, toggleResolvedStatus() { @@ -167,11 +168,11 @@ export default { }) .then(({ data }) => { if (data.errors?.length > 0) { - this.$emit('resolveDiscussionError', data.errors[0]); + this.$emit('resolve-discussion-error', data.errors[0]); } }) .catch(err => { - this.$emit('resolveDiscussionError', err); + this.$emit('resolve-discussion-error', err); }) .finally(() => { this.isResolving = false; @@ -192,13 +193,12 @@ export default { <template> <div class="design-discussion-wrapper"> - <div - class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center" + <gl-badge + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer" :class="{ resolved: discussion.resolved }" - type="button" > {{ discussion.index }} - </div> + </gl-badge> <ul class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none" data-qa-selector="design_discussion_content" @@ -208,7 +208,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" :class="{ 'gl-bg-blue-50': isDiscussionActive }" - @error="$emit('updateNoteError', $event)" + @error="$emit('update-note-error', $event)" > <template v-if="discussion.resolvable" #resolveDiscussion> <button @@ -216,7 +216,6 @@ export default { :class="{ 'is-active': discussion.resolved }" :title="resolveCheckboxText" :aria-label="resolveCheckboxText" - type="button" class="line-resolve-btn note-action-button gl-mr-3" data-testid="resolve-button" @click.stop="toggleResolvedStatus" @@ -252,7 +251,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" :class="{ 'gl-bg-blue-50': isDiscussionActive }" - @error="$emit('updateNoteError', $event)" + @error="$emit('update-note-error', $event)" /> <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> <reply-placeholder @@ -275,8 +274,8 @@ export default { v-model="discussionComment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="hideForm" + @submit-form="mutate" + @cancel-form="hideForm" > <template v-if="discussion.resolvable" #resolveCheckbox> <label data-testid="resolve-checkbox"> diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index 6c380153a3f..18444a2cc2f 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable vue/no-v-html */ import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui'; import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -18,6 +18,7 @@ export default { DesignReplyForm, ApolloMutation, GlIcon, + GlLink, }, directives: { GlTooltip: GlTooltipDirective, @@ -83,27 +84,27 @@ export default { :img-alt="author.username" :img-size="40" /> - <div class="d-flex justify-content-between"> + <div class="gl-display-flex gl-justify-content-space-between"> <div> - <a + <gl-link v-once :href="author.webUrl" class="js-user-link" :data-user-id="author.id" :data-username="author.username" > - <span class="note-header-author-name bold">{{ author.name }}</span> + <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span> <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> <span class="note-headline-light">@{{ author.username }}</span> - </a> + </gl-link> <span class="note-headline-light note-headline-meta"> <span class="system-note-message"> <slot></slot> </span> - <a + <gl-link class="note-timestamp system-note-separator gl-display-block gl-mb-2" :href="`#note_${noteAnchorId}`" > <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> - </a> + </gl-link> </span> </div> <div class="gl-display-flex"> @@ -122,7 +123,7 @@ export default { </div> <template v-if="!isEditing"> <div - class="note-text js-note-text md" + class="note-text js-note-text" data-qa-selector="note_content" v-html="note.bodyHtml" ></div> @@ -143,9 +144,9 @@ export default { :is-saving="loading" :markdown-preview-path="markdownPreviewPath" :is-new-comment="false" - class="mt-5" - @submitForm="mutate" - @cancelForm="hideForm" + class="gl-mt-5" + @submit-form="mutate" + @cancel-form="hideForm" /> </apollo-mutation> </timeline-entry-item> diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 969034909f2..3754e1dbbc1 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -1,5 +1,5 @@ <script> -import { GlDeprecatedButton, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { s__ } from '~/locale'; @@ -7,7 +7,7 @@ export default { name: 'DesignReplyForm', components: { MarkdownField, - GlDeprecatedButton, + GlButton, GlModal, }, props: { @@ -66,13 +66,13 @@ export default { }, methods: { submitForm() { - if (this.hasValue) this.$emit('submitForm'); + if (this.hasValue) this.$emit('submit-form'); }, cancelComment() { if (this.hasValue && this.formText !== this.value) { this.$refs.cancelCommentModal.show(); } else { - this.$emit('cancelForm'); + this.$emit('cancel-form'); } }, focusInput() { @@ -112,20 +112,21 @@ export default { </markdown-field> <slot name="resolveCheckbox"></slot> <div class="note-form-actions gl-display-flex gl-justify-content-space-between"> - <gl-deprecated-button + <gl-button ref="submitButton" :disabled="!hasValue || isSaving" + category="primary" variant="success" type="submit" data-track-event="click_button" data-qa-selector="save_comment_button" - @click="$emit('submitForm')" + @click="$emit('submit-form')" > {{ buttonText }} - </gl-deprecated-button> - <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{ + </gl-button> + <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{ __('Cancel') - }}</gl-deprecated-button> + }}</gl-button> </div> <gl-modal ref="cancelCommentModal" @@ -134,7 +135,7 @@ export default { :ok-title="modalSettings.okTitle" :cancel-title="modalSettings.cancelTitle" modal-id="cancel-comment-modal" - @ok="$emit('cancelForm')" + @ok="$emit('cancel-form')" >{{ modalSettings.content }} </gl-modal> </form> diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue index 9cfd2ea43a9..df425e3b96d 100644 --- a/app/assets/javascripts/design_management/components/design_sidebar.vue +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -159,11 +159,11 @@ export default { :resolved-discussions-expanded="resolvedDiscussionsExpanded" :discussion-with-open-form="discussionWithOpenForm" data-testid="unresolved-discussion" - @createNoteError="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @resolveDiscussionError="$emit('resolveDiscussionError', $event)" + @create-note-error="$emit('onDesignDiscussionError', $event)" + @update-note-error="$emit('updateNoteError', $event)" + @resolve-discussion-error="$emit('resolveDiscussionError', $event)" @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - @openForm="updateDiscussionWithOpenForm" + @open-form="updateDiscussionWithOpenForm" /> <template v-if="resolvedDiscussions.length > 0"> <gl-button diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 8a9911f55a3..c6225c516e2 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -372,8 +372,8 @@ export default { v-model="comment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="closeCommentForm" + @submit-form="mutate" + @cancel-form="closeCommentForm" /> </apollo-mutation ></template> </design-sidebar> diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 36c586ddfd2..d57fde59db7 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -71,12 +71,15 @@ class GfmAutoComplete { setupLifecycle() { this.input.each((i, input) => { const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); - // This triggers at.js again - // Needed for quick actions with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - $input.on('clear-commands-cache.atwho', () => this.clearCache()); + if (!$input.hasClass('js-gfm-input-initialized')) { + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); + // This triggers at.js again + // Needed for quick actions with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + $input.on('clear-commands-cache.atwho', () => this.clearCache()); + $input.addClass('js-gfm-input-initialized'); + } }); } diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 3f9163e924d..575d3618313 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -2,9 +2,6 @@ import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import { highCountTrim } from '~/lib/utils/text_utility'; -import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; -import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; /** @@ -26,51 +23,43 @@ export default function initTodoToggle() { function initStatusTriggers() { const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger'); - const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); - if (setStatusModalTriggerEl || setStatusModalWrapperEl) { - Vue.use(Translate); + if (setStatusModalTriggerEl) { + setStatusModalTriggerEl.addEventListener('click', () => { + import( + /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue' + ) + .then(({ default: SetStatusModalWrapper }) => { + const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); + const statusModalElement = document.createElement('div'); + setStatusModalWrapperEl.appendChild(statusModalElement); - // eslint-disable-next-line no-new - new Vue({ - el: setStatusModalTriggerEl, - data() { - const { hasStatus } = this.$options.el.dataset; + Vue.use(Translate); - return { - hasStatus: parseBoolean(hasStatus), - }; - }, - render(createElement) { - return createElement(SetStatusModalTrigger, { - props: { - hasStatus: this.hasStatus, - }, - }); - }, - }); - - // eslint-disable-next-line no-new - new Vue({ - el: setStatusModalWrapperEl, - data() { - const { currentEmoji, currentMessage } = this.$options.el.dataset; + // eslint-disable-next-line no-new + new Vue({ + el: statusModalElement, + data() { + const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset; - return { - currentEmoji, - currentMessage, - }; - }, - render(createElement) { - const { currentEmoji, currentMessage } = this; + return { + currentEmoji, + currentMessage, + }; + }, + render(createElement) { + const { currentEmoji, currentMessage } = this; - return createElement(SetStatusModalWrapper, { - props: { - currentEmoji, - currentMessage, - }, - }); - }, + return createElement(SetStatusModalWrapper, { + props: { + currentEmoji, + currentMessage, + }, + }); + }, + }); + }) + .catch(() => {}); }); } } @@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() { document.addEventListener('DOMContentLoaded', () => { requestIdleCallback(initStatusTriggers); - initNavUserDropdownTracking(); + requestIdleCallback(initNavUserDropdownTracking); }); diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index 0bfad0befb3..183816921c1 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -44,6 +44,7 @@ export default { :aria-label="s__('IDE|Edit')" data-container="body" data-placement="right" + data-qa-selector="edit_mode_tab" type="button" class="ide-sidebar-link js-ide-edit-mode" @click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)" @@ -78,8 +79,9 @@ export default { :aria-label="s__('IDE|Commit')" data-container="body" data-placement="right" + data-qa-selector="commit_mode_tab" type="button" - class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab" + class="ide-sidebar-link js-ide-commit-mode" @click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)" > <gl-icon name="commit" /> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 77a7151e275..146e818d654 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -103,6 +103,7 @@ export default { :title="lastCommit.message" :href="getCommitPath(lastCommit.short_id)" class="commit-sha" + data-qa-selector="commit_sha_content" >{{ lastCommit.short_id }}</a > by diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 2ef0feaa2af..4e2770a24c2 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -31,7 +31,6 @@ import initLogoAnimation from './logo'; import initFrequentItemDropdowns from './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initUsagePingConsent from './usage_ping_consent'; -import initPerformanceBar from './performance_bar'; import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; @@ -164,8 +163,6 @@ document.addEventListener('DOMContentLoaded', () => { const $document = $(document); const bootstrapBreakpoint = bp.getBreakpointSize(); - if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); - initUserTracking(); initLayoutNav(); initAlertHandler(); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 88d513f6076..cfe674f9c52 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -380,7 +380,7 @@ export default { dir="auto" :disabled="isSubmitting" name="note[note]" - class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 0ef2d5743b1..88b4461cf38 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -337,7 +337,7 @@ export default { v-model="updatedNoteBody" :data-supports-quick-actions="!isEditing" name="note[note]" - class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input" dir="auto" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index ac10d99612c..f88314dabcf 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; -export default ({ container }) => +const initPerformanceBar = ({ container }) => new Vue({ el: container, components: { @@ -118,3 +118,9 @@ export default ({ container }) => }); }, }); + +document.addEventListener('DOMContentLoaded', () => { + initPerformanceBar({ container: '#js-peek' }); +}); + +export default initPerformanceBar; diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js deleted file mode 100644 index e31806ad199..00000000000 --- a/app/assets/javascripts/set_status_modal/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -export default createEventHub(); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue deleted file mode 100644 index 0e8b6d93f42..00000000000 --- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue +++ /dev/null @@ -1,27 +0,0 @@ -<script> -import { s__ } from '~/locale'; -import eventHub from './event_hub'; - -export default { - props: { - hasStatus: { - type: Boolean, - required: true, - }, - }, - computed: { - buttonText() { - return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status'); - }, - }, - methods: { - openModal() { - eventHub.$emit('openModal'); - }, - }, -}; -</script> - -<template> - <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button> -</template> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index a841cca8c95..09e893ff285 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -6,7 +6,6 @@ import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __, s__ } from '~/locale'; import Api from '~/api'; -import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; import * as Emoji from '~/emoji'; @@ -48,15 +47,12 @@ export default { }, }, mounted() { - eventHub.$on('openModal', this.openModal); + this.$root.$emit('bv::show::modal', this.modalId); }, beforeDestroy() { this.emojiMenu.destroy(); }, methods: { - openModal() { - this.$root.$emit('bv::show::modal', this.modalId); - }, closeModal() { this.$root.$emit('bv::hide::modal', this.modalId); }, diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue new file mode 100644 index 00000000000..2cd71669ecb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue @@ -0,0 +1,47 @@ +<script> +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlLoadingIcon, + GlTable, + }, + props: { + alert: { + type: Object, + required: false, + default: null, + }, + loading: { + type: Boolean, + required: true, + }, + }, + tableHeader: { + [s__('AlertManagement|Full Alert Payload')]: s__('AlertManagement|Value'), + }, + computed: { + items() { + if (!this.alert) { + return []; + } + return [{ ...this.$options.tableHeader, ...this.alert }]; + }, + }, +}; +</script> +<template> + <gl-table + class="alert-management-details-table" + :busy="loading" + :empty-text="s__('AlertManagement|No alert data to display.')" + :items="items" + show-empty + stacked + > + <template #table-busy> + <gl-loading-icon size="lg" color="dark" class="gl-mt-5" /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index f6d0fa9d8ed..f30676e8ef3 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,4 +1,6 @@ <script> +/* eslint-disable vue/no-v-html */ + /** * Common component to render a system note, icon and user information. * @@ -106,7 +108,7 @@ export default { :class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }" class="note system-note note-wrapper" > - <div v-safe-html="iconHtml" class="timeline-icon"></div> + <div class="timeline-icon" v-html="iconHtml"></div> <div class="timeline-content"> <div class="note-header"> <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d1491fb50b0..cfe62d73e5d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -152,6 +152,18 @@ $red-800: #8d1300 !default; $red-900: #660e00 !default; $red-950: #4d0a00 !default; +$purple-50: #f4f0ff !default; +$purple-100: #e1d8f9 !default; +$purple-200: #cbbbf2 !default; +$purple-300: #ac93e6 !default; +$purple-400: #9475db !default; +$purple-500: #7b58cf !default; +$purple-600: #694cc0 !default; +$purple-700: #5943b6 !default; +$purple-800: #453894 !default; +$purple-900: #2f2a6b !default; +$purple-950: #232150 !default; + $gray-10: #fafafa !default; $gray-50: #f0f0f0 !default; $gray-100: #dbdbdb !default; @@ -221,6 +233,20 @@ $reds: ( '950': $red-950 ); +$purples: ( + '50': $purple-50, + '100': $purple-100, + '200': $purple-200, + '300': $purple-300, + '400': $purple-400, + '500': $purple-500, + '600': $purple-600, + '700': $purple-700, + '800': $purple-800, + '900': $purple-900, + '950': $purple-950 +); + $grays: ( '10': $gray-10, '50': $gray-50, diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c2841c254eb..af1fc870f54 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -10,13 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController include SpammableActions include RecordUserLastActivity - def issue_except_actions - %i[index calendar new create bulk_update import_csv export_csv service_desk] - end - - def set_issuables_index_only_actions - %i[index calendar service_desk] - end + ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze + SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) } @@ -25,10 +20,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :check_issues_available! - before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) } - after_action :log_issue_show, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) } + before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) } + after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) } - before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) } + before_action :set_issuables_index, if: ->(c) { SET_ISSUEABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) } # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb new file mode 100644 index 00000000000..e610286c1a9 --- /dev/null +++ b/app/graphql/types/current_user_todos.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Interface to expose todos for the current_user on the `object` +module Types + module CurrentUserTodos + include BaseInterface + + field_class Types::BaseField + + field :current_user_todos, Types::TodoType.connection_type, + description: 'Todos for the current user', + null: false do + argument :state, Types::TodoStateEnum, + description: 'State of the todos', + required: false + end + + def current_user_todos(state: nil) + state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending` + + TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute + end + end +end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index 3c84dc151bd..4e11a7aaf09 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -12,6 +12,7 @@ module Types implements(Types::Notes::NoteableType) implements(Types::DesignManagement::DesignFields) + implements(Types::CurrentUserTodos) field :versions, Types::DesignManagement::VersionType.connection_type, diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index df789f3cf47..d6253f74ce5 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -7,6 +7,7 @@ module Types connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_issue diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 01b02b7976f..805ae111ff7 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -7,6 +7,7 @@ module Types connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_merge_request diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 08e7fabeb74..4f21da3d897 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -26,7 +26,7 @@ module Types resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find } field :author, Types::UserType, - description: 'The owner of this todo', + description: 'The author of this todo', null: false, resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find } diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb index 3e7cb940a62..df7bbe4dc08 100644 --- a/app/models/concerns/has_wiki.rb +++ b/app/models/concerns/has_wiki.rb @@ -25,10 +25,6 @@ module HasWiki wiki.repository_exists? end - def after_wiki_activity - true - end - private def check_wiki_path_conflict diff --git a/app/models/project.rb b/app/models/project.rb index d588cb791de..4c189197c99 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3,7 +3,6 @@ require 'carrierwave/orm/activerecord' class Project < ApplicationRecord - extend ::Gitlab::Utils::Override include Gitlab::ConfigHelper include Gitlab::VisibilityLevel include AccessRequestable @@ -2470,11 +2469,6 @@ class Project < ApplicationRecord jira_imports.last end - override :after_wiki_activity - def after_wiki_activity - touch(:last_activity_at, :last_repository_updated_at) - end - def metrics_setting super || build_metrics_setting end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index c4fcdff8386..b9916a54d75 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -5,6 +5,7 @@ module ChatMessage attr_reader :merge_request_iid attr_reader :source_branch attr_reader :target_branch + attr_reader :action attr_reader :state attr_reader :title @@ -16,6 +17,7 @@ module ChatMessage @merge_request_iid = obj_attr[:iid] @source_branch = obj_attr[:source_branch] @target_branch = obj_attr[:target_branch] + @action = obj_attr[:action] @state = obj_attr[:state] @title = format_title(obj_attr[:title]) end @@ -63,11 +65,17 @@ module ChatMessage "#{project_url}/-/merge_requests/#{merge_request_iid}" end - # overridden in EE def state_or_action_text - state + case action + when 'approved', 'unapproved' + action + when 'approval' + 'added their approval to' + when 'unapproval' + 'removed their approval from' + else + state + end end end end - -ChatMessage::MergeMessage.prepend_if_ee('::EE::ChatMessage::MergeMessage') diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 5df0a33dc9a..bd570cf7ead 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -10,6 +10,23 @@ class ProjectWiki < Wiki def disk_path(*args, &block) container.disk_path + '.wiki' end + + override :after_wiki_activity + def after_wiki_activity + # Update activity columns, this is done synchronously to avoid + # replication delays in Geo. + project.touch(:last_activity_at, :last_repository_updated_at) + end + + override :after_post_receive + def after_post_receive + # Update storage statistics + ProjectCacheWorker.perform_async(project.id, [], [:wiki_size]) + + # This call is repeated for post-receive, to make sure we're updating + # the activity columns for Git pushes as well. + after_wiki_activity + end end # TODO: Remove this once we implement ES support for group wikis. diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 22ae9b65564..9462f7401c4 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -133,8 +133,9 @@ class Wiki commit = commit_details(:created, message, title) wiki.write_page(title, format.to_sym, content, commit) + after_wiki_activity - update_container_activity + true rescue Gitlab::Git::Wiki::DuplicatePageError => e @error_message = "Duplicate page: #{e.message}" false @@ -144,16 +145,18 @@ class Wiki commit = commit_details(:updated, message, page.title) wiki.update_page(page.path, title || page.name, format.to_sym, content, commit) + after_wiki_activity - update_container_activity + true end def delete_page(page, message = nil) return unless page wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) + after_wiki_activity - update_container_activity + true end def page_title_and_dir(title) @@ -209,6 +212,17 @@ class Wiki web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '') end + # Callbacks for synchronous processing after wiki changes. + # These will be executed after any change made through GitLab itself (web UI and API), + # but not for Git pushes. + def after_wiki_activity + end + + # Callbacks for background processing after wiki changes. + # These will be executed after any change to the wiki repository. + def after_post_receive + end + private def commit_details(action, message = nil, title = nil) @@ -225,10 +239,6 @@ class Wiki def default_message(action, title) "#{user.username} #{action} page: #{title}" end - - def update_container_activity - container.after_wiki_activity - end end Wiki.prepend_if_ee('EE::Wiki') diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb index f9de72f2d5f..fa3019ee9d6 100644 --- a/app/services/git/wiki_push_service.rb +++ b/app/services/git/wiki_push_service.rb @@ -5,7 +5,16 @@ module Git # Maximum number of change events we will process on any single push MAX_CHANGES = 100 + attr_reader :wiki + + def initialize(wiki, current_user, params) + @wiki, @current_user, @params = wiki, current_user, params.dup + end + def execute + # Execute model-specific callbacks + wiki.after_post_receive + process_changes end @@ -23,7 +32,11 @@ module Git end def can_process_wiki_events? - Feature.enabled?(:wiki_events_on_git_push, project) + # TODO: Support activity events for group wikis + # https://gitlab.com/gitlab-org/gitlab/-/issues/209306 + return false unless wiki.is_a?(ProjectWiki) + + Feature.enabled?(:wiki_events_on_git_push, wiki.container) end def push_changes @@ -36,10 +49,6 @@ module Git wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev]) end - def wiki - project.wiki - end - def create_event_for(change) event_service.execute( change.last_known_slug, @@ -54,7 +63,7 @@ module Git end def on_default_branch?(change) - project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref]) + wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref]) end # See: [Gitlab::GitPostReceive#changes] diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb index 562c43487e9..3d1d0fe8c4e 100644 --- a/app/services/git/wiki_push_service/change.rb +++ b/app/services/git/wiki_push_service/change.rb @@ -5,11 +5,11 @@ module Git class Change include Gitlab::Utils::StrongMemoize - # @param [ProjectWiki] wiki + # @param [Wiki] wiki # @param [Hash] change - must have keys `:oldrev` and `:newrev` # @param [Gitlab::Git::RawDiffChange] raw_change - def initialize(project_wiki, change, raw_change) - @wiki, @raw_change, @change = project_wiki, raw_change, change + def initialize(wiki, change, raw_change) + @wiki, @raw_change, @change = wiki, raw_change, change end def page diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index c861f475cac..1c87452f0a3 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -70,6 +70,7 @@ = yield :page_specific_javascripts = webpack_controller_bundle_tags + = webpack_bundle_tag 'performance_bar' if performance_bar_enabled? = webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<84.0.4147.125"]) || browser.edge?([">=84", "<84.0.522.59"]) = yield :project_javascripts diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index 4c659241f99..22c2be3b7da 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -14,7 +14,11 @@ %li.divider - if can?(current_user, :update_user_status, current_user) %li - .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } } + %button.btn.menu-item.js-set-status-modal-trigger{ type: 'button' } + - if current_user.status.present? + = s_('SetStatusModal|Edit status') + - else + = s_('SetStatusModal|Set status') - if current_user_menu?(:profile) %li = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 8f844bd0b47..27dad744d0c 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -12,8 +12,8 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker def perform(gl_repository, identifier, changes, push_options = {}) container, project, repo_type = Gitlab::GlRepository.parse(gl_repository) - if project.nil? && (!repo_type.snippet? || container.is_a?(ProjectSnippet)) - log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"") + if container.nil? || (container.is_a?(ProjectSnippet) && project.nil?) + log("Triggered hook for non-existing gl_repository \"#{gl_repository}\"") return false end @@ -24,7 +24,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options) if repo_type.wiki? - process_wiki_changes(post_received, container) + process_wiki_changes(post_received, container.wiki) elsif repo_type.project? process_project_changes(post_received, container) elsif repo_type.snippet? @@ -59,18 +59,15 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker after_project_changes_hooks(project, user, changes.refs, changes.repository_data) end - def process_wiki_changes(post_received, project) - project.touch(:last_activity_at, :last_repository_updated_at) - project.wiki.repository.expire_statistics_caches - ProjectCacheWorker.perform_async(project.id, [], [:wiki_size]) - + def process_wiki_changes(post_received, wiki) user = identify_user(post_received) return false unless user # We only need to expire certain caches once per push - expire_caches(post_received, project.wiki.repository) + expire_caches(post_received, wiki.repository) + wiki.repository.expire_statistics_caches - ::Git::WikiPushService.new(project, user, changes: post_received.changes).execute + ::Git::WikiPushService.new(wiki, user, changes: post_received.changes).execute end def process_snippet_changes(post_received, snippet) |