From 446fc7497c27f58fcbe9cd0f33d4ab931637fc22 Mon Sep 17 00:00:00 2001
From: vashjs
Date: Thu, 1 Aug 2024 16:31:36 +0200
Subject: [PATCH] UITEN-299 Rewrite all class components to functional ones
(ui-tenant-settings module)
---
package.json | 1 +
src/components/Period/Period.js | 190 ++---
src/hooks/useCampusDetails.js | 20 +
src/hooks/useCampuses.js | 19 +
src/hooks/useConfigurations.js | 19 +
src/hooks/useConfigurationsCreate.js | 16 +
src/hooks/useConfigurationsUpdate.js | 16 +
src/hooks/useInstitutionDetails.js | 20 +
src/hooks/useInstitutions.js | 19 +
src/hooks/useLibraries.js | 19 +
src/hooks/useLibraryDetails.js | 20 +
src/hooks/useLocationCreate.js | 16 +
src/hooks/useLocationDelete.js | 16 +
src/hooks/useLocationUpdate.js | 16 +
src/hooks/useLocations.js | 19 +
src/hooks/useRemoteStorageConfigurations.js | 21 +
src/hooks/useRemoteStorageMappingDelete.js | 16 +
src/hooks/useRemoteStorageMappingUpdate.js | 16 +
src/hooks/useRemoteStorageMappings.js | 21 +
src/hooks/useSamlConfiguration.js | 19 +
src/hooks/useSamlConfigurationUpdate.js | 16 +
src/hooks/useSamlDownload.js | 24 +
src/hooks/useServicePoints.js | 19 +
src/settings/Addresses.js | 192 ++---
src/settings/Bindings/Bindings.js | 52 +-
src/settings/Bindings/BindingsForm.js | 139 ++--
src/settings/Locale.js | 77 +-
src/settings/LocaleForm.js | 263 +++---
src/settings/LocationCampuses.js | 255 +++---
src/settings/LocationCampuses.test.js | 115 ++-
src/settings/LocationInstitutions.js | 141 ++--
src/settings/LocationInstitutions.test.js | 63 +-
src/settings/LocationLibraries.js | 327 +++-----
src/settings/LocationLibraries.test.js | 111 ++-
.../LocationLocations/LocationDetail.js | 115 ++-
.../LocationForm/DetailsField.js | 99 +--
.../LocationForm/DetailsField.test.js | 9 +-
.../LocationForm/LocationForm.js | 580 ++++++-------
.../LocationForm/LocationFormContainer.js | 53 +-
.../LocationFormContainer.test.js | 270 +++---
.../LocationForm/RemoteStorageField.js | 13 +-
.../LocationForm/ServicePointsFields.js | 118 ++-
.../LocationLocations/LocationManager.js | 779 +++++++-----------
.../LocationLocations/LocationManager.test.js | 233 ++----
.../RemoteStorage/Control.js | 17 +-
.../RemoteStorage/Control.test.js | 26 +-
.../RemoteStorage/Provider.js | 100 +--
.../LocationLocations/RemoteStorageDetails.js | 3 +-
.../LocationLocations/locationDetail.test.js | 76 +-
.../remoteStorageDetails.test.js | 23 +-
src/settings/LocationLocations/utils.js | 8 +-
src/settings/Plugins/PluginForm.js | 126 ++-
src/settings/Plugins/Plugins.js | 153 ++--
src/settings/Plugins/Plugins.test.js | 67 +-
.../ReadingRoomAccess/ReadingRoomAccess.js | 16 +-
.../ReadingRoomAccess.test.js | 13 +-
src/settings/SSOSettings/SSOSettings.js | 174 ++--
src/settings/SSOSettings/SSOSettings.test.js | 63 +-
src/settings/SSOSettings/SamlForm.js | 292 ++++---
.../ServicePoints/StaffSlipEditList.js | 36 +-
src/settings/ServicePoints/StaffSlipList.js | 49 +-
src/settings/index.js | 228 +++--
src/settings/index.test.js | 47 +-
test/jest/__mocks__/stripesCore.mock.js | 160 ++--
64 files changed, 2842 insertions(+), 3417 deletions(-)
create mode 100644 src/hooks/useCampusDetails.js
create mode 100644 src/hooks/useCampuses.js
create mode 100644 src/hooks/useConfigurations.js
create mode 100644 src/hooks/useConfigurationsCreate.js
create mode 100644 src/hooks/useConfigurationsUpdate.js
create mode 100644 src/hooks/useInstitutionDetails.js
create mode 100644 src/hooks/useInstitutions.js
create mode 100644 src/hooks/useLibraries.js
create mode 100644 src/hooks/useLibraryDetails.js
create mode 100644 src/hooks/useLocationCreate.js
create mode 100644 src/hooks/useLocationDelete.js
create mode 100644 src/hooks/useLocationUpdate.js
create mode 100644 src/hooks/useLocations.js
create mode 100644 src/hooks/useRemoteStorageConfigurations.js
create mode 100644 src/hooks/useRemoteStorageMappingDelete.js
create mode 100644 src/hooks/useRemoteStorageMappingUpdate.js
create mode 100644 src/hooks/useRemoteStorageMappings.js
create mode 100644 src/hooks/useSamlConfiguration.js
create mode 100644 src/hooks/useSamlConfigurationUpdate.js
create mode 100644 src/hooks/useSamlDownload.js
create mode 100644 src/hooks/useServicePoints.js
diff --git a/package.json b/package.json
index c1ce21e0..7f108ad2 100644
--- a/package.json
+++ b/package.json
@@ -305,6 +305,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.4.4",
+ "react-query": "^3.6.0",
"react-router-dom": "^5.2.0"
}
}
diff --git a/src/components/Period/Period.js b/src/components/Period/Period.js
index 9f999028..65d6b441 100644
--- a/src/components/Period/Period.js
+++ b/src/components/Period/Period.js
@@ -1,31 +1,24 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
-
-import {
- Field,
-} from 'react-final-form';
-
-import {
- get,
- isEmpty,
- isNumber,
-} from 'lodash';
+import { Field } from 'react-final-form';
+import { get, isEmpty, isNumber } from 'lodash';
import {
Col,
Row,
Select,
TextField,
- Label,
+ Label
} from '@folio/stripes/components';
-import css from './Period.css';
import {
shortTermExpiryPeriod,
shortTermClosedDateManagementMenu,
longTermClosedDateManagementMenu
} from '../../settings/ServicePoints/constants';
+import css from './Period.css';
+
const validateDuration = value => {
if (typeof value !== 'number') {
@@ -39,32 +32,19 @@ const validateDuration = value => {
return undefined;
};
-class Period extends React.Component {
- static propTypes = {
- fieldLabel: PropTypes.string.isRequired,
- selectPlaceholder: PropTypes.string.isRequired,
- dependentValuePath: PropTypes.string.isRequired,
- inputValuePath: PropTypes.string.isRequired,
- selectValuePath: PropTypes.string.isRequired,
- entity: PropTypes.object.isRequired,
- intervalPeriods: PropTypes.arrayOf(PropTypes.object),
- changeFormValue: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
-
- this.inputRef = React.createRef();
- }
-
- onInputBlur = () => {
- const {
- inputValuePath,
- selectValuePath,
- entity,
- changeFormValue,
- } = this.props;
-
+const Period = ({
+ fieldLabel,
+ selectPlaceholder,
+ dependentValuePath,
+ inputValuePath,
+ selectValuePath,
+ entity,
+ intervalPeriods,
+ changeFormValue
+}) => {
+ const inputRef = useRef(null);
+
+ const onInputBlur = () => {
const inputValue = get(entity, inputValuePath);
if (isNumber(inputValue)) {
@@ -74,22 +54,11 @@ class Period extends React.Component {
changeFormValue(selectValuePath, '');
};
- onInputClear = () => {
- const {
- inputValuePath,
- changeFormValue,
- } = this.props;
-
+ const onInputClear = () => {
changeFormValue(inputValuePath, '');
};
- onSelectChange = (e) => {
- const {
- selectValuePath,
- changeFormValue,
- dependentValuePath,
- } = this.props;
-
+ const onSelectChange = (e) => {
changeFormValue(selectValuePath, e.target.value);
const holdShelfClosedLibraryDateManagementValue =
shortTermExpiryPeriod.findIndex(item => item === e.target.value) > -1
@@ -97,10 +66,10 @@ class Period extends React.Component {
: longTermClosedDateManagementMenu[0].value;
changeFormValue(dependentValuePath, holdShelfClosedLibraryDateManagementValue);
- this.inputRef.current.focus();
+ inputRef.current.focus();
};
- transformInputValue = (value) => {
+ const transformInputValue = (value) => {
if (isEmpty(value)) {
return '';
}
@@ -108,12 +77,7 @@ class Period extends React.Component {
return Number(value);
};
- generateOptions = () => {
- const {
- intervalPeriods,
- selectValuePath,
- } = this.props;
-
+ const generateOptions = () => {
return intervalPeriods.map(({ value, label }) => (
+
+ [example]
+
+
+
+
+
+
+
+ );
+};
BindingsForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool,
- stripes: stripesShape.isRequired,
submitting: PropTypes.bool,
label: PropTypes.node,
};
@@ -114,4 +109,4 @@ BindingsForm.propTypes = {
export default stripesFinalForm({
validate,
navigationCheck: true,
-})(withStripes(BindingsForm));
+})(BindingsForm);
diff --git a/src/settings/Locale.js b/src/settings/Locale.js
index 6a2505b7..d5fdfab5 100644
--- a/src/settings/Locale.js
+++ b/src/settings/Locale.js
@@ -1,56 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+
import { ConfigManager } from '@folio/stripes/smart-components';
+import { TitleManager, useStripes } from '@folio/stripes/core';
-import { injectIntl } from 'react-intl';
-import { TitleManager } from '@folio/stripes/core';
import LocaleForm from './LocaleForm';
import { parseSerializedLocale, serializeLocale } from './localeHelpers';
-class Locale extends React.Component {
- static propTypes = {
- stripes: PropTypes.shape({
- setCurrency: PropTypes.func.isRequired,
- setLocale: PropTypes.func.isRequired,
- setTimezone: PropTypes.func.isRequired,
- connect: PropTypes.func.isRequired,
- }).isRequired,
- label: PropTypes.node.isRequired,
- intl: PropTypes.object,
- };
- constructor(props) {
- super(props);
- this.configManager = props.stripes.connect(ConfigManager);
- this.afterSave = this.afterSave.bind(this);
- }
+const Locale = ({ label, ...rest }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
- afterSave(setting) {
+ const ConnectedConfigManager = stripes.connect(ConfigManager);
+
+ const afterSave = (setting) => {
const localeValues = JSON.parse(setting.value);
const { locale, timezone, currency } = localeValues;
setTimeout(() => {
- if (locale) this.props.stripes.setLocale(locale);
- if (timezone) this.props.stripes.setTimezone(timezone);
- if (currency) this.props.stripes.setCurrency(currency);
+ if (locale) stripes.setLocale(locale);
+ if (timezone) stripes.setTimezone(timezone);
+ if (currency) stripes.setCurrency(currency);
}, 500);
- }
-
- render() {
- return (
-
-
-
- );
- }
-}
-
-export default injectIntl(Locale);
+ };
+
+ return (
+
+
+
+ );
+};
+
+Locale.propTypes = {
+ label: PropTypes.node.isRequired,
+};
+
+export default Locale;
diff --git a/src/settings/LocaleForm.js b/src/settings/LocaleForm.js
index c5d263e0..5660e7a5 100644
--- a/src/settings/LocaleForm.js
+++ b/src/settings/LocaleForm.js
@@ -1,10 +1,9 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import {
createIntl,
createIntlCache,
- FormattedMessage,
- injectIntl,
+ FormattedMessage, useIntl,
} from 'react-intl';
import { Field } from 'react-final-form';
@@ -24,49 +23,24 @@ import {
IfPermission,
supportedLocales,
supportedNumberingSystems,
- withStripes,
+ useStripes,
} from '@folio/stripes/core';
import styles from './Locale.css';
-const timezoneOptions = timezones.map(timezone => (
- {
- value: timezone.value,
- label: timezone.value,
- }
-));
-
-/**
- * localesList: list of available locales suitable for a Select
- * label contains language in context's locale and in iteree's locale
- * e.g. given the context's locale is `en` and the keys `ar` and `zh-CN` show:
- * Arabic / العربية
- * Chinese (China) / 中文(中国)
- * e.g. given the context's locale is `ar` and the keys `ar` and `zh-CN` show:
- * العربية / العربية
- * الصينية (الصين) / 中文(中国)
- *
- * @param {object} intl react-intl object in the current context's locale
- * @returns {array} array of {value, label} suitable for a Select
- */
+
+const timezoneOptions = timezones.map(timezone => ({
+ value: timezone.value,
+ label: timezone.value,
+}));
+
const localesList = (intl) => {
- // This is optional but highly recommended
- // since it prevents memory leak
const cache = createIntlCache();
- // error handler if an intl context cannot be created,
- // i.e. if the browser is missing support for the requested locale
- const logLocaleError = (e) => {
- console.warn(e); // eslint-disable-line
- };
-
- // iterate through the locales list to build an array of { value, label } objects
const locales = supportedLocales.map(l => {
- // intl instance with locale of current iteree
const lIntl = createIntl({
locale: l,
messages: {},
- onError: logLocaleError,
},
cache);
@@ -79,24 +53,10 @@ const localesList = (intl) => {
return locales.sort((a, b) => a.label.localeCompare(b.label));
};
-/**
- * numberingSystemsList: list of available systems, suitable for a Select
- * label contains the name and the digits zero-nine in the given system
- * e.g. given the system is `latn` show:
- * latn (0 1 2 3 4 5 6 7 8 9)
- * e.g. given the system is `arab` show:
- * arab (٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩)
- * More info on numbering systems:
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/numberingSystem
-
- * @returns {array} array of {value, label} suitable for a Select
- */
const numberingSystemsList = () => {
- // This is optional but highly recommended
- // since it prevents memory leak
const cache = createIntlCache();
- const formats = supportedNumberingSystems.map(f => {
+ return supportedNumberingSystems.map(f => {
const lIntl = createIntl({
locale: `en-u-nu-${f}`,
messages: {},
@@ -108,136 +68,123 @@ const numberingSystemsList = () => {
label: `${f} (${Array.from(Array(10).keys()).map(i => lIntl.formatNumber(i)).join(' ')})`,
};
});
-
- return formats;
};
-class LocaleForm extends React.Component {
- constructor(props) {
- super(props);
-
- this.isReadOnly = !props.stripes.hasPerm('ui-tenant-settings.settings.locale');
- this.localesOptions = localesList(props.intl);
- this.numberingSystemOptions = [
- { value: '', label: '---' },
- ...numberingSystemsList()
- ];
- }
-
- getFooter() {
- const { pristine, submitting } = this.props;
-
- return !this.isReadOnly && (
+
+const LocaleForm = ({ handleSubmit, pristine, submitting, label }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
+
+ const isReadOnly = !stripes.hasPerm('ui-tenant-settings.settings.locale');
+ const localesOptions = useMemo(() => localesList(intl), [intl]);
+ const numberingSystemOptions = useMemo(() => [
+ { value: '', label: '---' },
+ ...numberingSystemsList(),
+ ], []);
+
+ const getFooter = () => (
+ !isReadOnly && (
)}
/>
- );
- }
-
- render() {
- const { handleSubmit, label, intl: { formatMessage } } = this.props;
-
- return (
-
- );
- }
-}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
LocaleForm.propTypes = {
handleSubmit: PropTypes.func.isRequired,
pristine: PropTypes.bool,
submitting: PropTypes.bool,
label: PropTypes.node,
- intl: PropTypes.object,
- stripes: PropTypes.shape({
- hasPerm: PropTypes.func.isRequired,
- })
};
export default stripesFinalForm({
navigationCheck: true,
-})(withStripes(injectIntl(LocaleForm)));
+})(LocaleForm);
diff --git a/src/settings/LocationCampuses.js b/src/settings/LocationCampuses.js
index e8ba0f27..edd4f4fb 100644
--- a/src/settings/LocationCampuses.js
+++ b/src/settings/LocationCampuses.js
@@ -1,20 +1,17 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- FormattedMessage,
- injectIntl,
-} from 'react-intl';
+import React, { useState, useCallback } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+
import { ControlledVocab } from '@folio/stripes/smart-components';
-import {
- Select,
- TextLink,
-} from '@folio/stripes/components';
+import { Select, TextLink } from '@folio/stripes/components';
+import { TitleManager, useStripes } from '@folio/stripes/core';
-import { TitleManager } from '@folio/stripes/core';
import locationCodeValidator from './locationCodeValidator';
import composeValidators from '../util/composeValidators';
-import css from './LocationInstitutions.css';
import { CAMPUS_ID_LIBRARIES, INSTITUTION_ID_CAMPUS, INSTITUTION_ID_LIBRARIES } from '../constants';
+import { useLibraries } from '../hooks/useLibraries';
+import { useInstitutions } from '../hooks/useInstitutions';
+import css from './LocationInstitutions.css';
+
const translations = {
cannotDeleteTermHeader: 'ui-tenant-settings.settings.location.campuses.cannotDeleteTermHeader',
@@ -24,89 +21,32 @@ const translations = {
termWillBeDeleted: 'ui-tenant-settings.settings.location.campuses.termWillBeDeleted',
};
-class LocationCampuses extends React.Component {
- static manifest = {
- institutions: {
- type: 'okapi',
- records: 'locinsts',
- path: 'location-units/institutions?query=cql.allRecords=1 sortby name&limit=100',
- accumulate: true,
- },
- libraries: {
- type: 'okapi',
- path: 'location-units/libraries',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '1000',
- },
- records: 'loclibs',
- accumulate: true,
+
+const LocationCampuses = (props) => {
+ const stripes = useStripes();
+ const { formatMessage } = useIntl();
+ const [institutionId, setInstitutionId] = useState(sessionStorage.getItem(INSTITUTION_ID_CAMPUS) || null);
+
+ const hasAllLocationPerms = stripes.hasPerm('ui-tenant-settings.settings.location');
+ const ConnectedControlledVocab = stripes.connect(ControlledVocab);
+
+ const { institutions } = useInstitutions({
+ searchParams: {
+ limit: 100,
+ query: 'cql.allRecords=1 sortby name',
},
- };
-
- static propTypes = {
- stripes: PropTypes.shape({
- connect: PropTypes.func.isRequired,
- hasPerm: PropTypes.func.isRequired
- }).isRequired,
- resources: PropTypes.shape({
- institutions: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object),
- }),
- locationsPerCampus: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object),
- }),
- libraries: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object),
- })
- }),
- intl: PropTypes.object,
- mutator: PropTypes.shape({
- institutions: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- locationsPerCampus: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- }),
- };
-
- constructor(props) {
- super(props);
- this.connectedControlledVocab = props.stripes.connect(ControlledVocab);
- this.hasAllLocationPerms = props.stripes.hasPerm('ui-tenant-settings.settings.location');
- this.numberOfObjectsFormatter = this.numberOfObjectsFormatter.bind(this);
-
- this.state = {
- institutionId: null,
- };
- }
+ });
- /**
- * Refresh lookup tables when the component mounts. Fetches in the manifest
- * will only run once (in the constructor) but because this object may be
- * unmounted/remounted without being destroyed/recreated, the lookup tables
- * will be stale if they change between unmounting/remounting.
- */
- componentDidMount() {
- const institutionId = sessionStorage.getItem(INSTITUTION_ID_CAMPUS);
- this.setState({ institutionId });
- ['institutions', 'libraries'].forEach(i => {
- this.props.mutator[i].reset();
- this.props.mutator[i].GET();
- });
- }
+ const { libraries } = useLibraries({ searchParams: {
+ limit: 1000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
- numberOfObjectsFormatter = (item) => {
- const records = (this.props.resources.libraries || {}).records || [];
- const numberOfObjects = records.reduce((count, loc) => {
- return loc.campusId === item.id ? count + 1 : count;
- }, 0);
+ const numberOfObjectsFormatter = useCallback((item) => {
+ const numberOfObjects = libraries.reduce((count, loc) => (loc.campusId === item.id ? count + 1 : count), 0);
const onNumberOfObjectsClick = () => {
- sessionStorage.setItem(INSTITUTION_ID_LIBRARIES, this.state.institutionId);
+ sessionStorage.setItem(INSTITUTION_ID_LIBRARIES, institutionId);
sessionStorage.setItem(CAMPUS_ID_LIBRARIES, item.id);
};
@@ -118,81 +58,74 @@ class LocationCampuses extends React.Component {
to="./location-libraries"
>
{numberOfObjects}
- );
- }
+
+ );
+ }, [libraries, institutionId]);
- onChangeInstitution = (e) => {
+ const onChangeInstitution = useCallback((e) => {
const value = e.target.value;
- this.setState({ institutionId: value });
-
+ setInstitutionId(value);
sessionStorage.setItem(INSTITUTION_ID_CAMPUS, value);
- }
+ }, []);
- render() {
- const institutions = [];
- (((this.props.resources.institutions || {}).records || []).forEach(i => {
- institutions.push(
-
- );
- }));
-
- if (!institutions.length) {
- return ;
- }
-
- const rowFilter = (
- }
- id="institutionSelect"
- name="institutionSelect"
- onChange={this.onChangeInstitution}
- value={this.state.institutionId}
- >
-
- {selectText => (
-
- )}
-
- {institutions}
-
- );
+ const institutionOptions = institutions.map(i => (
+
+ ));
- return (
-
- from being overwritten by the props.resources here.
- dataKey={undefined}
- baseUrl="location-units/campuses"
- records="loccamps"
- rowFilter={rowFilter}
- rowFilterFunction={(row) => row.institutionId === this.state.institutionId}
- label={this.props.intl.formatMessage({ id: 'ui-tenant-settings.settings.location.campuses' })}
- translations={translations}
- objectLabel={}
- visibleFields={['name', 'code']}
- columnMapping={{
- name: ,
- code: ,
- }}
- formatter={{ numberOfObjects: this.numberOfObjectsFormatter }}
- nameKey="group"
- id="campuses"
- preCreateHook={(item) => ({ ...item, institutionId: this.state.institutionId })}
- listSuppressor={() => !this.state.institutionId}
- listSuppressorText={}
- sortby="name"
- validate={composeValidators(locationCodeValidator.validate)}
- editable={this.hasAllLocationPerms}
- canCreate={this.hasAllLocationPerms}
- />
-
- );
+ if (!institutionOptions.length) {
+ return ;
}
-}
-export default injectIntl(LocationCampuses);
+ const rowFilter = (
+ }
+ id="institutionSelect"
+ name="institutionSelect"
+ onChange={onChangeInstitution}
+ value={institutionId}
+ >
+
+ {selectText => (
+
+ )}
+
+ {institutionOptions}
+
+ );
+
+ return (
+
+ row.institutionId === institutionId}
+ label={formatMessage({ id: 'ui-tenant-settings.settings.location.campuses' })}
+ translations={translations}
+ objectLabel={}
+ visibleFields={['name', 'code']}
+ columnMapping={{
+ name: ,
+ code: ,
+ }}
+ formatter={{ numberOfObjects: numberOfObjectsFormatter }}
+ nameKey="group"
+ id="campuses"
+ preCreateHook={(item) => ({ ...item, institutionId })}
+ listSuppressor={() => !institutionId}
+ listSuppressorText={}
+ sortby="name"
+ validate={composeValidators(locationCodeValidator.validate)}
+ editable={hasAllLocationPerms}
+ canCreate={hasAllLocationPerms}
+ />
+
+ );
+};
+
+export default LocationCampuses;
diff --git a/src/settings/LocationCampuses.test.js b/src/settings/LocationCampuses.test.js
index 58905add..ca8727d2 100644
--- a/src/settings/LocationCampuses.test.js
+++ b/src/settings/LocationCampuses.test.js
@@ -1,14 +1,16 @@
import React from 'react';
-
+import { QueryClient, QueryClientProvider } from 'react-query';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import '../../test/jest/__mocks__';
-import buildStripes from '../../test/jest/__new_mocks__/stripesCore.mock';
import { renderWithRouter } from '../../test/jest/helpers';
-
import LocationCampuses from './LocationCampuses';
+import '../../test/jest/__mocks__';
+import { useInstitutions } from '../hooks/useInstitutions';
+import { useLibraries } from '../hooks/useLibraries';
+
+
jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn(({
rowFilter,
label,
@@ -40,78 +42,69 @@ jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn((
>
)));
-const STRIPES = buildStripes();
-
-const resourcesMock = {
- institutions: {
- records: [
- { code: 'KU',
+jest.mock('../hooks/useInstitutions', () => ({
+ useInstitutions: jest.fn(() => ({
+ institutions: [
+ {
+ code: 'KU',
id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'Københavns Universitet' }
+ name: 'Københavns Universitet'
+ }
+ ],
+ })),
+}));
+
+jest.mock('../hooks/useLibraries', () => ({
+ useLibraries: jest.fn(() => ({
+ libraries: [
+ {
+ 'id': '5d78803e-ca04-4b4a-aeae-2c63b924518b',
+ 'name': 'Datalogisk Institut',
+ 'code': 'DI',
+ 'campusId': '62cf76b7-cca5-4d33-9217-edf42ce1a848',
+ 'metadata': {
+ 'createdDate': '2023-06-23T02:10:45.756+00:00',
+ 'updatedDate': '2023-06-23T02:10:45.756+00:00'
+ }
+ },
+ {
+ 'id': 'c2549bb4-19c7-4fcc-8b52-39e612fb7dbe',
+ 'name': 'Online',
+ 'code': 'E',
+ 'campusId': '470ff1dd-937a-4195-bf9e-06bcfcd135df',
+ 'metadata': {
+ 'createdDate': '2023-06-23T02:10:45.756+00:00',
+ 'updatedDate': '2023-06-23T02:10:45.756+00:00'
+ }
+ }
]
- },
- libraries: {
- records:
- [
- {
- 'id': '5d78803e-ca04-4b4a-aeae-2c63b924518b',
- 'name': 'Datalogisk Institut',
- 'code': 'DI',
- 'campusId': '62cf76b7-cca5-4d33-9217-edf42ce1a848',
- 'metadata': {
- 'createdDate': '2023-06-23T02:10:45.756+00:00',
- 'updatedDate': '2023-06-23T02:10:45.756+00:00'
- }
- },
- {
- 'id': 'c2549bb4-19c7-4fcc-8b52-39e612fb7dbe',
- 'name': 'Online',
- 'code': 'E',
- 'campusId': '470ff1dd-937a-4195-bf9e-06bcfcd135df',
- 'metadata': {
- 'createdDate': '2023-06-23T02:10:45.756+00:00',
- 'updatedDate': '2023-06-23T02:10:45.756+00:00'
- }
- }
-
- ]
- }
-};
-
-const mutatorMock = {
- libraries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- institutions: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
-};
-
-const renderLocationCampuses = (resources = {}) => renderWithRouter(
-
+ })),
+}));
+
+const renderLocationCampuses = () => renderWithRouter(
+
+
+
);
describe('LocationCampuses', () => {
it('should render LocationCampuses with empty resources', async () => {
+ useInstitutions.mockImplementationOnce(() => ({ institutions: [] }));
+ useLibraries.mockImplementationOnce(() => ({ libraries: [] }));
+
renderLocationCampuses();
- expect(screen.getByTestId('institutuins-empty')).toBeVisible();
+ expect(screen.getByTestId('institutions-empty')).toBeVisible();
});
it('should render LocationCampuses', async () => {
- renderLocationCampuses(resourcesMock);
+ renderLocationCampuses();
expect(renderLocationCampuses).toBeDefined();
});
it('should render LocationCampuses changed option value', async () => {
- renderLocationCampuses(resourcesMock);
+ renderLocationCampuses();
const checkboxInstitution = screen.getByRole('combobox');
@@ -123,7 +116,7 @@ describe('LocationCampuses', () => {
});
it('should render LocationCampuses changed option value and call click', async () => {
- renderLocationCampuses(resourcesMock);
+ renderLocationCampuses();
const checkboxInstitution = screen.getByRole('combobox');
diff --git a/src/settings/LocationInstitutions.js b/src/settings/LocationInstitutions.js
index 7f081385..aa609aaf 100644
--- a/src/settings/LocationInstitutions.js
+++ b/src/settings/LocationInstitutions.js
@@ -1,18 +1,15 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import {
- FormattedMessage,
- injectIntl,
-} from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import { ControlledVocab } from '@folio/stripes/smart-components';
-
import { TextLink } from '@folio/stripes/components';
-import { TitleManager } from '@folio/stripes/core';
+import { TitleManager, useStripes } from '@folio/stripes/core';
+
import composeValidators from '../util/composeValidators';
import locationCodeValidator from './locationCodeValidator';
-import css from './LocationInstitutions.css';
import { INSTITUTION_ID_CAMPUS } from '../constants';
+import { useCampuses } from '../hooks/useCampuses';
+import css from './LocationInstitutions.css';
const translations = {
cannotDeleteTermHeader: 'ui-tenant-settings.settings.location.institutions.cannotDeleteTermHeader',
@@ -22,54 +19,20 @@ const translations = {
termWillBeDeleted: 'ui-tenant-settings.settings.location.institutions.termWillBeDeleted',
};
-class LocationInstitutions extends React.Component {
- static manifest = Object.freeze({
- campuses: {
- type: 'okapi',
- records: 'loccamps',
- path: 'location-units/campuses?query=cql.allRecords=1 sortby name&limit=2000',
- accumulate: true,
- },
- });
- static propTypes = {
- intl: PropTypes.object,
- stripes: PropTypes.shape({
- connect: PropTypes.func.isRequired,
- hasPerm: PropTypes.func.isRequired,
- }).isRequired,
- resources: PropTypes.shape({
- campuses: PropTypes.object,
- }).isRequired,
- mutator: PropTypes.shape({
- campuses: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- }),
- };
-
- constructor(props) {
- super(props);
- this.hasAllLocationPerms = props.stripes.hasPerm('ui-tenant-settings.settings.location');
- this.connectedControlledVocab = props.stripes.connect(ControlledVocab);
- this.numberOfObjectsFormatter = this.numberOfObjectsFormatter.bind(this);
- }
+const LocationInstitutions = (props) => {
+ const stripes = useStripes();
+ const { formatMessage } = useIntl();
+ const hasAllLocationPerms = stripes.hasPerm('ui-tenant-settings.settings.location');
+ const ConnectedControlledVocab = stripes.connect(ControlledVocab);
- /**
- * Refresh lookup tables when the component mounts. Fetches in the manifest
- * will only run once (in the constructor) but because this object may be
- * unmounted/remounted without being destroyed/recreated, the lookup tables
- * will be stale if they change between unmounting/remounting.
- */
- componentDidMount() {
- this.props.mutator.campuses.reset();
- this.props.mutator.campuses.GET();
- }
+ const { campuses } = useCampuses({ searchParams: {
+ limit: 1000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
- numberOfObjectsFormatter = (item) => {
- const records = (this.props.resources.campuses || {}).records || [];
- const numberOfObjects = records.reduce((count, loc) => {
+ const numberOfObjectsFormatter = (item) => {
+ const numberOfObjects = campuses.reduce((count, loc) => {
return loc.institutionId === item.id ? count + 1 : count;
}, 0);
@@ -84,42 +47,42 @@ class LocationInstitutions extends React.Component {
to="./location-campuses"
>
{numberOfObjects}
- );
- }
+
+ );
+ };
- render() {
- const formatter = {
- numberOfObjects: this.numberOfObjectsFormatter,
- };
+ const formatter = {
+ numberOfObjects: numberOfObjectsFormatter,
+ };
- return (
-
- from being overwritten by the props.resources here.
- dataKey={undefined}
- baseUrl="location-units/institutions"
- records="locinsts"
- label={this.props.intl.formatMessage({ id: 'ui-tenant-settings.settings.location.institutions' })}
- translations={translations}
- objectLabel={}
- visibleFields={['name', 'code']}
- columnMapping={{
- name: ,
- code: ,
- }}
- formatter={formatter}
- nameKey="name"
- id="institutions"
- sortby="name"
- validate={composeValidators(locationCodeValidator.validate)}
- editable={this.hasAllLocationPerms}
- canCreate={this.hasAllLocationPerms}
- />
-
- );
- }
-}
+ return (
+
+ }
+ visibleFields={['name', 'code']}
+ columnMapping={{
+ name: ,
+ code: ,
+ }}
+ formatter={formatter}
+ nameKey="name"
+ id="institutions"
+ sortby="name"
+ validate={composeValidators(locationCodeValidator.validate)}
+ editable={hasAllLocationPerms}
+ canCreate={hasAllLocationPerms}
+ />
+
+ );
+};
-export default injectIntl(LocationInstitutions);
+export default LocationInstitutions;
diff --git a/src/settings/LocationInstitutions.test.js b/src/settings/LocationInstitutions.test.js
index 3a1c2402..696fb3c7 100644
--- a/src/settings/LocationInstitutions.test.js
+++ b/src/settings/LocationInstitutions.test.js
@@ -1,9 +1,19 @@
import React from 'react';
import { screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from 'react-query';
import LocationInstitutions from './LocationInstitutions';
import { renderWithRouter } from '../../test/jest/helpers';
+
+const mockCampuses = [
+ {
+ 'id' : '1',
+ 'name' : 'Københavns Universitet',
+ 'code' : 'KU',
+ }
+];
+
jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn(({
formatter,
label
@@ -11,17 +21,17 @@ jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn((
<>
{label}
- {formatter.numberOfObjects(
- {
- 'id' : '1',
- 'name' : 'Københavns Universitet',
- 'code' : 'KU',
- }
- )}
+ {formatter.numberOfObjects(mockCampuses[0])}
>
)));
+jest.mock('../hooks/useCampuses', () => ({
+ useCampuses: jest.fn(() => ({
+ campuses: mockCampuses,
+ })),
+}));
+
const stripesMock = {
connect: component => component,
hasPerm: jest.fn().mockResolvedValue(true),
@@ -30,39 +40,14 @@ const stripesMock = {
},
};
-const resourcesMock = {
- locationsPerInstitution: {
- records: [
- {
- 'id' : '1',
- 'name' : 'Annex',
- 'code' : 'KU/CC/DI/A',
- 'isActive' : true,
- 'institutionId' : '1',
- 'campusId' : '1',
- 'libraryId' : '1',
- 'primaryServicePoint' : '1',
- 'servicePointIds' : ['1'],
- 'servicePoints' : [],
- },
- ]
- },
-};
-
-const mutatorMock = {
- campuses: {
- GET: jest.fn(),
- reset: jest.fn(),
- }
-};
-
const renderLocationInstitutions = () => (
renderWithRouter(
-
+
+
+
+
)
);
@@ -71,8 +56,6 @@ describe('LocationInstitutions', () => {
renderLocationInstitutions();
const numbersOfObjectsCells = screen.getAllByText('ui-tenant-settings.settings.location.institutions');
- expect(mutatorMock.campuses.GET).toBeCalled();
- expect(mutatorMock.campuses.reset).toBeCalled();
numbersOfObjectsCells.forEach((el) => {
expect(el).toBeVisible();
});
diff --git a/src/settings/LocationLibraries.js b/src/settings/LocationLibraries.js
index 5c656976..aeeaefda 100644
--- a/src/settings/LocationLibraries.js
+++ b/src/settings/LocationLibraries.js
@@ -1,18 +1,10 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- FormattedMessage,
- injectIntl,
-} from 'react-intl';
-import { get } from 'lodash';
+import React, { useState, useEffect, useCallback } from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
import { ControlledVocab } from '@folio/stripes/smart-components';
-import {
- Select,
- TextLink
-} from '@folio/stripes/components';
+import { Select, TextLink } from '@folio/stripes/components';
+import { TitleManager, useStripes } from '@folio/stripes/core';
-import { TitleManager } from '@folio/stripes/core';
import composeValidators from '../util/composeValidators';
import locationCodeValidator from './locationCodeValidator';
import {
@@ -23,6 +15,9 @@ import {
LOCATION_LIBRARY_ID_KEY
} from '../constants';
import css from './LocationInstitutions.css';
+import { useInstitutions } from '../hooks/useInstitutions';
+import { useCampuses } from '../hooks/useCampuses';
+import { useLocations } from '../hooks/useLocations';
const translations = {
cannotDeleteTermHeader: 'ui-tenant-settings.settings.location.libraries.cannotDeleteTermHeader',
@@ -32,97 +27,43 @@ const translations = {
termWillBeDeleted: 'ui-tenant-settings.settings.location.libraries.termWillBeDeleted',
};
-class LocationLibraries extends React.Component {
- static manifest = Object.freeze({
- institutions: {
- type: 'okapi',
- records: 'locinsts',
- path: 'location-units/institutions?query=cql.allRecords=1 sortby name&limit=100',
- accumulate: true,
- },
- campuses: {
- type: 'okapi',
- records: 'loccamps',
- path: 'location-units/campuses?query=cql.allRecords=1 sortby name&limit=100',
- accumulate: true,
- },
- locationsPerLibrary: {
- type: 'okapi',
- records: 'locations',
- path: 'locations',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '10000',
- },
- accumulate: true,
- },
- });
- static propTypes = {
- intl: PropTypes.object,
- stripes: PropTypes.shape({
- connect: PropTypes.func.isRequired,
- hasPerm: PropTypes.func.isRequired,
- }).isRequired,
- resources: PropTypes.shape({
- institutions: PropTypes.object,
- campuses: PropTypes.object,
- locationsPerLibrary: PropTypes.object,
- }).isRequired,
- mutator: PropTypes.shape({
- institutions: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- campuses: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- locationsPerLibrary: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- }),
- };
+const LocationLibraries = (props) => {
+ const stripes = useStripes();
+ const { formatMessage } = useIntl();
+ const [institutionId, setInstitutionId] = useState(sessionStorage.getItem(INSTITUTION_ID_LIBRARIES) || null);
+ const [campusId, setCampusId] = useState(sessionStorage.getItem(CAMPUS_ID_LIBRARIES) || null);
- constructor(props) {
- super(props);
- this.connectedControlledVocab = props.stripes.connect(ControlledVocab);
- this.hasAllLocationPerms = props.stripes.hasPerm('ui-tenant-settings.settings.location');
- this.numberOfObjectsFormatter = this.numberOfObjectsFormatter.bind(this);
+ const hasAllLocationPerms = stripes.hasPerm('ui-tenant-settings.settings.location');
+ const ConnectedControlledVocab = stripes.connect(ControlledVocab);
- this.state = {
- institutionId: null,
- campusId: null,
- };
- }
+ const { institutions } = useInstitutions({ searchParams: {
+ limit: 100,
+ query: 'cql.allRecords=1 sortby name',
+ } });
- /**
- * Refresh lookup tables when the component mounts. Fetches in the manifest
- * will only run once (in the constructor) but because this object may be
- * unmounted/remounted without being destroyed/recreated, the lookup tables
- * will be stale if they change between unmounting/remounting.
- */
- componentDidMount() {
- const institutionId = sessionStorage.getItem(INSTITUTION_ID_LIBRARIES);
- const campusId = sessionStorage.getItem(CAMPUS_ID_LIBRARIES);
- this.setState({ institutionId, campusId });
- ['institutions', 'campuses', 'locationsPerLibrary'].forEach(i => {
- this.props.mutator[i].reset();
- this.props.mutator[i].GET();
- });
- }
+ const { campuses } = useCampuses({ searchParams: {
+ limit: 100,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ const { locations } = useLocations({ searchParams: {
+ limit: 10000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ useEffect(() => {
+ sessionStorage.setItem(INSTITUTION_ID_LIBRARIES, institutionId);
+ sessionStorage.setItem(CAMPUS_ID_LIBRARIES, campusId);
+ }, [institutionId, campusId]);
- numberOfObjectsFormatter = (item) => {
- const records = (this.props.resources.locationsPerLibrary || {}).records || [];
- const numberOfObjects = records.reduce((count, loc) => {
- return loc.libraryId === item.id ? count + 1 : count;
- }, 0);
+ const numberOfObjectsFormatter = useCallback((item) => {
+ const numberOfObjects = locations.reduce((count, loc) => (loc.libraryId === item.id ? count + 1 : count), 0);
const onNumberOfObjectsClick = () => {
sessionStorage.setItem(LOCATION_LIBRARY_ID_KEY, item.id);
- sessionStorage.setItem(LOCATION_INSTITUTION_ID_KEY, this.state.institutionId);
- sessionStorage.setItem(LOCATION_CAMPUS_ID_KEY, this.state.campusId);
+ sessionStorage.setItem(LOCATION_INSTITUTION_ID_KEY, institutionId);
+ sessionStorage.setItem(LOCATION_CAMPUS_ID_KEY, campusId);
};
return (
@@ -133,124 +74,104 @@ class LocationLibraries extends React.Component {
to="./location-locations"
>
{numberOfObjects}
- );
- }
+
+ );
+ }, [locations, institutionId, campusId]);
- onChangeInstitution = (e) => {
+ const onChangeInstitution = useCallback((e) => {
const value = e.target.value;
- this.setState({ institutionId: value, campusId: null });
-
- sessionStorage.setItem(INSTITUTION_ID_LIBRARIES, value);
- sessionStorage.setItem(CAMPUS_ID_LIBRARIES, '');
- }
+ setInstitutionId(value);
+ setCampusId(null);
+ }, []);
- onChangeCampus = (e) => {
+ const onChangeCampus = useCallback((e) => {
const value = e.target.value;
- this.setState({ campusId: value });
-
- sessionStorage.setItem(CAMPUS_ID_LIBRARIES, value);
+ setCampusId(value);
+ }, []);
+
+ const institutionOptions = institutions.map(i => (
+
+ ));
+
+ const campusOptions = campuses.filter(c => c.institutionId === institutionId).map(c => (
+
+ ));
+
+ if (!institutionOptions.length) {
+ return ;
}
- render() {
- const { institutionId, campusId } = this.state;
- const { resources } = this.props;
-
- const institutions = get(resources, 'institutions.records', []).map(i => (
-
- ));
-
- if (!institutions.length) {
- return ;
- }
-
- const campuses = [];
-
- get(resources, 'campuses.records', []).forEach(c => {
- if (c.institutionId === institutionId) {
- campuses.push(
-
- );
- }
- });
-
- const formatter = {
- numberOfObjects: this.numberOfObjectsFormatter,
- };
-
- const filterBlock = (
- <>
+ const filterBlock = (
+ <>
+ }
+ id="institutionSelect"
+ name="institutionSelect"
+ onChange={onChangeInstitution}
+ value={institutionId}
+ >
+
+ {selectText => (
+
+ )}
+
+ {institutionOptions}
+
+ {institutionId && (
}
- id="institutionSelect"
- name="institutionSelect"
- onChange={this.onChangeInstitution}
- value={this.state.institutionId}
+ label={}
+ id="campusSelect"
+ name="campusSelect"
+ onChange={onChangeCampus}
+ value={campusId}
>
-
+
{selectText => (
-
+
)}
- {institutions}
+ {campusOptions}
- {institutionId &&
- }
- id="campusSelect"
- name="campusSelect"
- onChange={this.onChangeCampus}
- value={this.state.campusId}
- >
-
- {selectText => (
-
- )}
-
- {campuses}
-
- }
- >
- );
-
- return (
-
- from being overwritten by the props.resources here.
- dataKey={undefined}
- baseUrl="location-units/libraries"
- records="loclibs"
- rowFilter={filterBlock}
- rowFilterFunction={(row) => row.campusId === campusId}
- label={this.props.intl.formatMessage({ id: 'ui-tenant-settings.settings.location.libraries' })}
- translations={translations}
- objectLabel={}
- visibleFields={['name', 'code']}
- columnMapping={{
- name: ,
- code: ,
- }}
- formatter={formatter}
- nameKey="group"
- id="libraries"
- preCreateHook={(item) => ({ ...item, campusId })}
- listSuppressor={() => !(institutionId && campusId)}
- listSuppressorText={}
- sortby="name"
- validate={composeValidators(locationCodeValidator.validate)}
- editable={this.hasAllLocationPerms}
- canCreate={this.hasAllLocationPerms}
- />
-
- );
- }
-}
+ )}
+ >
+ );
+
+ return (
+
+ row.campusId === campusId}
+ label={formatMessage({ id: 'ui-tenant-settings.settings.location.libraries' })}
+ translations={translations}
+ objectLabel={}
+ visibleFields={['name', 'code']}
+ columnMapping={{
+ name: ,
+ code: ,
+ }}
+ formatter={{ numberOfObjects: numberOfObjectsFormatter }}
+ nameKey="group"
+ id="libraries"
+ preCreateHook={(item) => ({ ...item, campusId })}
+ listSuppressor={() => !(institutionId && campusId)}
+ listSuppressorText={}
+ sortby="name"
+ validate={composeValidators(locationCodeValidator.validate)}
+ editable={hasAllLocationPerms}
+ canCreate={hasAllLocationPerms}
+ />
+
+ );
+};
-export default injectIntl(LocationLibraries);
+export default LocationLibraries;
diff --git a/src/settings/LocationLibraries.test.js b/src/settings/LocationLibraries.test.js
index faf18275..de698ca5 100644
--- a/src/settings/LocationLibraries.test.js
+++ b/src/settings/LocationLibraries.test.js
@@ -1,9 +1,12 @@
import React from 'react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { QueryClient, QueryClientProvider } from 'react-query';
-import LocacationLibraries from './LocationLibraries';
+import LocationLibraries from './LocationLibraries';
import { renderWithRouter } from '../../test/jest/helpers';
+import { useInstitutions } from '../hooks/useInstitutions';
+
jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn(({
rowFilter,
@@ -36,39 +39,40 @@ jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn((
>
)));
-const stripesMock = {
- connect: component => component,
- hasPerm: jest.fn().mockResolvedValue(true),
- config: {
- platform: 'tenant-settings'
- },
-};
-
-const mutatorMock = {
- locationsPerLibrary: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- institutions: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- campuses: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
-};
-
-const resourcesMock = {
- institutions: {
- records: [
- { code: 'KU',
+jest.mock('../hooks/useCampuses', () => ({
+ useCampuses: jest.fn(() => ({
+ campuses: [
+ {
+ code: 'CC',
+ id: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
+ institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ name: 'City Campus',
+ },
+ {
+ code: 'E',
+ id: '470ff1dd-937a-4195-bf9e-06bcfcd135df',
+ institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ name: 'Online'
+ }
+ ],
+ })),
+}));
+
+jest.mock('../hooks/useInstitutions', () => ({
+ useInstitutions: jest.fn(() => ({
+ institutions: [
+ {
+ code: 'KU',
id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'Københavns Universitet' }
+ name: 'Københavns Universitet'
+ }
]
- },
- locationsPerLibrary: {
- records: [
+ })),
+}));
+
+jest.mock('../hooks/useLocations', () => ({
+ useLocations: jest.fn(() => ({
+ locations: [
{
campusId: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
code: 'KU/CC/DI/A',
@@ -87,49 +91,36 @@ const resourcesMock = {
name: 'Dematic',
}
]
- },
- campuses: {
- records: [{
- code: 'CC',
- id: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
- institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'City Campus',
- },
- {
- code: 'E',
- id: '470ff1dd-937a-4195-bf9e-06bcfcd135df',
- institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'Online'
- }
- ]
- },
-};
-
-const renderLocationLibaries = (resources = {}) => renderWithRouter(
-
+ })),
+}));
+
+
+const renderLocationLibaries = () => renderWithRouter(
+
+
+
);
describe('LocationLibraries', () => {
it('should render LocationLibraries with empty resources', async () => {
+ useInstitutions.mockImplementationOnce(() => ({ institutions: [] }));
+
renderLocationLibaries();
expect(screen.getByTestId('libraries-empty')).toBeVisible();
});
+
it('should render LocationLibraries with resourses', async () => {
- renderLocationLibaries(resourcesMock);
+ renderLocationLibaries();
expect(renderLocationLibaries).toBeDefined();
});
it('should render LocationLibraries changed option value', async () => {
- renderLocationLibaries(resourcesMock);
+ renderLocationLibaries();
- const checkboxInstitution = screen.getByRole('combobox');
+ const checkboxInstitution = screen.getAllByRole('combobox')[0];
await userEvent.selectOptions(checkboxInstitution, 'Københavns Universitet (KU)');
@@ -139,7 +130,7 @@ describe('LocationLibraries', () => {
});
it('should render LocationLibraries with filled form', async () => {
- renderLocationLibaries(resourcesMock);
+ renderLocationLibaries();
const checkboxInstitution = screen.getByRole('combobox', { name: 'ui-tenant-settings.settings.location.institutions.institution' });
diff --git a/src/settings/LocationLocations/LocationDetail.js b/src/settings/LocationLocations/LocationDetail.js
index d54ee7b0..bab2ca1d 100644
--- a/src/settings/LocationLocations/LocationDetail.js
+++ b/src/settings/LocationLocations/LocationDetail.js
@@ -1,4 +1,4 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import {
@@ -20,21 +20,21 @@ import {
} from '@folio/stripes/components';
import { ViewMetaData } from '@folio/stripes/smart-components';
import {
- stripesConnect,
- IfPermission, useStripes, TitleManager,
+ IfPermission,
+ useStripes,
+ TitleManager,
} from '@folio/stripes/core';
import LocationInUseModal from './LocationInUseModal';
import { useRemoteStorageApi } from './RemoteStorage';
import RemoteStorageDetails from './RemoteStorageDetails';
+import { useCampusDetails } from '../../hooks/useCampusDetails';
+import { useInstitutionDetails } from '../../hooks/useInstitutionDetails';
+import { useLibraryDetails } from '../../hooks/useLibraryDetails';
+
const LocationDetail = ({
initialValues: loc,
- resources: {
- institutions,
- campuses,
- libraries,
- },
servicePointsById,
onEdit,
onClone,
@@ -51,11 +51,16 @@ const LocationDetail = ({
const [isDeleteLocationModalOpened, setIsDeleteLocationModalOpened] = useState(false);
const [isLocationInUseModalOpened, setIsLocationInUseModalOpened] = useState(false);
+ const { campus } = useCampusDetails({ id: loc.campusId });
+ const { institution } = useInstitutionDetails({ id: loc.institutionId });
+ const { library } = useLibraryDetails({ id: loc.libraryId });
const { setMapping } = useRemoteStorageApi();
- const handleExpandAll = setSections;
+ const handleExpandAll = (newSections) => {
+ setSections(newSections);
+ };
- const renderServicePoint = useCallback((sp, index) => {
+ const renderServicePoint = (sp, index) => {
return (
index === 0 ?
@@ -65,9 +70,9 @@ const LocationDetail = ({
{sp}
);
- }, []);
+ };
- const renderServicePoints = useCallback(() => {
+ const renderServicePoints = () => {
const itemsList = [];
// as primary servicePoint surely exists and servicePointsById shouldn't be empty, its index would be at the 0th position of itemsList array
if (!isEmpty(servicePointsById) && loc.servicePointIds.length !== 0) {
@@ -85,19 +90,19 @@ const LocationDetail = ({
isEmptyMessage="No servicePoints found"
/>
);
- }, [loc, renderServicePoint, servicePointsById]);
+ };
- const handleSectionToggle = useCallback(({ id }) => {
+ const handleSectionToggle = ({ id }) => {
setSections(prevSections => ({ ...prevSections, [id]: !prevSections[id] }));
- }, []);
+ };
- const toggleDeleteLocationConfirmation = useCallback(() => {
+ const toggleDeleteLocationConfirmation = () => {
setIsDeleteLocationModalOpened(prevState => !prevState);
- }, []);
+ };
- const toggleLocationInUseModal = useCallback(() => {
+ const toggleLocationInUseModal = () => {
setIsLocationInUseModalOpened(prevState => !prevState);
- }, []);
+ };
const removeLocation = () => {
toggleDeleteLocationConfirmation();
@@ -112,6 +117,7 @@ const LocationDetail = ({
});
};
+ // eslint-disable-next-line react/prop-types
const renderActionMenu = item => ({ onToggle }) => {
if (!hasAllLocationPerms) return null;
@@ -162,15 +168,6 @@ const LocationDetail = ({
);
};
- const institutionList = institutions?.records || [];
- const institution = institutionList.length === 1 ? institutionList[0] : null;
-
- const campusList = campuses?.records || [];
- const campus = campusList.length === 1 ? campusList[0] : null;
-
- const libraryList = libraries?.records || [];
- const library = libraryList.length === 1 ? libraryList[0] : null;
-
// massage the "details" property which is represented in the API as
// an object but displayed on the details page as an array of
// key-value pairs sorted by key.
@@ -222,7 +219,7 @@ const LocationDetail = ({
- }
+ }
{
- isDeleteLocationModalOpened && (
- }
- message={confirmationMessage}
- onConfirm={removeLocation}
- onCancel={toggleDeleteLocationConfirmation}
- confirmLabel={}
- />
- )
- }
+ isDeleteLocationModalOpened && (
+ }
+ message={confirmationMessage}
+ onConfirm={removeLocation}
+ onCancel={toggleDeleteLocationConfirmation}
+ confirmLabel={}
+ />
+ )
+ }
{
- isLocationInUseModalOpened && (
-
- )
- }
+ isLocationInUseModalOpened && (
+
+ )
+ }
);
};
-LocationDetail.manifest = Object.freeze({
- institutions: {
- type: 'okapi',
- path: 'location-units/institutions/!{initialValues.institutionId}',
- },
- campuses: {
- type: 'okapi',
- path: 'location-units/campuses/!{initialValues.campusId}',
- },
- libraries: {
- type: 'okapi',
- path: 'location-units/libraries/!{initialValues.libraryId}',
- },
-});
-
LocationDetail.propTypes = {
- stripes: PropTypes.shape({
- connect: PropTypes.func.isRequired,
- }).isRequired,
initialValues: PropTypes.object,
- resources: PropTypes.shape({
- institutions: PropTypes.object,
- campuses: PropTypes.object,
- libraries: PropTypes.object,
- }).isRequired,
+ institutions: PropTypes.arrayOf(PropTypes.object),
+ campuses: PropTypes.arrayOf(PropTypes.object),
+ libraries: PropTypes.arrayOf(PropTypes.object),
servicePointsById: PropTypes.object,
onEdit: PropTypes.func.isRequired,
onClone: PropTypes.func.isRequired,
@@ -363,4 +340,4 @@ LocationDetail.propTypes = {
onRemove: PropTypes.func.isRequired,
};
-export default stripesConnect(LocationDetail);
+export default LocationDetail;
diff --git a/src/settings/LocationLocations/LocationForm/DetailsField.js b/src/settings/LocationLocations/LocationForm/DetailsField.js
index 1628701e..1c2f3622 100644
--- a/src/settings/LocationLocations/LocationForm/DetailsField.js
+++ b/src/settings/LocationLocations/LocationForm/DetailsField.js
@@ -1,6 +1,5 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
-import PropTypes from 'prop-types';
import {
AutoSuggest,
@@ -9,31 +8,16 @@ import {
} from '@folio/stripes/components';
import RepeatableField from '../../../components/RepeatableField';
+import { useLocations } from '../../../hooks/useLocations';
-class DetailsField extends React.Component {
- static manifest = {
- locations: {
- type: 'okapi',
- path: 'locations?query=(details=*)',
- },
- };
-
- static propTypes = {
- resources: PropTypes.shape({
- locations: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object),
- }),
- }).isRequired,
- };
- constructor(props) {
- super(props);
- this.getSuggestedTerms = this.getSuggestedTerms.bind(this);
- }
+const DetailsField = () => {
+ const { locations } = useLocations({ searchParams: {
+ query: '(details=*)'
+ } });
- getSuggestedTerms(locationsArray) {
+ const getSuggestedTerms = (locationsArray) => {
const terms = [];
- // eslint-disable-next-line no-unused-vars
for (const item of locationsArray) {
if (item.details) {
Object.keys(item.details).forEach(name => {
@@ -44,44 +28,39 @@ class DetailsField extends React.Component {
}
}
return terms;
- }
-
- render() {
- const { locations } = this.props.resources;
- const locationsArray = locations ? locations.records[0] ? locations.records[0].locations : [] : [];
- const suggestedTerms = this.getSuggestedTerms(locationsArray);
- const detailNames = suggestedTerms.length > 0 ? suggestedTerms.map(locationName => (
- { value: locationName })) : [];
+ };
- return (
-
-
-
- }
- addButtonId="clickable-add-location-details"
- template={[
- {
- name: 'name',
- label: ,
- component: AutoSuggest,
- items: detailNames,
- renderValue: item => item || '',
- withFinalForm: true,
- },
- {
- name: 'value',
- label: ,
- component: TextField,
- },
- ]}
- newItemTemplate={{ name: '', value: '' }}
- />
- );
- }
-}
+ const suggestedTerms = getSuggestedTerms(locations);
+ const detailNames = useMemo(() => (suggestedTerms.length > 0 ? suggestedTerms.map(locationName => (
+ { value: locationName })) : []), [suggestedTerms]);
+ return (
+
+
+
+ }
+ addButtonId="clickable-add-location-details"
+ template={[
+ {
+ name: 'name',
+ label: ,
+ component: AutoSuggest,
+ items: detailNames,
+ renderValue: item => item || '',
+ withFinalForm: true,
+ },
+ {
+ name: 'value',
+ label: ,
+ component: TextField,
+ },
+ ]}
+ newItemTemplate={{ name: '', value: '' }}
+ />
+ );
+};
export default DetailsField;
diff --git a/src/settings/LocationLocations/LocationForm/DetailsField.test.js b/src/settings/LocationLocations/LocationForm/DetailsField.test.js
index 943fbba6..90da0c3b 100644
--- a/src/settings/LocationLocations/LocationForm/DetailsField.test.js
+++ b/src/settings/LocationLocations/LocationForm/DetailsField.test.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { QueryClientProvider, QueryClient } from 'react-query';
import { render } from '@testing-library/react';
@@ -64,9 +65,11 @@ const renderDetailsField = () => render(
onSubmit={() => {}}
mutators={{ ...arrayMutators }}
render={() => (
-
+
+
+
)}
/>
);
diff --git a/src/settings/LocationLocations/LocationForm/LocationForm.js b/src/settings/LocationLocations/LocationForm/LocationForm.js
index e41e2960..b7049c1c 100644
--- a/src/settings/LocationLocations/LocationForm/LocationForm.js
+++ b/src/settings/LocationLocations/LocationForm/LocationForm.js
@@ -1,12 +1,13 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { cloneDeep, sortBy } from 'lodash';
import { Field } from 'react-final-form';
-import { FormattedMessage, injectIntl } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import {
TitleManager,
- withStripes,
+ useOkapiKy,
+ useStripes,
} from '@folio/stripes/core';
import {
Accordion,
@@ -32,65 +33,61 @@ import ServicePointsFields from './ServicePointsFields';
import DetailsField from './DetailsField';
import { RemoteStorageField } from './RemoteStorageField';
-class LocationForm extends React.Component {
- static propTypes = {
- stripes: PropTypes.shape({
- hasPerm: PropTypes.func.isRequired,
- connect: PropTypes.func.isRequired,
- }).isRequired,
- locationResources: PropTypes.shape({
- institutions: PropTypes.object,
- campuses: PropTypes.object,
- libraries: PropTypes.object,
- servicePoints: PropTypes.object,
- }),
- parentMutator: PropTypes.object.isRequired,
- initialValues: PropTypes.object,
- intl: PropTypes.object,
- handleSubmit: PropTypes.func.isRequired,
- onCancel: PropTypes.func,
- pristine: PropTypes.bool,
- submitting: PropTypes.bool,
- cloning: PropTypes.bool,
- form: PropTypes.object.isRequired,
- };
- constructor(props) {
- super(props);
+const LocationForm = ({
+ handleSubmit,
+ initialValues,
+ form,
+ onCancel,
+ pristine,
+ submitting,
+ cloning,
+ institutions,
+ campuses,
+ libraries,
+ servicePoints,
+ checkLocationHasHoldingsOrItems,
+}) => {
+ const stripes = useStripes();
+ const ky = useOkapiKy();
+ const { formatMessage } = useIntl();
- this.handleExpandAll = this.handleExpandAll.bind(this);
- this.handleSectionToggle = this.handleSectionToggle.bind(this);
- this.cViewMetaData = props.stripes.connect(ViewMetaData);
- this.cDetailsField = props.stripes.connect(DetailsField);
+ const [sections, setSections] = useState({
+ generalSection: true,
+ detailsSection: true,
+ });
- this.state = {
- sections: {
- generalSection: true,
- detailsSection: true,
- },
- };
- }
+ const mappedInstitutions = institutions.map(i => ({ value: i.id, label: `${i.name} ${i.code ? `(${i.code})` : ''}` }));
+ const mappedServicePoints = sortBy(servicePoints, ['name']).map(i => ({ label: `${i.name}` }));
- addFirstMenu() {
- return (
-
-
- {ariaLabel => (
-
- )}
-
-
- );
- }
+ const handleExpandAll = (newSections) => {
+ setSections(newSections);
+ };
+
+ const handleSectionToggle = ({ id }) => {
+ setSections((curState) => {
+ const newState = cloneDeep(curState);
+ newState[id] = !newState[id];
+ return newState;
+ });
+ };
- renderFooter() {
- const { pristine, submitting, cloning, onCancel } = this.props;
+ const addFirstMenu = () => (
+
+
+ {ariaLabel => (
+
+ )}
+
+
+ );
+ const renderFooter = () => {
const closeButton = (
);
- }
-
- handleSectionToggle({ id }) {
- this.setState((curState) => {
- const newState = cloneDeep(curState);
- newState.sections[id] = !newState.sections[id];
- return newState;
- });
- }
-
- handleExpandAll(sections) {
- this.setState((curState) => {
- const newState = cloneDeep(curState);
- newState.sections = sections;
- return newState;
- });
- }
+ };
- renderPaneTitle() {
- const { initialValues } = this.props;
+ const renderPaneTitle = () => {
const loc = initialValues || {};
if (loc.id) {
@@ -151,258 +131,222 @@ class LocationForm extends React.Component {
}
return ;
- }
-
- render() {
- const {
- stripes,
- handleSubmit,
- initialValues,
- locationResources,
- intl: { formatMessage },
- form,
- parentMutator
- } = this.props;
- const loc = initialValues || {};
- const { sections } = this.state;
- const disabled = !stripes.hasPerm('settings.tenant-settings.enabled');
-
- const institutions = [];
- ((locationResources.institutions || {}).records || []).forEach(i => {
- institutions.push({ value: i.id, label: `${i.name} ${i.code ? `(${i.code})` : ''}` });
- });
-
- const servicePoints = [];
- const entryList = sortBy((locationResources.servicePoints || {}).records || [], ['name']);
- entryList.forEach(i => {
- servicePoints.push({ label: `${i.name}` });
- });
+ };
- const formValues = form.getState().values;
+ const formValues = form.getState().values;
- const titleManagerLabel = initialValues?.name && initialValues?.id ? formatMessage({ id:'ui-tenant-settings.settings.items.edit.title' }, { item: initialValues?.name })
- :
- formatMessage({ id:'ui-tenant-settings.settings.location.createNew.title' });
+ const titleManagerLabel = initialValues?.name && initialValues?.id
+ ? formatMessage({ id:'ui-tenant-settings.settings.items.edit.title' }, { item: initialValues?.name })
+ : formatMessage({ id:'ui-tenant-settings.settings.location.createNew.title' });
- return (
-
-
- );
- }
-}
+
+ { label => (
+
+ )}
+
+
+ { label => (
+
+ )}
+
+
+
+
+
+
+ }
+ name="description"
+ id="input-location-description"
+ component={TextArea}
+ fullWidth
+ disabled={!stripes.hasPerm('settings.tenant-settings.enabled')}
+ />
+
+
+
+ }
+ >
+
+
+
+
+
+
+ );
+};
+LocationForm.propTypes = {
+ institutions: PropTypes.arrayOf(PropTypes.object),
+ campuses: PropTypes.arrayOf(PropTypes.object),
+ libraries: PropTypes.arrayOf(PropTypes.object),
+ servicePoints: PropTypes.arrayOf(PropTypes.object),
+ initialValues: PropTypes.object,
+ handleSubmit: PropTypes.func.isRequired,
+ onCancel: PropTypes.func,
+ pristine: PropTypes.bool,
+ submitting: PropTypes.bool,
+ cloning: PropTypes.bool,
+ form: PropTypes.object.isRequired,
+ checkLocationHasHoldingsOrItems: PropTypes.func
+};
export default stripesFinalForm({
navigationCheck: true,
@@ -425,4 +369,4 @@ export default stripesFinalForm({
},
validate,
validateOnBlur: true,
-})(withStripes(injectIntl(LocationForm)));
+})(LocationForm);
diff --git a/src/settings/LocationLocations/LocationForm/LocationFormContainer.js b/src/settings/LocationLocations/LocationForm/LocationFormContainer.js
index e1d0adfd..2853148c 100644
--- a/src/settings/LocationLocations/LocationForm/LocationFormContainer.js
+++ b/src/settings/LocationLocations/LocationForm/LocationFormContainer.js
@@ -1,31 +1,26 @@
-import React, {
- useState,
- useEffect,
- useCallback,
- useContext,
-} from 'react';
+import React, { useContext } from 'react';
import { cloneDeep } from 'lodash';
import { FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+import { useQueryClient } from 'react-query';
import { CalloutContext } from '@folio/stripes/core';
-import PropTypes from 'prop-types';
import LocationForm from './LocationForm';
import { useRemoteStorageApi } from '../RemoteStorage';
+import { useLocationCreate } from '../../../hooks/useLocationCreate';
+import { useLocationUpdate } from '../../../hooks/useLocationUpdate';
+import { LOCATIONS } from '../../../hooks/useLocations';
+import { SERVICE_POINTS } from '../../../hooks/useServicePoints';
const LocationFormContainer = ({
onSave,
servicePointsByName,
initialValues: location,
- parentMutator,
...rest
}) => {
- const [initialValues, setInitialValues] = useState(location);
-
- useEffect(() => {
- setInitialValues(location);
- }, [location?.id]);
+ const queryClient = useQueryClient();
const callout = useContext(CalloutContext);
@@ -38,13 +33,27 @@ const LocationFormContainer = ({
const { setMapping } = useRemoteStorageApi();
+ const { createLocation } = useLocationCreate({
+ onSuccess: () => {
+ queryClient.invalidateQueries(SERVICE_POINTS);
+ queryClient.invalidateQueries(LOCATIONS);
+ },
+ });
+
+ const { updateLocation } = useLocationUpdate({
+ onSuccess: () => {
+ queryClient.invalidateQueries(SERVICE_POINTS);
+ queryClient.invalidateQueries(LOCATIONS);
+ },
+ });
+
const initiateSetMapping = (...args) => setMapping(...args).catch(showSubmitErrorCallout);
const saveData = async (formData) => {
const { remoteId: configurationId, ...locationData } = formData;
if (locationData.id === undefined) {
- const newLocation = await parentMutator.entries.POST(locationData);
+ const newLocation = await createLocation({ data: locationData });
initiateSetMapping({ folioLocationId: newLocation?.id, configurationId });
return newLocation;
@@ -52,10 +61,11 @@ const LocationFormContainer = ({
initiateSetMapping({ folioLocationId: locationData.id, configurationId });
- return parentMutator.entries.PUT(locationData);
+ return updateLocation({ locationId: locationData.id, data: locationData })
+ .then(() => locationData);
};
- const saveLocation = useCallback((updatedLocation) => {
+ const saveLocation = (updatedLocation) => {
const data = cloneDeep(updatedLocation);
const servicePointsObject = {};
@@ -83,13 +93,12 @@ const LocationFormContainer = ({
saveData(data)
.then(onSave)
.catch(showSubmitErrorCallout);
- }, [onSave, servicePointsByName, saveData]);
+ };
return (
);
@@ -101,12 +110,6 @@ LocationFormContainer.propTypes = {
initialValues: PropTypes.shape({
id: PropTypes.string,
}),
- parentMutator: PropTypes.shape({
- entries: PropTypes.shape({
- POST: PropTypes.func,
- PUT: PropTypes.func,
- })
- }),
};
export default LocationFormContainer;
diff --git a/src/settings/LocationLocations/LocationForm/LocationFormContainer.test.js b/src/settings/LocationLocations/LocationForm/LocationFormContainer.test.js
index 6d9dcd06..47f766dd 100644
--- a/src/settings/LocationLocations/LocationForm/LocationFormContainer.test.js
+++ b/src/settings/LocationLocations/LocationForm/LocationFormContainer.test.js
@@ -1,13 +1,92 @@
import React from 'react';
-
+import { QueryClient, QueryClientProvider } from 'react-query';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '../../../../test/jest/__mocks__';
-import buildStripes from '../../../../test/jest/__new_mocks__/stripesCore.mock';
import { renderWithRouter } from '../../../../test/jest/helpers';
import LocationFormContainer from './LocationFormContainer';
+import { mockUseOkapiKy } from '../../../../test/jest/__mocks__/stripesCore.mock';
+
+
+const mockCreateLocation = jest.fn(() => Promise.resolve());
+const mockUpdateLocation = jest.fn(() => Promise.resolve());
+
+
+jest.mock('../../../hooks/useLocationCreate', () => ({
+ useLocationCreate: jest.fn(() => ({
+ createLocation: mockCreateLocation,
+ isCreatingLocation: false,
+ })),
+}));
+
+jest.mock('../../../hooks/useLocationUpdate', () => ({
+ useLocationUpdate: jest.fn(() => ({
+ updateLocation: mockUpdateLocation,
+ isUpdatingLocation: false,
+ })),
+}));
+
+
+const institutions = [
+ {
+ code: 'KU',
+ id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ metadata: { createdDate: '2021-11-08T03:24:13.021+00:00', updatedDate: '2021-11-08T03:24:13.021+00:00' },
+ name: 'Københavns Universitet',
+ }
+];
+
+const campuses = [
+ {
+ code: 'DI',
+ id: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
+ institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ name: 'City Campus',
+ },
+ {
+ code: 'E',
+ id: '470ff1dd-937a-4195-bf9e-06bcfcd135df',
+ institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ name: 'Online',
+ }
+];
+
+const libraries = [
+ {
+ campusId: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
+ code: 'DI',
+ id: '5d78803e-ca04-4b4a-aeae-2c63b924518b',
+ name: 'Datalogisk Institut'
+ }
+];
+
+const servicePoints = [
+ {
+ code: 'cd1',
+ discoveryDisplayName: 'Circulation Desk -- Hallway',
+ holdShelfExpiryPeriod: { duration: 3, intervalId: 'Weeks' },
+ id: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
+ name: 'Circ Desk 1',
+ pickupLocation: true,
+ staffSlips: [],
+ },
+ {
+ code: 'cd2',
+ discoveryDisplayName: 'Circulation Desk -- Back Entrance',
+ holdShelfExpiryPeriod: {
+ duration: 5,
+ intervalId: 'Days',
+ },
+
+ id: 'c4c90014-c8c9-4ade-8f24-b5e313319f4b',
+ name: 'Circ Desk 2',
+ pickupLocation: true,
+ staffSlips: []
+ }
+];
+
jest.mock('./DetailsField', () => {
return () => DetaolsField;
@@ -18,29 +97,18 @@ const mockSetMapping = jest.fn().mockResolvedValue(true);
jest.mock('../RemoteStorage/Provider', () => ({
...jest.requireActual('../RemoteStorage/Provider'),
useRemoteStorageApi: () => ({
- remoteMap: {
- },
- mappings: {
- failed: false,
- hasLoaded: true,
- isPending: false,
- records: [
- {
- configurationId: 'de17bad7-2a30-4f1c-bee5-f653ded15629',
- folioLocationId: '53cf956f-c1df-410b-8bea-27f712cca7c0'
- },
- {
- configurationId: 'de17bad7-2a30-4f1c-bee5-f653ded15629',
- folioLocationId: 'c0762159-8fe3-4cbc-ae64-fa274f7acc47'
- }
- ],
- },
- configurations: {
- failed: false,
- hasLoaded: true,
- isPending: false,
- records: [],
- },
+ remoteMap: {},
+ mappings: [
+ {
+ configurationId: 'de17bad7-2a30-4f1c-bee5-f653ded15629',
+ folioLocationId: '53cf956f-c1df-410b-8bea-27f712cca7c0'
+ },
+ {
+ configurationId: 'de17bad7-2a30-4f1c-bee5-f653ded15629',
+ folioLocationId: 'c0762159-8fe3-4cbc-ae64-fa274f7acc47'
+ }
+ ],
+ configurations: [],
translate: () => 'str',
setMapping: mockSetMapping
})
@@ -48,8 +116,6 @@ jest.mock('../RemoteStorage/Provider', () => ({
const onSaveMock = jest.fn();
-const STRIPES = buildStripes();
-
const initialValuesMock = {
id: '1',
name: 'Initial Value',
@@ -69,6 +135,7 @@ const initialValuesMock = {
}
]
};
+
const servicePointsByNameMock = {
CircDesk1: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
CircDesk2: 'c4c90014-c8c9-4ade-8f24-b5e313319f4b',
@@ -80,145 +147,28 @@ const restPropsMock = {
pristine: false,
submitting: false,
cloning: true,
- stripes: STRIPES,
- locationResources: {
- campuses: {
- records: [
- {
- code: 'DI',
- id: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
- institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'City Campus',
- },
- {
- code: 'E',
- id: '470ff1dd-937a-4195-bf9e-06bcfcd135df',
- institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'Online',
- }
- ]
- },
- entries: {
- records: [{
- campusId: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
- code: 'KU/CC/DI/A',
- id: '53cf956f-c1df-410b-8bea-27f712cca7c0',
- institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- isActive: true,
- libraryId: '5d78803e-ca04-4b4a-aeae-2c63b924518b',
- name: 'Annex',
- primaryServicePoint: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
- servicePointIds: ['3a40852d-49fd-4df2-a1f9-6e2641a6e91f'],
- servicePoints: [],
- }
- ]
- },
- institutions: {
- records: [
- {
- code: 'KU',
- id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- metadata: { createdDate: '2021-11-08T03:24:13.021+00:00', updatedDate: '2021-11-08T03:24:13.021+00:00' },
- name: 'Københavns Universitet',
- }
- ]
- },
- libraries: {
- dataKey: 'location-locations',
- hasLoaded: true,
- records: [
- {
- campusId: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
- code: 'DI',
- id: '5d78803e-ca04-4b4a-aeae-2c63b924518b',
- name: 'Datalogisk Institut'
- }
- ]
- },
- servicePoints: {
- records: [
- {
- code: 'cd1',
- discoveryDisplayName: 'Circulation Desk -- Hallway',
- holdShelfExpiryPeriod: { duration: 3, intervalId: 'Weeks' },
- id: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
- name: 'Circ Desk 1',
- pickupLocation: true,
- staffSlips: [],
- },
- {
- code: 'cd2',
- discoveryDisplayName: 'Circulation Desk -- Back Entrance',
- holdShelfExpiryPeriod: {
- duration: 5,
- intervalId: 'Days',
- },
-
- id: 'c4c90014-c8c9-4ade-8f24-b5e313319f4b',
- name: 'Circ Desk 2',
- pickupLocation: true,
- staffSlips: []
- }
- ]
- },
- locations: {
- records: []
- }
- }
-};
-
-const mutatorMock = {
- servicePoints: {
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- DELETE: jest.fn(() => Promise.resolve()),
- },
- institutions: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- campuses: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- libraries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- holdingsEntries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- itemEntries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- entries: {
- DELETE: jest.fn(() => Promise.resolve()),
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- },
- uniquenessValidator: {
- DELETE: jest.fn(() => Promise.resolve()),
- GET: jest.fn(() => Promise.resolve()),
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- cancel: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- }
};
const renderLocationFormContainer = () => renderWithRouter(
-
+
+
+
);
describe('LocationFormContainer', () => {
+ mockUseOkapiKy.mockImplementation(() => ({
+ get: jest.fn(() => ({ json: jest.fn().mockResolvedValue(Promise.resolve()) })),
+ }));
+
it('should render ServicePointManager titles', () => {
renderLocationFormContainer();
diff --git a/src/settings/LocationLocations/LocationForm/RemoteStorageField.js b/src/settings/LocationLocations/LocationForm/RemoteStorageField.js
index 143d06fa..2a03c3f8 100644
--- a/src/settings/LocationLocations/LocationForm/RemoteStorageField.js
+++ b/src/settings/LocationLocations/LocationForm/RemoteStorageField.js
@@ -1,11 +1,12 @@
import React, { useState, useEffect, useMemo } from 'react';
+import PropTypes from 'prop-types';
import { useField } from 'react-final-form';
import { useStripes } from '@folio/stripes/core';
-import PropTypes from 'prop-types';
import { Control, useRemoteStorageApi } from '../RemoteStorage';
+
export const RemoteStorageField = ({ initialValues, checkLocationHasHoldingsOrItems }) => {
const stripes = useStripes();
const noInterfaces = useMemo(
@@ -13,7 +14,7 @@ export const RemoteStorageField = ({ initialValues, checkLocationHasHoldingsOrIt
[stripes]
);
- const { remoteMap, mappings, translate: t } = useRemoteStorageApi();
+ const { remoteMap, isMappingsError, isMappingsLoading, translate: t } = useRemoteStorageApi();
const [isReadOnly, setIsReadOnly] = useState(true);
@@ -40,16 +41,14 @@ export const RemoteStorageField = ({ initialValues, checkLocationHasHoldingsOrIt
if (noInterfaces) return null;
- const message = (mappings.failed && t('failed')) || (mappings.isPending && t('loading')) || (isReadOnly && t('readonly'));
-
- const isDisabled = !mappings.hasLoaded;
+ const message = (isMappingsError && t('failed')) || (isMappingsLoading && t('loading')) || (isReadOnly && t('readonly'));
return (
diff --git a/src/settings/LocationLocations/LocationForm/ServicePointsFields.js b/src/settings/LocationLocations/LocationForm/ServicePointsFields.js
index 14bf5568..b94561ee 100644
--- a/src/settings/LocationLocations/LocationForm/ServicePointsFields.js
+++ b/src/settings/LocationLocations/LocationForm/ServicePointsFields.js
@@ -4,6 +4,7 @@ import { Field } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
import PropTypes from 'prop-types';
import { sortBy, cloneDeep, findIndex } from 'lodash';
+
import {
Icon,
Layout,
@@ -11,8 +12,10 @@ import {
RepeatableField,
Select,
} from '@folio/stripes/components';
+
import css from './ServicePointsFields.css';
+
const omitUsedOptions = (list, usedValues, key, id) => {
const unUsedValues = cloneDeep(list);
if (usedValues) {
@@ -30,24 +33,10 @@ const omitUsedOptions = (list, usedValues, key, id) => {
return unUsedValues;
};
-class ServicePointsFields extends React.Component {
- static propTypes = {
- servicePoints: PropTypes.arrayOf(PropTypes.object),
- changePrimary: PropTypes.func.isRequired,
- formValues: PropTypes.object.isRequired,
- };
-
- constructor(props) {
- super(props);
- this.singlePrimary = this.singlePrimary.bind(this);
- this.renderFields = this.renderFields.bind(this);
- this.radioButtonComp = this.radioButtonComp.bind(this);
- this.list = {};
- }
-
- singlePrimary(id) {
- const { changePrimary, formValues } = this.props;
+const ServicePointsFields = ({ servicePoints, changePrimary, formValues }) => {
+ const list = omitUsedOptions(servicePoints, formValues.servicePointIds, 'selectSP');
+ const singlePrimary = (id) => {
formValues.servicePointIds.forEach((a, i) => {
if (i === id) {
changePrimary(i, true);
@@ -55,25 +44,22 @@ class ServicePointsFields extends React.Component {
changePrimary(i, false);
}
});
- }
+ };
- radioButtonComp({ input, ...props }) {
+ const radioButtonComp = ({ input, fieldIndex }) => {
return (
{ this.singlePrimary(props.fieldIndex); }}
+ onChange={() => { singlePrimary(fieldIndex); }}
checked={input.value}
name={input.name}
- aria-label={`servicePoint use as primary ${props.fieldIndex}`}
+ aria-label={`servicePoint use as primary ${fieldIndex}`}
/>
);
- }
-
- renderFields(field, index) {
- const { formValues } = this.props;
-
- this.list = omitUsedOptions(this.props.servicePoints, formValues.servicePointIds, 'selectSP', index);
+ };
- const sortedList = sortBy(this.list, ['label']);
+ const renderFields = (field, index) => {
+ const availableOptions = omitUsedOptions(servicePoints, formValues.servicePointIds, 'selectSP', index);
+ const sortedList = sortBy(availableOptions, ['label']);
const options = [{ label: 'Select service point', value: '' }, ...sortedList];
return (
@@ -95,52 +81,54 @@ class ServicePointsFields extends React.Component {
);
- }
-
- render() {
- const { formValues } = this.props;
+ };
- // make the last existing service point to be the primary one
- if (formValues.servicePointIds && formValues.servicePointIds.length === 1 && !formValues.servicePointIds[0].primary) {
- this.singlePrimary(0);
- }
+ // Make the last existing service point to be the primary one
+ if (formValues.servicePointIds && formValues.servicePointIds.length === 1 && !formValues.servicePointIds[0].primary) {
+ singlePrimary(0);
+ }
- const legend = (
-
-
-
- *
-
-
-
-
+ const legend = (
+
+
+
+ *
- );
+
+
+
+
+ );
- return (
- <>
- 1 ?
- Add service point :
- ''
- }
- legend={legend}
- emptyMessage={Location must have at least one service point}
- component={RepeatableField}
- name="servicePointIds"
- renderField={this.renderFields}
- />
- >
- );
- }
-}
+ return (
+ <>
+ 1 ?
+ Add service point :
+ ''
+ }
+ legend={legend}
+ emptyMessage={Location must have at least one service point}
+ component={RepeatableField}
+ name="servicePointIds"
+ renderField={renderFields}
+ />
+ >
+ );
+};
+
+ServicePointsFields.propTypes = {
+ servicePoints: PropTypes.arrayOf(PropTypes.object),
+ changePrimary: PropTypes.func.isRequired,
+ formValues: PropTypes.object.isRequired,
+};
export default ServicePointsFields;
diff --git a/src/settings/LocationLocations/LocationManager.js b/src/settings/LocationLocations/LocationManager.js
index 310f35a9..9ea9a294 100644
--- a/src/settings/LocationLocations/LocationManager.js
+++ b/src/settings/LocationLocations/LocationManager.js
@@ -1,20 +1,24 @@
-import React from 'react';
+import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
-import { Route } from 'react-router-dom';
-
+import {
+ Route,
+ useHistory,
+ useLocation,
+ useRouteMatch
+} from 'react-router-dom';
import {
FormattedMessage,
- injectIntl,
+ useIntl,
} from 'react-intl';
import {
cloneDeep,
find,
isEmpty,
omit,
- get,
forEach,
} from 'lodash';
import queryString from 'query-string';
+import { useQueryClient } from 'react-query';
import {
SearchAndSortQuery,
@@ -32,8 +36,12 @@ import {
Layer,
Callout,
} from '@folio/stripes/components';
+import {
+ TitleManager,
+ useOkapiKy,
+ useStripes
+} from '@folio/stripes/core';
-import { TitleManager } from '@folio/stripes/core';
import {
LOCATION_CAMPUS_ID_KEY,
LOCATION_INSTITUTION_ID_KEY,
@@ -43,327 +51,181 @@ import {
import LocationDetail from './LocationDetail';
import EditForm from './LocationForm';
import { RemoteStorageApiProvider } from './RemoteStorage';
-
-class LocationManager extends React.Component {
- static manifest = Object.freeze({
- entries: {
- type: 'okapi',
- records: 'locations',
- path: 'locations',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '3000',
- },
- },
- uniquenessValidator: {
- type: 'okapi',
- records: 'locations',
- accumulate: 'true',
- path: 'locations',
- fetch: false,
- },
- institutions: {
- type: 'okapi',
- path: 'location-units/institutions',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '1000',
- },
- records: 'locinsts',
- accumulate: true,
- },
- campuses: {
- type: 'okapi',
- path: 'location-units/campuses',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '1000',
- },
- records: 'loccamps',
- accumulate: true,
- },
- libraries: {
- type: 'okapi',
- path: 'location-units/libraries',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '1000',
- },
- records: 'loclibs',
- accumulate: true,
- },
- servicePoints: {
- type: 'okapi',
- records: 'servicepoints',
- path: 'service-points',
- params: {
- query: 'cql.allRecords=1 sortby name',
- limit: '1000',
- },
- resourceShouldRefresh: true,
- },
- holdingsEntries: {
- type: 'okapi',
- path: 'holdings-storage/holdings',
- records: 'holdingsRecords',
- accumulate: true,
- },
- itemEntries: {
- type: 'okapi',
- path: 'inventory/items',
- records: 'items',
- accumulate: true,
- },
- });
-
- static propTypes = {
- intl: PropTypes.object,
- label: PropTypes.node.isRequired,
- location: PropTypes.shape({
- search: PropTypes.string,
- pathname: PropTypes.string,
- }).isRequired,
- history: PropTypes.shape({ push: PropTypes.func.isRequired }).isRequired,
- match: PropTypes.shape({ path: PropTypes.string.isRequired }).isRequired,
- resources: PropTypes.shape({
- entries: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }),
- servicePoints: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object),
- hasLoaded: PropTypes.bool }),
- institutions: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }),
- campuses: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }),
- libraries: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }),
- }).isRequired,
- mutator: PropTypes.shape({
- entries: PropTypes.shape({
- POST: PropTypes.func,
- PUT: PropTypes.func,
- DELETE: PropTypes.func,
- }),
- servicePoints: PropTypes.shape({
- POST: PropTypes.func,
- PUT: PropTypes.func,
- DELETE: PropTypes.func,
- }),
- institutions: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- campuses: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- libraries: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- holdingsEntries: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- itemEntries: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- uniquenessValidator: PropTypes.object,
- }).isRequired,
- stripes: PropTypes.shape({
- hasPerm: PropTypes.func.isRequired,
- connect: PropTypes.func.isRequired,
- hasInterface: PropTypes.func.isRequired,
- }),
+import { useInstitutions } from '../../hooks/useInstitutions';
+import { useLibraries } from '../../hooks/useLibraries';
+import { useCampuses } from '../../hooks/useCampuses';
+import { SERVICE_POINTS, useServicePoints } from '../../hooks/useServicePoints';
+import { LOCATIONS, useLocations } from '../../hooks/useLocations';
+import { useLocationDelete } from '../../hooks/useLocationDelete';
+
+
+const initialSelectedLocationId = (location) => {
+ const idFromPathnameRe = '/([^/]+)$';
+ const reMatches = new RegExp(idFromPathnameRe).exec(location.pathname);
+ return reMatches ? reMatches[1] : null;
+};
+
+const initialSort = (location) => {
+ const { sort = 'name', sortDir = SORT_TYPES.ASCENDING } = queryString.parse(location.search.slice(1));
+ return { sort, sortDir };
+};
+
+const LocationManager = ({ label }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
+ const queryClient = useQueryClient();
+ const ky = useOkapiKy();
+ const callout = useRef(null);
+ const location = useLocation();
+ const history = useHistory();
+ const match = useRouteMatch();
+
+ const entryLabel = intl.formatMessage({ id: 'ui-tenant-settings.settings.location.locations.location' });
+ const locationListVisibleColumns = ['isActive', 'name', 'code'];
+ const locationListColumnMapping = {
+ isActive: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.locations.status' }),
+ name: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.locations.detailsName' }),
+ code: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.code' }),
};
-
- constructor(props) {
- super(props);
-
- this.state = {
- institutionId: sessionStorage.getItem(LOCATION_INSTITUTION_ID_KEY) || '',
- campusId: sessionStorage.getItem(LOCATION_CAMPUS_ID_KEY) || '',
- libraryId: sessionStorage.getItem(LOCATION_LIBRARY_ID_KEY) || '',
- servicePointsById: {},
- servicePointsByName: {},
- selectedId: this.initialSelectedLocationId,
- ...this.initialSort,
- };
-
- this.callout = React.createRef();
-
- const { formatMessage } = props.intl;
-
- this.entryLabel = formatMessage({ id: 'ui-tenant-settings.settings.location.locations.location' });
- this.locationListVisibleColumns = ['isActive', 'name', 'code'];
- this.locationListColumnMapping = {
- isActive: formatMessage({ id: 'ui-tenant-settings.settings.location.locations.status' }),
- name: formatMessage({ id: 'ui-tenant-settings.settings.location.locations.detailsName' }),
- code: formatMessage({ id: 'ui-tenant-settings.settings.location.code' }),
- };
- this.locationListFormatter = {
- isActive: item => {
- const locationId = item.isActive ? 'active' : 'inactive';
-
- return formatMessage({ id: `ui-tenant-settings.settings.location.locations.${locationId}` });
- }
- };
- this.hasAllLocationPerms = props.stripes.hasPerm('ui-tenant-settings.settings.location');
- }
-
- static getDerivedStateFromProps(nextProps) {
- const { resources } = nextProps;
- const servicePointsByName = {};
- if (resources.servicePoints && resources.servicePoints.hasLoaded) {
- const servicePointsById = ((resources.servicePoints || {}).records || []).reduce((map, item) => {
- map[item.id] = item.name;
- servicePointsByName[item.name] = item.id;
- return map;
- }, {});
- return { servicePointsById, servicePointsByName };
+ const locationListFormatter = {
+ isActive: item => {
+ const locationId = item.isActive ? 'active' : 'inactive';
+ return intl.formatMessage({ id: `ui-tenant-settings.settings.location.locations.${locationId}` });
}
- return null;
- }
-
- /**
- * Refresh lookup tables when the component mounts. Fetches in the manifest
- * will only run once (in the constructor) but because this object may be
- * unmounted/remounted without being destroyed/recreated, the lookup tables
- * will be stale if they change between unmounting/remounting.
- */
- componentDidMount() {
- ['institutions', 'campuses', 'libraries'].forEach(i => {
- this.props.mutator[i].reset();
- this.props.mutator[i].GET();
- });
- }
-
- get initialSelectedLocationId() {
- const { location } = this.props;
+ };
+ const hasAllLocationPerms = stripes.hasPerm('ui-tenant-settings.settings.location');
- const idFromPathnameRe = '/([^/]+)$';
- const reMatches = new RegExp(idFromPathnameRe).exec(location.pathname);
+ const showCalloutMessage = (name) => {
+ if (!callout.current) return;
+ const message = (
+
+ );
+ callout.current.sendCallout({ message });
+ };
- return reMatches ? reMatches[1] : null;
- }
+ const [institutionId, setInstitutionId] = useState(sessionStorage.getItem(LOCATION_INSTITUTION_ID_KEY) || '');
+ const [campusId, setCampusId] = useState(sessionStorage.getItem(LOCATION_CAMPUS_ID_KEY) || '');
+ const [libraryId, setLibraryId] = useState(sessionStorage.getItem(LOCATION_LIBRARY_ID_KEY) || '');
+ const [selectedId, setSelectedId] = useState(initialSelectedLocationId(location));
+ const [sortState, setSortState] = useState(initialSort(location));
+
+ const { institutions } = useInstitutions({ searchParams: {
+ limit: 100,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ const { libraries } = useLibraries({ searchParams: {
+ limit: 1000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ const { campuses } = useCampuses({ searchParams: {
+ limit: 1000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ const { servicePoints } = useServicePoints({ searchParams: {
+ limit: 1000,
+ query: 'cql.allRecords=1 sortby name',
+ } });
+
+ const { locations: locationEntries } = useLocations({ searchParams: {
+ limit: 3000,
+ query: 'cql.allRecords=1 sortby name'
+ } });
+
+ const { deleteLocation } = useLocationDelete({
+ onSuccess: () => {
+ queryClient.invalidateQueries(SERVICE_POINTS);
+ queryClient.invalidateQueries(LOCATIONS);
+ }
+ });
- get initialSort() {
- const { location: { search } } = this.props;
+ const { servicePointsById, servicePointsByName } = servicePoints.reduce((acc, item) => {
+ acc.servicePointsById[item.id] = item.name;
+ acc.servicePointsByName[item.name] = item.id;
- const {
- sort = 'name',
- sortDir = SORT_TYPES.ASCENDING,
- } = queryString.parse(search.slice(1));
+ return acc;
+ }, { servicePointsById: {}, servicePointsByName: {} });
- return {
- sort,
- sortDir,
- };
- }
+ const transitionToParams = (values) => {
+ const url = buildUrl(location, values);
+ history.push(url);
+ };
- onSort = (e, { name: fieldName }) => {
- const {
- sort,
- sortDir,
- } = this.state;
+ const formatLocationDisplayName = (loc) => {
+ return `${loc.name}${loc.code ? ` (${loc.code})` : ''}`;
+ };
+ const onSort = (e, { name: fieldName }) => {
+ const { sort, sortDir } = sortState;
const isSameField = sort === fieldName;
let newSortDir = SORT_TYPES.ASCENDING;
-
if (isSameField) {
newSortDir = newSortDir === sortDir ? SORT_TYPES.DESCENDING : newSortDir;
}
-
- const sortState = {
- sort: fieldName,
- sortDir: newSortDir,
- };
-
- this.setState(sortState);
- this.transitionToParams(sortState);
+ const newSortState = { sort: fieldName, sortDir: newSortDir };
+ setSortState(newSortState);
+ transitionToParams(newSortState);
};
- onSelectRow = (e, meta) => {
- const { match: { path } } = this.props;
-
- this.transitionToParams({ _path: `${path}/${meta.id}` });
- this.setState({ selectedId: meta.id });
+ const onSelectRow = (e, meta) => {
+ transitionToParams({ _path: `${match.path}/${meta.id}` });
+ setSelectedId(meta.id);
};
- transitionToParams(values) {
- const {
- location,
- history,
- } = this.props;
-
- const url = buildUrl(location, values);
-
- history.push(url);
- }
-
- onChangeInstitution = (e) => {
- const institutionId = e.target.value;
-
- sessionStorage.setItem(LOCATION_INSTITUTION_ID_KEY, institutionId);
+ const onChangeInstitution = (e) => {
+ const newInstitutionId = e.target.value;
+ sessionStorage.setItem(LOCATION_INSTITUTION_ID_KEY, newInstitutionId);
sessionStorage.setItem(LOCATION_CAMPUS_ID_KEY, '');
sessionStorage.setItem(LOCATION_LIBRARY_ID_KEY, '');
-
- this.setState({
- institutionId,
- campusId: '',
- libraryId: '',
- });
+ setInstitutionId(newInstitutionId);
+ setCampusId('');
+ setLibraryId('');
};
- onChangeCampus = (e) => {
- const campusId = e.target.value;
-
- sessionStorage.setItem(LOCATION_CAMPUS_ID_KEY, campusId);
-
- this.setState({
- campusId,
- libraryId: '',
- });
+ const onChangeCampus = (e) => {
+ const newCampusId = e.target.value;
+ sessionStorage.setItem(LOCATION_CAMPUS_ID_KEY, newCampusId);
+ setCampusId(newCampusId);
+ setLibraryId('');
};
- onChangeLibrary = (e) => {
- const libraryId = e.target.value;
-
- sessionStorage.setItem(LOCATION_LIBRARY_ID_KEY, libraryId);
-
- this.setState({ libraryId });
+ const onChangeLibrary = (e) => {
+ const newLibraryId = e.target.value;
+ sessionStorage.setItem(LOCATION_LIBRARY_ID_KEY, newLibraryId);
+ setLibraryId(newLibraryId);
};
- renderFilter() {
- const {
- resources,
- location,
- intl: { formatMessage },
- } = this.props;
- const {
- institutionId,
- campusId,
- libraryId,
- } = this.state;
-
- const institutions = get(resources.institutions, 'records', [])
+ const renderFilter = () => {
+ const formattedInstitutions = institutions
.map(institution => ({
value: institution.id,
- label: this.formatLocationDisplayName(institution),
+ label: formatLocationDisplayName(institution),
}));
- if (isEmpty(institutions)) {
+ if (isEmpty(formattedInstitutions)) {
return ;
}
- const campuses = get(resources.campuses, 'records', [])
+ const formattedCampuses = campuses
.filter(campus => campus.institutionId === institutionId)
.map(campus => ({
value: campus.id,
- label: this.formatLocationDisplayName(campus),
+ label: formatLocationDisplayName(campus),
}));
- const libraries = get(resources.libraries, 'records', [])
+ const formattedLibraries = libraries
.filter(library => library.campusId === campusId)
.map(library => ({
value: library.id,
- label: this.formatLocationDisplayName(library),
+ label: formatLocationDisplayName(library),
}));
return (
@@ -374,8 +236,8 @@ class LocationManager extends React.Component {
id="institutionSelect"
name="institutionSelect"
value={institutionId}
- dataOptions={[{ label: formatMessage({ id: 'ui-tenant-settings.settings.location.institutions.selectInstitution' }), value: '' }, ...institutions]}
- onChange={this.onChangeInstitution}
+ dataOptions={[{ label: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.institutions.selectInstitution' }), value: '' }, ...formattedInstitutions]}
+ onChange={onChangeInstitution}
/>
@@ -384,25 +246,25 @@ class LocationManager extends React.Component {
id="campusSelect"
name="campusSelect"
value={campusId}
- dataOptions={[{ label: formatMessage({ id: 'ui-tenant-settings.settings.location.campuses.selectCampus' }), value: '' }, ...campuses]}
- onChange={this.onChangeCampus}
+ dataOptions={[{ label: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.campuses.selectCampus' }), value: '' }, ...formattedCampuses]}
+ onChange={onChangeCampus}
/>}
{campusId && }
id="librarySelect"
- name="campusSelect"
+ name="librarySelect"
value={libraryId}
- dataOptions={[{ label: formatMessage({ id: 'ui-tenant-settings.settings.location.libraries.selectLibrary' }), value: '' }, ...libraries]}
- onChange={this.onChangeLibrary}
+ dataOptions={[{ label: intl.formatMessage({ id: 'ui-tenant-settings.settings.location.libraries.selectLibrary' }), value: '' }, ...formattedLibraries]}
+ onChange={onChangeLibrary}
/>}
- {this.hasAllLocationPerms && (
+ {hasAllLocationPerms && (
@@ -426,249 +288,186 @@ class LocationManager extends React.Component {
}
);
- }
-
- formatLocationDisplayName(location) {
- return `${location.name}${location.code ? ` (${location.code})` : ''}`;
- }
+ };
- parseInitialValues(loc, cloning = false) {
+ const parseInitialValues = (loc, cloning = false) => {
if (!loc) return loc;
-
- loc.detailsArray = Object.keys(loc.details || []).map(name => {
- return { name, value: loc.details[name] };
- }).sort();
-
+ loc.detailsArray = Object.keys(loc.details || []).map(name => ({ name, value: loc.details[name] })).sort();
return cloning ? omit(loc, 'id') : loc;
- }
-
- handleDetailClose = () => {
- this.transitionToParams({ _path: this.props.match.path });
- this.setState({ selectedId: null });
};
- prepareLocationsData() {
- const { resources } = this.props;
- const {
- sort,
- sortDir,
- } = this.state;
+ const handleDetailClose = () => {
+ transitionToParams({ _path: match.path });
+ setSelectedId(null);
+ };
+ const prepareLocationsData = () => {
+ const { sort, sortDir } = sortState;
const sortDirValue = sortDir === SORT_TYPES.ASCENDING ? 1 : -1;
-
- return cloneDeep((resources.entries || {}).records || []).map(location => {
- location.servicePointIds = (location.servicePointIds || []).map(id => ({
- selectSP: this.state.servicePointsById[id],
- primary: (location.primaryServicePoint === id),
+ return cloneDeep((locationEntries)).map(loc => {
+ loc.servicePointIds = (loc.servicePointIds || []).map(id => ({
+ selectSP: servicePointsById[id],
+ primary: (loc.primaryServicePoint === id),
}));
- return location;
+ return loc;
}).sort((a, b) => sortDirValue * `${a[sort]}`.localeCompare(`${b[sort]}`));
- }
+ };
- onCancel = (e) => {
+ const onCancel = (e) => {
if (e) {
e.preventDefault();
}
-
- this.transitionToParams({ layer: null });
+ transitionToParams({ layer: null });
};
- handleDetailEdit = location => {
- this.setState({ selectedId: location.id });
- this.transitionToParams({ layer: 'edit' });
+ const handleDetailEdit = loc => {
+ setSelectedId(loc.id);
+ transitionToParams({ layer: 'edit' });
};
- handleDetailClone = location => {
- this.setState({ selectedId: location.id });
- this.transitionToParams({ layer: 'clone' });
+ const handleDetailClone = loc => {
+ setSelectedId(loc.id);
+ transitionToParams({ layer: 'clone' });
};
- checkLocationHasHoldingsOrItems = async (locationId) => {
- const { mutator } = this.props;
+ const checkLocationHasHoldingsOrItems = async (locationId) => {
const query = `permanentLocationId=="${locationId}" or temporaryLocationId=="${locationId}"`;
- mutator.holdingsEntries.reset();
- mutator.itemEntries.reset();
-
const results = await Promise.all([
- mutator.holdingsEntries.GET({ params: { query } }),
- mutator.itemEntries.GET({ params: { query } }),
+ ky.get('inventory/items', { searchParams: { query } }).json(),
+ ky.get('holdings-storage/holdings', { searchParams: { query } }).json(),
]);
return results.some(records => records.length > 0);
};
- onRemove = location => {
- const {
- match,
- mutator,
- } = this.props;
-
- return this.checkLocationHasHoldingsOrItems(location.id)
- .then(hasSomething => !hasSomething && mutator.entries.DELETE(location))
+ const onRemove = loc => {
+ return checkLocationHasHoldingsOrItems(loc.id)
+ .then(hasSomething => !hasSomething && deleteLocation({ locationId: loc.id }))
.then(result => {
const isRemoved = (result !== false);
-
if (isRemoved) {
- this.showCalloutMessage(location.name);
- this.transitionToParams({
+ showCalloutMessage(loc.name);
+ transitionToParams({
_path: `${match.path}`,
layer: null
});
}
-
return isRemoved;
});
};
- updateSelected = location => {
- this.transitionToParams({
- _path: `${this.props.match.path}/${location.id}`,
+ const updateSelected = loc => {
+ transitionToParams({
+ _path: `${match.path}/${loc.id}`,
layer: null,
});
- this.setState({ selectedId: location.id });
+ setSelectedId(loc.id);
};
- showCalloutMessage(name) {
- if (!this.callout.current) return;
-
- const message = (
-
- );
-
- this.callout.current.sendCallout({ message });
- }
-
- render() {
- const {
- match,
- label,
- location: { search },
- mutator,
- } = this.props;
- const {
- institutionId,
- campusId,
- libraryId,
- sort,
- sortDir,
- selectedId,
- servicePointsById,
- servicePointsByName,
- } = this.state;
-
- const locations = this.prepareLocationsData();
- const contentData = locations.filter(row => row.libraryId === libraryId);
- const query = queryString.parse(search);
- const defaultEntry = { isActive: true, institutionId, campusId, libraryId, servicePointIds: [{ selectSP: '', primary: true }] };
- const adding = search.match('layer=add');
- const cloning = search.match('layer=clone');
-
- // Providing default 'isActive' value is used here when the 'isActive' property is missing in the 'locations' loaded via the API.
- forEach(contentData, location => {
- if (location.isActive === undefined) {
- location.isActive = false;
- }
- });
-
- const selectedItem = (selectedId && !adding)
- ? find(contentData, entry => entry.id === selectedId) : defaultEntry;
+ const locations = prepareLocationsData();
+ const contentData = locations.filter(row => row.libraryId === libraryId);
+ const query = queryString.parse(location.search);
+ const defaultEntry = { isActive: true, institutionId, campusId, libraryId, servicePointIds: [{ selectSP: '', primary: true }] };
+ const adding = location.search.match('layer=add');
+ const cloning = location.search.match('layer=clone');
- const initialValues = this.parseInitialValues(selectedItem, cloning);
+ forEach(contentData, loc => {
+ if (loc.isActive === undefined) {
+ loc.isActive = false;
+ }
+ });
- const container = document.getElementById('ModuleContainer');
+ const selectedItem = (selectedId && !adding) ? find(contentData, entry => entry.id === selectedId) : defaultEntry;
+ const initialValues = parseInitialValues(selectedItem, cloning);
- if (!container) return ();
+ const container = document.getElementById('ModuleContainer');
+ if (!container) return ();
- return (
-
+
-
-
- {() => (
- <>
-
- {this.renderFilter()}
-
-
- >
- )}
-
-
- {
- if (!selectedItem) return null;
-
- return (
-
-
-
- );
- }
- }
- />
-
-
- {contentLabelChunks => (
-
-
+ {() => (
+ <>
+
+ {renderFilter()}
+
-
- )}
-
-
-
-
- );
- }
-}
-
-export default injectIntl(LocationManager);
-// export default LocationManager;
+
+ >
+ )}
+
+
+ {
+ if (!selectedItem) return null;
+ return (
+
+
+
+ );
+ }}
+ />
+
+
+ {contentLabelChunks => (
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+LocationManager.propTypes = {
+ label: PropTypes.node.isRequired,
+};
+
+export default LocationManager;
diff --git a/src/settings/LocationLocations/LocationManager.test.js b/src/settings/LocationLocations/LocationManager.test.js
index ad106c6a..8f1a0b0e 100644
--- a/src/settings/LocationLocations/LocationManager.test.js
+++ b/src/settings/LocationLocations/LocationManager.test.js
@@ -1,51 +1,30 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { createMemoryHistory } from 'history';
-
-import '../../../test/jest/__mocks__';
+import { QueryClient, QueryClientProvider } from 'react-query';
import userEvent from '@testing-library/user-event';
-import buildStripes from '../../../test/jest/__new_mocks__/stripesCore.mock';
+
import {
renderWithRouter
} from '../../../test/jest/helpers';
-
import LocationManager from './LocationManager';
+import '../../../test/jest/__mocks__';
+
+
jest.mock('./RemoteStorage/Provider', () => ({
...jest.requireActual('./RemoteStorage/Provider'),
useRemoteStorageApi: () => ({
remoteMap: {},
- mappings: {
- failed: false,
- hasLoaded: true,
- isPending: false
- },
- configurations: {
- failed: false,
- hasLoaded: true,
- isPending: false,
- records: []
- },
+ mappings: [],
+ configurations: [],
translate: () => 'str'
})
}));
-const STRIPES = buildStripes();
-
-const history = createMemoryHistory();
-
-const locationMock = {
- pathname: '/settings/tenant-settings/location-locations',
- search: '',
- hash: '',
- key: '00ee83',
-};
-
-const resourcesMock = {
- campuses: { hasLoaded: true,
- resource: 'campuses',
- dataKey: 'location-locations',
- records:[
+jest.mock('../../hooks/useCampuses', () => ({
+ useCampuses: jest.fn(() => ({
+ campuses: [
{ code: 'DI',
id: '40ee00ca-a518-4b49-be01-0638d0a4ac57ff',
institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
@@ -56,10 +35,38 @@ const resourcesMock = {
institutionId: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
name: 'Online'
}
- ] },
- entries:{
- hasLoaded: true,
- records:[
+ ],
+ })),
+}));
+
+jest.mock('../../hooks/useInstitutions', () => ({
+ useInstitutions: jest.fn(() => ({
+ institutions: [
+ {
+ code: 'KU',
+ id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
+ name: 'Københavns Universitet',
+ }
+ ],
+ })),
+}));
+
+jest.mock('../../hooks/useLibraries', () => ({
+ useLibraries: jest.fn(() => ({
+ libraries: [
+ {
+ campusId: '40ee00ca-a518-4b49-be01-0638d0a4ac57ff',
+ code: 'DI',
+ id: '5d78803e-ca04-4b4a-aeae-2c63b924518b',
+ name: 'Datalogisk Institut'
+ }
+ ],
+ })),
+}));
+
+jest.mock('../../hooks/useLocations', () => ({
+ useLocations: jest.fn(() => ({
+ locations: [
{
campusId: '62cf76b7-cca5-4d33-9217-edf42ce1a848',
code: 'KU/CC/DI/2',
@@ -86,113 +93,59 @@ const resourcesMock = {
servicePointIds: ['3a40852d-49fd-4df2-a1f9-6e2641a6e91f'],
servicePoints: []
}
- ],
- resource: 'entries'
- },
- institutions: {
- hasLoaded: true,
- records: [
- {
- code: 'KU',
- id: '40ee00ca-a518-4b49-be01-0638d0a4ac57',
- name: 'Københavns Universitet',
- }
]
- },
- libraries:{
- dataKey: 'location-locations',
- hasLoaded: true,
- records: [
+ })),
+}));
+
+jest.mock('../../hooks/useServicePoints', () => ({
+ useServicePoints: jest.fn(() => ({
+ servicePoints: [
{
- campusId: '40ee00ca-a518-4b49-be01-0638d0a4ac57ff',
- code: 'DI',
- id: '5d78803e-ca04-4b4a-aeae-2c63b924518b',
- name: 'Datalogisk Institut'
+ code: 'cd1',
+ discoveryDisplayName: 'Circulation Desk -- Hallway',
+ holdShelfExpiryPeriod: { duration: 3, intervalId: 'Weeks' },
+ id: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
+ name: 'Circ Desk 1',
+ pickupLocation: true,
+ staffSlips: [],
+ },
+ {
+ code: 'Online',
+ discoveryDisplayName: 'Online',
+ id: '7c5abc9f-f3d7-4856-b8d7-6712462ca007',
+ metadata: { createdDate: '2021-11-04T03:24:42.555+00:00', updatedDate: '2021-11-04T03:24:42.555+00:00' },
+ name: 'Online',
+ pickupLocation: false,
+ shelvingLagTime: 0,
+ staffSlips: [],
}
]
- },
- servicePoints: {
- hasLoaded: true,
- records: [{
- code: 'cd1',
- discoveryDisplayName: 'Circulation Desk -- Hallway',
- holdShelfExpiryPeriod: { duration: 3, intervalId: 'Weeks' },
- id: '3a40852d-49fd-4df2-a1f9-6e2641a6e91f',
- name: 'Circ Desk 1',
- pickupLocation: true,
- staffSlips: [],
- },
- {
- code: 'Online',
- discoveryDisplayName: 'Online',
- id: '7c5abc9f-f3d7-4856-b8d7-6712462ca007',
- metadata: { createdDate: '2021-11-04T03:24:42.555+00:00', updatedDate: '2021-11-04T03:24:42.555+00:00' },
- name: 'Online',
- pickupLocation: false,
- shelvingLagTime: 0,
- staffSlips: [],
- }
- ]
- }
-};
-
-const mutatorMock = {
- servicePoints: {
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- DELETE: jest.fn(() => Promise.resolve()),
- },
- institutions: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- campuses: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- libraries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- holdingsEntries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- itemEntries: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- entries: {
- DELETE: jest.fn(() => Promise.resolve()),
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- },
- uniquenessValidator: {
- DELETE: jest.fn(() => Promise.resolve()),
- GET: jest.fn(() => Promise.resolve()),
- POST: jest.fn(() => Promise.resolve()),
- PUT: jest.fn(() => Promise.resolve()),
- cancel: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- }
-};
-
-const matchMock = {
- path: '/settings/tenant-settings/location-locations',
- isExact: true,
- params: {}
-};
-
-const renderLocationManager = (match = matchMock) => renderWithRouter(
- ServicePointManager}
- location={locationMock}
- history={history}
- match={match}
- />
+ })),
+}));
+
+jest.mock('../../hooks/useLocationCreate', () => ({
+ useLocationCreate: jest.fn(() => ({
+ createLocation: jest.fn(),
+ isCreatingLocation: false,
+ })),
+}));
+
+jest.mock('../../hooks/useLocationUpdate', () => ({
+ useLocationUpdate: jest.fn(() => ({
+ updateLocation: jest.fn(),
+ isUpdatingLocation: false,
+ })),
+}));
+
+
+const history = createMemoryHistory();
+
+const renderLocationManager = () => renderWithRouter(
+
+ ServicePointManager}
+ />
+
);
describe('LocationManager', () => {
@@ -254,10 +207,6 @@ describe('LocationManager', () => {
));
libraryOption.forEach((el) => expect(el.selected).toBe(true));
- const rowButtons = screen.getAllByRole('button', { name: 'row button' });
- const headerButton = screen.getByRole('cell', { name: 'ui-tenant-settings.settings.location.locations.status' });
- userEvent.click(headerButton);
- userEvent.click(rowButtons[0]);
});
it('should render select Service points', () => {
diff --git a/src/settings/LocationLocations/RemoteStorage/Control.js b/src/settings/LocationLocations/RemoteStorage/Control.js
index d335d9f3..18daf5b3 100644
--- a/src/settings/LocationLocations/RemoteStorage/Control.js
+++ b/src/settings/LocationLocations/RemoteStorage/Control.js
@@ -30,15 +30,20 @@ const CustomSelect = ({ message, ...rest }) => (
);
export const Control = ({ disabled, readOnly, message, ...rest }) => {
- const { configurations, translate: t } = useRemoteStorageApi();
+ const {
+ configurations,
+ isConfigurationsLoading,
+ isConfigurationsError,
+ translate: t
+ } = useRemoteStorageApi();
- const errorMessage = configurations.failed && t('failed');
- const loadingMessage = configurations.isPending && t('loading');
- const isDisabled = disabled || !configurations.hasLoaded;
+ const errorMessage = isConfigurationsError && t('failed');
+ const loadingMessage = isConfigurationsLoading && t('loading');
+ const isDisabled = disabled || isConfigurationsLoading;
- const configurationOptions = configurations.records.map(c => ({ label: c.name, value: c.id }));
+ const configurationOptions = configurations.map(c => ({ label: c.name, value: c.id }));
const defaultOption = { label: t('no'), value: '' };
- const options = configurations.hasLoaded ? [defaultOption, ...configurationOptions] : undefined;
+ const options = !isConfigurationsLoading ? [defaultOption, ...configurationOptions] : undefined;
return (
render(
-
- Control}
- sub={SubControl}
- dataOptions={dataOptionsMock}
- />
-
+
+
+ Control}
+ sub={SubControl}
+ dataOptions={dataOptionsMock}
+ />
+
+
);
describe('Control', () => {
diff --git a/src/settings/LocationLocations/RemoteStorage/Provider.js b/src/settings/LocationLocations/RemoteStorage/Provider.js
index 64c1c094..cc55377e 100644
--- a/src/settings/LocationLocations/RemoteStorage/Provider.js
+++ b/src/settings/LocationLocations/RemoteStorage/Provider.js
@@ -1,37 +1,62 @@
import React, {
- useEffect,
- useState,
useContext,
createContext,
useMemo,
} from 'react';
-import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-import { stripesConnect } from '@folio/stripes/core';
+import { useStripes } from '@folio/stripes/core';
+
+import { useRemoteStorageConfigurations } from '../../../hooks/useRemoteStorageConfigurations';
+import { useRemoteStorageMappingUpdate } from '../../../hooks/useRemoteStorageMappingUpdate';
+import { useRemoteStorageMappingDelete } from '../../../hooks/useRemoteStorageMappingDelete';
+import { useRemoteStorageMappings } from '../../../hooks/useRemoteStorageMappings';
+
const Context = createContext({});
export const useRemoteStorageApi = () => useContext(Context);
-const Provider = ({ resources, mutator, stripes, ...rest }) => {
- const [persistentMutator] = useState(mutator);
+export const RemoteStorageApiProvider = (props) => {
+ const stripes = useStripes();
- useEffect(() => {
- if (stripes.hasInterface('remote-storage-configurations')) {
- persistentMutator.configurations.reset();
- persistentMutator.configurations.GET();
+ const {
+ configurations,
+ isConfigurationsLoading,
+ isConfigurationsError
+ } = useRemoteStorageConfigurations({
+ searchParams: {
+ limit: 10000
+ },
+ options: {
+ enabled: stripes.hasInterface('remote-storage-configurations'),
}
- }, [persistentMutator.configurations, stripes]);
+ });
+
+ const {
+ mappings,
+ isMappingsLoading,
+ isMappingsError
+ } = useRemoteStorageMappings({
+ searchParams: {
+ limit: 10000
+ },
+ options: {
+ enabled: stripes.hasInterface('remote-storage-mappings'),
+ }
+ });
+
+ const { updateMapping } = useRemoteStorageMappingUpdate();
+ const { deleteMapping } = useRemoteStorageMappingDelete();
const { formatMessage } = useIntl();
const translate = key => formatMessage({ id: `ui-tenant-settings.settings.location.remotes.${key}` });
const remoteMap = useMemo(
- () => Object.fromEntries(resources.mappings.records.map(
+ () => Object.fromEntries(mappings.map(
({ folioLocationId, configurationId }) => [folioLocationId, configurationId]
)),
- [resources.mappings.records]
+ [mappings]
);
const setMapping = ({ folioLocationId, configurationId }) => {
@@ -39,49 +64,26 @@ const Provider = ({ resources, mutator, stripes, ...rest }) => {
return Promise.resolve();
}
- if (configurationId) return persistentMutator.mappings.POST({ folioLocationId, configurationId });
+ if (configurationId) {
+ return updateMapping({
+ data: { folioLocationId, configurationId }
+ });
+ }
- return persistentMutator.mappings.DELETE({ folioLocationId });
+ return deleteMapping({ folioLocationId });
};
const context = {
- ...resources,
remoteMap,
+ mappings,
+ isMappingsLoading,
+ isMappingsError,
+ configurations,
+ isConfigurationsLoading,
+ isConfigurationsError,
setMapping,
translate,
};
- return ;
-};
-
-Provider.manifest = Object.freeze({
- configurations: {
- type: 'okapi',
- path: 'remote-storage/configurations?limit=10000',
- accumulate: true,
- records: 'configurations',
- throwErrors: false,
- },
- mappings: {
- type: 'okapi',
- path: 'remote-storage/mappings',
- perRequest: 10000,
- records: 'mappings',
- pk: 'folioLocationId',
- clientGeneratePk: false, // because we use POST instead of PUT for modification here (there's no PUT)
- throwErrors: false,
- fetch: ({ stripes }) => stripes.hasInterface('remote-storage-mappings'),
- },
-});
-
-Provider.propTypes = {
- mutator: PropTypes.object.isRequired,
- stripes: PropTypes.object,
- resources: PropTypes.shape({
- mappings: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object)
- })
- })
+ return ;
};
-
-export const RemoteStorageApiProvider = stripesConnect(Provider);
diff --git a/src/settings/LocationLocations/RemoteStorageDetails.js b/src/settings/LocationLocations/RemoteStorageDetails.js
index 900d5191..dc074ca5 100644
--- a/src/settings/LocationLocations/RemoteStorageDetails.js
+++ b/src/settings/LocationLocations/RemoteStorageDetails.js
@@ -10,10 +10,11 @@ import {
import { useRemoteStorageApi } from './RemoteStorage';
+
const RemoteStorageDetails = ({ locationId }) => {
const { remoteMap, configurations } = useRemoteStorageApi();
- const currentConfig = configurations?.records.find(config => remoteMap[locationId] === config.id);
+ const currentConfig = configurations.find(config => remoteMap[locationId] === config.id);
return (
<>
diff --git a/src/settings/LocationLocations/locationDetail.test.js b/src/settings/LocationLocations/locationDetail.test.js
index d1385d08..671b1fa7 100644
--- a/src/settings/LocationLocations/locationDetail.test.js
+++ b/src/settings/LocationLocations/locationDetail.test.js
@@ -1,10 +1,14 @@
import React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import user from '@testing-library/user-event';
import LocationDetail from './LocationDetail';
+import '../../../test/jest/__mocks__';
+
+
const mockInitialValues = {
id: '1',
servicePointIds: [
@@ -15,21 +19,31 @@ const mockInitialValues = {
],
};
-const mockSetMapping = jest.fn();
-
-jest.mock(
- '@folio/stripes/core',
- () => ({
- stripesConnect: Component => props => ,
- IfPermission : ({ children }) => <>{children}>,
- useStripes: () => ({
- hasPerm: () => true
- }),
- TitleManager: jest.fn(({ children, ...rest }) => (
- {children}
- ))
+jest.mock('../../hooks/useCampusDetails', () => ({
+ useCampusDetails: jest.fn(() => ({
+ campus: {},
+ })),
+}));
+
+jest.mock('../../hooks/useLibraryDetails', () => ({
+ useLibraryDetails: jest.fn(() => ({
+ library: {},
+ })),
+}));
+
+jest.mock('../../hooks/useInstitutionDetails', () => ({
+ useInstitutionDetails: jest.fn(() => ({
+ institution: {},
+ })),
+}));
+
+jest.mock('./RemoteStorage', () => ({
+ useRemoteStorageApi: () => ({
+ remoteMap: {},
+ configurations: [],
+ setMapping: jest.fn(),
}),
-);
+}));
jest.mock(
'@folio/stripes-components/lib/Icon',
@@ -38,20 +52,8 @@ jest.mock(
},
);
-jest.mock(
- './RemoteStorage',
- () => ({
- useRemoteStorageApi: () => ({ setMapping: mockSetMapping }),
- }),
-);
-
const renderLocationDetail = ({
initialValues = mockInitialValues,
- resources = {
- institutions: {},
- campuses: {},
- libraries: {},
- },
servicePointsById = {
'1': 'Circ Desk 1'
},
@@ -64,16 +66,17 @@ const renderLocationDetail = ({
},
} = {}) => (render(
-
+
+
+
));
@@ -127,7 +130,6 @@ describe('LocationDetail', () => {
user.click(editButton);
expect(onEdit).toHaveBeenCalled();
- expect(mockSetMapping).toHaveBeenCalled();
});
it('should call onClone when duplicate button in action menu clicked', () => {
diff --git a/src/settings/LocationLocations/remoteStorageDetails.test.js b/src/settings/LocationLocations/remoteStorageDetails.test.js
index acf27445..682ef81d 100644
--- a/src/settings/LocationLocations/remoteStorageDetails.test.js
+++ b/src/settings/LocationLocations/remoteStorageDetails.test.js
@@ -1,26 +1,31 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+
import { useRemoteStorageApi } from './RemoteStorage';
import RemoteStorageDetails from './RemoteStorageDetails';
-const mockConfigurations = {
- records: [
- { name: 'RS1', id: 1 },
- { name: 'RS2', id: 2, returningWorkflowDetails: 'Scanned to folio' }
- ]
-};
+
+const mockConfigurations = [
+ { name: 'RS1', id: 1 },
+ { name: 'RS2', id: 2, returningWorkflowDetails: 'Scanned to folio' }
+];
const mockRemoteMap = {
locationWithoutDetails: 1,
locationWithDetails: 2,
};
-jest.mock('./RemoteStorage');
+jest.mock('./RemoteStorage', () => ({
+ useRemoteStorageApi: jest.fn(),
+}));
const renderRemoteStorageDetails = ({
locationId
}) => (render(
-
+
+
+
));
describe('RemoteStorageDetails', () => {
@@ -47,7 +52,7 @@ describe('RemoteStorageDetails', () => {
expect(screen.getByText('RS2')).toBeVisible();
- useRemoteStorageApi.mockImplementation(() => ({ remoteMap: mockRemoteMap, configurations: undefined }));
+ useRemoteStorageApi.mockImplementation(() => ({ remoteMap: mockRemoteMap, configurations: [] }));
sut.rerender();
expect(screen.queryByText('RS2')).not.toBeInTheDocument();
diff --git a/src/settings/LocationLocations/utils.js b/src/settings/LocationLocations/utils.js
index e8ac5c9d..6c188ee5 100644
--- a/src/settings/LocationLocations/utils.js
+++ b/src/settings/LocationLocations/utils.js
@@ -57,18 +57,16 @@ export const validate = values => {
return errors;
};
-export const getUniquenessValidation = (field, mutator, id) => {
+export const getUniquenessValidation = (field, ky, id) => {
return (value, allValues, meta) => {
if (!value) return Promise.resolve();
if (id && !meta.dirty) return Promise.resolve();
- mutator.reset();
-
const query = `(${field}=="${value.replace(/"/gi, '\\"')}")`;
- return mutator.GET({ params: { query } })
- .then((locations) => {
+ return ky.get('locations', { searchParams: { query } }).json()
+ .then(({ locations }) => {
if (locations.length !== 0) return Promise.reject();
return undefined;
diff --git a/src/settings/Plugins/PluginForm.js b/src/settings/Plugins/PluginForm.js
index 7af6b959..a64b323b 100644
--- a/src/settings/Plugins/PluginForm.js
+++ b/src/settings/Plugins/PluginForm.js
@@ -17,32 +17,27 @@ import stripesFinalForm from '@folio/stripes/final-form';
import styles from './Plugins.css';
-class PluginForm extends React.Component {
- static propTypes = {
- handleSubmit: PropTypes.func.isRequired,
- pristine: PropTypes.bool,
- submitting: PropTypes.bool,
- label: PropTypes.node,
- pluginTypes: PropTypes.object,
- readOnly: PropTypes.bool,
- };
-
- constructor(props) {
- super(props);
- this.renderPlugins = this.renderPlugins.bind(this);
- }
- renderPlugin(field, plugin) {
- const intl = useIntl();
- const { pluginTypes, readOnly } = this.props;
+const PluginForm = ({
+ handleSubmit,
+ pristine,
+ submitting,
+ label,
+ pluginTypes,
+ readOnly,
+}) => {
+ const intl = useIntl();
+ const renderPlugin = (field, plugin) => {
const pluginType = pluginTypes[plugin.configName];
const options = [{ value: '@@', label: '(none)' }].concat(pluginType.map(p => ({
value: p.module,
- label: intl.formatMessage({ id: `ui-tenant-settings.settings.pluginNames.${p.pluginType}` }) + ` ${p.version}`,
+ label: `${intl.formatMessage({ id: `ui-tenant-settings.settings.pluginNames.${p.pluginType}` })} ${p.version}`,
})));
- const label = ;
+
+ const lbl = ;
+
return (
@@ -50,7 +45,7 @@ class PluginForm extends React.Component {
readOnly={readOnly}
id={plugin.configName}
data-testid={plugin.configName}
- label={label}
+ label={lbl}
name={`${field}.value`}
placeholder="---"
component={Select}
@@ -59,59 +54,54 @@ class PluginForm extends React.Component {
);
- }
-
- renderPlugins({ fields }) {
- const plugins = fields.map((field, index) => (
- this.renderPlugin(field, fields.value[index])
- ));
+ };
- return (
- {plugins}
- );
- }
+ const renderPlugins = ({ fields }) => {
+ const plugins = fields.map((field, index) => renderPlugin(field, fields.value[index]));
- render() {
- const {
- handleSubmit,
- pristine,
- submitting,
- label,
- readOnly,
- } = this.props;
+ return {plugins}
;
+ };
- const footer = !readOnly && (
-
-
-
- )}
- />
- );
+ const footer = !readOnly && (
+
+
+
+ )}
+ />
+ );
- return (
-
- );
- }
-}
+
+
+
+ );
+};
+
+PluginForm.propTypes = {
+ handleSubmit: PropTypes.func.isRequired,
+ pristine: PropTypes.bool,
+ submitting: PropTypes.bool,
+ label: PropTypes.node,
+ pluginTypes: PropTypes.object,
+ readOnly: PropTypes.bool,
+};
export default stripesFinalForm({
navigationCheck: true,
diff --git a/src/settings/Plugins/Plugins.js b/src/settings/Plugins/Plugins.js
index 62673216..2f2acb92 100644
--- a/src/settings/Plugins/Plugins.js
+++ b/src/settings/Plugins/Plugins.js
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage, injectIntl } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
import { map, omit } from 'lodash';
import { modules } from 'stripes-config'; // eslint-disable-line import/no-unresolved, import/no-extraneous-dependencies
@@ -8,124 +8,97 @@ import {
Callout,
Layout,
} from '@folio/stripes/components';
+import { TitleManager, useStripes } from '@folio/stripes/core';
-import { TitleManager } from '@folio/stripes/core';
import PluginForm from './PluginForm';
+import { useConfigurationsCreate } from '../../hooks/useConfigurationsCreate';
+import { useConfigurationsUpdate } from '../../hooks/useConfigurationsUpdate';
+import { useConfigurations } from '../../hooks/useConfigurations';
-class Plugins extends React.Component {
- static manifest = Object.freeze({
- recordId: {},
- settings: {
- type: 'okapi',
- records: 'configs',
- path: 'configurations/entries?query=(module==PLUGINS) sortby configName&limit=1000',
- POST: {
- path: 'configurations/entries',
- },
- PUT: {
- path: 'configurations/entries/%{recordId}',
- },
- },
- });
- static propTypes = {
- label: PropTypes.node.isRequired,
- stripes: PropTypes.shape({
- logger: PropTypes.shape({
- log: PropTypes.func.isRequired,
- }).isRequired,
- setSinglePlugin: PropTypes.func.isRequired,
- hasPerm: PropTypes.func.isRequired,
- }).isRequired,
- resources: PropTypes.shape({
- settings: PropTypes.shape({
- records: PropTypes.arrayOf(PropTypes.object),
- }),
- }).isRequired,
- mutator: PropTypes.shape({
- recordId: PropTypes.shape({
- replace: PropTypes.func,
- }),
- settings: PropTypes.shape({
- POST: PropTypes.func.isRequired,
- PUT: PropTypes.func.isRequired,
- }),
- }).isRequired,
- intl: PropTypes.object,
- };
+const Plugins = ({ label }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
+ const callout = useRef(null);
- constructor(props) {
- super(props);
+ const { configs } = useConfigurations({
+ searchParams: {
+ query: '(module==PLUGINS) sortby configName',
+ limit: '1000',
+ },
+ });
+ const { createConfiguration } = useConfigurationsCreate();
+ const { updateConfiguration } = useConfigurationsUpdate();
+ const pluginTypes = (() => {
const plugins = modules.plugin || [];
- this.pluginTypes = plugins.reduce((pt, plugin) => {
+ return plugins.reduce((pt, plugin) => {
const type = plugin.pluginType;
- // eslint-disable-next-line no-param-reassign
pt[type] = pt[type] || [];
pt[type].push(plugin);
return pt;
}, {});
+ })();
- this.save = this.save.bind(this);
- }
-
- getPlugins() {
- const settings = ((this.props.resources.settings || {}).records || []);
+ const getPlugins = (settings) => {
const pluginsByType = settings.reduce((memo, setting) => {
- // eslint-disable-next-line no-param-reassign
memo[setting.configName] = setting;
return memo;
}, {});
- return map(this.pluginTypes, (types, key) => {
+ return map(pluginTypes, (types, key) => {
const plugin = pluginsByType[key];
return plugin || { configName: key };
});
- }
+ };
- savePlugin(plugin) {
+ const savePlugin = (plugin) => {
const value = plugin.value;
if (plugin.id) {
- // Setting has been set previously: replace it
- this.props.mutator.recordId.replace(plugin.id);
- this.props.mutator.settings.PUT(omit(plugin, ['metadata']));
+ updateConfiguration({
+ id: plugin.id,
+ data: omit(plugin, ['metadata']),
+ });
} else {
- // No setting: create a new one
- this.props.mutator.settings.POST({
- module: 'PLUGINS',
- configName: plugin.configName,
- value,
+ createConfiguration({
+ data: {
+ module: 'PLUGINS',
+ configName: plugin.configName,
+ value,
+ },
});
}
- this.props.stripes.setSinglePlugin(plugin.configName, value);
- }
+ stripes.setSinglePlugin(plugin.configName, value);
+ };
- save(data) {
- data.plugins.forEach(p => this.savePlugin(p));
+ const save = (data) => {
+ data.plugins.forEach(p => savePlugin(p));
const updateMsg = ;
- this.callout.sendCallout({ message: updateMsg });
- }
+ callout.current.sendCallout({ message: updateMsg });
+ };
+
+ const plugins = getPlugins(configs);
+ const isReadOnly = !stripes.hasPerm('ui-tenant-settings.settings.plugins');
- render() {
- const plugins = this.getPlugins();
- const isReadOnly = !this.props.stripes.hasPerm('ui-tenant-settings.settings.plugins');
+ return (
+
+
+
+
+
+
+ );
+};
- return (
-
-
-
-
- { this.callout = ref; }} />
-
- );
- }
-}
+Plugins.propTypes = {
+ label: PropTypes.node.isRequired,
+};
-export default injectIntl(Plugins);
+export default Plugins;
diff --git a/src/settings/Plugins/Plugins.test.js b/src/settings/Plugins/Plugins.test.js
index a7f3ab2b..655cb227 100644
--- a/src/settings/Plugins/Plugins.test.js
+++ b/src/settings/Plugins/Plugins.test.js
@@ -1,72 +1,53 @@
import React from 'react';
-
+import { QueryClient, QueryClientProvider } from 'react-query';
import { screen } from '@testing-library/react';
import user from '@testing-library/user-event';
-import '../../../test/jest/__mocks__';
import { renderWithRouter } from '../../../test/jest/helpers';
-
import Plugins from './Plugins';
-const setSinglePlugin = jest.fn();
-const hasPermMock = jest.fn().mockReturnValue(true);
+import '../../../test/jest/__mocks__';
+import { mockHasPerm } from '../../../test/jest/__mocks__/stripesCore.mock';
-const renderPlugins = (props) => renderWithRouter();
+const mockCreateConfiguration = jest.fn(() => Promise.resolve());
+
+jest.mock('../../hooks/useConfigurationsCreate', () => ({
+ useConfigurationsCreate: jest.fn(() => ({
+ createConfiguration: mockCreateConfiguration,
+ isCreatingConfiguration: false,
+ })),
+}));
+
+const renderPlugins = (props) => renderWithRouter(
+
+
+
+);
describe('Plugins', () => {
afterEach(() => {
- setSinglePlugin.mockClear();
- hasPermMock.mockClear();
+ mockHasPerm.mockClear();
});
it('should render plugins', async () => {
- const { findAllByText } = await renderPlugins({ label: 'plugins',
- stripes: {
- setSinglePlugin,
- hasPerm: hasPermMock,
- logger: {
- log: jest.fn(),
- },
- },
- resources: {} });
+ const { findAllByText } = await renderPlugins({ label: 'plugins' });
+
expect(await findAllByText('plugins')).toBeDefined();
});
it('should choose and save plugin', async () => {
- renderPlugins({ label: 'plugins',
- stripes: {
- setSinglePlugin,
- hasPerm: hasPermMock,
- logger: {
- log: jest.fn(),
- },
- },
- resources: {},
- mutator: {
- settings: { POST: jest.fn() }
- } });
+ renderPlugins({ label: 'plugins' });
user.selectOptions(screen.getByTestId('find-instance'), ['@folio/plugin-find-instance']);
user.click(screen.getByRole('button', { type: /submit/i }));
- expect(setSinglePlugin).toHaveBeenCalledTimes(1);
+ expect(mockCreateConfiguration).toHaveBeenCalledTimes(1);
});
it('submit button should not be present without permissions', async () => {
- hasPermMock.mockReturnValueOnce(false);
+ mockHasPerm.mockReturnValueOnce(false);
- renderPlugins({ label: 'plugins',
- stripes: {
- setSinglePlugin,
- hasPerm: hasPermMock,
- logger: {
- log: jest.fn(),
- },
- },
- resources: {},
- mutator: {
- settings: { POST: jest.fn() }
- } });
+ renderPlugins({ label: 'plugins' });
expect(screen.queryByRole('button', { type: /submit/i })).toBeNull();
});
diff --git a/src/settings/ReadingRoomAccess/ReadingRoomAccess.js b/src/settings/ReadingRoomAccess/ReadingRoomAccess.js
index 8dc0e697..8ce07e89 100644
--- a/src/settings/ReadingRoomAccess/ReadingRoomAccess.js
+++ b/src/settings/ReadingRoomAccess/ReadingRoomAccess.js
@@ -5,8 +5,7 @@ import _ from 'lodash';
import {
TitleManager,
- withStripes,
- stripesShape
+ useStripes
} from '@folio/stripes/core';
import { Label } from '@folio/stripes/components';
import { ControlledVocab } from '@folio/stripes/smart-components';
@@ -16,6 +15,7 @@ import { getFormatter } from './getFormatter';
import { getFieldComponents } from './getFieldComponents';
import { getValidators } from './getValidators';
+
const hiddenFields = ['numberOfObjects', 'lastUpdated'];
const translations = {
cannotDeleteTermHeader: 'ui-tenant-settings.settings.reading-room-access.cannotDeleteTermHeader',
@@ -27,14 +27,15 @@ const translations = {
const ReadingRoomAccess = (props) => {
const intl = useIntl();
- const { resources, stripes } = props;
+ const stripes = useStripes();
+ const { resources } = props;
// service points defined in the tenant
const servicePoints = _.get(resources, ['RRAServicePoints', 'records', 0, 'servicepoints'], []);
/**
* A reading room can have more than one service points assigned to it.
* but a servicepoint cannot be mapped to more than one reading room
- */
+ */
const sps = [];
const rrs = _.get(resources, ['values', 'records']);
rrs.forEach(rr => {
@@ -78,7 +79,7 @@ const ReadingRoomAccess = (props) => {
const formatter = useMemo(() => getFormatter({ fieldLabels }), [fieldLabels]);
const validateItem = useCallback((item, items) => {
- const errors = Object.values(readingRoomAccessColumns).reduce((acc, field) => {
+ return Object.values(readingRoomAccessColumns).reduce((acc, field) => {
const error = getValidators(field)?.(item, items);
if (error) {
@@ -87,8 +88,6 @@ const ReadingRoomAccess = (props) => {
return acc;
}, {});
-
- return errors;
}, []);
const validate = (item, index, items) => validateItem(item, items) || {};
@@ -140,7 +139,6 @@ ReadingRoomAccess.manifest = Object.freeze({
ReadingRoomAccess.propTypes = {
resources: PropTypes.object,
mutator: PropTypes.object,
- stripes: stripesShape.isRequired,
};
-export default withStripes(ReadingRoomAccess);
+export default ReadingRoomAccess;
diff --git a/src/settings/ReadingRoomAccess/ReadingRoomAccess.test.js b/src/settings/ReadingRoomAccess/ReadingRoomAccess.test.js
index 8b000d8d..f05c8e47 100644
--- a/src/settings/ReadingRoomAccess/ReadingRoomAccess.test.js
+++ b/src/settings/ReadingRoomAccess/ReadingRoomAccess.test.js
@@ -4,14 +4,11 @@ import { Form } from 'react-final-form';
import { runAxeTest } from '@folio/stripes-testing';
-import '../../../test/jest/__mocks__';
-import buildStripes from '../../../test/jest/__new_mocks__/stripesCore.mock';
-
import { renderWithRouter } from '../../../test/jest/helpers';
-
import ReadingRoomAccess from './ReadingRoomAccess';
-const stripes = buildStripes();
+import '../../../test/jest/__mocks__';
+import { mockHasPerm } from '../../../test/jest/__mocks__/stripesCore.mock';
const mutatorPutMock = jest.fn(() => Promise.resolve());
const mutatorMock = {
@@ -28,7 +25,6 @@ const mutatorMock = {
replace: jest.fn()
},
};
-
const resourcesMock = {
values: {
dataKey: 'reading-room',
@@ -179,12 +175,11 @@ describe('Reading Room Access', () => {
const props = {
mutator: mutatorMock,
resources: resourcesMock,
- stripes,
};
describe('when all permissions are available', () => {
beforeEach(() => {
- stripes.hasPerm = jest.fn().mockReturnValue(true);
+ mockHasPerm.mockReturnValue(true);
renderReadingRoomAccess(props);
});
@@ -253,7 +248,7 @@ describe('Reading Room Access', () => {
describe('when permissions are not available', () => {
beforeEach(() => {
- stripes.hasPerm = jest.fn().mockReturnValue(false);
+ mockHasPerm.mockReturnValue(false);
renderReadingRoomAccess(props);
});
diff --git a/src/settings/SSOSettings/SSOSettings.js b/src/settings/SSOSettings/SSOSettings.js
index 4bf532a8..ae9ebc67 100644
--- a/src/settings/SSOSettings/SSOSettings.js
+++ b/src/settings/SSOSettings/SSOSettings.js
@@ -1,103 +1,53 @@
-import _ from 'lodash';
-import React from 'react';
+import React, { useRef } from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage, injectIntl } from 'react-intl';
-import { stripesShape, TitleManager } from '@folio/stripes/core';
+import { FormattedMessage, useIntl } from 'react-intl';
+
+import { TitleManager, useOkapiKy, useStripes } from '@folio/stripes/core';
import {
Callout,
Layout,
} from '@folio/stripes/components';
import { patronIdentifierTypes, samlBindingTypes } from '../../constants';
-
import SamlForm from './SamlForm';
-
-class SSOSettings extends React.Component {
- static manifest = Object.freeze({
- recordId: {},
- samlconfig: {
- type: 'okapi',
- path: 'saml/configuration',
- PUT: {
- path: 'saml/configuration',
- },
- },
- downloadFile: {
- accumulate: true,
- type: 'okapi',
- path: 'saml/regenerate',
- },
- urlValidator: {
- type: 'okapi',
- accumulate: 'true',
- path: 'saml/validate',
- fetch: false,
- throwErrors: false,
- },
+import { useSamlConfiguration } from '../../hooks/useSamlConfiguration';
+import { useSamlConfigurationUpdate } from '../../hooks/useSamlConfigurationUpdate';
+
+
+const SSOSettings = ({ label }) => {
+ const intl = useIntl();
+ const stripes = useStripes();
+ const ky = useOkapiKy();
+ const callout = useRef(null);
+ const downloadButton = useRef(null);
+ const isReadOnly = !stripes.hasPerm('ui-tenant-settings.settings.sso');
+
+ const { samlConfig } = useSamlConfiguration();
+ const { updateSamlConfiguration } = useSamlConfigurationUpdate({
+ onSuccess: () => {
+ callout.current.sendCallout({ message: });
+ }
});
- static propTypes = {
- label: PropTypes.node.isRequired,
- stripes: stripesShape.isRequired,
- resources: PropTypes.shape({
- samlconfig: PropTypes.object,
- }).isRequired,
- mutator: PropTypes.shape({
- recordId: PropTypes.shape({
- replace: PropTypes.func,
- }),
- samlconfig: PropTypes.shape({
- PUT: PropTypes.func.isRequired,
- }),
- downloadFile: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- urlValidator: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- }).isRequired,
- intl: PropTypes.object
- };
-
- constructor(props) {
- super(props);
-
- this.validateIdpUrl = this.validateIdpUrl.bind(this);
- this.updateSettings = this.updateSettings.bind(this);
- }
-
- getConfig() {
- const { resources } = this.props;
- const config = (resources.samlconfig || {}).records || [];
- const configValue = (config.length === 0) ? {} : config[0];
- const configData = configValue ? _.cloneDeep(configValue) : configValue;
- return configData;
- }
-
- updateSettings(settings) {
- const updateMsg = ;
- settings.okapiUrl = this.props.stripes.okapi.url;
- this.props.mutator.samlconfig.PUT(settings).then(() => {
- this.callout.sendCallout({ message: updateMsg });
+ const updateSettings = (settings) => {
+ updateSamlConfiguration({
+ data: {
+ ...settings,
+ okapiUrl: stripes.okapi.url,
+ }
});
- }
-
- async validateIdpUrl(value) {
- const { mutator: { urlValidator } } = this.props;
+ };
+ const validateIdpUrl = async (value) => {
if (!value) {
return ;
}
const error = ;
- const params = { type: 'idpurl', value };
-
- urlValidator.reset();
+ const searchParams = { type: 'idpurl', value };
try {
- const result = await urlValidator.GET({ params });
+ const result = await ky.get('saml/validate', { searchParams }).json();
if (!result?.valid) {
return error;
@@ -107,36 +57,34 @@ class SSOSettings extends React.Component {
}
return '';
- }
-
- render() {
- const samlFormData = this.getConfig();
- const isReadOnly = !this.props.stripes.hasPerm('ui-tenant-settings.settings.sso');
-
- return (
-
-
- { this.updateSettings(record); }}
- optionLists={{ identifierOptions: patronIdentifierTypes, samlBindingOptions: samlBindingTypes }}
- parentMutator={this.props.mutator}
- validateIdpUrl={this.validateIdpUrl}
- stripes={this.props.stripes}
- readOnly={isReadOnly}
- />
- { this.downloadButton = reference; return reference; }}
- >
-
-
- { this.callout = ref; }} />
-
-
- );
- }
-}
+ };
-export default injectIntl(SSOSettings);
+ return (
+
+
+ { updateSettings(record); }}
+ optionLists={{ identifierOptions: patronIdentifierTypes, samlBindingOptions: samlBindingTypes }}
+ validateIdpUrl={validateIdpUrl}
+ stripes={stripes}
+ readOnly={isReadOnly}
+ />
+
+
+
+
+
+
+ );
+};
+
+SSOSettings.propTypes = {
+ label: PropTypes.node.isRequired,
+};
+
+export default SSOSettings;
diff --git a/src/settings/SSOSettings/SSOSettings.test.js b/src/settings/SSOSettings/SSOSettings.test.js
index 78c9553c..b4b44a2b 100644
--- a/src/settings/SSOSettings/SSOSettings.test.js
+++ b/src/settings/SSOSettings/SSOSettings.test.js
@@ -1,70 +1,27 @@
import React from 'react';
import { act, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { QueryClient, QueryClientProvider } from 'react-query';
+
import SSOSettings from './SSOSettings';
-import buildStripes from '../../../test/jest/__new_mocks__/stripesCore.mock';
import { renderWithRouter } from '../../../test/jest/helpers';
-
-const hasPermMock = jest.fn().mockReturnValue(true);
-
-const STRIPES = {
- ...buildStripes(),
- hasPerm: hasPermMock,
- config: {
- platform: 'tenant-settings'
- },
-};
-
-const resourcesMock = {
- values: {
- failed: false,
- hasLoaded: true,
- httpStatus: 200,
- isPending: false,
- module: '@folio/tenant-settings',
- },
-};
-
-const samlconfigCallback = jest.fn(() => Promise.resolve());
-const urlValidatorCallback = jest.fn(() => Promise.resolve({ valid: true }));
-
-const mutatorMock = {
- recordId: {
- replace: jest.fn(() => Promise.resolve()),
- },
- samlconfig: {
- PUT: samlconfigCallback,
- },
- downloadFile: {
- GET: jest.fn(() => Promise.resolve()),
- reset: jest.fn(() => Promise.resolve()),
- },
- urlValidator: {
- GET: urlValidatorCallback,
- reset: jest.fn(() => Promise.resolve()),
- }
-};
-
+import '../../../test/jest/__mocks__';
+import { mockHasPerm } from '../../../test/jest/__mocks__/stripesCore.mock';
const SSOSettingsLabel = 'SSOSettings label';
const renderSSOSettings = () => {
renderWithRouter(
- {SSOSettingsLabel}}
- stripes={STRIPES}
- resources={resourcesMock}
- mutator={mutatorMock}
- />
+
+ {SSOSettingsLabel}}
+ />
+
);
};
describe('SSOSettings', () => {
- afterEach(() => {
- hasPermMock.mockClear();
- });
-
it('should render SSOSettings label', () => {
renderSSOSettings();
@@ -152,7 +109,7 @@ describe('SSOSettings', () => {
});
it('should render SSOSettings form elements "read-only mode"', async () => {
- hasPermMock.mockReturnValue(false);
+ mockHasPerm.mockReturnValue(false);
renderSSOSettings();
diff --git a/src/settings/SSOSettings/SamlForm.js b/src/settings/SSOSettings/SamlForm.js
index 89c46004..ca0215a1 100644
--- a/src/settings/SSOSettings/SamlForm.js
+++ b/src/settings/SSOSettings/SamlForm.js
@@ -16,6 +16,8 @@ import stripesFinalForm from '@folio/stripes/final-form';
import { IfPermission } from '@folio/stripes/core';
import styles from './SSOSettings.css';
+import { useSamlDownload } from '../../hooks/useSamlDownload';
+
const validate = (values) => {
const errors = {};
@@ -32,169 +34,151 @@ const validate = (values) => {
return errors;
};
-class SamlForm extends React.Component {
- static propTypes = {
- validateIdpUrl: PropTypes.func.isRequired,
- handleSubmit: PropTypes.func.isRequired,
- reset: PropTypes.func,
- pristine: PropTypes.bool,
- submitting: PropTypes.bool,
- initialValues: PropTypes.object.isRequired, // eslint-disable-line react/no-unused-prop-types
- values: PropTypes.object,
- optionLists: PropTypes.shape({
- identifierOptions: PropTypes.arrayOf(PropTypes.object),
- samlBindingOptions: PropTypes.arrayOf(PropTypes.object),
- }),
- parentMutator: PropTypes.shape({ // eslint-disable-line react/no-unused-prop-types
- urlValidator: PropTypes.shape({
- reset: PropTypes.func.isRequired,
- GET: PropTypes.func.isRequired,
- }).isRequired,
- downloadFile: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }),
- }),
- label: PropTypes.node,
- readOnly: PropTypes.bool,
- };
-
- updateMetadataInvalidated = () => {
- this.props.initialValues.metadataInvalidated = false;
- this.forceUpdate();
- }
- downloadMetadata = () => {
- this.props.parentMutator.downloadFile.reset();
- this.props.parentMutator.downloadFile.GET().then((result) => {
+const SamlForm = ({
+ validateIdpUrl,
+ handleSubmit,
+ pristine,
+ submitting,
+ initialValues,
+ optionLists,
+ label,
+ values,
+ readOnly,
+}) => {
+ const { downloadFile } = useSamlDownload({
+ onSuccess: (result) => {
const anchor = document.createElement('a');
- anchor.href = `data:text/plain;base64,${result.fileContent}`;
+ anchor.href = `data:text/plain;base64,${result?.fileContent}`;
anchor.download = 'sp-metadata.xml';
anchor.click();
- this.updateMetadataInvalidated();
- });
- }
- render() {
- const {
- handleSubmit,
- pristine,
- submitting,
- initialValues,
- optionLists,
- label,
- validateIdpUrl,
- values,
- readOnly,
- } = this.props;
+ initialValues.metadataInvalidated = false;
+ },
+ });
- const identifierOptions = (optionLists.identifierOptions || []).map(i => (
- { id: i.key, label: i.label, value: i.key, selected: initialValues.userProperty === i.key }
- ));
- const samlBindingOptions = optionLists.samlBindingOptions.map(i => (
- { id: i.key, label: i.label, value: i.key, selected: initialValues.samlBinding === i.key }
- ));
+ const identifierOptions = (optionLists.identifierOptions || []).map(i => (
+ { id: i.key, label: i.label, value: i.key, selected: initialValues.userProperty === i.key }
+ ));
- const footer = !readOnly && (
-
-
-
- )}
- />
- );
+ const samlBindingOptions = optionLists.samlBindingOptions.map(i => (
+ { id: i.key, label: i.label, value: i.key, selected: initialValues.samlBinding === i.key }
+ ));
- return (
-
- );
- }
-}
+
+
+ )}
+ />
+ );
+
+ return (
+
+ );
+};
+
+SamlForm.propTypes = {
+ validateIdpUrl: PropTypes.func.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ pristine: PropTypes.bool,
+ submitting: PropTypes.bool,
+ initialValues: PropTypes.object.isRequired, // eslint-disable-line react/no-unused-prop-types
+ values: PropTypes.object,
+ optionLists: PropTypes.shape({
+ identifierOptions: PropTypes.arrayOf(PropTypes.object),
+ samlBindingOptions: PropTypes.arrayOf(PropTypes.object),
+ }),
+ label: PropTypes.node,
+ readOnly: PropTypes.bool,
+};
export default stripesFinalForm({
validate,
diff --git a/src/settings/ServicePoints/StaffSlipEditList.js b/src/settings/ServicePoints/StaffSlipEditList.js
index 03890df5..6d9e7512 100644
--- a/src/settings/ServicePoints/StaffSlipEditList.js
+++ b/src/settings/ServicePoints/StaffSlipEditList.js
@@ -6,13 +6,9 @@ import { FieldArray } from 'react-final-form-arrays';
import { Col, Row, Checkbox } from '@folio/stripes/components';
-class StaffSlipEditList extends React.Component {
- static propTypes = {
- staffSlips: PropTypes.arrayOf(PropTypes.object),
- };
- renderList = () => {
- const { staffSlips } = this.props;
+const StaffSlipEditList = ({ staffSlips }) => {
+ const renderList = () => {
const items = staffSlips.map((staffSlip, index) => (
@@ -30,24 +26,24 @@ class StaffSlipEditList extends React.Component {
return (
<>
-
+
{items}
>
);
- }
+ };
- render() {
- return (
-
- );
- }
-}
+ return (
+
+ );
+};
+
+StaffSlipEditList.propTypes = {
+ staffSlips: PropTypes.arrayOf(PropTypes.object),
+};
export default StaffSlipEditList;
diff --git a/src/settings/ServicePoints/StaffSlipList.js b/src/settings/ServicePoints/StaffSlipList.js
index 20bcb51a..ef70a0cc 100644
--- a/src/settings/ServicePoints/StaffSlipList.js
+++ b/src/settings/ServicePoints/StaffSlipList.js
@@ -2,15 +2,12 @@ import { keyBy, isUndefined } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
+
import { List, KeyValue } from '@folio/stripes/components';
-class StaffSlipList extends React.Component {
- static propTypes = {
- staffSlips: PropTypes.arrayOf(PropTypes.object),
- servicePoint: PropTypes.object,
- };
- renderItem = (staffSlip, slipMap) => {
+const StaffSlipList = ({ staffSlips, servicePoint }) => {
+ const renderItem = (staffSlip, slipMap) => {
const { id, name } = staffSlip;
const { printByDefault } = (slipMap[id] || {});
const yesNo = (printByDefault || isUndefined(printByDefault)) ? 'yes' : 'no';
@@ -23,27 +20,29 @@ class StaffSlipList extends React.Component {
/>
);
- }
+ };
- render() {
- const { staffSlips, servicePoint } = this.props;
- const slipMap = keyBy(servicePoint.staffSlips, 'id');
+ const slipMap = keyBy(servicePoint.staffSlips, 'id');
- if (!staffSlips.length) return null;
+ if (!staffSlips.length) return null;
- return (
- }
- >
-
- this.renderItem(staffSlip, slipMap)}
- />
-
-
- );
- }
-}
+ return (
+ }
+ >
+
+ renderItem(staffSlip, slipMap)}
+ />
+
+
+ );
+};
+
+StaffSlipList.propTypes = {
+ staffSlips: PropTypes.arrayOf(PropTypes.object),
+ servicePoint: PropTypes.object,
+};
export default StaffSlipList;
diff --git a/src/settings/index.js b/src/settings/index.js
index 323ed55f..53107ec4 100644
--- a/src/settings/index.js
+++ b/src/settings/index.js
@@ -1,9 +1,9 @@
import React from 'react';
-import { FormattedMessage, injectIntl } from 'react-intl';
+import { FormattedMessage, useIntl } from 'react-intl';
+
import { Settings } from '@folio/stripes/smart-components';
-import { stripesShape, TitleManager } from '@folio/stripes/core';
+import { TitleManager, useStripes } from '@folio/stripes/core';
-import PropTypes from 'prop-types';
import Addresses from './Addresses';
import Locale from './Locale';
import Plugins from './Plugins';
@@ -15,130 +15,108 @@ import LocationLibraries from './LocationLibraries';
import LocationLocations from './LocationLocations';
import ServicePoints from './ServicePoints';
-class Organization extends React.Component {
- static propTypes = {
- stripes: stripesShape.isRequired,
- intl: PropTypes.object,
- }
-
- constructor(props) {
- super(props);
- this.sections = [
- {
- label:
- (
-
-
-
- ),
- pages: [
- {
- route: 'addresses',
- label: ,
- component: Addresses,
- perm: 'ui-tenant-settings.settings.addresses.view',
- },
- {
- route: 'locale',
- label: ,
- component: Locale,
- perm: 'ui-tenant-settings.settings.locale.view',
- },
- {
- route: 'plugins',
- label: ,
- component: Plugins,
- perm: 'ui-tenant-settings.settings.plugins.view',
- },
- {
- route: 'reading-room',
- label: ,
- component: ReadingRoomAccess,
- perm: 'ui-tenant-settings.settings.reading-room-access.view',
- iface: 'reading-room'
- },
- {
- route: 'ssosettings',
- label: ,
- component: SSOSettings,
- perm: 'ui-tenant-settings.settings.sso.view',
- iface: 'login-saml'
- },
- {
- route: 'servicePoints',
- label: ,
- component: ServicePoints,
- perm: 'ui-tenant-settings.settings.servicepoints.view',
- iface: 'service-points',
- },
- ],
- },
- {
- label: ,
- pages: [
- {
- route: 'location-institutions',
- label: ,
- component: LocationInstitutions,
- perm: 'ui-tenant-settings.settings.location.view',
- iface: 'location-units',
- },
- {
- route: 'location-campuses',
- label: ,
- component: LocationCampuses,
- perm: 'ui-tenant-settings.settings.location.view',
- iface: 'location-units',
- },
- {
- route: 'location-libraries',
- label: ,
- component: LocationLibraries,
- perm: 'ui-tenant-settings.settings.location.view',
- iface: 'location-units',
- },
- {
- route: 'location-locations',
- label: ,
- component: LocationLocations,
- perm: 'ui-tenant-settings.settings.location.view',
- iface: 'location-units',
- },
- ],
- }
- ];
- }
- /*
-
-
- {navLinks}
-
-
-
-
-
-
+const Organization = (props) => {
+ const intl = useIntl();
+ const stripes = useStripes();
- */
+ const sections = [
+ {
+ label: (
+
+
+
+ ),
+ pages: [
+ {
+ route: 'addresses',
+ label: ,
+ component: Addresses,
+ perm: 'ui-tenant-settings.settings.addresses.view',
+ },
+ {
+ route: 'locale',
+ label: ,
+ component: Locale,
+ perm: 'ui-tenant-settings.settings.locale.view',
+ },
+ {
+ route: 'plugins',
+ label: ,
+ component: Plugins,
+ perm: 'ui-tenant-settings.settings.plugins.view',
+ },
+ {
+ route: 'reading-room',
+ label: ,
+ component: ReadingRoomAccess,
+ perm: 'ui-tenant-settings.settings.reading-room-access.view',
+ iface: 'reading-room'
+ },
+ {
+ route: 'ssosettings',
+ label: ,
+ component: SSOSettings,
+ perm: 'ui-tenant-settings.settings.sso.view',
+ iface: 'login-saml'
+ },
+ {
+ route: 'servicePoints',
+ label: ,
+ component: ServicePoints,
+ perm: 'ui-tenant-settings.settings.servicepoints.view',
+ iface: 'service-points',
+ },
+ ],
+ },
+ {
+ label: ,
+ pages: [
+ {
+ route: 'location-institutions',
+ label: ,
+ component: LocationInstitutions,
+ perm: 'ui-tenant-settings.settings.location.view',
+ iface: 'location-units',
+ },
+ {
+ route: 'location-campuses',
+ label: ,
+ component: LocationCampuses,
+ perm: 'ui-tenant-settings.settings.location.view',
+ iface: 'location-units',
+ },
+ {
+ route: 'location-libraries',
+ label: ,
+ component: LocationLibraries,
+ perm: 'ui-tenant-settings.settings.location.view',
+ iface: 'location-units',
+ },
+ {
+ route: 'location-locations',
+ label: ,
+ component: LocationLocations,
+ perm: 'ui-tenant-settings.settings.location.view',
+ iface: 'location-units',
+ },
+ ],
+ }
+ ];
- render() {
- // If this PR is accepted, we will not need to do this filtering by hand:
- // https://github.com/folio-org/stripes-smart-components/pull/1401#issuecomment-1771334495
- // But for now ...
- const sections = this.sections.map(section => ({
- label: section.label,
- pages: section.pages.filter(page => !page.iface || this.props.stripes.hasInterface(page.iface)),
- }));
+ const filteredSections = sections.map(section => ({
+ label: section.label,
+ pages: section.pages.filter(page => !page.iface || stripes.hasInterface(page.iface)),
+ }));
- return (
- }
- />
- );
- }
-}
+ return (
+ }
+ />
+ );
+};
-export default injectIntl(Organization);
+export default Organization;
diff --git a/src/settings/index.test.js b/src/settings/index.test.js
index d77e36c7..546f1236 100644
--- a/src/settings/index.test.js
+++ b/src/settings/index.test.js
@@ -1,22 +1,12 @@
import React from 'react';
-import '../../test/jest/__mocks__';
-import {
- renderWithRouter
-} from '../../test/jest/helpers';
-import buildStripes from '../../test/jest/__new_mocks__/stripesCore.mock';
-
+import { renderWithRouter } from '../../test/jest/helpers';
import Organization from './index';
-const stripes = buildStripes();
-
-const intl = {
- formatMessage: jest.fn()
-};
+import '../../test/jest/__mocks__';
+import { mockHasInterface, mockHasPerm } from '../../test/jest/__mocks__/stripesCore.mock';
const props = {
- stripes,
- intl,
history: {},
location: {
pathname: '/tenant-settings'
@@ -30,43 +20,34 @@ const props = {
};
describe('Organization', () => {
- beforeEach(() => {
- stripes.discovery = {
- interfaces: {}
- };
- stripes.setIsAuthenticated = jest.fn();
- stripes.hasInterface = jest.fn().mockReturnValue(false);
- stripes.hasPerm = jest.fn().mockReturnValue(false);
- });
-
it('should render SSO Settings when login-saml interface is present', () => {
- stripes.hasInterface = jest.fn().mockReturnValue(true);
- stripes.hasPerm = jest.fn().mockReturnValue(true);
const { queryByText } = renderWithRouter();
expect(queryByText('ui-tenant-settings.settings.ssoSettings.label')).toBeTruthy();
});
it('should not render SSO Settings when login-saml interface is not present', () => {
- const { queryByText } = renderWithRouter();
- expect(queryByText('ui-tenant-settings.settings.ssoSettings.label')).toBeNull();
- });
+ mockHasInterface.mockReturnValue(false);
+ mockHasPerm.mockReturnValue(false);
- it('should render Reading room access when associated permission and interface are present', () => {
- stripes.hasInterface = jest.fn().mockReturnValue(true);
- stripes.hasPerm = jest.fn().mockReturnValue(true);
const { queryByText } = renderWithRouter();
- expect(queryByText('ui-tenant-settings.settings.reading-room-access.label')).toBeTruthy();
+ expect(queryByText('ui-tenant-settings.settings.ssoSettings.label')).toBeNull();
});
it('should not render Reading room access when ui-tenant-settings.settings.reading-room-access.view permission is not present', () => {
- stripes.hasInterface = jest.fn().mockReturnValue(true);
const { queryByText } = renderWithRouter();
expect(queryByText('ui-tenant-settings.settings.reading-room-access.label')).toBeNull();
});
it('should not render Reading room access when reading-room interface is not present', () => {
- stripes.hasPerm = jest.fn().mockReturnValue(true);
const { queryByText } = renderWithRouter();
expect(queryByText('ui-tenant-settings.settings.reading-room-access.label')).toBeNull();
});
+
+ it('should render Reading room access when associated permission and interface are present', () => {
+ mockHasInterface.mockReturnValue(true);
+ mockHasPerm.mockReturnValue(true);
+
+ const { queryByText } = renderWithRouter();
+ expect(queryByText('ui-tenant-settings.settings.reading-room-access.label')).toBeTruthy();
+ });
});
diff --git a/test/jest/__mocks__/stripesCore.mock.js b/test/jest/__mocks__/stripesCore.mock.js
index 096eb1ce..8dd4b970 100644
--- a/test/jest/__mocks__/stripesCore.mock.js
+++ b/test/jest/__mocks__/stripesCore.mock.js
@@ -1,47 +1,57 @@
-import React from 'react';
-
-jest.mock('@folio/stripes/core', () => {
- const STRIPES = {
- actionNames: [],
- clone: () => ({ ...STRIPES }),
- connect: (Component) => Component,
- config: {},
- currency: 'USD',
- hasInterface: () => true,
- hasPerm: jest.fn().mockReturnValue(true),
- locale: 'en-US',
- logger: {
- log: () => { },
- },
- okapi: {
- tenant: 'diku',
- url: 'https://folio-testing-okapi.dev.folio.org',
- },
- plugins: {},
- setBindings: () => { },
- setCurrency: () => { },
- setLocale: () => { },
- setSinglePlugin: () => { },
- setTimezone: () => { },
- setToken: () => { },
- store: {
- getState: () => { },
- dispatch: () => { },
- subscribe: () => { },
- replaceReducer: () => { },
- },
- timezone: 'UTC',
+export const mockHasPerm = jest.fn(() => true);
+export const mockHasInterface = jest.fn().mockReturnValue(true);
+export const mockUseOkapiKy = jest.fn();
+
+export const buildStripes = (otherProperties = {}) => ({
+ actionNames: [],
+ clone: buildStripes,
+ connect: Comp => Comp,
+ config: {
+ platformName: 'bulk-edit'
+ },
+ currency: 'USD',
+ hasInterface: jest.fn().mockReturnValue(true),
+ hasPerm: mockHasPerm,
+ locale: 'en-US',
+ logger: {
+ log: () => { },
+ },
+ okapi: {
+ tenant: 'diku',
+ url: 'https://folio-testing-okapi.dev.folio.org',
+ },
+ plugins: {},
+ setBindings: () => { },
+ setCurrency: () => { },
+ setLocale: () => { },
+ setSinglePlugin: () => { },
+ setTimezone: () => { },
+ setToken: () => { },
+ store: {
+ getState: () => { },
+ dispatch: () => { },
+ subscribe: () => { },
+ replaceReducer: () => { },
+ },
+ timezone: 'UTC',
+ user: {
+ perms: {},
user: {
- perms: {},
- user: {
- id: 'b1add99d-530b-5912-94f3-4091b4d87e2c',
- username: 'diku_admin',
+ id: 'b1add99d-530b-5912-94f3-4091b4d87e2c',
+ username: 'diku_admin',
+ consortium: {
+ centralTenantId: 'consortia',
},
},
- withOkapi: true,
- };
+ },
+ withOkapi: true,
+ ...otherProperties,
+});
- const stripesConnect = (Component, options) => ({ mutator, resources, stripes, ...rest }) => {
+const STRIPES = buildStripes();
+
+const mockStripesCore = {
+ stripesConnect: Component => ({ mutator, resources, stripes, ...rest }) => {
const fakeMutator = mutator || Object.keys(Component.manifest).reduce((acc, mutatorName) => {
const returnValue = Component.manifest[mutatorName].records ? [] : {};
@@ -51,59 +61,53 @@ jest.mock('@folio/stripes/core', () => {
POST: jest.fn().mockReturnValue(Promise.resolve()),
DELETE: jest.fn().mockReturnValue(Promise.resolve()),
reset: jest.fn(),
+ update: jest.fn(),
+ replace: jest.fn(),
};
return acc;
}, {});
const fakeResources = resources || Object.keys(Component.manifest).reduce((acc, resourceName) => {
- if (options?.resources?.[resourceName]) {
- acc[resourceName] = options.resources[resourceName];
- } else {
- acc[resourceName] = {
- records: [],
- };
- }
+ acc[resourceName] = {
+ records: [],
+ };
return acc;
}, {});
const fakeStripes = stripes || STRIPES;
- if (options?.stripes) {
- Object.assign(fakeStripes, options.stripes);
- }
-
return ;
- };
+ },
- const withStripes = (Component, options) => ({ stripes, ...rest }) => {
- const fakeStripes = stripes || STRIPES;
+ useOkapiKy: mockUseOkapiKy,
- fakeStripes.connect = Comp => stripesConnect(Comp, options);
+ useStripes: () => STRIPES,
- if (options?.stripes) {
- Object.assign(fakeStripes, options.stripes);
- }
+ withStripes: Component => ({ stripes, ...rest }) => {
+ const fakeStripes = stripes || STRIPES;
return ;
- };
-
- const useStripes = ({ ...STRIPES, connect: Component => stripesConnect(Component) });
- const TitleManager = jest.fn(({ children, ...rest }) => (
- {children}
- ));
-
- return {
- ...jest.requireActual('@folio/stripes/core'),
- stripesConnect,
- useStripes,
- withStripes,
- // eslint-disable-next-line react/prop-types
- Pluggable: props => <>{props.children}>,
- IfPermission: ({ children }) => <>{children}>,
- TitleManager,
- supportedLocales: ['ar', 'en', 'fr-FR'],
- supportedNumberingSystems: ['latn', 'arab'],
- };
-}, { virtual: true });
+ },
+
+ // eslint-disable-next-line react/prop-types
+ Pluggable: props => <>
+
+ >,
+
+ // eslint-disable-next-line react/prop-types
+ IfPermission: jest.fn(props => <>{props.children}>),
+
+ // eslint-disable-next-line react/prop-types
+ IfInterface: jest.fn(props => <>{props.children}>),
+
+ useNamespace: ({ key }) => [`@folio/bulk-edit:${key}`],
+ TitleManager: ({ children }) => <>{children}>,
+ checkIfUserInMemberTenant: () => true,
+};
+
+jest.mock('@folio/stripes/core', () => ({
+ ...jest.requireActual('@folio/stripes/core'),
+ ...mockStripesCore
+}), { virtual: true });