Skip to content

Commit

Permalink
feat: UILD-408: STORY: Continue
Browse files Browse the repository at this point in the history
  • Loading branch information
s3fs committed Nov 7, 2024
1 parent 4c4ef2f commit 941bb6b
Show file tree
Hide file tree
Showing 24 changed files with 225 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ const createRouter = (basename: string) => createBrowserRouter(routes, { basenam
const Container: FC<IContainer> = ({ routePrefix = '', config }) => {
const locale = useRecoilValue(state.config.locale);
const setCustomEvents = useSetRecoilState(state.config.customEvents);
const setHasNavigationOrigin = useSetRecoilState(state.config.hasNavigationOrigin);
const { i18nMessages, baseLocaleMessages } = useLoadI18nMessages();

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

return (
Expand Down
10 changes: 6 additions & 4 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 All @@ -56,7 +58,7 @@ const request = async ({ url, urlParams, requestParams = {}, sameOrigin = true }
tenant && sameOrigin
? {
'x-okapi-tenant': tenant,
...(token && { 'x-okapi-token': token })
...(token && { 'x-okapi-token': token }),
}
: undefined;

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
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,
};
};
39 changes: 35 additions & 4 deletions src/common/hooks/useRecordControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ 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,
} 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 @@ -25,7 +30,8 @@ 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
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { useNavigate } from 'react-router-dom';
import { Button, ButtonType } from '@components/Button';
import Times16 from '@src/assets/times-16.svg?react';
import { useRecoilValue } from 'recoil';
import state from '@state';
import { getRecordTitle } from '@common/helpers/record.helper';
import { useContainerEvents } from '@common/hooks/useContainerEvents';

export const PreviewExternalResourcePane = () => {
const navigate = useNavigate();
const record = useRecoilValue(state.inputs.record);
const { dispatchNavigateToOriginEventWithFallback } = useContainerEvents();

return (
<div className="nav-block nav-block-fixed-height">
<nav>
<Button
data-testid="nav-close-button"
type={ButtonType.Icon}
onClick={() => navigate(-1)}
onClick={dispatchNavigateToOriginEventWithFallback}
className="nav-close"
>
<Times16 />
Expand Down
6 changes: 6 additions & 0 deletions src/state/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ const clonePrototypes = atom<string[]>({
default: [],
});

const hasNavigationOrigin = atom<boolean>({
key: 'config.hasNavigationOrigin',
default: false,
});

export default {
profiles,
selectedProfile,
Expand All @@ -70,4 +75,5 @@ export default {
collectRecordDataForPreview,
customEvents,
clonePrototypes,
hasNavigationOrigin,
};
Loading

0 comments on commit 941bb6b

Please sign in to comment.