diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-09 15:09:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-09 15:09:24 +0300 |
commit | 0ab6d56c15ebf4a12981556c7d3bc53d9b62cdb9 (patch) | |
tree | 31f28e85bb24de18240f3ddeaf5c3367e4510c3a /app | |
parent | 079ad2772f2b78f56b26730307cc73d1376fa6d6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
66 files changed, 567 insertions, 429 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index f24c52f61da..9420480e35a 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -58,6 +58,11 @@ export default { required: false, default: false, }, + currentIntegration: { + type: Object, + required: false, + default: null, + }, }, fields: [ { @@ -82,17 +87,16 @@ export default { integrationToDelete: integrationToDeleteDefault, }; }, - computed: { - tbodyTrClass() { - return { - [bodyTrClass]: this.integrations.length, - }; - }, - }, mounted() { this.trackPageViews(); }, methods: { + tbodyTrClass(item) { + return { + [bodyTrClass]: this.integrations.length, + 'gl-bg-blue-50': item?.id === this.currentIntegration?.id, + }; + }, trackPageViews() { const { category, action } = trackAlertIntegrationsViewsOptions; Tracking.event(category, action); diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue index 946da8ef34c..a08100f3938 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue @@ -33,6 +33,9 @@ export default { step1: { label: s__('AlertSettings|1. Select integration type'), help: s__('AlertSettings|Learn more about our upcoming %{linkStart}integrations%{linkEnd}'), + enterprise: s__( + 'AlertSettings|In free versions of GitLab, only one integration for each type can be added. %{linkStart}Upgrade your subscription%{linkEnd} to add additional integrations.', + ), }, step2: { label: s__('AlertSettings|2. Name integration'), @@ -107,6 +110,10 @@ export default { required: false, default: null, }, + canAddIntegration: { + type: Boolean, + required: true, + }, }, data() { return { @@ -236,15 +243,24 @@ export default { > <gl-form-select v-model="selectedIntegration" - :disabled="currentIntegration !== null" + :disabled="currentIntegration !== null || !canAddIntegration" :options="options" @change="integrationTypeSelect" /> - <alert-settings-form-help-block - :message="$options.i18n.integrationFormSteps.step1.help" - link="https://gitlab.com/groups/gitlab-org/-/epics/4390" - /> + <div class="gl-my-4"> + <alert-settings-form-help-block + :message="$options.i18n.integrationFormSteps.step1.help" + link="https://gitlab.com/groups/gitlab-org/-/epics/4390" + /> + </div> + + <div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported"> + <alert-settings-form-help-block + :message="$options.i18n.integrationFormSteps.step1.enterprise" + link="https://about.gitlab.com/pricing" + /> + </div> </gl-form-group> <gl-collapse v-model="formVisible" class="gl-mt-3"> <gl-form-group diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index e9e7b1407bc..57fc1984990 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -19,6 +19,12 @@ import { updateStoreAfterIntegrationDelete, updateStoreAfterIntegrationAdd, } from '../utils/cache_updates'; +import { + DELETE_INTEGRATION_ERROR, + ADD_INTEGRATION_ERROR, + RESET_INTEGRATION_TOKEN_ERROR, + UPDATE_INTEGRATION_ERROR, +} from '../utils/error_messages'; export default { typeSet, @@ -44,6 +50,9 @@ export default { projectPath: { default: '', }, + multiIntegrations: { + default: false, + }, }, apollo: { integrations: { @@ -91,6 +100,9 @@ export default { }, ]; }, + canAddIntegration() { + return this.multiIntegrations || this.integrations?.list?.length < 2; + }, }, methods: { createNewIntegration({ type, variables }) { @@ -121,8 +133,8 @@ export default { type: FLASH_TYPES.SUCCESS, }); }) - .catch(err => { - createFlash({ message: err }); + .catch(() => { + createFlash({ message: ADD_INTEGRATION_ERROR }); }) .finally(() => { this.isUpdating = false; @@ -151,8 +163,8 @@ export default { type: FLASH_TYPES.SUCCESS, }); }) - .catch(err => { - createFlash({ message: err }); + .catch(() => { + createFlash({ message: UPDATE_INTEGRATION_ERROR }); }) .finally(() => { this.isUpdating = false; @@ -187,8 +199,8 @@ export default { }); }, ) - .catch(err => { - createFlash({ message: err }); + .catch(() => { + createFlash({ message: RESET_INTEGRATION_TOKEN_ERROR }); }) .finally(() => { this.isUpdating = false; @@ -222,9 +234,8 @@ export default { type: FLASH_TYPES.SUCCESS, }); }) - .catch(err => { - this.errored = true; - createFlash({ message: err }); + .catch(() => { + createFlash({ message: DELETE_INTEGRATION_ERROR }); }) .finally(() => { this.isUpdating = false; @@ -242,6 +253,7 @@ export default { <integrations-list :integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld" :loading="loading" + :current-integration="currentIntegration" @edit-integration="editIntegration" @delete-integration="deleteIntegration" /> @@ -249,6 +261,7 @@ export default { v-if="glFeatures.httpIntegrationsList" :loading="isUpdating" :current-integration="currentIntegration" + :can-add-integration="canAddIntegration" @create-new-integration="createNewIntegration" @update-integration="updateIntegration" @reset-token="resetToken" diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index 9cf2f356e0a..19eaccf05fc 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -57,15 +57,6 @@ export const typeSet = { prometheus: 'PROMETHEUS', }; -export const defaultFormState = { - name: '', - active: false, - token: '', - url: '', - apiUrl: '', - integrationTestPayload: { json: null, error: null }, -}; - export const integrationToDeleteDefault = { id: null, name: '' }; export const JSON_VALIDATE_DELAY = 250; diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index 2ae0dd447a1..a9d109b3f3e 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -29,19 +29,16 @@ export default el => { opsgenieMvcEnabled, opsgenieMvcTargetUrl, projectPath, + multiIntegrations, } = el.dataset; - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - cacheConfig: {}, - }, - ), - }); + const resolvers = {}; - apolloProvider.clients.defaultClient.cache.writeData({ - data: {}, + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(resolvers, { + cacheConfig: {}, + assumeImmutableResults: true, + }), }); return new Vue({ @@ -70,6 +67,7 @@ export default el => { opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), }, projectPath, + multiIntegrations: parseBoolean(multiIntegrations), }, apolloProvider, components: { diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js index 2e6058fc81a..7df5d444a53 100644 --- a/app/assets/javascripts/alerts_settings/utils/error_messages.js +++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js @@ -7,3 +7,11 @@ export const DELETE_INTEGRATION_ERROR = s__( export const ADD_INTEGRATION_ERROR = s__( 'AlertsIntegrations|The integration could not be added. Please try again.', ); + +export const UPDATE_INTEGRATION_ERROR = s__( + 'AlertsIntegrations|The current integration could not be updated. Please try again.', +); + +export const RESET_INTEGRATION_TOKEN_ERROR = s__( + 'AlertsIntegrations|The integration token could not be reset. Please try again.', +); diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index cb0e6345059..233c5f84340 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -25,7 +25,7 @@ function importMermaidModule() { return import(/* webpackChunkName: 'mermaid' */ 'mermaid') .then(mermaid => { let theme = 'neutral'; - const ideDarkThemes = ['dark', 'solarized-dark']; + const ideDarkThemes = ['dark', 'solarized-dark', 'monokai']; if ( ideDarkThemes.includes(window.gon?.user_color_scheme) && diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue index a04b1361d4e..b2c3b6aa8ab 100644 --- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue +++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue @@ -26,7 +26,7 @@ export default { data() { return { participants: [], - selected: this.$store.getters.getActiveIssue.assignees, + selected: this.$store.getters.activeIssue.assignees, }; }, apollo: { @@ -34,7 +34,7 @@ export default { query: getIssueParticipants, variables() { return { - id: `gid://gitlab/Issue/${this.getActiveIssue.iid}`, + id: `gid://gitlab/Issue/${this.activeIssue.iid}`, }; }, update(data) { @@ -43,7 +43,7 @@ export default { }, }, computed: { - ...mapGetters(['getActiveIssue']), + ...mapGetters(['activeIssue']), assigneeText() { return n__('Assignee', '%d Assignees', this.selected.length); }, @@ -88,7 +88,7 @@ export default { <template> <board-editable-item :title="assigneeText" @close="saveAssignees"> <template #collapsed> - <issuable-assignees :users="getActiveIssue.assignees" /> + <issuable-assignees :users="activeIssue.assignees" /> </template> <template #default> diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index 7a468abddf1..c8b713b0c5a 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -46,7 +46,7 @@ export default { }; }, computed: { - ...mapGetters(['getIssues']), + ...mapGetters(['getIssuesByList']), showBoardListAndBoardInfo() { return this.list.type !== ListType.promotion; }, @@ -58,7 +58,7 @@ export default { if (!this.glFeatures.graphqlBoardLists) { return this.list.issues; } - return this.getIssues(this.list.id); + return this.getIssuesByList(this.list.id); }, shouldFetchIssues() { return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank; diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 06319df6ea5..80070b25bd0 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -69,14 +69,18 @@ export default { eventHub.$off('sidebar.closeAll', this.unsetActiveId); }, methods: { - ...mapActions(['unsetActiveId']), + ...mapActions(['unsetActiveId', 'removeList']), showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, deleteBoard() { // eslint-disable-next-line no-alert - if (window.confirm(__('Are you sure you want to delete this list?'))) { - this.activeList.destroy(); + if (window.confirm(__('Are you sure you want to remove this list?'))) { + if (this.shouldUseGraphQL) { + this.removeList(this.activeId); + } else { + this.activeList.destroy(); + } this.unsetActiveId(); } }, diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index c8926c5ef2a..47eee5306da 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -7,6 +7,7 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; import { fullLabelId } from '../boards_util'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import store from '~/boards/stores'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; @@ -61,7 +62,7 @@ export default function initNewListDropdown() { const active = boardsStore.findListByLabelId(label.id); const $li = $('<li />'); const $a = $('<a />', { - class: active ? `is-active js-board-list-${active.id}` : '', + class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '', text: label.title, href: '#', }); diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue index 97fe7572493..19e6f8a2269 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue @@ -18,7 +18,7 @@ export default { }; }, computed: { - ...mapGetters({ issue: 'getActiveIssue' }), + ...mapGetters({ issue: 'activeIssue' }), hasDueDate() { return this.issue.dueDate != null; }, diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue index 0f063c7582e..31094939733 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue @@ -21,7 +21,7 @@ export default { }, inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], computed: { - ...mapGetters({ issue: 'getActiveIssue' }), + ...mapGetters({ issue: 'activeIssue' }), selectedLabels() { const { labels = [] } = this.issue; diff --git a/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql new file mode 100644 index 00000000000..ef3fd36e980 --- /dev/null +++ b/app/assets/javascripts/boards/queries/board_list_destroy.mutation.graphql @@ -0,0 +1,5 @@ +mutation DestroyBoardList($listId: ID!) { + destroyBoardList(input: { listId: $listId }) { + errors + } +} diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index df5b84a974a..bbc7559cd86 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -18,6 +18,7 @@ import boardLabelsQuery from '../queries/board_labels.query.graphql'; import createBoardListMutation from '../queries/board_list_create.mutation.graphql'; import updateBoardListMutation from '../queries/board_list_update.mutation.graphql'; import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql'; +import destroyBoardListMutation from '../queries/board_list_destroy.mutation.graphql'; import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import issueSetLabels from '../queries/issue_set_labels.mutation.graphql'; import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql'; @@ -212,8 +213,26 @@ export default { }); }, - deleteList: () => { - notImplemented(); + removeList: ({ state, commit }, listId) => { + const listsBackup = { ...state.boardLists }; + + commit(types.REMOVE_LIST, listId); + + return gqlClient + .mutate({ + mutation: destroyBoardListMutation, + variables: { + listId, + }, + }) + .then(({ data: { destroyBoardList: { errors } } }) => { + if (errors.length > 0) { + commit(types.REMOVE_LIST_FAILURE, listsBackup); + } + }) + .catch(() => { + commit(types.REMOVE_LIST_FAILURE, listsBackup); + }); }, fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => { @@ -324,7 +343,7 @@ export default { }, setActiveIssueLabels: async ({ commit, getters }, input) => { - const activeIssue = getters.getActiveIssue; + const { activeIssue } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetLabels, variables: { @@ -349,7 +368,7 @@ export default { }, setActiveIssueDueDate: async ({ commit, getters }, input) => { - const activeIssue = getters.getActiveIssue; + const { activeIssue } = getters; const { data } = await gqlClient.mutate({ mutation: issueSetDueDate, variables: { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index d1a5db1bcc5..337b2897fe9 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,7 +1,6 @@ /* eslint-disable no-shadow, no-param-reassign,consistent-return */ /* global List */ /* global ListIssue */ -import $ from 'jquery'; import { sortBy, pick } from 'lodash'; import Vue from 'vue'; import Cookies from 'js-cookie'; @@ -119,8 +118,12 @@ const boardsStore = { // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821 }); }, + updateNewListDropdown(listId) { - $(`.js-board-list-${listId}`).removeClass('is-active'); + // eslint-disable-next-line no-unused-expressions + document + .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`) + ?.classList.remove('is-active'); }, shouldAddBlankState() { // Decide whether to add the blank state diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index 89a3b14b262..f717b4101ab 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -2,7 +2,7 @@ import { find } from 'lodash'; import { inactiveId } from '../constants'; export default { - getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'), + labelToggleState: state => (state.isShowingLabels ? 'on' : 'off'), isSidebarOpen: state => state.activeId !== inactiveId, isSwimlanesOn: state => { if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) { @@ -15,12 +15,12 @@ export default { return state.issues[id] || {}; }, - getIssues: (state, getters) => listId => { + getIssuesByList: (state, getters) => listId => { const listIssueIds = state.issuesByListId[listId] || []; return listIssueIds.map(id => getters.getIssueById(id)); }, - getActiveIssue: state => { + activeIssue: state => { return state.issues[state.activeId] || {}; }, diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index d5ddba710a9..29468105b5c 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -12,9 +12,8 @@ export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; export const MOVE_LIST = 'MOVE_LIST'; export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; -export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST'; -export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS'; -export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR'; +export const REMOVE_LIST = 'REMOVE_LIST'; +export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE'; export const REQUEST_ISSUES_FOR_LIST = 'REQUEST_ISSUES_FOR_LIST'; export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE'; export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 361bb94abe0..eb2003a6ed3 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -93,16 +93,13 @@ export default { Vue.set(state, 'boardLists', backupList); }, - [mutationTypes.REQUEST_REMOVE_LIST]: () => { - notImplemented(); + [mutationTypes.REMOVE_LIST]: (state, listId) => { + Vue.delete(state.boardLists, listId); }, - [mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => { - notImplemented(); + [mutationTypes.REMOVE_LIST_FAILURE](state, listsBackup) { + state.error = s__('Boards|An error occurred while removing the list. Please try again.'); + state.boardLists = listsBackup; }, [mutationTypes.REQUEST_ISSUES_FOR_LIST]: (state, { listId, fetchNext }) => { diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js index 7a81d5963f2..5487aeb9391 100644 --- a/app/assets/javascripts/graphql_shared/utils.js +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -7,7 +7,7 @@ * @returns {Number} */ export const getIdFromGraphQLId = (gid = '') => - parseInt((gid || '').replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null; + parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10) || null; export const MutationOperationMode = { Append: 'APPEND', diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 54365df2119..e1d2895831a 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,5 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { WEBIDE_MARK_APP_START, @@ -14,15 +13,8 @@ import { import { performanceMarkAndMeasure } from '~/performance/utils'; import { modalTypes } from '../constants'; import eventHub from '../eventhub'; -import FindFile from '~/vue_shared/components/file_finder/index.vue'; -import NewModal from './new_dropdown/modal.vue'; import IdeSidebar from './ide_side_bar.vue'; -import RepoTabs from './repo_tabs.vue'; -import IdeStatusBar from './ide_status_bar.vue'; import RepoEditor from './repo_editor.vue'; -import RightPane from './panes/right.vue'; -import ErrorMessage from './error_message.vue'; -import CommitEditorHeader from './commit_sidebar/editor_header.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { measurePerformance } from '../utils'; @@ -43,19 +35,24 @@ eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () => export default { components: { - NewModal, IdeSidebar, - RepoTabs, - IdeStatusBar, RepoEditor, - FindFile, - ErrorMessage, - CommitEditorHeader, - GlButton, - GlLoadingIcon, - RightPane, + 'error-message': () => import('./error_message.vue'), + 'gl-button': () => import('@gitlab/ui/src/components/base/button/button.vue'), + 'gl-loading-icon': () => import('@gitlab/ui/src/components/base/loading_icon/loading_icon.vue'), + 'commit-editor-header': () => import('./commit_sidebar/editor_header.vue'), + 'repo-tabs': () => import('./repo_tabs.vue'), + 'ide-status-bar': () => import('./ide_status_bar.vue'), + 'find-file': () => import('~/vue_shared/components/file_finder/index.vue'), + 'right-pane': () => import('./panes/right.vue'), + 'new-modal': () => import('./new_dropdown/modal.vue'), }, mixins: [glFeatureFlagsMixin()], + data() { + return { + loadDeferred: false, + }; + }, computed: { ...mapState([ 'openFiles', @@ -107,6 +104,9 @@ export default { createNewFile() { this.$refs.newModal.open(modalTypes.blob); }, + loadDeferredComponents() { + this.loadDeferred = true; + }, }, }; </script> @@ -118,19 +118,23 @@ export default { > <error-message v-if="errorMessage" :message="errorMessage" /> <div class="ide-view flex-grow d-flex"> - <find-file - v-show="fileFindVisible" - :files="allBlobs" - :visible="fileFindVisible" - :loading="loading" - @toggle="toggleFileFinder" - @click="openFile" - /> - <ide-sidebar /> + <template v-if="loadDeferred"> + <find-file + v-show="fileFindVisible" + :files="allBlobs" + :visible="fileFindVisible" + :loading="loading" + @toggle="toggleFileFinder" + @click="openFile" + /> + </template> + <ide-sidebar @tree-ready="loadDeferredComponents" /> <div class="multi-file-edit-pane"> <template v-if="activeFile"> - <commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" /> - <repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" /> + <template v-if="loadDeferred"> + <commit-editor-header v-if="isCommitModeActive" :active-file="activeFile" /> + <repo-tabs v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" /> + </template> <repo-editor :file="activeFile" class="multi-file-edit-pane-content" /> </template> <template v-else> @@ -177,9 +181,13 @@ export default { </div> </template> </div> - <right-pane v-if="currentProjectId" /> + <template v-if="loadDeferred"> + <right-pane v-if="currentProjectId" /> + </template> </div> - <ide-status-bar /> - <new-modal ref="newModal" /> + <template v-if="loadDeferred"> + <ide-status-bar /> + <new-modal ref="newModal" /> + </template> </article> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 53dfc133fc8..99215d6c3f1 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -4,21 +4,19 @@ import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; -import RepoCommitSection from './repo_commit_section.vue'; import CommitForm from './commit_sidebar/form.vue'; -import IdeReview from './ide_review.vue'; import IdeProjectHeader from './ide_project_header.vue'; -import { SIDEBAR_INIT_WIDTH } from '../constants'; +import { SIDEBAR_INIT_WIDTH, leftSidebarViews } from '../constants'; export default { components: { GlSkeletonLoading, ResizablePanel, ActivityBar, - RepoCommitSection, IdeTree, + [leftSidebarViews.review.name]: () => import('./ide_review.vue'), + [leftSidebarViews.commit.name]: () => import('./repo_commit_section.vue'), CommitForm, - IdeReview, IdeProjectHeader, }, computed: { @@ -49,7 +47,7 @@ export default { <div class="multi-file-commit-panel-inner" data-testid="ide-side-bar-inner"> <div class="multi-file-commit-panel-inner-content"> <keep-alive> - <component :is="currentActivityView" /> + <component :is="currentActivityView" @tree-ready="$emit('tree-ready')" /> </keep-alive> </div> <commit-form /> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 56fcb6c2600..e563de6659a 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -51,7 +51,7 @@ export default { </script> <template> - <ide-tree-list> + <ide-tree-list @tree-ready="$emit('tree-ready')"> <template #header> {{ __('Edit') }} <div class="ide-tree-actions ml-auto d-flex" data-testid="ide-root-actions"> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index d7ff1b8316f..e7e94f5b5da 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -32,6 +32,13 @@ export default { return !this.currentTree || this.currentTree.loading; }, }, + watch: { + showLoading(newVal) { + if (!newVal) { + this.$emit('tree-ready'); + } + }, + }, beforeCreate() { performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START }); }, diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 96cb024c768..61e5db0970a 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -136,6 +136,16 @@ export default { type: String, required: true, }, + isConfidential: { + type: Boolean, + required: false, + default: false, + }, + isLocked: { + type: Boolean, + required: false, + default: false, + }, issuableType: { type: String, required: false, @@ -453,6 +463,12 @@ export default { <gl-icon :name="statusIcon" class="gl-display-block d-sm-none gl-h-6!" /> <span class="gl-display-none d-sm-block">{{ statusText }}</span> </p> + <span v-if="isLocked" data-testid="locked" class="issuable-warning-icon"> + <gl-icon name="lock" :aria-label="__('Locked')" /> + </span> + <span v-if="isConfidential" data-testid="confidential" class="issuable-warning-icon"> + <gl-icon name="eye-slash" :aria-label="__('Confidential')" /> + </span> <p class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0" :title="state.titleText" diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index fc9e8e051bb..b5cd466596e 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -17,6 +17,8 @@ export function initIssuableApp(issuableData, store) { return createElement(IssuableApp, { props: { ...issuableData, + isConfidential: this.getNoteableData?.confidential, + isLocked: this.getNoteableData?.discussion_locked, issuableStatus: this.getNoteableData?.state, }, }); diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 878a748e99a..0272790a75d 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -45,7 +45,7 @@ export default { return this.discussion.notes.filter(x => x.resolvable); }, userCanResolveDiscussion() { - return this.resolvableNotes.every(note => note.current_user && note.current_user.can_resolve); + return this.resolvableNotes.every(note => note.current_user?.can_resolve_discussion); }, }, }; diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 4b3f23e742d..43f17c5d65c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -121,7 +121,13 @@ export default { return this.withBatchComments && this.noteId === '' && !this.discussion.for_commit; }, showResolveDiscussionToggle() { - return (this.discussion?.id && this.discussion.resolvable) || this.isDraft; + if (!this.discussion?.notes) return false; + + return ( + this.discussion?.notes + .filter(n => n.resolvable) + .some(n => n.current_user?.can_resolve_discussion) || this.isDraft + ); }, noteHash() { if (this.noteId) { diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 1775e83978e..9be53fe60f2 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -141,6 +141,8 @@ export default { canResolve() { if (this.glFeatures.removeResolveNote && !this.discussionRoot) return false; + if (this.glFeatures.removeResolveNote) return this.note.current_user.can_resolve_discussion; + return ( this.note.current_user.can_resolve || (this.note.isDraft && this.note.discussion_id !== null) diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 09f40601fbf..4ed0aae0d1e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -132,24 +132,26 @@ export default { <div class="ci-job-component" data-qa-selector="job_item_container"> <gl-link v-if="status.has_details" - v-gl-tooltip="{ boundary, placement: 'bottom' }" + v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" :href="status.details_path" :title="tooltipText" :class="jobClasses" class="js-pipeline-graph-job-link qa-job-link menu-item" data-testid="job-with-link" @click.stop="hideTooltips" + @mouseout="hideTooltips" > <job-name-component :name="job.name" :status="job.status" /> </gl-link> <div v-else - v-gl-tooltip="{ boundary, placement: 'bottom' }" + v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" :title="tooltipText" :class="jobClasses" class="js-job-component-tooltip non-details-job-component" data-testid="job-without-link" + @mouseout="hideTooltips" > <job-name-component :name="job.name" :status="job.status" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 00ccf59e37c..9ee427d01fd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -337,7 +337,7 @@ export default { :message="emptyTabMessage" /> - <div v-else-if="stateToRender === $options.stateMap.tableList" class="table-holder"> + <div v-else-if="stateToRender === $options.stateMap.tableList"> <pipelines-table-component :pipelines="state.pipelines" :pipeline-schedule-url="pipelineScheduleUrl" diff --git a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue b/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue deleted file mode 100644 index 08619fa2066..00000000000 --- a/app/assets/javascripts/search/dropdown_filter/components/dropdown_filter.vue +++ /dev/null @@ -1,108 +0,0 @@ -<script> -import { mapState } from 'vuex'; -import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; -import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; -import { sprintf, s__ } from '~/locale'; - -export default { - name: 'DropdownFilter', - components: { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - }, - props: { - filterData: { - type: Object, - required: true, - }, - }, - computed: { - ...mapState(['query']), - scope() { - return this.query.scope; - }, - supportedScopes() { - return Object.values(this.filterData.scopes); - }, - initialFilter() { - return this.query[this.filterData.filterParam]; - }, - filter() { - return this.initialFilter || this.filterData.filters.ANY.value; - }, - filtersArray() { - return this.filterData.filterByScope[this.scope]; - }, - selectedFilter: { - get() { - if (this.filtersArray.some(({ value }) => value === this.filter)) { - return this.filter; - } - - return this.filterData.filters.ANY.value; - }, - set(filter) { - // we need to remove the pagination cursor to ensure the - // relevancy of the new results - - visitUrl( - setUrlParams({ - page: null, - [this.filterData.filterParam]: filter, - }), - ); - }, - }, - selectedFilterText() { - const f = this.filtersArray.find(({ value }) => value === this.selectedFilter); - if (!f || f === this.filterData.filters.ANY) { - return sprintf(s__('Any %{header}'), { header: this.filterData.header }); - } - - return f.label; - }, - showDropdown() { - return this.supportedScopes.includes(this.scope); - }, - }, - methods: { - dropDownItemClass(filter) { - return { - 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2': - filter === this.filterData.filters.ANY, - }; - }, - isFilterSelected(filter) { - return filter === this.selectedFilter; - }, - handleFilterChange(filter) { - this.selectedFilter = filter; - }, - }, -}; -</script> - -<template> - <gl-dropdown - v-if="showDropdown" - :text="selectedFilterText" - class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4" - menu-class="gl-w-full! gl-pl-0" - > - <header class="gl-text-center gl-font-weight-bold gl-font-lg"> - {{ filterData.header }} - </header> - <gl-dropdown-divider /> - <gl-dropdown-item - v-for="f in filtersArray" - :key="f.value" - :is-check-item="true" - :is-checked="isFilterSelected(f.value)" - :class="dropDownItemClass(f)" - @click="handleFilterChange(f.value)" - > - {{ f.label }} - </gl-dropdown-item> - </gl-dropdown> -</template> diff --git a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js deleted file mode 100644 index b29daca89cb..00000000000 --- a/app/assets/javascripts/search/dropdown_filter/constants/confidential_filter_data.js +++ /dev/null @@ -1,36 +0,0 @@ -import { __ } from '~/locale'; - -const header = __('Confidentiality'); - -const filters = { - ANY: { - label: __('Any'), - value: null, - }, - CONFIDENTIAL: { - label: __('Confidential'), - value: 'yes', - }, - NOT_CONFIDENTIAL: { - label: __('Not confidential'), - value: 'no', - }, -}; - -const scopes = { - ISSUES: 'issues', -}; - -const filterByScope = { - [scopes.ISSUES]: [filters.ANY, filters.CONFIDENTIAL, filters.NOT_CONFIDENTIAL], -}; - -const filterParam = 'confidential'; - -export default { - header, - filters, - scopes, - filterByScope, - filterParam, -}; diff --git a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js b/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js deleted file mode 100644 index 0b93aa0be29..00000000000 --- a/app/assets/javascripts/search/dropdown_filter/constants/state_filter_data.js +++ /dev/null @@ -1,42 +0,0 @@ -import { __ } from '~/locale'; - -const header = __('Status'); - -const filters = { - ANY: { - label: __('Any'), - value: 'all', - }, - OPEN: { - label: __('Open'), - value: 'opened', - }, - CLOSED: { - label: __('Closed'), - value: 'closed', - }, - MERGED: { - label: __('Merged'), - value: 'merged', - }, -}; - -const scopes = { - ISSUES: 'issues', - MERGE_REQUESTS: 'merge_requests', -}; - -const filterByScope = { - [scopes.ISSUES]: [filters.ANY, filters.OPEN, filters.CLOSED], - [scopes.MERGE_REQUESTS]: [filters.ANY, filters.OPEN, filters.MERGED, filters.CLOSED], -}; - -const filterParam = 'state'; - -export default { - header, - filters, - scopes, - filterByScope, - filterParam, -}; diff --git a/app/assets/javascripts/search/dropdown_filter/index.js b/app/assets/javascripts/search/dropdown_filter/index.js deleted file mode 100644 index e5e0745d990..00000000000 --- a/app/assets/javascripts/search/dropdown_filter/index.js +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; -import DropdownFilter from './components/dropdown_filter.vue'; -import stateFilterData from './constants/state_filter_data'; -import confidentialFilterData from './constants/confidential_filter_data'; - -Vue.use(Translate); - -const mountDropdownFilter = (store, { id, filterData }) => { - const el = document.getElementById(id); - - if (!el) return false; - - return new Vue({ - el, - store, - render(createElement) { - return createElement(DropdownFilter, { - props: { - filterData, - }, - }); - }, - }); -}; - -const dropdownFilters = [ - { - id: 'js-search-filter-by-state', - filterData: stateFilterData, - }, - { - id: 'js-search-filter-by-confidential', - filterData: confidentialFilterData, - }, -]; - -export default store => [...dropdownFilters].map(filter => mountDropdownFilter(store, filter)); diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js index 275d6351adc..7508b3c9a55 100644 --- a/app/assets/javascripts/search/index.js +++ b/app/assets/javascripts/search/index.js @@ -1,17 +1,11 @@ import { queryToObject } from '~/lib/utils/url_utility'; import createStore from './store'; -import initDropdownFilters from './dropdown_filter'; import { initSidebar } from './sidebar'; import initGroupFilter from './group_filter'; export default () => { const store = createStore({ query: queryToObject(window.location.search) }); - if (gon.features.searchFacets) { - initSidebar(store); - } else { - initDropdownFilters(store); - } - + initSidebar(store); initGroupFilter(store); }; diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue new file mode 100644 index 00000000000..0c50f93d381 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -0,0 +1,41 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlButton, GlLink } from '@gitlab/ui'; +import StatusFilter from './status_filter.vue'; +import ConfidentialityFilter from './confidentiality_filter.vue'; + +export default { + name: 'GlobalSearchSidebar', + components: { + GlButton, + GlLink, + StatusFilter, + ConfidentialityFilter, + }, + computed: { + ...mapState(['query']), + showReset() { + return this.query.state || this.query.confidential; + }, + }, + methods: { + ...mapActions(['applyQuery', 'resetQuery']), + }, +}; +</script> + +<template> + <form + class="gl-display-flex gl-flex-direction-column col-md-3 gl-mr-4 gl-mb-6 gl-mb gl-mt-5" + @submit.prevent="applyQuery" + > + <status-filter /> + <confidentiality-filter /> + <div class="gl-display-flex gl-align-items-center gl-mt-3"> + <gl-button variant="success" type="submit">{{ __('Apply') }}</gl-button> + <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{ + __('Reset filters') + }}</gl-link> + </div> + </form> +</template> diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue index f8938e799aa..38dccb9675d 100644 --- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue @@ -21,5 +21,6 @@ export default { <template> <div v-if="showDropdown"> <radio-filter :filter-data="$options.confidentialFilterData" /> + <hr class="gl-my-5 gl-border-gray-100" /> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue index 876123ccc52..5cec2090906 100644 --- a/app/assets/javascripts/search/sidebar/components/status_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue @@ -21,5 +21,6 @@ export default { <template> <div v-if="showDropdown"> <radio-filter :filter-data="$options.stateFilterData" /> + <hr class="gl-my-5 gl-border-gray-100" /> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js index b19016edf3d..6419e8ac2c6 100644 --- a/app/assets/javascripts/search/sidebar/index.js +++ b/app/assets/javascripts/search/sidebar/index.js @@ -1,12 +1,11 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import StatusFilter from './components/status_filter.vue'; -import ConfidentialityFilter from './components/confidentiality_filter.vue'; +import GlobalSearchSidebar from './components/app.vue'; Vue.use(Translate); -const mountRadioFilters = (store, { id, component }) => { - const el = document.getElementById(id); +export const initSidebar = store => { + const el = document.getElementById('js-search-sidebar'); if (!el) return false; @@ -14,21 +13,7 @@ const mountRadioFilters = (store, { id, component }) => { el, store, render(createElement) { - return createElement(component); + return createElement(GlobalSearchSidebar); }, }); }; - -const radioFilters = [ - { - id: 'js-search-filter-by-state', - component: StatusFilter, - }, - { - id: 'js-search-filter-by-confidential', - component: ConfidentialityFilter, - }, -]; - -export const initSidebar = store => - [...radioFilters].map(filter => mountRadioFilters(store, filter)); diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 722ed2eec26..447278aa223 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -1,6 +1,7 @@ import Api from '~/api'; import createFlash from '~/flash'; import { __ } from '~/locale'; +import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import * as types from './mutation_types'; export const fetchGroups = ({ commit }, search) => { @@ -18,3 +19,11 @@ export const fetchGroups = ({ commit }, search) => { export const setQuery = ({ commit }, { key, value }) => { commit(types.SET_QUERY, { key, value }); }; + +export const applyQuery = ({ state }) => { + visitUrl(setUrlParams({ ...state.query, page: null })); +}; + +export const resetQuery = ({ state }) => { + visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null })); +}; diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index 034c8dea012..09bc176c399 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -37,6 +37,14 @@ export default { required: false, default: '', }, + branch: { + type: String, + required: true, + }, + baseUrl: { + type: String, + required: true, + }, mounts: { type: Array, required: true, @@ -75,7 +83,7 @@ export default { return this.editorMode === EDITOR_TYPES.wysiwyg; }, customRenderers() { - const imageRenderer = renderImage.build(this.mounts, this.project); + const imageRenderer = renderImage.build(this.mounts, this.project, this.branch, this.baseUrl); return { image: [imageRenderer], }; diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql index 4842dfcda49..e422a4b6036 100644 --- a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql +++ b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql @@ -6,6 +6,8 @@ query appData { sourcePath username returnUrl + branch + baseUrl mounts { source target diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql index fdac87cd91c..00af6c10359 100644 --- a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql +++ b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql @@ -26,6 +26,8 @@ type AppData { returnUrl: String sourcePath: String! username: String! + branch: String! + baseUrl: String! mounts: [Mount]! imageUploadPath: String! } diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index dd684429794..b58564388de 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -9,6 +9,7 @@ const initStaticSiteEditor = el => { isSupportedContent, path: sourcePath, baseUrl, + branch, namespace, project, mergeRequestsIllustrationPath, @@ -27,6 +28,8 @@ const initStaticSiteEditor = el => { hasSubmittedChanges: false, project: `${namespace}/${project}`, mounts: JSON.parse(mounts), // NOTE that the object in 'mounts' is a JSON string from the data attribute, so it must be parsed into an object. + branch, + baseUrl, returnUrl, sourcePath, username, diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue index 3e2e6e3b3ee..b61e0968d7d 100644 --- a/app/assets/javascripts/static_site_editor/pages/home.vue +++ b/app/assets/javascripts/static_site_editor/pages/home.vue @@ -139,6 +139,8 @@ export default { :saving-changes="isSavingChanges" :return-url="appData.returnUrl" :mounts="appData.mounts" + :branch="appData.branch" + :base-url="appData.baseUrl" :project="appData.project" :image-root="appData.imageUploadPath" @submit="onPrepareSubmit" diff --git a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js index 38304a1c57f..5e5e5d1c860 100644 --- a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js +++ b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js @@ -1,29 +1,80 @@ +import { isAbsolute, getBaseURL, joinPaths } from '~/lib/utils/url_utility'; + const canRender = ({ type }) => type === 'image'; -// NOTE: the `metadata` is not used yet, but will be used in a follow-up iteration -// To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531 -// eslint-disable-next-line no-unused-vars let metadata; -const render = (node, { skipChildren }) => { - skipChildren(); +const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/'); + +const extractSourceDirectory = url => { + const sourceDir = /^(.+)\/([^/]+)$/.exec(url); // Extracts the base path and fileName from an image path + return sourceDir || [null, null, url]; // If no source directory was extracted it means only a fileName was specified (e.g. url='file.png') +}; + +const parseCurrentDirectory = basePath => { + const baseUrl = decodeURIComponent(metadata.baseUrl); + const sourceDirectory = extractSourceDirectory(baseUrl)[1]; + const currentDirectory = sourceDirectory.split(`/-/sse/${metadata.branch}`)[1]; + + return joinPaths(currentDirectory, basePath); +}; + +// For more context around this logic, please see the following comment: +// https://gitlab.com/gitlab-org/gitlab/-/issues/241166#note_409413500 +const generateSourceDirectory = basePath => { + let sourceDir = ''; + let defaultSourceDir = ''; + + if (!basePath || isRelativeToCurrentDirectory(basePath)) { + return parseCurrentDirectory(basePath); + } + + if (!metadata.mounts.length) { + return basePath; + } - // To be removed with the next iteration of https://gitlab.com/gitlab-org/gitlab/-/issues/218531 - // TODO resolve relative paths + metadata.mounts.forEach(({ source, target }) => { + const hasTarget = target !== ''; + + if (hasTarget && basePath.includes(target)) { + sourceDir = source; + } else if (!hasTarget) { + defaultSourceDir = joinPaths(source, basePath); + } + }); + + return sourceDir || defaultSourceDir; +}; + +const resolveFullPath = originalSrc => { + if (isAbsolute(originalSrc)) { + return originalSrc; + } + + const sourceDirectory = extractSourceDirectory(originalSrc); + const [, basePath, fileName] = sourceDirectory; + const sourceDir = generateSourceDirectory(basePath); + + return joinPaths(getBaseURL(), metadata.project, '/-/raw/', metadata.branch, sourceDir, fileName); +}; + +const render = ({ destination: originalSrc, firstChild }, { skipChildren }) => { + skipChildren(); return { type: 'openTag', tagName: 'img', selfClose: true, attributes: { - src: node.destination, - alt: node.firstChild.literal, + 'data-original-src': !isAbsolute(originalSrc) ? originalSrc : '', + src: resolveFullPath(originalSrc), + alt: firstChild.literal, }, }; }; -const build = (mounts, project) => { - metadata = { mounts, project }; +const build = (mounts = [], project, branch, baseUrl) => { + metadata = { mounts, project, branch, baseUrl }; return { canRender, render }; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js index 2bce691e793..9744e25a8e1 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js @@ -99,6 +99,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => ? `\n\n${node.innerText}\n\n` : baseRenderer.convert(node, subContent); }, + IMG(node) { + const { originalSrc } = node.dataset; + return `![${node.alt}](${originalSrc || node.src})`; + }, }; }; diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index 14274c0747a..52cc7d3449e 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -151,7 +151,7 @@ border-color: var(--ide-border-color-alt, $gray-100); code { - background-color: var(--ide-border-color, inherit); + background-color: var(--ide-empty-state-background, inherit); } } @@ -427,7 +427,7 @@ } .md table:not(.code) tbody { - background-color: var(--ide-border-color, $white); + background-color: var(--ide-empty-state-background, $white); } .animation-container { diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 54cd267b993..15cc10d1532 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -5,7 +5,9 @@ @import './ide_theme_overrides'; @import './ide_themes/dark'; +@import './ide_themes/solarized-light'; @import './ide_themes/solarized-dark'; +@import './ide_themes/monokai'; $search-list-icon-width: 18px; $ide-activity-bar-width: 60px; @@ -176,11 +178,11 @@ $ide-commit-header-height: 48px; height: 100%; overflow: auto; padding: $gl-padding; - background-color: var(--ide-border-color, transparent); + background-color: var(--ide-empty-state-background, transparent); } .file-container { - background-color: var(--ide-border-color, $gray-darker); + background-color: var(--ide-empty-state-background, $gray-darker); display: flex; height: 100%; align-items: center; @@ -491,7 +493,7 @@ $ide-commit-header-height: 48px; height: 100vh; align-items: center; justify-content: center; - background-color: var(--ide-border-color, transparent); + background-color: var(--ide-empty-state-background, transparent); } .ide { diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss index 5f31d554c19..c7aae77c412 100644 --- a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss +++ b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss @@ -12,6 +12,7 @@ --ide-highlight-background: #252526; --ide-link-color: #428fdc; --ide-footer-background: #060606; + --ide-empty-state-background: var(--ide-border-color); --ide-input-border: #868686; --ide-input-background: transparent; diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss b/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss new file mode 100644 index 00000000000..f53ace0b6c2 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss @@ -0,0 +1,66 @@ +// ------- +// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes +// ------- +.ide.theme-monokai { + --ide-border-color: #1a1a18; + --ide-border-color-alt: #3f4237; + --ide-highlight-accent: #fff; + --ide-text-color: #ccc; + --ide-text-color-secondary: #b7b7b7; + --ide-background: #282822; + --ide-background-hover: #2d2d2d; + --ide-highlight-background: #1f1f1d; + --ide-link-color: #428fdc; + --ide-footer-background: #404338; + --ide-empty-state-background: #1a1a18; + + --ide-input-border: #7d8175; + --ide-input-background: transparent; + --ide-input-color: #fff; + + --ide-btn-default-background: transparent; + --ide-btn-default-border: #7d8175; + --ide-btn-default-hover-border: #b5bda5; + --ide-btn-default-hover-border-width: 2px; + --ide-btn-default-focus-box-shadow: 0 0 0 1px #bfbfbf; + + --ide-btn-primary-background: #1068bf; + --ide-btn-primary-border: #428fdc; + --ide-btn-primary-hover-border: #63a6e9; + --ide-btn-primary-hover-border-width: 2px; + --ide-btn-primary-focus-box-shadow: 0 0 0 1px #63a6e9; + + --ide-btn-success-background: #217645; + --ide-btn-success-border: #108548; + --ide-btn-success-hover-border: #2da160; + --ide-btn-success-hover-border-width: 2px; + --ide-btn-success-focus-box-shadow: 0 0 0 1px #2da160; + + // Danger styles should be the same as default styles in dark theme + --ide-btn-danger-secondary-background: var(--ide-btn-default-background); + --ide-btn-danger-secondary-border: var(--ide-btn-default-border); + --ide-btn-danger-secondary-hover-border: var(--ide-btn-default-hover-border); + --ide-btn-danger-secondary-hover-border-width: var(--ide-btn-default-hover-border-width); + --ide-btn-danger-secondary-focus-box-shadow: var(--ide-btn-default-focus-box-shadow); + + --ide-btn-disabled-background: transparent; + --ide-btn-disabled-border: rgba(223, 223, 223, 0.24); + --ide-btn-disabled-hover-border: rgba(223, 223, 223, 0.24); + --ide-btn-disabled-hover-border-width: 1px; + --ide-btn-disabled-focus-box-shadow: 0 0 0 0 transparent; + --ide-btn-disabled-color: rgba(145, 145, 145, 0.48); + + --ide-dropdown-background: #36382f; + --ide-dropdown-hover-background: #404338; + + --ide-dropdown-btn-hover-border: #b5bda5; + --ide-dropdown-btn-hover-background: #3f4237; + + --ide-file-row-btn-hover-background: #404338; + + --ide-diff-insert: rgba(155, 185, 85, 0.2); + --ide-diff-remove: rgba(255, 0, 0, 0.2); + + --ide-animation-gradient-1: #404338; + --ide-animation-gradient-2: #36382f; +} diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss index 83c55310063..1906b3ca938 100644 --- a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss +++ b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss @@ -12,6 +12,7 @@ --ide-highlight-background: #003240; --ide-link-color: #73b9ff; --ide-footer-background: var(--ide-highlight-background); + --ide-empty-state-background: var(--ide-border-color); --ide-input-border: #d8d8d8; --ide-input-background: transparent; diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss new file mode 100644 index 00000000000..315a0ae6202 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss @@ -0,0 +1,57 @@ +// ------- +// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes +// ------- +.ide.theme-solarized-light { + --ide-border-color: #dfd7bf; + --ide-border-color-alt: #dfd7bf; + --ide-highlight-accent: #5c4e21; + --ide-text-color: #616161; + --ide-text-color-secondary: #526f76; + --ide-background: #efe8d3; + --ide-background-hover: #ded6be; + --ide-highlight-background: #fef6e1; + --ide-link-color: #955800; + --ide-footer-background: #efe8d3; + --ide-empty-state-background: #fef6e1; + + --ide-input-border: #c0b9a4; + --ide-input-background: transparent; + + --ide-btn-default-background: transparent; + --ide-btn-default-border: #c0b9a4; + --ide-btn-default-hover-border: #c0b9a4; + + --ide-btn-primary-background: #b16802; + --ide-btn-primary-border: #a35f00; + --ide-btn-primary-hover-border: #955800; + --ide-btn-primary-hover-border-width: 2px; + --ide-btn-primary-focus-box-shadow: 0 0 0 1px #dd8101; + + --ide-btn-danger-secondary-background: transparent; + + --ide-btn-disabled-background: transparent; + --ide-btn-disabled-border: rgba(192, 185, 64, 0.48); + --ide-btn-disabled-hover-border: rgba(192, 185, 64, 0.48); + --ide-btn-disabled-hover-border-width: 1px; + --ide-btn-disabled-focus-box-shadow: transparent; + --ide-btn-disabled-color: rgba(82, 82, 82, 0.48); + + --ide-dropdown-background: #fef6e1; + --ide-dropdown-hover-background: #efe8d3; + + --ide-dropdown-btn-hover-border: #dfd7bf; + --ide-dropdown-btn-hover-background: #efe8d3; + + --ide-file-row-btn-hover-background: #ded6be; + + --ide-animation-gradient-1: #d3cbb3; + --ide-animation-gradient-2: #efe8d3; + + .ide-empty-state, + .ide-sidebar, + .ide-commit-empty-state { + img { + filter: sepia(1) brightness(0.7); + } + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 00d32b75628..cc4827f75d4 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,16 +1,12 @@ .issuable-warning-icon { background-color: $orange-50; border-radius: $border-radius-default; + color: $orange-600; width: $issuable-warning-size; height: $issuable-warning-size; text-align: center; margin-right: $issuable-warning-icon-margin; line-height: $gl-line-height-24; - - .icon { - fill: $orange-600; - vertical-align: text-bottom; - } } .limit-container-width { diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index b005347c43a..a45205c5da7 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -9,9 +9,13 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def create - @personal_access_token = finder.build(personal_access_token_params) + result = ::PersonalAccessTokens::CreateService.new( + current_user: current_user, target_user: current_user, params: personal_access_token_params + ).execute - if @personal_access_token.save + @personal_access_token = result.payload[:personal_access_token] + + if result.success? PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token) redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.") else diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 0f149c24a59..4b21edc98d5 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -24,10 +24,6 @@ class SearchController < ApplicationController search_term_present && !params[:project_id].present? end - before_action do - push_frontend_feature_flag(:search_facets) - end - layout 'search' feature_category :global_search diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index 3e802156c8f..8105fce10cf 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -30,7 +30,8 @@ module OperationsHelper 'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), 'alerts_usage_url' => project_alert_management_index_path(@project), 'disabled' => disabled.to_s, - 'project_path' => @project.full_path + 'project_path' => @project.full_path, + 'multi_integrations' => 'false' } end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 61030b6d5e6..70e8fb32064 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -27,6 +27,7 @@ class UserPolicy < BasePolicy rule { default }.enable :read_user_profile rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile rule { user_is_self | admin }.enable :disable_two_factor + rule { (user_is_self | admin) & ~blocked }.enable :create_user_personal_access_token end UserPolicy.prepend_if_ee('EE::UserPolicy') diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index ef305195e22..cf7a71a11d0 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -34,6 +34,10 @@ class NoteEntity < API::Entities::Note expose :can_resolve do |note| note.resolvable? && can?(current_user, :resolve_note, note) end + + expose :can_resolve_discussion do |note| + note.discussion.resolvable? && note.discussion.can_resolve?(current_user) + end end expose :suggestions, using: SuggestionEntity diff --git a/app/services/personal_access_tokens/create_service.rb b/app/services/personal_access_tokens/create_service.rb index ff9bb7d6802..93a0135669f 100644 --- a/app/services/personal_access_tokens/create_service.rb +++ b/app/services/personal_access_tokens/create_service.rb @@ -2,23 +2,30 @@ module PersonalAccessTokens class CreateService < BaseService - def initialize(current_user, params = {}) + def initialize(current_user:, target_user:, params: {}) @current_user = current_user + @target_user = target_user @params = params.dup + @ip_address = @params.delete(:ip_address) end def execute - personal_access_token = current_user.personal_access_tokens.create(params.slice(*allowed_params)) + return ServiceResponse.error(message: 'Not permitted to create') unless creation_permitted? - if personal_access_token.persisted? - ServiceResponse.success(payload: { personal_access_token: personal_access_token }) + token = target_user.personal_access_tokens.create(params.slice(*allowed_params)) + + if token.persisted? + log_event(token) + ServiceResponse.success(payload: { personal_access_token: token }) else - ServiceResponse.error(message: personal_access_token.errors.full_messages.to_sentence) + ServiceResponse.error(message: token.errors.full_messages.to_sentence, payload: { personal_access_token: token }) end end private + attr_reader :target_user, :ip_address + def allowed_params [ :name, @@ -27,5 +34,15 @@ module PersonalAccessTokens :expires_at ] end + + def creation_permitted? + Ability.allowed?(current_user, :create_user_personal_access_token, target_user) + end + + def log_event(token) + log_info("PAT CREATION: created_by: '#{current_user.username}', created_for: '#{token.user.username}', token_id: '#{token.id}'") + end end end + +PersonalAccessTokens::CreateService.prepend_if_ee('EE::PersonalAccessTokens::CreateService') diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb index 17405002d8d..34d542acab1 100644 --- a/app/services/personal_access_tokens/revoke_service.rb +++ b/app/services/personal_access_tokens/revoke_service.rb @@ -4,16 +4,17 @@ module PersonalAccessTokens class RevokeService attr_reader :token, :current_user, :group - def initialize(current_user = nil, params = { token: nil, group: nil }) + def initialize(current_user = nil, token: nil, group: nil ) @current_user = current_user - @token = params[:token] - @group = params[:group] + @token = token + @group = group end def execute return ServiceResponse.error(message: 'Not permitted to revoke') unless revocation_permitted? if token.revoke! + log_event ServiceResponse.success(message: success_message) else ServiceResponse.error(message: error_message) @@ -33,6 +34,10 @@ module PersonalAccessTokens def revocation_permitted? Ability.allowed?(current_user, :revoke_token, token) end + + def log_event + Gitlab::AppLogger.info("PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '#{token.id}'") + end end end diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb index c2b4773a505..70e09be9407 100644 --- a/app/services/resource_access_tokens/create_service.rb +++ b/app/services/resource_access_tokens/create_service.rb @@ -83,7 +83,9 @@ module ResourceAccessTokens end def create_personal_access_token(user) - PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute + PersonalAccessTokens::CreateService.new( + current_user: user, target_user: user, params: personal_access_token_params + ).execute end def personal_access_token_params diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 607e759928c..3af4437a63a 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,7 +1,10 @@ - if @search_objects.to_a.empty? - = render partial: "search/results/filters" - = render partial: "search/results/empty" - = render_if_exists 'shared/promotions/promote_advanced_search' + .gl-display-md-flex + - if %w(issues merge_requests).include?(@scope) + #js-search-sidebar.gl-display-flex.gl-flex-direction-column.col-md-3.gl-mr-4{ } + .gl-w-full + = render partial: "search/results/empty" + = render_if_exists 'shared/promotions/promote_advanced_search' - else .search-results-status .row-content-block.gl-display-flex @@ -24,19 +27,21 @@ .gl-display-md-flex.gl-flex-direction-column = render partial: 'search/sort_dropdown' = render_if_exists 'shared/promotions/promote_advanced_search' - = render partial: "search/results/filters" - .results.gl-mt-3 - - if @scope == 'commits' - %ul.content-list.commit-list - = render partial: "search/results/commit", collection: @search_objects - - else - .search-results - - if @scope == 'projects' - .term - = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - - else - = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects + .results.gl-display-md-flex.gl-mt-3 + - if %w(issues merge_requests).include?(@scope) + #js-search-sidebar{ } + .gl-w-full + - if @scope == 'commits' + %ul.content-list.commit-list + = render partial: "search/results/commit", collection: @search_objects + - else + .search-results + - if @scope == 'projects' + .term + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false + - else + = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects - - if @scope != 'projects' - = paginate_collection(@search_objects) + - if @scope != 'projects' + = paginate_collection(@search_objects) diff --git a/app/views/search/results/_filters.html.haml b/app/views/search/results/_filters.html.haml deleted file mode 100644 index 2356a6e1f2c..00000000000 --- a/app/views/search/results/_filters.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.d-lg-flex.align-items-end - #js-search-filter-by-state{ 'v-cloak': true } - #js-search-filter-by-confidential{ 'v-cloak': true } - - - if %w(issues merge_requests).include?(@scope) - %hr.gl-mt-4.gl-mb-4 diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index f56a6cd9fa2..35844fdf297 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -7,11 +7,19 @@ class RemoveExpiredMembersWorker # rubocop:disable Scalability/IdempotentWorker feature_category :authentication_and_authorization worker_resource_boundary :cpu + # rubocop: disable CodeReuse/ActiveRecord def perform - Member.expired.find_each do |member| + Member.expired.preload(:user).find_each do |member| Members::DestroyService.new.execute(member, skip_authorization: true) + + expired_user = member.user + + if expired_user.project_bot? + Users::DestroyService.new(nil).execute(expired_user, skip_authorization: true) + end rescue => ex logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") end end + # rubocop: enable CodeReuse/ActiveRecord end |