in translate/src/modules/entitieslist/components/EntitiesList.tsx [86:308]
export function EntitiesList(): React.ReactElement<'div'> {
const dispatch = useAppDispatch();
const store = useAppStore();
const showNotification = useContext(ShowNotification);
const batchactions = useBatchactions();
const { entities, fetchCount, fetching, hasMore, page } = useEntities();
const location = useContext(Location);
const isAuthUser = useAppSelector((state) => state[USER].isAuthenticated);
const { checkUnsavedChanges } = useContext(UnsavedActions);
const mounted = useRef(false);
const list = useRef<HTMLDivElement>(null);
const selectedEntitiesCount = batchactions.entities.length;
const windowWidth = useWindowWidth();
const entitiesList = useContext(EntitiesListContext);
const quitBatchActions = useCallback(() => dispatch(resetSelection()), []);
const showBatchActions = useCallback(() => entitiesList.show(false), []);
useEffect(() => {
const handleShortcuts = (ev: KeyboardEvent) => {
// On Ctrl + Shift + A, select all entities for batch editing.
if (ev.keyCode === 65 && !ev.altKey && ev.ctrlKey && ev.shiftKey) {
ev.preventDefault();
dispatch(selectAll(location));
}
};
document.addEventListener('keydown', handleShortcuts);
return () => document.removeEventListener('keydown', handleShortcuts);
}, [dispatch, location]);
const selectEntity = useCallback(
(entity: EntityType, replaceHistory?: boolean) => {
// Do not re-select already selected entity
if (entity.pk !== location.entity) {
checkUnsavedChanges(() => {
dispatch(resetSelection());
const nextLocation = { entity: entity.pk };
if (replaceHistory) {
location.replace(nextLocation);
} else {
location.push(nextLocation);
}
});
}
},
[dispatch, location, store],
);
/*
* If entity not provided through a URL parameter, or if provided entity
* cannot be found, select the first entity in the list.
*/
useEffect(() => {
const selectedEntity = location.entity;
const firstEntity = entities[0];
const isValid = entities.some(({ pk }) => pk === selectedEntity);
if ((!selectedEntity || !isValid) && firstEntity) {
// Replace the last history item instead of pushing a new one.
selectEntity(firstEntity, true);
// Only do this the very first time entities are loaded.
if (fetchCount === 1 && selectedEntity && !isValid) {
showNotification(ENTITY_NOT_FOUND);
}
}
});
// Whenever the route changes, we want to verify that the user didn't
// change locale, project, resource... If they did, then we'll have
// to reset the current list of entities, in order to start a fresh
// list and hide the previous entities.
//
// Notes:
// * It might seem to be an anti-pattern to change the state after the
// component has rendered, but that's actually the easiest way to
// implement that feature.
// * Other solutions might involve using `history.listen` to trigger
// an action on each location change.
// * I haven't been able to figure out how to test this feature. It
// is possible that going for another possible solutions will make
// testing easier, which would be very desirable.
useEffect(() => {
if (mounted.current) {
dispatch(resetEntities());
}
}, [
dispatch,
// Note: location.entity is explicitly not included here
location.locale,
location.project,
location.resource,
location.search,
location.status,
location.extra,
location.tag,
location.author,
location.time,
]);
const scrollToSelected = useCallback(() => {
if (!mounted.current) {
return;
}
const element = list.current?.querySelector('li.selected');
const mediaQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
element?.scrollIntoView?.({
behavior: mediaQuery?.matches ? 'auto' : 'smooth',
block: 'nearest',
});
}, []);
// Scroll to selected entity when entity changes
// and when entity list loads for the first time
const prevEntityCount = usePrevious(entities.length);
useEffect(() => {
if (!prevEntityCount && entities.length > 0) {
scrollToSelected();
}
}, [entities.length]);
useEffect(scrollToSelected, [location.entity]);
const { code } = useContext(Locale);
const getSiblingEntities_ = useCallback(
(entity: number) => dispatch(getSiblingEntities(entity, code)),
[dispatch, code],
);
const toggleForBatchEditing = useCallback(
(entity: number, shiftKeyPressed: boolean) => {
checkUnsavedChanges(() => {
// If holding Shift, check all entities in the entity list between the
// lastCheckedEntity and the entity if entity not checked. If entity
// checked, uncheck all entities in-between.
if (shiftKeyPressed && batchactions.lastCheckedEntity) {
const entityListIds = entities.map((e) => e.pk);
const start = entityListIds.indexOf(entity);
const end = entityListIds.indexOf(batchactions.lastCheckedEntity);
const entitySelection = entityListIds.slice(
Math.min(start, end),
Math.max(start, end) + 1,
);
if (batchactions.entities.includes(entity)) {
dispatch(uncheckSelection(entitySelection, entity));
} else {
dispatch(checkSelection(entitySelection, entity));
}
} else {
dispatch(toggleSelection(entity));
}
});
},
[batchactions, dispatch, entities, store],
);
const getMoreEntities = useCallback(() => {
if (!fetching) {
// Currently shown entities should be excluded from the next results.
dispatch(getEntities(location, page));
}
}, [dispatch, entities, fetching, location, page]);
// Must be after other useEffect() calls, as they are run in order during mount
useEffect(() => {
mounted.current = true;
}, []);
const hasNextPage = fetching || hasMore;
const [sentryRef, { rootRef }] = useInfiniteScroll({
loading: fetching,
hasNextPage,
onLoadMore: getMoreEntities,
rootMargin: '0px 0px 600px 0px',
});
useEffect(() => {
rootRef(list.current);
}, [list.current]);
if (entities.length === 0 && !hasNextPage) {
// When there are no results for the current search.
return (
<div className='entities unselectable' ref={list}>
<h3 className='no-results'>
<div className='fas fa-exclamation-circle'></div>
No results
</h3>
</div>
);
}
return (
<div className='entities unselectable' ref={list}>
<ul>
{entities.map((entity) => (
<Entity
key={entity.pk}
checkedForBatchEditing={batchactions.entities.includes(entity.pk)}
toggleForBatchEditing={toggleForBatchEditing}
entity={entity}
isReadOnlyEditor={entity.readonly || !isAuthUser}
selected={!selectedEntitiesCount && entity.pk === location.entity}
selectEntity={selectEntity}
getSiblingEntities={getSiblingEntities_}
parameters={location}
/>
))}
</ul>
{hasNextPage && <SkeletonLoader items={entities} sentryRef={sentryRef} />}
{selectedEntitiesCount === 0 || windowWidth !== 'narrow' ? null : (
<EntitiesToolbar
count={selectedEntitiesCount}
onEdit={showBatchActions}
onClear={quitBatchActions}
/>
)}
</div>
);
}