Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/folio-org/ui-linked-data
Browse files Browse the repository at this point in the history
…into fix/UILD-410-tests-sonar-and-issues
  • Loading branch information
SKarolFolio committed Nov 11, 2024
2 parents 87324c7 + e07ece0 commit 96a5f4a
Show file tree
Hide file tree
Showing 30 changed files with 320 additions and 30 deletions.
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { ErrorBoundary } from '@components/ErrorBoundary';
import { Loading } from '@components/Loading';
import { ROUTES } from '@common/constants/routes.constants';
import { DEFAULT_LOCALE } from '@common/constants/i18n.constants';
import { OKAPI_CONFIG } from '@common/constants/api.constants';
import { localStorageService } from '@common/services/storage';
import { Root, Search, EditWrapper, ExternalResourcePreview } from '@views';
import state from '@state';
import en from '../translations/ui-linked-data/en.json';
import { AsyncIntlProvider, ServicesProvider } from './providers';
import './App.scss';

Expand Down Expand Up @@ -53,10 +55,12 @@ const createRouter = (basename: string) => createBrowserRouter(routes, { basenam

const Container: FC<IContainer> = ({ routePrefix = '', config }) => {
const setCustomEvents = useSetRecoilState(state.config.customEvents);
const cachedMessages = useRef({});
const setHasNavigationOrigin = useSetRecoilState(state.config.hasNavigationOrigin);
const cachedMessages = useRef({ [DEFAULT_LOCALE]: en });

useEffect(() => {
setCustomEvents(config?.customEvents as Record<string, string>);
config?.navigationOrigin && setHasNavigationOrigin(true);
}, [config]);

return (
Expand Down
8 changes: 5 additions & 3 deletions src/common/api/base.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ async function doRequest({ url, requestParams, headers }: DoRequest) {
});

if (!response.ok) {
const errorBody = await response.text();
const errorBody = await response.json();
throw errorBody;
}

return response;
} catch (err) {
console.error(err);
} catch (err: any) {
const selectedError = err?.errors?.[0];

selectedError && console.error(`${selectedError?.type}: ${selectedError?.message}`);

throw err;
}
Expand Down
15 changes: 15 additions & 0 deletions src/common/api/records.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ export const getRecord = async ({ recordId, idType }: IGetRecord) => {
});
};

const graphIdByInventoryIdUrl = '/resource/import/:recordId';

export const getGraphIdByExternalId = async ({ recordId }: IGetRecord) => {
const url = baseApi.generateUrl(graphIdByInventoryIdUrl, { name: ':recordId', value: recordId });

const response = await baseApi.request({
url,
requestParams: {
method: 'POST',
},
});

return await response?.json();
};

const singleRecordMarcUrl = `${BIBFRAME_API_ENDPOINT}/:recordId/marc`;

export const getMarcRecord = async ({ recordId, endpointUrl }: SingleRecord & { endpointUrl?: string }) => {
Expand Down
4 changes: 4 additions & 0 deletions src/common/constants/api.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const DEFAULT_PAGES_METADATA = {
totalPages: 0,
};

export enum ApiErrorCodes {
AlreadyExists = 'already_exists_error',
}

export enum ExternalResourceIdType {
Inventory = 'inventory',
}
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/i18n.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DEFAULT_LOCALE = 'en';
4 changes: 4 additions & 0 deletions src/common/helpers/api.helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getLookupDict } from '@common/api/lookup.api';
import { ApiErrorCodes } from '@common/constants/api.constants';

export const loadSimpleLookup = async (
uris: string | string[],
Expand Down Expand Up @@ -30,3 +31,6 @@ const fetchSimpleLookup = async (url: string): Promise<any> => {

return response;
};

export const checkHasErrorOfCodeType = (err: ApiError, codeType: ApiErrorCodes) =>
err?.errors.find(e => e.code === codeType);
21 changes: 19 additions & 2 deletions src/common/hooks/useContainerEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useRecoilValue } from 'recoil';
import { IS_EMBEDDED_MODE } from '@common/constants/build.constants';
import { dispatchEventWrapper, getWrapperAsWebComponent } from '@common/helpers/dom.helper';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ROUTES } from '@common/constants/routes.constants';

type IUseContainerEvents =
| {
Expand All @@ -12,9 +14,17 @@ type IUseContainerEvents =
| undefined;

export const useContainerEvents = ({ onTriggerModal, watchEditedState = false }: IUseContainerEvents = {}) => {
const hasNavigationOrigin = useRecoilValue(state.config.hasNavigationOrigin);
const isEdited = useRecoilValue(state.status.recordIsEdited);
const { BLOCK_NAVIGATION, UNBLOCK_NAVIGATION, TRIGGER_MODAL, PROCEED_NAVIGATION } =
useRecoilValue(state.config.customEvents) ?? {};
const {
BLOCK_NAVIGATION,
UNBLOCK_NAVIGATION,
TRIGGER_MODAL,
PROCEED_NAVIGATION,
NAVIGATE_TO_ORIGIN,
DROP_NAVIGATE_TO_ORIGIN,
} = useRecoilValue(state.config.customEvents) ?? {};
const navigate = useNavigate();

useEffect(() => {
if (IS_EMBEDDED_MODE && TRIGGER_MODAL && onTriggerModal) {
Expand All @@ -36,9 +46,16 @@ export const useContainerEvents = ({ onTriggerModal, watchEditedState = false }:

const dispatchProceedNavigationEvent = () => dispatchEventWrapper(PROCEED_NAVIGATION);

const dispatchNavigateToOriginEventWithFallback = (fallbackUri?: string) =>
hasNavigationOrigin ? dispatchEventWrapper(NAVIGATE_TO_ORIGIN) : navigate(fallbackUri ?? ROUTES.SEARCH.uri);

const dispatchDropNavigateToOriginEvent = () => dispatchEventWrapper(DROP_NAVIGATE_TO_ORIGIN);

return {
dispatchUnblockEvent,
dispatchProceedNavigationEvent,
dispatchBlockEvent,
dispatchNavigateToOriginEventWithFallback,
dispatchDropNavigateToOriginEvent,
};
};
4 changes: 2 additions & 2 deletions src/common/hooks/useLoadI18nMessages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { loadI18nMessages } from '@common/helpers/locales.helper';

export const useLoadI18nMessages = (cachedMessages: I18nMessages) => {
export const useLoadI18nMessages = (cachedMessages: I18nMessages, defaultLocale = 'en') => {
const getMessages = (locale: string) => {
if (cachedMessages?.[locale]) {
return cachedMessages?.[locale];
Expand All @@ -13,7 +13,7 @@ export const useLoadI18nMessages = (cachedMessages: I18nMessages) => {
const messages = await loadI18nMessages(locale);

if (messages) {
cachedMessages[locale] = messages;
cachedMessages[locale] = {...cachedMessages[defaultLocale], ...messages};
}
};

Expand Down
41 changes: 36 additions & 5 deletions src/common/hooks/useRecordControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { flushSync } from 'react-dom';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { applyUserValues } from '@common/helpers/profile.helper';
import { postRecord, putRecord, deleteRecord as deleteRecordRequest } from '@common/api/records.api';
import {
postRecord,
putRecord,
deleteRecord as deleteRecordRequest,
getGraphIdByExternalId,
getRecord,
} from '@common/api/records.api';
import { BibframeEntities, PROFILE_BFIDS } from '@common/constants/bibframe.constants';
import { StatusType } from '@common/constants/status.constants';
import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants';
Expand All @@ -17,15 +23,15 @@ import { UserNotificationFactory } from '@common/services/userNotification';
import { PreviewParams, useConfig } from '@common/hooks/useConfig.hook';
import { getSavedRecord } from '@common/helpers/record.helper';
import { formatRecord } from '@common/helpers/recordFormatting.helper';
import { getRecord } from '@common/api/records.api';
import { QueryParams, ROUTES } from '@common/constants/routes.constants';
import { BLOCKS_BFLITE } from '@common/constants/bibframeMapping.constants';
import { RecordStatus, ResourceType } from '@common/constants/record.constants';
import { generateEditResourceUrl } from '@common/helpers/navigation.helper';
import { useBackToSearchUri } from './useBackToSearchUri';
import state from '@state';
import { useContainerEvents } from './useContainerEvents';
import { ExternalResourceIdType } from '@common/constants/api.constants';
import { ApiErrorCodes, ExternalResourceIdType } from '@common/constants/api.constants';
import { checkHasErrorOfCodeType } from '@common/helpers/api.helper';

type SaveRecordProps = {
asRefToNewRecord?: boolean;
Expand Down Expand Up @@ -56,13 +62,14 @@ export const useRecordControls = () => {
const setCurrentlyEditedEntityBfid = useSetRecoilState(state.ui.currentlyEditedEntityBfid);
const setCurrentlyPreviewedEntityBfid = useSetRecoilState(state.ui.currentlyPreviewedEntityBfid);
const [selectedRecordBlocks, setSelectedRecordBlocks] = useRecoilState(state.inputs.selectedRecordBlocks);
const setIsDuplicateImportedResourceModalOpen = useSetRecoilState(state.ui.isDuplicateImportedResourceModalOpen);
const profile = PROFILE_BFIDS.MONOGRAPH;
const currentRecordId = getRecordId(record);
const { getProfiles } = useConfig();
const navigate = useNavigate();
const location = useLocation();
const searchResultsUri = useBackToSearchUri();
const { dispatchUnblockEvent } = useContainerEvents();
const { dispatchUnblockEvent, dispatchNavigateToOriginEventWithFallback } = useContainerEvents();
const [queryParams] = useSearchParams();
const isClone = queryParams.get(QueryParams.CloneOf);

Expand Down Expand Up @@ -203,7 +210,7 @@ export const useRecordControls = () => {
const discardRecord = (clearState = true) => {
if (clearState) clearRecordState();

navigate(searchResultsUri);
dispatchNavigateToOriginEventWithFallback(searchResultsUri);
};

const deleteRecord = async () => {
Expand Down Expand Up @@ -296,6 +303,29 @@ export const useRecordControls = () => {
});
};

const tryFetchExternalRecordForEdit = async (recordId?: string) => {
try {
if (!recordId) return;

setIsLoading(true);

const { id } = await getGraphIdByExternalId({ recordId });

id && navigate(generateEditResourceUrl(id), { replace: true });
} catch (err: unknown) {
if (checkHasErrorOfCodeType(err as ApiError, ApiErrorCodes.AlreadyExists)) {
setIsDuplicateImportedResourceModalOpen(true);
} else {
setStatusMessages(currentStatus => [
...currentStatus,
UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetchingExternalResourceForEditing'),
]);
}
} finally {
setIsLoading(false);
}
};

return {
fetchRecord,
saveRecord,
Expand All @@ -305,5 +335,6 @@ export const useRecordControls = () => {
clearRecordState,
fetchRecordAndSelectEntityValues,
fetchExternalRecordForPreview,
tryFetchExternalRecordForEdit,
};
};
3 changes: 3 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface Props {
showCloseIconButton?: boolean;
showModalControls?: boolean;
titleClassName?: string;
alignTitleCenter?: boolean;
}

const Modal: FC<Props> = ({
Expand All @@ -44,6 +45,7 @@ const Modal: FC<Props> = ({
showCloseIconButton = true,
showModalControls = true,
titleClassName,
alignTitleCenter = false,
}) => {
const portalElement = document.getElementById(MODAL_CONTAINER_ID) as Element;
// TODO: uncomment for using with Shadow DOM
Expand Down Expand Up @@ -73,6 +75,7 @@ const Modal: FC<Props> = ({
</button>
)}
<h3 className={classNames(['title', titleClassName])}>{title}</h3>
{alignTitleCenter && <span className="empty-block" />}
</div>
{!!children && children}
{showModalControls && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.duplicate-imported-resource {
padding-left: 0;
padding-right: 0;

> * {
padding: 0 1.5rem 0 1.5rem;
}

&-contents {
padding: 0.75rem 1.5rem;
margin: 0.75rem 0;

border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

.modal-controls {
justify-content: space-between;
}

.title {
font-size: 1rem;
}

.modal-header {
justify-content: space-between;

.empty-block {
min-width: 1.5rem;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { memo } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Modal } from '@components/Modal';
import { useRecoilState } from 'recoil';
import state from '@state';
import { useContainerEvents } from '@common/hooks/useContainerEvents';
import './ModalDuplicateImportedResource.scss';

export const ModalDuplicateImportedResource = memo(() => {
const [isDuplicateImportedResourceModalOpen, setIsDuplicateImportedResourceModalOpen] = useRecoilState(
state.ui.isDuplicateImportedResourceModalOpen,
);
const { formatMessage } = useIntl();
const { dispatchNavigateToOriginEventWithFallback } = useContainerEvents();

return (
<Modal
className="duplicate-imported-resource"
isOpen={isDuplicateImportedResourceModalOpen}
title={formatMessage({ id: 'ld.duplicateImportWarn' })}
submitButtonLabel={formatMessage({ id: 'ld.continue' })}
submitButtonDisabled
alignTitleCenter
cancelButtonLabel={formatMessage({ id: 'ld.backToInventory' })}
onClose={() => setIsDuplicateImportedResourceModalOpen(false)}
onCancel={dispatchNavigateToOriginEventWithFallback}
>
<div className="duplicate-imported-resource-contents" data-testid="modal-duplicate-imported-resource">
<FormattedMessage id="ld.rdPropertiesMatchContinue" />
</div>
</Modal>
);
});
1 change: 1 addition & 0 deletions src/components/ModalDuplicateImportedResource/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ModalDuplicateImportedResource } from './ModalDuplicateImportedResource';
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { Button, ButtonType } from '@components/Button';
import { FormattedMessage } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useRecordControls } from '@common/hooks/useRecordControls';
import { useContainerEvents } from '@common/hooks/useContainerEvents';
import './PreviewExternalResourceControls.scss';

export const PreviewExternalResourceControls = () => {
const navigate = useNavigate();
const { tryFetchExternalRecordForEdit } = useRecordControls();
const { dispatchNavigateToOriginEventWithFallback } = useContainerEvents();
const { externalId } = useParams();

return (
<div className="preview-external-resource-controls">
<Button data-testid="close-external-preview-button" type={ButtonType.Primary} onClick={() => navigate(-1)}>
<Button
data-testid="close-external-preview-button"
type={ButtonType.Primary}
onClick={dispatchNavigateToOriginEventWithFallback}
>
<FormattedMessage id="ld.cancel" />
</Button>
<Button data-testid="continue-external-preview-button" type={ButtonType.Highlighted}>
<Button
data-testid="continue-external-preview-button"
type={ButtonType.Highlighted}
onClick={() => tryFetchExternalRecordForEdit(externalId)}
>
<FormattedMessage id="ld.continue" />
</Button>
</div>
Expand Down
Loading

0 comments on commit 96a5f4a

Please sign in to comment.