Skip to content

Commit

Permalink
feat: UILD-406: STORY: Read only screen in LDE
Browse files Browse the repository at this point in the history
  • Loading branch information
s3fs committed Oct 30, 2024
1 parent 70c8319 commit e4676c8
Show file tree
Hide file tree
Showing 26 changed files with 330 additions and 124 deletions.
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ROUTES } from '@common/constants/routes.constants';
import { OKAPI_CONFIG } from '@common/constants/api.constants';
import { BASE_LOCALE, i18nMessages } from '@common/i18n/messages';
import { localStorageService } from '@common/services/storage';
import { Root, Search, Load, EditWrapper, ExternalResourceEdit } from '@views';
import { Root, Search, Load, EditWrapper, ExternalResourcePreview } from '@views';
import state from '@state';
import { ServicesProvider } from './providers';
import './App.scss';
Expand Down Expand Up @@ -43,8 +43,8 @@ export const routes: RouteObject[] = [
element: <Load />,
},
{
path: ROUTES.EXTERNAL_RESOURCE_EDIT.uri,
element: <ExternalResourceEdit />,
path: ROUTES.EXTERNAL_RESOURCE_PREVIEW.uri,
element: <ExternalResourcePreview />,
},
{
path: '*',
Expand Down
16 changes: 13 additions & 3 deletions src/common/api/records.api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { BIBFRAME_API_ENDPOINT, MAX_LIMIT } from '@common/constants/api.constants';
import {
BIBFRAME_API_ENDPOINT,
ExternalResourceIdType,
GET_RESOURCE_BY_TYPE_URIS,
MAX_LIMIT,
} from '@common/constants/api.constants';
import baseApi from './base.api';
import { TYPE_URIS } from '@common/constants/bibframe.constants';

type SingleRecord = {
recordId: string;
};

type IGetRecord = SingleRecord & {
idType?: ExternalResourceIdType;
};

type GetAllRecords = {
pageSize?: number;
pageNumber?: number;
Expand All @@ -14,8 +23,9 @@ type GetAllRecords = {

const singleRecordUrl = `${BIBFRAME_API_ENDPOINT}/:recordId`;

export const getRecord = async ({ recordId }: SingleRecord) => {
const url = baseApi.generateUrl(singleRecordUrl, { name: ':recordId', value: recordId });
export const getRecord = async ({ recordId, idType }: IGetRecord) => {
const selectedUrl = (idType && GET_RESOURCE_BY_TYPE_URIS[idType]) ?? singleRecordUrl;
const url = baseApi.generateUrl(selectedUrl, { name: ':recordId', value: recordId });

return baseApi.getJson({
url,
Expand Down
8 changes: 8 additions & 0 deletions src/common/constants/api.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ export const DEFAULT_PAGES_METADATA = {
totalElements: 0,
totalPages: 0,
};

export enum ExternalResourceIdType {
Inventory = 'inventory',
}

export const GET_RESOURCE_BY_TYPE_URIS = {
[ExternalResourceIdType.Inventory]: `${BIBFRAME_API_ENDPOINT}/preview/:recordId`,
};
7 changes: 4 additions & 3 deletions src/common/constants/routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ export const ROUTES = {
uri: '/resources/:resourceId/edit',
name: 'ld.editResource',
},
EXTERNAL_RESOURCE_EDIT: {
uri: '/resources/external/:resourceId/edit',
name: 'ld.externalResource',
EXTERNAL_RESOURCE_PREVIEW: {
uri: '/resources/external/:externalId/preview',
name: 'ld.externalResourcePreview',
},
};

export const RESOURCE_URLS = [ROUTES.RESOURCE_EDIT.uri];
export const EXTERNAL_RESOURCE_URLS = [ROUTES.EXTERNAL_RESOURCE_PREVIEW.uri];

export const RESOURCE_EDIT_CREATE_URLS = [ROUTES.RESOURCE_EDIT.uri, ROUTES.RESOURCE_CREATE.uri];

Expand Down
12 changes: 10 additions & 2 deletions src/common/helpers/record.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,18 @@ export const getSelectedRecordBlocks = (searchParams: URLSearchParams) => {
};

export const getRecordTitle = (record: RecordEntry) => {
const { block } = getEditingRecordBlocks(record);
// TODO: unify interactions with record and its format
// Some functions expect { resource: { %RECORD_CONTENTS% }}
// Others, like this one, expect { %RECORD_CONTENTS% }
const recordContents = unwrapRecordValuesFromCommonContainer(record);

const { block } = getEditingRecordBlocks(recordContents);

let selectedTitle;

TITLE_CONTAINER_URIS.every(uri => {
const selectedTitleContainer = (
record[block!]?.['http://bibfra.me/vocab/marc/title'] as unknown as Record<string, any>[]
recordContents[block!]?.['http://bibfra.me/vocab/marc/title'] as unknown as Record<string, any>[]
)?.find(obj => Object.hasOwn(obj, uri));

if (selectedTitleContainer) {
Expand All @@ -180,6 +185,9 @@ export const getAdjustedRecordContents = ({ record, block, reference, asClone }:
};
};

export const unwrapRecordValuesFromCommonContainer = (record: RecordEntry) =>
(record.resource ?? record) as RecordEntry;

export const wrapRecordValuesWithCommonContainer = (record: RecordEntry) => ({ resource: record });

export const checkIfRecordHasDependencies = (record: RecordEntry) => {
Expand Down
79 changes: 44 additions & 35 deletions src/common/hooks/useConfig.hook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext } from 'react';
import { useContext, useRef } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import state from '@state';
Expand Down Expand Up @@ -33,6 +33,7 @@ export const useConfig = () => {
const setPreviewContent = useSetRecoilState(state.inputs.previewContent);
const setSelectedRecordBlocks = useSetRecoilState(state.inputs.selectedRecordBlocks);
const { getProcessedRecordAndSchema } = useProcessedRecordAndSchema();
const isProcessingProfiles = useRef(false);

const prepareFields = (profiles: ProfileEntry[]): ResourceTemplates => {
const preparedFields = profiles.reduce<ResourceTemplates>((fields, profile) => {
Expand Down Expand Up @@ -87,43 +88,51 @@ export const useConfig = () => {
};

const getProfiles = async ({ record, recordId, previewParams, asClone }: GetProfiles): Promise<any> => {
const hasStoredProfiles = profiles?.length;
const response = hasStoredProfiles ? profiles : await fetchProfiles();
// TODO: check a list of supported profiles and implement the profile selection
const selectedProfile = response.find(({ name }: ProfileEntry) => name === PROFILE_NAMES.MONOGRAPH);
const templates = preparedFields || prepareFields(response);

if (!hasStoredProfiles) {
setProfiles(response);
}
if (isProcessingProfiles.current && (record || recordId)) return;

try {
isProcessingProfiles.current = true;

setUserValues({});

const recordData = record?.resource || {};
const recordTitle = getRecordTitle(recordData as RecordEntry);
const entities = getPrimaryEntitiesFromRecord(record as RecordEntry);

if (selectedProfile) {
setSelectedProfile(selectedProfile);

const { updatedSchema, initKey } = await buildSchema(selectedProfile, templates, recordData, asClone);

if (previewParams && recordId) {
setPreviewContent(prev => [
...(previewParams.singular ? [] : prev.filter(({ id }) => id !== recordId)),
{
id: recordId,
base: updatedSchema,
userValues: userValuesService.getAllValues(),
initKey,
title: recordTitle,
entities,
},
]);
const hasStoredProfiles = profiles?.length;
const response = hasStoredProfiles ? profiles : await fetchProfiles();
// TODO: check a list of supported profiles and implement the profile selection
const selectedProfile = response.find(({ name }: ProfileEntry) => name === PROFILE_NAMES.MONOGRAPH);
const templates = preparedFields || prepareFields(response);

if (!hasStoredProfiles) {
setProfiles(response);
}
}

return response;
setUserValues({});

const recordData = record?.resource || {};
const recordTitle = getRecordTitle(recordData as RecordEntry);
const entities = getPrimaryEntitiesFromRecord(record as RecordEntry);

if (selectedProfile) {
setSelectedProfile(selectedProfile);

const { updatedSchema, initKey } = await buildSchema(selectedProfile, templates, recordData, asClone);

if (previewParams && recordId) {
setPreviewContent(prev => [
...(previewParams.singular ? [] : prev.filter(({ id }) => id !== recordId)),
{
id: recordId,
base: updatedSchema,
userValues: userValuesService.getAllValues(),
initKey,
title: recordTitle,
entities,
},
]);
}
}

return response;
} finally {
isProcessingProfiles.current = false;
}
};

return { getProfiles, prepareFields };
Expand Down
84 changes: 60 additions & 24 deletions src/common/hooks/useRecordControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ 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';

type SaveRecordProps = {
asRefToNewRecord?: boolean;
shouldSetSearchParams?: boolean;
isNavigatingBack?: boolean;
};

type IBaseFetchRecord = {
recordId?: string;
cachedRecord?: RecordEntry;
idType?: ExternalResourceIdType;
errorMessage?: string;
};

export const useRecordControls = () => {
const [searchParams, setSearchParams] = useSearchParams();
const setIsLoading = useSetRecoilState(state.loadingState.isLoading);
Expand Down Expand Up @@ -59,30 +67,30 @@ export const useRecordControls = () => {
const isClone = queryParams.get(QueryParams.CloneOf);

const fetchRecord = async (recordId: string, previewParams?: PreviewParams) => {
try {
const profile = PROFILE_BFIDS.MONOGRAPH;
const locallySavedData = getSavedRecord(profile, recordId);
const recordData: RecordEntry =
locallySavedData && !previewParams ? locallySavedData.data : await getRecord({ recordId });

if (!previewParams) {
setCurrentlyEditedEntityBfid(new Set(getPrimaryEntitiesFromRecord(recordData)));
setRecord(recordData);
}
const profile = PROFILE_BFIDS.MONOGRAPH;
const locallySavedData = getSavedRecord(profile, recordId);
const cachedRecord: RecordEntry | undefined =
locallySavedData && !previewParams ? (locallySavedData.data as RecordEntry) : undefined;

setCurrentlyPreviewedEntityBfid(new Set(getPrimaryEntitiesFromRecord(recordData, !!previewParams)));
const recordData = await getRecordAndInitializeParsing({ recordId, cachedRecord });

await getProfiles({ record: recordData, recordId, previewParams, asClone: Boolean(isClone) });
if (!recordData) return;

setIsEdited(false);
} catch (_err) {
console.error('Error fetching record.');

setStatusMessages(currentStatus => [
...currentStatus,
UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching'),
]);
if (!previewParams) {
setCurrentlyEditedEntityBfid(new Set(getPrimaryEntitiesFromRecord(recordData)));
setRecord(recordData);
}

setCurrentlyPreviewedEntityBfid(new Set(getPrimaryEntitiesFromRecord(recordData, !!previewParams)));

await getProfiles({
record: recordData,
recordId,
previewParams,
asClone: Boolean(isClone),
});

setIsEdited(false);
};

const saveRecord = async ({
Expand Down Expand Up @@ -120,10 +128,7 @@ export const useRecordControls = () => {

setStatusMessages(currentStatus => [
...currentStatus,
UserNotificationFactory.createMessage(
StatusType.success,
recordId ? 'ld.rdUpdateSuccess' : 'ld.rdSaveSuccess',
),
UserNotificationFactory.createMessage(StatusType.success, recordId ? 'ld.rdUpdateSuccess' : 'ld.rdSaveSuccess'),
]);

// TODO: isEdited state update is not immediately reflected in the <Prompt />
Expand Down Expand Up @@ -261,6 +266,36 @@ export const useRecordControls = () => {
}
};

const getRecordAndInitializeParsing = async ({ recordId, cachedRecord, idType, errorMessage }: IBaseFetchRecord) => {
if (!recordId && !cachedRecord) return;

try {
const recordData: RecordEntry = cachedRecord ?? (recordId && (await getRecord({ recordId, idType })));

await getProfiles({
record: recordData,
recordId,
});

return recordData;
} catch (_err) {
setStatusMessages(currentStatus => [
...currentStatus,
UserNotificationFactory.createMessage(StatusType.error, errorMessage ?? 'ld.errorFetching'),
]);
}
};

const fetchExternalRecordForPreview = async (recordId?: string, idType = ExternalResourceIdType.Inventory) => {
if (!recordId) return;

await getRecordAndInitializeParsing({
recordId,
idType,
errorMessage: 'ld.errorFetchingExternalResourceForPreview',
});
};

return {
fetchRecord,
saveRecord,
Expand All @@ -269,5 +304,6 @@ export const useRecordControls = () => {
discardRecord,
clearRecordState,
fetchRecordAndSelectEntityValues,
fetchExternalRecordForPreview,
};
};
6 changes: 4 additions & 2 deletions src/common/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,14 @@ export const BASE_LOCALE = {
'ld.to': 'To',
'ld.apply': 'Apply',
'ld.notSpecified': 'Not specified',
'ld.externalResource': 'External resource',
'ld.externalResourcePreview': 'External resource preview',
'ld.errorFetchingExternalResourceForPreview': 'Error fetching external resource for preview',
'ld.fetchingExternalResourceById': 'Fetching external resource id {resourceId}...',
'ld.lastUpdated': 'Last updated',
'ld.marcAuthorityRecord': 'MARC authority record',
'ld.selectBrowseOption': 'Select a browse option',
'ld.searchQueryWouldBeHere': '{query} would be here'
'ld.searchQueryWouldBeHere': '{query} would be here',
'ld.continue': 'Continue',
};

export const i18nMessages = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { useParams } from 'react-router-dom';
import './ExternalResourceLoader.scss';

export const ExternalResourceLoader = () => {
const { resourceId } = useParams();
const { externalId } = useParams();

return (
<div className="external-resource-loader">
<div className="contents">
<FormattedMessage id="ld.fetchingExternalResourceById" values={{ resourceId }} />
<FormattedMessage id="ld.fetchingExternalResourceById" values={{ resourceId: externalId }} />
</div>
</div>
);
Expand Down
Loading

0 comments on commit e4676c8

Please sign in to comment.