From 679782132758ce177dc3f999be45fb145ce7c853 Mon Sep 17 00:00:00 2001 From: nikolai Date: Mon, 2 Dec 2024 17:59:04 +0500 Subject: [PATCH 1/3] feat: UILD-409: Write installation documentation for ui-linked-data module (#44) --- README.md | 57 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 20 deletions(-) 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
From e7970054269659b11d69d7d34ab5dd573b1e9492 Mon Sep 17 00:00:00 2001 From: nikolai Date: Thu, 5 Dec 2024 17:42:37 +0500 Subject: [PATCH 2/3] feat: UILD-421: STORY: Multiple instances per Work (#48) * feat: UILD-421: STORY: Multiple instances per Work * unit tests, unused code cleanup * style and general fixes * fix tests --- src/App.scss | 8 ++ .../constants/bibframeMapping.constants.ts | 20 +++- src/common/helpers/record.helper.ts | 30 ++++-- src/common/helpers/recordFormatting.helper.ts | 34 +++++++ src/common/hooks/useConfig.hook.ts | 47 +++++---- .../hooks/useProcessedRecordAndSchema.hook.ts | 5 +- src/common/hooks/useRecordControls.ts | 5 +- .../recordToSchemaMapping.service.ts | 7 +- src/components/EditPreview/EditPreview.tsx | 62 ++++++------ src/components/EditSection/EditSection.scss | 1 + src/components/FullDisplay/PreviewContent.tsx | 2 +- .../InstancesList/InstancesList.scss | 52 +++++++++- .../InstancesList/InstancesList.tsx | 95 ++++++++++++++++++- src/components/Preview/Fields.tsx | 34 ++----- src/components/Preview/Labels.tsx | 27 ------ src/components/Preview/Preview.scss | 35 +++++++ src/components/Preview/Preview.tsx | 14 +-- src/components/Preview/TitledPreview.tsx | 86 +++++++++++++++++ .../PreviewActionsDropdown.tsx | 2 +- src/components/Table/Table.tsx | 2 +- .../common/hooks/useRecordControls.mock.ts | 2 + .../common/helpers/record.helper.test.ts | 39 +++++++- .../helpers/recordFormatting.helper.test.ts | 44 +++++++++ .../hooks/useProcessedRecordAndSchema.test.ts | 39 ++++++++ .../__tests__/components/EditPreview.test.tsx | 10 +- .../components/InstancesList.test.tsx | 75 +++++++++++++++ .../components/TitledPreview.test.tsx | 59 ++++++++++++ src/views/Edit/Edit.scss | 1 - .../ExternalResourcePreview.tsx | 2 - translations/ui-linked-data/en.json | 2 + 30 files changed, 682 insertions(+), 159 deletions(-) create mode 100644 src/components/Preview/TitledPreview.tsx create mode 100644 src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts create mode 100644 src/test/__tests__/components/InstancesList.test.tsx create mode 100644 src/test/__tests__/components/TitledPreview.test.tsx 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 34b94eb3..97c40cf3 100644 --- a/src/common/hooks/useConfig.hook.ts +++ b/src/common/hooks/useConfig.hook.ts @@ -9,16 +9,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; @@ -56,12 +65,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 = {}; @@ -75,18 +79,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 { @@ -102,16 +109,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(prev => [ diff --git a/src/common/hooks/useProcessedRecordAndSchema.hook.ts b/src/common/hooks/useProcessedRecordAndSchema.hook.ts index 7eb5c183..b817a5a8 100644 --- a/src/common/hooks/useProcessedRecordAndSchema.hook.ts +++ b/src/common/hooks/useProcessedRecordAndSchema.hook.ts @@ -18,6 +18,7 @@ type IGetProcessedRecordAndSchema = { record: Record | Array; userValues: UserValues; asClone?: boolean; + noStateUpdate?: boolean; }; export const useProcessedRecordAndSchema = () => { @@ -28,7 +29,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; @@ -52,7 +53,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 4b9b7821..1b440858 100644 --- a/src/common/hooks/useRecordControls.ts +++ b/src/common/hooks/useRecordControls.ts @@ -44,6 +44,7 @@ type IBaseFetchRecord = { cachedRecord?: RecordEntry; idType?: ExternalResourceIdType; errorMessage?: string; + previewParams?: PreviewParams; }; export const useRecordControls = () => { @@ -270,7 +271,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 { @@ -279,6 +280,7 @@ export const useRecordControls = () => { await getProfiles({ record: recordData, recordId, + previewParams, }); return recordData; @@ -333,5 +335,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 a3ad6545..3bd14dad 100644 --- a/src/components/EditPreview/EditPreview.tsx +++ b/src/components/EditPreview/EditPreview.tsx @@ -1,33 +1,34 @@ -import { Preview } from '@components/Preview'; -import { Button, ButtonType } from '@components/Button'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; import state from '@state'; import classNames from 'classnames'; -import { useRecoilValue } from 'recoil'; -import { FormattedMessage } from 'react-intl'; +import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'; 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 { getRecordDependencies } from '@common/helpers/record.helper'; +import { memo, useEffect } from 'react'; +import { TitledPreview } from '@components/Preview/TitledPreview'; import './EditPreview.scss'; -export const EditPreview = () => { +export const EditPreview = memo(() => { const currentlyPreviewedEntityBfid = useRecoilValue(state.ui.currentlyPreviewedEntityBfid); - const isEdited = useRecoilValue(state.status.recordIsEdited); const record = useRecoilValue(state.inputs.record); - const isPositionedSecond = - currentlyPreviewedEntityBfid.has(PROFILE_BFIDS.INSTANCE) && currentlyPreviewedEntityBfid.values.length <= 1; - const { resourceId } = useParams(); + const [previewContent, setPreviewContent] = useRecoilState(state.inputs.previewContent); + const resetPreviewContent = useResetRecoilState(state.inputs.previewContent); 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/PreviewContent.tsx b/src/components/FullDisplay/PreviewContent.tsx index 3c3fcc9f..843e7c5c 100644 --- a/src/components/FullDisplay/PreviewContent.tsx +++ b/src/components/FullDisplay/PreviewContent.tsx @@ -38,7 +38,7 @@ export const PreviewContent = () => {
{Object.keys(userValues).length ? (
- +
) : (
diff --git a/src/components/InstancesList/InstancesList.scss b/src/components/InstancesList/InstancesList.scss index c4ad7f6b..c111e2dd 100644 --- a/src/components/InstancesList/InstancesList.scss +++ b/src/components/InstancesList/InstancesList.scss @@ -1,6 +1,48 @@ -.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 + } + + .button-primary { + margin: 0.4rem 0; + } + + .button-link { + text-decoration: underline; + } + + .edit-ctl { + width: 0.5rem; + } + + 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 69d04294..f942b474 100644 --- a/src/components/Preview/Fields.tsx +++ b/src/components/Preview/Fields.tsx @@ -1,14 +1,10 @@ import { ReactNode } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; 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 state from '@state'; import { Labels } from './Labels'; @@ -60,7 +56,7 @@ type FieldsProps = { altSchema?: Schema; altUserValues?: UserValues; altDisplayNames?: Record; - hideActions?: boolean; + hideEntities?: boolean; forceRenderAllTopLevelEntities?: boolean; }; @@ -72,29 +68,16 @@ export const Fields = ({ altSchema, altUserValues, altDisplayNames, - hideActions, + hideEntities, forceRenderAllTopLevelEntities, }: FieldsProps) => { const userValuesFromState = useRecoilValue(state.inputs.userValues); const schemaFromState = useRecoilValue(state.config.schema); const selectedEntries = useRecoilValue(state.config.selectedEntries); - const setRecordStatus = useSetRecoilState(state.status.recordStatus); - const record = useRecoilValue(state.inputs.record); const currentlyPreviewedEntityBfid = useRecoilValue(state.ui.currentlyPreviewedEntityBfid); - const isEdited = useRecoilValue(state.status.recordIsEdited); - const { navigateToEditPage } = useNavigateToEditPage(); 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; @@ -125,7 +108,6 @@ export const Fields = ({ isBlock, isBlockContents, isInstance, - showEntityActions, wrapEntities, } = getPreviewFieldsConditions({ entry, @@ -135,7 +117,7 @@ export const Fields = ({ schema, isOnBranchWithUserValue, altDisplayNames, - hideActions, + hideEntities, isEntity, forceRenderAllTopLevelEntities, }); @@ -153,10 +135,6 @@ export const Fields = ({ isInstance={isInstance} altDisplayNames={altDisplayNames} displayNameWithAltValue={displayNameWithAltValue} - showEntityActions={showEntityActions} - bfid={bfid} - handleNavigateToEditPage={handleNavigateToEditPage} - record={record} /> )} {shouldRenderValuesOrPlaceholders && ( @@ -186,7 +164,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..0b583c83 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,36 @@ 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 { + display: flex; + flex-direction: column; + align-items: center; + + 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 d03b19b9..a6315938 100644 --- a/src/components/Preview/Preview.tsx +++ b/src/components/Preview/Preview.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; import { useRecoilValue } from 'recoil'; -import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import state from '@state'; import { Fields } from './Fields'; @@ -11,8 +10,7 @@ type IPreview = { altUserValues?: UserValues; altInitKey?: string; altDisplayNames?: Record; - headless?: boolean; - hideActions?: boolean; + hideEntities?: boolean; forceRenderAllTopLevelEntities?: boolean; entityRowDisplay?: boolean; }; @@ -22,8 +20,7 @@ export const Preview: FC = ({ altUserValues, altInitKey, altDisplayNames, - headless = false, - hideActions, + hideEntities, forceRenderAllTopLevelEntities, entityRowDisplay, }) => { @@ -39,11 +36,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..d11833d0 --- /dev/null +++ b/src/components/Preview/TitledPreview.tsx @@ -0,0 +1,86 @@ +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}
} + {type && ( + + + + )} +
+ + ); + + const navigateToOwnEditPage = () => selectedOwnId && navigateToEditPage(generateEditResourceUrl(selectedOwnId)); + const navigateToRefEditPage = () => refId && navigateToEditPage(generateEditResourceUrl(refId)); + + 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 b3fb0863..183a1699 100644 --- a/src/test/__tests__/components/EditPreview.test.tsx +++ b/src/test/__tests__/components/EditPreview.test.tsx @@ -1,10 +1,12 @@ import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; import { EditPreview } from '@components/EditPreview'; import state from '@state'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { RouterProvider, createMemoryRouter } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; +jest.mock('@common/constants/build.constants', () => ({ IS_EMBEDDED_MODE: true })); + const navigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -32,12 +34,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 5741f991..241ec145 100644 --- a/src/views/ExternalResource/ExternalResourcePreview.tsx +++ b/src/views/ExternalResource/ExternalResourcePreview.tsx @@ -25,8 +25,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", From 3d6e79899a8a1fee9b85d43fe39f59548d62f3fd Mon Sep 17 00:00:00 2001 From: nikolai Date: Tue, 10 Dec 2024 15:21:43 +0500 Subject: [PATCH 3/3] fix: UILD-421: Multiple instances: style, navigation fixes (#49) * fix: UILD-421: Multiple instances: style, navigation fixes * remove resource type label within titled preview --- src/components/FullDisplay/FullDisplay.scss | 2 +- .../InstancesList/InstancesList.scss | 20 ++++++++++++++----- src/components/Preview/Preview.scss | 4 +--- src/components/Preview/TitledPreview.tsx | 8 +------- 4 files changed, 18 insertions(+), 16 deletions(-) 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/InstancesList/InstancesList.scss b/src/components/InstancesList/InstancesList.scss index c111e2dd..8e55610d 100644 --- a/src/components/InstancesList/InstancesList.scss +++ b/src/components/InstancesList/InstancesList.scss @@ -24,19 +24,29 @@ } tr { - padding: 0.5rem 0 + padding: 0.5rem 0; } - .button-primary { - margin: 0.4rem 0; + .publisher, + .publication-date { + max-width: 10rem; } - .button-link { - text-decoration: underline; + .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 { diff --git a/src/components/Preview/Preview.scss b/src/components/Preview/Preview.scss index 0b583c83..a9dbc66a 100644 --- a/src/components/Preview/Preview.scss +++ b/src/components/Preview/Preview.scss @@ -102,9 +102,7 @@ } .details { - display: flex; - flex-direction: column; - align-items: center; + padding: 0.7rem; h5 { font-size: 0.95rem; diff --git a/src/components/Preview/TitledPreview.tsx b/src/components/Preview/TitledPreview.tsx index d11833d0..e4cbc0d6 100644 --- a/src/components/Preview/TitledPreview.tsx +++ b/src/components/Preview/TitledPreview.tsx @@ -39,17 +39,11 @@ export const TitledPreview = ({ )}
{title &&
{title}
} - {type && ( - - - - )}
); const navigateToOwnEditPage = () => selectedOwnId && navigateToEditPage(generateEditResourceUrl(selectedOwnId)); - const navigateToRefEditPage = () => refId && navigateToEditPage(generateEditResourceUrl(refId)); return (
@@ -66,7 +60,7 @@ export const TitledPreview = ({ type={ButtonType.Primary} className="toggle-entity-edit" data-testid="edit-self-as-ref" - onClick={navigateToRefEditPage} + onClick={navigateToOwnEditPage} >