From 8f3d5a00c2b941dd16935051504b7f2a0ee1090e Mon Sep 17 00:00:00 2001 From: yashin4112 Date: Wed, 15 Jul 2026 12:20:50 +0530 Subject: [PATCH] fix: implement new empty state for search results for ContentMapper,EntryMapper,AssetMapper and refactor asset mapper components --- .../components/ContentMapper/assetMapper.tsx | 129 ++++++++---------- .../ContentMapper/entryAssetMapper.tsx | 5 +- .../components/ContentMapper/entryMapper.tsx | 18 ++- ui/src/components/ContentMapper/index.scss | 59 +++++++- ui/src/components/ContentMapper/index.tsx | 17 ++- ui/src/utilities/constants.ts | 8 ++ upload-api/src/config/index.json | 4 +- 7 files changed, 154 insertions(+), 86 deletions(-) diff --git a/ui/src/components/ContentMapper/assetMapper.tsx b/ui/src/components/ContentMapper/assetMapper.tsx index fb8f04df5..89fd69a87 100644 --- a/ui/src/components/ContentMapper/assetMapper.tsx +++ b/ui/src/components/ContentMapper/assetMapper.tsx @@ -19,11 +19,10 @@ import { import { RootState } from '../../store'; // Utilities -import { ASSET_MAPPER_EMPTY_STATE } from '../../utilities/constants'; +import { ASSET_MAPPER_EMPTY_STATE, MAPPER_SEARCH_EMPTY_STATE } from '../../utilities/constants'; // Interface import { AssetMapperType, TableTypes, UidMap } from './contentMapper.interface'; -import { ItemStatusMapProp } from '@contentstack/venus-components/build/components/Table/types'; // Styles and Assets import { NoDataFound } from '../../common/assets'; @@ -42,10 +41,8 @@ import { import './index.scss'; const AssetMapper = ({ - tableHeight, onCountChange, }: { - tableHeight: number; onCountChange?: (count: number) => void; }) => { const { projectId = '' } = useParams<{ projectId: string }>(); @@ -56,33 +53,37 @@ const AssetMapper = ({ const [tableData, setTableData] = useState([]); const [loading, setLoading] = useState(false); const [totalCounts, setTotalCounts] = useState(0); - const [itemStatusMap, setItemStatusMap] = useState({}); const [rowIds, setRowIds] = useState>({}); const [persistedRowIds, setPersistedRowIds] = useState>({}); const [isLoadingSaveButton, setisLoadingSaveButton] = useState(false); // True once the initial fetch has settled — used to gate the empty state so it // doesn't flash before assets have loaded. const [hasFetched, setHasFetched] = useState(false); + // Current search term driven by the table. When a search is active we keep the + // table (and its search box) mounted even on 0 results, otherwise the user is + // stranded on the full-page empty state with no way to clear the search. + const [searchText, setSearchText] = useState(''); useEffect(() => { - fetchAssets(''); + fetchAssets('', { seedSelection: true }); }, []); - const fetchAssets = async (searchText: string) => { + // Single server-paginated fetch (same pattern as entryMapper's fetchEntries). The + // Venus table drives paging by calling fetchData with { skip, limit, searchText }; + // we ask the API for just that page and use the returned `count` as the grand total. + // + // seedSelection: only on the initial load do we seed rowIds/persistedRowIds from the + // server's persisted selection. On search / page change we instead re-apply the + // user's current (possibly unsaved) selection so nothing gets wiped. + const fetchAssets = async ( + searchVal: string, + { skip = 0, limit = 30, seedSelection = false }: { skip?: number; limit?: number; seedSelection?: boolean } = {}, + ) => { try { - const statusMap: ItemStatusMapProp = {}; - for (let index = 0; index <= 1000; index++) { - statusMap[index] = 'loading'; - } - setItemStatusMap(statusMap); setLoading(true); - const { data } = await getAssetMapping(0, 1000, searchText ?? '', projectId); + const { data } = await getAssetMapping(skip, limit, searchVal ?? '', projectId); - for (let index = 0; index <= 1000; index++) { - statusMap[index] = 'loaded'; - } - setItemStatusMap({ ...statusMap }); setLoading(false); const validTableData: AssetMapperType[] = mapAssetsToRows(data?.assetMapping); @@ -91,60 +92,33 @@ const AssetMapper = ({ // sends back. Use that so the count and pagination are correct when the // result set is larger than the requested page size. const total = data?.count ?? validTableData?.length ?? 0; + setTotalCounts(total); + onCountChange?.(total); + setHasFetched(true); + + if (!seedSelection) { + // Re-apply the user's current selection onto the freshly fetched page; + // don't touch rowIds/persistedRowIds so nothing gets deselected. + setTableData(applySelectionToAssets(validTableData ?? [], rowIds)); + return; + } const initialSelected = buildSelectedRowIds(validTableData ?? []); setTableData(validTableData ?? []); setRowIds(initialSelected); setPersistedRowIds(initialSelected); - setTotalCounts(total); - onCountChange?.(total); - setHasFetched(true); } catch (error) { console.error('fetchAssets -> error', error); + setLoading(false); setHasFetched(true); } }; - // Fetch table data - const fetchData = async ({ searchText }: TableTypes) => { - fetchAssets(searchText ?? ''); - }; - - // Method for Load more table data - const loadMoreItems = async ({ searchText, skip, limit, startIndex, stopIndex }: TableTypes) => { - try { - const itemStatusMapCopy: ItemStatusMapProp = { ...itemStatusMap }; - for (let index = startIndex; index <= stopIndex; index++) { - itemStatusMapCopy[index] = 'loading'; - } - setItemStatusMap({ ...itemStatusMapCopy }); - setLoading(true); - - const { data } = await getAssetMapping(skip, limit, searchText ?? '', projectId); - - const updateditemStatusMapCopy: ItemStatusMapProp = { ...itemStatusMap }; - for (let index = startIndex; index <= stopIndex; index++) { - updateditemStatusMapCopy[index] = 'loaded'; - } - setItemStatusMap({ ...updateditemStatusMapCopy }); - setLoading(false); - - const validTableData: AssetMapperType[] = mapAssetsToRows(data?.assetMapping); - const newRows = applySelectionToAssets(validTableData ?? [], rowIds); - - // Merge the fetched page into the existing rows at its offset so the - // virtualized table keeps previously loaded rows instead of dropping - // them when the next range is requested. - setTableData((prev) => { - const merged = [...(prev ?? [])]; - newRows.forEach((row, index) => { - merged[Number(skip) + index] = row; - }); - return merged; - }); - } catch (error) { - console.error('loadMoreItems -> error', error); - } + // Driven by the table: page change, rows-per-page change, and search all land here. + const fetchData = async ({ searchText: search, skip, limit }: TableTypes) => { + setSearchText(search ?? ''); + // Searching / paging must not drop the user's in-progress selection. + fetchAssets(search ?? '', { skip, limit }); }; /** @@ -263,32 +237,33 @@ const AssetMapper = ({ ), accessor: accessorAssetName, id: 'uuid', - width: '220px', + width: '400px', }, { disableSortBy: true, Header: ({'Path:'}), accessor: accessorAssetPath, - id: '1' + id: '1', + width: '550px', }, { disableSortBy: true, Header: ({'Size:'}), accessor: accessorFileSize, id: '2', - width: '100px', + width: '120px', }, { disableSortBy: true, Header: ({'Contentstack UIDs:'}), accessor: accessorContentstackUid, - id: '3' + id: '3', } ]; return (
- {(hasFetched && !loading && totalCounts === 0) ? + {(hasFetched && !loading && totalCounts === 0 && !searchText) ? {ASSET_MAPPER_EMPTY_STATE.NO_ASSETS_HEADING}
} @@ -302,9 +277,10 @@ const AssetMapper = ({ version="v2" testId="no-results-found-page" /> : -
+
+ } />
-
Total Assets: {totalCounts}
+
{mapperView === 'assets' ? ( - + ) : ( )} diff --git a/ui/src/components/ContentMapper/entryMapper.tsx b/ui/src/components/ContentMapper/entryMapper.tsx index 2dec101c1..209f3249c 100644 --- a/ui/src/components/ContentMapper/entryMapper.tsx +++ b/ui/src/components/ContentMapper/entryMapper.tsx @@ -30,7 +30,7 @@ import { RootState } from '../../store'; import { updateMigrationData, updateNewMigrationData } from '../../store/slice/migrationDataSlice'; // Utilities -import { CS_ENTRIES, CONTENT_MAPPING_STATUS, STATUS_ICON_Mapping, ENTRY_MAPPER_EMPTY_STATE } from '../../utilities/constants'; +import { CS_ENTRIES, CONTENT_MAPPING_STATUS, STATUS_ICON_Mapping, ENTRY_MAPPER_EMPTY_STATE, MAPPER_SEARCH_EMPTY_STATE } from '../../utilities/constants'; import { validateArray } from '../../utilities/functions'; // Interface @@ -232,7 +232,9 @@ const EntryMapper = ({ handleStepChange }: entryMapperProps) => { setContentTypes(next); setFilteredContentTypes(next); setCount(next?.length ?? 0); - if (!next?.length) clearEntryTableState(); + // When the search matches no content types, keep the currently-selected content + // type and its entries on the right — only the left list shows "No Content Types + // Found." Clearing the table here would strand the user on "No Records Found". } catch (error) { console.error(error); return error; @@ -504,7 +506,7 @@ const EntryMapper = ({ handleStepChange }: entryMapperProps) => {
:
- {(contentTypes?.length > 0 || tableData?.length > 0) ? + {(contentTypes?.length > 0 || tableData?.length > 0 || searchContentType?.length > 0) ?
{/* Content Types List */}
@@ -660,6 +662,16 @@ const EntryMapper = ({ handleStepChange }: entryMapperProps) => { singular: '', plural: `${totalCounts === 0 ? 'Count' : ''}` }} + customEmptyState={ + + } /> {(totalCounts > 0 || (tableData?.length ?? 0) > 0) && (
diff --git a/ui/src/components/ContentMapper/index.scss b/ui/src/components/ContentMapper/index.scss index 6a9f37123..c8987d358 100644 --- a/ui/src/components/ContentMapper/index.scss +++ b/ui/src/components/ContentMapper/index.scss @@ -440,6 +440,57 @@ div .table-row { line-height: 1.2 !important; } +// Center the search-empty state vertically. With v2Features.isNewEmptyState on, Venus +// wraps customEmptyState in `.Table__centerWrapper` inside the table body. The wrapper +// collapses to content height, so make the body a flex column and let the wrapper grow +// to fill it, then center its own content. +.entry-mapper-container .Table:has(.Table__centerWrapper) .Table__body { + display: flex; + flex-direction: column; +} + +.entry-mapper-container .Table__centerWrapper { + flex: 1 1 auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + min-height: 0; + // Nudge the empty state slightly above dead-center so it reads a touch higher. + padding-bottom: 6rem; +} + +// Field mapping table (Map Content Fields step): center the search-empty state. Anchor on +// the venus .Table and take .Table__centerWrapper out of flow (position: absolute) so it +// overlays the full table area and centers, instead of stacking under the header rows. +.field-mapper-container .Table:has(.Table__centerWrapper) { + position: relative; + // Absolute-positioned .Table__centerWrapper adds no height, so give the table an + // explicit tall area to center the empty state within. + height: calc(100vh - 300px); + + .Table__centerWrapper { + position: absolute !important; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } +} + +// Asset mapper table: venus's .Table sets min-height: 31.25rem, forcing the table +// tall and pushing the pagination bar down with a dead gap. Pin an explicit height +// so the table + pagination end where we want and the Save button can sit below. +.entry-asset-mapper .step-container .Table { + height: 400px !important; + min-height: 0px !important; +} + .custom-empty-state { .Icon--original { width: 207px !important; @@ -572,7 +623,7 @@ div .table-row { .entry-mapper-container { // Empty state: venus renders "No Records Found" (.EmptyState) with no scroll body, so // the table collapses. Give .Table an explicit height so it fills like ExecutionLogs. - .Table:has(.EmptyState) { + &:not(.asset-mapper-container) .Table:has(.EmptyState) { height: calc(100vh - 22rem) !important; } @@ -580,8 +631,10 @@ div .table-row { // prop: this keeps the venus pagination bar (a sibling below the body) visible while // the body scrolls internally. Height fits a full page and reserves room for the // search row, pagination bar and Save footer. - .Table__body, - .Table.TableWithPaginated .Table__body { + // Entry mapper only — the asset mapper lets venus size the body from its measured + // tableHeight prop, and this !important would override those inline styles. + &:not(.asset-mapper-container) .Table__body, + &:not(.asset-mapper-container) .Table.TableWithPaginated .Table__body { height: calc(100vh - 520px) !important; max-height: calc(100vh - 520px) !important; } diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index b0574fc51..9e0d5ff28 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -41,7 +41,7 @@ import { RootState } from '../../store'; import { updateMigrationData, updateNewMigrationData } from '../../store/slice/migrationDataSlice'; // Utilities -import { CS_ENTRIES, CONTENT_MAPPING_STATUS, STATUS_ICON_Mapping, CONTENT_MAPPER_EMPTY_STATE } from '../../utilities/constants'; +import { CS_ENTRIES, CONTENT_MAPPING_STATUS, STATUS_ICON_Mapping, CONTENT_MAPPER_EMPTY_STATE, MAPPER_SEARCH_EMPTY_STATE } from '../../utilities/constants'; import { isEmptyString, validateArray } from '../../utilities/functions'; import useBlockNavigation from '../../hooks/userNavigation'; @@ -3289,7 +3289,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
:
- {(contentTypes?.length > 0 || tableData?.length > 0) ? + {(contentTypes?.length > 0 || tableData?.length > 0 || searchContentType?.length > 0) ?
{/* Content Types List */}
@@ -3405,7 +3405,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: {/* Content Type Fields */}
-
+
+ } /> {totalCounts > 0 && (
diff --git a/ui/src/utilities/constants.ts b/ui/src/utilities/constants.ts index 1cc9d1f59..6a2dfd054 100644 --- a/ui/src/utilities/constants.ts +++ b/ui/src/utilities/constants.ts @@ -215,6 +215,14 @@ export const ENTRY_MAPPER_EMPTY_STATE = { 'There are no entries available to map for the already-migrated content types. You can still continue.' } +// Shared "search returned nothing" empty state for the mapper tables — mirrors the +// ExecutionLogs NO_MATCH state so a zero-result search reads consistently across the app. +export const MAPPER_SEARCH_EMPTY_STATE = { + NO_MATCH_HEADING: 'No matching result found', + NO_MATCH_DESCRIPTION: 'Try changing the search query to find what you are looking for.', + NO_MATCH_ICON: 'NoSearchResult' +} + // Asset Mapper (Map Entry step → Assets tab) empty-state text. export const ASSET_MAPPER_EMPTY_STATE = { NO_ASSETS_HEADING: 'No assets available for mapping', diff --git a/upload-api/src/config/index.json b/upload-api/src/config/index.json index 336d27e6b..21c9104b6 100644 --- a/upload-api/src/config/index.json +++ b/upload-api/src/config/index.json @@ -4,7 +4,7 @@ "optionLimit": 100 } }, - "cmsType": "sitecore", + "cmsType": "cmsType", "isLocalPath": true, "awsData": { "awsRegion": "us-east-2", @@ -25,5 +25,5 @@ "base_url": "drupal_assets_base_url", "public_path": "drupal_assets_public_path" }, - "localPath": "/Users/yash.shinde/Documents/Migration/Expample-Data/Sitecore/delta/airports_only_add_v1.zip" + "localPath": "your_local_cms_data_path" } \ No newline at end of file