Commit c8666a78 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '322755-add-missing-things-to-issues-page-refactor' into 'master'

Add missing things to issues page refactor

See merge request gitlab-org/gitlab!60648
parents 9efb39d3 15b7869c
......@@ -3,7 +3,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi
import { getIdFromGraphQLId } from '~/graphql_shared/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 { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
......@@ -50,6 +50,10 @@ export default {
},
},
computed: {
createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY;
},
author() {
return this.issuable.author;
},
......@@ -152,7 +156,12 @@ export default {
</script>
<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 v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
......
......@@ -21,7 +21,6 @@ import {
MAX_LIST_SIZE,
PAGE_SIZE,
RELATIVE_POSITION_ASC,
sortOptions,
sortParams,
} from '~/issues_list/constants';
import {
......@@ -30,6 +29,7 @@ import {
convertToUrlParams,
getFilterTokens,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
......@@ -48,7 +48,6 @@ export default {
i18n,
IssuableListTabs,
PAGE_SIZE,
sortOptions,
sortParams,
components: {
CsvImportExportButtons,
......@@ -87,6 +86,9 @@ export default {
exportCsvPath: {
default: '',
},
hasBlockedIssuesFeature: {
default: false,
},
hasIssues: {
default: false,
},
......@@ -147,6 +149,9 @@ export default {
};
},
computed: {
isBulkEditButtonDisabled() {
return this.showBulkEditSidebar || !this.issues.length;
},
isManualOrdering() {
return this.sortKey === RELATIVE_POSITION_ASC;
},
......@@ -252,6 +257,9 @@ export default {
showPaginationControls() {
return this.issues.length > 0;
},
sortOptions() {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
},
tabCounts() {
return Object.values(IssuableStates).reduce(
(acc, state) => ({
......@@ -346,6 +354,15 @@ export default {
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
getStatus(issue) {
if (issue.closedAt && issue.movedToId) {
return __('CLOSED (MOVED)');
}
if (issue.closedAt) {
return __('CLOSED');
}
return undefined;
},
handleUpdateLegacyBulkEdit() {
// If "select all" checkbox was checked, wait for all checkboxes
// to be checked before updating IssuableBulkUpdateSidebar class
......@@ -434,7 +451,7 @@ export default {
:search-input-placeholder="__('Search or filter results…')"
:search-tokens="searchTokens"
:initial-filter-value="filterTokens"
:sort-options="$options.sortOptions"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
:tabs="$options.IssuableListTabs"
......@@ -472,13 +489,14 @@ export default {
:aria-label="$options.i18n.calendarLabel"
/>
<csv-import-export-buttons
v-if="isSignedIn"
class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="totalIssues"
/>
<gl-button
v-if="canBulkUpdate"
:disabled="showBulkEditSidebar"
:disabled="isBulkEditButtonDisabled"
@click="handleBulkUpdateClick"
>
{{ __('Edit issues') }}
......@@ -492,6 +510,10 @@ export default {
<issue-card-time-info :issue="issuable" />
</template>
<template #status="{ issuable = {} }">
{{ getStatus(issuable) }}
</template>
<template #statistics="{ issuable = {} }">
<li
v-if="issuable.mergeRequestsCount"
......
......@@ -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 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) =>
Object.keys(sortParams).find(
(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 urlParamKeys = tokenTypes.flatMap((key) => Object.values(filters[key].urlParam));
......
......@@ -4,6 +4,8 @@ import { isString, mapValues, isNumber, reduce } from 'lodash';
import * as timeago from 'timeago.js';
import { languageCode, s__, __, n__ } from '../../locale';
export const SECONDS_IN_DAY = 86400;
const DAYS_IN_WEEK = 7;
window.timeago = timeago;
......
......@@ -453,5 +453,31 @@ describe('IssuableItem', () => {
expect(updatedAtEl.find('span').attributes('title')).toBe('Sep 10, 2020 11:41am GMT+0000');
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 {
PAGE_SIZE,
PAGE_SIZE_MANUAL,
RELATIVE_POSITION_ASC,
sortOptions,
sortParams,
} from '~/issues_list/constants';
import eventHub from '~/issues_list/eventhub';
import { getSortOptions } from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
......@@ -35,7 +35,9 @@ describe('IssuesListApp component', () => {
emptyStateSvgPath: 'empty-state.svg',
endpoint: 'api/endpoint',
exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
hasIssues: true,
hasIssueWeightsFeature: true,
isSignedIn: false,
issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
......@@ -43,7 +45,6 @@ describe('IssuesListApp component', () => {
projectLabelsPath: 'project/labels/path',
projectPath: 'path/to/project',
rssPath: 'rss/path',
showImportButton: true,
showNewIssueLink: true,
signInPath: 'sign/in/path',
};
......@@ -105,7 +106,7 @@ describe('IssuesListApp component', () => {
namespace: defaultProvide.projectPath,
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: 'Search or filter results…',
sortOptions,
sortOptions: getSortOptions(true, true),
initialSortBy: CREATED_DESC,
tabs: IssuableListTabs,
currentTab: IssuableStates.Opened,
......@@ -142,18 +143,33 @@ describe('IssuesListApp component', () => {
});
});
it('renders csv import/export component', async () => {
const search = '?page=1&search=refactor&state=opened&order_by=created_at&sort=desc';
describe('csv import/export component', () => {
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({
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
issuableCount: xTotal,
expect(findCsvImportExportButtons().props()).toMatchObject({
exportCsvPath: `${defaultProvide.exportCsvPath}${search}`,
issuableCount: xTotal,
});
});
});
describe('when user is not signed in', () => {
it('does not render', () => {
wrapper = mountComponent({ provide: { ...defaultProvide, isSignedIn: false } });
expect(findCsvImportExportButtons().exists()).toBe(false);
});
});
});
......@@ -369,11 +385,11 @@ describe('IssuesListApp component', () => {
it('shows Jira integration information', () => {
const paragraphs = wrapper.findAll('p');
expect(paragraphs.at(2).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
expect(paragraphs.at(3).text()).toContain(
expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle);
expect(paragraphs.at(2).text()).toContain(
'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,
);
expect(findGlLink().text()).toBe('Enable the Jira integration');
......
......@@ -6,6 +6,7 @@ import {
convertToUrlParams,
getFilterTokens,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
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', () => {
it('returns filtered tokens given "window.location.search"', () => {
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