Skip to content

Commit

Permalink
Merge branch 'master' into b1.2
Browse files Browse the repository at this point in the history
  • Loading branch information
ncovercash committed Nov 12, 2024
2 parents f889696 + 229c653 commit 421b94c
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 30 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

## (in progress)

* [UIPQB-126](https://folio-org.atlassian.net/browse/UIPQB-126) Convert local date to UTC with respect to tenant zone, rather than user.

## [1.2.2](https://github.com/folio-org/ui-plugin-query-builder/tree/v1.2.2) (2024-11-08)

* [UIPQB-102](https://folio-org.atlassian.net/browse/UIPQB-102)The 'Organization accounting code' value contains incorrect '\'(backslash) in the query.
* [UIPQB-102](https://folio-org.atlassian.net/browse/UIPQB-102) The 'Organization accounting code' value contains incorrect '\'(backslash) in the query.
* [UIPQB-147](https://folio-org.atlassian.net/browse/UIPQB-147) Filtering of available values is case sensitive.
* [UIPQB-138](https://folio-org.atlassian.net/browse/UIPQB-138) [QB] It's not possible to select the current date from the DatePicker.
* [UIPQB-138](https://folio-org.atlassian.net/browse/UIPQB-138) It's not possible to select the current date from the DatePicker.

## [1.2.1](https://github.com/folio-org/ui-plugin-query-builder/tree/v1.2.1) (2024-10-31)
* Bump version to v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"pluginType": "query-builder",
"displayName": "ui-plugin-query-builder.meta.title",
"okapiInterfaces": {
"fqm-query": "2.0"
"fqm-query": "2.0",
"configuration": "2.0"
},
"stripesDeps": [
"@folio/stripes-acq-components"
Expand Down Expand Up @@ -68,7 +69,6 @@
"react-query": "^3.6.0",
"ky": "^0.33.2",
"lodash": "^4.17.5",
"moment": "^2.29.4",
"prop-types": "^15.5.10"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormattedMessage } from 'react-intl';
import { DATA_TYPES } from '../../../../constants/dataTypes';
import { COLUMN_KEYS } from '../../../../constants/columnKeys';
import { OPERATORS } from '../../../../constants/operators';
import useTenantTimezone from '../../../../hooks/useTenantTimezone';
import { SelectionContainer } from '../SelectionContainer/SelectionContainer';

import css from '../../../QueryBuilder.css';
Expand Down Expand Up @@ -37,6 +38,8 @@ export const DataTypeInput = ({
].includes(operator);
const hasSourceOrValues = source || availableValues;

const { tenantTimezone: timezone } = useTenantTimezone();

const textControl = ({ testId, type = 'text', textClass }) => {
const onKeyDown = (event) => {
// prevent typing e, +, - in number type
Expand Down Expand Up @@ -97,6 +100,7 @@ export const DataTypeInput = ({

return (
<Datepicker
timeZone={timezone}
data-testid="data-input-dateType"
onChange={(e, value, formattedValue) => {
onChange(formattedValue.replace('Z', ''), index, COLUMN_KEYS.VALUE);
Expand Down
4 changes: 2 additions & 2 deletions src/QueryBuilder/QueryBuilder/helpers/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { getOperatorOptions } from './selectOptions';

export const DEFAULT_PREVIEW_INTERVAL = 3000;

export const getQueryStr = (rows, fieldOptions) => {
export const getQueryStr = (rows, fieldOptions, intl, timezone) => {
return rows.reduce((str, row, index) => {
const bool = row[COLUMN_KEYS.BOOLEAN].current;
const field = row[COLUMN_KEYS.FIELD].current;
const operator = row[COLUMN_KEYS.OPERATOR].current;
const value = row[COLUMN_KEYS.VALUE].current;
const builtValue = valueBuilder({ value, field, operator, fieldOptions });
const builtValue = valueBuilder({ value, field, operator, fieldOptions, intl, timezone });
const baseQuery = `(${field} ${operator} ${builtValue})`;

// if there aren't values yet - return empty string
Expand Down
1 change: 0 additions & 1 deletion src/QueryBuilder/QueryBuilder/helpers/timeUtils.js
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export const ISO_FORMAT = 'YYYY-MM-DD';
export const UTC_FORMAT = 'YYYY-MM-DDTHH:mm:ss.sss';
17 changes: 8 additions & 9 deletions src/QueryBuilder/QueryBuilder/helpers/valueBuilder.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import moment from 'moment/moment';
import { dayjs } from '@folio/stripes/components';
import { DATA_TYPES } from '../../../constants/dataTypes';
import { OPERATORS } from '../../../constants/operators';
import { ISO_FORMAT } from './timeUtils';

export const getCommaSeparatedStr = (arr) => {
const str = arr?.map(el => `"${el?.value}"`).join(',');
Expand All @@ -27,17 +26,17 @@ export const getFormattedUUID = (value, isInRelatedOperator) => {
: getQuotedStr(value);
};

const formatDateToPreview = (dateString) => {
const date = moment(dateString);
const formatDateToPreview = (dateString, intl, timezone) => {
const formattedDate = dayjs.utc(dateString);

if (date.isValid()) {
return date.format(ISO_FORMAT);
if (formattedDate.isValid()) {
return intl.formatDate(formattedDate.toDate(), { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: timezone });
}

return dateString;
};

export const valueBuilder = ({ value, field, operator, fieldOptions }) => {
export const valueBuilder = ({ value, field, operator, fieldOptions, intl, timezone }) => {
const dataType = fieldOptions?.find(o => o.value === field)?.dataType || DATA_TYPES.BooleanType;
const isInRelatedOperator = [OPERATORS.IN, OPERATORS.NOT_IN].includes(operator);
const isArray = Array.isArray(value);
Expand All @@ -59,12 +58,12 @@ export const valueBuilder = ({ value, field, operator, fieldOptions }) => {

[DATA_TYPES.ObjectType]: () => getQuotedStr(value, isInRelatedOperator),

[DATA_TYPES.DateType]: () => getQuotedStr(formatDateToPreview(value), isInRelatedOperator),
[DATA_TYPES.DateType]: () => getQuotedStr(formatDateToPreview(value, intl, timezone), isInRelatedOperator),

[DATA_TYPES.OpenUUIDType]: () => getFormattedUUID(value, isInRelatedOperator),

[DATA_TYPES.StringUUIDType]: () => getFormattedUUID(value, isInRelatedOperator),
};

return valueMap[dataType]?.();
};
};
19 changes: 14 additions & 5 deletions src/QueryBuilder/QueryBuilder/helpers/valueBuilder.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import moment from 'moment';
import { getCommaSeparatedStr, getQuotedStr, valueBuilder } from './valueBuilder';
import { OPERATORS } from '../../../constants/operators';
import { ISO_FORMAT } from './timeUtils';
import { fieldOptions } from '../../../../test/jest/data/entityType';

describe('valueBuilder', () => {
Expand Down Expand Up @@ -70,13 +68,24 @@ describe('valueBuilder', () => {
});

test('should return a string enclosed in double quotes for DateType if value is truthy', () => {
const value = new Date('2024-10-16T04:00:00.000');
const value = '2024-11-06';
const field = 'user_expiration_date';
const operator = OPERATORS.EQUAL;

const date = moment(value).format(ISO_FORMAT);
const intl = {
formatDate: (val, { timeZone }) => `${val.toUTCString()} in ${timeZone}`,
};

expect(valueBuilder({ value: date, field, operator, fieldOptions })).toBe(`"${date}"`);
expect(valueBuilder({ value, field, operator, fieldOptions, intl, timezone: 'Narnia' }))
.toBe('"Wed, 06 Nov 2024 00:00:00 GMT in Narnia"');
});

test('should return the original string for an invalid date', () => {
const value = 'invalid-date';
const field = 'user_expiration_date';
const operator = OPERATORS.EQUAL;

expect(valueBuilder({ value, field, operator, fieldOptions })).toBe('"invalid-date"');
});

test('should return a string enclosed in double quotes for ArrayType if value is a string', () => {
Expand Down
6 changes: 5 additions & 1 deletion src/QueryBuilder/ResultViewer/DynamicTable/DynamicTable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { DATA_TYPES } from '../../../constants/dataTypes';
import css from './DynamicTable.css';

Expand All @@ -14,6 +14,10 @@ function getCellValue(row, property) {
: <FormattedMessage id="ui-plugin-query-builder.options.false" />;
}

if (property.dataType.dataType === DATA_TYPES.DateType) {
return row[property.property] ? <FormattedDate value={row[property.property]} /> : '';
}

return row[property.property];
}

Expand Down
45 changes: 42 additions & 3 deletions src/QueryBuilder/ResultViewer/DynamicTable/DynamicTable.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
import React from 'react';
import { IntlProvider } from 'react-intl';
import { DynamicTable } from './DynamicTable';

describe('DynamicTable component', () => {
Expand Down Expand Up @@ -68,6 +69,30 @@ describe('DynamicTable component', () => {
labelAlias: 'Empty bool column',
property: 'isEmptyBool',
},
{
name: 'cool_date',
dataType: {
dataType: 'dateType',
},
labelAlias: 'Date column',
property: 'coolDate',
},
{
name: 'less_cool_date',
dataType: {
dataType: 'dateType',
},
labelAlias: 'Date column 2',
property: 'lessCoolDate',
},
{
name: 'empty_date',
dataType: {
dataType: 'dateType',
},
labelAlias: 'Empty date column',
property: 'emptyDate',
},
];

it.each(['[]', undefined, null])(
Expand All @@ -89,12 +114,19 @@ describe('DynamicTable component', () => {
"distributionType": "percentage",
"isCool": true,
"isNotCool": false,
"isEmptyBool": null
"isEmptyBool": null,
"coolDate": "2021-01-01T05:00:00.000Z",
"lessCoolDate": "2021-01-01T04:59:00.000Z",
"emptyDate": null
}
]`;

it('renders table with correct properties and values', () => {
const { getByText } = render(<DynamicTable properties={properties} values={values} />);
const { getByText } = render(
<IntlProvider timeZone="America/New_York">
<DynamicTable properties={properties} values={values} />
</IntlProvider>,
);

properties.forEach((property) => {
const label = getByText(property.labelAlias);
Expand All @@ -114,5 +146,12 @@ describe('DynamicTable component', () => {
expect(trueCell).toBeInTheDocument();
expect(falseCell).toBeInTheDocument();
expect(trueCell.compareDocumentPosition(falseCell)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);

const coolDateCell = getByText('1/1/2021'); // after midnight in EST
const lessCoolDateCell = getByText('12/31/2020'); // before midnight in EST

expect(coolDateCell).toBeInTheDocument();
expect(lessCoolDateCell).toBeInTheDocument();
expect(coolDateCell.compareDocumentPosition(lessCoolDateCell)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
});
});
2 changes: 1 addition & 1 deletion src/hooks/useQuerySource.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const useQuerySource = ({ initialValues, entityType, getParamsSource }) =
const fieldOptions = useMemo(() => getFieldOptions(entityType?.columns), [entityType]);
const stringifiedFieldOptions = useMemo(() => JSON.stringify(fieldOptions), [fieldOptions]);

const queryStr = getQueryStr(source, fieldOptions);
const queryStr = getQueryStr(source, fieldOptions, intl);
const isQueryFilled = isQueryValid(source);
const fqlQuery = sourceToMongoQuery(source);

Expand Down
84 changes: 84 additions & 0 deletions src/hooks/useTenantTimezone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { userLocaleConfig, useStripes, useOkapiKy } from '@folio/stripes/core';
import { useQuery } from 'react-query';

export function getQueryWarning(tenantTimezone, userTimezone) {
if (tenantTimezone === userTimezone) {
return null;
}
if (!tenantTimezone || !userTimezone) {
return null;
}

// TODO: add a warning here!, and also use this...
return 'a warning should go here! TODO [UIPQB-155]';
}

export async function getConfigEntryTimezone(ky, query) {
return JSON.parse(
(
await ky
.get('configurations/entries', {
searchParams: {
query,
},
})
.json()
).configs?.[0].value ?? '{}',
).timezone;
}

/**
* Determines the timezone that should be used when building a query and displaying results.
*
* We specifically do NOT want the user's timezone, as this may cause weird inconsistencies
* if the same queries are run by different users in the same tenant. As such, we will always
* use the tenant's timezone.
*
* Additionally, the backend will always use the tenant's timezone when exporting queries, so
* we want to ensure expectations are met across the board.
*
* @returns `{
* userTimezone?: string;
* tenantTimezone?: string;
* timezoneQueryWarning: ReactNode;
* }`
* `userTimezone`: The timezone of the current user if an override exists, the timezone of the
* tenant if no override exists, or undefined if queries are still resolving.
* `tenantTimezone`: The timezone of the tenant, or undefined if the query is still resolving.
* `timezoneQueryWarning`: A warning message that should be displayed if the query contains
* date fields. Will only be present iff userTimezone ≠ tenantTimezone.
*/
export default function useTenantTimezone() {
const stripes = useStripes();
const ky = useOkapiKy();

const tenantTimezone = useQuery({
queryKey: ['@folio/plugin-query-builder', 'timezone-config', 'tenant'],
queryFn: () => getConfigEntryTimezone(
ky,
`(${[
'module==ORG',
'configName == localeSettings',
'(cql.allRecords=1 NOT userId="" NOT code="")',
].join(' AND ')})`,
),
refetchOnMount: false,
});

const userTimezone = useQuery({
queryKey: ['@folio/plugin-query-builder', 'timezone-config', 'user'],
queryFn: () => getConfigEntryTimezone(
ky,
`(${Object.entries({ ...userLocaleConfig, userId: stripes.user.user.id })
.map(([k, v]) => `"${k}"=="${v}"`)
.join(' AND ')})`,
),
refetchOnMount: false,
});

return {
userTimezone: userTimezone.data ?? tenantTimezone.data,
tenantTimezone: tenantTimezone.data,
timezoneQueryWarning: getQueryWarning(tenantTimezone.data, userTimezone.data),
};
}
20 changes: 20 additions & 0 deletions src/hooks/useTenantTimezone.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getQueryWarning } from './useTenantTimezone';

describe('timezone query warning', () => {
it.each([
[null, null],
[null, undefined],
['', null],
['', undefined],
['Narnia', null],
['Narnia', undefined],
['Narnia', 'Narnia'],
])('returns no warning for [%s,%s]', (a, b) => {
expect(getQueryWarning(a, b)).toBeNull();
expect(getQueryWarning(b, a)).toBeNull();
});

it('returns a warning for different timezones', () => {
expect(getQueryWarning('Narnia', 'Atlantis')).not.toBeNull();
});
});
6 changes: 2 additions & 4 deletions test/jest/__mock__/intlProvider.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import componentsTranslations from '@folio/stripes-components/translations/strip
import smartComponentsTranslations from '@folio/stripes-smart-components/translations/stripes-smart-components/en';
// eslint-disable-next-line import/no-extraneous-dependencies
import stripesCoreTranslations from '@folio/stripes-core/translations/stripes-core/en';
import findFincMetadataCollectionProviderTranslations from '../../../translations/ui-plugin-query-builder/en.json';
import uiPluginQueryBuilderTranslations from '../../../translations/ui-plugin-query-builder/en';

const prefixKeys = (translations, prefix) => {
return Object.keys(translations).reduce(
Expand All @@ -19,14 +19,12 @@ const prefixKeys = (translations, prefix) => {
};

const translations = {
...prefixKeys(findFincMetadataCollectionProviderTranslations, 'ui-plugin-find-finc-metadata-collection'),
...prefixKeys(uiPluginQueryBuilderTranslations, 'ui-plugin-query-builder'),
...prefixKeys(componentsTranslations, 'stripes-components'),
...prefixKeys(smartComponentsTranslations, 'stripes-smart-components'),
...prefixKeys(stripesCoreTranslations, 'stripes-core'),
};

// eslint-disable-next-line react/prop-types

const Intl = ({ children }) => (
<IntlProvider locale="en" messages={translations}>
{children}
Expand Down

0 comments on commit 421b94c

Please sign in to comment.