Commit dc963512 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch...

Merge branch '238314-search-ui-implement-issue-scope-results-filter-by-confidentiality' into 'master'

Search UI - Issue scope results filter by confidentiality

See merge request gitlab-org/gitlab!40793
parents 045489f6 034140b3
import Search from './search'; import Search from './search';
import initStateFilter from '~/search/state_filter'; import initStateFilter from '~/search/state_filter';
import initConfidentialFilter from '~/search/confidential_filter';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initStateFilter(); initStateFilter();
initConfidentialFilter();
return new Search(); return new Search();
}); });
<script> <script>
import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
import {
FILTER_STATES,
SCOPES,
FILTER_STATES_BY_SCOPE,
FILTER_HEADER,
FILTER_TEXT,
} from '../constants';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
const FILTERS_ARRAY = Object.values(FILTER_STATES);
export default { export default {
name: 'StateFilter', name: 'DropdownFilter',
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
}, },
props: { props: {
scope: { initialFilter: {
type: String, type: String,
required: false,
default: null,
},
filters: {
type: Object,
required: true, required: true,
}, },
state: { filtersArray: {
type: Array,
required: true,
},
header: {
type: String, type: String,
required: false, required: true,
default: FILTER_STATES.ANY.value, },
validator: v => FILTERS_ARRAY.some(({ value }) => value === v), param: {
type: String,
required: true,
},
scope: {
type: String,
required: true,
},
supportedScopes: {
type: Array,
required: true,
}, },
}, },
computed: { computed: {
filter() {
return this.initialFilter || this.filters.ANY.value;
},
selectedFilterText() { selectedFilterText() {
const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter); const f = this.filtersArray.find(({ value }) => value === this.selectedFilter);
if (!filter || filter === FILTER_STATES.ANY) { if (!f || f === this.filters.ANY) {
return FILTER_TEXT; return sprintf(s__('Any %{header}'), { header: this.header });
} }
return filter.label; return f.label;
}, },
showDropdown() { showDropdown() {
return Object.values(SCOPES).includes(this.scope); return this.supportedScopes.includes(this.scope);
}, },
selectedFilter: { selectedFilter: {
get() { get() {
if (FILTERS_ARRAY.some(({ value }) => value === this.state)) { if (this.filtersArray.some(({ value }) => value === this.filter)) {
return this.state; return this.filter;
} }
return FILTER_STATES.ANY.value; return this.filters.ANY.value;
}, },
set(state) { set(filter) {
visitUrl(setUrlParams({ state })); visitUrl(setUrlParams({ [this.param]: filter }));
}, },
}, },
}, },
...@@ -59,36 +73,39 @@ export default { ...@@ -59,36 +73,39 @@ export default {
dropDownItemClass(filter) { dropDownItemClass(filter) {
return { return {
'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2': 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2':
filter === FILTER_STATES.ANY, filter === this.filters.ANY,
}; };
}, },
isFilterSelected(filter) { isFilterSelected(filter) {
return filter === this.selectedFilter; return filter === this.selectedFilter;
}, },
handleFilterChange(state) { handleFilterChange(filter) {
this.selectedFilter = state; this.selectedFilter = filter;
}, },
}, },
filterStates: FILTER_STATES,
filterHeader: FILTER_HEADER,
filtersByScope: FILTER_STATES_BY_SCOPE,
}; };
</script> </script>
<template> <template>
<gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0"> <gl-dropdown
v-if="showDropdown"
:text="selectedFilterText"
class="col-3 gl-pt-4 gl-pl-0 gl-pr-0 gl-mr-4"
menu-class="gl-w-full! gl-pl-0"
>
<header class="gl-text-center gl-font-weight-bold gl-font-lg"> <header class="gl-text-center gl-font-weight-bold gl-font-lg">
{{ $options.filterHeader }} {{ header }}
</header> </header>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-dropdown-item <gl-dropdown-item
v-for="filter in $options.filtersByScope[scope]" v-for="f in filtersArray"
:key="filter.value" :key="f.value"
:is-check-item="true" :is-check-item="true"
:is-checked="isFilterSelected(filter.value)" :is-checked="isFilterSelected(f.value)"
:class="dropDownItemClass(filter)" :class="dropDownItemClass(f)"
@click="handleFilterChange(filter.value)" @click="handleFilterChange(f.value)"
>{{ filter.label }}</gl-dropdown-item
> >
{{ f.label }}
</gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</template> </template>
import { __ } from '~/locale';
export const FILTER_HEADER = __('Confidentiality');
export const FILTER_STATES = {
ANY: {
label: __('Any'),
value: null,
},
CONFIDENTIAL: {
label: __('Confidential'),
value: 'yes',
},
NOT_CONFIDENTIAL: {
label: __('Not confidential'),
value: 'no',
},
};
export const SCOPES = {
ISSUES: 'issues',
};
export const FILTER_STATES_BY_SCOPE = {
[SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.CONFIDENTIAL, FILTER_STATES.NOT_CONFIDENTIAL],
};
export const FILTER_PARAM = 'confidential';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import DropdownFilter from '../components/dropdown_filter.vue';
import {
FILTER_HEADER,
FILTER_PARAM,
FILTER_STATES_BY_SCOPE,
FILTER_STATES,
SCOPES,
} from './constants';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-search-filter-by-confidential');
if (!el) return false;
return new Vue({
el,
data() {
return { ...el.dataset };
},
render(createElement) {
return createElement(DropdownFilter, {
props: {
initialFilter: this.filter,
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
filters: FILTER_STATES,
header: FILTER_HEADER,
param: FILTER_PARAM,
scope: this.scope,
supportedScopes: Object.values(SCOPES),
},
});
},
});
};
...@@ -2,8 +2,6 @@ import { __ } from '~/locale'; ...@@ -2,8 +2,6 @@ import { __ } from '~/locale';
export const FILTER_HEADER = __('Status'); export const FILTER_HEADER = __('Status');
export const FILTER_TEXT = __('Any Status');
export const FILTER_STATES = { export const FILTER_STATES = {
ANY: { ANY: {
label: __('Any'), label: __('Any'),
...@@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = { ...@@ -37,3 +35,5 @@ export const FILTER_STATES_BY_SCOPE = {
FILTER_STATES.CLOSED, FILTER_STATES.CLOSED,
], ],
}; };
export const FILTER_PARAM = 'state';
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import StateFilter from './components/state_filter.vue'; import DropdownFilter from '../components/dropdown_filter.vue';
import {
FILTER_HEADER,
FILTER_PARAM,
FILTER_STATES_BY_SCOPE,
FILTER_STATES,
SCOPES,
} from './constants';
Vue.use(Translate); Vue.use(Translate);
...@@ -11,22 +18,20 @@ export default () => { ...@@ -11,22 +18,20 @@ export default () => {
return new Vue({ return new Vue({
el, el,
components: {
StateFilter,
},
data() { data() {
const { dataset } = this.$options.el; return { ...el.dataset };
return {
scope: dataset.scope,
state: dataset.state,
};
}, },
render(createElement) { render(createElement) {
return createElement('state-filter', { return createElement(DropdownFilter, {
props: { props: {
initialFilter: this.filter,
filtersArray: FILTER_STATES_BY_SCOPE[this.scope],
filters: FILTER_STATES,
header: FILTER_HEADER,
param: FILTER_PARAM,
scope: this.scope, scope: this.scope,
state: this.state, supportedScopes: Object.values(SCOPES),
}, },
}); });
}, },
......
- if @search_objects.to_a.empty? - if @search_objects.to_a.empty?
= render partial: "search/results/filters"
= render partial: "search/results/empty" = render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render_if_exists 'search/form_revert_to_basic' = render_if_exists 'search/form_revert_to_basic'
...@@ -21,8 +22,7 @@ ...@@ -21,8 +22,7 @@
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1') - link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search' = render_if_exists 'shared/promotions/promote_advanced_search'
= render partial: "search/results/filters"
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, state: params[:state] } }
.results.gl-mt-3 .results.gl-mt-3
- if @scope == 'commits' - if @scope == 'commits'
......
.d-lg-flex.align-items-end
#js-search-filter-by-state{ 'v-cloak': true, data: { scope: @scope, filter: params[:state]} }
- if Feature.enabled?(:search_filter_by_confidential, @group)
#js-search-filter-by-confidential{ 'v-cloak': true, data: { scope: @scope, filter: params[:confidential] } }
- if %w(issues merge_requests).include?(@scope)
%hr.gl-mt-4.gl-mb-4
---
name: search_filter_by_confidential
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40793
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/244923
group: group::global search
type: development
default_enabled: false
\ No newline at end of file
...@@ -3037,10 +3037,10 @@ msgstr "" ...@@ -3037,10 +3037,10 @@ msgstr ""
msgid "Any" msgid "Any"
msgstr "" msgstr ""
msgid "Any Author" msgid "Any %{header}"
msgstr "" msgstr ""
msgid "Any Status" msgid "Any Author"
msgstr "" msgstr ""
msgid "Any branch" msgid "Any branch"
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import StateFilter from '~/search/state_filter/components/state_filter.vue'; import DropdownFilter from '~/search/components/dropdown_filter.vue';
import { import {
FILTER_STATES, FILTER_STATES,
SCOPES,
FILTER_STATES_BY_SCOPE, FILTER_STATES_BY_SCOPE,
FILTER_TEXT, FILTER_HEADER,
SCOPES,
} from '~/search/state_filter/constants'; } from '~/search/state_filter/constants';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
...@@ -15,14 +15,19 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -15,14 +15,19 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
function createComponent(props = { scope: 'issues' }) { function createComponent(props = { scope: 'issues' }) {
return shallowMount(StateFilter, { return shallowMount(DropdownFilter, {
propsData: { propsData: {
filtersArray: FILTER_STATES_BY_SCOPE.issues,
filters: FILTER_STATES,
header: FILTER_HEADER,
param: 'state',
supportedScopes: Object.values(SCOPES),
...props, ...props,
}, },
}); });
} }
describe('StateFilter', () => { describe('DropdownFilter', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
...@@ -41,7 +46,7 @@ describe('StateFilter', () => { ...@@ -41,7 +46,7 @@ describe('StateFilter', () => {
describe('template', () => { describe('template', () => {
describe.each` describe.each`
scope | showStateDropdown scope | showDropdown
${'issues'} | ${true} ${'issues'} | ${true}
${'merge_requests'} | ${true} ${'merge_requests'} | ${true}
${'projects'} | ${false} ${'projects'} | ${false}
...@@ -50,26 +55,25 @@ describe('StateFilter', () => { ...@@ -50,26 +55,25 @@ describe('StateFilter', () => {
${'notes'} | ${false} ${'notes'} | ${false}
${'wiki_blobs'} | ${false} ${'wiki_blobs'} | ${false}
${'blobs'} | ${false} ${'blobs'} | ${false}
`(`state dropdown`, ({ scope, showStateDropdown }) => { `(`dropdown`, ({ scope, showDropdown }) => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ scope }); wrapper = createComponent({ scope });
}); });
it(`does${showStateDropdown ? '' : ' not'} render when scope is ${scope}`, () => { it(`does${showDropdown ? '' : ' not'} render when scope is ${scope}`, () => {
expect(findGlDropdown().exists()).toBe(showStateDropdown); expect(findGlDropdown().exists()).toBe(showDropdown);
}); });
}); });
describe.each` describe.each`
state | label initialFilter | label
${FILTER_STATES.ANY.value} | ${FILTER_TEXT} ${FILTER_STATES.ANY.value} | ${`Any ${FILTER_HEADER}`}
${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label} ${FILTER_STATES.OPEN.value} | ${FILTER_STATES.OPEN.label}
${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label} ${FILTER_STATES.CLOSED.value} | ${FILTER_STATES.CLOSED.label}
${FILTER_STATES.MERGED.value} | ${FILTER_STATES.MERGED.label} `(`filter text`, ({ initialFilter, label }) => {
`(`filter text`, ({ state, label }) => { describe(`when initialFilter is ${initialFilter}`, () => {
describe(`when state is ${state}`, () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ scope: 'issues', state }); wrapper = createComponent({ scope: 'issues', initialFilter });
}); });
it(`sets dropdown label to ${label}`, () => { it(`sets dropdown label to ${label}`, () => {
......
...@@ -60,6 +60,28 @@ RSpec.describe 'search/_results' do ...@@ -60,6 +60,28 @@ RSpec.describe 'search/_results' do
expect(rendered).to have_selector('#js-search-filter-by-state') expect(rendered).to have_selector('#js-search-filter-by-state')
end end
context 'Feature search_filter_by_confidential' do
context 'when disabled' do
before do
stub_feature_flags(search_filter_by_confidential: false)
end
it 'does not render the confidential drop down' do
render
expect(rendered).not_to have_selector('#js-search-filter-by-confidential')
end
end
context 'when enabled' do
it 'renders the confidential drop down' do
render
expect(rendered).to have_selector('#js-search-filter-by-confidential')
end
end
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment