diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-28 18:09:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-28 18:09:45 +0300 |
commit | d62fd6e04c272d48dccde4033529ca97c27502f6 (patch) | |
tree | e3bbea524f4bccb92048fd8a52a42b757618b57b | |
parent | aaff41e10e8c03e545af9ba157e79f67686972a0 (diff) |
Add latest changes from gitlab-org/gitlab@master
66 files changed, 1142 insertions, 725 deletions
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 719f252781d..5cf0f4ad5ab 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -464,6 +464,7 @@ export default { </template> <gl-dropdown-item v-if="showToggleIssueStateButton && glFeatures.moveCloseIntoDropdown" + data-testid="toggle-issue-state-button" @click="toggleIssueState" > {{ buttonText }} diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index cd289be4c05..cd0976a9fd6 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -5,7 +5,8 @@ import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_na import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue'; import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS, SCOPE_BLOB } from '../constants'; import IssuesFilters from './issues_filters.vue'; -import LanguageFilter from './language_filter/index.vue'; +import MergeRequestsFilters from './merge_requests_filters.vue'; +import BlobsFilters from './blobs_filters.vue'; export default { name: 'GlobalSearchSidebar', @@ -13,25 +14,27 @@ export default { IssuesFilters, ScopeLegacyNavigation, ScopeSidebarNavigation, - LanguageFilter, SidebarPortal, + MergeRequestsFilters, + BlobsFilters, }, computed: { // useSidebarNavigation refers to whether the new left sidebar navigation is enabled ...mapState(['useSidebarNavigation']), ...mapGetters(['currentScope']), - showIssueAndMergeFilters() { - return this.currentScope === SCOPE_ISSUES || this.currentScope === SCOPE_MERGE_REQUESTS; + showIssuesFilters() { + return this.currentScope === SCOPE_ISSUES; }, - showBlobFilter() { - return this.currentScope === SCOPE_BLOB; + showMergeRequestFilters() { + return this.currentScope === SCOPE_MERGE_REQUESTS; }, - showLabelFilter() { - return this.currentScope === SCOPE_ISSUES; + showBlobFilters() { + return this.currentScope === SCOPE_BLOB; }, showScopeNavigation() { // showScopeNavigation refers to whether the scope navigation should be shown - // while the legacy navigation is being used and there are no search results the scope navigation has to be hidden + // while the legacy navigation is being used and there are no search results + // the scope navigation has to be hidden return Boolean(this.currentScope); }, }, @@ -42,8 +45,9 @@ export default { <section v-if="useSidebarNavigation"> <sidebar-portal> <scope-sidebar-navigation /> - <issues-filters v-if="showIssueAndMergeFilters" /> - <language-filter v-if="showBlobFilter" /> + <issues-filters v-if="showIssuesFilters" /> + <merge-requests-filters v-if="showMergeRequestFilters" /> + <blobs-filters v-if="showBlobFilters" /> </sidebar-portal> </section> <section @@ -51,7 +55,8 @@ export default { class="search-sidebar gl-display-flex gl-flex-direction-column gl-md-mr-5 gl-mb-6 gl-mt-5" > <scope-legacy-navigation /> - <issues-filters v-if="showIssueAndMergeFilters" /> - <language-filter v-if="showBlobFilter" /> + <issues-filters v-if="showIssuesFilters" /> + <merge-requests-filters v-if="showMergeRequestFilters" /> + <blobs-filters v-if="showBlobFilters" /> </section> </template> diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue new file mode 100644 index 00000000000..5f4d6fbd56c --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue @@ -0,0 +1,18 @@ +<script> +import LanguageFilter from './language_filter/index.vue'; +import FiltersTemplate from './filters_template.vue'; + +export default { + name: 'BlobsFilters', + components: { + LanguageFilter, + FiltersTemplate, + }, +}; +</script> + +<template> + <filters-template> + <language-filter class="gl-mb-5" /> + </filters-template> +</template> diff --git a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue deleted file mode 100644 index bca049e56c7..00000000000 --- a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue +++ /dev/null @@ -1,96 +0,0 @@ -<script> -import Vue from 'vue'; -import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui'; -import { mapState, mapActions, mapGetters } from 'vuex'; -import { intersection } from 'lodash'; -import Tracking from '~/tracking'; -import { NAV_LINK_COUNT_DEFAULT_CLASSES, LABEL_DEFAULT_CLASSES } from '../constants'; -import { formatSearchResultCount } from '../../store/utils'; - -export const TRACKING_LABEL_SET = 'set'; -export const TRACKING_LABEL_CHECKBOX = 'checkbox'; - -export default { - name: 'CheckboxFilter', - components: { - GlFormCheckboxGroup, - GlFormCheckbox, - }, - props: { - filtersData: { - type: Object, - required: true, - }, - trackingNamespace: { - type: String, - required: true, - }, - }, - computed: { - ...mapState(['query', 'useSidebarNavigation']), - ...mapGetters(['queryLanguageFilters']), - dataFilters() { - return Object.values(this.filtersData?.filters || []); - }, - flatDataFilterValues() { - return this.dataFilters.map(({ value }) => value); - }, - selectedFilter: { - get() { - return intersection(this.flatDataFilterValues, this.queryLanguageFilters); - }, - async set(value) { - this.setQuery({ key: this.filtersData?.filterParam, value }); - - await Vue.nextTick(); - this.trackSelectCheckbox(); - }, - }, - labelCountClasses() { - return [...NAV_LINK_COUNT_DEFAULT_CLASSES, 'gl-text-gray-500']; - }, - }, - methods: { - ...mapActions(['setQuery']), - getFormattedCount(count) { - return formatSearchResultCount(count); - }, - trackSelectCheckbox() { - Tracking.event(this.trackingNamespace, TRACKING_LABEL_CHECKBOX, { - label: TRACKING_LABEL_SET, - property: this.selectedFilter, - }); - }, - }, - NAV_LINK_COUNT_DEFAULT_CLASSES, - LABEL_DEFAULT_CLASSES, -}; -</script> - -<template> - <div class="gl-mx-5"> - <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useSidebarNavigation }"> - {{ filtersData.header }} - </h5> - <gl-form-checkbox-group v-model="selectedFilter"> - <gl-form-checkbox - v-for="f in dataFilters" - :key="f.label" - :value="f.label" - class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full" - :class="$options.LABEL_DEFAULT_CLASSES" - > - <span - class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full" - > - <span data-testid="label"> - {{ f.label }} - </span> - <span v-if="f.count" :class="labelCountClasses" data-testid="labelCount"> - {{ getFormattedCount(f.count) }} - </span> - </span> - </gl-form-checkbox> - </gl-form-checkbox-group> - </div> -</template> diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue index 312092b9904..ea3a27168c8 100644 --- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue @@ -1,6 +1,5 @@ <script> import { mapState } from 'vuex'; -import { HR_DEFAULT_CLASSES } from '../../constants'; import RadioFilter from '../radio_filter.vue'; import { confidentialFilterData } from './data'; @@ -13,7 +12,6 @@ export default { ...mapState(['useSidebarNavigation']), }, confidentialFilterData, - HR_DEFAULT_CLASSES, }; </script> diff --git a/app/assets/javascripts/search/sidebar/components/filters_template.vue b/app/assets/javascripts/search/sidebar/components/filters_template.vue new file mode 100644 index 00000000000..29c62096dcf --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/filters_template.vue @@ -0,0 +1,59 @@ +<script> +import { GlButton, GlLink, GlForm } from '@gitlab/ui'; +import { mapActions, mapState, mapGetters } from 'vuex'; +import Tracking from '~/tracking'; + +import { + HR_DEFAULT_CLASSES, + TRACKING_ACTION_CLICK, + TRACKING_LABEL_APPLY, + TRACKING_LABEL_RESET, +} from '../constants/index'; + +export default { + name: 'FiltersTemplate', + components: { + GlButton, + GlLink, + GlForm, + }, + computed: { + ...mapState(['sidebarDirty', 'useSidebarNavigation']), + ...mapGetters(['currentScope']), + hrClasses() { + return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; + }, + }, + methods: { + ...mapActions(['applyQuery', 'resetQuery']), + applyQueryWithTracking() { + Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, { + label: this.currentScope, + }); + this.applyQuery(); + }, + resetQueryWithTracking() { + Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_RESET, { + label: this.currentScope, + }); + this.resetQuery(); + }, + }, +}; +</script> + +<template> + <gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking"> + <hr v-if="!useSidebarNavigation" :class="hrClasses" /> + <slot></slot> + <hr v-if="!useSidebarNavigation" :class="hrClasses" /> + <div class="gl-display-flex gl-align-items-center gl-mt-4"> + <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> + {{ __('Apply') }} + </gl-button> + <gl-link v-if="sidebarDirty" class="gl-ml-auto" @click="resetQueryWithTracking">{{ + __('Reset filters') + }}</gl-link> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue index 5fd8b6418d7..df3fd464a2d 100644 --- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue @@ -1,15 +1,7 @@ <script> -import { GlButton, GlLink } from '@gitlab/ui'; -import { mapActions, mapState, mapGetters } from 'vuex'; -import Tracking from '~/tracking'; +import { mapGetters, mapState } from 'vuex'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { - HR_DEFAULT_CLASSES, - TRACKING_ACTION_CLICK, - TRACKING_LABEL_APPLY, - TRACKING_CATEGORY, - TRACKING_LABEL_RESET, -} from '../constants/index'; +import { HR_DEFAULT_CLASSES } from '../constants/index'; import { confidentialFilterData } from './confidentiality_filter/data'; import { statusFilterData } from './status_filter/data'; import ConfidentialityFilter from './confidentiality_filter/index.vue'; @@ -17,22 +9,20 @@ import { labelFilterData } from './label_filter/data'; import LabelFilter from './label_filter/index.vue'; import StatusFilter from './status_filter/index.vue'; +import FiltersTemplate from './filters_template.vue'; + export default { name: 'IssuesFilters', components: { - GlButton, - GlLink, StatusFilter, ConfidentialityFilter, LabelFilter, + FiltersTemplate, }, mixins: [glFeatureFlagsMixin()], computed: { - ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']), ...mapGetters(['currentScope']), - showReset() { - return this.urlQuery.state || this.urlQuery.confidential || this.urlQuery.labels; - }, + ...mapState(['useSidebarNavigation']), showConfidentialityFilter() { return Object.values(confidentialFilterData.scopes).includes(this.currentScope); }, @@ -45,46 +35,22 @@ export default { this.glFeatures.searchIssueLabelAggregation ); }, + showDivider() { + return !this.useSidebarNavigation; + }, hrClasses() { return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; }, }, - methods: { - ...mapActions(['applyQuery', 'resetQuery']), - applyQueryWithTracking() { - Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, { - label: TRACKING_CATEGORY, - }); - this.applyQuery(); - }, - resetQueryWithTracking() { - Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_RESET, { - label: TRACKING_CATEGORY, - }); - this.resetQuery(); - }, - }, }; </script> <template> - <form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking"> - <hr v-if="!useSidebarNavigation" :class="hrClasses" /> + <filters-template> <status-filter v-if="showStatusFilter" class="gl-mb-5" /> - <hr v-if="!useSidebarNavigation" :class="hrClasses" /> + <hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" /> <confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" /> - <hr - v-if="!useSidebarNavigation && showConfidentialityFilter && showLabelFilter" - :class="hrClasses" - /> + <hr v-if="showLabelFilter && showDivider" :class="hrClasses" /> <label-filter v-if="showLabelFilter" /> - <div class="gl-display-flex gl-align-items-center gl-mt-4"> - <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> - {{ __('Apply') }} - </gl-button> - <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQueryWithTracking">{{ - __('Reset filters') - }}</gl-link> - </div> - </form> + </filters-template> </template> diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue index eb3556ac2cf..f14c95b41aa 100644 --- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue @@ -19,8 +19,6 @@ import { sprintf } from '~/locale'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { I18N } from '~/vue_shared/global_search/constants'; - -import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants'; import LabelDropdownItems from './label_dropdown_items.vue'; import { @@ -62,7 +60,6 @@ export default { 'filteredUnselectedLabels', 'filteredAppliedSelectedLabels', 'appliedSelectedLabels', - 'filteredUnappliedSelectedLabels', ]), searchInputDescribeBy() { if (this.isLoggedIn) { @@ -107,9 +104,6 @@ export default { hasUnselectedLabels() { return this.filteredUnselectedLabels.length > 0; }, - dividerClasses() { - return [...HR_DEFAULT_CLASSES, ...ONLY_SHOW_MD]; - }, labelSearchBox() { return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]'); }, @@ -277,6 +271,5 @@ export default { <gl-loading-icon v-if="aggregations.fetching" size="lg" class="my-4" /> </div> </div> - <hr v-if="!useSidebarNavigation" :class="dividerClasses" /> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue index b42fe9185cb..404cfd9a1e5 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue +++ b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue @@ -1,7 +1,7 @@ <script> import Vue from 'vue'; import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui'; -import { mapState, mapActions, mapGetters } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import { intersection } from 'lodash'; import Tracking from '~/tracking'; import { NAV_LINK_COUNT_DEFAULT_CLASSES, LABEL_DEFAULT_CLASSES } from '../../constants'; @@ -27,7 +27,6 @@ export default { }, }, computed: { - ...mapState(['query', 'useSidebarNavigation']), ...mapGetters(['queryLanguageFilters']), dataFilters() { return Object.values(this.filtersData?.filters || []); @@ -62,7 +61,6 @@ export default { }); }, }, - NAV_LINK_COUNT_DEFAULT_CLASSES, LABEL_DEFAULT_CLASSES, }; </script> diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue index e5560dd5b55..b958c4d8eeb 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue @@ -1,17 +1,10 @@ <script> -import { GlButton, GlAlert, GlForm } from '@gitlab/ui'; +import { GlButton, GlAlert } from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; -import { __, s__, sprintf } from '~/locale'; -import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../../constants'; +import { s__, sprintf } from '~/locale'; import { convertFiltersData } from '../../utils'; import CheckboxFilter from './checkbox_filter.vue'; -import { - trackShowMore, - trackShowHasOverMax, - trackSubmitQuery, - trackResetQuery, - TRACKING_ACTION_SELECT, -} from './tracking'; +import { trackShowMore, trackShowHasOverMax, TRACKING_ACTION_SELECT } from './tracking'; import { DEFAULT_ITEM_LENGTH, MAX_ITEM_LENGTH, languageFilterData } from './data'; @@ -21,7 +14,6 @@ export default { CheckboxFilter, GlButton, GlAlert, - GlForm, }, data() { return { @@ -30,18 +22,12 @@ export default { }, i18n: { showMore: s__('GlobalSearch|Show more'), - apply: __('Apply'), showingMax: sprintf(s__('GlobalSearch|Showing top %{maxItems}'), { maxItems: MAX_ITEM_LENGTH }), loadError: s__('GlobalSearch|Aggregations load error.'), - reset: s__('GlobalSearch|Reset filters'), }, computed: { - ...mapState(['aggregations', 'sidebarDirty', 'useSidebarNavigation']), - ...mapGetters([ - 'languageAggregationBuckets', - 'currentUrlQueryHasLanguageFilters', - 'queryLanguageFilters', - ]), + ...mapState(['aggregations', 'useSidebarNavigation']), + ...mapGetters(['languageAggregationBuckets']), hasBuckets() { return this.languageAggregationBuckets.length > 0; }, @@ -63,26 +49,12 @@ export default { hasOverMax() { return this.languageAggregationBuckets.length > MAX_ITEM_LENGTH; }, - dividerClassesTop() { - return [...HR_DEFAULT_CLASSES, ...ONLY_SHOW_MD]; - }, - dividerClassesBottom() { - return [...HR_DEFAULT_CLASSES, 'gl-mt-5']; - }, - hasQueryFilters() { - return this.queryLanguageFilters.length > 0; - }, }, async created() { await this.fetchAllAggregation(); }, methods: { - ...mapActions([ - 'applyQuery', - 'resetLanguageQuery', - 'resetLanguageQueryWithRedirect', - 'fetchAllAggregation', - ]), + ...mapActions(['fetchAllAggregation']), onShowMore() { this.showAll = true; trackShowMore(); @@ -91,91 +63,47 @@ export default { trackShowHasOverMax(); } }, - submitQuery() { - trackSubmitQuery(); - this.applyQuery(); - }, trimBuckets(length) { return this.languageAggregationBuckets.slice(0, length); }, - cleanResetFilters() { - trackResetQuery(); - if (this.currentUrlQueryHasLanguageFilters) { - return this.resetLanguageQueryWithRedirect(); - } - this.showAll = false; - return this.resetLanguageQuery(); - }, }, - HR_DEFAULT_CLASSES, TRACKING_ACTION_SELECT, languageFilterData, }; </script> <template> - <div> - <gl-form - v-if="hasBuckets" - class="gl-m-5 gl-my-0 language-filter-checkbox" - @submit.prevent="submitQuery" + <div v-if="hasBuckets" class="gl-my-0 language-filter-checkbox"> + <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> + {{ $options.languageFilterData.header }} + </h5> + <div + v-if="!aggregations.error" + class="gl-overflow-x-hidden gl-overflow-y-auto" + :class="{ 'language-filter-max-height': showAll }" > - <hr v-if="!useSidebarNavigation" :class="dividerClassesTop" /> - <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }"> - {{ $options.languageFilterData.header }} - </h5> - <div - v-if="!aggregations.error" - class="gl-overflow-x-hidden gl-overflow-y-auto" - :class="{ 'language-filter-max-height': showAll }" + <checkbox-filter + :filters-data="filtersData" + :tracking-namespace="$options.TRACKING_ACTION_SELECT" + /> + <span v-if="showAll && hasOverMax" data-testid="has-over-max-text">{{ + $options.i18n.showingMax + }}</span> + </div> + <gl-alert v-else class="gl-mx-5" variant="danger" :dismissible="false">{{ + $options.i18n.loadError + }}</gl-alert> + <div v-if="hasShowMore && !showAll" class="language-filter-show-all"> + <gl-button + data-testid="show-more-button" + category="tertiary" + variant="link" + size="small" + button-text-classes="gl-font-sm" + @click="onShowMore" > - <checkbox-filter - :filters-data="filtersData" - :tracking-namespace="$options.TRACKING_ACTION_SELECT" - /> - <span v-if="showAll && hasOverMax" data-testid="has-over-max-text">{{ - $options.i18n.showingMax - }}</span> - </div> - <gl-alert v-else class="gl-mx-5" variant="danger" :dismissible="false">{{ - $options.i18n.loadError - }}</gl-alert> - <div v-if="hasShowMore && !showAll" class="gl-px-5 language-filter-show-all"> - <gl-button - data-testid="show-more-button" - category="tertiary" - variant="link" - size="small" - button-text-classes="gl-font-sm" - @click="onShowMore" - > - {{ $options.i18n.showMore }} - </gl-button> - </div> - <div v-if="!aggregations.error"> - <hr v-if="!useSidebarNavigation" :class="dividerClassesBottom" /> - <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-mt-4"> - <gl-button - category="primary" - variant="confirm" - type="submit" - :disabled="!sidebarDirty" - data-testid="apply-button" - > - {{ $options.i18n.apply }} - </gl-button> - <gl-button - v-if="hasQueryFilters && sidebarDirty" - category="tertiary" - variant="link" - size="small" - data-testid="reset-button" - @click="cleanResetFilters" - > - {{ $options.i18n.reset }} - </gl-button> - </div> - </div> - </gl-form> + {{ $options.i18n.showMore }} + </gl-button> + </div> </div> </template> diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/tracking.js b/app/assets/javascripts/search/sidebar/components/language_filter/tracking.js index db107830329..5f085c7df7e 100644 --- a/app/assets/javascripts/search/sidebar/components/language_filter/tracking.js +++ b/app/assets/javascripts/search/sidebar/components/language_filter/tracking.js @@ -27,13 +27,3 @@ export const trackShowHasOverMax = () => label: TRACKING_LABEL_MAX, property: TRACKING_PROPERTY_MAX, }); - -export const trackSubmitQuery = () => - Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, { - label: TRACKING_CATEGORY, - }); - -export const trackResetQuery = () => - Tracking.event(TRACKING_ACTION_CLICK, TRACKING_LABEL_RESET, { - label: TRACKING_CATEGORY, - }); diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue new file mode 100644 index 00000000000..bc5b797dd56 --- /dev/null +++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue @@ -0,0 +1,18 @@ +<script> +import StatusFilter from './status_filter/index.vue'; +import FiltersTemplate from './filters_template.vue'; + +export default { + name: 'MergeRequestsFilters', + components: { + StatusFilter, + FiltersTemplate, + }, +}; +</script> + +<template> + <filters-template> + <status-filter class="gl-mb-5" /> + </filters-template> +</template> diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue deleted file mode 100644 index 88e434cf99e..00000000000 --- a/app/assets/javascripts/search/sidebar/components/results_filters.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script> -import { GlButton, GlLink } from '@gitlab/ui'; -import { mapActions, mapState, mapGetters } from 'vuex'; -import { HR_DEFAULT_CLASSES } from '../constants/index'; -import { confidentialFilterData } from './confidentiality_filter/data'; -import { statusFilterData } from './status_filter/data'; -import ConfidentialityFilter from './confidentiality_filter/index.vue'; -import StatusFilter from './status_filter/index.vue'; - -export default { - name: 'ResultsFilters', - components: { - GlButton, - GlLink, - StatusFilter, - ConfidentialityFilter, - }, - computed: { - ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']), - ...mapGetters(['currentScope']), - showReset() { - return this.urlQuery.state || this.urlQuery.confidential; - }, - showConfidentialityFilter() { - return Object.values(confidentialFilterData.scopes).includes(this.currentScope); - }, - showStatusFilter() { - return Object.values(statusFilterData.scopes).includes(this.currentScope); - }, - hrClasses() { - return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; - }, - }, - methods: { - ...mapActions(['applyQuery', 'resetQuery']), - }, -}; -</script> - -<template> - <form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery"> - <hr v-if="!useSidebarNavigation" :class="hrClasses" /> - <status-filter v-if="showStatusFilter" /> - <confidentiality-filter v-if="showConfidentialityFilter" /> - <div class="gl-display-flex gl-align-items-center gl-mt-4 gl-px-5"> - <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty"> - {{ __('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/scope_sidebar_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue index 3707e152e47..70fad49724c 100644 --- a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue +++ b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue @@ -1,7 +1,6 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; import { s__ } from '~/locale'; -import Tracking from '~/tracking'; import NavItem from '~/super_sidebar/components/nav_item.vue'; import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants'; @@ -13,7 +12,6 @@ export default { components: { NavItem, }, - mixins: [Tracking.mixin()], computed: { ...mapState(['navigation', 'urlQuery']), ...mapGetters(['navigationItems']), diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue index 010cfbad590..a5f717dcf06 100644 --- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue +++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue @@ -1,5 +1,4 @@ <script> -import { mapState } from 'vuex'; import { HR_DEFAULT_CLASSES } from '../../constants'; import RadioFilter from '../radio_filter.vue'; import { statusFilterData } from './data'; @@ -9,16 +8,11 @@ export default { components: { RadioFilter, }, - computed: { - ...mapState(['useSidebarNavigation']), - }, statusFilterData, HR_DEFAULT_CLASSES, }; </script> <template> - <div> - <radio-filter :filter-data="$options.statusFilterData" /> - </div> + <radio-filter :filter-data="$options.statusFilterData" /> </template> diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 077c46bbe22..f4928834bf3 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -4,7 +4,6 @@ import axios from '~/lib/utils/axios_utils'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { logError } from '~/lib/logger'; import { __ } from '~/locale'; -import { languageFilterData } from '~/search/sidebar/components/language_filter/data'; import { labelFilterData } from '~/search/sidebar/components/label_filter/data'; import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants'; import * as types from './mutation_types'; @@ -127,14 +126,6 @@ export const setLabelFilterSearch = ({ commit }, { value }) => { commit(types.SET_LABEL_SEARCH_STRING, value); }; -export const resetLanguageQueryWithRedirect = ({ state }) => { - visitUrl(setUrlParams({ ...state.query, language: null }, undefined, true)); -}; - -export const resetLanguageQuery = ({ commit }) => { - commit(types.SET_QUERY, { key: languageFilterData?.filterParam, value: [] }); -}; - export const fetchSidebarCount = ({ commit, state }) => { const promises = Object.values(state.navigation).map((navItem) => { // active nav item has count already so we skip it diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js index c7cb595f42f..6c9834d0d6c 100644 --- a/app/assets/javascripts/search/store/getters.js +++ b/app/assets/javascripts/search/store/getters.js @@ -1,4 +1,4 @@ -import { findKey, has } from 'lodash'; +import { findKey } from 'lodash'; import { languageFilterData } from '~/search/sidebar/components/language_filter/data'; import { labelFilterData } from '~/search/sidebar/components/label_filter/data'; import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils'; @@ -62,10 +62,6 @@ export const currentScope = (state) => findKey(state.navigation, { active: true export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || []; -export const currentUrlQueryHasLanguageFilters = (state) => - has(state.urlQuery, languageFilterData.filterParam) && - state.urlQuery[languageFilterData.filterParam]?.length > 0; - export const navigationItems = (state) => Object.values(state.navigation).map((item) => ({ title: item.label, diff --git a/app/assets/javascripts/super_sidebar/components/context_header.vue b/app/assets/javascripts/super_sidebar/components/context_header.vue new file mode 100644 index 00000000000..11b9840a409 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/context_header.vue @@ -0,0 +1,56 @@ +<script> +import { GlTruncate, GlAvatar, GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlTruncate, + GlAvatar, + GlIcon, + }, + props: { + /* + * Contains metadata about the current view, e.g. `id`, `title` and `avatar` + */ + context: { + type: Object, + required: true, + }, + tag: { + type: String, + required: false, + default: 'div', + }, + }, + computed: { + avatarShape() { + return this.context.avatar_shape || 'rect'; + }, + }, +}; +</script> + +<template> + <component + :is="tag" + class="border-top border-bottom gl-border-gray-a-08! gl-display-flex gl-align-items-center gl-gap-3 gl-font-weight-bold gl-w-full gl-h-8 gl-px-4 gl-flex-shrink-0" + > + <span + v-if="context.icon" + class="gl-avatar avatar-container gl-bg-t-gray-a-08 icon-avatar rect-avatar s24" + > + <gl-icon class="gl-text-gray-700" :name="context.icon" :size="16" /> + </span> + <gl-avatar + v-else + :size="24" + :shape="avatarShape" + :entity-name="context.title" + :entity-id="context.id" + :src="context.avatar" + /> + <div class="gl-flex-grow-1 gl-overflow-auto gl-text-gray-900"> + <gl-truncate :text="context.title" /> + </div> + <slot name="end"></slot> + </component> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue index 17227a2b123..f6aa2b637eb 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue @@ -1,11 +1,11 @@ <script> -import { GlTruncate, GlAvatar, GlIcon } from '@gitlab/ui'; +import { GlIcon } from '@gitlab/ui'; +import ContextHeader from './context_header.vue'; export default { components: { - GlTruncate, - GlAvatar, GlIcon, + ContextHeader, }, props: { /* @@ -24,39 +24,20 @@ export default { collapseIcon() { return this.expanded ? 'chevron-up' : 'chevron-down'; }, - avatarShape() { - return this.context.avatar_shape || 'rect'; - }, }, }; </script> <template> - <button + <context-header + :context="context" + tag="button" type="button" - class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 border-top border-bottom gl-border-gray-a-08! gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-h-8 gl-flex-shrink-0" + class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 gl-box-shadow-none gl-text-left" data-qa-selector="context_switcher" > - <span - v-if="context.icon" - class="gl-avatar avatar-container gl-bg-t-gray-a-08 icon-avatar rect-avatar s24 gl-mr-3 gl-ml-4" - > - <gl-icon class="gl-text-gray-700" :name="context.icon" :size="16" /> - </span> - <gl-avatar - v-else - :size="24" - :shape="avatarShape" - :entity-name="context.title" - :entity-id="context.id" - :src="context.avatar" - class="gl-mr-3 gl-ml-4" - /> - <div class="gl-overflow-auto gl-text-gray-900"> - <gl-truncate :text="context.title" /> - </div> - <span class="gl-flex-grow-1 gl-text-right gl-mr-4"> + <template #end> <gl-icon class="gl-text-gray-400" :name="collapseIcon" /> - </span> - </button> + </template> + </context-header> </template> diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js index 4a42f416206..89bd41ea6c4 100644 --- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js +++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js @@ -1,6 +1,8 @@ import { omitBy, isNil } from 'lodash'; import { objectToQuery } from '~/lib/utils/url_utility'; import { + ISSUES_CATEGORY, + MERGE_REQUEST_CATEGORY, MSG_ISSUES_ASSIGNED_TO_ME, MSG_ISSUES_IVE_CREATED, MSG_MR_ASSIGNED_TO_ME, @@ -61,6 +63,19 @@ export const scopedMRPath = (state) => { export const defaultSearchOptions = (state, getters) => { const userName = gon.current_username; + if (!userName) { + return [ + { + text: ISSUES_CATEGORY, + href: getters.scopedIssuesPath, + }, + { + text: MERGE_REQUEST_CATEGORY, + href: getters.scopedMRPath, + }, + ]; + } + const issues = [ { text: MSG_ISSUES_ASSIGNED_TO_ME, diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index 86fe5c9ad5c..821b9dbcb7b 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -30,6 +30,10 @@ export default { type: Array, required: true, }, + isLoggedIn: { + type: Boolean, + required: true, + }, pinnedItemIds: { type: Array, required: false, @@ -42,7 +46,8 @@ export default { }, updatePinsUrl: { type: String, - required: true, + required: false, + default: '', }, }, @@ -97,7 +102,7 @@ export default { .filter(Boolean); }, supportsPins() { - return PANELS_WITH_PINS.includes(this.panelType); + return this.isLoggedIn && PANELS_WITH_PINS.includes(this.panelType); }, hasStaticItems() { return this.staticItems.length > 0; diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue index 6058ed3a1cd..ec728b4af9e 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue @@ -11,6 +11,12 @@ export const STATE_WILL_CLOSE = 'will-close'; export default { name: 'SidebarPeek', mixins: [Tracking.mixin()], + props: { + isMouseOverSidebar: { + type: Boolean, + required: true, + }, + }, created() { // Nothing needs to observe these properties, so they are not reactive. this.state = null; @@ -57,6 +63,11 @@ export default { this.close(); } } else if (this.state === STATE_OPEN) { + // Do not close the sidebar if it or one of its child elements still + // has mouseover. This allows to move the mouse from the sidebar to + // one of its flyout menus. + if (this.isMouseOverSidebar) return; + if (clientX >= this.xAwayFromSidebar) { this.close(); } else if (clientX >= this.xSidebarEdge) { diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index c194401ce95..dd07d2f21e0 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -8,6 +8,7 @@ import { sidebarState } from '../constants'; import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; import UserBar from './user_bar.vue'; import SidebarPortalTarget from './sidebar_portal_target.vue'; +import ContextHeader from './context_header.vue'; import ContextSwitcher from './context_switcher.vue'; import HelpCenter from './help_center.vue'; import SidebarMenu from './sidebar_menu.vue'; @@ -17,6 +18,7 @@ export default { components: { GlButton, UserBar, + ContextHeader, ContextSwitcher, HelpCenter, SidebarMenu, @@ -42,6 +44,7 @@ export default { return { sidebarState, showPeekHint: false, + isMouseover: false, }; }, computed: { @@ -57,7 +60,7 @@ export default { }, watch: { 'sidebarState.isCollapsed': function isCollapsedWatcher(newIsCollapsed) { - if (newIsCollapsed) { + if (newIsCollapsed && this.$refs['context-switcher']) { this.$refs['context-switcher'].close(); } }, @@ -118,6 +121,8 @@ export default { data-testid="super-sidebar" data-qa-selector="navbar" :inert="sidebarState.isCollapsed" + @mouseenter="isMouseover = true" + @mouseleave="isMouseover = false" > <user-bar :has-collapse-button="!sidebarState.isPeek" :sidebar-data="sidebarData" /> <div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2"> @@ -133,6 +138,7 @@ export default { data-testid="nav-container" > <context-switcher + v-if="sidebarData.is_logged_in" ref="context-switcher" :persistent-links="sidebarData.context_switcher_links" :username="sidebarData.username" @@ -142,9 +148,11 @@ export default { :context-header="sidebarData.current_context_header" @toggle="onContextSwitcherToggled" /> + <context-header v-else :context="sidebarData.current_context_header" /> <sidebar-menu v-if="menuItems.length" :items="menuItems" + :is-logged-in="sidebarData.is_logged_in" :panel-type="sidebarData.panel_type" :pinned-item-ids="sidebarData.pinned_items" :update-pins-url="sidebarData.update_pins_url" @@ -170,6 +178,10 @@ export default { Only mount SidebarPeekBehavior if the sidebar is peekable, to avoid setting up event listeners unnecessarily. --> - <sidebar-peek-behavior v-if="sidebarState.isPeekable" @change="onPeekChange" /> + <sidebar-peek-behavior + v-if="sidebarState.isPeekable" + :is-mouse-over-sidebar="isMouseover" + @change="onPeekChange" + /> </div> </template> diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index a882df057fa..0188585df40 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -105,17 +105,20 @@ export default { <template> <div class="user-bar"> <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2"> - <brand-logo :logo-url="sidebarData.logo_url" /> - <gl-badge - v-if="sidebarData.gitlab_com_and_canary" - variant="success" - :href="sidebarData.canary_toggle_com_url" - size="sm" - class="gl-ml-2" - > - {{ $options.NEXT_LABEL }} - </gl-badge> - <div class="gl-flex-grow-1"></div> + <template v-if="sidebarData.is_logged_in"> + <brand-logo :logo-url="sidebarData.logo_url" /> + <gl-badge + v-if="sidebarData.gitlab_com_and_canary" + variant="success" + :href="sidebarData.canary_toggle_com_url" + size="sm" + class="gl-ml-2" + > + {{ $options.NEXT_LABEL }} + </gl-badge> + <div class="gl-flex-grow-1"></div> + </template> + <super-sidebar-toggle v-if="hasCollapseButton" :class="$options.JS_TOGGLE_COLLAPSE_CLASS" @@ -123,7 +126,7 @@ export default { tooltip-container="super-sidebar" data-testid="super-sidebar-collapse-button" /> - <create-menu :groups="sidebarData.create_new_menu_groups" /> + <create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" /> <gl-button id="super-sidebar-search" @@ -136,7 +139,7 @@ export default { /> <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" /> - <user-menu :data="sidebarData" /> + <user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" /> <gl-button v-if="isImpersonating" @@ -151,7 +154,10 @@ export default { data-testid="stop-impersonation-btn" /> </div> - <div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"> + <div + v-if="sidebarData.is_logged_in" + class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2" + > <counter v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues" class="gl-flex-basis-third dashboard-shortcuts-issues" diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 358f599e0e9..3b0cacb2350 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -45,6 +45,14 @@ gl-emoji { .emoji-picker-category-tab { border-bottom-color: transparent; + + &:hover { + @include gl-text-gray-900; + + &:not(.emoji-picker-category-active) { + @include gl-border-b-gray-200; + } + } } .emoji-picker-category-active { diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index fd699737b63..6af81e2fa06 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -499,7 +499,8 @@ module ApplicationSettingsHelper :gitlab_dedicated_instance, :ci_max_includes, :allow_account_deletion, - :gitlab_shell_operation_limit + :gitlab_shell_operation_limit, + :namespace_aggregation_schedule_lease_duration_in_seconds ].tap do |settings| next if Gitlab.com? diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index c7864c1d45f..4cbd5029ac9 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -90,7 +90,7 @@ module NavHelper # The new sidebar is not enabled for anonymous use # Once we enable the new sidebar by default, this # should return true - return false unless user + return Feature.enabled?(:super_sidebar_logged_out) unless user # Users who got the special `super_sidebar_nav_enrolled` enabled, # see the new nav as long as they don't explicitly opt-out via the toggle diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 90917cb96e0..0329a3e136b 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -45,9 +45,31 @@ module SidebarsHelper end def super_sidebar_context(user, group:, project:, panel:, panel_type:) # rubocop:disable Metrics/AbcSize + return super_sidebar_logged_out_context(panel: panel, panel_type: panel_type) unless user + + super_sidebar_logged_in_context(user, group: group, project: project, panel: panel, panel_type: panel_type) + end + + def super_sidebar_logged_out_context(panel:, panel_type:) # rubocop:disable Metrics/AbcSize { + is_logged_in: false, current_menu_items: panel.super_sidebar_menu_items, current_context_header: panel.super_sidebar_context_header, + support_path: support_url, + display_whats_new: display_whats_new?, + whats_new_most_recent_release_items_count: whats_new_most_recent_release_items_count, + whats_new_version_digest: whats_new_version_digest, + show_version_check: show_version_check?, + gitlab_version: Gitlab.version_info, + gitlab_version_check: gitlab_version_check, + search: search_data, + panel_type: panel_type + } + end + + def super_sidebar_logged_in_context(user, group:, project:, panel:, panel_type:) # rubocop:disable Metrics/AbcSize + super_sidebar_logged_out_context(panel: panel, panel_type: panel_type).merge({ + is_logged_in: true, name: user.name, username: user.username, avatar_url: user.avatar_url, @@ -75,26 +97,17 @@ module SidebarsHelper merge_request_menu: create_merge_request_menu(user), projects_path: dashboard_projects_path, groups_path: dashboard_groups_path, - support_path: support_url, - display_whats_new: display_whats_new?, - whats_new_most_recent_release_items_count: whats_new_most_recent_release_items_count, - whats_new_version_digest: whats_new_version_digest, - show_version_check: show_version_check?, - gitlab_version: Gitlab.version_info, - gitlab_version_check: gitlab_version_check, gitlab_com_but_not_canary: Gitlab.com_but_not_canary?, gitlab_com_and_canary: Gitlab.com_and_canary?, canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url, current_context: super_sidebar_current_context(project: project, group: group), context_switcher_links: context_switcher_links, - search: search_data, pinned_items: user.pinned_nav_items[panel_type] || super_sidebar_default_pins(panel_type), - panel_type: panel_type, update_pins_url: pins_url, is_impersonating: impersonating?, stop_impersonation_path: admin_impersonation_path, shortcut_links: shortcut_links(user, project: project) - } + }) end def super_sidebar_nav_panel( diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index 0f4cbd6642b..12f78d9bd16 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -89,6 +89,8 @@ module Users end def gitlab_com_user_created_after_new_nav_rollout? + return true unless current_user + Gitlab.com? && current_user.created_at >= Date.new(2023, 6, 2) end diff --git a/config/feature_flags/development/super_sidebar_logged_out.yml b/config/feature_flags/development/super_sidebar_logged_out.yml new file mode 100644 index 00000000000..8deeb63b537 --- /dev/null +++ b/config/feature_flags/development/super_sidebar_logged_out.yml @@ -0,0 +1,8 @@ +--- +name: super_sidebar_logged_out +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127756 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419936 +milestone: '16.3' +type: development +group: group::foundations +default_enabled: false diff --git a/db/migrate/20230727203840_drop_unique_idx_on_vuln_signatures.rb b/db/migrate/20230727203840_drop_unique_idx_on_vuln_signatures.rb new file mode 100644 index 00000000000..df5957cb24a --- /dev/null +++ b/db/migrate/20230727203840_drop_unique_idx_on_vuln_signatures.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class DropUniqueIdxOnVulnSignatures < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'idx_vuln_signatures_on_occurrences_id_and_signature_sha' + + def up + remove_concurrent_index_by_name :vulnerability_finding_signatures, INDEX_NAME + end + + def down + add_concurrent_index :vulnerability_finding_signatures, %i[finding_id signature_sha], unique: true, name: INDEX_NAME + end +end diff --git a/db/post_migrate/20230726201351_remove_issues_issue_type_column.rb b/db/post_migrate/20230726201351_remove_issues_issue_type_column.rb new file mode 100644 index 00000000000..4f066adbc97 --- /dev/null +++ b/db/post_migrate/20230726201351_remove_issues_issue_type_column.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class RemoveIssuesIssueTypeColumn < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def up + remove_column :issues, :issue_type + end + + def down + add_column :issues, :issue_type, :smallint, default: 0, null: false + end +end diff --git a/db/schema_migrations/20230726201351 b/db/schema_migrations/20230726201351 new file mode 100644 index 00000000000..5c853a999ea --- /dev/null +++ b/db/schema_migrations/20230726201351 @@ -0,0 +1 @@ +03a51d3154a9e9a0b60375c75fe9289d59939187c0443f59ed22da95d446c134
\ No newline at end of file diff --git a/db/schema_migrations/20230727203840 b/db/schema_migrations/20230727203840 new file mode 100644 index 00000000000..cf392959479 --- /dev/null +++ b/db/schema_migrations/20230727203840 @@ -0,0 +1 @@ +259dbdf6d142a0af4c1761eef22e52965ebca63dc801c4b8ed9d885323f24eaa
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 899b2922cad..93e3ad40756 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17541,7 +17541,6 @@ CREATE TABLE issues ( health_status smallint, external_key character varying(255), sprint_id bigint, - issue_type smallint DEFAULT 0 NOT NULL, blocking_issues_count integer DEFAULT 0 NOT NULL, upvotes_count integer DEFAULT 0 NOT NULL, work_item_type_id bigint, @@ -30184,8 +30183,6 @@ CREATE UNIQUE INDEX idx_uniq_analytics_dashboards_pointers_on_project_id ON anal CREATE INDEX idx_user_details_on_provisioned_by_group_id_user_id ON user_details USING btree (provisioned_by_group_id, user_id); -CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, signature_sha); - CREATE UNIQUE INDEX idx_vuln_signatures_uniqueness_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, algorithm_type, signature_sha); CREATE INDEX idx_vulnerabilities_on_project_id_and_id_active_cis_dft_branch ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4])) AND (present_on_default_branch IS TRUE)); diff --git a/doc/.vale/gitlab/Markdown_emoji.yml b/doc/.vale/gitlab/Markdown_emoji.yml deleted file mode 100644 index 20f3ed0f5cb..00000000000 --- a/doc/.vale/gitlab/Markdown_emoji.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -# Warning: gitlab.Markdown_emoji -# -# Check for use of GLFM emoji syntax (https://docs.gitlab.com/ee/user/markdown.html#emoji), which doesn't render correctly in documentation. -# -# For a list of all options, see https://vale.sh/docs/topics/styles/ -extends: existence -message: "Replace '%s' with GitLab SVGs or Unicode emoji." -link: https://docs.gitlab.com/ee/development/documentation/styleguide/#gitlab-svg-icons -level: warning -scope: text -raw: - - '(?:\s+|^):[a-zA-Z0-9\-_\+]+:(?:\s+|$|\.)' diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md index db445b68d5a..98bbcc464b8 100644 --- a/doc/administration/reference_architectures/index.md +++ b/doc/administration/reference_architectures/index.md @@ -93,7 +93,7 @@ If you still need to have HA for a lower number of users, this can be achieved w #### Zero Downtime Upgrades -[Zero Downtime Upgrades](../../update/zero_downtime.md) are available for standard Reference Architecture environments with HA (Cloud Native Hybrid is not supported at this time). This allows for an environment to stay up during an upgrade, but the process is more complex as a result and has some limitations as detailed in the documentation. +[Zero Downtime Upgrades](../../update/zero_downtime.md) are available for standard Reference Architecture environments with HA (Cloud Native Hybrid is [not supported](https://gitlab.com/groups/gitlab-org/cloud-native/-/epics/52)). This allows for an environment to stay up during an upgrade, but the process is more complex as a result and has some limitations as detailed in the documentation. When going through this process it's worth noting that there may still be brief moments of downtime when the HA mechanisms take effect. diff --git a/doc/architecture/blueprints/gitlab_ci_events/index.md b/doc/architecture/blueprints/gitlab_ci_events/index.md index fb78c0f5d9d..51d65869dfb 100644 --- a/doc/architecture/blueprints/gitlab_ci_events/index.md +++ b/doc/architecture/blueprints/gitlab_ci_events/index.md @@ -44,7 +44,37 @@ Events" blueprint is about making it possible to: 1. Describe technology required to match subscriptions with events at GitLab.com scale and beyond. 1. Describe technology we could use to reduce the cost of running automation jobs significantly. -## Proposals +## Proposal + +### Requirements + +Any accepted proposal should take in consideration the following requirements and characteristics: + +1. Defining events should be done in separate files. + - If we define all events in a single file, then the single file gets too complicated and hard to + maintain for users. Then, users need to separate their configs with the `include` keyword again and we end up + with the same solution. + - The structure of the pipelines, the personas and the jobs will be different depending on the events being + subscribed to and the goals of the subscription. +1. A single subscription configuration file should define a single pipeline that is created when an event is triggered. + - The pipeline config can include other files with the `include` keyword. + - The pipeline can have many jobs and trigger child pipelines or multi-project pipelines. +1. The events and handling syntax should use the existing CI config syntax where it is pragmatic to do so. + - It'll be easier for users to adapt. It'll require less work to implement. +1. The event subscription and emiting events should be performant, scalable, and non blocking. + - Reading from the database is usually faster than reading from files. + - A CI event can potentially have many subscriptions. + This also includes evaluating the right YAML files to create pipelines. + - The main business logic (e.g. creating an issue) should not be affected + by any subscriptions to the given CI event (e.g. issue created). +1. The CI events design should be implemented in a maintainable and extensible way. + - If there is a `issues/create` event, then any new event (`merge_request/created`) can be added without + much effort. + - We expect that many events will be added. It should be trivial for developers to + register domain events (e.g. 'issue closed') as GitLab-defined CI events. + - Also, we should consider the opportunity of supporting user-defined CI events long term (e.g. 'order shipped'). + +### Options For now, we have technical 5 proposals; diff --git a/doc/development/sql.md b/doc/development/sql.md index 3292c90ee7c..e98d563f31b 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -558,3 +558,129 @@ searchable using Kibana. See [the runbook](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/patroni/pg_collect_query_data.md#searching-postgresql-logs-with-kibanaelasticsearch) for more details. + +## When to use common table expressions + +You can use common table expressions (CTEs) to create a temporary result set within a more complex query. +You can also use a recursive CTE to reference the CTE's result set within +the query itself. The following example queries a chain of +`personal access tokens` referencing each other in the +`previous_personal_access_token_id` column. + +```sql +WITH RECURSIVE "personal_access_tokens_cte" AS ( +( + SELECT + "personal_access_tokens".* + FROM + "personal_access_tokens" + WHERE + "personal_access_tokens"."previous_personal_access_token_id" = 15) + UNION ( + SELECT + "personal_access_tokens".* + FROM + "personal_access_tokens", + "personal_access_tokens_cte" + WHERE + "personal_access_tokens"."previous_personal_access_token_id" = "personal_access_tokens_cte"."id")) +SELECT + "personal_access_tokens".* +FROM + "personal_access_tokens_cte" AS "personal_access_tokens" + + id | previous_personal_access_token_id +----+----------------------------------- + 16 | 15 + 17 | 16 + 18 | 17 + 19 | 18 + 20 | 19 + 21 | 20 +(6 rows) +``` + +As CTEs are temporary result sets, you can use them within another `SELECT` +statement. Using CTEs with `UPDATE`, or `DELETE` could lead to unexpected +behavior: + +Consider the following method: + +```ruby +def personal_access_token_chain(token) + cte = Gitlab::SQL::RecursiveCTE.new(:personal_access_tokens_cte) + personal_access_token_table = Arel::Table.new(:personal_access_tokens) + + cte << PersonalAccessToken + .where(personal_access_token_table[:previous_personal_access_token_id].eq(token.id)) + cte << PersonalAccessToken + .from([personal_access_token_table, cte.table]) + .where(personal_access_token_table[:previous_personal_access_token_id].eq(cte.table[:id])) + PersonalAccessToken.with.recursive(cte.to_arel).from(cte.alias_to(personal_access_token_table)) +end +``` + +It works as expected when it is used to query data: + +```sql +> personal_access_token_chain(token) + +WITH RECURSIVE "personal_access_tokens_cte" AS ( +( + SELECT + "personal_access_tokens".* + FROM + "personal_access_tokens" + WHERE + "personal_access_tokens"."previous_personal_access_token_id" = 11) + UNION ( + SELECT + "personal_access_tokens".* + FROM + "personal_access_tokens", + "personal_access_tokens_cte" + WHERE + "personal_access_tokens"."previous_personal_access_token_id" = "personal_access_tokens_cte"."id")) +SELECT + "personal_access_tokens".* +FROM + "personal_access_tokens_cte" AS "personal_access_tokens" +``` + +However, the CTE is dropped when used with `#update_all`. As a result, the method +updates the entire table: + +```sql +> personal_access_token_chain(token).update_all(revoked: true) + +UPDATE + "personal_access_tokens" +SET + "revoked" = TRUE +``` + +To work around this behavior: + +1. Query the `ids` of the records: + + ```ruby + > token_ids = personal_access_token_chain(token).pluck_primary_key + => [16, 17, 18, 19, 20, 21] + ``` + +1. Use this array to scope `PersonalAccessTokens`: + + ```ruby + PersonalAccessToken.where(id: token_ids).update_all(revoked: true) + ``` + +Alternatively, combine these two steps: + +```ruby +PersonalAccessToken + .where(id: personal_access_token_chain(token).pluck_primary_key) + .update_all(revoked: true) +``` + +NOTE: +Avoid updating large volumes of unbounded data. If there are no [application limits](application_limits.md) on the data, or you are unsure about the data volume, you should [update the data in batches](database/iterating_tables_in_batches.md). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index c3e4f77411c..8550d64854c 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -215,8 +215,6 @@ For more information, see the [Kroki integration](../administration/integration/ :::TabTitle Rendered Markdown -<!-- vale gitlab.Markdown_emoji = NO --> - Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/monkey.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":monkey:" alt=":monkey:"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/star2.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":star2:" alt=":star2:"> to your <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":speech_balloon:" alt=":speech_balloon:">. Well we have a gift for you: <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/zap.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":zap:" alt=":zap:">You can use emoji anywhere GitLab Flavored Markdown is supported. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/v.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":v:" alt=":v:"> @@ -227,8 +225,6 @@ If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-f Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":thumbsup:" alt=":thumbsup:"> -<!-- vale gitlab.Markdown_emoji = YES --> - :::TabTitle Code ```markdown diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b12ca48829b..bbd7ed1014c 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -210,6 +210,7 @@ module API requires :slack_app_signing_secret, type: String, desc: 'The signing secret of the GitLab for Slack app. Used for authenticating API requests from the app' requires :slack_app_verification_token, type: String, desc: 'The verification token of the GitLab for Slack app. This method of authentication is deprecated by Slack and used only for authenticating slash commands from the app' end + optional :namespace_aggregation_schedule_lease_duration_in_seconds, type: Integer, desc: 'Maximum duration (in seconds) between refreshes of namespace statistics (Default: 300)' Gitlab::SSHPublicKey.supported_types.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index fe2fa892058..27c4ec2f7be 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -51,6 +51,7 @@ module Gitlab gon.dot_com = Gitlab.com? gon.uf_error_prefix = ::Gitlab::Utils::ErrorMessage::UF_ERROR_PREFIX gon.pat_prefix = Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix + gon.use_new_navigation = NavHelper.show_super_sidebar?(current_user) gon.diagramsnet_url = Gitlab::CurrentSettings.diagramsnet_url if Gitlab::CurrentSettings.diagramsnet_enabled @@ -61,7 +62,6 @@ module Gitlab gon.current_user_fullname = current_user.name gon.current_user_avatar_url = current_user.avatar_url gon.time_display_relative = current_user.time_display_relative - gon.use_new_navigation = NavHelper.show_super_sidebar?(current_user) end # Initialize gon.features with any flags that should be diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0615ec61cf4..6f39e75156d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -21419,9 +21419,6 @@ msgstr "" msgid "GlobalSearch|Recent merge requests" msgstr "" -msgid "GlobalSearch|Reset filters" -msgstr "" - msgid "GlobalSearch|Result count is over limit." msgstr "" diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index c95375dbbb9..214eaebfa75 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -19,8 +19,8 @@ module QA end view 'app/assets/javascripts/issues/show/components/header_actions.vue' do - element :toggle_issue_state_button - element :desktop_dropdown + element 'toggle-issue-state-button' + element 'desktop-dropdown' element :delete_issue_button end @@ -63,21 +63,17 @@ module QA end def click_close_issue_button - # Click by JS is needed to bypass the Moved MR actions popover - # Change back to regular click_element when moved_mr_sidebar FF is removed - # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 - click_by_javascript(find_element(:toggle_issue_state_button, text: 'Close issue')) + open_actions_dropdown + click_element('toggle-issue-state-button', text: 'Close issue') end def has_reopen_issue_button? - has_element?(:toggle_issue_state_button, text: 'Reopen issue') + open_actions_dropdown + has_element?('toggle-issue-state-button', text: 'Reopen issue') end def has_delete_issue_button? - # Click by JS is needed to bypass the Moved MR actions popover - # Change back to regular click_element when moved_mr_sidebar FF is removed - # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/385460 - click_by_javascript(find('[data-testid="desktop-dropdown"] > button')) + open_actions_dropdown has_element?(:delete_issue_button) end @@ -94,6 +90,11 @@ module QA wait_for_requests end + + def open_actions_dropdown + # We use find here because these are gitlab-ui elements + find('[data-testid="desktop-dropdown"] > button').click + end end end end diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js index ba492833ec4..aace9c8f83e 100644 --- a/spec/frontend/search/sidebar/components/app_spec.js +++ b/spec/frontend/search/sidebar/components/app_spec.js @@ -4,9 +4,10 @@ import Vuex from 'vuex'; import { MOCK_QUERY } from 'jest/search/mock_data'; import GlobalSearchSidebar from '~/search/sidebar/components/app.vue'; import IssuesFilters from '~/search/sidebar/components/issues_filters.vue'; +import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue'; +import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue'; import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue'; import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue'; -import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue'; Vue.use(Vuex); @@ -17,7 +18,7 @@ describe('GlobalSearchSidebar', () => { currentScope: jest.fn(() => 'issues'), }; - const createComponent = (initialState = {}, featureFlags = {}) => { + const createComponent = (initialState = {}) => { const store = new Vuex.Store({ state: { urlQuery: MOCK_QUERY, @@ -28,19 +29,15 @@ describe('GlobalSearchSidebar', () => { wrapper = shallowMount(GlobalSearchSidebar, { store, - provide: { - glFeatures: { - ...featureFlags, - }, - }, }); }; const findSidebarSection = () => wrapper.find('section'); - const findFilters = () => wrapper.findComponent(IssuesFilters); + const findIssuesFilters = () => wrapper.findComponent(IssuesFilters); + const findMergeRequestsFilters = () => wrapper.findComponent(MergeRequestsFilters); + const findBlobsFilters = () => wrapper.findComponent(BlobsFilters); const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation); const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation); - const findLanguageAggregation = () => wrapper.findComponent(LanguageFilter); describe('renders properly', () => { describe('always', () => { @@ -53,23 +50,18 @@ describe('GlobalSearchSidebar', () => { }); describe.each` - scope | showFilters | showsLanguage - ${'issues'} | ${true} | ${false} - ${'merge_requests'} | ${true} | ${false} - ${'projects'} | ${false} | ${false} - ${'blobs'} | ${false} | ${true} - `('sidebar scope: $scope', ({ scope, showFilters, showsLanguage }) => { + scope | filter + ${'issues'} | ${findIssuesFilters} + ${'merge_requests'} | ${findMergeRequestsFilters} + ${'blobs'} | ${findBlobsFilters} + `('with sidebar $scope scope:', ({ scope, filter }) => { beforeEach(() => { getterSpies.currentScope = jest.fn(() => scope); createComponent({ urlQuery: { scope } }); }); - it(`${!showFilters ? "doesn't" : ''} shows filters`, () => { - expect(findFilters().exists()).toBe(showFilters); - }); - - it(`${!showsLanguage ? "doesn't" : ''} shows language filters`, () => { - expect(findLanguageAggregation().exists()).toBe(showsLanguage); + it(`shows filter ${filter.name.replace('find', '')}`, () => { + expect(filter().exists()).toBe(true); }); }); diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js new file mode 100644 index 00000000000..ff93e6f32e4 --- /dev/null +++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; +import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue'; +import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue'; +import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; + +describe('GlobalSearch BlobsFilters', () => { + let wrapper; + + const findLanguageFilter = () => wrapper.findComponent(LanguageFilter); + const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate); + + const createComponent = () => { + wrapper = shallowMount(BlobsFilters); + }; + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent(); + }); + it('renders FiltersTemplate', () => { + expect(findLanguageFilter().exists()).toBe(true); + }); + + it('renders ConfidentialityFilter', () => { + expect(findFiltersTemplate().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js index 546a84ff040..42508d7e216 100644 --- a/spec/frontend/search/sidebar/components/filters_spec.js +++ b/spec/frontend/search/sidebar/components/filters_spec.js @@ -1,4 +1,3 @@ -import { GlButton, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; @@ -36,11 +35,8 @@ describe('GlobalSearchSidebarFilters', () => { }); }; - const findSidebarForm = () => wrapper.find('form'); const findStatusFilter = () => wrapper.findComponent(StatusFilter); const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); - const findApplyButton = () => wrapper.findComponent(GlButton); - const findResetLinkButton = () => wrapper.findComponent(GlLink); describe('Renders correctly', () => { beforeEach(() => { @@ -53,82 +49,6 @@ describe('GlobalSearchSidebarFilters', () => { it('renders ConfidentialityFilter', () => { expect(findConfidentialityFilter().exists()).toBe(true); }); - - it('renders ApplyButton', () => { - expect(findApplyButton().exists()).toBe(true); - }); - }); - - describe('ApplyButton', () => { - describe('when sidebarDirty is false', () => { - beforeEach(() => { - createComponent({ sidebarDirty: false }); - }); - - it('disables the button', () => { - expect(findApplyButton().attributes('disabled')).toBeDefined(); - }); - }); - - describe('when sidebarDirty is true', () => { - beforeEach(() => { - createComponent({ sidebarDirty: true }); - }); - - it('enables the button', () => { - expect(findApplyButton().attributes('disabled')).toBe(undefined); - }); - }); - }); - - describe('ResetLinkButton', () => { - describe('with no filter selected', () => { - beforeEach(() => { - createComponent({ urlQuery: {} }); - }); - - it('does not render', () => { - expect(findResetLinkButton().exists()).toBe(false); - }); - }); - - describe('with filter selected', () => { - beforeEach(() => { - createComponent({ urlQuery: MOCK_QUERY }); - }); - - it('does render', () => { - expect(findResetLinkButton().exists()).toBe(true); - }); - }); - - describe('with filter selected and user updated query back to default', () => { - beforeEach(() => { - createComponent({ urlQuery: MOCK_QUERY, query: {} }); - }); - - it('does render', () => { - expect(findResetLinkButton().exists()).toBe(true); - }); - }); - }); - - describe('actions', () => { - beforeEach(() => { - createComponent({}); - }); - - it('clicking ApplyButton calls applyQuery', () => { - findSidebarForm().trigger('submit'); - - expect(actionSpies.applyQuery).toHaveBeenCalled(); - }); - - it('clicking ResetLinkButton calls resetQuery', () => { - findResetLinkButton().vm.$emit('click'); - - expect(actionSpies.resetQuery).toHaveBeenCalled(); - }); }); describe.each` diff --git a/spec/frontend/search/sidebar/components/filters_template_spec.js b/spec/frontend/search/sidebar/components/filters_template_spec.js new file mode 100644 index 00000000000..11c7c541b54 --- /dev/null +++ b/spec/frontend/search/sidebar/components/filters_template_spec.js @@ -0,0 +1,167 @@ +import { GlForm, GlButton, GlLink } from '@gitlab/ui'; +import Vue from 'vue'; +import Vuex from 'vuex'; + +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { MOCK_QUERY, MOCK_AGGREGATIONS } from 'jest/search/mock_data'; + +import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; + +import { + TRACKING_ACTION_CLICK, + TRACKING_LABEL_APPLY, + TRACKING_LABEL_RESET, +} from '~/search/sidebar/constants/index'; + +Vue.use(Vuex); + +describe('GlobalSearchSidebarLanguageFilter', () => { + let wrapper; + let trackingSpy; + + const actionSpies = { + applyQuery: jest.fn(), + resetQuery: jest.fn(), + }; + + const getterSpies = { + currentScope: jest.fn(() => 'issues'), + }; + + const createComponent = (initialState) => { + const store = new Vuex.Store({ + state: { + query: MOCK_QUERY, + urlQuery: MOCK_QUERY, + aggregations: MOCK_AGGREGATIONS, + sidebarDirty: false, + ...initialState, + }, + actions: actionSpies, + getters: getterSpies, + }); + + wrapper = shallowMountExtended(FiltersTemplate, { + store, + slots: { + default: '<p>Filters Content</p>', + }, + }); + }; + + const findForm = () => wrapper.findComponent(GlForm); + const findDividers = () => wrapper.findAll('hr'); + const findApplyButton = () => wrapper.findComponent(GlButton); + const findResetButton = () => wrapper.findComponent(GlLink); + const findSlotContent = () => wrapper.findByText('Filters Content'); + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders form', () => { + expect(findForm().exists()).toBe(true); + }); + + it('renders dividers', () => { + expect(findDividers()).toHaveLength(2); + }); + + it('renders slot content', () => { + expect(findSlotContent().exists()).toBe(true); + }); + + it('renders ApplyButton', () => { + expect(findApplyButton().exists()).toBe(true); + }); + + it('renders reset button', () => { + expect(findResetButton().exists()).toBe(false); + }); + }); + + describe('resetButton', () => { + describe.each` + description | sidebarDirty | queryLangFilters | exists + ${'sidebar dirty only'} | ${true} | ${[]} | ${true} + ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${false} + ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${true} + ${'sidebar not dirty and no query filters'} | ${false} | ${[]} | ${false} + `('$description', ({ sidebarDirty, queryLangFilters, exists }) => { + beforeEach(() => { + getterSpies.queryLanguageFilters = jest.fn(() => queryLangFilters); + + const query = { + ...MOCK_QUERY, + language: queryLangFilters, + state: undefined, + labels: undefined, + confidential: undefined, + }; + + createComponent({ + sidebarDirty, + query, + urlQuery: query, + }); + }); + + it(`button is ${exists ? 'shown' : 'hidden'}`, () => { + expect(findResetButton().exists()).toBe(exists); + }); + }); + }); + + describe('ApplyButton', () => { + describe('when sidebarDirty is false', () => { + beforeEach(() => { + createComponent({ sidebarDirty: false }); + }); + + it('disables the button', () => { + expect(findApplyButton().attributes('disabled')).toBeDefined(); + }); + }); + + describe('when sidebarDirty is true', () => { + beforeEach(() => { + createComponent({ sidebarDirty: true }); + }); + + it('enables the button', () => { + expect(findApplyButton().attributes('disabled')).toBe(undefined); + }); + }); + }); + + describe('actions', () => { + beforeEach(() => { + createComponent({ sidebarDirty: true }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('clicking ApplyButton calls applyQuery', () => { + findForm().vm.$emit('submit', { preventDefault: () => {} }); + + expect(actionSpies.applyQuery).toHaveBeenCalled(); + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, { + label: getterSpies.currentScope(), + }); + }); + + it('clicking resetButton calls resetQuery', () => { + findResetButton().vm.$emit('click'); + + expect(actionSpies.resetQuery).toHaveBeenCalled(); + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_RESET, { + label: getterSpies.currentScope(), + }); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js new file mode 100644 index 00000000000..cab3a78bd34 --- /dev/null +++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js @@ -0,0 +1,106 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import { MOCK_QUERY } from 'jest/search/mock_data'; +import IssuesFilters from '~/search/sidebar/components/issues_filters.vue'; +import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter/index.vue'; +import StatusFilter from '~/search/sidebar/components/status_filter/index.vue'; +import LabelFilter from '~/search/sidebar/components/label_filter/index.vue'; + +Vue.use(Vuex); + +describe('GlobalSearch IssuesFilters', () => { + let wrapper; + + const defaultGetters = { + currentScope: () => 'issues', + }; + + const createComponent = (initialState, ff = true) => { + const store = new Vuex.Store({ + state: { + urlQuery: MOCK_QUERY, + ...initialState, + }, + getters: defaultGetters, + }); + + wrapper = shallowMount(IssuesFilters, { + store, + provide: { + glFeatures: { + searchIssueLabelAggregation: ff, + }, + }, + }); + }; + + const findStatusFilter = () => wrapper.findComponent(StatusFilter); + const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter); + const findLabelFilter = () => wrapper.findComponent(LabelFilter); + const findDividers = () => wrapper.findAll('hr'); + + describe('Renders correctly with FF enabled', () => { + beforeEach(() => { + createComponent({ urlQuery: MOCK_QUERY }); + }); + it('renders StatusFilter', () => { + expect(findStatusFilter().exists()).toBe(true); + }); + + it('renders ConfidentialityFilter', () => { + expect(findConfidentialityFilter().exists()).toBe(true); + }); + + it('renders LabelFilter', () => { + expect(findLabelFilter().exists()).toBe(true); + }); + + it('renders dividers correctly', () => { + expect(findDividers()).toHaveLength(2); + }); + }); + + describe('Renders correctly with FF disabled', () => { + beforeEach(() => { + createComponent({ urlQuery: MOCK_QUERY }, false); + }); + it('renders StatusFilter', () => { + expect(findStatusFilter().exists()).toBe(true); + }); + + it('renders ConfidentialityFilter', () => { + expect(findConfidentialityFilter().exists()).toBe(true); + }); + + it("doesn't render LabelFilter", () => { + expect(findLabelFilter().exists()).toBe(false); + }); + + it('renders divider correctly', () => { + expect(findDividers()).toHaveLength(1); + }); + }); + + describe('Renders correctly with wrong scope', () => { + beforeEach(() => { + defaultGetters.currentScope = () => 'blobs'; + createComponent({ urlQuery: MOCK_QUERY }); + }); + it("doesn't render StatusFilter", () => { + expect(findStatusFilter().exists()).toBe(false); + }); + + it("doesn't render ConfidentialityFilter", () => { + expect(findConfidentialityFilter().exists()).toBe(false); + }); + + it("doesn't render LabelFilter", () => { + expect(findLabelFilter().exists()).toBe(false); + }); + + it("doesn't render dividers", () => { + expect(findDividers()).toHaveLength(0); + }); + }); +}); diff --git a/spec/frontend/search/sidebar/components/language_filter_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js index 817199d7cfe..88be8f908d7 100644 --- a/spec/frontend/search/sidebar/components/language_filter_spec.js +++ b/spec/frontend/search/sidebar/components/language_filter_spec.js @@ -1,4 +1,4 @@ -import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui'; +import { GlAlert, GlFormCheckbox } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; @@ -13,13 +13,11 @@ import CheckboxFilter from '~/search/sidebar/components/language_filter/checkbox import { TRACKING_LABEL_SHOW_MORE, - TRACKING_CATEGORY, TRACKING_PROPERTY_MAX, TRACKING_LABEL_MAX, TRACKING_LABEL_FILTERS, TRACKING_ACTION_SHOW, TRACKING_ACTION_CLICK, - TRACKING_LABEL_APPLY, TRACKING_LABEL_ALL, } from '~/search/sidebar/components/language_filter/tracking'; @@ -61,10 +59,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }); }; - const findForm = () => wrapper.findComponent(GlForm); const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter); - const findApplyButton = () => wrapper.findByTestId('apply-button'); - const findResetButton = () => wrapper.findByTestId('reset-button'); const findShowMoreButton = () => wrapper.findByTestId('show-more-button'); const findAlert = () => wrapper.findComponent(GlAlert); const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); @@ -80,10 +75,6 @@ describe('GlobalSearchSidebarLanguageFilter', () => { unmockTracking(); }); - it('renders form', () => { - expect(findForm().exists()).toBe(true); - }); - it('renders checkbox-filter', () => { expect(findCheckboxFilter().exists()).toBe(true); }); @@ -93,10 +84,6 @@ describe('GlobalSearchSidebarLanguageFilter', () => { expect(findAllCheckboxes()).toHaveLength(10); }); - it('renders ApplyButton', () => { - expect(findApplyButton().exists()).toBe(true); - }); - it('renders Show More button', () => { expect(findShowMoreButton().exists()).toBe(true); }); @@ -106,47 +93,6 @@ describe('GlobalSearchSidebarLanguageFilter', () => { }); }); - describe('resetButton', () => { - describe.each` - description | sidebarDirty | queryFilters | exists - ${'sidebar dirty only'} | ${true} | ${[]} | ${false} - ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${false} - ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${true} - ${'no sidebar and no query filters'} | ${false} | ${[]} | ${false} - `('$description', ({ sidebarDirty, queryFilters, exists }) => { - beforeEach(() => { - getterSpies.queryLanguageFilters = jest.fn(() => queryFilters); - createComponent({ sidebarDirty, query: { ...MOCK_QUERY, language: queryFilters } }); - }); - - it(`button is ${exists ? 'shown' : 'hidden'}`, () => { - expect(findResetButton().exists()).toBe(exists); - }); - }); - }); - - describe('ApplyButton', () => { - describe('when sidebarDirty is false', () => { - beforeEach(() => { - createComponent({ sidebarDirty: false }); - }); - - it('disables the button', () => { - expect(findApplyButton().attributes('disabled')).toBeDefined(); - }); - }); - - describe('when sidebarDirty is true', () => { - beforeEach(() => { - createComponent({ sidebarDirty: true }); - }); - - it('enables the button', () => { - expect(findApplyButton().attributes('disabled')).toBe(undefined); - }); - }); - }); - describe('Show All button works', () => { beforeEach(() => { createComponent(); @@ -211,19 +157,5 @@ describe('GlobalSearchSidebarLanguageFilter', () => { it('uses action fetchAllAggregation', () => { expect(actionSpies.fetchAllAggregation).toHaveBeenCalled(); }); - - it('clicking ApplyButton calls applyQuery', () => { - findForm().vm.$emit('submit', { preventDefault: () => {} }); - - expect(actionSpies.applyQuery).toHaveBeenCalled(); - }); - - it('sends tracking information clicking ApplyButton', () => { - findForm().vm.$emit('submit', { preventDefault: () => {} }); - - expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, { - label: TRACKING_CATEGORY, - }); - }); }); }); diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js new file mode 100644 index 00000000000..0932f8e47d2 --- /dev/null +++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js @@ -0,0 +1,28 @@ +import { shallowMount } from '@vue/test-utils'; +import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue'; +import StatusFilter from '~/search/sidebar/components/status_filter/index.vue'; +import FiltersTemplate from '~/search/sidebar/components/filters_template.vue'; + +describe('GlobalSearch MergeRequestsFilters', () => { + let wrapper; + + const findStatusFilter = () => wrapper.findComponent(StatusFilter); + const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate); + + const createComponent = () => { + wrapper = shallowMount(MergeRequestsFilters); + }; + + describe('Renders correctly', () => { + beforeEach(() => { + createComponent(); + }); + it('renders ConfidentialityFilter', () => { + expect(findStatusFilter().exists()).toBe(true); + }); + + it('renders FiltersTemplate', () => { + expect(findFiltersTemplate().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js index 2051e731647..a6ec7a06cfb 100644 --- a/spec/frontend/search/store/actions_spec.js +++ b/spec/frontend/search/store/actions_spec.js @@ -327,28 +327,6 @@ describe('Global Search Store Actions', () => { }); }); - describe('resetLanguageQueryWithRedirect', () => { - it('calls visitUrl and setParams with the state.query', () => { - return testAction(actions.resetLanguageQueryWithRedirect, null, state, [], [], () => { - expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null }); - expect(urlUtils.visitUrl).toHaveBeenCalled(); - }); - }); - }); - - describe('resetLanguageQuery', () => { - it('calls commit SET_QUERY with value []', () => { - state = { ...state, query: { ...state.query, language: ['YAML', 'Text', 'Markdown'] } }; - return testAction( - actions.resetLanguageQuery, - null, - state, - [{ type: types.SET_QUERY, payload: { key: 'language', value: [] } }], - [], - ); - }); - }); - describe('closeLabel', () => { beforeEach(() => { state = createState({ diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js index 772acb39a57..66a888037ea 100644 --- a/spec/frontend/search/store/getters_spec.js +++ b/spec/frontend/search/store/getters_spec.js @@ -70,18 +70,6 @@ describe('Global Search Store Getters', () => { }); }); - describe('currentUrlQueryHasLanguageFilters', () => { - it.each` - description | lang | result - ${'has valid language'} | ${{ language: ['a', 'b'] }} | ${true} - ${'has empty lang'} | ${{ language: [] }} | ${false} - ${'has no lang'} | ${{}} | ${false} - `('$description', ({ lang, result }) => { - state.urlQuery = lang; - expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result); - }); - }); - describe('navigationItems', () => { it('returns the re-mapped navigation data', () => { state.navigation = MOCK_NAVIGATION; diff --git a/spec/frontend/super_sidebar/components/context_header_spec.js b/spec/frontend/super_sidebar/components/context_header_spec.js new file mode 100644 index 00000000000..943b659c997 --- /dev/null +++ b/spec/frontend/super_sidebar/components/context_header_spec.js @@ -0,0 +1,50 @@ +import { GlAvatar } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ContextHeader from '~/super_sidebar/components/context_header.vue'; + +describe('ContextHeader component', () => { + let wrapper; + + const context = { + id: 1, + title: 'Title', + avatar: '/path/to/avatar.png', + }; + + const findGlAvatar = () => wrapper.getComponent(GlAvatar); + + const createWrapper = (props = {}) => { + wrapper = shallowMountExtended(ContextHeader, { + propsData: { + context, + expanded: false, + ...props, + }, + }); + }; + + describe('with an avatar', () => { + it('passes the correct props to GlAvatar', () => { + createWrapper(); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe('rect'); + expect(avatar.props('entityName')).toBe(context.title); + expect(avatar.props('entityId')).toBe(context.id); + expect(avatar.props('src')).toBe(context.avatar); + }); + + it('renders the avatar with a custom shape', () => { + const customShape = 'circle'; + createWrapper({ + context: { + ...context, + avatar_shape: customShape, + }, + }); + const avatar = findGlAvatar(); + + expect(avatar.props('shape')).toBe(customShape); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js index 7172b60d0fa..c20d3c2745f 100644 --- a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js +++ b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js @@ -1,4 +1,4 @@ -import { GlAvatar } from '@gitlab/ui'; +import { GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue'; @@ -11,7 +11,7 @@ describe('ContextSwitcherToggle component', () => { avatar: '/path/to/avatar.png', }; - const findGlAvatar = () => wrapper.getComponent(GlAvatar); + const findGlIcon = () => wrapper.getComponent(GlIcon); const createWrapper = (props = {}) => { wrapper = shallowMountExtended(ContextSwitcherToggle, { @@ -23,28 +23,17 @@ describe('ContextSwitcherToggle component', () => { }); }; - describe('with an avatar', () => { - it('passes the correct props to GlAvatar', () => { - createWrapper(); - const avatar = findGlAvatar(); + it('renders "chevron-down" icon when not expanded', () => { + createWrapper(); - expect(avatar.props('shape')).toBe('rect'); - expect(avatar.props('entityName')).toBe(context.title); - expect(avatar.props('entityId')).toBe(context.id); - expect(avatar.props('src')).toBe(context.avatar); - }); + expect(findGlIcon().props('name')).toBe('chevron-down'); + }); - it('renders the avatar with a custom shape', () => { - const customShape = 'circle'; - createWrapper({ - context: { - ...context, - avatar_shape: customShape, - }, - }); - const avatar = findGlAvatar(); - - expect(avatar.props('shape')).toBe(customShape); + it('renders "chevron-up" icon when expanded', () => { + createWrapper({ + expanded: true, }); + + expect(findGlIcon().props('name')).toBe('chevron-up'); }); }); diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js index ac94f3f8f82..5d9a35fbf70 100644 --- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js +++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js @@ -25,6 +25,7 @@ describe('Sidebar Menu', () => { }, propsData: { items: sidebarData.current_menu_items, + isLoggedIn: sidebarData.is_logged_in, pinnedItemIds: sidebarData.pinned_items, panelType: sidebarData.panel_type, updatePinsUrl: sidebarData.update_pins_url, diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js index abd9c1dc44d..94ef072a951 100644 --- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js +++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js @@ -35,8 +35,10 @@ describe('SidebarPeek component', () => { let wrapper; let trackingSpy = null; - const createComponent = () => { - wrapper = mount(SidebarPeek); + const createComponent = (props = { isMouseOverSidebar: false }) => { + wrapper = mount(SidebarPeek, { + propsData: props, + }); }; const moveMouse = (clientX) => { @@ -163,6 +165,17 @@ describe('SidebarPeek component', () => { expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_CLOSED]); }); + it('does not transition to will-close or closed when mouse is over sidebar child element', () => { + createComponent({ isMouseOverSidebar: true }); + moveMouse(0); + jest.runOnlyPendingTimers(); + + moveMouse(X_SIDEBAR_EDGE); + moveMouse(X_AWAY_FROM_SIDEBAR); + + expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]); + }); + it('immediately transitions will-close -> closed if mouse moves far away', () => { moveMouse(0); jest.runOnlyPendingTimers(); diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js index 0c785109b5e..7b7b8a7be13 100644 --- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js +++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js @@ -11,6 +11,7 @@ import SidebarPeekBehavior, { STATE_WILL_CLOSE, } from '~/super_sidebar/components/sidebar_peek_behavior.vue'; import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue'; +import ContextHeader from '~/super_sidebar/components/context_header.vue'; import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue'; import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue'; import { sidebarState } from '~/super_sidebar/constants'; @@ -20,7 +21,7 @@ import { } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; import { stubComponent } from 'helpers/stub_component'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; -import { sidebarData as mockSidebarData } from '../mock_data'; +import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data'; const initialSidebarState = { ...sidebarState }; @@ -42,6 +43,7 @@ describe('SuperSidebar component', () => { const findSidebar = () => wrapper.findByTestId('super-sidebar'); const findUserBar = () => wrapper.findComponent(UserBar); + const findContextHeader = () => wrapper.findComponent(ContextHeader); const findContextSwitcher = () => wrapper.findComponent(ContextSwitcher); const findNavContainer = () => wrapper.findByTestId('nav-container'); const findHelpCenter = () => wrapper.findComponent(HelpCenter); @@ -230,6 +232,15 @@ describe('SuperSidebar component', () => { expect(findSidebar().classes()).not.toContain(peekHintClass); }, ); + + it('keeps track of if sidebar has mouseover or not', async () => { + createWrapper({ sidebarState: { isCollapsed: false, isPeekable: true } }); + expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(false); + await findSidebar().trigger('mouseenter'); + expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(true); + await findSidebar().trigger('mouseleave'); + expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(false); + }); }); describe('nav container', () => { @@ -259,4 +270,15 @@ describe('SuperSidebar component', () => { expect(findTrialStatusPopover().exists()).toBe(true); }); }); + + describe('Logged out', () => { + beforeEach(() => { + createWrapper({ sidebarData: loggedOutSidebarData }); + }); + + it('renders context header instead of context switcher', () => { + expect(findContextHeader().exists()).toBe(true); + expect(findContextSwitcher().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js index 272e0237219..d90553dfdd3 100644 --- a/spec/frontend/super_sidebar/components/user_bar_spec.js +++ b/spec/frontend/super_sidebar/components/user_bar_spec.js @@ -4,6 +4,7 @@ import Vue, { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { __ } from '~/locale'; import CreateMenu from '~/super_sidebar/components/create_menu.vue'; +import UserMenu from '~/super_sidebar/components/user_menu.vue'; import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue'; import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue'; import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue'; @@ -11,13 +12,14 @@ import UserBar from '~/super_sidebar/components/user_bar.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import waitForPromises from 'helpers/wait_for_promises'; import { userCounts } from '~/super_sidebar/user_counts_manager'; -import { sidebarData } from '../mock_data'; +import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data'; import { MOCK_DEFAULT_SEARCH_OPTIONS } from './global_search/mock_data'; describe('UserBar component', () => { let wrapper; const findCreateMenu = () => wrapper.findComponent(CreateMenu); + const findUserMenu = () => wrapper.findComponent(UserMenu); const findIssuesCounter = () => wrapper.findByTestId('issues-shortcut-button'); const findMRsCounter = () => wrapper.findByTestId('merge-requests-shortcut-button'); const findTodosCounter = () => wrapper.findByTestId('todos-shortcut-button'); @@ -37,13 +39,13 @@ describe('UserBar component', () => { }); const createWrapper = ({ hasCollapseButton = true, - extraSidebarData = {}, + sidebarData = mockSidebarData, provideOverrides = {}, } = {}) => { wrapper = shallowMountExtended(UserBar, { propsData: { hasCollapseButton, - sidebarData: { ...sidebarData, ...extraSidebarData }, + sidebarData, }, provide: { toggleNewNavEndpoint: '/-/profile/preferences', @@ -63,17 +65,17 @@ describe('UserBar component', () => { }); it('passes the "Create new..." menu groups to the create-menu component', () => { - expect(findCreateMenu().props('groups')).toBe(sidebarData.create_new_menu_groups); + expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups); }); it('passes the "Merge request" menu groups to the merge_request_menu component', () => { - expect(findMergeRequestMenu().props('items')).toBe(sidebarData.merge_request_menu); + expect(findMergeRequestMenu().props('items')).toBe(mockSidebarData.merge_request_menu); }); it('renders issues counter', () => { const isuesCounter = findIssuesCounter(); expect(isuesCounter.props('count')).toBe(userCounts.assigned_issues); - expect(isuesCounter.props('href')).toBe(sidebarData.issues_dashboard_path); + expect(isuesCounter.props('href')).toBe(mockSidebarData.issues_dashboard_path); expect(isuesCounter.props('label')).toBe(__('Issues')); expect(isuesCounter.attributes('data-track-action')).toBe('click_link'); expect(isuesCounter.attributes('data-track-label')).toBe('issues_link'); @@ -95,7 +97,7 @@ describe('UserBar component', () => { describe('Todos counter', () => { it('renders it', () => { const todosCounter = findTodosCounter(); - expect(todosCounter.props('href')).toBe(sidebarData.todos_dashboard_path); + expect(todosCounter.props('href')).toBe(mockSidebarData.todos_dashboard_path); expect(todosCounter.props('label')).toBe(__('To-Do list')); expect(todosCounter.attributes('data-track-action')).toBe('click_link'); expect(todosCounter.attributes('data-track-label')).toBe('todos_link'); @@ -114,7 +116,7 @@ describe('UserBar component', () => { it('renders branding logo', () => { expect(findBrandLogo().exists()).toBe(true); - expect(findBrandLogo().props('logoUrl')).toBe(sidebarData.logo_url); + expect(findBrandLogo().props('logoUrl')).toBe(mockSidebarData.logo_url); }); it('does not render the "Stop impersonating" button', () => { @@ -134,16 +136,16 @@ describe('UserBar component', () => { describe('GitLab Next badge', () => { describe('when on canary', () => { it('should render a badge to switch off GitLab Next', () => { - createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } }); + createWrapper({ sidebarData: { ...mockSidebarData, gitlab_com_and_canary: true } }); const badge = wrapper.findComponent(GlBadge); expect(badge.text()).toBe('Next'); - expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url); + expect(badge.attributes('href')).toBe(mockSidebarData.canary_toggle_com_url); }); }); describe('when not on canary', () => { it('should not render the GitLab Next badge', () => { - createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } }); + createWrapper({ sidebarData: { ...mockSidebarData, gitlab_com_and_canary: false } }); const badge = wrapper.findComponent(GlBadge); expect(badge.exists()).toBe(false); }); @@ -206,8 +208,36 @@ describe('UserBar component', () => { it('sets the href and data-method attributes', () => { const btn = findStopImpersonationButton(); - expect(btn.attributes('href')).toBe(sidebarData.stop_impersonation_path); + expect(btn.attributes('href')).toBe(mockSidebarData.stop_impersonation_path); expect(btn.attributes('data-method')).toBe('delete'); }); }); + + describe('Logged out', () => { + beforeEach(() => { + createWrapper({ sidebarData: loggedOutSidebarData, gitlab_com_and_canary: true }); + }); + + it('does not render brand logo', () => { + expect(findBrandLogo().exists()).toBe(false); + }); + + it('does not render Next badge', () => { + expect(wrapper.findComponent(GlBadge).exists()).toBe(false); + }); + + it('does not render create menu', () => { + expect(findCreateMenu().exists()).toBe(false); + }); + + it('does not render user menu', () => { + expect(findUserMenu().exists()).toBe(false); + }); + + it('does not render counters', () => { + expect(findIssuesCounter().exists()).toBe(false); + expect(findMRsCounter().exists()).toBe(false); + expect(findTodosCounter().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index 72c67e34038..df45360a898 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -72,6 +72,7 @@ export const mergeRequestMenuGroup = [ ]; export const sidebarData = { + is_logged_in: true, current_menu_items: [], current_context_header: { title: 'Your Work', @@ -120,6 +121,26 @@ export const sidebarData = { ], }; +export const loggedOutSidebarData = { + is_logged_in: false, + current_menu_items: [], + current_context_header: { + title: 'Your Work', + icon: 'work', + }, + support_path: '/support', + display_whats_new: true, + whats_new_most_recent_release_items_count: 5, + whats_new_version_digest: 1, + show_version_check: false, + gitlab_version: { major: 16, minor: 0 }, + gitlab_version_check: { severity: 'success' }, + search: { + search_path: '/search', + }, + panel_type: 'your_work', +}; + export const userMenuMockStatus = { can_update: false, busy: false, diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb index f924704ab54..9d591164547 100644 --- a/spec/helpers/application_settings_helper_spec.rb +++ b/spec/helpers/application_settings_helper_spec.rb @@ -74,6 +74,10 @@ RSpec.describe ApplicationSettingsHelper do expect(helper.visible_attributes).to include(*params) end + it 'contains :namespace_aggregation_schedule_lease_duration_in_seconds' do + expect(helper.visible_attributes).to include(:namespace_aggregation_schedule_lease_duration_in_seconds) + end + context 'when on SaaS', :saas do it 'does not contain :deactivate_dormant_users' do expect(helper.visible_attributes).not_to include(:deactivate_dormant_users) diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb index 4a02b184522..4b83561b265 100644 --- a/spec/helpers/nav_helper_spec.rb +++ b/spec/helpers/nav_helper_spec.rb @@ -169,15 +169,39 @@ RSpec.describe NavHelper, feature_category: :navigation do end end - context 'when nil is provided' do - specify { expect(helper.show_super_sidebar?(nil)).to eq false } + shared_examples 'anonymous show_super_sidebar is supposed to' do + before do + stub_feature_flags(super_sidebar_logged_out: feature_flag) + end + + context 'when super_sidebar_logged_out feature flag is disabled' do + let(:feature_flag) { false } + + specify { expect(subject).to eq false } + end + + context 'when super_sidebar_logged_out feature flag is enabled' do + let(:feature_flag) { true } + + specify { expect(subject).to eq true } + end end - context 'when no user is signed-in' do - specify do - allow(helper).to receive(:current_user).and_return(nil) + context 'without a user' do + context 'with current_user (nil) as a default' do + before do + allow(helper).to receive(:current_user).and_return(nil) + end + + subject { helper.show_super_sidebar? } + + it_behaves_like 'anonymous show_super_sidebar is supposed to' + end + + context 'with nil provided as an argument' do + subject { helper.show_super_sidebar?(nil) } - expect(helper.show_super_sidebar?).to eq false + it_behaves_like 'anonymous show_super_sidebar is supposed to' end end diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index 8d8bbcd2737..1a81fec3a0b 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -91,10 +91,16 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do allow(user).to receive(:pinned_nav_items).and_return({ panel_type => %w[foo bar], 'another_panel' => %w[baz] }) end + # Tests for logged-out sidebar context + it_behaves_like 'logged-out super-sidebar context' + + # Tests for logged-in sidebar context below + it_behaves_like 'shared super sidebar context' + it { is_expected.to include({ is_logged_in: true }) } + it 'returns sidebar values from user', :use_clean_rails_memory_store_caching do expect(subject).to include({ - current_context_header: nil, - current_menu_items: nil, + is_logged_in: true, name: user.name, username: user.username, avatar_url: user.avatar_url, @@ -128,25 +134,10 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do todos_dashboard_path: dashboard_todos_path, projects_path: dashboard_projects_path, groups_path: dashboard_groups_path, - support_path: helper.support_url, - display_whats_new: helper.display_whats_new?, - whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count, - whats_new_version_digest: helper.whats_new_version_digest, - show_version_check: helper.show_version_check?, - gitlab_version: Gitlab.version_info, - gitlab_version_check: helper.gitlab_version_check, gitlab_com_but_not_canary: Gitlab.com_but_not_canary?, gitlab_com_and_canary: Gitlab.com_and_canary?, canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url, - search: { - search_path: search_path, - issues_path: issues_dashboard_path, - mr_path: merge_requests_dashboard_path, - autocomplete_path: search_autocomplete_path, - search_context: helper.header_search_context - }, pinned_items: %w[foo bar], - panel_type: panel_type, update_pins_url: pins_url, shortcut_links: [ { @@ -471,8 +462,11 @@ RSpec.describe SidebarsHelper, feature_category: :navigation do end describe 'when impersonating' do + before do + session[:impersonator_id] = 5 + end + it 'sets is_impersonating to `true`' do - expect(helper).to receive(:session).and_return({ impersonator_id: 1 }) expect(subject[:is_impersonating]).to be(true) end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index dfaba969153..6179c36b0ea 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -81,6 +81,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['ci_max_includes']).to eq(150) expect(json_response['allow_account_deletion']).to eq(true) expect(json_response['gitlab_shell_operation_limit']).to eq(600) + expect(json_response['namespace_aggregation_schedule_lease_duration_in_seconds']).to eq(300) end end @@ -193,7 +194,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu silent_mode_enabled: true, valid_runner_registrars: ['group'], allow_account_deletion: false, - gitlab_shell_operation_limit: 500 + gitlab_shell_operation_limit: 500, + namespace_aggregation_schedule_lease_duration_in_seconds: 400 } expect(response).to have_gitlab_http_status(:ok) @@ -270,6 +272,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['valid_runner_registrars']).to eq(['group']) expect(json_response['allow_account_deletion']).to be(false) expect(json_response['gitlab_shell_operation_limit']).to be(500) + expect(json_response['namespace_aggregation_schedule_lease_duration_in_seconds']).to be(400) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d66784d943..651581ba270 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -295,8 +295,14 @@ RSpec.configure do |config| # Only a few percent of users will be "enrolled" into the new nav with this flag. # Having it enabled globally would make it impossible to test the current nav. + # https://gitlab.com/gitlab-org/gitlab/-/issues/420121 stub_feature_flags(super_sidebar_nav_enrolled: false) + # The anonymous super-sidebar is under heavy development and enabling the flag + # globally leads to a lot of errors. This issue is for fixing all test to work with the + # new nav: https://gitlab.com/gitlab-org/gitlab/-/issues/420119 + stub_feature_flags(super_sidebar_logged_out: false) + # It's disabled in specs because we don't support certain features which # cause spec failures. stub_feature_flags(gitlab_error_tracking: false) diff --git a/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb new file mode 100644 index 00000000000..636b9870bd7 --- /dev/null +++ b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'shared super sidebar context' do + it 'returns sidebar values for logged-in users and logged-out users', :use_clean_rails_memory_store_caching do + expect(subject).to include({ + current_menu_items: nil, + current_context_header: nil, + support_path: helper.support_url, + display_whats_new: helper.display_whats_new?, + whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count, + whats_new_version_digest: helper.whats_new_version_digest, + show_version_check: helper.show_version_check?, + gitlab_version: Gitlab.version_info, + gitlab_version_check: helper.gitlab_version_check, + search: { + search_path: search_path, + issues_path: issues_dashboard_path, + mr_path: merge_requests_dashboard_path, + autocomplete_path: search_autocomplete_path, + search_context: helper.header_search_context + }, + panel_type: panel_type + }) + end +end + +RSpec.shared_examples 'logged-out super-sidebar context' do + subject do + helper.super_sidebar_context(nil, group: nil, project: nil, panel: panel, panel_type: panel_type) + end + + it_behaves_like 'shared super sidebar context' + + it { is_expected.to include({ is_logged_in: false }) } +end |