Skip to content

Commit 51d5633

Browse files
jstoffanmergify[bot]
authored andcommitted
feat(versions): Add version group headers based on relative date (#1330)
1 parent 95a3c73 commit 51d5633

15 files changed

+525
-112
lines changed

i18n/en-US.properties

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,16 +322,22 @@ be.sidebarVersions.download = Download
322322
be.sidebarVersions.empty = No prior versions are available for this file.
323323
# Label for the version preview action.
324324
be.sidebarVersions.preview = Preview
325+
# Header to display for group of versions created in the prior week
326+
be.sidebarVersions.priorWeek = Last Week
325327
# Label for the version promote action.
326328
be.sidebarVersions.promote = Make Current
327329
# Label for the version restore action.
328330
be.sidebarVersions.restore = Restore
329331
# Message displayed for a restored version. {name} is the user who performed the action.
330332
be.sidebarVersions.restoredBy = Restored by {name}
333+
# Header to display for group of versions created in the current month
334+
be.sidebarVersions.thisMonth = This Month
331335
# Title for the preview versions sidebar
332336
be.sidebarVersions.title = Version History
337+
# Header to display for group of versions created yesterday
338+
be.sidebarVersions.today = Today
333339
# Label for the version actions dropdown menu toggle button.
334-
be.sidebarVersions.toggle = Toggle
340+
be.sidebarVersions.toggle = Toggle Actions Menu
335341
# Message displayed for an uploaded version. {name} is the user who performed the action.
336342
be.sidebarVersions.uploadedBy = Uploaded by {name}
337343
# Text to display in the version badge.
@@ -340,6 +346,8 @@ be.sidebarVersions.versionNumberBadge = V{versionNumber}
340346
be.sidebarVersions.versionNumberLabel = Version number {versionNumber}
341347
# Name displayed for unknown or deleted users.
342348
be.sidebarVersions.versionUserUnknown = Unknown
349+
# Header to display for group of versions created today
350+
be.sidebarVersions.yesterday = Yesterday
343351
# Size ascending option shown in the share access drop down select.
344352
be.sizeASC = Size: Smallest → Largest
345353
# Size descending option shown in the share access drop down select.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @flow
3+
* @file Versions Group component
4+
* @author Box
5+
*/
6+
7+
import React from 'react';
8+
import { FormattedDate, FormattedMessage } from 'react-intl';
9+
import * as util from '../../../utils/datetime';
10+
import messages from './messages';
11+
import VersionsList from './VersionsList';
12+
import './VersionsGroup.scss';
13+
14+
type Props = {
15+
currentId: string,
16+
fileId: string,
17+
versionGroup: string,
18+
versions: Array<BoxItemVersion>,
19+
};
20+
21+
export const GROUPS = {
22+
PRIOR_MONTH: 'PRIOR_MONTH',
23+
PRIOR_WEEK: 'PRIOR_WEEK',
24+
PRIOR_YEAR: 'PRIOR_YEAR',
25+
THIS_MONTH: 'THIS_MONTH',
26+
TODAY: 'TODAY',
27+
WEEKDAY: 'WEEKDAY',
28+
YESTERDAY: 'YESTERDAY',
29+
};
30+
31+
export const getGroup = ({ created_at: createdAt }: BoxItemVersion) => {
32+
const currentDate = new Date();
33+
const currentDay = currentDate.getDay();
34+
const currentSunday = currentDate.getDate() - currentDay;
35+
const createdAtDate = util.convertToDate(createdAt);
36+
let group;
37+
38+
if (util.isToday(createdAtDate)) {
39+
group = GROUPS.TODAY;
40+
} else if (util.isYesterday(createdAtDate)) {
41+
group = GROUPS.YESTERDAY;
42+
} else if (!util.isCurrentYear(createdAtDate)) {
43+
group = GROUPS.PRIOR_YEAR;
44+
} else if (!util.isCurrentMonth(createdAtDate)) {
45+
group = GROUPS.PRIOR_MONTH;
46+
} else if (createdAtDate.getDate() <= currentSunday - 7) {
47+
group = GROUPS.THIS_MONTH;
48+
} else if (createdAtDate.getDate() <= currentSunday) {
49+
group = GROUPS.PRIOR_WEEK;
50+
} else {
51+
group = GROUPS.WEEKDAY;
52+
}
53+
54+
return group;
55+
};
56+
57+
export const getHeading = (date?: string | Date, group?: string) => {
58+
if (!date || !group) {
59+
return null;
60+
}
61+
62+
switch (group) {
63+
case GROUPS.TODAY:
64+
return <FormattedMessage {...messages.versionsToday} />; // Today
65+
case GROUPS.YESTERDAY:
66+
return <FormattedMessage {...messages.versionsYesterday} />; // Yesterday
67+
case GROUPS.WEEKDAY:
68+
return <FormattedDate value={date} weekday="long" />; // Monday
69+
case GROUPS.PRIOR_WEEK:
70+
return <FormattedMessage {...messages.versionsPriorWeek} />; // Last Week
71+
case GROUPS.THIS_MONTH:
72+
return <FormattedMessage {...messages.versionsThisMonth} />; // This Month
73+
case GROUPS.PRIOR_MONTH:
74+
return <FormattedDate value={date} month="long" />; // January
75+
case GROUPS.PRIOR_YEAR:
76+
return <FormattedDate value={date} year="numeric" />; // 2018
77+
default:
78+
return null;
79+
}
80+
};
81+
82+
const VersionsGroup = ({ versionGroup, versions, ...rest }: Props) => {
83+
const { created_at: groupDate } = versions[0];
84+
85+
return (
86+
<section className="bcs-VersionsGroup">
87+
<h1 className="bcs-VersionsGroup-heading">{getHeading(groupDate, versionGroup)}</h1>
88+
89+
<VersionsList versions={versions} {...rest} />
90+
</section>
91+
);
92+
};
93+
94+
export default VersionsGroup;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@import '../../../styles/variables';
2+
3+
.bcs-VersionsGroup-heading {
4+
color: $bdl-gray-50;
5+
font-size: 14px;
6+
line-height: 1;
7+
margin-bottom: 0;
8+
margin-top: 0;
9+
padding-bottom: 10px;
10+
padding-top: 20px;
11+
}

src/elements/content-sidebar/versions/VersionsList.js

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,32 @@
66

77
import React from 'react';
88
import { Route } from 'react-router-dom';
9-
import { FormattedMessage } from 'react-intl';
10-
import messages from './messages';
119
import VersionsItem from './VersionsItem';
12-
import type { VersionActionCallback } from './Versions';
1310
import './VersionsList.scss';
1411

1512
type Props = {
13+
currentId: string,
1614
fileId: string,
17-
isLoading: boolean,
18-
onDelete: VersionActionCallback,
19-
onDownload: VersionActionCallback,
20-
onPreview: VersionActionCallback,
21-
onPromote: VersionActionCallback,
22-
onRestore: VersionActionCallback,
2315
versions: Array<BoxItemVersion>,
2416
};
2517

26-
const VersionsList = ({ isLoading, versions, ...rest }: Props) => {
27-
if (!isLoading && !versions.length) {
28-
return (
29-
<div className="bcs-VersionsList bcs-is-empty">
30-
<FormattedMessage {...messages.versionsEmpty} />
31-
</div>
32-
);
33-
}
34-
35-
return (
36-
<ul className="bcs-VersionsList">
37-
{versions.map((version, index) => (
38-
<li className="bcs-VersionsList-item" key={version.id}>
39-
<Route
40-
render={({ match }) => (
41-
<VersionsItem
42-
isCurrent={index === 0}
43-
isSelected={match.params.versionId === version.id}
44-
version={version}
45-
{...rest}
46-
/>
47-
)}
48-
/>
49-
</li>
50-
))}
51-
</ul>
52-
);
53-
};
18+
const VersionsList = ({ currentId, versions, ...rest }: Props) => (
19+
<ul className="bcs-VersionsList">
20+
{versions.map(version => (
21+
<li className="bcs-VersionsList-item" key={version.id}>
22+
<Route
23+
render={({ match }) => (
24+
<VersionsItem
25+
isCurrent={currentId === version.id}
26+
isSelected={match.params.versionId === version.id}
27+
version={version}
28+
{...rest}
29+
/>
30+
)}
31+
/>
32+
</li>
33+
))}
34+
</ul>
35+
);
5436

5537
export default VersionsList;

src/elements/content-sidebar/versions/VersionsList.scss

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@
22

33
.bcs-VersionsList {
44
.be & {
5-
padding-bottom: 20px;
6-
}
7-
8-
&.bcs-is-empty {
9-
padding-top: 10px;
10-
text-align: center;
5+
margin: 0;
116
}
127
}
138

src/elements/content-sidebar/versions/VersionsSidebar.js

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,42 +10,71 @@ import InlineError from '../../../components/inline-error';
1010
import messages from './messages';
1111
import messagesCommon from '../../common/messages';
1212
import SidebarContent from '../SidebarContent';
13-
import VersionsList from './VersionsList';
13+
import VersionsGroup, { getGroup } from './VersionsGroup';
1414
import { BackButton } from '../../common/nav-button';
1515
import { LoadingIndicatorWrapper } from '../../../components/loading-indicator';
16-
import type { VersionActionCallback } from './Versions';
1716
import './VersionsSidebar.scss';
1817

1918
type Props = {
2019
error?: string,
2120
fileId: string,
2221
isLoading: boolean,
23-
onDelete: VersionActionCallback,
24-
onDownload: VersionActionCallback,
25-
onPreview: VersionActionCallback,
26-
onPromote: VersionActionCallback,
27-
onRestore: VersionActionCallback,
2822
parentName: string,
2923
versions: Array<BoxItemVersion>,
3024
};
3125

32-
const VersionsSidebar = ({ error, isLoading, parentName, ...rest }: Props) => (
33-
<SidebarContent
34-
className="bcs-Versions"
35-
data-resin-component="preview"
36-
data-resin-feature="versions"
37-
title={
38-
<React.Fragment>
39-
<BackButton data-resin-target="back" to={`/${parentName}`} />
40-
<FormattedMessage {...messages.versionsTitle} />
41-
</React.Fragment>
42-
}
43-
>
44-
<LoadingIndicatorWrapper className="bcs-Versions-content" crawlerPosition="top" isLoading={isLoading}>
45-
{error && <InlineError title={<FormattedMessage {...messagesCommon.error} />}>{error}</InlineError>}
46-
<VersionsList isLoading={isLoading} {...rest} />
47-
</LoadingIndicatorWrapper>
48-
</SidebarContent>
49-
);
26+
const VersionsSidebar = ({ error, isLoading, fileId, parentName, versions, ...rest }: Props) => {
27+
const { id: currentId } = versions[0] || {};
28+
const showVersions = !!versions.length;
29+
const showEmpty = !isLoading && !showVersions;
30+
const versionGroups = versions.reduce((groups, version) => {
31+
const versionGroup = getGroup(version);
32+
33+
groups[versionGroup] = groups[versionGroup] || [];
34+
groups[versionGroup].push(version);
35+
36+
return groups;
37+
}, {});
38+
39+
return (
40+
<SidebarContent
41+
className="bcs-Versions"
42+
data-resin-component="preview"
43+
data-resin-feature="versions"
44+
title={
45+
<React.Fragment>
46+
<BackButton data-resin-target="back" to={`/${parentName}`} />
47+
<FormattedMessage {...messages.versionsTitle} />
48+
</React.Fragment>
49+
}
50+
>
51+
<LoadingIndicatorWrapper className="bcs-Versions-content" crawlerPosition="top" isLoading={isLoading}>
52+
{error && <InlineError title={<FormattedMessage {...messagesCommon.error} />}>{error}</InlineError>}
53+
54+
{showEmpty && (
55+
<div className="bcs-Versions-empty">
56+
<FormattedMessage {...messages.versionsEmpty} />
57+
</div>
58+
)}
59+
60+
{showVersions && (
61+
<ul className="bcs-Versions-menu">
62+
{Object.keys(versionGroups).map(versionGroupKey => (
63+
<li className="bcs-Versions-menu-item" key={versionGroupKey}>
64+
<VersionsGroup
65+
currentId={currentId}
66+
fileId={fileId}
67+
versionGroup={versionGroupKey}
68+
versions={versionGroups[versionGroupKey]}
69+
{...rest}
70+
/>
71+
</li>
72+
))}
73+
</ul>
74+
)}
75+
</LoadingIndicatorWrapper>
76+
</SidebarContent>
77+
);
78+
};
5079

5180
export default VersionsSidebar;

src/elements/content-sidebar/versions/VersionsSidebar.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import '../../../styles/variables';
2+
13
.bcs-Versions {
24
.bcs-scroll-content {
35
width: 100%;
@@ -23,3 +25,20 @@
2325
padding-left: 25px;
2426
padding-right: 25px;
2527
}
28+
29+
.bcs-Versions-empty {
30+
padding-top: 10px;
31+
text-align: center;
32+
}
33+
34+
.bcs-Versions-menu {
35+
.be & {
36+
padding-bottom: 20px;
37+
}
38+
}
39+
40+
.bcs-Versions-menu-item {
41+
& + & {
42+
border-top: 1px solid $bdl-gray-10;
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import { shallow } from 'enzyme/build';
3+
import VersionsGroup, { getGroup, GROUPS } from '../VersionsGroup';
4+
5+
describe('elements/content-sidebar/versions/VersionsGroup', () => {
6+
const defaultDate = '2019-06-20T20:00:00.000Z';
7+
const getWrapper = (props = {}) => shallow(<VersionsGroup {...props} />);
8+
const GlobalDate = Date;
9+
10+
beforeEach(() => {
11+
global.Date = jest.fn(date => new GlobalDate(date || defaultDate));
12+
});
13+
14+
afterEach(() => {
15+
global.Date = GlobalDate;
16+
});
17+
18+
describe('getGroup', () => {
19+
test.each`
20+
createdAt | expected
21+
${'2019-06-20T20:00:00.000Z'} | ${GROUPS.TODAY}
22+
${'2019-06-19T20:00:00.000Z'} | ${GROUPS.YESTERDAY}
23+
${'2019-06-18T20:00:00.000Z'} | ${GROUPS.WEEKDAY}
24+
${'2019-06-17T20:00:00.000Z'} | ${GROUPS.WEEKDAY}
25+
${'2019-06-16T20:00:00.000Z'} | ${GROUPS.PRIOR_WEEK}
26+
${'2019-06-01T20:00:00.000Z'} | ${GROUPS.THIS_MONTH}
27+
${'2019-05-30T20:00:00.000Z'} | ${GROUPS.PRIOR_MONTH}
28+
${'2019-02-01T20:00:00.000Z'} | ${GROUPS.PRIOR_MONTH}
29+
${'2018-05-01T20:00:00.000Z'} | ${GROUPS.PRIOR_YEAR}
30+
`('should return $expected when called with $createdAt', ({ createdAt, expected }) => {
31+
expect(getGroup({ created_at: createdAt })).toEqual(expected);
32+
});
33+
});
34+
35+
describe('render', () => {
36+
test.each(Object.values(GROUPS))('should match its snapshot when group is %s', versionGroup => {
37+
const wrapper = getWrapper({
38+
versionGroup,
39+
versions: [{ created_at: defaultDate }],
40+
});
41+
expect(wrapper).toMatchSnapshot();
42+
});
43+
});
44+
});

0 commit comments

Comments
 (0)