Skip to content

Commit

Permalink
UILD-427: Authority validation - Creator of Work (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
SKarolFolio authored Dec 20, 2024
1 parent 06ae48f commit cc8184b
Show file tree
Hide file tree
Showing 24 changed files with 562 additions and 42 deletions.
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

0 comments on commit cc8184b

Please sign in to comment.