From 0326c7365ebf8b59efea2875aa391b3ddc44c523 Mon Sep 17 00:00:00 2001 From: nikolai Date: Wed, 25 Dec 2024 16:36:40 +0400 Subject: [PATCH] feat: UILD-420: Compare Bib Resources | Linked --- src/App.scss | 4 - src/assets/times-circle-12.svg | 6 + src/assets/transfer-16.svg | 18 ++ src/common/constants/search.constants.ts | 2 + src/common/constants/uiElements.constants.ts | 5 + src/common/helpers/recordFormatting.helper.ts | 11 +- src/common/hooks/useConfig.hook.ts | 9 +- src/common/hooks/useSearch.ts | 9 +- src/components/Comparison/Comparison.scss | 86 ++++++++++ src/components/Comparison/Comparison.tsx | 127 ++++++++++++++ src/components/Comparison/index.ts | 1 + src/components/FullDisplay/FullDisplay.scss | 3 - src/components/FullDisplay/FullDisplay.tsx | 21 +-- src/components/FullDisplay/PreviewContent.tsx | 5 +- src/components/Pagination/Pagination.tsx | 2 +- .../SearchControlPane/SearchControlPane.scss | 11 ++ .../SearchControlPane/SearchControlPane.tsx | 16 +- .../SearchControls/SearchControls.scss | 11 ++ .../SearchControls/SearchControls.tsx | 159 ++++++++++-------- .../SearchResultEntry/SearchResultEntry.scss | 36 ++-- .../SearchResultEntry/SearchResultEntry.tsx | 103 +++++++----- src/components/Table/Table.scss | 7 +- src/store/stores/search.ts | 6 +- src/store/stores/ui.ts | 9 + src/types/record.d.ts | 1 + src/views/Search/Search.tsx | 43 ++++- translations/ui-linked-data/en.json | 10 ++ 27 files changed, 550 insertions(+), 171 deletions(-) create mode 100644 src/assets/times-circle-12.svg create mode 100644 src/assets/transfer-16.svg create mode 100644 src/components/Comparison/Comparison.scss create mode 100644 src/components/Comparison/Comparison.tsx create mode 100644 src/components/Comparison/index.ts diff --git a/src/App.scss b/src/App.scss index fb3867bf..279dc7c1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -63,10 +63,6 @@ h6 { line-height: 1.1; } - h3 { - margin: 0; - } - select, input { &:disabled { diff --git a/src/assets/times-circle-12.svg b/src/assets/times-circle-12.svg new file mode 100644 index 00000000..242aaecf --- /dev/null +++ b/src/assets/times-circle-12.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/assets/transfer-16.svg b/src/assets/transfer-16.svg new file mode 100644 index 00000000..d4caa5b1 --- /dev/null +++ b/src/assets/transfer-16.svg @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/src/common/constants/search.constants.ts b/src/common/constants/search.constants.ts index f84ed370..3b2e79be 100644 --- a/src/common/constants/search.constants.ts +++ b/src/common/constants/search.constants.ts @@ -94,6 +94,8 @@ export type AdvancedSearchSchema = AdvancedSearchSchemaRow[]; export const SEARCH_RESULTS_LIMIT = 10; +export const MIN_AMT_OF_INSTANCES_TO_COMPARE = 2; + export const BROWSE_PRECEDING_RECORDS_COUNT = 5; export const SELECT_IDENTIFIERS = Object.values(SearchIdentifiers); diff --git a/src/common/constants/uiElements.constants.ts b/src/common/constants/uiElements.constants.ts index f36c595d..19864f9d 100644 --- a/src/common/constants/uiElements.constants.ts +++ b/src/common/constants/uiElements.constants.ts @@ -22,6 +22,11 @@ export enum DropdownItemType { customComponent = 'customComponent', } +export enum FullDisplayType { + Basic = 'basic', + Comparison = 'comparison', +} + export enum AriaModalKind { Basic = 'Basic', AdvancedSearch = 'Advanced search', diff --git a/src/common/helpers/recordFormatting.helper.ts b/src/common/helpers/recordFormatting.helper.ts index 731bc95c..80e72877 100644 --- a/src/common/helpers/recordFormatting.helper.ts +++ b/src/common/helpers/recordFormatting.helper.ts @@ -6,7 +6,7 @@ import { NON_BF_RECORD_CONTAINERS, NON_BF_RECORD_ELEMENTS, } from '@common/constants/bibframeMapping.constants'; -import { getRecordPropertyData } from './record.helper'; +import { getEditingRecordBlocks, getRecordPropertyData, unwrapRecordValuesFromCommonContainer } from './record.helper'; import { Row } from '@components/Table'; export const formatRecord = ({ @@ -267,3 +267,12 @@ export const formatDependeciesTable = (deps: Record[]): Row[] = }; }) as Row[]; }; + +export const getReferenceIdsRaw = (record: RecordEntry) => { + if (!record) return; + + const contents = unwrapRecordValuesFromCommonContainer(record); + const { block, reference } = getEditingRecordBlocks(contents); + + if (block && reference) return getReferenceIds(record, block, reference?.key); +}; diff --git a/src/common/hooks/useConfig.hook.ts b/src/common/hooks/useConfig.hook.ts index cca639f2..8ce94753 100644 --- a/src/common/hooks/useConfig.hook.ts +++ b/src/common/hooks/useConfig.hook.ts @@ -6,6 +6,7 @@ import { getPrimaryEntitiesFromRecord, getRecordTitle } from '@common/helpers/re import { useInputsState, useProfileState } from '@src/store'; import { useProcessedRecordAndSchema } from './useProcessedRecordAndSchema.hook'; import { useServicesContext } from './useServicesContext'; +import { getReferenceIdsRaw } from '@common/helpers/recordFormatting.helper'; export type PreviewParams = { noStateUpdate?: boolean; @@ -39,7 +40,7 @@ export const useConfig = () => { setInitialSchemaKey, setSchema, } = useProfileState(); - const { setUserValues, previewContent, setPreviewContent, setSelectedRecordBlocks, setSelectedEntries } = + const { setUserValues, setPreviewContent, setSelectedRecordBlocks, setSelectedEntries } = useInputsState(); const { getProcessedRecordAndSchema } = useProcessedRecordAndSchema(); const isProcessingProfiles = useRef(false); @@ -113,6 +114,7 @@ export const useConfig = () => { const recordData = record?.resource || {}; const recordTitle = getRecordTitle(recordData as RecordEntry); const entities = getPrimaryEntitiesFromRecord(record as RecordEntry); + const referenceIds = getReferenceIdsRaw(record as RecordEntry); if (selectedProfile) { !previewParams?.noStateUpdate && setSelectedProfile(selectedProfile); @@ -126,8 +128,8 @@ export const useConfig = () => { }); if (previewParams && recordId) { - setPreviewContent([ - ...(previewParams.singular ? [] : previewContent.filter(({ id }) => id !== recordId)), + setPreviewContent(prev => [ + ...(previewParams.singular ? [] : prev.filter(({ id }) => id !== recordId)), { id: recordId, base: updatedSchema, @@ -135,6 +137,7 @@ export const useConfig = () => { initKey, title: recordTitle, entities, + referenceIds, }, ]); } diff --git a/src/common/hooks/useSearch.ts b/src/common/hooks/useSearch.ts index 5cac6d9d..58ecfea2 100644 --- a/src/common/hooks/useSearch.ts +++ b/src/common/hooks/useSearch.ts @@ -7,7 +7,8 @@ import { generateSearchParamsState } from '@common/helpers/search.helper'; import { usePagination } from '@common/hooks/usePagination'; import { useSearchContext } from '@common/hooks/useSearchContext'; import { useFetchSearchData } from '@common/hooks/useFetchSearchData'; -import { useInputsState, useLoadingState, useSearchState } from '@src/store'; +import { useInputsState, useLoadingState, useSearchState, useUIState } from '@src/store'; +import { FullDisplayType } from '@common/constants/uiElements.constants'; export const useSearch = () => { const { @@ -39,7 +40,7 @@ export const useSearch = () => { setFacetsBySegments, resetFacetsBySegments, } = useSearchState(); - + const { fullDisplayComponentType } = useUIState(); const { fetchData } = useFetchSearchData(); const { getCurrentPageNumber, @@ -72,7 +73,7 @@ export const useSearch = () => { const submitSearch = useCallback(() => { clearPagination(); - resetPreviewContent(); + fullDisplayComponentType !== FullDisplayType.Comparison && resetPreviewContent(); updateFacetsBySegments(query, searchBy, facets); if (hasSearchParams) { @@ -90,7 +91,7 @@ export const useSearch = () => { setSearchBy(defaultSearchBy); setQuery(''); setMessage(''); - resetPreviewContent(); + fullDisplayComponentType !== FullDisplayType.Comparison && resetPreviewContent(); updateFacetsBySegments('', defaultSearchBy, {} as Limiters); }, [defaultSearchBy]); diff --git a/src/components/Comparison/Comparison.scss b/src/components/Comparison/Comparison.scss new file mode 100644 index 00000000..3f1e41f9 --- /dev/null +++ b/src/components/Comparison/Comparison.scss @@ -0,0 +1,86 @@ +.comparison { + display: flex; + flex-direction: column; + width: 700px; + border-left: 1px solid rgba(0, 0, 0, 0.1); + + header { + display: flex; + flex-direction: column; + + h2 { + display: flex; + align-items: center; + gap: 0.2rem; + font-size: 0.875rem; + line-height: normal; + } + + .heading, + .subheading { + padding: 0.5rem 0.7rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + + > * { + font-size: 0.875rem; + font-weight: 700; + } + } + } + + &-contents { + display: flex; + flex-grow: 1; + overflow: hidden; + + .entry { + width: 100%; + min-width: 50%; + overflow-x: hidden; + overflow-y: scroll; + flex: 1; + border-right: 1px solid rgba(0, 0, 0, 0.1); + + &-header { + display: flex; + flex-direction: column; + gap: 0.2rem; + padding: 0.938rem; + + &-controls { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + } + } + + .preview-entity { + border-right: none; + } + } + + .titled-preview { + width: 100%; + } + + .insufficient-resource-amt { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + width: 100%; + font-size: 1.138rem; + color: rgba(0, 0, 0, 0.62); + + svg { + width: 2.5rem; + } + } + } +} diff --git a/src/components/Comparison/Comparison.tsx b/src/components/Comparison/Comparison.tsx new file mode 100644 index 00000000..d04ebfb0 --- /dev/null +++ b/src/components/Comparison/Comparison.tsx @@ -0,0 +1,127 @@ +import { FormattedMessage, useIntl } from 'react-intl'; +import { Pagination } from '@components/Pagination'; +import { useState } from 'react'; +import { useInputsState, useSearchState, useUIState } from '@src/store'; +import { Button, ButtonType } from '@components/Button'; +import Times16 from '@src/assets/times-16.svg?react'; +import TimesCircle12 from '@src/assets/times-circle-12.svg?react'; +import Transfer16 from '@src/assets/transfer-16.svg?react'; +import GeneralSearch from '@src/assets/general-search.svg?react'; +import { Preview } from '@components/Preview'; +import { PreviewActionsDropdown } from '@components/PreviewActionsDropdown'; +import { ResourceType } from '@common/constants/record.constants'; +import { generateEditResourceUrl } from '@common/helpers/navigation.helper'; +import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; +import './Comparison.scss'; + +export const Comparison = () => { + const { formatMessage } = useIntl(); + const { previewContent, setPreviewContent, resetPreviewContent } = useInputsState(); + const { resetSelectedInstances } = useSearchState(); + const { resetFullDisplayComponentType } = useUIState(); + const { navigateToEditPage } = useNavigateToEditPage(); + const [currentPage, setCurrentPage] = useState(0); + + const handleCloseComparison = () => { + resetPreviewContent(); + resetFullDisplayComponentType(); + resetSelectedInstances(); + }; + + const handleRemoveComparisonItem = (id: string) => { + setPreviewContent(prev => prev.filter(({ id: prevId }) => prevId !== id)); + }; + + const handleNavigateToOwnEditPage = (id: string) => navigateToEditPage(generateEditResourceUrl(id)); + const totalPages = (previewContent.length > 1 ? previewContent.length : 2) - 1; + + return ( +
+
+
+ +

+ + +

+ +
+
+ + + + +
+
+
+ {previewContent + .slice(currentPage, currentPage + 2) + .map(({ initKey, base, userValues, id, title, referenceIds }) => ( +
+
+
+ + handleNavigateToOwnEditPage(id)} + /> +
+

{title}

+
+ +
+ ))} + {previewContent.length <= 1 && ( +
+ + +
+ )} +
+ +
+ ); +}; diff --git a/src/components/Comparison/index.ts b/src/components/Comparison/index.ts new file mode 100644 index 00000000..5f02e38b --- /dev/null +++ b/src/components/Comparison/index.ts @@ -0,0 +1 @@ +export { Comparison } from './Comparison'; diff --git a/src/components/FullDisplay/FullDisplay.scss b/src/components/FullDisplay/FullDisplay.scss index e01e8586..4899037c 100644 --- a/src/components/FullDisplay/FullDisplay.scss +++ b/src/components/FullDisplay/FullDisplay.scss @@ -1,8 +1,5 @@ .full-display { &-container { - display: flex; - flex-direction: row; - gap: 0.5rem; width: 540px; border-left: 1px solid rgba(0, 0, 0, 0.1); diff --git a/src/components/FullDisplay/FullDisplay.tsx b/src/components/FullDisplay/FullDisplay.tsx index 83ced297..e02d7b07 100644 --- a/src/components/FullDisplay/FullDisplay.tsx +++ b/src/components/FullDisplay/FullDisplay.tsx @@ -1,16 +1,17 @@ -import { DOM_ELEMENTS } from '@common/constants/domElementsIdentifiers.constants'; -import { useInputsState } from '@src/store'; -import './FullDisplay.scss'; +import { useInputsState, useUIState } from '@src/store'; import { PreviewContent } from './PreviewContent'; +import { FullDisplayType } from '@common/constants/uiElements.constants'; +import { Comparison } from '@components/Comparison'; +import './FullDisplay.scss'; export const FullDisplay = () => { const { previewContent } = useInputsState(); + const { fullDisplayComponentType } = useUIState(); + + const contents = { + [FullDisplayType.Basic]: !!previewContent.length && , + [FullDisplayType.Comparison]: , + }; - return ( - !!previewContent.length && ( -
- -
- ) - ); + return contents?.[fullDisplayComponentType]; }; diff --git a/src/components/FullDisplay/PreviewContent.tsx b/src/components/FullDisplay/PreviewContent.tsx index b3cad58d..3bd9dbf0 100644 --- a/src/components/FullDisplay/PreviewContent.tsx +++ b/src/components/FullDisplay/PreviewContent.tsx @@ -6,6 +6,7 @@ import { Button, ButtonType } from '@components/Button'; import { Preview } from '@components/Preview'; import { useInputsState } from '@src/store'; import Times16 from '@src/assets/times-16.svg?react'; +import { DOM_ELEMENTS } from '@common/constants/domElementsIdentifiers.constants'; import './FullDisplay.scss'; export const PreviewContent = () => { @@ -19,7 +20,7 @@ export const PreviewContent = () => { const handleButtonClick = () => setPreviewContent(previewContent.filter(entry => entry.id !== id)); return ( -
+
)} -
+ ); }); }; diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index e9a83c12..ef9dcd94 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -29,7 +29,7 @@ export const Pagination: FC = memo( isLooped = false, }) => { const isFirstPage = currentPage === 0; - const isDisabledNext = totalPages ? currentPage === totalPages - 1 : false; + const isDisabledNext = totalPages ? currentPage >= totalPages - 1 : false; const startCount = isFirstPage ? 1 : currentPage * pageSize + 1; const pageNumber = currentPage + 1; let endCount; diff --git a/src/components/SearchControlPane/SearchControlPane.scss b/src/components/SearchControlPane/SearchControlPane.scss index b44db141..8dab645d 100644 --- a/src/components/SearchControlPane/SearchControlPane.scss +++ b/src/components/SearchControlPane/SearchControlPane.scss @@ -8,6 +8,17 @@ height: 2.75rem; align-items: center; + .open-ctl { + height: 2.125rem; + width: 2.125rem; + padding: 0.5rem; + + svg { + height: 12px; + transform: rotate(90deg); + } + } + &-title { display: flex; flex-direction: column; diff --git a/src/components/SearchControlPane/SearchControlPane.tsx b/src/components/SearchControlPane/SearchControlPane.tsx index c37d4f9e..a7ed59f5 100644 --- a/src/components/SearchControlPane/SearchControlPane.tsx +++ b/src/components/SearchControlPane/SearchControlPane.tsx @@ -3,7 +3,10 @@ import classNames from 'classnames'; import { IS_EMBEDDED_MODE } from '@common/constants/build.constants'; import { useSearchContext } from '@common/hooks/useSearchContext'; import { SearchSegment } from '@common/constants/search.constants'; -import { useSearchState } from '@src/store'; +import { useSearchState, useUIState } from '@src/store'; +import { Button } from '@components/Button'; +import CaretDown from '@src/assets/caret-down.svg?react'; +import { useIntl } from 'react-intl'; import './SearchControlPane.scss'; type SearchControlPaneProps = { @@ -21,7 +24,9 @@ export const SearchControlPane: FC = ({ renderCloseButton, segmentsConfig, }) => { + const { formatMessage } = useIntl(); const { pageMetadata: searchResultsMetadata } = useSearchState(); + const { isSearchPaneCollapsed, setIsSearchPaneCollapsed } = useUIState(); const { navigationSegment } = useSearchContext(); const selectedSegment = navigationSegment?.value; const isVisibleSubLabel = segmentsConfig @@ -31,6 +36,15 @@ export const SearchControlPane: FC = ({ return (
{renderCloseButton?.()} + {isSearchPaneCollapsed && ( + + )}

{label} diff --git a/src/components/SearchControls/SearchControls.scss b/src/components/SearchControls/SearchControls.scss index 294cd3de..41db121c 100644 --- a/src/components/SearchControls/SearchControls.scss +++ b/src/components/SearchControls/SearchControls.scss @@ -37,6 +37,17 @@ padding: 0 1rem; background-color: rgba(0, 0, 0, 0.03); + .close-ctl { + height: 2.125rem; + width: 2.125rem; + padding: 0.5rem; + + svg { + height: 12px; + transform: rotate(-90deg); + } + } + &-title { font-size: 0.875rem; } diff --git a/src/components/SearchControls/SearchControls.tsx b/src/components/SearchControls/SearchControls.tsx index 7e6e2140..15c3d1ef 100644 --- a/src/components/SearchControls/SearchControls.tsx +++ b/src/components/SearchControls/SearchControls.tsx @@ -11,7 +11,7 @@ import { Select } from '@components/Select'; import { SearchFilters } from '@components/SearchFilters'; import { Textarea } from '@components/Textarea'; import { Announcement } from '@components/Announcement'; -import { useSearchState, useUIState } from '@src/store'; +import { useInputsState, useSearchState, useUIState } from '@src/store'; import SearchSegments from './SearchSegments'; import CaretDown from '@src/assets/caret-down.svg?react'; import XInCircle from '@src/assets/x-in-circle.svg?react'; @@ -47,7 +47,8 @@ export const SearchControls: FC = ({ submitSearch, changeSegment, clearVa resetFacets: resetControls, setFacetsBySegments, } = useSearchState(); - const { setIsAdvancedSearchOpen } = useUIState(); + const { isSearchPaneCollapsed, setIsSearchPaneCollapsed, setIsAdvancedSearchOpen } = useUIState(); + const { resetPreviewContent } = useInputsState(); const [searchParams, setSearchParams] = useSearchParams(); const [announcementMessage, setAnnouncementMessage] = useState(''); const searchQueryParam = searchParams.get(SearchQueryParams.Query); @@ -70,6 +71,7 @@ export const SearchControls: FC = ({ submitSearch, changeSegment, clearVa const onResetButtonClick = () => { clearValuesAndResetControls(); + resetPreviewContent(); hasSearchParams && setSearchParams({}); hasSearchParams && setNavigationState({}); setAnnouncementMessage(formatMessage({ id: 'ld.aria.filters.reset.announce' })); @@ -80,88 +82,97 @@ export const SearchControls: FC = ({ submitSearch, changeSegment, clearVa setFacetsBySegments(DEFAULT_FACET_BY_SEGMENT); }, []); - return ( -
-
-

- -

- -
-
- {isVisibleSegments && } + useEffect(() => () => setIsSearchPaneCollapsed(false), []); -
- {isVisibleSearchByControl && ( -