Commit c44465b4 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch...

Merge branch '322755-add-milestone-myreaction-confidential-tokens-to-issues-list-page-refactor' into 'master'

Add Milestone, My-Reaction and Confidential tokens to issues refactor

See merge request gitlab-org/gitlab!60120
parents 79eff458 00033e3c
<script> <script>
import { GlButton, GlEmptyState, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import {
GlButton,
GlEmptyState,
GlFilteredSearchToken,
GlIcon,
GlLink,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { toNumber } from 'lodash'; import { toNumber } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -26,7 +34,9 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -26,7 +34,9 @@ import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.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 LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_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';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue'; import IssueCardTimeInfo from './issue_card_time_info.vue';
...@@ -52,6 +62,9 @@ export default { ...@@ -52,6 +62,9 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: { inject: {
autocompleteAwardEmojisPath: {
default: '',
},
autocompleteUsersPath: { autocompleteUsersPath: {
default: '', default: '',
}, },
...@@ -88,6 +101,9 @@ export default { ...@@ -88,6 +101,9 @@ export default {
projectLabelsPath: { projectLabelsPath: {
default: '', default: '',
}, },
projectMilestonesPath: {
default: '',
},
projectPath: { projectPath: {
default: '', default: '',
}, },
...@@ -155,6 +171,15 @@ export default { ...@@ -155,6 +171,15 @@ export default {
defaultAuthors: [], defaultAuthors: [],
fetchAuthors: this.fetchUsers, fetchAuthors: this.fetchUsers,
}, },
{
type: 'milestone',
title: __('Milestone'),
icon: 'clock',
token: MilestoneToken,
unique: true,
defaultMilestones: [],
fetchMilestones: this.fetchMilestones,
},
{ {
type: 'labels', type: 'labels',
title: __('Label'), title: __('Label'),
...@@ -163,6 +188,28 @@ export default { ...@@ -163,6 +188,28 @@ export default {
defaultLabels: [], defaultLabels: [],
fetchLabels: this.fetchLabels, fetchLabels: this.fetchLabels,
}, },
{
type: 'my_reaction_emoji',
title: __('My-Reaction'),
icon: 'thumb-up',
token: EmojiToken,
unique: true,
operators: [{ value: '=', description: __('is') }],
defaultEmojis: [],
fetchEmojis: this.fetchEmojis,
},
{
type: 'confidential',
title: __('Confidential'),
icon: 'eye-slash',
token: GlFilteredSearchToken,
unique: true,
operators: [{ value: '=', description: __('is') }],
options: [
{ icon: 'eye-slash', value: 'yes', title: __('Yes') },
{ icon: 'eye', value: 'no', title: __('No') },
],
},
]; ];
}, },
showPaginationControls() { showPaginationControls() {
...@@ -187,29 +234,40 @@ export default { ...@@ -187,29 +234,40 @@ export default {
}; };
}, },
}, },
created() {
this.cache = {};
},
mounted() { mounted() {
eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => { eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
this.showBulkEditSidebar = showBulkEditSidebar;
});
this.fetchIssues(); this.fetchIssues();
}, },
beforeDestroy() { beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
eventHub.$off('issuables:toggleBulkEdit');
}, },
methods: { methods: {
fetchLabels(search) { fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
if (this.labelsCache) { if (this.cache[cacheName]) {
return search const data = search
? Promise.resolve(fuzzaldrinPlus.filter(this.labelsCache, search, { key: 'title' })) ? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
: Promise.resolve(this.labelsCache.slice(0, MAX_LIST_SIZE)); : this.cache[cacheName].slice(0, MAX_LIST_SIZE);
return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
} }
return axios.get(this.projectLabelsPath).then(({ data }) => { return axios.get(path).then(({ data }) => {
this.labelsCache = data; this.cache[cacheName] = data;
return data.slice(0, MAX_LIST_SIZE); const result = data.slice(0, MAX_LIST_SIZE);
return wrapData ? { data: result } : result;
}); });
}, },
fetchEmojis(search) {
return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search);
},
fetchLabels(search) {
return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search);
},
fetchMilestones(search) {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
},
fetchUsers(search) { fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } }); return axios.get(this.autocompleteUsersPath, { params: { search } });
}, },
...@@ -310,6 +368,9 @@ export default { ...@@ -310,6 +368,9 @@ export default {
this.sortKey = value; this.sortKey = value;
this.fetchIssues(); this.fetchIssues();
}, },
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
}, },
}; };
</script> </script>
......
...@@ -298,6 +298,16 @@ export const filters = { ...@@ -298,6 +298,16 @@ export const filters = {
[OPERATOR_IS_NOT]: 'not[assignee_username][]', [OPERATOR_IS_NOT]: 'not[assignee_username][]',
}, },
}, },
milestone: {
apiParam: {
[OPERATOR_IS]: 'milestone',
[OPERATOR_IS_NOT]: 'not[milestone]',
},
urlParam: {
[OPERATOR_IS]: 'milestone_title',
[OPERATOR_IS_NOT]: 'not[milestone_title]',
},
},
labels: { labels: {
apiParam: { apiParam: {
[OPERATOR_IS]: 'labels', [OPERATOR_IS]: 'labels',
...@@ -308,4 +318,20 @@ export const filters = { ...@@ -308,4 +318,20 @@ export const filters = {
[OPERATOR_IS_NOT]: 'not[label_name][]', [OPERATOR_IS_NOT]: 'not[label_name][]',
}, },
}, },
my_reaction_emoji: {
apiParam: {
[OPERATOR_IS]: 'my_reaction_emoji',
},
urlParam: {
[OPERATOR_IS]: 'my_reaction_emoji',
},
},
confidential: {
apiParam: {
[OPERATOR_IS]: 'confidential',
},
urlParam: {
[OPERATOR_IS]: 'confidential',
},
},
}; };
...@@ -73,6 +73,7 @@ export function initIssuesListApp() { ...@@ -73,6 +73,7 @@ export function initIssuesListApp() {
} }
const { const {
autocompleteAwardEmojisPath,
autocompleteUsersPath, autocompleteUsersPath,
calendarPath, calendarPath,
canBulkUpdate, canBulkUpdate,
...@@ -94,6 +95,7 @@ export function initIssuesListApp() { ...@@ -94,6 +95,7 @@ export function initIssuesListApp() {
newIssuePath, newIssuePath,
projectImportJiraPath, projectImportJiraPath,
projectLabelsPath, projectLabelsPath,
projectMilestonesPath,
projectPath, projectPath,
rssPath, rssPath,
showNewIssueLink, showNewIssueLink,
...@@ -106,6 +108,7 @@ export function initIssuesListApp() { ...@@ -106,6 +108,7 @@ export function initIssuesListApp() {
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153 // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider: {}, apolloProvider: {},
provide: { provide: {
autocompleteAwardEmojisPath,
autocompleteUsersPath, autocompleteUsersPath,
calendarPath, calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate), canBulkUpdate: parseBoolean(canBulkUpdate),
...@@ -120,6 +123,7 @@ export function initIssuesListApp() { ...@@ -120,6 +123,7 @@ export function initIssuesListApp() {
jiraIntegrationPath, jiraIntegrationPath,
newIssuePath, newIssuePath,
projectLabelsPath, projectLabelsPath,
projectMilestonesPath,
projectPath, projectPath,
rssPath, rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink), showNewIssueLink: parseBoolean(showNewIssueLink),
......
...@@ -47,6 +47,16 @@ export default { ...@@ -47,6 +47,16 @@ export default {
); );
}, },
}, },
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.emojis.length) {
this.fetchEmojiBySearchTerm(this.value.data);
}
},
},
},
methods: { methods: {
fetchEmojiBySearchTerm(searchTerm) { fetchEmojiBySearchTerm(searchTerm) {
this.loading = true; this.loading = true;
......
...@@ -166,6 +166,7 @@ module IssuesHelper ...@@ -166,6 +166,7 @@ module IssuesHelper
def issues_list_data(project, current_user, finder) def issues_list_data(project, current_user, finder)
{ {
autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json), autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json),
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)), calendar_path: url_for(safe_params.merge(calendar_url_options)),
can_bulk_update: can?(current_user, :admin_issue, project).to_s, can_bulk_update: can?(current_user, :admin_issue, project).to_s,
can_edit: can?(current_user, :admin_project, project).to_s, can_edit: can?(current_user, :admin_project, project).to_s,
...@@ -183,6 +184,7 @@ module IssuesHelper ...@@ -183,6 +184,7 @@ module IssuesHelper
new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }), new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }),
project_import_jira_path: project_import_jira_path(project), project_import_jira_path: project_import_jira_path(project),
project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json), project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
project_milestones_path: project_milestones_path(project, format: :json),
project_path: project.full_path, project_path: project.full_path,
rss_path: url_for(safe_params.merge(rss_url_options)), rss_path: url_for(safe_params.merge(rss_url_options)),
show_new_issue_link: show_new_issue_link?(project).to_s, show_new_issue_link: show_new_issue_link?(project).to_s,
......
...@@ -3,7 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -3,7 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { filteredTokens, locationSearch } from 'jest/issues_list/mock_data'; import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
import createFlash from '~/flash'; import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
...@@ -558,25 +558,11 @@ describe('IssuesListApp component', () => { ...@@ -558,25 +558,11 @@ describe('IssuesListApp component', () => {
}); });
it('makes an API call to search for issues with the search term', () => { it('makes an API call to search for issues with the search term', () => {
expect(axiosMock.history.get[1].params).toMatchObject({ expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: 'bart',
'not[assignee_username]': 'lisa',
labels: 'cartoon,tv',
'not[labels]': 'live action,drama',
});
}); });
it('updates IssuableList with url params', () => { it('updates IssuableList with url params', () => {
expect(findIssuableList().props('urlParams')).toMatchObject({ expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
author_username: ['homer'],
'not[author_username]': ['marge'],
'assignee_username[]': ['bart'],
'not[assignee_username][]': ['lisa'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
});
}); });
}); });
}); });
......
...@@ -6,10 +6,14 @@ export const locationSearch = [ ...@@ -6,10 +6,14 @@ export const locationSearch = [
'not[author_username]=marge', 'not[author_username]=marge',
'assignee_username[]=bart', 'assignee_username[]=bart',
'not[assignee_username][]=lisa', 'not[assignee_username][]=lisa',
'milestone_title=season+4',
'not[milestone_title]=season+20',
'label_name[]=cartoon', 'label_name[]=cartoon',
'label_name[]=tv', 'label_name[]=tv',
'not[label_name][]=live action', 'not[label_name][]=live action',
'not[label_name][]=drama', 'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
].join('&'); ].join('&');
export const filteredTokens = [ export const filteredTokens = [
...@@ -17,10 +21,40 @@ export const filteredTokens = [ ...@@ -17,10 +21,40 @@ export const filteredTokens = [
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } }, { type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } }, { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS_NOT } }, { type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS_NOT } },
{ type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } },
{ type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } }, { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } }, { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } }, { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } },
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } }, { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
{ type: 'filtered-search-term', value: { data: 'find' } }, { type: 'filtered-search-term', value: { data: 'find' } },
{ type: 'filtered-search-term', value: { data: 'issues' } }, { type: 'filtered-search-term', value: { data: 'issues' } },
]; ];
export const apiParams = {
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: 'bart',
'not[assignee_username]': 'lisa',
milestone: 'season 4',
'not[milestone]': 'season 20',
labels: 'cartoon,tv',
'not[labels]': 'live action,drama',
my_reaction_emoji: 'thumbsup',
confidential: 'no',
};
export const urlParams = {
author_username: ['homer'],
'not[author_username]': ['marge'],
'assignee_username[]': ['bart'],
'not[assignee_username][]': ['lisa'],
milestone_title: ['season 4'],
'not[milestone_title]': ['season 20'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: ['thumbsup'],
confidential: ['no'],
};
import { filteredTokens, locationSearch } from 'jest/issues_list/mock_data'; import { apiParams, filteredTokens, locationSearch, urlParams } from 'jest/issues_list/mock_data';
import { sortParams } from '~/issues_list/constants'; import { sortParams } from '~/issues_list/constants';
import { import {
convertToApiParams, convertToApiParams,
...@@ -23,27 +23,13 @@ describe('getFilterTokens', () => { ...@@ -23,27 +23,13 @@ describe('getFilterTokens', () => {
describe('convertToApiParams', () => { describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => { it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual({ expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: 'bart',
'not[assignee_username]': 'lisa',
labels: 'cartoon,tv',
'not[labels]': 'live action,drama',
});
}); });
}); });
describe('convertToUrlParams', () => { describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => { it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual({ expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
author_username: ['homer'],
'not[author_username]': ['marge'],
'assignee_username[]': ['bart'],
'not[assignee_username][]': ['lisa'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
});
}); });
}); });
......
...@@ -293,6 +293,7 @@ RSpec.describe IssuesHelper do ...@@ -293,6 +293,7 @@ RSpec.describe IssuesHelper do
allow(helper).to receive(:url_for).and_return('#') allow(helper).to receive(:url_for).and_return('#')
expected = { expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json), autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json),
calendar_path: '#', calendar_path: '#',
can_bulk_update: 'true', can_bulk_update: 'true',
...@@ -311,6 +312,7 @@ RSpec.describe IssuesHelper do ...@@ -311,6 +312,7 @@ RSpec.describe IssuesHelper do
new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.id, milestone_id: finder.milestones.first.id }), new_issue_path: new_project_issue_path(project, issue: { assignee_id: finder.assignee.id, milestone_id: finder.milestones.first.id }),
project_import_jira_path: project_import_jira_path(project), project_import_jira_path: project_import_jira_path(project),
project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json), project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
project_milestones_path: project_milestones_path(project, format: :json),
project_path: project.full_path, project_path: project.full_path,
rss_path: '#', rss_path: '#',
show_new_issue_link: 'true', show_new_issue_link: 'true',
......
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