diff --git a/README.md b/README.md index 7019578b..5de5ac64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # @folio/linked-data -This project is a new MARVA editor. +© 2024 EBSCO Information Services. + +This software is distributed under the terms of the Apache License, Version 2.0. See the file "[LICENSE](LICENSE)" for more information. + +## Introduction +UI application designed for performing operations on a library's linked data graph. This application can run standalone outside of the FOLIO platform. The [ui-ld-folio-wrapper](https://github.com/folio-org/ui-ld-folio-wrapper) module integrates this application for use within FOLIO. ## Table of Contents @@ -24,39 +29,45 @@ or Yarn: yarn install ``` -### 2. Provide API config: - -#### a) For an external API: +### 2. Configure the application: Create a JSON object with required data, then use it in such cases: -- For development or using MARVA as a standalone application, in a browser's localstorage create a record with a key `okapi_config` and stringified JSON value (see "JSON config example"); +- For development or using Linked data as a standalone application, in a browser's localstorage create a record with a key `okapi_config` and stringified JSON value (see "JSON config example"); - For an embedded application: Use JSON in the `config` attribute, see [Usage](#usage) section. -###### JSON config example: +###### Configuration options: + * `basePath`: Backend URI to which requests from the frontend are going to be directed. + * `tenant`: Okapi tenant. + * `token`: Okapi token. + * `customEvents`: A dictionary with custom event names. The keys of this dictionary have to be specific while the values can be arbitrary but unique. Events are used to communicate between this application and its container when running in embedded mode. + * `TRIGGER_MODAL`: Root application can dispatch this event to open a prompt in Linked data application which will inform a user about unsaved changes before leaving an Edit or Create page. + * `PROCEED_NAVIGATION`: Linked data application dispatches this event when a user clicks in the prompt "Save and continue" button or closes the prompt. + * `BLOCK_NAVIGATION`: Linked data application dispatches this event when user makes changes in a work form ("Create" or "Edit" page). + * `UNBLOCK_NAVIGATION`: Root application can dispatch this event to allow Linked data to proceed its navigation after it's been blocked. + * `NAVIGATE_TO_ORIGIN`: Linked data application dispatches this event when there is a need to navigate to the entrypoint from where the navigation to the Linked data application happened. + * `DROP_NAVIGATE_TO_ORIGIN`: Linked data application dispatches this event when there is no longer a need to navigate to the entrypoint from where the navigation to the Linked data application happened. Subsequent `NAVIGATE_TO_ORIGIN` calls have no effect unless a new navigation origin is set within the root application. + +###### Configuration example: ```json { "basePath": "YOUR_API_URI", "tenant": "YOUR_TENANT", "token": "YOUR_TOKEN", - // For embedded application only. Events also should be dispatched or listened in the root application. "customEvents": { - "TRIGGER_MODAL": "triggermodal", // Root application can dispatch this event to open a prompt in MARVA which will inform a user about unsaved changes before leaving an Edit or Create page. - "PROCEED_NAVIGATION": "proceednavigation", // MARVA dispatches this event when a user clicks in the prompt "Save and continue" button or closes the prompt. - "BLOCK_NAVIGATION": "blocknavigation" // MARVA dispatches this event when user makes changes in a work form ("Create" or "Edit" page). + "TRIGGER_MODAL": "TRIGGER_MODAL", + "PROCEED_NAVIGATION": "PROCEED_NAVIGATION", + "BLOCK_NAVIGATION": "BLOCK_NAVIGATION", + "UNBLOCK_NAVIGATION": "UNBLOCK_NAVIGATION", + "NAVIGATE_TO_ORIGIN": "NAVIGATE_TO_ORIGIN", + "DROP_NAVIGATE_TO_ORIGIN": "DROP_NAVIGATE_TO_ORIGIN" } } ``` -#### b) For an opened API (e.g. locally started. Can be used for development purposes): - -1. Rename `.env` file to `.env.local`. - -2. In that file change `EDITOR_API_BASE_PATH` variable's value. - ## Scripts The following scripts are available: @@ -85,13 +96,19 @@ The following scripts are available: 1. Build the code as an embedded application using `npm run build:lib` command. The built code will be placed in `./dist` folder. 2. Add the script on a page: -```html - -``` + 1. As a package in the files where you plan to use the application if it was added to your project via package management tools: + ```js + import '@folio/linked-data'; + ``` + 2. Or as a script: + ```html + + ``` + 3. Add a web component in the required HTML container on the page. - Use a config with a required API config for passing it in the MARVA application through the web component (see JSON config example in [Installation](#installation) section): + Use a config with a required API config for passing it in the Linked data application through the web component (see JSON config example in [Installation](#installation) section): ```html
diff --git a/src/App.scss b/src/App.scss index 4ac8852a..fb3867bf 100644 --- a/src/App.scss +++ b/src/App.scss @@ -16,6 +16,14 @@ body > #editor-root, // font-size: 16px; } +h2, +h3, +h4, +h5, +h6 { + margin: 0; +} + #app-root { display: flex; flex-direction: column; diff --git a/src/common/constants/bibframeMapping.constants.ts b/src/common/constants/bibframeMapping.constants.ts index f226a839..65bad2fc 100644 --- a/src/common/constants/bibframeMapping.constants.ts +++ b/src/common/constants/bibframeMapping.constants.ts @@ -24,6 +24,7 @@ export const BFLITE_URIS = { CLASSIFICATION: 'http://bibfra.me/vocab/lite/classification', PROVISION_ACTIVITY: 'https://bibfra.me/vocab/marc/provisionActivity', TITLE: 'http://bibfra.me/vocab/marc/title', + TITLE_CONTAINER: 'http://bibfra.me/vocab/marc/Title', MAIN_TITLE: 'http://bibfra.me/vocab/marc/mainTitle', PRODUCTION: 'http://bibfra.me/vocab/marc/production', PUBLICATION: 'http://bibfra.me/vocab/marc/publication', @@ -215,29 +216,37 @@ export const BF_URIS = { LABEL: 'http://www.w3.org/2000/01/rdf-schema#label', }; +export const INSTANCE_REF_KEY = "_instanceReference" +export const WORK_REF_KEY = "_workReference" + export const BLOCKS_BFLITE = { INSTANCE: { uri: BFLITE_URIS.INSTANCE, - referenceKey: '_instanceReference', + referenceKey: INSTANCE_REF_KEY, resourceType: ResourceType.instance, reference: { - key: '_workReference', + key: WORK_REF_KEY, uri: BFLITE_URIS.WORK, name: ResourceType.work, }, }, WORK: { uri: BFLITE_URIS.WORK, - referenceKey: '_workReference', + referenceKey: WORK_REF_KEY, resourceType: ResourceType.work, reference: { - key: '_instanceReference', + key: INSTANCE_REF_KEY, uri: BFLITE_URIS.INSTANCE, name: ResourceType.instance, }, }, }; +export const REF_TO_NAME = { + [INSTANCE_REF_KEY]: ResourceType.instance, + [WORK_REF_KEY]: ResourceType.work, +} + export const BFLITE_BFID_TO_BLOCK = { 'lc:RT:bf2:Monograph:Instance': BLOCKS_BFLITE.INSTANCE, 'lc:RT:bf2:Monograph:Work': BLOCKS_BFLITE.WORK, @@ -609,6 +618,9 @@ export const TYPE_MAP = { export const NEW_BF2_TO_BFLITE_MAPPING = { [BFLITE_URIS.INSTANCE]: { + [BFLITE_URIS.INSTANCE]: { + container: { bf2Uri: 'http://id.loc.gov/ontologies/bibframe/Instance' } + }, 'http://bibfra.me/vocab/marc/title': { container: { bf2Uri: 'http://id.loc.gov/ontologies/bibframe/title' }, options: { diff --git a/src/common/helpers/record.helper.ts b/src/common/helpers/record.helper.ts index 177a1ed0..8f25896d 100644 --- a/src/common/helpers/record.helper.ts +++ b/src/common/helpers/record.helper.ts @@ -10,7 +10,12 @@ import { TYPE_URIS, } from '@common/constants/bibframe.constants'; import { formatRecord } from './recordFormatting.helper'; -import { BFLITE_URI_TO_BLOCK, BFLITE_URIS, BLOCKS_BFLITE } from '@common/constants/bibframeMapping.constants'; +import { + BFLITE_URI_TO_BLOCK, + BFLITE_URIS, + BLOCKS_BFLITE, + REF_TO_NAME, +} from '@common/constants/bibframeMapping.constants'; import { ResourceType } from '@common/constants/record.constants'; import { QueryParams } from '@common/constants/routes.constants'; import { cloneDeep } from 'lodash'; @@ -216,7 +221,7 @@ export const getPreviewFieldsConditions = ({ schema, isOnBranchWithUserValue, altDisplayNames, - hideActions, + hideEntities, isEntity, forceRenderAllTopLevelEntities, }: { @@ -227,7 +232,7 @@ export const getPreviewFieldsConditions = ({ schema: Schema; isOnBranchWithUserValue: boolean; altDisplayNames?: Record; - hideActions?: boolean; + hideEntities?: boolean; isEntity: boolean; forceRenderAllTopLevelEntities?: boolean; }) => { @@ -241,7 +246,7 @@ export const getPreviewFieldsConditions = ({ const isBranchEndWithoutValues = !selectedUserValues && isBranchEnd; const isBranchEndWithValues = !!selectedUserValues; const shouldRenderLabelOrPlaceholders = - (isPreviewable && isGroupable) || + (!(isEntity && hideEntities) && isPreviewable && isGroupable) || type === AdvancedFieldType.dropdown || (isBranchEndWithValues && type !== AdvancedFieldType.complex) || isBranchEndWithoutValues; @@ -257,7 +262,6 @@ export const getPreviewFieldsConditions = ({ const isBlock = level === GROUP_BY_LEVEL && shouldRenderLabelOrPlaceholders; const isBlockContents = level === GROUP_CONTENTS_LEVEL; const isInstance = bfid === PROFILE_BFIDS.INSTANCE; - const showEntityActions = !hideActions && isEntity; const wrapEntities = forceRenderAllTopLevelEntities && isEntity; return { @@ -270,7 +274,21 @@ export const getPreviewFieldsConditions = ({ isBlock, isBlockContents, isInstance, - showEntityActions, wrapEntities, }; }; + +export const getRecordDependencies = (record?: RecordEntry | null) => { + if (!record) return; + + const contents = unwrapRecordValuesFromCommonContainer(record); + const { block, reference } = getEditingRecordBlocks(contents); + + if (block && reference) { + return { + keys: reference, + type: REF_TO_NAME[reference.key as keyof typeof REF_TO_NAME], + entries: contents[block][reference.key] as unknown as RecursiveRecordSchema[], + }; + } +}; diff --git a/src/common/helpers/recordFormatting.helper.ts b/src/common/helpers/recordFormatting.helper.ts index fec66d12..fc5f9435 100644 --- a/src/common/helpers/recordFormatting.helper.ts +++ b/src/common/helpers/recordFormatting.helper.ts @@ -7,6 +7,7 @@ import { NON_BF_RECORD_ELEMENTS, } from '@common/constants/bibframeMapping.constants'; import { getRecordPropertyData } from './record.helper'; +import { Row } from '@components/Table'; export const formatRecord = ({ parsedRecord, @@ -232,3 +233,36 @@ export const applyIntlToTemplates = ({ ...rest, template: Object.fromEntries(Object.entries(template).map(([k, v]) => [k, format({ id: v })])), })); + +export const formatDependeciesTable = (deps: Record[]): Row[] => { + return deps.map(({ id, ...rest }) => { + const selectedPublication = (rest?.[BFLITE_URIS.PUBLICATION] as Record)?.[0] as Record< + string, + unknown[] + >; + const selectedTitle = (rest?.[BFLITE_URIS.TITLE] as Record)?.[0] as Record< + string, + Record + >; + + return { + __meta: { + id, + key: id, + ...rest, + }, + title: { + label: selectedTitle?.[BFLITE_URIS.TITLE_CONTAINER]?.[BFLITE_URIS.MAIN_TITLE]?.[0], + className: 'title', + }, + publisher: { + label: selectedPublication?.[BFLITE_URIS.NAME]?.[0], + className: 'publisher', + }, + pubDate: { + label: selectedPublication?.[BFLITE_URIS.DATE]?.[0], + className: 'publication-date', + }, + }; + }) as Row[]; +}; diff --git a/src/common/hooks/useConfig.hook.ts b/src/common/hooks/useConfig.hook.ts index 798e9228..cca639f2 100644 --- a/src/common/hooks/useConfig.hook.ts +++ b/src/common/hooks/useConfig.hook.ts @@ -8,16 +8,25 @@ import { useProcessedRecordAndSchema } from './useProcessedRecordAndSchema.hook' import { useServicesContext } from './useServicesContext'; export type PreviewParams = { + noStateUpdate?: boolean; singular?: boolean; }; -type GetProfiles = { +type IGetProfiles = { record?: RecordEntry; recordId?: string; previewParams?: PreviewParams; asClone?: boolean; }; +type IBuildSchema = { + profile: ProfileEntry; + templates: ResourceTemplates; + record: Record | Array; + asClone?: boolean; + noStateUpdate?: boolean; +}; + export const useConfig = () => { const { schemaCreatorService, userValuesService, selectedEntriesService } = useServicesContext() as Required; @@ -57,12 +66,7 @@ export const useConfig = () => { return preparedFields; }; - const buildSchema = async ( - profile: ProfileEntry, - templates: ResourceTemplates, - record: Record | Array, - asClone = false, - ) => { + const buildSchema = async ({ profile, templates, record, asClone = false, noStateUpdate = false }: IBuildSchema) => { const initKey = uuidv4(); const userValues: UserValues = {}; @@ -76,18 +80,21 @@ export const useConfig = () => { record, userValues, asClone, + noStateUpdate, }); - setUserValues(updatedUserValues || userValues); - setInitialSchemaKey(initKey); - setSelectedEntries(selectedEntriesService.get()); - setSchema(updatedSchema); - setSelectedRecordBlocks(selectedRecordBlocks); + if (!noStateUpdate) { + setUserValues(updatedUserValues || userValues); + setInitialSchemaKey(initKey); + setSelectedEntries(selectedEntriesService.get()); + setSchema(updatedSchema); + setSelectedRecordBlocks(selectedRecordBlocks); + } return { updatedSchema, initKey }; }; - const getProfiles = async ({ record, recordId, previewParams, asClone }: GetProfiles): Promise => { + const getProfiles = async ({ record, recordId, previewParams, asClone }: IGetProfiles): Promise => { if (isProcessingProfiles.current && (record || recordId)) return; try { @@ -103,16 +110,20 @@ export const useConfig = () => { setProfiles(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); + !previewParams?.noStateUpdate && setSelectedProfile(selectedProfile); + + const { updatedSchema, initKey } = await buildSchema({ + profile: selectedProfile, + templates, + record: recordData, + asClone, + noStateUpdate: previewParams?.noStateUpdate, + }); if (previewParams && recordId) { setPreviewContent([ diff --git a/src/common/hooks/useProcessedRecordAndSchema.hook.ts b/src/common/hooks/useProcessedRecordAndSchema.hook.ts index f3d717d1..f23d01c3 100644 --- a/src/common/hooks/useProcessedRecordAndSchema.hook.ts +++ b/src/common/hooks/useProcessedRecordAndSchema.hook.ts @@ -17,6 +17,7 @@ type IGetProcessedRecordAndSchema = { record: Record | Array; userValues: UserValues; asClone?: boolean; + noStateUpdate?: boolean; }; export const useProcessedRecordAndSchema = () => { @@ -27,7 +28,7 @@ export const useProcessedRecordAndSchema = () => { useServicesContext() as Required; const getProcessedRecordAndSchema = useCallback( - async ({ baseSchema, record, userValues, asClone = false }: IGetProcessedRecordAndSchema) => { + async ({ baseSchema, record, userValues, asClone = false, noStateUpdate }: IGetProcessedRecordAndSchema) => { let updatedSchema = baseSchema; let updatedUserValues = userValues; let selectedRecordBlocks = undefined; @@ -51,7 +52,7 @@ export const useProcessedRecordAndSchema = () => { }) : undefined; - setRecord(wrapRecordValuesWithCommonContainer(adjustedRecord)); + !noStateUpdate && setRecord(wrapRecordValuesWithCommonContainer(adjustedRecord)); selectedRecordBlocks = { block, reference }; schemaWithDuplicatesService.set(baseSchema); recordNormalizingService.init(adjustedRecord, block, reference); diff --git a/src/common/hooks/useRecordControls.ts b/src/common/hooks/useRecordControls.ts index bcf24309..e903c332 100644 --- a/src/common/hooks/useRecordControls.ts +++ b/src/common/hooks/useRecordControls.ts @@ -43,6 +43,7 @@ type IBaseFetchRecord = { cachedRecord?: RecordEntry; idType?: ExternalResourceIdType; errorMessage?: string; + previewParams?: PreviewParams; }; export const useRecordControls = () => { @@ -247,7 +248,7 @@ export const useRecordControls = () => { } }; - const getRecordAndInitializeParsing = async ({ recordId, cachedRecord, idType, errorMessage }: IBaseFetchRecord) => { + const getRecordAndInitializeParsing = async ({ recordId, cachedRecord, idType, previewParams, errorMessage }: IBaseFetchRecord) => { if (!recordId && !cachedRecord) return; try { @@ -256,6 +257,7 @@ export const useRecordControls = () => { await getProfiles({ record: recordData, recordId, + previewParams, }); return recordData; @@ -306,5 +308,6 @@ export const useRecordControls = () => { fetchRecordAndSelectEntityValues, fetchExternalRecordForPreview, tryFetchExternalRecordForEdit, + getRecordAndInitializeParsing, }; }; diff --git a/src/common/services/recordToSchemaMapping/recordToSchemaMapping.service.ts b/src/common/services/recordToSchemaMapping/recordToSchemaMapping.service.ts index 885e409f..68c0621d 100644 --- a/src/common/services/recordToSchemaMapping/recordToSchemaMapping.service.ts +++ b/src/common/services/recordToSchemaMapping/recordToSchemaMapping.service.ts @@ -176,8 +176,8 @@ export class RecordToSchemaMappingService implements IRecordToSchemaMapping { private readonly getSchemaEntries = (containerBf2Uri?: string, containerDataTypeUri?: string) => { return this.schemaArray.filter((entry: SchemaEntry) => { - const hasTheSameUri = entry.uri === containerBf2Uri; - const hasTheSameDataTypeUri = containerDataTypeUri + const isOfSameUri = entry.uri === containerBf2Uri; + const isOfSameDataTypeUri = containerDataTypeUri ? entry.constraints?.valueDataType?.dataTypeURI === containerDataTypeUri : true; let hasBlockParent = false; @@ -195,8 +195,7 @@ export class RecordToSchemaMappingService implements IRecordToSchemaMapping { if (this.updatedSchema?.get(getParentEntryUuid(entry.path))?.type === AdvancedFieldTypeEnum.block) { hasBlockParent = true; } - - return hasTheSameUri && hasTheSameDataTypeUri && hasProperBlock && hasBlockParent; + return isOfSameUri && isOfSameDataTypeUri && hasProperBlock && hasBlockParent; }); }; diff --git a/src/components/EditPreview/EditPreview.tsx b/src/components/EditPreview/EditPreview.tsx index 50bba5ba..4a98de3e 100644 --- a/src/components/EditPreview/EditPreview.tsx +++ b/src/components/EditPreview/EditPreview.tsx @@ -1,32 +1,31 @@ -import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import { Preview } from '@components/Preview'; -import { Button, ButtonType } from '@components/Button'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; +import classNames from 'classnames'; import { useParams, useSearchParams } from 'react-router-dom'; -import { QueryParams, RESOURCE_CREATE_URLS, ROUTES } from '@common/constants/routes.constants'; +import { QueryParams, RESOURCE_CREATE_URLS } from '@common/constants/routes.constants'; import { ResourceType } from '@common/constants/record.constants'; import { InstancesList } from '@components/InstancesList'; import { useRoutePathPattern } from '@common/hooks/useRoutePathPattern'; -import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; -import { checkIfRecordHasDependencies } from '@common/helpers/record.helper'; -import { useInputsState, useStatusState, useUIState } from '@src/store'; +import { getRecordDependencies } from '@common/helpers/record.helper'; +import { memo, useEffect } from 'react'; +import { TitledPreview } from '@components/Preview/TitledPreview'; import './EditPreview.scss'; +import { useInputsState, useUIState } from '@src/store'; -export const EditPreview = () => { +export const EditPreview = memo(() => { + const { record, previewContent, setPreviewContent, resetPreviewContent } = useInputsState(); const { currentlyPreviewedEntityBfid } = useUIState(); - const { isEditedRecord: isEdited } = useStatusState(); - const { record } = useInputsState(); - const isPositionedSecond = - currentlyPreviewedEntityBfid.has(PROFILE_BFIDS.INSTANCE) && currentlyPreviewedEntityBfid.values.length <= 1; - const { resourceId } = useParams(); const isCreatePageOpen = useRoutePathPattern(RESOURCE_CREATE_URLS); + const { resourceId } = useParams(); const [queryParams] = useSearchParams(); + const isPositionedSecond = + currentlyPreviewedEntityBfid.has(PROFILE_BFIDS.INSTANCE) && currentlyPreviewedEntityBfid.values.length <= 1; const typeParam = queryParams.get(QueryParams.Type); const isCreateWorkPageOpened = isCreatePageOpen && typeParam === ResourceType.work; - const recordHasDependencies = checkIfRecordHasDependencies(record as RecordEntry); - const showPreview = recordHasDependencies && !isCreateWorkPageOpened; - const { navigateToEditPage } = useNavigateToEditPage(); + const dependencies = getRecordDependencies(record); + const showPreview = (dependencies?.entries?.length === 1 && !isCreateWorkPageOpened) || previewContent.length; + const selectedForPreview = previewContent?.[0]; + + useEffect(() => resetPreviewContent, [resourceId]); return (
{ 'positioned-second': isPositionedSecond, })} > - {currentlyPreviewedEntityBfid.has(PROFILE_BFIDS.INSTANCE) && ( -
- {!recordHasDependencies && ( - - )} -
+ {showPreview ? ( + setPreviewContent(prev => prev.filter(({ id }) => id !== selectedForPreview.id))} + showCloseCtl={(dependencies?.entries?.length ?? 0) > 1} + /> + ) : ( + )} - {showPreview ? : }
); -}; +}); diff --git a/src/components/EditSection/EditSection.scss b/src/components/EditSection/EditSection.scss index feac4e01..e921d34d 100644 --- a/src/components/EditSection/EditSection.scss +++ b/src/components/EditSection/EditSection.scss @@ -5,6 +5,7 @@ flex-direction: column; gap: 0.313rem; width: 100%; + padding: 0.75rem 0.938rem; .input-group { display: flex; diff --git a/src/components/FullDisplay/FullDisplay.scss b/src/components/FullDisplay/FullDisplay.scss index 9bd0bf35..e01e8586 100644 --- a/src/components/FullDisplay/FullDisplay.scss +++ b/src/components/FullDisplay/FullDisplay.scss @@ -9,7 +9,7 @@ .preview-contents-container { height: 100%; overflow-y: scroll; - padding: 0.938rem; + padding: 0.938rem 0; padding-bottom: 4rem; } diff --git a/src/components/FullDisplay/PreviewContent.tsx b/src/components/FullDisplay/PreviewContent.tsx index c36a2f57..1602d94e 100644 --- a/src/components/FullDisplay/PreviewContent.tsx +++ b/src/components/FullDisplay/PreviewContent.tsx @@ -37,7 +37,7 @@ export const PreviewContent = () => {
{Object.keys(userValues).length ? (
- +
) : (
diff --git a/src/components/InstancesList/InstancesList.scss b/src/components/InstancesList/InstancesList.scss index c4ad7f6b..8e55610d 100644 --- a/src/components/InstancesList/InstancesList.scss +++ b/src/components/InstancesList/InstancesList.scss @@ -1,6 +1,58 @@ -.instances-list-empty { - margin-top: 99px; - font-size: 1.125rem; - text-align: center; - color: rgba(0, 0, 0, 0.62); +.instances-list { + padding: 0.75rem 0.938rem; + + &-header { + display: flex; + justify-content: space-between; + padding-bottom: 0.5rem; + } + + h3 { + font-size: 1.138rem; + } + + .no-instances { + font-size: 1.125rem; + color: rgba(0, 0, 0, 0.62); + text-align: center; + padding: 2.5rem 0; + } + + table { + th { + background-color: transparent; + } + + tr { + padding: 0.5rem 0; + } + + .publisher, + .publication-date { + max-width: 10rem; + } + + .title { + .button-link { + text-decoration: underline; + text-align: left; + height: 100%; + } + } + + .edit-ctl { + width: 0.5rem; + + .button-primary { + margin: 0.4rem 0; + font-weight: 600; + } + } + + td { + > * { + font-weight: normal; + } + } + } } diff --git a/src/components/InstancesList/InstancesList.tsx b/src/components/InstancesList/InstancesList.tsx index 681a6794..39679a9a 100644 --- a/src/components/InstancesList/InstancesList.tsx +++ b/src/components/InstancesList/InstancesList.tsx @@ -1,13 +1,100 @@ import { FormattedMessage } from 'react-intl'; +import { FC } from 'react'; +import { Row, Table } from '@components/Table'; +import { Button, ButtonType } from '@components/Button'; +import { formatDependeciesTable } from '@common/helpers/recordFormatting.helper'; +import { useRecordControls } from '@common/hooks/useRecordControls'; +import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; +import { generateEditResourceUrl } from '@common/helpers/navigation.helper'; +import { wrapRecordValuesWithCommonContainer } from '@common/helpers/record.helper'; +import { ROUTES, QueryParams } from '@common/constants/routes.constants'; import './InstancesList.scss'; -export const InstancesList = () => { +type IInstancesList = { + contents?: { keys: { key?: string; uri?: string }; entries: Record[] }; + type?: string; + refId?: string; +}; + +const instancesListHeader: Row = { + title: { + label: , + position: 0, + }, + publisher: { + label: , + position: 1, + }, + pubDate: { + label: , + position: 2, + }, + editCtl: { + position: 3, + }, +}; + +export const InstancesList: FC = ({ contents: { keys, entries } = {}, type, refId }) => { + const { getRecordAndInitializeParsing } = useRecordControls(); + const { navigateToEditPage } = useNavigateToEditPage(); + + const onClickNewInstance = () => + type && + refId && + navigateToEditPage(`${ROUTES.RESOURCE_CREATE.uri}?${QueryParams.Type}=${type}&${QueryParams.Ref}=${refId}`); + + const applyActionItems = (rows: Row[]): Row[] => + rows.map(row => { + const onClickPreview = () => + keys?.uri && + getRecordAndInitializeParsing({ + recordId: row.__meta.id, + cachedRecord: wrapRecordValuesWithCommonContainer({ [keys?.uri]: row.__meta }), + previewParams: { singular: true, noStateUpdate: true }, + }); + + const onClickEdit = () => navigateToEditPage(generateEditResourceUrl(row.__meta?.id)); + + return { + ...row, + title: { + ...row.title, + children: ( + + ), + }, + editCtl: { + className: 'edit-ctl', + children: ( + + ), + }, + }; + }); + + const formattedInstances = applyActionItems(formatDependeciesTable(entries ?? [])); + return (
- {/* Show this only when there is no Instances */} -
- +
+

+ +

+
+ {entries?.length ? ( + + ) : ( +
+ +
+ )} ); }; diff --git a/src/components/Preview/Fields.tsx b/src/components/Preview/Fields.tsx index 1592a63d..53d65bf2 100644 --- a/src/components/Preview/Fields.tsx +++ b/src/components/Preview/Fields.tsx @@ -1,15 +1,11 @@ import { ReactNode } from 'react'; import classNames from 'classnames'; import { ENTITY_LEVEL, GROUP_BY_LEVEL } from '@common/constants/bibframe.constants'; -import { BFLITE_BFID_TO_BLOCK } from '@common/constants/bibframeMapping.constants'; -import { RecordStatus } from '@common/constants/record.constants'; import { AdvancedFieldType } from '@common/constants/uiControls.constants'; -import { generateEditResourceUrl } from '@common/helpers/navigation.helper'; -import { getRecordId, getPreviewFieldsConditions } from '@common/helpers/record.helper'; +import { getPreviewFieldsConditions } from '@common/helpers/record.helper'; import { getParentEntryUuid } from '@common/helpers/schema.helper'; -import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; import { ConditionalWrapper } from '@components/ConditionalWrapper'; -import { useInputsState, useProfileState, useStatusState, useUIState } from '@src/store'; +import { useInputsState, useProfileState, useUIState } from '@src/store'; import { Labels } from './Labels'; import { Values } from './Values'; @@ -59,7 +55,7 @@ type FieldsProps = { altSchema?: Schema; altUserValues?: UserValues; altDisplayNames?: Record; - hideActions?: boolean; + hideEntities?: boolean; forceRenderAllTopLevelEntities?: boolean; }; @@ -71,26 +67,15 @@ export const Fields = ({ altSchema, altUserValues, altDisplayNames, - hideActions, + hideEntities, forceRenderAllTopLevelEntities, }: FieldsProps) => { - const { currentlyPreviewedEntityBfid } = useUIState(); - const { userValues: userValuesFromState, record, selectedEntries } = useInputsState(); + const { userValues: userValuesFromState, selectedEntries } = useInputsState(); const { schema: schemaFromState } = useProfileState(); - const { isEditedRecord: isEdited, setRecordStatus } = useStatusState(); - const { navigateToEditPage } = useNavigateToEditPage(); + const { currentlyPreviewedEntityBfid } = useUIState(); const userValues = altUserValues || userValuesFromState; const schema = altSchema || schemaFromState; - const handleNavigateToEditPage = () => { - setRecordStatus({ type: isEdited ? RecordStatus.saveAndClose : RecordStatus.close }); - - const typedSelectedBlock = BFLITE_BFID_TO_BLOCK[bfid as keyof typeof BFLITE_BFID_TO_BLOCK]; - const id = getRecordId(record, typedSelectedBlock.reference.uri, typedSelectedBlock.referenceKey); - - navigateToEditPage(generateEditResourceUrl(id)); - }; - const entry = base.get(uuid); const isOnBranchWithUserValue = paths.includes(uuid); const isEntity = level === ENTITY_LEVEL; @@ -121,7 +106,6 @@ export const Fields = ({ isBlock, isBlockContents, isInstance, - showEntityActions, wrapEntities, } = getPreviewFieldsConditions({ entry, @@ -131,7 +115,7 @@ export const Fields = ({ schema, isOnBranchWithUserValue, altDisplayNames, - hideActions, + hideEntities, isEntity, forceRenderAllTopLevelEntities, }); @@ -149,10 +133,6 @@ export const Fields = ({ isInstance={isInstance} altDisplayNames={altDisplayNames} displayNameWithAltValue={displayNameWithAltValue} - showEntityActions={showEntityActions} - bfid={bfid} - handleNavigateToEditPage={handleNavigateToEditPage} - record={record} /> )} {shouldRenderValuesOrPlaceholders && ( @@ -182,7 +162,7 @@ export const Fields = ({ altSchema={altSchema} altUserValues={altUserValues} altDisplayNames={altDisplayNames} - hideActions={hideActions} + hideEntities={hideEntities} forceRenderAllTopLevelEntities={forceRenderAllTopLevelEntities} /> diff --git a/src/components/Preview/Labels.tsx b/src/components/Preview/Labels.tsx index e1775554..d37ded2a 100644 --- a/src/components/Preview/Labels.tsx +++ b/src/components/Preview/Labels.tsx @@ -1,12 +1,6 @@ import { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Lightbulb16 from '@src/assets/lightbulb-shining-16.svg?react'; -import { RESOURCE_TEMPLATE_IDS } from '@common/constants/bibframe.constants'; -import { BLOCKS_BFLITE } from '@common/constants/bibframeMapping.constants'; -import { getRecordId } from '@common/helpers/record.helper'; -import { Button, ButtonType } from '@components/Button'; -import { PreviewActionsDropdown } from '@components/PreviewActionsDropdown'; type LabelProps = { isEntity: boolean; @@ -15,10 +9,6 @@ type LabelProps = { isInstance: boolean; altDisplayNames?: Record; displayNameWithAltValue: string; - showEntityActions: boolean; - bfid: string; - handleNavigateToEditPage: VoidFunction; - record: RecordEntry | null; }; export const Labels: FC = ({ @@ -28,10 +18,6 @@ export const Labels: FC = ({ isInstance, altDisplayNames, displayNameWithAltValue, - showEntityActions, - bfid, - handleNavigateToEditPage, - record, }) => { return ( = ({ > {isEntity && !isInstance && !altDisplayNames && } {displayNameWithAltValue} - {showEntityActions && !isInstance && ( - - )} - {showEntityActions && isInstance && ( - - )} ); }; diff --git a/src/components/Preview/Preview.scss b/src/components/Preview/Preview.scss index 24c59026..a9dbc66a 100644 --- a/src/components/Preview/Preview.scss +++ b/src/components/Preview/Preview.scss @@ -1,4 +1,6 @@ .preview-panel { + padding: 0 0.938rem 0.75rem 0.938rem; + .block { border: 1px solid red; min-height: 0.5rem; @@ -81,3 +83,34 @@ flex-direction: row; justify-content: center; } + +.titled-preview { + padding: 0; + + &-header { + padding: 0.2rem 0.938rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.05); + + &-plain { + padding-top: 0.75rem; + border-bottom: none; + background-color: transparent; + } + + .details { + padding: 0.7rem; + + h5 { + font-size: 0.95rem; + } + + span { + color: rgba(0, 0, 0, 0.6) + } + } + } +} diff --git a/src/components/Preview/Preview.tsx b/src/components/Preview/Preview.tsx index 0993712a..b7d0e89f 100644 --- a/src/components/Preview/Preview.tsx +++ b/src/components/Preview/Preview.tsx @@ -1,5 +1,4 @@ import { FC } from 'react'; -import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { useInputsState, useProfileState } from '@src/store'; import { Fields } from './Fields'; @@ -10,8 +9,7 @@ type IPreview = { altUserValues?: UserValues; altInitKey?: string; altDisplayNames?: Record; - headless?: boolean; - hideActions?: boolean; + hideEntities?: boolean; forceRenderAllTopLevelEntities?: boolean; entityRowDisplay?: boolean; }; @@ -21,8 +19,7 @@ export const Preview: FC = ({ altUserValues, altInitKey, altDisplayNames, - headless = false, - hideActions, + hideEntities, forceRenderAllTopLevelEntities, entityRowDisplay, }) => { @@ -37,11 +34,6 @@ export const Preview: FC = ({ className={classNames('preview-panel', { 'preview-panel-row': entityRowDisplay })} data-testid="preview-fields" > - {!headless && ( -

- -

- )} {initialSchemaKey && ( = ({ altSchema={altSchema} altUserValues={altUserValues} altDisplayNames={altDisplayNames} - hideActions={hideActions} + hideEntities={hideEntities} forceRenderAllTopLevelEntities={forceRenderAllTopLevelEntities} /> )} diff --git a/src/components/Preview/TitledPreview.tsx b/src/components/Preview/TitledPreview.tsx new file mode 100644 index 00000000..e4cbc0d6 --- /dev/null +++ b/src/components/Preview/TitledPreview.tsx @@ -0,0 +1,80 @@ +import { Button, ButtonType } from '@components/Button'; +import { Preview } from './Preview'; +import { PreviewActionsDropdown } from '@components/PreviewActionsDropdown'; +import Times16 from '@src/assets/times-16.svg?react'; +import { FormattedMessage } from 'react-intl'; +import { ResourceType } from '@common/constants/record.constants'; +import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; +import { generateEditResourceUrl } from '@common/helpers/navigation.helper'; +import classNames from 'classnames'; + +export type ITitledPreview = { + showCloseCtl?: boolean; + ownId?: string; + refId?: string | null; + type?: string; + previewContent?: PreviewContent; + onClickClose?: VoidFunction; +}; + +export const TitledPreview = ({ + showCloseCtl = true, + ownId, + refId, + type, + previewContent, + onClickClose, +}: ITitledPreview) => { + const { navigateToEditPage } = useNavigateToEditPage(); + const { title, id, base, initKey, userValues } = previewContent ?? {}; + const selectedOwnId = id ?? ownId; + const withPreviewContent = ( + <> + {showCloseCtl ? ( + + ) : ( + + )} +
+ {title &&
{title}
} +
+ + ); + + const navigateToOwnEditPage = () => selectedOwnId && navigateToEditPage(generateEditResourceUrl(selectedOwnId)); + + return ( +
+
+ {previewContent ? ( + withPreviewContent + ) : ( +

+ +

+ )} + {type === ResourceType.work && !previewContent && ( + + )} + {type === ResourceType.instance && ( + + )} +
+ +
+ ); +}; diff --git a/src/components/PreviewActionsDropdown/PreviewActionsDropdown.tsx b/src/components/PreviewActionsDropdown/PreviewActionsDropdown.tsx index 1a265239..bbf86a52 100644 --- a/src/components/PreviewActionsDropdown/PreviewActionsDropdown.tsx +++ b/src/components/PreviewActionsDropdown/PreviewActionsDropdown.tsx @@ -8,7 +8,7 @@ import { FC } from 'react'; import { QueryParams, ROUTES } from '@common/constants/routes.constants'; type Props = { - referenceId?: string; + referenceId?: string | null; entityType?: string; ownId?: string; handleNavigateToEditPage?: VoidFunction; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index e6cab131..a02d03a0 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -21,7 +21,7 @@ export type Table = { }; export const Table = ({ header, data, className, onRowClick, onHeaderCellClick, selectedRows }: Table) => { - const sortedHeaderEntries = Object.entries(header).sort( + const sortedHeaderEntries = Object.entries(header).toSorted( ([_key1, value1], [_key2, value2]) => (value1?.position ?? 0) - (value2?.position ?? 0), ); diff --git a/src/test/__mocks__/common/hooks/useRecordControls.mock.ts b/src/test/__mocks__/common/hooks/useRecordControls.mock.ts index 74de1ed5..f0bc2369 100644 --- a/src/test/__mocks__/common/hooks/useRecordControls.mock.ts +++ b/src/test/__mocks__/common/hooks/useRecordControls.mock.ts @@ -5,6 +5,7 @@ export const fetchRecord = jest.fn(); export const clearRecordState = jest.fn(); export const fetchRecordAndSelectEntityValues = jest.fn(); export const fetchExternalRecordForPreview = jest.fn(); +export const getRecordAndInitializeParsing = jest.fn(); jest.mock('@common/hooks/useRecordControls', () => ({ useRecordControls: () => ({ @@ -15,5 +16,6 @@ jest.mock('@common/hooks/useRecordControls', () => ({ saveRecordLocally, fetchRecordAndSelectEntityValues, fetchExternalRecordForPreview, + getRecordAndInitializeParsing, }), })); diff --git a/src/test/__tests__/common/helpers/record.helper.test.ts b/src/test/__tests__/common/helpers/record.helper.test.ts index 16670899..5cc3743f 100644 --- a/src/test/__tests__/common/helpers/record.helper.test.ts +++ b/src/test/__tests__/common/helpers/record.helper.test.ts @@ -199,9 +199,9 @@ describe('record.helper', () => { }); describe('getRecordTitle', () => { - const mockMainTitle = '80085' + const mockMainTitle = '80085'; const mockRecord = { - 'testInstanceUri': { + testInstanceUri: { 'http://bibfra.me/vocab/marc/title': [ { 'http://bibfra.me/vocab/marc/Title': { @@ -216,4 +216,39 @@ describe('record.helper', () => { expect(RecordHelper.getRecordTitle(mockRecord as unknown as RecordEntry)).toBe(mockMainTitle); }); }); + + describe('getRecordDependencies', () => { + test("doesn't work if there's no record", () => { + expect(RecordHelper.getRecordDependencies(null)).toBeFalsy(); + }); + + test('returns record dependencies', () => { + const record = { + testInstanceUri: { + testFieldUri_1: [], + testFieldUri_2: [], + workReferenceKey: [ + { + testWorkFieldUri_1: [], + id: ['testWorkId'], + }, + ], + }, + } as unknown as RecordEntry; + + jest.spyOn(RecordHelper, 'getEditingRecordBlocks').mockReturnValue({ + block: 'testInstanceUri', + reference: { + key: 'workReferenceKey', + uri: 'testWorkUri', + }, + }); + + expect(RecordHelper.getRecordDependencies(record)).toEqual({ + entries: [{ id: ['testWorkId'], testWorkFieldUri_1: [] }], + keys: { key: 'workReferenceKey', uri: 'testWorkUri' }, + type: undefined, + }); + }); + }); }); diff --git a/src/test/__tests__/common/helpers/recordFormatting.helper.test.ts b/src/test/__tests__/common/helpers/recordFormatting.helper.test.ts index 315c5c58..a0cdf4bc 100644 --- a/src/test/__tests__/common/helpers/recordFormatting.helper.test.ts +++ b/src/test/__tests__/common/helpers/recordFormatting.helper.test.ts @@ -26,12 +26,17 @@ describe('recordFormatting', () => { WORK: testWorkUri, NOTE: 'testNoteUri', NAME: 'testNameUri', + DATE: 'testDateUri', LABEL: 'testLabelUri', SOURCE: 'testSourceUri', CREATOR: 'testCreatorUri', CONTRIBUTOR: 'testContributorUri', PROVIDER_PLACE: 'testProviderPlaceUri', CLASSIFICATION: 'testClassificationUri', + PUBLICATION: 'testPubUri', + TITLE: 'testTitleUri', + TITLE_CONTAINER: 'testTitleContainerUri', + MAIN_TITLE: 'testMainTitleUri', }); mockBF2Constant({ ...BF2_URIS, CREATOR_NAME: 'creatorNameBF2Uri', ROLE: 'roleBF2Uri' }); mockNonBFRecordElementsConstant({ @@ -386,4 +391,43 @@ describe('recordFormatting', () => { expect(result).toEqual(testResult); }); }); + + describe('formatDependeciesTable', () => { + test('converts record dependencies into Rows', () => { + expect( + RecordFormattingHelper.formatDependeciesTable([ + { + id: 'mockId', + testTitleUri: [ + { + testTitleContainerUri: { + testMainTitleUri: ['mockTitle'], + }, + }, + ], + testPubUri: [ + { + testNameUri: ['mockPubName'], + testDateUri: ['mockPubDate'], + }, + ], + }, + ])[0], + ).toMatchObject({ + __meta: { + id: 'mockId', + key: 'mockId', + }, + title: { + label: 'mockTitle', + }, + publisher: { + label: 'mockPubName', + }, + pubDate: { + label: 'mockPubDate', + }, + }); + }); + }); }); diff --git a/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts b/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts new file mode 100644 index 00000000..89ba9552 --- /dev/null +++ b/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts @@ -0,0 +1,39 @@ +import '@src/test/__mocks__/common/hooks/useServicesContext.mock'; +import { useProcessedRecordAndSchema } from '@common/hooks/useProcessedRecordAndSchema.hook'; +import { act, renderHook } from '@testing-library/react'; +import { useSetRecoilState } from 'recoil'; + +jest.mock('recoil'); + +describe('useProcessedRecordAndSchema', () => { + const mockSetState = jest.fn(); + const props = { + baseSchema: {} as Schema, + userValues: {}, + record: { key: 'value' }, + }; + + beforeEach(() => { + (useSetRecoilState as jest.Mock).mockReturnValueOnce(mockSetState).mockReturnValueOnce(jest.fn()); + }); + + test("doesn't update state when asked not to", () => { + const { result } = renderHook(useProcessedRecordAndSchema); + + act(() => { + result.current.getProcessedRecordAndSchema({ ...props, noStateUpdate: true }); + }); + + expect(mockSetState).not.toHaveBeenCalled(); + }); + + test('updates state when not asked to not update state', () => { + const { result } = renderHook(useProcessedRecordAndSchema); + + act(() => { + result.current.getProcessedRecordAndSchema(props); + }); + + expect(mockSetState).toHaveBeenCalled(); + }); +}); diff --git a/src/test/__tests__/components/EditPreview.test.tsx b/src/test/__tests__/components/EditPreview.test.tsx index 279b16e8..6c82147d 100644 --- a/src/test/__tests__/components/EditPreview.test.tsx +++ b/src/test/__tests__/components/EditPreview.test.tsx @@ -1,9 +1,11 @@ +import { render, screen } from '@testing-library/react'; +import { RouterProvider, createMemoryRouter } from 'react-router-dom'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; import { EditPreview } from '@components/EditPreview'; import { useInputsStore, useUIStore } from '@src/store'; import { setInitialGlobalState } from '@src/test/__mocks__/store'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { RouterProvider, createMemoryRouter } from 'react-router-dom'; + +jest.mock('@common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: true })); const navigate = jest.fn(); @@ -36,12 +38,6 @@ describe('EditPreview', () => { const { getByTestId } = screen; - test('navigates to add new instance screen', () => { - fireEvent.click(getByTestId('create-instance-button')); - - expect(navigate).toHaveBeenCalled(); - }); - test('contains instances list when create work page is opened', () => { expect(getByTestId('instances-list')).toBeInTheDocument(); }); diff --git a/src/test/__tests__/components/InstancesList.test.tsx b/src/test/__tests__/components/InstancesList.test.tsx new file mode 100644 index 00000000..b02ff2a4 --- /dev/null +++ b/src/test/__tests__/components/InstancesList.test.tsx @@ -0,0 +1,75 @@ +import '@src/test/__mocks__/common/hooks/useRecordControls.mock'; +import '@src/test/__mocks__/common/hooks/useNavigateToEditPage.mock'; +import { navigateToEditPage } from '@src/test/__mocks__/common/hooks/useNavigateToEditPage.mock'; +import { getRecordAndInitializeParsing } from '@src/test/__mocks__/common/hooks/useRecordControls.mock'; +import * as RecordFormatter from '@common/helpers/recordFormatting.helper'; +import { InstancesList } from '@components/InstancesList'; +import { fireEvent, render } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; + +jest.mock('recoil'); +jest.mock('@common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: true })); + +describe('InstancesList', () => { + const renderWithProps = () => { + const contents = [ + { + __meta: { + id: 'mockId', + key: 'mockId', + }, + title: { + label: 'mockTitle', + }, + publisher: { + label: 'mockPubName', + }, + pubDate: { + label: 'mockPubDate', + }, + }, + ]; + + jest.spyOn(RecordFormatter, 'formatDependeciesTable').mockReturnValue(contents); + + return render( + + + , + , + ); + }; + test("renders table when there's content", () => { + const { getByText } = renderWithProps(); + + expect(getByText('mockTitle')).toBeInTheDocument(); + }); + + test('invokes new instance control', () => { + const { getByTestId } = renderWithProps(); + + fireEvent.click(getByTestId('new-instance')); + + expect(navigateToEditPage).toHaveBeenCalled(); + }); + + test('invokes preview control', () => { + const { getByTestId } = renderWithProps(); + + fireEvent.click(getByTestId('preview-button__mockId')); + + expect(getRecordAndInitializeParsing).toHaveBeenCalled(); + }); + + test('invokes edit control', () => { + const { getByTestId } = renderWithProps(); + + fireEvent.click(getByTestId('edit-button__mockId')); + + expect(navigateToEditPage).toHaveBeenCalled(); + }); +}); diff --git a/src/test/__tests__/components/TitledPreview.test.tsx b/src/test/__tests__/components/TitledPreview.test.tsx new file mode 100644 index 00000000..6b622eae --- /dev/null +++ b/src/test/__tests__/components/TitledPreview.test.tsx @@ -0,0 +1,59 @@ +import '@src/test/__mocks__/common/hooks/useNavigateToEditPage.mock'; +import { navigateToEditPage } from '@src/test/__mocks__/common/hooks/useNavigateToEditPage.mock'; +import { fireEvent, render } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { ITitledPreview, TitledPreview } from '@components/Preview/TitledPreview'; + +jest.mock('recoil'); +jest.mock('@common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: true })); + +describe('TitledPreview', () => { + const defaultProps = { + showCloseCtl: true, + type: 'work', + onClickClose: jest.fn(), + previewContent: { + title: 'mockTitle', + id: 'mockId', + base: new Map(), + initKey: 'mockInitKey', + userValues: {}, + }, + }; + + const renderWithProps = (props: Partial | undefined = defaultProps) => + render( + + , + , + ); + + test("renders view appropriate for when there's preview content", () => { + const { getByTestId } = renderWithProps(); + + expect(getByTestId('nav-close-button')).toBeInTheDocument(); + }); + + test("renders view appropriate for when there's no preview content (single dep)", () => { + const { queryByTestId } = renderWithProps({ ...defaultProps, previewContent: undefined }); + + expect(queryByTestId('nav-close-button')).not.toBeInTheDocument(); + }); + + test('navigates to ref edit page', () => { + const { getByTestId } = renderWithProps({ ...defaultProps, previewContent: undefined }); + + fireEvent.click(getByTestId('edit-self-as-ref')); + + expect(navigateToEditPage).toHaveBeenCalled(); + }); + + test('navigates to own edit page', () => { + const { getByTestId } = renderWithProps({ ...defaultProps, type: 'instance' }); + + fireEvent.click(getByTestId('preview-actions-dropdown')); + fireEvent.click(getByTestId('preview-actions-dropdown__option-ld.edit')); + + expect(navigateToEditPage).toHaveBeenCalled(); + }); +}); diff --git a/src/views/Edit/Edit.scss b/src/views/Edit/Edit.scss index 222324e4..0f52c7ea 100644 --- a/src/views/Edit/Edit.scss +++ b/src/views/Edit/Edit.scss @@ -7,6 +7,5 @@ > *:not(.view-marc-modal) { overflow: auto; - padding: 0.75rem 0.938rem; } } diff --git a/src/views/ExternalResource/ExternalResourcePreview.tsx b/src/views/ExternalResource/ExternalResourcePreview.tsx index b4024faa..5fbf1d06 100644 --- a/src/views/ExternalResource/ExternalResourcePreview.tsx +++ b/src/views/ExternalResource/ExternalResourcePreview.tsx @@ -24,8 +24,6 @@ export const ExternalResourcePreview = () => { {record ? ( diff --git a/translations/ui-linked-data/en.json b/translations/ui-linked-data/en.json index 53038711..1624e6db 100644 --- a/translations/ui-linked-data/en.json +++ b/translations/ui-linked-data/en.json @@ -90,6 +90,8 @@ "ld.classificationNumber": "Classification number", "ld.work": "Work", "ld.instances": "Instances", + "ld.instance": "Instance", + "ld.year": "Year", "ld.noInstancesAvailable": "No instances available.", "ld.noInstancesAdded": "No instances added", "ld.addAnInstance": "Add an instance",