Skip to content
Merged

Dev #1116

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 58 additions & 71 deletions ui/src/components/ContentMapper/assetMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -42,10 +41,8 @@ import {
import './index.scss';

const AssetMapper = ({
tableHeight,
onCountChange,
}: {
tableHeight: number;
onCountChange?: (count: number) => void;
}) => {
const { projectId = '' } = useParams<{ projectId: string }>();
Expand All @@ -56,33 +53,37 @@ const AssetMapper = ({
const [tableData, setTableData] = useState<AssetMapperType[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [totalCounts, setTotalCounts] = useState<number>(0);
const [itemStatusMap, setItemStatusMap] = useState({});
const [rowIds, setRowIds] = useState<Record<string, boolean>>({});
const [persistedRowIds, setPersistedRowIds] = useState<Record<string, boolean>>({});
const [isLoadingSaveButton, setisLoadingSaveButton] = useState<boolean>(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<boolean>(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<string>('');

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);
Expand All @@ -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 });
};

/**
Expand Down Expand Up @@ -263,32 +237,33 @@ const AssetMapper = ({
),
accessor: accessorAssetName,
id: 'uuid',
width: '220px',
width: '400px',
},
{
disableSortBy: true,
Header: (<span>{'Path:'}</span>),
accessor: accessorAssetPath,
id: '1'
id: '1',
width: '550px',
},
{
disableSortBy: true,
Header: (<span>{'Size:'}</span>),
accessor: accessorFileSize,
id: '2',
width: '100px',
width: '120px',
},
{
disableSortBy: true,
Header: (<span>{'Contentstack UIDs:'}</span>),
accessor: accessorContentstackUid,
id: '3'
id: '3',
}
];

return (
<div className="step-container">
{(hasFetched && !loading && totalCounts === 0) ?
{(hasFetched && !loading && totalCounts === 0 && !searchText) ?
<EmptyState
forPage="emptyStateV2"
heading={<div className="empty_search_heading">{ASSET_MAPPER_EMPTY_STATE.NO_ASSETS_HEADING}</div>}
Expand All @@ -302,9 +277,10 @@ const AssetMapper = ({
version="v2"
testId="no-results-found-page"
/> :
<div className='entry-mapper-container'>
<div>
<InfiniteScrollTable
key={'asset-mapper-table'}
className={'asset-mapper-table'}
loading={loading}
canSearch={true}
totalCounts={Math.max(0, totalCounts)}
Expand All @@ -313,23 +289,34 @@ const AssetMapper = ({
uniqueKey={'id'}
isRowSelect={true}
fullRowSelect={true}
itemStatusMap={itemStatusMap}
fetchTableData={fetchData}
loadMoreItems={loadMoreItems}
tableHeight={tableHeight}
tableHeight={400}
equalWidthColumns={false}
columnSelector={false}
v2Features={{ pagination: true, isNewEmptyState: true }}
rowPerPageOptions={[10, 30, 50, 100]}
minBatchSizeToFetch={30}
initialSelectedRowIds={rowIds}
itemSize={80}
itemSize={60}
getSelectedRow={handleSelectedAssets}
rowSelectCheckboxProp={{ key: '_canSelect', value: true }}
name={{
singular: '',
plural: `${totalCounts === 0 ? 'Count' : ''}`
}}
customEmptyState={
<EmptyState
forPage="list"
heading={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_HEADING}
description={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_DESCRIPTION}
moduleIcon={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_ICON}
type="secondary"
className="custom-empty-state"
/>
}
/>
<div className="mapper-footer">
<div>Total Assets: <strong>{totalCounts}</strong></div>
<div></div>
<Button
className="saveButton"
onClick={handleSaveAssets}
Expand Down
5 changes: 1 addition & 4 deletions ui/src/components/ContentMapper/entryAssetMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ interface entryAssetMapperProps {
const EntryAssetMapper = ({ handleStepChange }: entryAssetMapperProps) => {
const [mapperView, setMapperView] = useState<'entries' | 'assets'>('entries');

const calcHeight = () => window.innerHeight - 361;
const tableHeight = calcHeight();

return (
// Plain flex-column wrapper β€” NOT .step-container. Both child mappers render their own
// .step-container (height: 100%), so reusing it here would nest two full-height flex
Expand All @@ -51,7 +48,7 @@ const EntryAssetMapper = ({ handleStepChange }: entryAssetMapperProps) => {
</div>

{mapperView === 'assets' ? (
<AssetMapper tableHeight={tableHeight} />
<AssetMapper />
) : (
<EntryMapper handleStepChange={handleStepChange} />
)}
Expand Down
18 changes: 15 additions & 3 deletions ui/src/components/ContentMapper/entryMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -504,7 +506,7 @@ const EntryMapper = ({ handleStepChange }: entryMapperProps) => {
</div>
:
<div className="step-container">
{(contentTypes?.length > 0 || tableData?.length > 0) ?
{(contentTypes?.length > 0 || tableData?.length > 0 || searchContentType?.length > 0) ?
<div className="d-flex flex-wrap table-container">
{/* Content Types List */}
<div className="content-types-list-wrapper">
Expand Down Expand Up @@ -660,6 +662,16 @@ const EntryMapper = ({ handleStepChange }: entryMapperProps) => {
singular: '',
plural: `${totalCounts === 0 ? 'Count' : ''}`
}}
customEmptyState={
<EmptyState
forPage="list"
heading={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_HEADING}
description={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_DESCRIPTION}
moduleIcon={MAPPER_SEARCH_EMPTY_STATE.NO_MATCH_ICON}
type="secondary"
className="custom-empty-state"
/>
}
/>
{(totalCounts > 0 || (tableData?.length ?? 0) > 0) && (
<div className="mapper-footer">
Expand Down
59 changes: 56 additions & 3 deletions ui/src/components/ContentMapper/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -572,16 +623,18 @@ 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;
}

// Size the scroll body via CSS (ExecutionLogs pattern) rather than the tableHeight
// 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;
}
Expand Down
Loading
Loading