Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UILD-427: Authority validation - Creator of Work #61

Merged
merged 10 commits into from
Dec 20, 2024
Merged
3 changes: 2 additions & 1 deletion src/common/constants/api.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ export const EDITOR_API_BASE_PATH = 'EDITOR_API_BASE_PATH';

// API endpoints
export const BIBFRAME_API_ENDPOINT = '/linked-data/resource';
export const INVENTORY_API_ENDPOINT = '/linked-data/inventory-instance'
export const INVENTORY_API_ENDPOINT = '/linked-data/inventory-instance';
export const PROFILE_API_ENDPOINT = '/linked-data/profile';
export const SEARCH_API_ENDPOINT = '/search/linked-data';
export const SEARCH_RESOURCE_API_ENDPOINT = `${SEARCH_API_ENDPOINT}/works`;
export const AUTHORITY_ASSIGNMENT_CHECK_API_ENDPOINT = '/linked-data/authority-assignment-check';

export const DEFAULT_PAGES_METADATA = {
totalElements: 0,
Expand Down
8 changes: 8 additions & 0 deletions src/common/constants/complexLookup.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,11 @@ export const COMPLEX_LOOKUPS_LINKED_FIELDS_MAPPING = {
export const EMPTY_LINKED_DROPDOWN_OPTION_SUFFIX = 'empty';
export const VALUE_DIVIDER = ' ,';
export const __MOCK_URI_CHANGE_WHEN_IMPLEMENTING = '__MOCK_URI_CHANGE_WHEN_IMPLEMENTING';

export enum Authority {
Creator = 'creator',
}

export enum AuthorityValidationTarget {
CreatorOfWork = 'CREATOR_OF_WORK',
}
16 changes: 16 additions & 0 deletions src/common/helpers/complexLookup.helper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AuthorityValidationTarget,
COMPLEX_LOOKUPS_LINKED_FIELDS_MAPPING,
EMPTY_LINKED_DROPDOWN_OPTION_SUFFIX,
} from '@common/constants/complexLookup.constants';
Expand Down Expand Up @@ -66,3 +67,18 @@ export const getUpdatedSelectedEntries = ({

return selectedEntriesService.get();
};

export const generateValidationRequestBody = (
marcData: MarcDTO | null,
target = AuthorityValidationTarget.CreatorOfWork,
) => {
if (!marcData) return {};

const rawMarcEncoded = JSON.stringify(marcData?.parsedRecord?.content, null, 2);
const escapedString = rawMarcEncoded.replace(/\r/g, '\r').replace(/\n/g, '\n');

return {
rawMarc: escapedString,
target,
};
};
64 changes: 64 additions & 0 deletions src/common/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState, useCallback } from 'react';
import BaseApi from '@common/api/base.api';
import { StatusType } from '@common/constants/status.constants';
import { UserNotificationFactory } from '@common/services/userNotification';
import { useLoadingState, useStatusState } from '@src/store';

interface RequestConfig {
url: string;
method?: APIRequestMethod;
urlParams?: Record<string, string>;
urlParam?: { name: string; value: string | number };
requestParams?: RequestInit;
body?: unknown;
errorMessageId?: string;
}

interface ApiResponse<T> {
data: T | null;
}

export function useApi<T>() {
const { setIsLoading } = useLoadingState();
const { addStatusMessagesItem } = useStatusState();
const [state, setState] = useState<ApiResponse<T>>({
data: null,
});

const makeRequest = useCallback(
async ({ url, method = 'GET', urlParams, urlParam, requestParams, body, errorMessageId }: RequestConfig) => {
setIsLoading(true);

try {
const finalUrl = BaseApi.generateUrl(url, urlParam);

const response = await BaseApi.getJson({
url: finalUrl,
urlParams,
requestParams: {
method,
headers: { 'content-type': 'application/json' },
...requestParams,
...(typeof body === 'object' && body !== null ? { body: JSON.stringify(body, null, 2) } : {}),
},
});

setState({ data: response });

return response;
} catch (error) {
addStatusMessagesItem?.(
UserNotificationFactory.createMessage(StatusType.error, errorMessageId ?? 'ld.errorMakingApiRequest'),
);
} finally {
setIsLoading(false);
}
},
[],
);

return {
...state,
makeRequest,
};
}
83 changes: 69 additions & 14 deletions src/common/hooks/useComplexLookup.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { ChangeEvent, useCallback, useState } from 'react';
import {
generateEmptyValueUuid,
generateValidationRequestBody,
getLinkedField,
getUpdatedSelectedEntries,
updateLinkedFieldValue,
} from '@common/helpers/complexLookup.helper';
import { __MOCK_URI_CHANGE_WHEN_IMPLEMENTING } from '@common/constants/complexLookup.constants';
import { __MOCK_URI_CHANGE_WHEN_IMPLEMENTING, Authority } from '@common/constants/complexLookup.constants';
import { AdvancedFieldType } from '@common/constants/uiControls.constants';
import { useInputsState, useMarcPreviewState, useProfileState, useStatusState, useUIState } from '@src/store';
import { UserNotificationFactory } from '@common/services/userNotification';
import { StatusType } from '@common/constants/status.constants';
import { AUTHORITY_ASSIGNMENT_CHECK_API_ENDPOINT } from '@common/constants/api.constants';
import { useModalControls } from './useModalControls';
import { useMarcData } from './useMarcData';
import { useServicesContext } from './useServicesContext';
import { useInputsState, useMarcPreviewState, useProfileState, useUIState } from '@src/store';
import { useApi } from './useApi';
import { useComplexLookupValidation } from './useComplexLookupValidation';

export const useComplexLookup = ({
entry,
value,
lookupConfig,
authority = Authority.Creator,
onChange,
}: {
entry: SchemaEntry;
value?: UserValueContents[];
lookupConfig: ComplexLookupsConfigEntry;
authority?: string;
onChange: (uuid: string, contents: Array<UserValueContents>) => void;
}) => {
const { selectedEntriesService } = useServicesContext() as Required<ServicesParams>;
const [localValue, setLocalValue] = useState<UserValueContents[]>(value || []);
const { schema } = useProfileState();
const { selectedEntries, setSelectedEntries } = useInputsState();
const {
complexValue,
setComplexValue,
resetComplexValue: resetMarcPreviewData,
metadata: marcPreviewMetadata,
Expand All @@ -38,6 +47,9 @@ export const useComplexLookup = ({
const { fetchMarcData } = useMarcData(setComplexValue);
const { uuid, linkedEntry } = entry;
const linkedField = getLinkedField({ schema, linkedEntry });
const { makeRequest } = useApi();
const { addStatusMessagesItem } = useStatusState();
const { addFailedEntryId, clearFailedEntryIds } = useComplexLookupValidation();

const handleDelete = (id?: string) => {
onChange(uuid, []);
Expand All @@ -56,20 +68,37 @@ export const useComplexLookup = ({
};

const closeModal = useCallback(() => {
clearFailedEntryIds();
setIsModalOpen(false);
}, []);

const handleAssign = async ({ id, title, linkedFieldValue }: ComplexLookupAssignRecordDTO) => {
let srsId;
const reset = () => {
resetMarcPreviewData();
resetMarcPreviewMetadata();
resetIsMarcPreviewOpen();
};

if (marcPreviewMetadata?.baseId === id) {
srsId = marcPreviewMetadata.srsId;
} else {
const marcData = await fetchMarcData(id, lookupConfig.api.endpoints.marcPreview);
const validateMarcRecord = (marcData: MarcDTO | null) => {
const { endpoints, validationTarget } = lookupConfig.api;

srsId = marcData?.matchedId;
}
return makeRequest({
url: endpoints.validation ?? AUTHORITY_ASSIGNMENT_CHECK_API_ENDPOINT,
method: 'POST',
body: generateValidationRequestBody(marcData, validationTarget?.[authority]),
});
};

const assignMarcRecord = ({
id,
title,
srsId,
linkedFieldValue,
}: {
id: string;
title: string;
srsId?: string;
linkedFieldValue?: string;
}) => {
const newValue = {
id,
label: title,
Expand All @@ -95,11 +124,37 @@ export const useComplexLookup = ({

setSelectedEntries(updatedSelectedEntries);
}
};

resetMarcPreviewData();
resetMarcPreviewMetadata();
resetIsMarcPreviewOpen();
closeModal();
const handleAssign = async ({ id, title, linkedFieldValue }: ComplexLookupAssignRecordDTO) => {
let srsId;
let marcData = complexValue;

if (marcPreviewMetadata?.baseId === id) {
srsId = marcPreviewMetadata.srsId;
} else {
const response = await fetchMarcData(id, lookupConfig.api.endpoints.marcPreview);

if (response) {
marcData = response;
srsId = marcData?.matchedId;
}
}

const isValid = await validateMarcRecord(marcData);

if (isValid) {
assignMarcRecord({ id, title, srsId, linkedFieldValue });
clearFailedEntryIds();
reset();
closeModal();
} else {
addFailedEntryId(id);

addStatusMessagesItem?.(
UserNotificationFactory.createMessage(StatusType.error, 'ld.errorValidatingAuthorityRecord'),
);
}
};

const handleOnChangeBase = ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
Expand Down
10 changes: 9 additions & 1 deletion src/common/hooks/useComplexLookupSearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type Row } from '@components/Table';
import { useSearchContext } from '@common/hooks/useSearchContext';
import { ComplexLookupSearchResultsProps } from '@components/ComplexLookupField/ComplexLookupSearchResults';
import { useSearchState } from '@src/store';
import { useComplexLookupValidation } from './useComplexLookupValidation';

export const useComplexLookupSearchResults = ({
onTitleClick,
Expand All @@ -13,6 +14,7 @@ export const useComplexLookupSearchResults = ({
const { onAssignRecord } = useSearchContext();
const { data, sourceData } = useSearchState();
const { formatMessage } = useIntl();
const { checkFailedId } = useComplexLookupValidation();

const applyActionItems = useCallback(
(rows: Row[]): Row[] =>
Expand All @@ -23,7 +25,13 @@ export const useComplexLookupSearchResults = ({
formattedRow[key] = {
...row[key],
children: column.formatter
? column.formatter({ row, formatMessage, onAssign: onAssignRecord, onTitleClick })
? column.formatter({
row,
formatMessage,
onAssign: onAssignRecord,
onTitleClick,
checkFailedId,
})
: row[key].label,
};
});
Expand Down
25 changes: 25 additions & 0 deletions src/common/hooks/useComplexLookupValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useComplexLookupState } from '@src/store';

export const useComplexLookupValidation = () => {
const {
authorityAssignmentCheckFailedIds,
addAuthorityAssignmentCheckFailedIdsItem,
resetAuthorityAssignmentCheckFailedIds,
} = useComplexLookupState();

const addFailedEntryId = (id: string) => {
addAuthorityAssignmentCheckFailedIdsItem?.(id);
};

const clearFailedEntryIds = () => {
resetAuthorityAssignmentCheckFailedIds();
};

const checkFailedId = (id?: string) => {
if (!id) return false;

return authorityAssignmentCheckFailedIds.includes(id);
};

return { addFailedEntryId, clearFailedEntryIds, checkFailedId };
};
1 change: 1 addition & 0 deletions src/components/ComplexLookupField/ComplexLookupField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const ComplexLookupField: FC<Props> = ({ value = undefined, id, entry, on
entry,
value,
lookupConfig,
authority: layout?.baseLabelType,
onChange,
});

Expand Down
13 changes: 9 additions & 4 deletions src/components/ComplexLookupField/MarcPreviewComplexLookup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC } from 'react';
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import { useSearchContext } from '@common/hooks/useSearchContext';
import { useComplexLookupValidation } from '@common/hooks/useComplexLookupValidation';
import { useMarcPreviewState, useUIState } from '@src/store';
import { SearchControlPane } from '@components/SearchControlPane';
import { MarcContent } from '@components/MarcContent';
Expand All @@ -17,6 +18,7 @@ export const MarcPreviewComplexLookup: FC<MarcPreviewComplexLookupProps> = ({ on
const { formatMessage } = useIntl();
const { isMarcPreviewOpen } = useUIState();
const { complexValue: marcPreviewData, metadata: marcPreviewMetadata } = useMarcPreviewState();
const { checkFailedId } = useComplexLookupValidation();

const renderCloseButton = () => (
<Button
Expand All @@ -41,18 +43,20 @@ export const MarcPreviewComplexLookup: FC<MarcPreviewComplexLookupProps> = ({ on

const onClickAssignButton = () => {
onAssignRecord?.({
id: marcPreviewMetadata?.baseId || '',
title: marcPreviewMetadata?.title || '',
linkedFieldValue: marcPreviewMetadata?.headingType || '',
id: marcPreviewMetadata?.baseId ?? '',
title: marcPreviewMetadata?.title ?? '',
linkedFieldValue: marcPreviewMetadata?.headingType ?? '',
});
};

const isDisabledButton = checkFailedId(marcPreviewMetadata?.baseId);

return (
<>
{isMarcPreviewOpen && marcPreviewData ? (
<div className="marc-preview-container">
<SearchControlPane
label={marcPreviewMetadata?.title || ''}
label={marcPreviewMetadata?.title ?? ''}
renderSubLabel={renderSubLabel}
renderCloseButton={renderCloseButton}
>
Expand All @@ -61,6 +65,7 @@ export const MarcPreviewComplexLookup: FC<MarcPreviewComplexLookupProps> = ({ on
type={ButtonType.Highlighted}
onClick={onClickAssignButton}
ariaLabel={formatMessage({ id: 'ld.aria.marcAuthorityPreview.close' })}
disabled={isDisabledButton}
>
<FormattedMessage id="ld.assign" />
</Button>
Expand Down
4 changes: 2 additions & 2 deletions src/components/ComplexLookupField/ModalComplexLookup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SEARCH_RESULTS_FORMATTER } from '@common/helpers/search/formatters';
import { SEARCH_QUERY_BUILDER } from '@common/helpers/search/queryBuilder';
import { IS_EMBEDDED_MODE } from '@common/constants/build.constants';
import { SearchSegment } from '@common/constants/search.constants';
import { ComplexLookupType } from '@common/constants/complexLookup.constants';
import { Authority, ComplexLookupType } from '@common/constants/complexLookup.constants';
import { useComplexLookupApi } from '@common/hooks/useComplexLookupApi';
import { useMarcData } from '@common/hooks/useMarcData';
import { COMPLEX_LOOKUPS_CONFIG } from '@src/configs';
Expand Down Expand Up @@ -35,7 +35,7 @@ export const ModalComplexLookup: FC<ModalComplexLookupProps> = memo(
onClose,
value,
assignEntityName = ComplexLookupType.Authorities,
baseLabelType = 'creator',
baseLabelType = Authority.Creator,
}) => {
const {
api,
Expand Down
Loading
Loading