diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index cee4f644e29..68aaf8014d9 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -3393,12 +3393,9 @@ RSpec/MissingFeatureCategory: - 'spec/lib/gitlab/database/migrations/test_background_runner_spec.rb' - 'spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb' - 'spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb' - - 'spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb' - - 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb' - 'spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb' - 'spec/lib/gitlab/database/partitioning/replace_table_spec.rb' - 'spec/lib/gitlab/database/partitioning/single_numeric_list_partition_spec.rb' - - 'spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb' - 'spec/lib/gitlab/database/partitioning/time_partition_spec.rb' - 'spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb' - 'spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb' diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue new file mode 100644 index 00000000000..a87f4b8467e --- /dev/null +++ b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue @@ -0,0 +1,34 @@ + + diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue index 4711d5b00e3..305d7004357 100644 --- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue +++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue @@ -3,8 +3,10 @@ import { isEmpty } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; import { forwardDeploymentFailureModalId } from '~/ci/constants'; +import { filterAnnotations } from '~/ci/job_details/utils'; import ArtifactsBlock from './artifacts_block.vue'; import CommitBlock from './commit_block.vue'; +import ExternalLinksBlock from './external_links_block.vue'; import JobsContainer from './jobs_container.vue'; import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue'; import JobSidebarDetailsContainer from './sidebar_job_details_container.vue'; @@ -25,6 +27,7 @@ export default { SidebarHeader, StagesDropdown, TriggerBlock, + ExternalLinksBlock, }, props: { artifactHelpUrl: { @@ -40,6 +43,9 @@ export default { // the artifact object will always have a locked property return Object.keys(this.job.artifact).length > 1; }, + hasExternalLinks() { + return this.externalLinks.length > 0; + }, hasTriggers() { return !isEmpty(this.job.trigger); }, @@ -52,6 +58,9 @@ export default { shouldShowJobRetryForwardDeploymentModal() { return this.job.retry_path && this.hasForwardDeploymentFailure; }, + externalLinks() { + return filterAnnotations(this.job.annotations, 'external_link'); + }, }, watch: { job(value, oldValue) { @@ -88,6 +97,13 @@ export default { :help-url="artifactHelpUrl" /> + + { return compactedLog; }; + +export const filterAnnotations = (annotations, type) => { + return [...annotations] + .sort((a, b) => a.name.localeCompare(b.name)) + .flatMap((annotationList) => annotationList.data) + .flatMap((annotation) => annotation[type] ?? []); +}; diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue index 3e7827dc416..055b6742ae1 100644 --- a/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue +++ b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue @@ -54,6 +54,7 @@ export default { :title="emptyStateText.title" :description="emptyStateText.description" :svg-path="emptyStateImagePath" + :svg-height="150" :primary-button-link="testReportDocPath" :primary-button-text="emptyStateText.button" /> diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js index bcda2c55974..67afbee3854 100644 --- a/app/assets/javascripts/projects/settings/init_access_dropdown.js +++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js @@ -18,6 +18,15 @@ export const initAccessDropdown = (el, options) => { return new Vue({ el, + name: 'AccessDropdownRoot', + data() { + return { preselected }; + }, + methods: { + setPreselectedItems(items) { + this.preselected = items; + }, + }, render(createElement) { const vm = this; return createElement(AccessDropdown, { @@ -25,7 +34,7 @@ export const initAccessDropdown = (el, options) => { label, disabled, accessLevelsData: accessLevelsData.roles, - preselectedItems: preselected, + preselectedItems: this.preselected, ...props, }, on: { @@ -35,6 +44,9 @@ export const initAccessDropdown = (el, options) => { shown() { vm.$emit('shown'); }, + hidden() { + vm.$emit('hidden'); + }, }, }); }, diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index bbf10ef3b01..29034b3bc0e 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -2,28 +2,23 @@ import { find } from 'lodash'; import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; -import AccessDropdown from '~/projects/settings/access_dropdown'; import { initToggle } from '~/toggles'; +import { initAccessDropdown } from '~/projects/settings/init_access_dropdown'; import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; export default class ProtectedBranchEdit { constructor(options) { this.hasLicense = options.hasLicense; - this.$wraps = {}; this.hasChanges = false; this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest( - `.${ACCESS_LEVELS.MERGE}-container`, - ); - this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest( - `.${ACCESS_LEVELS.PUSH}-container`, - ); + this.selectedItems = { + [ACCESS_LEVELS.PUSH]: [], + [ACCESS_LEVELS.MERGE]: [], + }; + this.initDropdowns(); - this.buildDropdowns(); this.initToggles(); } @@ -67,6 +62,66 @@ export default class ProtectedBranchEdit { } } + initDropdowns() { + // Allowed to Merge dropdown + this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown( + 'js-allowed-to-merge', + ACCESS_LEVELS.MERGE, + gon.merge_access_levels, + 'protected-branch-allowed-to-merge', + ); + + // Allowed to Push dropdown + this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown( + 'js-allowed-to-push', + ACCESS_LEVELS.PUSH, + gon.push_access_levels, + 'protected-branch-allowed-to-push', + ); + } + + buildDropdown(selector, accessLevel, accessLevelsData, testId) { + const [el] = this.$wrap.find(`.${selector}`); + if (!el) return undefined; + + const projectId = gon.current_project_id; + const dropdown = initAccessDropdown(el, { + toggleClass: selector, + hasLicense: this.hasLicense, + searchEnabled: el.dataset.filter !== undefined, + showUsers: projectId !== undefined, + block: true, + accessLevel, + accessLevelsData, + testId, + }); + + dropdown.$on('select', (selected) => this.onSelectItems(accessLevel, selected)); + dropdown.$on('hidden', () => this.onDropdownHide()); + + this.initSelectedItems(dropdown, accessLevel); + return dropdown; + } + + initSelectedItems(dropdown, accessLevel) { + this.selectedItems[accessLevel] = dropdown.preselected.map((item) => { + if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id }; + if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level }; + if (item.type === LEVEL_TYPES.GROUP) return { id: item.id, group_id: item.group_id }; + return { id: item.id, deploy_key_id: item.deploy_key_id }; + }); + } + + onSelectItems(accessLevel, selected) { + this.selectedItems[accessLevel] = selected; + this.hasChanges = true; + } + + onDropdownHide() { + if (!this.hasChanges) return; + this.updatePermissions(); + } + updateProtectedBranch(formData, callback) { axios .patch(this.$wrap.data('url'), { @@ -78,79 +133,25 @@ export default class ProtectedBranchEdit { }); } - buildDropdowns() { - // Allowed to merge dropdown - this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({ - accessLevel: ACCESS_LEVELS.MERGE, - accessLevelsData: gon.merge_access_levels, - $dropdown: this.$allowedToMergeDropdown, - onSelect: this.onSelectOption.bind(this), - onHide: this.onDropdownHide.bind(this), - hasLicense: this.hasLicense, - }); - - // Allowed to push dropdown - this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({ - accessLevel: ACCESS_LEVELS.PUSH, - accessLevelsData: gon.push_access_levels, - $dropdown: this.$allowedToPushDropdown, - onSelect: this.onSelectOption.bind(this), - onHide: this.onDropdownHide.bind(this), - hasLicense: this.hasLicense, - }); - } - - onSelectOption() { - this.hasChanges = true; - } - - onDropdownHide() { - if (!this.hasChanges) { - return; - } - - this.hasChanges = true; - this.updatePermissions(); - } - updatePermissions() { - const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => { - const accessLevelName = ACCESS_LEVELS[level]; - const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName); - acc[`${accessLevelName}_attributes`] = inputData; - + const formData = Object.values(ACCESS_LEVELS).reduce((acc, level) => { + acc[`${level}_attributes`] = this.selectedItems[level]; return acc; }, {}); - - axios - .patch(this.$wrap.data('url'), { - protected_branch: formData, - }) - .then(({ data }) => { - this.hasChanges = false; - - Object.keys(ACCESS_LEVELS).forEach((level) => { - const accessLevelName = ACCESS_LEVELS[level]; - - // The data coming from server will be the new persisted *state* for each dropdown - this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`); - }); - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - }) - .catch(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - createAlert({ message: __('Failed to update branch!') }); + this.updateProtectedBranch(formData, ({ data }) => { + this.hasChanges = false; + Object.values(ACCESS_LEVELS).forEach((level) => { + this.setSelectedItemsToDropdown(data[level], level); }); + }); } - setSelectedItemsToDropdown(items = [], dropdownName) { + setSelectedItemsToDropdown(items = [], accessLevel) { const itemsToAdd = items.map((currentItem) => { if (currentItem.user_id) { // Do this only for users for now // get the current data for selected items - const selectedItems = this[dropdownName].getSelectedItems(); + const selectedItems = this.selectedItems[accessLevel]; const currentSelectedItem = find(selectedItems, { user_id: currentItem.user_id, }); @@ -182,6 +183,7 @@ export default class ProtectedBranchEdit { }; }); - this[dropdownName].setSelectedItems(itemsToAdd); + this.selectedItems[accessLevel] = itemsToAdd; + this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd); } } diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index eb7971a9d5a..da743f5c496 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -3,8 +3,11 @@ import { mapState, mapGetters } from 'vuex'; import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; +import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue'; import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue'; +import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import DomElementListener from '~/vue_shared/components/dom_element_listener.vue'; import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS, @@ -21,12 +24,14 @@ export default { name: 'GlobalSearchSidebar', components: { IssuesFilters, - ScopeLegacyNavigation, - ScopeSidebarNavigation, - SidebarPortal, MergeRequestsFilters, BlobsFilters, ProjectsFilters, + ScopeLegacyNavigation, + ScopeSidebarNavigation, + SidebarPortal, + DomElementListener, + SmallScreenDrawerNavigation, }, mixins: [glFeatureFlagsMixin()], computed: { @@ -53,11 +58,17 @@ export default { return Boolean(this.currentScope); }, }, + methods: { + toggleFiltersFromSidebar() { + toggleSuperSidebarCollapsed(); + }, + }, }; diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js index 77efbdd9e60..d765a821116 100644 --- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js +++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js @@ -7,6 +7,7 @@ export const TRACKING_LABEL_CHECKBOX = 'checkbox'; const scopes = { PROJECTS: 'projects', + ISSUES: 'issues', }; const filterParam = 'include_archived'; diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue index 1984e3a36c4..c31c46f2e6a 100644 --- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue @@ -14,7 +14,7 @@ export default { GlFormCheckbox, }, computed: { - ...mapState(['urlQuery']), + ...mapState(['urlQuery', 'useSidebarNavigation']), selectedFilter: { get() { return [parseBoolean(this.urlQuery?.include_archived)]; @@ -41,7 +41,9 @@ export default {