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 {
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,
......
......@@ -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,
};
},
......
......@@ -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);
}
},
......
......@@ -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',
......
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,
]);
});
});
......
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),
};
......@@ -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 },
......
......@@ -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 },
});
});
......
......@@ -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();
});
});
});
......
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