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>
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 { toNumber } from 'lodash';
import createFlash from '~/flash';
......@@ -26,7 +34,9 @@ import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
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 MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
......@@ -52,6 +62,9 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: {
autocompleteAwardEmojisPath: {
default: '',
},
autocompleteUsersPath: {
default: '',
},
......@@ -88,6 +101,9 @@ export default {
projectLabelsPath: {
default: '',
},
projectMilestonesPath: {
default: '',
},
projectPath: {
default: '',
},
......@@ -155,6 +171,15 @@ export default {
defaultAuthors: [],
fetchAuthors: this.fetchUsers,
},
{
type: 'milestone',
title: __('Milestone'),
icon: 'clock',
token: MilestoneToken,
unique: true,
defaultMilestones: [],
fetchMilestones: this.fetchMilestones,
},
{
type: 'labels',
title: __('Label'),
......@@ -163,6 +188,28 @@ export default {
defaultLabels: [],
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() {
......@@ -187,29 +234,40 @@ export default {
};
},
},
created() {
this.cache = {};
},
mounted() {
eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
this.showBulkEditSidebar = showBulkEditSidebar;
});
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
this.fetchIssues();
},
beforeDestroy() {
// eslint-disable-next-line @gitlab/no-global-event-off
eventHub.$off('issuables:toggleBulkEdit');
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
},
methods: {
fetchLabels(search) {
if (this.labelsCache) {
return search
? Promise.resolve(fuzzaldrinPlus.filter(this.labelsCache, search, { key: 'title' }))
: Promise.resolve(this.labelsCache.slice(0, MAX_LIST_SIZE));
fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
if (this.cache[cacheName]) {
const data = search
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
}
return axios.get(this.projectLabelsPath).then(({ data }) => {
this.labelsCache = data;
return data.slice(0, MAX_LIST_SIZE);
return axios.get(path).then(({ data }) => {
this.cache[cacheName] = data;
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) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
},
......@@ -310,6 +368,9 @@ export default {
this.sortKey = value;
this.fetchIssues();
},
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
},
};
</script>
......
......@@ -298,6 +298,16 @@ export const filters = {
[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: {
apiParam: {
[OPERATOR_IS]: 'labels',
......@@ -308,4 +318,20 @@ export const filters = {
[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() {
}
const {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate,
......@@ -94,6 +95,7 @@ export function initIssuesListApp() {
newIssuePath,
projectImportJiraPath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
rssPath,
showNewIssueLink,
......@@ -106,6 +108,7 @@ export function initIssuesListApp() {
// issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
apolloProvider: {},
provide: {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
......@@ -120,6 +123,7 @@ export function initIssuesListApp() {
jiraIntegrationPath,
newIssuePath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
......
......@@ -47,6 +47,16 @@ export default {
);
},
},
watch: {
active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.emojis.length) {
this.fetchEmojiBySearchTerm(this.value.data);
}
},
},
},
methods: {
fetchEmojiBySearchTerm(searchTerm) {
this.loading = true;
......
......@@ -166,6 +166,7 @@ module IssuesHelper
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_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
can_bulk_update: can?(current_user, :admin_issue, project).to_s,
can_edit: can?(current_user, :admin_project, project).to_s,
......@@ -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) }),
project_import_jira_path: project_import_jira_path(project),
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,
rss_path: url_for(safe_params.merge(rss_url_options)),
show_new_issue_link: show_new_issue_link?(project).to_s,
......
......@@ -3,7 +3,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
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 CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
......@@ -558,25 +558,11 @@ describe('IssuesListApp component', () => {
});
it('makes an API call to search for issues with the search term', () => {
expect(axiosMock.history.get[1].params).toMatchObject({
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: 'bart',
'not[assignee_username]': 'lisa',
labels: 'cartoon,tv',
'not[labels]': 'live action,drama',
});
expect(axiosMock.history.get[1].params).toMatchObject(apiParams);
});
it('updates IssuableList with url params', () => {
expect(findIssuableList().props('urlParams')).toMatchObject({
author_username: ['homer'],
'not[author_username]': ['marge'],
'assignee_username[]': ['bart'],
'not[assignee_username][]': ['lisa'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
});
expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
});
});
});
......
......@@ -6,10 +6,14 @@ export const locationSearch = [
'not[author_username]=marge',
'assignee_username[]=bart',
'not[assignee_username][]=lisa',
'milestone_title=season+4',
'not[milestone_title]=season+20',
'label_name[]=cartoon',
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
].join('&');
export const filteredTokens = [
......@@ -17,10 +21,40 @@ export const filteredTokens = [
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ 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: 'tv', operator: OPERATOR_IS } },
{ type: 'labels', value: { data: 'live action', 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: '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 {
convertToApiParams,
......@@ -23,27 +23,13 @@ describe('getFilterTokens', () => {
describe('convertToApiParams', () => {
it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual({
author_username: 'homer',
'not[author_username]': 'marge',
assignee_username: 'bart',
'not[assignee_username]': 'lisa',
labels: 'cartoon,tv',
'not[labels]': 'live action,drama',
});
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
});
describe('convertToUrlParams', () => {
it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual({
author_username: ['homer'],
'not[author_username]': ['marge'],
'assignee_username[]': ['bart'],
'not[assignee_username][]': ['lisa'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
});
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
});
......
......@@ -293,6 +293,7 @@ RSpec.describe IssuesHelper do
allow(helper).to receive(:url_for).and_return('#')
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),
calendar_path: '#',
can_bulk_update: 'true',
......@@ -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 }),
project_import_jira_path: project_import_jira_path(project),
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,
rss_path: '#',
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