Skip to content

Commit

Permalink
Migrate "time ago" components to React (#3385)
Browse files Browse the repository at this point in the history
* Replace <am-time-ago> (angular-moment) and <rd-timer> with React component

* PropTypes: Moment validation

* Increase polling interval

* Refine component implementation

* Add tooltip with formatted date/time

* Refine component implementation
  • Loading branch information
kravets-levko authored Feb 2, 2019
1 parent 324a1f5 commit 807e6aa
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 79 deletions.
10 changes: 2 additions & 8 deletions client/app/components/DateInput.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';

export function DateInput({
value,
Expand All @@ -27,13 +27,7 @@ export function DateInput({
}

DateInput.propTypes = {
value: (props, propName, componentName) => {
const value = props[propName];
if ((value !== null) && !moment.isMoment(value)) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be a Moment.js instance.');
}
},
value: Moment,
onSelect: PropTypes.func,
className: PropTypes.string,
};
Expand Down
15 changes: 2 additions & 13 deletions client/app/components/DateRangeInput.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import moment from 'moment';
import { isArray } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';

const { RangePicker } = DatePicker;

Expand All @@ -29,18 +29,7 @@ export function DateRangeInput({
}

DateRangeInput.propTypes = {
value: (props, propName, componentName) => {
const value = props[propName];
if (
(value !== null) && !(
isArray(value) && (value.length === 2) &&
moment.isMoment(value[0]) && moment.isMoment(value[1])
)
) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be an array of two Moment.js instances.');
}
},
value: PropTypes.arrayOf(Moment),
onSelect: PropTypes.func,
className: PropTypes.string,
};
Expand Down
10 changes: 2 additions & 8 deletions client/app/components/DateTimeInput.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';

export function DateTimeInput({
value,
Expand All @@ -30,13 +30,7 @@ export function DateTimeInput({
}

DateTimeInput.propTypes = {
value: (props, propName, componentName) => {
const value = props[propName];
if ((value !== null) && !moment.isMoment(value)) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be a Moment.js instance.');
}
},
value: Moment,
withSeconds: PropTypes.bool,
onSelect: PropTypes.func,
className: PropTypes.string,
Expand Down
15 changes: 2 additions & 13 deletions client/app/components/DateTimeRangeInput.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import moment from 'moment';
import { isArray } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';

const { RangePicker } = DatePicker;

Expand Down Expand Up @@ -32,18 +32,7 @@ export function DateTimeRangeInput({
}

DateTimeRangeInput.propTypes = {
value: (props, propName, componentName) => {
const value = props[propName];
if (
(value !== null) && !(
isArray(value) && (value.length === 2) &&
moment.isMoment(value[0]) && moment.isMoment(value[1])
)
) {
return new Error('Prop `' + propName + '` supplied to `' + componentName +
'` should be an array of two Moment.js instances.');
}
},
value: PropTypes.arrayOf(Moment),
withSeconds: PropTypes.bool,
onSelect: PropTypes.func,
className: PropTypes.string,
Expand Down
98 changes: 98 additions & 0 deletions client/app/components/TimeAgo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import moment from 'moment';
import { isNil } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { Moment } from '@/components/proptypes';
import { clientConfig } from '@/services/auth';

const autoUpdateList = new Set();

function updateComponents() {
autoUpdateList.forEach(component => component.update());
setTimeout(updateComponents, 30 * 1000);
}
updateComponents();

export class TimeAgo extends React.PureComponent {
static propTypes = {
// `date` and `placeholder` used in `getDerivedStateFromProps`
// eslint-disable-next-line react/no-unused-prop-types
date: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
// eslint-disable-next-line react/no-unused-prop-types
placeholder: PropTypes.string,
autoUpdate: PropTypes.bool,
};

static defaultProps = {
date: null,
placeholder: '',
autoUpdate: true,
};

static getDerivedStateFromProps({ date, placeholder }) {
// if `date` prop is not empty and a valid date/time - convert it to `moment`
date = !isNil(date) ? moment(date) : null;
date = date && date.isValid() ? date : null;

return {
value: date ? date.fromNow() : placeholder,
title: date ? date.format(clientConfig.dateTimeFormat) : '',
};
}

// Initial state, to get rid of React warning
state = {
title: null,
value: null,
};

componentDidMount() {
autoUpdateList.add(this);
this.update(true);
}

componentWillUnmount() {
autoUpdateList.delete(this);
}

update(force = false) {
if (force || this.props.autoUpdate) {
this.setState(this.constructor.getDerivedStateFromProps(this.props));
}
}

render() {
return <span title={this.state.title}>{this.state.value}</span>;
}
}

export default function init(ngModule) {
ngModule.directive('amTimeAgo', () => ({
link($scope, element, attr) {
const modelName = attr.amTimeAgo;
$scope.$watch(modelName, (value) => {
ReactDOM.render(<TimeAgo date={value} />, element[0]);
});
},
}));

ngModule.component('rdTimeAgo', {
bindings: {
value: '=',
},
controller($scope, $element) {
$scope.$watch('$ctrl.value', () => {
// Initial render will occur here as well
ReactDOM.render(<TimeAgo date={this.value} placeholder="-" />, $element[0]);
});
},
});
}

init.init = true;
14 changes: 14 additions & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import PropTypes from 'prop-types';
import { wrap } from 'lodash';
import moment from 'moment';

export const DataSource = PropTypes.shape({
syntax: PropTypes.string,
Expand Down Expand Up @@ -49,3 +51,15 @@ export const Action = PropTypes.shape({
export const AntdForm = PropTypes.shape({
validateFieldsAndScroll: PropTypes.func,
});

function checkMoment(isRequired, props, propName, componentName) {
const value = props[propName];
const isRequiredValid = isRequired && (value !== null);
const isOptionalValid = !isRequired && ((value === null) || moment.isMoment(value));
if (!isRequiredValid && !isOptionalValid) {
return new Error('Prop `' + propName + '` supplied to `' + componentName + '` should be a Moment.js instance.');
}
}

export const Moment = wrap(false, checkMoment);
Moment.isRequired = wrap(true, checkMoment);
17 changes: 0 additions & 17 deletions client/app/components/rd-time-ago.js

This file was deleted.

2 changes: 0 additions & 2 deletions client/app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import ngMessages from 'angular-messages';
import toastr from 'angular-toastr';
import ngUpload from 'angular-base64-upload';
import vsRepeat from 'angular-vs-repeat';
import 'angular-moment';
import 'brace';
import 'angular-ui-ace';
import 'angular-resizable';
Expand Down Expand Up @@ -49,7 +48,6 @@ const requirements = [
uiBootstrap,
ngMessages,
uiSelect,
'angularMoment',
toastr,
'ui.ace',
ngUpload,
Expand Down
Loading

0 comments on commit 807e6aa

Please sign in to comment.