diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 4ba2bc3a8e3f3b85653c4f148818c3642ba39bf2..d5cab77f26c6dc0f0211b94fa10bc5bd4123b230 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -205,6 +205,19 @@ export default { return convertToSearchQuery(this.filterTokens) || undefined; }, searchTokens() { + let preloadedAuthors = []; + + if (gon.current_user_id) { + preloadedAuthors = [ + { + id: gon.current_user_id, + name: gon.current_user_fullname, + username: gon.current_username, + avatar_url: gon.current_user_avatar_url, + }, + ]; + } + const tokens = [ { type: TOKEN_TYPE_AUTHOR, @@ -215,6 +228,7 @@ export default { unique: true, defaultAuthors: [], fetchAuthors: this.fetchUsers, + preloadedAuthors, }, { type: TOKEN_TYPE_ASSIGNEE, @@ -225,6 +239,7 @@ export default { unique: !this.hasMultipleIssueAssigneesFeature, defaultAuthors: DEFAULT_NONE_ANY, fetchAuthors: this.fetchUsers, + preloadedAuthors, }, { type: TOKEN_TYPE_MILESTONE, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index db8d67d86dc030b90a830e24982ad2c2d75e4738..2e7b3e149b20d32004e28bc071d309d67cb8f9ce 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -32,14 +32,7 @@ export default { return { authors: this.config.initialAuthors || [], defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], - preloadedAuthors: [ - { - id: gon.current_user_id, - name: gon.current_user_fullname, - username: gon.current_username, - avatar_url: gon.current_user_avatar_url, - }, - ], + preloadedAuthors: this.config.preloadedAuthors || [], loading: false, }; }, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index 6bd67a4cdf00f04fa8a34d80c15e4e0bb3ad0eee..fb6b9e4bc0d4a89d5454440737ab75e742f5b8fa 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -83,7 +83,10 @@ export default { return Boolean(this.recentTokenValuesStorageKey); }, recentTokenIds() { - return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name); + return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + }, + preloadedTokenIds() { + return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); }, currentTokenValue() { if (this.fnCurrentTokenValue) { @@ -103,7 +106,9 @@ export default { return this.searchKey ? this.tokenValues : this.tokenValues.filter( - (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]), + (tokenValue) => + !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) && + !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), ); }, }, @@ -125,7 +130,15 @@ export default { }, DEBOUNCE_DELAY); }, handleTokenValueSelected(activeTokenValue) { - if (this.isRecentTokenValuesEnabled && activeTokenValue) { + // Make sure that; + // 1. Recently used values feature is enabled + // 2. User has actually selected a value + // 3. Selected value is not part of preloaded list. + if ( + this.isRecentTokenValuesEnabled && + activeTokenValue && + !this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier]) + ) { setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); } }, diff --git a/ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js b/ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js index 85be18d5ecde112d1b511712f404ed05f328c87e..423d153f4bfd76f2822bdaf2663829052ddefe8f 100644 --- a/ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js +++ b/ee/app/assets/javascripts/roadmap/mixins/filtered_search_mixin.js @@ -43,6 +43,19 @@ export default { }, methods: { getFilteredSearchTokens({ supportsEpic = true } = {}) { + let preloadedAuthors = []; + + if (gon.current_user_id) { + preloadedAuthors = [ + { + id: gon.current_user_id, + name: gon.current_user_fullname, + username: gon.current_username, + avatar_url: gon.current_user_avatar_url, + }, + ]; + } + const tokens = [ { type: 'author_username', @@ -54,6 +67,7 @@ export default { operators: OPERATOR_IS_ONLY, recentTokenValuesStorageKey: `${this.groupFullPath}-epics-recent-tokens-author_username`, fetchAuthors: Api.users.bind(Api), + preloadedAuthors, }, { type: 'label_name', diff --git a/ee/spec/frontend/roadmap/components/roadmap_filters_spec.js b/ee/spec/frontend/roadmap/components/roadmap_filters_spec.js index e187e22c6016f188065914b016e9547b8a6eb25e..f8447a159992a03fac471e781fc73a3442cefaec 100644 --- a/ee/spec/frontend/roadmap/components/roadmap_filters_spec.js +++ b/ee/spec/frontend/roadmap/components/roadmap_filters_spec.js @@ -1,4 +1,4 @@ -import { GlSegmentedControl, GlDropdown, GlDropdownItem, GlFilteredSearchToken } from '@gitlab/ui'; +import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; @@ -6,17 +6,21 @@ import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue'; import { PRESET_TYPES, EPICS_STATES } from 'ee/roadmap/constants'; import createStore from 'ee/roadmap/store'; import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; -import { mockSortedBy, mockTimeframeInitialDate } from 'ee_jest/roadmap/mock_data'; +import { + mockSortedBy, + mockTimeframeInitialDate, + mockAuthorTokenConfig, + mockLabelTokenConfig, + mockMilestoneTokenConfig, + mockConfidentialTokenConfig, + mockEpicTokenConfig, + mockReactionEmojiTokenConfig, +} from 'ee_jest/roadmap/mock_data'; import { TEST_HOST } from 'helpers/test_constants'; import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; + import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; -import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; -import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; -import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; -import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; jest.mock('~/lib/utils/url_utility', () => ({ mergeUrlParams: jest.fn(), @@ -164,66 +168,6 @@ describe('RoadmapFilters', () => { ]; let filteredSearchBar; - const operators = OPERATOR_IS_ONLY; - - const filterTokens = [ - { - type: 'author_username', - icon: 'user', - title: 'Author', - unique: true, - symbol: '@', - token: AuthorToken, - operators, - recentTokenValuesStorageKey: 'gitlab-org-epics-recent-tokens-author_username', - fetchAuthors: expect.any(Function), - }, - { - type: 'label_name', - icon: 'labels', - title: 'Label', - unique: false, - symbol: '~', - token: LabelToken, - operators, - recentTokenValuesStorageKey: 'gitlab-org-epics-recent-tokens-label_name', - fetchLabels: expect.any(Function), - }, - { - type: 'milestone_title', - icon: 'clock', - title: 'Milestone', - unique: true, - symbol: '%', - token: MilestoneToken, - operators, - fetchMilestones: expect.any(Function), - }, - { - type: 'confidential', - icon: 'eye-slash', - title: 'Confidential', - unique: true, - token: GlFilteredSearchToken, - operators, - options: [ - { icon: 'eye-slash', value: true, title: 'Yes' }, - { icon: 'eye', value: false, title: 'No' }, - ], - }, - { - type: 'epic_iid', - icon: 'epic', - title: 'Epic', - unique: true, - symbol: '&', - token: EpicToken, - operators, - defaultEpics: [], - fetchEpics: expect.any(Function), - }, - ]; - beforeEach(() => { filteredSearchBar = wrapper.find(FilteredSearchBar); }); @@ -235,7 +179,13 @@ describe('RoadmapFilters', () => { }); it('includes `Author`, `Milestone`, `Confidential`, `Epic` and `Label` tokens when user is not logged in', () => { - expect(filteredSearchBar.props('tokens')).toEqual(filterTokens); + expect(filteredSearchBar.props('tokens')).toEqual([ + mockAuthorTokenConfig, + mockLabelTokenConfig, + mockMilestoneTokenConfig, + mockConfidentialTokenConfig, + mockEpicTokenConfig, + ]); }); it('includes "Start date" and "Due date" sort options', () => { @@ -308,20 +258,29 @@ describe('RoadmapFilters', () => { describe('when user is logged in', () => { beforeAll(() => { gon.current_user_id = 1; + gon.current_user_fullname = 'Administrator'; + gon.current_username = 'root'; + gon.current_user_avatar_url = 'avatar/url'; }); it('includes `Author`, `Milestone`, `Confidential`, `Label` and `My-Reaction` tokens', () => { expect(filteredSearchBar.props('tokens')).toEqual([ - ...filterTokens, { - type: 'my_reaction_emoji', - icon: 'thumb-up', - title: 'My-Reaction', - unique: true, - token: EmojiToken, - operators, - fetchEmojis: expect.any(Function), + ...mockAuthorTokenConfig, + preloadedAuthors: [ + { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: 'avatar/url', + }, + ], }, + mockLabelTokenConfig, + mockMilestoneTokenConfig, + mockConfidentialTokenConfig, + mockEpicTokenConfig, + mockReactionEmojiTokenConfig, ]); }); }); diff --git a/ee/spec/frontend/roadmap/mock_data.js b/ee/spec/frontend/roadmap/mock_data.js index c79a6a55482f61152070796e099677f175bb942a..0961b7a158d634e65fb34d3704c87d8197e3654e 100644 --- a/ee/spec/frontend/roadmap/mock_data.js +++ b/ee/spec/frontend/roadmap/mock_data.js @@ -1,3 +1,4 @@ +import { GlFilteredSearchToken } from '@gitlab/ui'; import { getTimeframeForWeeksView, getTimeframeForMonthsView, @@ -5,6 +6,13 @@ import { } from 'ee/roadmap/utils/roadmap_utils'; import { dateFromString } from 'helpers/datetime_helpers'; +import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; + +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; +import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; +import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; export const mockScrollBarSize = 15; @@ -758,3 +766,74 @@ export const mockEpicsWithParents = [ }, }, ]; + +export const mockAuthorTokenConfig = { + type: 'author_username', + icon: 'user', + title: 'Author', + unique: true, + symbol: '@', + token: AuthorToken, + operators: OPERATOR_IS_ONLY, + recentTokenValuesStorageKey: 'gitlab-org-epics-recent-tokens-author_username', + fetchAuthors: expect.any(Function), + preloadedAuthors: [], +}; + +export const mockLabelTokenConfig = { + type: 'label_name', + icon: 'labels', + title: 'Label', + unique: false, + symbol: '~', + token: LabelToken, + operators: OPERATOR_IS_ONLY, + recentTokenValuesStorageKey: 'gitlab-org-epics-recent-tokens-label_name', + fetchLabels: expect.any(Function), +}; + +export const mockMilestoneTokenConfig = { + type: 'milestone_title', + icon: 'clock', + title: 'Milestone', + unique: true, + symbol: '%', + token: MilestoneToken, + operators: OPERATOR_IS_ONLY, + fetchMilestones: expect.any(Function), +}; + +export const mockConfidentialTokenConfig = { + type: 'confidential', + icon: 'eye-slash', + title: 'Confidential', + unique: true, + token: GlFilteredSearchToken, + operators: OPERATOR_IS_ONLY, + options: [ + { icon: 'eye-slash', value: true, title: 'Yes' }, + { icon: 'eye', value: false, title: 'No' }, + ], +}; + +export const mockEpicTokenConfig = { + type: 'epic_iid', + icon: 'epic', + title: 'Epic', + unique: true, + symbol: '&', + token: EpicToken, + operators: OPERATOR_IS_ONLY, + defaultEpics: [], + fetchEpics: expect.any(Function), +}; + +export const mockReactionEmojiTokenConfig = { + type: 'my_reaction_emoji', + icon: 'thumb-up', + title: 'My-Reaction', + unique: true, + token: EmojiToken, + operators: OPERATOR_IS_ONLY, + fetchEmojis: expect.any(Function), +}; diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js index 8a91dca51c25c874876a2457c7be6e09cff0275d..d78a436c618b9bc86132f5a36d1d35cb887be346 100644 --- a/spec/frontend/issues_list/components/issues_list_app_spec.js +++ b/spec/frontend/issues_list/components/issues_list_app_spec.js @@ -440,6 +440,13 @@ describe('IssuesListApp component', () => { }); describe('tokens', () => { + const mockCurrentUser = { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: 'avatar/url', + }; + describe('when user is signed out', () => { beforeEach(() => { wrapper = mountComponent({ @@ -451,6 +458,8 @@ describe('IssuesListApp component', () => { it('does not render My-Reaction or Confidential tokens', () => { expect(findIssuableList().props('searchTokens')).not.toMatchObject([ + { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] }, + { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] }, { type: TOKEN_TYPE_MY_REACTION }, { type: TOKEN_TYPE_CONFIDENTIAL }, ]); @@ -506,7 +515,17 @@ describe('IssuesListApp component', () => { }); describe('when all tokens are available', () => { + const originalGon = window.gon; + beforeEach(() => { + window.gon = { + ...originalGon, + current_user_id: mockCurrentUser.id, + current_user_fullname: mockCurrentUser.name, + current_username: mockCurrentUser.username, + current_user_avatar_url: mockCurrentUser.avatar_url, + }; + wrapper = mountComponent({ provide: { isSignedIn: true, @@ -519,8 +538,8 @@ describe('IssuesListApp component', () => { it('renders all tokens', () => { expect(findIssuableList().props('searchTokens')).toMatchObject([ - { type: TOKEN_TYPE_AUTHOR }, - { type: TOKEN_TYPE_ASSIGNEE }, + { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] }, + { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] }, { type: TOKEN_TYPE_MILESTONE }, { type: TOKEN_TYPE_LABEL }, { type: TOKEN_TYPE_MY_REACTION }, diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js index 899f6c78e899442f522eea1384d4cb06eaa7b2fb..f50eafdbc52d13d1119e8e8e1c7d288a8e83bdd5 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js @@ -30,6 +30,15 @@ const defaultStubs = { }, }; +const mockPreloadedAuthors = [ + { + id: 13, + name: 'Administrator', + username: 'root', + avatar_url: 'avatar/url', + }, +]; + function createComponent(options = {}) { const { config = mockAuthorToken, @@ -65,13 +74,6 @@ describe('AuthorToken', () => { const getBaseToken = () => wrapper.findComponent(BaseToken); beforeEach(() => { - window.gon = { - ...originalGon, - current_user_id: 13, - current_user_fullname: 'Administrator', - current_username: 'root', - current_user_avatar_url: 'avatar/url', - }; mock = new MockAdapter(axios); }); @@ -133,6 +135,13 @@ describe('AuthorToken', () => { }); describe('template', () => { + const activateTokenValuesList = async () => { + const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment); + const suggestionsSegment = tokenSegments.at(2); + suggestionsSegment.vm.$emit('activate'); + await wrapper.vm.$nextTick(); + }; + it('renders base-token component', () => { wrapper = createComponent({ value: { data: mockAuthors[0].username }, @@ -206,13 +215,11 @@ describe('AuthorToken', () => { const defaultAuthors = DEFAULT_NONE_ANY; wrapper = createComponent({ active: true, - config: { ...mockAuthorToken, defaultAuthors }, + config: { ...mockAuthorToken, defaultAuthors, preloadedAuthors: mockPreloadedAuthors }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); - const suggestionsSegment = tokenSegments.at(2); - suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + + await activateTokenValuesList(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -239,13 +246,11 @@ describe('AuthorToken', () => { it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => { wrapper = createComponent({ active: true, - config: { ...mockAuthorToken }, + config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors }, stubs: { Portal: true }, }); - const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment); - const suggestionsSegment = tokenSegments.at(2); - suggestionsSegment.vm.$emit('activate'); - await wrapper.vm.$nextTick(); + + await activateTokenValuesList(); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); @@ -257,7 +262,11 @@ describe('AuthorToken', () => { beforeEach(() => { wrapper = createComponent({ active: true, - config: { ...mockAuthorToken, defaultAuthors: [] }, + config: { + ...mockAuthorToken, + preloadedAuthors: mockPreloadedAuthors, + defaultAuthors: [], + }, stubs: { Portal: true }, }); }); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js index 0db47f1f18973ba3c0e1bbb291318f8958d3fa79..602864f4fa5feb465436b1b9a62a3d542122bfb8 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js @@ -175,6 +175,23 @@ describe('BaseToken', () => { expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); }); + + it('does not add token from preloadedTokenValues', async () => { + const mockTokenValue = { + id: 1, + title: 'Foo', + }; + + wrapper.setProps({ + preloadedTokenValues: [mockTokenValue], + }); + + await wrapper.vm.$nextTick(); + + wrapper.vm.handleTokenValueSelected(mockTokenValue); + + expect(setTokenValueToRecentlyUsed).not.toHaveBeenCalled(); + }); }); });