|
At least one property is required.
diff --git a/ui/src/app/admin/component/PropertyInput.js b/ui/src/app/admin/component/PropertyInput.js
new file mode 100644
index 000000000..5db0b6d1b
--- /dev/null
+++ b/ui/src/app/admin/component/PropertyInput.js
@@ -0,0 +1,57 @@
+import React, { Fragment } from 'react';
+import FloatingLabel from 'react-bootstrap/FloatingLabel';
+import Form from 'react-bootstrap/Form';
+import { useValidator } from '../../core/hooks/useValidator';
+import { useTranslator } from '../../i18n/hooks';
+
+export function PropertyInput({ property, onChange }) {
+
+ const translate = useTranslator();
+
+ const value = React.useMemo(() => {
+ const { propertyValue, displayType } = property;
+ switch (displayType) {
+ case 'number':
+ return parseInt(propertyValue);
+ case 'boolean':
+ return propertyValue ? Boolean(propertyValue) : false;
+ default:
+ return propertyValue || '';
+ }
+ }, [property]);
+
+ const { errors } = useValidator({ value }, {
+ value: {
+ custom: {
+ isValid: (value) => !value || value?.length <= 255,
+ message: translate(`message.must-be-unique`),
+ }
+ }
+ });
+
+ return (
+
+ {property.displayType !== 'boolean' ?
+
+ onChange({ id: property.resourceId, propertyValue: evt.target.value }) }
+ maxLength={255}
+ isInvalid={ errors.value } />
+
+ :
+ onChange({ id: property.resourceId, propertyValue: evt.target.checked }) }
+ className="my-3" />
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/component/PropertySelector.js b/ui/src/app/admin/component/PropertySelector.js
index 9f219e403..30091f98a 100644
--- a/ui/src/app/admin/component/PropertySelector.js
+++ b/ui/src/app/admin/component/PropertySelector.js
@@ -20,6 +20,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) {
}
const cat = {category: item, propertyName: item, isCategory: true};
const catSelected = selected.some(s => s.propertyName === item);
+ const sorted = orderBy(grouped[item], 'propertyName');
return (
{index !== 0 && }
@@ -32,7 +33,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) {
{item} - Add all
- {grouped[item].map((i) => {
+ {sorted.map((i) => {
if (!properties.some((p) => p.propertyName === i.propertyName)) {
index = index + 1;
const item =
diff --git a/ui/src/app/admin/container/ConfigurationList.js b/ui/src/app/admin/container/ConfigurationList.js
index 6595dbe78..d025db4bc 100644
--- a/ui/src/app/admin/container/ConfigurationList.js
+++ b/ui/src/app/admin/container/ConfigurationList.js
@@ -15,8 +15,9 @@ import { useTranslator } from '../../i18n/hooks';
import useFetch from 'use-http';
import API_BASE_PATH from '../../App.constant';
import { downloadAsZip } from '../../core/utility/download_as';
+import { useGetConfigurationsQuery } from '../../store/configurations/ConfigurationApi';
-export function ConfigurationList({ configurations, onDelete, loading }) {
+export function ConfigurationList({ onDelete, loading }) {
const remove = (id) => {
onDelete(id);
@@ -24,6 +25,8 @@ export function ConfigurationList({ configurations, onDelete, loading }) {
const translate = useTranslator();
+ const { data: configurations } = useGetConfigurationsQuery();
+
const downloader = useFetch(`${API_BASE_PATH}/shib/property/set`, {
cachePolicy: 'no-cache',
headers: {
diff --git a/ui/src/app/admin/container/EditConfiguration.js b/ui/src/app/admin/container/EditConfiguration.js
index 05978d9e7..5555e7fdc 100644
--- a/ui/src/app/admin/container/EditConfiguration.js
+++ b/ui/src/app/admin/container/EditConfiguration.js
@@ -1,64 +1,52 @@
-import React from 'react';
-import { useDispatch } from 'react-redux';
+import React, { Fragment } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import Translate from '../../i18n/components/translate';
-import { useConfiguration } from '../hooks';
import { ConfigurationForm } from '../component/ConfigurationForm';
-import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice';
-import { useTranslator } from '../../i18n/hooks';
-import { PropertiesProvider } from '../hoc/PropertiesProvider';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { usePrompt } from '../../core/hooks/usePrompt';
+import { useGetConfigurationQuery, useGetConfigurationsQuery, useUpdateConfigurationMutation } from '../../store/configurations/ConfigurationApi';
+import { configurationReceived } from '../../store/configurations/ConfigurationSlice';
+import { useDispatch } from 'react-redux';
-export function EditConfiguration({ configurations }) {
+export function EditConfiguration() {
const navigate = useNavigate();
- const notifier = useDispatch();
- const translator = useTranslator();
const { id } = useParams();
+ const dispatch = useDispatch();
- const { put, get, response, loading } = useConfiguration({});
+ const { data: configuration = {}, isFetching: loadingDetail, isSuccess: loaded } = useGetConfigurationQuery(id);
+ const { data: configurations = [], isFetching: loadingList } = useGetConfigurationsQuery();
const [blocking, setBlocking] = React.useState(false);
- const [configuration, setConfiguration] = React.useState();
-
- async function save(config) {
- let toast;
- const resp = await put(`${config.resourceId}`, config);
- if (response.ok) {
- gotoList();
- toast = createNotificationAction(`Updated configuration successfully.`, NotificationTypes.SUCCESS);
- } else {
- toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR);
- }
- if (toast) {
- notifier(toast);
- }
- };
-
- async function loadConfiguration(id) {
- const config = await get(`/${id}`);
- if (response.ok) {
- setConfiguration(config);
- }
- }
-
- /*eslint-disable react-hooks/exhaustive-deps*/
- React.useEffect(() => { loadConfiguration(id) }, []);
+ const [update, {isSuccess}] = useUpdateConfigurationMutation();
const cancel = () => {
gotoList();
};
- const gotoList = () => {
+ const gotoList = React.useCallback(() => {
setBlocking(false);
setTimeout(() => navigate(`/configurations`), 0);
- };
+ }, [navigate]);
+
+ React.useEffect(() => {
+ if (isSuccess) {
+ gotoList({ refresh: true });
+ }
+ }, [isSuccess, gotoList]);
+
+ React.useEffect(() => {
+ if (loaded) {
+ dispatch(configurationReceived(configuration));
+ }
+ }, [loaded, configuration, dispatch]);
const prompt = usePrompt(blocking, `message.unsaved-editor`);
+ const { resourceId } = configuration;
+
return (
{prompt}
@@ -71,19 +59,23 @@ export function EditConfiguration({ configurations }) {
- {loading ?
+ {loadingDetail || loadingList ?
:
-
+
{configuration && save(data)}
- onCancel={() => cancel()} /> }
-
+ loading={loadingDetail || loadingList}
+ onSave={(configuration) => update({ id: resourceId, configuration: {
+ ...configuration,
+ resourceId
+ } })}
+ onCancel={() => cancel()} />
+ }
+
}
diff --git a/ui/src/app/admin/container/MetadataActions.js b/ui/src/app/admin/container/MetadataActions.js
index 80adbf8ec..3fd77264a 100644
--- a/ui/src/app/admin/container/MetadataActions.js
+++ b/ui/src/app/admin/container/MetadataActions.js
@@ -3,7 +3,7 @@ import { DeleteConfirmation } from '../../core/components/DeleteConfirmation';
import { useMetadataActivator, useMetadataApprover, useMetadataEntity } from '../../metadata/hooks/api';
import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice';
-import { useApproveSourceMutation, useDeleteSourceMutation, useEnableSourceMutation } from '../../store/metadata/SourceSlice';
+import { useApproveSourceMutation, useDeleteSourceMutation, useEnableSourceMutation } from '../../store/metadata/SourceApi';
import { useDispatch } from 'react-redux';
export function MetadataActions ({type, children}) {
diff --git a/ui/src/app/admin/container/NewConfiguration.js b/ui/src/app/admin/container/NewConfiguration.js
index 4e0f96a80..fb644893f 100644
--- a/ui/src/app/admin/container/NewConfiguration.js
+++ b/ui/src/app/admin/container/NewConfiguration.js
@@ -2,39 +2,22 @@ import React from 'react';
import { useNavigate } from 'react-router-dom';
import Translate from '../../i18n/components/translate';
-import { useConfiguration } from '../hooks';
-import { Schema } from '../../form/Schema';
import { ConfigurationForm } from '../component/ConfigurationForm';
-
-import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice';
-import { useTranslator } from '../../i18n/hooks';
-import { BASE_PATH } from '../../App.constant';
-import { PropertiesProvider } from '../hoc/PropertiesProvider';
-import { useDispatch } from 'react-redux';
import { usePrompt } from '../../core/hooks/usePrompt';
+import { useGetConfigurationsQuery, useSaveConfigurationMutation } from '../../store/configurations/ConfigurationApi';
+import { useDispatch } from 'react-redux';
+import { configurationReceived } from '../../store/configurations/ConfigurationSlice';
-export function NewConfiguration({ configurations }) {
+export function NewConfiguration() {
const navigate = useNavigate();
- const notifier = useDispatch();
- const translator = useTranslator();
-
- const { post, response, loading } = useConfiguration({});
+ const dispatch = useDispatch();
const [blocking, setBlocking] = React.useState(false);
- async function save(config) {
- let toast;
- const resp = await post(``, config);
- if (response.ok) {
- gotoList({ refresh: true });
- toast = createNotificationAction(`Added configuration successfully.`, NotificationTypes.SUCCESS);
- } else {
- toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR);
- }
- if (toast) {
- notifier(toast);
- }
- };
+ const [create, {isSuccess, isLoading: loading}] = useSaveConfigurationMutation();
+ const { data: configurations = [] } = useGetConfigurationsQuery();
+
+ const save = React.useCallback(async (configuration) => await create({configuration}), [create]);
const cancel = () => {
gotoList();
@@ -42,10 +25,20 @@ export function NewConfiguration({ configurations }) {
const [configuration] = React.useState({});
- const gotoList = () => {
+ const gotoList = React.useCallback(() => {
setBlocking(false);
setTimeout(() => navigate(`/configurations`), 0);
- };
+ }, [navigate]);
+
+ React.useEffect(() => {
+ dispatch(configurationReceived({name: '', properties: []}));
+ }, [dispatch]);
+
+ React.useEffect(() => {
+ if (isSuccess) {
+ gotoList({ refresh: true });
+ }
+ }, [isSuccess, gotoList]);
const prompt = usePrompt(blocking, `message.unsaved-editor`);
@@ -61,18 +54,12 @@ export function NewConfiguration({ configurations }) {
-
-
- {(schema) =>
- save(data)}
- onCancel={() => cancel()} />}
-
-
+ save(data)}
+ onCancel={() => cancel()} />
diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js
index 0dd84a099..ba1fd5fbd 100644
--- a/ui/src/app/admin/container/UserManagement.js
+++ b/ui/src/app/admin/container/UserManagement.js
@@ -8,7 +8,7 @@ import Button from 'react-bootstrap/Button';
import Translate from '../../i18n/components/translate';
import API_BASE_PATH from '../../App.constant';
-import { useRemoveUserMutation, useSetUserGroupRequestMutation, useSetUserRoleRequestMutation } from '../../store/user/UserSlice';
+import { useRemoveUserMutation, useSetUserGroupRequestMutation, useSetUserRoleRequestMutation } from '../../store/user/UserApi';
import { cloneDeep } from 'lodash';
export default function UserManagement({ users = [], children, reload}) {
diff --git a/ui/src/app/admin/hoc/ConfigurationsProvider.js b/ui/src/app/admin/hoc/ConfigurationsProvider.js
deleted file mode 100644
index 0a710a579..000000000
--- a/ui/src/app/admin/hoc/ConfigurationsProvider.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import { useConfigurations } from '../hooks';
-import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice';
-import { useTranslator } from '../../i18n/hooks';
-import { useDispatch } from 'react-redux';
-
-export function ConfigurationsProvider({ children, cache = 'no-cache' }) {
-
- const [configurations, setConfigurations] = React.useState([]);
-
- const notifier = useDispatch();
- const translator = useTranslator();
-
- const { get, del, response, loading } = useConfigurations({
- cachePolicy: cache
- });
-
- async function loadConfigurations() {
- const list = await get(`shib/property/set`);
- if (response.ok) {
- setConfigurations(list);
- }
- }
-
- async function removeConfiguration(id) {
- let toast;
- const resp = await del(`shib/property/set/${id}`);
- if (response.ok) {
- loadConfigurations();
- toast = createNotificationAction(`Deleted property successfully.`, NotificationTypes.SUCCESS);
- } else {
- toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR);
- }
- if (toast) {
- notifier(toast);
- }
- }
-
- /*eslint-disable react-hooks/exhaustive-deps*/
- React.useEffect(() => { loadConfigurations() }, []);
-
- return (<>{children(configurations, removeConfiguration, loading)}>);
-}
diff --git a/ui/src/app/admin/hoc/PropertiesProvider.js b/ui/src/app/admin/hoc/PropertiesProvider.js
deleted file mode 100644
index 5ab7bde75..000000000
--- a/ui/src/app/admin/hoc/PropertiesProvider.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import useFetch from 'use-http';
-import API_BASE_PATH from '../../App.constant';
-
-const PropertiesContext = React.createContext();
-
-const { Provider, Consumer } = PropertiesContext;
-
-function PropertiesProvider({ children, cache = 'no-cache' }) {
-
- const [properties, setProperties] = React.useState([]);
-
-
- const { get, response, loading } = useFetch('', {
- cachePolicy: cache
- });
-
- async function loadProperties() {
- const list = await get(`${API_BASE_PATH}/shib/properties`);
- if (response.ok) {
- setProperties(list);
- }
- }
-
- /*eslint-disable react-hooks/exhaustive-deps*/
- React.useEffect(() => { loadProperties() }, []);
-
- return ({children});
-}
-
-function useProperties() {
- const { properties } = React.useContext(PropertiesContext);
- return properties.map((p, idx) => !p.category || p.category === '?' ? { ...p, category: 'Misc' } : p);;
-}
-
-function usePropertiesLoading() {
- const { loading } = React.useContext(PropertiesContext);
- return loading;
-}
-
-export {
- PropertiesProvider,
- PropertiesContext,
- Consumer as PropertiesConsumer,
- useProperties,
- usePropertiesLoading,
-};
diff --git a/ui/src/app/core/hooks/useValidator.js b/ui/src/app/core/hooks/useValidator.js
new file mode 100644
index 000000000..c4f0c5fef
--- /dev/null
+++ b/ui/src/app/core/hooks/useValidator.js
@@ -0,0 +1,32 @@
+import React from 'react';
+
+export function useValidator (data, validations) {
+
+ const errors = React.useMemo(() => {
+ const errors = {};
+ for (const key in validations) {
+ const value = data[key];
+ const validation = validations[key];
+ if (validation?.required?.value && !value) {
+ errors[key] = validation?.required?.message;
+ }
+
+ const pattern = validation?.pattern;
+ if (pattern?.value && !RegExp(pattern.value).test(value)) {
+ errors[key] = pattern.message;
+ }
+
+ const custom = validation?.custom;
+ if (custom?.isValid && !custom.isValid(value)) {
+ errors[key] = custom.message;
+ }
+ }
+
+ return errors;
+ }, [data, validations]);
+
+ return {
+ data,
+ errors,
+ };
+};
\ No newline at end of file
diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js
index 4706be423..077b2f78e 100644
--- a/ui/src/app/dashboard/view/ActionsTab.js
+++ b/ui/src/app/dashboard/view/ActionsTab.js
@@ -9,13 +9,13 @@ import { NavLink, Outlet } from 'react-router-dom';
import Translate from '../../i18n/components/translate';
-import { useGetNewUsersQuery } from '../../store/user/UserSlice';
-import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice';
+import { useGetNewUsersQuery } from '../../store/user/UserApi';
+import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi';
import {
useGetDisabledRegistrationsQuery,
useGetUnapprovedRegistrationsQuery
-} from '../../store/dynamic-registration/DynamicRegistrationSlice';
+} from '../../store/dynamic-registration/DynamicRegistrationApi';
import { useIsAdmin, useIsApprover } from '../../core/user/UserContext';
export function ActionsTab() {
diff --git a/ui/src/app/dashboard/view/AdminTab.js b/ui/src/app/dashboard/view/AdminTab.js
index a5b3f8ba8..8b476f99b 100644
--- a/ui/src/app/dashboard/view/AdminTab.js
+++ b/ui/src/app/dashboard/view/AdminTab.js
@@ -4,7 +4,7 @@ import UserMaintenance from '../../admin/component/UserMaintenance';
import Translate from '../../i18n/components/translate';
import Spinner from '../../core/components/Spinner';
-import { useGetUsersQuery } from '../../store/user/UserSlice';
+import { useGetUsersQuery } from '../../store/user/UserApi';
export function AdminTab () {
diff --git a/ui/src/app/dashboard/view/ApproveRegistrations.js b/ui/src/app/dashboard/view/ApproveRegistrations.js
index fb536450d..c7bad45a4 100644
--- a/ui/src/app/dashboard/view/ApproveRegistrations.js
+++ b/ui/src/app/dashboard/view/ApproveRegistrations.js
@@ -2,7 +2,7 @@ import React from 'react';
import Spinner from '../../core/components/Spinner';
import { DynamicRegistrationList } from '../../dynamic-registration/component/DynamicRegistrationList';
import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions';
-import { useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi';
export function ApproveRegistrations () {
diff --git a/ui/src/app/dashboard/view/ApproveSources.js b/ui/src/app/dashboard/view/ApproveSources.js
index b8adc082d..d8645212f 100644
--- a/ui/src/app/dashboard/view/ApproveSources.js
+++ b/ui/src/app/dashboard/view/ApproveSources.js
@@ -2,7 +2,7 @@ import React from 'react';
import { MetadataActions } from '../../admin/container/MetadataActions';
import Spinner from '../../core/components/Spinner';
import SourceList from '../../metadata/domain/source/component/SourceList';
-import { useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice';
+import { useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi';
export function ApproveSources () {
diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js
index e43753559..5d5ac7826 100644
--- a/ui/src/app/dashboard/view/Dashboard.js
+++ b/ui/src/app/dashboard/view/Dashboard.js
@@ -10,9 +10,9 @@ import { useCurrentUserLoading, useIsAdmin, useIsApprover } from '../../core/use
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import Badge from 'react-bootstrap/Badge';
-import { useGetNewUsersQuery } from '../../store/user/UserSlice';
-import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice';
-import { useGetDisabledRegistrationsQuery, useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useGetNewUsersQuery } from '../../store/user/UserApi';
+import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi';
+import { useGetDisabledRegistrationsQuery, useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi';
export function Dashboard () {
diff --git a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js
index 0de7b27d7..2292795a3 100644
--- a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js
+++ b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js
@@ -5,7 +5,7 @@ import { Search } from '../component/Search';
import {DynamicRegistrationList} from '../../dynamic-registration/component/DynamicRegistrationList';
import {
useGetDynamicRegistrationsQuery
-} from '../../store/dynamic-registration/DynamicRegistrationSlice';
+} from '../../store/dynamic-registration/DynamicRegistrationApi';
import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
diff --git a/ui/src/app/dashboard/view/EnableRegistrations.js b/ui/src/app/dashboard/view/EnableRegistrations.js
index 0d0fe2814..dd29f95d9 100644
--- a/ui/src/app/dashboard/view/EnableRegistrations.js
+++ b/ui/src/app/dashboard/view/EnableRegistrations.js
@@ -3,7 +3,7 @@ import { ProtectRoute } from '../../core/components/ProtectRoute';
import Spinner from '../../core/components/Spinner';
import { DynamicRegistrationList } from '../../dynamic-registration/component/DynamicRegistrationList';
import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions';
-import { useGetDisabledRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useGetDisabledRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi';
export function EnableRegistrations () {
diff --git a/ui/src/app/dashboard/view/EnableSources.js b/ui/src/app/dashboard/view/EnableSources.js
index 27b2aaa53..b6ff81c1f 100644
--- a/ui/src/app/dashboard/view/EnableSources.js
+++ b/ui/src/app/dashboard/view/EnableSources.js
@@ -3,7 +3,7 @@ import { MetadataActions } from '../../admin/container/MetadataActions';
import { ProtectRoute } from '../../core/components/ProtectRoute';
import Spinner from '../../core/components/Spinner';
import SourceList from '../../metadata/domain/source/component/SourceList';
-import { useGetDisabledSourcesQuery } from '../../store/metadata/SourceSlice';
+import { useGetDisabledSourcesQuery } from '../../store/metadata/SourceApi';
export function EnableSources () {
diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js
index d2553b19c..4f03ed4fb 100644
--- a/ui/src/app/dashboard/view/SourcesTab.js
+++ b/ui/src/app/dashboard/view/SourcesTab.js
@@ -6,7 +6,7 @@ import SourceList from '../../metadata/domain/source/component/SourceList';
import { Search } from '../component/Search';
import { Spinner } from '../../core/components/Spinner';
-import { useChangeSourceGroupMutation, useGetSourcesQuery } from '../../store/metadata/SourceSlice';
+import { useChangeSourceGroupMutation, useGetSourcesQuery } from '../../store/metadata/SourceApi';
import { Link } from 'react-router-dom';
import { faCube } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
diff --git a/ui/src/app/dashboard/view/UserAccess.js b/ui/src/app/dashboard/view/UserAccess.js
index c26af6c89..eab07d4de 100644
--- a/ui/src/app/dashboard/view/UserAccess.js
+++ b/ui/src/app/dashboard/view/UserAccess.js
@@ -2,7 +2,7 @@ import React from 'react';
import UserActions from '../../admin/container/UserActions';
import { ProtectRoute } from '../../core/components/ProtectRoute';
import Spinner from '../../core/components/Spinner';
-import { useGetNewUsersQuery } from '../../store/user/UserSlice';
+import { useGetNewUsersQuery } from '../../store/user/UserApi';
export function UserAccess () {
diff --git a/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js
index ad876a73e..1c70eeead 100644
--- a/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js
+++ b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js
@@ -9,7 +9,7 @@ import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/F
import { useDynamicRegistrationUiSchema } from '../api';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
-import { useGetDynamicRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useGetDynamicRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi';
import DynamicRegistrationDefinition from '../hoc/DynamicConfigurationDefinition';
diff --git a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js
index 768ff3172..6658923ed 100644
--- a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js
+++ b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js
@@ -7,7 +7,7 @@ import {
useApproveDynamicRegistrationMutation,
useEnableDynamicRegistrationMutation,
useChangeDynamicRegistrationGroupMutation,
-} from '../../store/dynamic-registration/DynamicRegistrationSlice';
+} from '../../store/dynamic-registration/DynamicRegistrationApi';
export function DynamicRegistrationActions ({ children }) {
diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js
index 1aaee37e9..43359576e 100644
--- a/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js
+++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js
@@ -6,7 +6,7 @@ import { Schema } from '../../form/Schema';
import { FormManager } from '../../form/FormManager';
import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm';
import DynamicConfigurationDefinition from '../hoc/DynamicConfigurationDefinition';
-import { useCreateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useCreateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationApi';
import Spinner from '../../core/components/Spinner';
import { usePrompt } from '../../core/hooks/usePrompt';
diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js
index f21eff8d0..1c0564840 100644
--- a/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js
+++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js
@@ -13,7 +13,7 @@ import { MetadataConfiguration } from '../../metadata/component/MetadataConfigur
import { Schema } from '../../form/Schema';
import definition from '../hoc/DynamicConfigurationDefinition';
-import { useSelectDynamicRegistrationQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useSelectDynamicRegistrationQuery } from '../../store/dynamic-registration/DynamicRegistrationApi';
import { DynamicRegistrationActions } from '../hoc/DynamicRegistrationActions';
import { useCanEnable, useIsAdmin } from '../../core/user/UserContext';
import { GroupsProvider } from '../../admin/hoc/GroupsProvider';
diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js
index 0170e45c6..15a1bdb78 100644
--- a/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js
+++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js
@@ -6,7 +6,7 @@ import { Schema } from '../../form/Schema';
import { FormManager } from '../../form/FormManager';
import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm';
import DynamicConfigurationDefinition from '../hoc/DynamicConfigurationDefinition';
-import { useSelectDynamicRegistrationQuery, useUpdateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationSlice';
+import { useSelectDynamicRegistrationQuery, useUpdateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationApi';
import Spinner from '../../core/components/Spinner';
import { usePrompt } from '../../core/hooks/usePrompt';
diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js
index 5fffb7c63..5ced7e7ce 100644
--- a/ui/src/app/metadata/hoc/MetadataSelector.js
+++ b/ui/src/app/metadata/hoc/MetadataSelector.js
@@ -1,7 +1,7 @@
import React from 'react';
import { useLocation, useParams } from 'react-router';
import Spinner from '../../core/components/Spinner';
-import { useLazySelectSourceQuery } from '../../store/metadata/SourceSlice';
+import { useLazySelectSourceQuery } from '../../store/metadata/SourceApi';
import { useMetadataEntity } from '../hooks/api';
export const MetadataTypeContext = React.createContext();
diff --git a/ui/src/app/store/configurations/ConfigurationApi.js b/ui/src/app/store/configurations/ConfigurationApi.js
new file mode 100644
index 000000000..a7404bb80
--- /dev/null
+++ b/ui/src/app/store/configurations/ConfigurationApi.js
@@ -0,0 +1,71 @@
+import { createApi } from '@reduxjs/toolkit/query/react';
+import { getBaseQuery } from '../baseQuery';
+import { createNotificationAction } from '../notifications/NotificationSlice';
+
+export const ConfigurationApi = createApi({
+ reducerPath: 'configurationApi',
+ tagTypes: ['Configuration'],
+ baseQuery: getBaseQuery(),
+ endpoints: (builder) => ({
+ getConfigurations: builder.query({
+ query: () => `/shib/property/set`,
+ providesTags: ['Configuration'],
+ }),
+ getConfiguration: builder.query({
+ query: (id) => `/shib/property/set/${id}`,
+ providesTags: ['Configuration'],
+ }),
+ saveConfiguration: builder.mutation({
+ query: ({ configuration: body }) => ({
+ url: `/shib/property/set`,
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: ['Configuration'],
+ async onQueryStarted(
+ arg,
+ { dispatch, queryFulfilled }
+ ) {
+ await queryFulfilled;
+ dispatch(createNotificationAction(`Added configuration successfully.`));
+ },
+ }),
+ updateConfiguration: builder.mutation({
+ query: ({ id, configuration: body }) => ({
+ url: `/shib/property/set/${id}`,
+ method: 'PUT',
+ body,
+ }),
+ invalidatesTags: ['Configuration'],
+ async onQueryStarted(
+ arg,
+ { dispatch, queryFulfilled }
+ ) {
+ await queryFulfilled;
+ dispatch(createNotificationAction(`Updated configuration successfully.`));
+ },
+ }),
+ deleteConfiguration: builder.mutation({
+ query: ({id}) => ({
+ url: `/shib/property/set/${id}`,
+ method: 'DELETE'
+ }),
+ invalidatesTags: ['Configuration'],
+ async onQueryStarted(
+ arg,
+ { dispatch, queryFulfilled }
+ ) {
+ await queryFulfilled;
+ dispatch(createNotificationAction(`Deleted configuration successfully.`));
+ },
+ })
+ }),
+})
+
+export const {
+ useGetConfigurationsQuery,
+ useGetConfigurationQuery,
+ useDeleteConfigurationMutation,
+ useSaveConfigurationMutation,
+ useUpdateConfigurationMutation,
+} = ConfigurationApi;
diff --git a/ui/src/app/store/configurations/ConfigurationSlice.js b/ui/src/app/store/configurations/ConfigurationSlice.js
new file mode 100644
index 000000000..9bb4e7348
--- /dev/null
+++ b/ui/src/app/store/configurations/ConfigurationSlice.js
@@ -0,0 +1,45 @@
+import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
+
+import { useSelector } from 'react-redux';
+
+const PropertiesAdapter = createEntityAdapter({
+ // Keep the "all IDs" array sorted based on book titles
+ sortComparer: (a, b) => a.propertyName.localeCompare(b.propertyName),
+ selectId: (prop) => prop.resourceId,
+})
+
+export const ConfigurationSlice = createSlice({
+ name: 'properties',
+ initialState: PropertiesAdapter.getInitialState({
+ name: '',
+ }),
+ reducers: {
+ propertyAdded: PropertiesAdapter.addOne,
+ configurationReceived(state, { payload: { properties, name } }) {
+ PropertiesAdapter.setAll(state, properties);
+ state.name = name;
+ },
+ nameUpdated (state, {payload}) {
+ state.name = payload
+ },
+ propertyUpdated: PropertiesAdapter.updateOne,
+ propertyRemoved: PropertiesAdapter.removeOne
+ }
+});
+
+const selectConfigurationState = state => state.configuration;
+
+export function useConfiguration() {
+ const config = useSelector(selectConfigurationState);
+ return ({ ...config });
+}
+
+export const {
+ propertyAdded,
+ propertyUpdated,
+ propertyRemoved,
+ configurationReceived,
+ nameUpdated,
+} = ConfigurationSlice.actions
+
+export default ConfigurationSlice.reducer;
\ No newline at end of file
diff --git a/ui/src/app/store/configurations/PropertyApi.js b/ui/src/app/store/configurations/PropertyApi.js
new file mode 100644
index 000000000..6c6a62966
--- /dev/null
+++ b/ui/src/app/store/configurations/PropertyApi.js
@@ -0,0 +1,19 @@
+import { createApi } from '@reduxjs/toolkit/query/react';
+import { getBaseQuery } from '../baseQuery';
+
+export const PropertyApi = createApi({
+ reducerPath: 'propertyApi',
+ tagTypes: ['Property'],
+ baseQuery: getBaseQuery(),
+ endpoints: (builder) => ({
+ getProperties: builder.query({
+ query: () => `/shib/properties`,
+ providesTags: ['Property'],
+ transformResponse: (properties) => properties.map((p) => !p.category || p.category === '?' ? { ...p, category: 'Misc' } : p)
+ }),
+ }),
+})
+
+export const {
+ useGetPropertiesQuery
+} = PropertyApi;
diff --git a/ui/src/app/store/dynamic-registration/DynamicRegistrationSlice.js b/ui/src/app/store/dynamic-registration/DynamicRegistrationApi.js
similarity index 100%
rename from ui/src/app/store/dynamic-registration/DynamicRegistrationSlice.js
rename to ui/src/app/store/dynamic-registration/DynamicRegistrationApi.js
diff --git a/ui/src/app/store/errorHandler.js b/ui/src/app/store/errorHandler.js
new file mode 100644
index 000000000..e8ad1b3f9
--- /dev/null
+++ b/ui/src/app/store/errorHandler.js
@@ -0,0 +1,17 @@
+import { isRejectedWithValue } from '@reduxjs/toolkit';
+import { createNotificationAction, NotificationTypes } from './notifications/NotificationSlice';
+
+
+export const rtkQueryErrorLogger = (api) => (next) => (action) => {
+
+ const { dispatch } = api;
+
+ if (isRejectedWithValue(action)) {
+ const { payload } = action;
+ const { data } = payload;
+ const { errorCode, errorMessage } = data;
+ dispatch(createNotificationAction(`${errorCode} - ${errorMessage}`, NotificationTypes.ERROR));
+ }
+
+ return next(action);
+}
\ No newline at end of file
diff --git a/ui/src/app/store/index.js b/ui/src/app/store/index.js
index a37083ca7..d05c31b9f 100644
--- a/ui/src/app/store/index.js
+++ b/ui/src/app/store/index.js
@@ -1,22 +1,32 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
-import { UserAdminApi } from './user/UserSlice';
-import { DynamicRegistrationApi } from './dynamic-registration/DynamicRegistrationSlice';
-import { MetadataSourceApi } from './metadata/SourceSlice';
+import { UserAdminApi } from './user/UserApi';
+import { DynamicRegistrationApi } from './dynamic-registration/DynamicRegistrationApi';
+import { MetadataSourceApi } from './metadata/SourceApi';
import { NotificationSlice } from './notifications/NotificationSlice';
+import { ConfigurationApi } from './configurations/ConfigurationApi';
+import { PropertyApi } from './configurations/PropertyApi';
+import { rtkQueryErrorLogger } from './errorHandler';
+import { ConfigurationSlice } from './configurations/ConfigurationSlice';
export const store = configureStore({
reducer: {
[UserAdminApi.reducerPath]: UserAdminApi.reducer,
[DynamicRegistrationApi.reducerPath]: DynamicRegistrationApi.reducer,
[MetadataSourceApi.reducerPath]: MetadataSourceApi.reducer,
+ [PropertyApi.reducerPath]: PropertyApi.reducer,
+ [ConfigurationApi.reducerPath]: ConfigurationApi.reducer,
notification: NotificationSlice.reducer,
+ configuration: ConfigurationSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
+ rtkQueryErrorLogger,
UserAdminApi.middleware,
DynamicRegistrationApi.middleware,
- MetadataSourceApi.middleware
+ MetadataSourceApi.middleware,
+ PropertyApi.middleware,
+ ConfigurationApi.middleware,
)
});
diff --git a/ui/src/app/store/metadata/SourceSlice.js b/ui/src/app/store/metadata/SourceApi.js
similarity index 100%
rename from ui/src/app/store/metadata/SourceSlice.js
rename to ui/src/app/store/metadata/SourceApi.js
diff --git a/ui/src/app/store/user/CurrentUserSlice.js b/ui/src/app/store/user/CurrentUserApi.js
similarity index 100%
rename from ui/src/app/store/user/CurrentUserSlice.js
rename to ui/src/app/store/user/CurrentUserApi.js
diff --git a/ui/src/app/store/user/UserSlice.js b/ui/src/app/store/user/UserApi.js
similarity index 100%
rename from ui/src/app/store/user/UserSlice.js
rename to ui/src/app/store/user/UserApi.js
|