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
12 changes: 12 additions & 0 deletions src/common/helpers/complexLookup.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ export const getUpdatedSelectedEntries = ({

return selectedEntriesService.get();
};

export const generateValidationRequestBody = (marcData: MarcDTO | null, target = 'CREATOR_OF_WORK') => {
SKarolFolio marked this conversation as resolved.
Show resolved Hide resolved
if (!marcData) return {};

const rawMarcEncoded = JSON.stringify(marcData?.parsedRecord?.content, null, 2);
const excapedString = rawMarcEncoded.replace(/\r/g, '\r').replace(/\n/g, '\n');
SKarolFolio marked this conversation as resolved.
Show resolved Hide resolved

return {
rawMarc: excapedString,
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,
};
}
81 changes: 68 additions & 13 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 { 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,
baseLabelType = 'creator',
SKarolFolio marked this conversation as resolved.
Show resolved Hide resolved
onChange,
}: {
entry: SchemaEntry;
value?: UserValueContents[];
lookupConfig: ComplexLookupsConfigEntry;
baseLabelType?: 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?.[baseLabelType]),
});
};

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,
baseLabelType: 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
6 changes: 3 additions & 3 deletions src/components/ComplexLookupField/configs/Authorities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ export const authoritiesTableConfig: SearchResultsTableConfig = {
},
title: {
label: 'ld.headingReference',
position: 1,
position: 2,
className: 'cell-fixed cell-fixed-370',
formatter: TitleFormatter,
},
subclass: {
label: 'ld.typeOfHeading',
position: 2,
position: 3,
className: 'cell-fixed cell-fixed-140',
},
authoritySource: {
label: 'ld.authoritySource',
position: 3,
position: 4,
className: 'cell-fixed cell-fixed-250',
},
},
Expand Down
4 changes: 4 additions & 0 deletions src/components/ComplexLookupField/formatters/Assign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import { Button, ButtonType } from '@components/Button';
export const AssignFormatter = ({
row,
onAssign,
checkFailedId,
}: {
row: SearchResultsTableRow;
onAssign: ({ id, title, linkedFieldValue }: ComplexLookupAssignRecordDTO) => void;
checkFailedId: (id: string) => boolean;
}) => {
const isAuthorized = row.authorized.label === AuthRefType.Authorized;
const isDisabled = checkFailedId(row.__meta.id);

return isAuthorized ? (
<Button
Expand All @@ -22,6 +25,7 @@ export const AssignFormatter = ({
})
}
data-testid={`assign-button-${row.__meta.id}`}
disabled={isDisabled}
>
<FormattedMessage id="ld.assign" />
</Button>
Expand Down
Loading
Loading