Updated "Created" member filter to work against site timezone instead of UTC

no issue

- updated NQL generation to adjust dates in filter string so they are the UTC equivalent
  - eg, in UTC-5 "created on or after 2022-02-22" becomes `created_at:>='2022-02-22 05:00:00'`
- updated NQL parsing to take a UTC date filter, convert to a date in the site timezone, then convert to a local date in a way that the datepicker input value matches the respective site timezone date
  - eg, in UTC-5 `created_at:<='2022-02-22 04:59:59` becomes "created on or before 2022-02-21"
This commit is contained in:
Kevin Ansfield 2022-03-03 18:17:14 +00:00
parent 443689ffb3
commit 7470f887cb
2 changed files with 58 additions and 14 deletions

View File

@ -136,10 +136,15 @@ class Filter {
this.type = options.type;
this.relation = options.relation;
this.relationOptions = options.relationOptions;
this.timezone = options.timezone || 'Etc/UTC';
const filterProperty = FILTER_PROPERTIES.find(prop => this.type === prop.name);
const value = filterProperty.valueType === 'date'
? moment(options.value).toDate()
// date string values are passed in as UTC strings
// we need to convert them to the site timezone and make a local date that matches
// so the date string output in the filter inputs is correct
const value = filterProperty.valueType === 'date' && typeof options.value === 'string'
? moment(moment.tz(moment.utc(options.value), this.timezone).format('YYYY-MM-DD')).toDate()
: options.value;
this.value = value;
@ -263,19 +268,23 @@ export default class MembersFilter extends Component {
if (operator === '>') {
relationStr = '>';
filterValue = `'${moment(filter.value).set({hour: 23, minute: 59, second: 59}).format(nqlDateFormat)}'`;
const tzMoment = moment.tz(moment(filter.value).format('YYYY-MM-DD'), this.settings.get('timezone')).set({hour: 23, minute: 59, second: 59});
filterValue = `'${tzMoment.utc().format(nqlDateFormat)}'`;
}
if (operator === '>=') {
relationStr = '>=';
filterValue = `'${moment(filter.value).set({hour: 0, minute: 0, second: 0}).format(nqlDateFormat)}'`;
const tzMoment = moment.tz(moment(filter.value).format('YYYY-MM-DD'), this.settings.get('timezone')).set({hour: 0, minute: 0, second: 0});
filterValue = `'${tzMoment.utc().format(nqlDateFormat)}'`;
}
if (operator === '<') {
relationStr = '<';
filterValue = `'${moment(filter.value).set({hour: 0, minute: 0, second: 0}).format(nqlDateFormat)}'`;
const tzMoment = moment.tz(moment(filter.value).format('YYYY-MM-DD'), this.settings.get('timezone')).set({hour: 0, minute: 0, second: 0});
filterValue = `'${tzMoment.utc().format(nqlDateFormat)}'`;
}
if (operator === '<=') {
relationStr = '<=';
filterValue = `'${moment(filter.value).set({hour: 23, minute: 59, second: 59}).format(nqlDateFormat)}'`;
const tzMoment = moment.tz(moment(filter.value).format('YYYY-MM-DD'), this.settings.get('timeone')).set({hour: 23, minute: 59, second: 59});
filterValue = `'${tzMoment.utc().format(nqlDateFormat)}'`;
}
query += `${filter.type}:${relationStr}${filterValue}+`;
@ -302,7 +311,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is',
value: value.$in,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -313,7 +323,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-not',
value: value.$nin,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -324,7 +335,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-not',
value: value.$ne,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -336,7 +348,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-greater',
value: value.$gt,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -348,7 +361,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-or-greater',
value: value.$gte,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -359,7 +373,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-less',
value: value.$lt,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}
@ -370,7 +385,8 @@ export default class MembersFilter extends Component {
type: key,
relation: 'is-or-less',
value: value.$lte,
relationOptions: FILTER_RELATIONS_OPTIONS[key]
relationOptions: FILTER_RELATIONS_OPTIONS[key],
timezone: this.settings.get('timezone')
});
}

View File

@ -699,7 +699,7 @@ describe('Acceptance: Members filtering', function () {
// with a site timezone UTC-5 (Eastern Time Zone) we would expect date-based NQL filter strings
// to be adjusted to UTC.
//
// Eg. "created at on or after 2022-02-22" = `created_at:>='2022-02-21 19:00:00'
// Eg. "created on or after 2022-02-22" = `created_at:>='2022-02-22 05:00:00'
//
// we also need to convert back when parsing the NQL-based query param and make sure dates
// shown in the members table match site timezone
@ -721,6 +721,34 @@ describe('Acceptance: Members filtering', function () {
const createdAtFields = findAll('[data-test-list="members-list-item"] [data-test-table-data="created-at"]');
expect(createdAtFields.filter(el => el.textContent.match(/21 Feb 2022/)).length).to.equal(3);
expect(createdAtFields.filter(el => el.textContent.match(/22 Feb 2022/)).length).to.equal(4);
const filterSelect = `[data-test-members-filter="0"]`;
const typeSelect = `${filterSelect} [data-test-select="members-filter"]`;
const operatorSelect = `${filterSelect} [data-test-select="members-filter-operator"]`;
const valueInput = `${filterSelect} [data-test-input="members-filter-value"] [data-test-date-picker-input]`;
// filter date is transformed to UTC equivalent timeframe when querying
await click('[data-test-button="members-filter-actions"]');
await fillIn(typeSelect, 'created_at');
await fillIn(operatorSelect, 'is-or-greater');
await fillIn(valueInput, '2022-02-22');
await blur(valueInput);
expect(findAll('[data-test-list="members-list-item"]').length, '# of member rows - post filter')
.to.equal(4);
// query param is transformed back to expected filter date value
await visit('/'); // TODO: remove once <Members::Filter> component reacts to filter updates
const filterQuery = encodeURIComponent(`created_at:<='2022-02-22 04:59:59'`);
await visit(`/members?filter=${filterQuery}`);
expect(findAll('[data-test-list="members-list-item"]').length, '# of member rows - post URL parse')
.to.equal(3);
await click('[data-test-button="members-filter-actions"]');
expect(find(operatorSelect)).to.have.value('is-or-less');
expect(find(valueInput)).to.have.value('2022-02-21');
});
it('can handle multiple filters', async function () {