Commit 1e9a2ab7 authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch 'kp-prevent-hard-coding-current-user-author-token' into 'master'

Provide preloaded authors externally in AuthorToken

See merge request gitlab-org/gitlab!63760
parents 6c385b20 aa0119fb
...@@ -205,6 +205,19 @@ export default { ...@@ -205,6 +205,19 @@ export default {
return convertToSearchQuery(this.filterTokens) || undefined; return convertToSearchQuery(this.filterTokens) || undefined;
}, },
searchTokens() { 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 = [ const tokens = [
{ {
type: TOKEN_TYPE_AUTHOR, type: TOKEN_TYPE_AUTHOR,
...@@ -215,6 +228,7 @@ export default { ...@@ -215,6 +228,7 @@ export default {
unique: true, unique: true,
defaultAuthors: [], defaultAuthors: [],
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
preloadedAuthors,
}, },
{ {
type: TOKEN_TYPE_ASSIGNEE, type: TOKEN_TYPE_ASSIGNEE,
...@@ -225,6 +239,7 @@ export default { ...@@ -225,6 +239,7 @@ export default {
unique: !this.hasMultipleIssueAssigneesFeature, unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY, defaultAuthors: DEFAULT_NONE_ANY,
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
preloadedAuthors,
}, },
{ {
type: TOKEN_TYPE_MILESTONE, type: TOKEN_TYPE_MILESTONE,
......
...@@ -32,14 +32,7 @@ export default { ...@@ -32,14 +32,7 @@ export default {
return { return {
authors: this.config.initialAuthors || [], authors: this.config.initialAuthors || [],
defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY],
preloadedAuthors: [ preloadedAuthors: this.config.preloadedAuthors || [],
{
id: gon.current_user_id,
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
},
],
loading: false, loading: false,
}; };
}, },
......
...@@ -83,7 +83,10 @@ export default { ...@@ -83,7 +83,10 @@ export default {
return Boolean(this.recentTokenValuesStorageKey); return Boolean(this.recentTokenValuesStorageKey);
}, },
recentTokenIds() { 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() { currentTokenValue() {
if (this.fnCurrentTokenValue) { if (this.fnCurrentTokenValue) {
...@@ -103,7 +106,9 @@ export default { ...@@ -103,7 +106,9 @@ export default {
return this.searchKey return this.searchKey
? this.tokenValues ? this.tokenValues
: this.tokenValues.filter( : 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 { ...@@ -125,7 +130,15 @@ export default {
}, DEBOUNCE_DELAY); }, DEBOUNCE_DELAY);
}, },
handleTokenValueSelected(activeTokenValue) { 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); setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
} }
}, },
......
...@@ -43,6 +43,19 @@ export default { ...@@ -43,6 +43,19 @@ export default {
}, },
methods: { methods: {
getFilteredSearchTokens({ supportsEpic = true } = {}) { 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 = [ const tokens = [
{ {
type: 'author_username', type: 'author_username',
...@@ -54,6 +67,7 @@ export default { ...@@ -54,6 +67,7 @@ export default {
operators: OPERATOR_IS_ONLY, operators: OPERATOR_IS_ONLY,
recentTokenValuesStorageKey: `${this.groupFullPath}-epics-recent-tokens-author_username`, recentTokenValuesStorageKey: `${this.groupFullPath}-epics-recent-tokens-author_username`,
fetchAuthors: Api.users.bind(Api), fetchAuthors: Api.users.bind(Api),
preloadedAuthors,
}, },
{ {
type: 'label_name', type: 'label_name',
......
import { GlSegmentedControl, GlDropdown, GlDropdownItem, GlFilteredSearchToken } from '@gitlab/ui'; import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -6,17 +6,21 @@ import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue'; ...@@ -6,17 +6,21 @@ import RoadmapFilters from 'ee/roadmap/components/roadmap_filters.vue';
import { PRESET_TYPES, EPICS_STATES } from 'ee/roadmap/constants'; import { PRESET_TYPES, EPICS_STATES } from 'ee/roadmap/constants';
import createStore from 'ee/roadmap/store'; import createStore from 'ee/roadmap/store';
import { getTimeframeForMonthsView } from 'ee/roadmap/utils/roadmap_utils'; 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 { TEST_HOST } from 'helpers/test_constants';
import { visitUrl, mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; 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 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', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.fn(), mergeUrlParams: jest.fn(),
...@@ -164,66 +168,6 @@ describe('RoadmapFilters', () => { ...@@ -164,66 +168,6 @@ describe('RoadmapFilters', () => {
]; ];
let filteredSearchBar; 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(() => { beforeEach(() => {
filteredSearchBar = wrapper.find(FilteredSearchBar); filteredSearchBar = wrapper.find(FilteredSearchBar);
}); });
...@@ -235,7 +179,13 @@ describe('RoadmapFilters', () => { ...@@ -235,7 +179,13 @@ describe('RoadmapFilters', () => {
}); });
it('includes `Author`, `Milestone`, `Confidential`, `Epic` and `Label` tokens when user is not logged in', () => { 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', () => { it('includes "Start date" and "Due date" sort options', () => {
...@@ -308,20 +258,29 @@ describe('RoadmapFilters', () => { ...@@ -308,20 +258,29 @@ describe('RoadmapFilters', () => {
describe('when user is logged in', () => { describe('when user is logged in', () => {
beforeAll(() => { beforeAll(() => {
gon.current_user_id = 1; 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', () => { it('includes `Author`, `Milestone`, `Confidential`, `Label` and `My-Reaction` tokens', () => {
expect(filteredSearchBar.props('tokens')).toEqual([ expect(filteredSearchBar.props('tokens')).toEqual([
...filterTokens,
{ {
type: 'my_reaction_emoji', ...mockAuthorTokenConfig,
icon: 'thumb-up', preloadedAuthors: [
title: 'My-Reaction', {
unique: true, id: 1,
token: EmojiToken, name: 'Administrator',
operators, username: 'root',
fetchEmojis: expect.any(Function), avatar_url: 'avatar/url',
},
],
}, },
mockLabelTokenConfig,
mockMilestoneTokenConfig,
mockConfidentialTokenConfig,
mockEpicTokenConfig,
mockReactionEmojiTokenConfig,
]); ]);
}); });
}); });
......
import { GlFilteredSearchToken } from '@gitlab/ui';
import { import {
getTimeframeForWeeksView, getTimeframeForWeeksView,
getTimeframeForMonthsView, getTimeframeForMonthsView,
...@@ -5,6 +6,13 @@ import { ...@@ -5,6 +6,13 @@ import {
} from 'ee/roadmap/utils/roadmap_utils'; } from 'ee/roadmap/utils/roadmap_utils';
import { dateFromString } from 'helpers/datetime_helpers'; 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; export const mockScrollBarSize = 15;
...@@ -758,3 +766,74 @@ export const mockEpicsWithParents = [ ...@@ -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),
};
...@@ -440,6 +440,13 @@ describe('IssuesListApp component', () => { ...@@ -440,6 +440,13 @@ describe('IssuesListApp component', () => {
}); });
describe('tokens', () => { describe('tokens', () => {
const mockCurrentUser = {
id: 1,
name: 'Administrator',
username: 'root',
avatar_url: 'avatar/url',
};
describe('when user is signed out', () => { describe('when user is signed out', () => {
beforeEach(() => { beforeEach(() => {
wrapper = mountComponent({ wrapper = mountComponent({
...@@ -451,6 +458,8 @@ describe('IssuesListApp component', () => { ...@@ -451,6 +458,8 @@ describe('IssuesListApp component', () => {
it('does not render My-Reaction or Confidential tokens', () => { it('does not render My-Reaction or Confidential tokens', () => {
expect(findIssuableList().props('searchTokens')).not.toMatchObject([ 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_MY_REACTION },
{ type: TOKEN_TYPE_CONFIDENTIAL }, { type: TOKEN_TYPE_CONFIDENTIAL },
]); ]);
...@@ -506,7 +515,17 @@ describe('IssuesListApp component', () => { ...@@ -506,7 +515,17 @@ describe('IssuesListApp component', () => {
}); });
describe('when all tokens are available', () => { describe('when all tokens are available', () => {
const originalGon = window.gon;
beforeEach(() => { 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({ wrapper = mountComponent({
provide: { provide: {
isSignedIn: true, isSignedIn: true,
...@@ -519,8 +538,8 @@ describe('IssuesListApp component', () => { ...@@ -519,8 +538,8 @@ describe('IssuesListApp component', () => {
it('renders all tokens', () => { it('renders all tokens', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([ expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_AUTHOR }, { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
{ type: TOKEN_TYPE_ASSIGNEE }, { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
{ type: TOKEN_TYPE_MILESTONE }, { type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_LABEL }, { type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MY_REACTION }, { type: TOKEN_TYPE_MY_REACTION },
......
...@@ -30,6 +30,15 @@ const defaultStubs = { ...@@ -30,6 +30,15 @@ const defaultStubs = {
}, },
}; };
const mockPreloadedAuthors = [
{
id: 13,
name: 'Administrator',
username: 'root',
avatar_url: 'avatar/url',
},
];
function createComponent(options = {}) { function createComponent(options = {}) {
const { const {
config = mockAuthorToken, config = mockAuthorToken,
...@@ -65,13 +74,6 @@ describe('AuthorToken', () => { ...@@ -65,13 +74,6 @@ describe('AuthorToken', () => {
const getBaseToken = () => wrapper.findComponent(BaseToken); const getBaseToken = () => wrapper.findComponent(BaseToken);
beforeEach(() => { 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); mock = new MockAdapter(axios);
}); });
...@@ -133,6 +135,13 @@ describe('AuthorToken', () => { ...@@ -133,6 +135,13 @@ describe('AuthorToken', () => {
}); });
describe('template', () => { 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', () => { it('renders base-token component', () => {
wrapper = createComponent({ wrapper = createComponent({
value: { data: mockAuthors[0].username }, value: { data: mockAuthors[0].username },
...@@ -206,13 +215,11 @@ describe('AuthorToken', () => { ...@@ -206,13 +215,11 @@ describe('AuthorToken', () => {
const defaultAuthors = DEFAULT_NONE_ANY; const defaultAuthors = DEFAULT_NONE_ANY;
wrapper = createComponent({ wrapper = createComponent({
active: true, active: true,
config: { ...mockAuthorToken, defaultAuthors }, config: { ...mockAuthorToken, defaultAuthors, preloadedAuthors: mockPreloadedAuthors },
stubs: { Portal: true }, stubs: { Portal: true },
}); });
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2); await activateTokenValuesList();
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
...@@ -239,13 +246,11 @@ describe('AuthorToken', () => { ...@@ -239,13 +246,11 @@ describe('AuthorToken', () => {
it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => { it('renders `DEFAULT_LABEL_ANY` as default suggestions', async () => {
wrapper = createComponent({ wrapper = createComponent({
active: true, active: true,
config: { ...mockAuthorToken }, config: { ...mockAuthorToken, preloadedAuthors: mockPreloadedAuthors },
stubs: { Portal: true }, stubs: { Portal: true },
}); });
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2); await activateTokenValuesList();
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion); const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
...@@ -257,7 +262,11 @@ describe('AuthorToken', () => { ...@@ -257,7 +262,11 @@ describe('AuthorToken', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
active: true, active: true,
config: { ...mockAuthorToken, defaultAuthors: [] }, config: {
...mockAuthorToken,
preloadedAuthors: mockPreloadedAuthors,
defaultAuthors: [],
},
stubs: { Portal: true }, stubs: { Portal: true },
}); });
}); });
......
...@@ -175,6 +175,23 @@ describe('BaseToken', () => { ...@@ -175,6 +175,23 @@ describe('BaseToken', () => {
expect(setTokenValueToRecentlyUsed).toHaveBeenCalledWith(mockStorageKey, mockTokenValue); 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();
});
}); });
}); });
......
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