Commit 15b7869c authored by Coung Ngo's avatar Coung Ngo Committed by Enrique Alcántara

Add missing things to issues page refactor

parent a50462d6
...@@ -3,7 +3,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi ...@@ -3,7 +3,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
import { getTimeago } from '~/lib/utils/datetime_utility'; import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility'; import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale'; import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
...@@ -50,6 +50,10 @@ export default { ...@@ -50,6 +50,10 @@ export default {
}, },
}, },
computed: { computed: {
createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY;
},
author() { author() {
return this.issuable.author; return this.issuable.author;
}, },
...@@ -152,7 +156,12 @@ export default { ...@@ -152,7 +156,12 @@ export default {
</script> </script>
<template> <template>
<li :id="`issuable_${issuable.id}`" class="issue gl-px-5!" :data-labels="labelIdsString"> <li
:id="`issuable_${issuable.id}`"
class="issue gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString"
>
<div class="issuable-info-container"> <div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check"> <div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox <gl-form-checkbox
......
...@@ -21,7 +21,6 @@ import { ...@@ -21,7 +21,6 @@ import {
MAX_LIST_SIZE, MAX_LIST_SIZE,
PAGE_SIZE, PAGE_SIZE,
RELATIVE_POSITION_ASC, RELATIVE_POSITION_ASC,
sortOptions,
sortParams, sortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import { import {
...@@ -30,6 +29,7 @@ import { ...@@ -30,6 +29,7 @@ import {
convertToUrlParams, convertToUrlParams,
getFilterTokens, getFilterTokens,
getSortKey, getSortKey,
getSortOptions,
} from '~/issues_list/utils'; } from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
...@@ -48,7 +48,6 @@ export default { ...@@ -48,7 +48,6 @@ export default {
i18n, i18n,
IssuableListTabs, IssuableListTabs,
PAGE_SIZE, PAGE_SIZE,
sortOptions,
sortParams, sortParams,
components: { components: {
CsvImportExportButtons, CsvImportExportButtons,
...@@ -87,6 +86,9 @@ export default { ...@@ -87,6 +86,9 @@ export default {
exportCsvPath: { exportCsvPath: {
default: '', default: '',
}, },
hasBlockedIssuesFeature: {
default: false,
},
hasIssues: { hasIssues: {
default: false, default: false,
}, },
...@@ -147,6 +149,9 @@ export default { ...@@ -147,6 +149,9 @@ export default {
}; };
}, },
computed: { computed: {
isBulkEditButtonDisabled() {
return this.showBulkEditSidebar || !this.issues.length;
},
isManualOrdering() { isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_ASC; return this.sortKey === RELATIVE_POSITION_ASC;
}, },
...@@ -252,6 +257,9 @@ export default { ...@@ -252,6 +257,9 @@ export default {
showPaginationControls() { showPaginationControls() {
return this.issues.length > 0; return this.issues.length > 0;
}, },
sortOptions() {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
},
tabCounts() { tabCounts() {
return Object.values(IssuableStates).reduce( return Object.values(IssuableStates).reduce(
(acc, state) => ({ (acc, state) => ({
...@@ -346,6 +354,15 @@ export default { ...@@ -346,6 +354,15 @@ export default {
getExportCsvPathWithQuery() { getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`; return `${this.exportCsvPath}${window.location.search}`;
}, },
getStatus(issue) {
if (issue.closedAt && issue.movedToId) {
return __('CLOSED (MOVED)');
}
if (issue.closedAt) {
return __('CLOSED');
}
return undefined;
},
handleUpdateLegacyBulkEdit() { handleUpdateLegacyBulkEdit() {
// If "select all" checkbox was checked, wait for all checkboxes // If "select all" checkbox was checked, wait for all checkboxes
// to be checked before updating IssuableBulkUpdateSidebar class // to be checked before updating IssuableBulkUpdateSidebar class
...@@ -423,7 +440,7 @@ export default { ...@@ -423,7 +440,7 @@ export default {
:search-input-placeholder="__('Search or filter results…')" :search-input-placeholder="__('Search or filter results…')"
:search-tokens="searchTokens" :search-tokens="searchTokens"
:initial-filter-value="filterTokens" :initial-filter-value="filterTokens"
:sort-options="$options.sortOptions" :sort-options="sortOptions"
:initial-sort-by="sortKey" :initial-sort-by="sortKey"
:issuables="issues" :issuables="issues"
:tabs="$options.IssuableListTabs" :tabs="$options.IssuableListTabs"
...@@ -461,13 +478,14 @@ export default { ...@@ -461,13 +478,14 @@ export default {
:aria-label="$options.i18n.calendarLabel" :aria-label="$options.i18n.calendarLabel"
/> />
<csv-import-export-buttons <csv-import-export-buttons
v-if="isSignedIn"
class="gl-mr-3" class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery" :export-csv-path="exportCsvPathWithQuery"
:issuable-count="totalIssues" :issuable-count="totalIssues"
/> />
<gl-button <gl-button
v-if="canBulkUpdate" v-if="canBulkUpdate"
:disabled="showBulkEditSidebar" :disabled="isBulkEditButtonDisabled"
@click="handleBulkUpdateClick" @click="handleBulkUpdateClick"
> >
{{ __('Edit issues') }} {{ __('Edit issues') }}
...@@ -481,6 +499,10 @@ export default { ...@@ -481,6 +499,10 @@ export default {
<issue-card-time-info :issue="issuable" /> <issue-card-time-info :issue="issuable" />
</template> </template>
<template #status="{ issuable = {} }">
{{ getStatus(issuable) }}
</template>
<template #statistics="{ issuable = {} }"> <template #statistics="{ issuable = {} }">
<li <li
v-if="issuable.mergeRequestsCount" v-if="issuable.mergeRequestsCount"
......
...@@ -188,89 +188,6 @@ export const sortParams = { ...@@ -188,89 +188,6 @@ export const sortParams = {
}, },
}; };
export const sortOptions = [
{
id: 1,
title: __('Priority'),
sortDirection: {
ascending: PRIORITY_ASC,
descending: PRIORITY_DESC,
},
},
{
id: 2,
title: __('Created date'),
sortDirection: {
ascending: CREATED_ASC,
descending: CREATED_DESC,
},
},
{
id: 3,
title: __('Last updated'),
sortDirection: {
ascending: UPDATED_ASC,
descending: UPDATED_DESC,
},
},
{
id: 4,
title: __('Milestone due date'),
sortDirection: {
ascending: MILESTONE_DUE_ASC,
descending: MILESTONE_DUE_DESC,
},
},
{
id: 5,
title: __('Due date'),
sortDirection: {
ascending: DUE_DATE_ASC,
descending: DUE_DATE_DESC,
},
},
{
id: 6,
title: __('Popularity'),
sortDirection: {
ascending: POPULARITY_ASC,
descending: POPULARITY_DESC,
},
},
{
id: 7,
title: __('Label priority'),
sortDirection: {
ascending: LABEL_PRIORITY_ASC,
descending: LABEL_PRIORITY_DESC,
},
},
{
id: 8,
title: __('Manual'),
sortDirection: {
ascending: RELATIVE_POSITION_ASC,
descending: RELATIVE_POSITION_ASC,
},
},
{
id: 9,
title: __('Weight'),
sortDirection: {
ascending: WEIGHT_ASC,
descending: WEIGHT_DESC,
},
},
{
id: 10,
title: __('Blocking'),
sortDirection: {
ascending: BLOCKING_ISSUES_ASC,
descending: BLOCKING_ISSUES_DESC,
},
},
];
export const MAX_LIST_SIZE = 10; export const MAX_LIST_SIZE = 10;
export const FILTERED_SEARCH_TERM = 'filtered-search-term'; export const FILTERED_SEARCH_TERM = 'filtered-search-term';
......
import { FILTERED_SEARCH_TERM, filters, sortParams } from '~/issues_list/constants'; import {
BLOCKING_ISSUES_ASC,
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
DUE_DATE_ASC,
DUE_DATE_DESC,
FILTERED_SEARCH_TERM,
filters,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
POPULARITY_ASC,
POPULARITY_DESC,
PRIORITY_ASC,
PRIORITY_DESC,
RELATIVE_POSITION_ASC,
sortParams,
UPDATED_ASC,
UPDATED_DESC,
WEIGHT_ASC,
WEIGHT_DESC,
} from '~/issues_list/constants';
import { __ } from '~/locale';
export const getSortKey = (orderBy, sort) => export const getSortKey = (orderBy, sort) =>
Object.keys(sortParams).find( Object.keys(sortParams).find(
(key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort, (key) => sortParams[key].order_by === orderBy && sortParams[key].sort === sort,
); );
export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
const sortOptions = [
{
id: 1,
title: __('Priority'),
sortDirection: {
ascending: PRIORITY_ASC,
descending: PRIORITY_DESC,
},
},
{
id: 2,
title: __('Created date'),
sortDirection: {
ascending: CREATED_ASC,
descending: CREATED_DESC,
},
},
{
id: 3,
title: __('Last updated'),
sortDirection: {
ascending: UPDATED_ASC,
descending: UPDATED_DESC,
},
},
{
id: 4,
title: __('Milestone due date'),
sortDirection: {
ascending: MILESTONE_DUE_ASC,
descending: MILESTONE_DUE_DESC,
},
},
{
id: 5,
title: __('Due date'),
sortDirection: {
ascending: DUE_DATE_ASC,
descending: DUE_DATE_DESC,
},
},
{
id: 6,
title: __('Popularity'),
sortDirection: {
ascending: POPULARITY_ASC,
descending: POPULARITY_DESC,
},
},
{
id: 7,
title: __('Label priority'),
sortDirection: {
ascending: LABEL_PRIORITY_ASC,
descending: LABEL_PRIORITY_DESC,
},
},
{
id: 8,
title: __('Manual'),
sortDirection: {
ascending: RELATIVE_POSITION_ASC,
descending: RELATIVE_POSITION_ASC,
},
},
];
if (hasIssueWeightsFeature) {
sortOptions.push({
id: 9,
title: __('Weight'),
sortDirection: {
ascending: WEIGHT_ASC,
descending: WEIGHT_DESC,
},
});
}
if (hasBlockedIssuesFeature) {
sortOptions.push({
id: 10,
title: __('Blocking'),
sortDirection: {
ascending: BLOCKING_ISSUES_ASC,
descending: BLOCKING_ISSUES_DESC,
},
});
}
return sortOptions;
};
const tokenTypes = Object.keys(filters); const tokenTypes = Object.keys(filters);
const urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam)); const urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam));
......
...@@ -4,6 +4,8 @@ import { isString, mapValues, isNumber, reduce } from 'lodash'; ...@@ -4,6 +4,8 @@ import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js'; import * as timeago from 'timeago.js';
import { languageCode, s__, __, n__ } from '../../locale'; import { languageCode, s__, __, n__ } from '../../locale';
export const SECONDS_IN_DAY = 86400;
const DAYS_IN_WEEK = 7; const DAYS_IN_WEEK = 7;
window.timeago = timeago; window.timeago = timeago;
......
...@@ -453,5 +453,31 @@ describe('IssuableItem', () => { ...@@ -453,5 +453,31 @@ describe('IssuableItem', () => {
expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000'); expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000');
expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt); expect(updatedAtEl.text()).toBe(wrapper.vm.updatedAt);
}); });
describe('when issuable is closed', () => {
it('renders issuable card with a closed style', () => {
wrapper = createComponent({ issuable: { ...mockIssuable, closedAt: '2020-12-10' } });
expect(wrapper.classes()).toContain('closed');
});
});
describe('when issuable was created within the past 24 hours', () => {
it('renders issuable card with a recently-created style', () => {
wrapper = createComponent({
issuable: { ...mockIssuable, createdAt: '2020-12-10T12:34:56' },
});
expect(wrapper.classes()).toContain('today');
});
});
describe('when issuable was created earlier than the past 24 hours', () => {
it('renders issuable card without a recently-created style', () => {
wrapper = createComponent({ issuable: { ...mockIssuable, createdAt: '2020-12-09' } });
expect(wrapper.classes()).not.toContain('today');
});
});
}); });
}); });
...@@ -15,10 +15,10 @@ import { ...@@ -15,10 +15,10 @@ import {
PAGE_SIZE, PAGE_SIZE,
PAGE_SIZE_MANUAL, PAGE_SIZE_MANUAL,
RELATIVE_POSITION_ASC, RELATIVE_POSITION_ASC,
sortOptions,
sortParams, sortParams,
} from '~/issues_list/constants'; } from '~/issues_list/constants';
import eventHub from '~/issues_list/eventhub'; import eventHub from '~/issues_list/eventhub';
import { getSortOptions } from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { setUrlParams } from '~/lib/utils/url_utility'; import { setUrlParams } from '~/lib/utils/url_utility';
...@@ -35,7 +35,9 @@ describe('IssuesListApp component', () => { ...@@ -35,7 +35,9 @@ describe('IssuesListApp component', () => {
emptyStateSvgPath: 'empty-state.svg', emptyStateSvgPath: 'empty-state.svg',
endpoint: 'api/endpoint', endpoint: 'api/endpoint',
exportCsvPath: 'export/csv/path', exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
hasIssues: true, hasIssues: true,
hasIssueWeightsFeature: true,
isSignedIn: false, isSignedIn: false,
issuesPath: 'path/to/issues', issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path', jiraIntegrationPath: 'jira/integration/path',
...@@ -43,7 +45,6 @@ describe('IssuesListApp component', () => { ...@@ -43,7 +45,6 @@ describe('IssuesListApp component', () => {
projectLabelsPath: 'project/labels/path', projectLabelsPath: 'project/labels/path',
projectPath: 'path/to/project', projectPath: 'path/to/project',
rssPath: 'rss/path', rssPath: 'rss/path',
showImportButton: true,
showNewIssueLink: true, showNewIssueLink: true,
signInPath: 'sign/in/path', signInPath: 'sign/in/path',
}; };
...@@ -105,7 +106,7 @@ describe('IssuesListApp component', () => { ...@@ -105,7 +106,7 @@ describe('IssuesListApp component', () => {
namespace: defaultProvide.projectPath, namespace: defaultProvide.projectPath,
recentSearchesStorageKey: 'issues', recentSearchesStorageKey: 'issues',
searchInputPlaceholder: 'Search or filter results…', searchInputPlaceholder: 'Search or filter results…',
sortOptions, sortOptions: getSortOptions(true, true),
initialSortBy: CREATED_DESC, initialSortBy: CREATED_DESC,
tabs: IssuableListTabs, tabs: IssuableListTabs,
currentTab: IssuableStates.Opened, currentTab: IssuableStates.Opened,
...@@ -142,18 +143,33 @@ describe('IssuesListApp component', () => { ...@@ -142,18 +143,33 @@ describe('IssuesListApp component', () => {
}); });
}); });
it('renders csv import/export component', async () => { describe('csv import/export component', () => {
const search = '?page=1&search=refactor&state=opened&order_by=created_at&sort=desc'; describe('when user is signed in', () => {
it('renders', async () => {
const search = '?page=1&search=refactor&state=opened&order_by=created_at&sort=desc';
global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` }); global.jsdom.reconfigure({ url: `${TEST_HOST}${search}` });
wrapper = mountComponent({ mountFn: mount }); wrapper = mountComponent({
provide: { ...defaultProvide, isSignedIn: true },
mountFn: mount,
});
await waitForPromises(); await waitForPromises();
expect(findCsvImportExportButtons().props()).toMatchObject({ expect(findCsvImportExportButtons().props()).toMatchObject({
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`, exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
issuableCount: xTotal, issuableCount: xTotal,
});
});
});
describe('when user is not signed in', () => {
it('does not render', () => {
wrapper = mountComponent({ provide: { ...defaultProvide, isSignedIn: false } });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
}); });
}); });
...@@ -367,11 +383,11 @@ describe('IssuesListApp component', () => { ...@@ -367,11 +383,11 @@ describe('IssuesListApp component', () => {
it('shows Jira integration information', () => { it('shows Jira integration information', () => {
const paragraphs = wrapper.findAll('p'); const paragraphs = wrapper.findAll('p');
expect(paragraphs.at(2).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle); expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
expect(paragraphs.at(3).text()).toContain( expect(paragraphs.at(2).text()).toContain(
'Enable the Jira integration to view your Jira issues in GitLab.', 'Enable the Jira integration to view your Jira issues in GitLab.',
); );
expect(paragraphs.at(4).text()).toContain( expect(paragraphs.at(3).text()).toContain(
IssuesListApp.i18n.jiraIntegrationSecondaryMessage, IssuesListApp.i18n.jiraIntegrationSecondaryMessage,
); );
expect(findGlLink().text()).toBe('Enable the Jira integration'); expect(findGlLink().text()).toBe('Enable the Jira integration');
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
convertToUrlParams, convertToUrlParams,
getFilterTokens, getFilterTokens,
getSortKey, getSortKey,
getSortOptions,
} from '~/issues_list/utils'; } from '~/issues_list/utils';
describe('getSortKey', () => { describe('getSortKey', () => {
...@@ -15,6 +16,39 @@ describe('getSortKey', () => { ...@@ -15,6 +16,39 @@ describe('getSortKey', () => {
}); });
}); });
describe('getSortOptions', () => {
describe.each`
hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking
${false} | ${false} | ${8} | ${false} | ${false}
${true} | ${false} | ${9} | ${true} | ${false}
${false} | ${true} | ${9} | ${false} | ${true}
${true} | ${true} | ${10} | ${true} | ${true}
`(
'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature',
({
hasIssueWeightsFeature,
hasBlockedIssuesFeature,
length,
containsWeight,
containsBlocking,
}) => {
const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature);
it('returns the correct length of sort options', () => {
expect(sortOptions).toHaveLength(length);
});
it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => {
expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight);
});
it(`${containsBlocking ? 'contains' : 'does not contain'} blocking option`, () => {
expect(sortOptions.some((option) => option.title === 'Blocking')).toBe(containsBlocking);
});
},
);
});
describe('getFilterTokens', () => { describe('getFilterTokens', () => {
it('returns filtered tokens given "window.location.search"', () => { it('returns filtered tokens given "window.location.search"', () => {
expect(getFilterTokens(locationSearch)).toEqual(filteredTokens); expect(getFilterTokens(locationSearch)).toEqual(filteredTokens);
......
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