import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators, compose } from 'redux'; import { get, isEmpty } from 'lodash'; import { FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useLocation } from 'react-router-dom'; import { Header } from '@buffetjs/custom'; import isEqual from 'react-fast-compare'; import { stringify } from 'qs'; import { PopUpWarning, request, CheckPermissions, useGlobalContext, useUserPermissions, } from 'strapi-helper-plugin'; import pluginId from '../../pluginId'; import pluginPermissions from '../../permissions'; import { useQueryParams } from '../../hooks'; import { formatFiltersFromQuery, generatePermissionsObject, getRequestUrl, getTrad, } from '../../utils'; import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown'; import Container from '../../components/Container'; import CustomTable from '../../components/CustomTable'; import FilterPicker from '../../components/FilterPicker'; import Search from '../../components/Search'; import ListViewProvider from '../ListViewProvider'; import { AddFilterCta, FilterIcon, Wrapper } from './components'; import Filter from './Filter'; import Footer from './Footer'; import { getData, getDataSucceeded, onChangeBulk, onChangeBulkSelectall, onDeleteDataError, onDeleteDataSucceeded, onDeleteSeveralDataSucceeded, resetProps, setModalLoadingState, toggleModalDelete, toggleModalDeleteAll, setLayout, onChangeListHeaders, onResetListHeaders, } from './actions'; import makeSelectListView from './selectors'; import { getAllAllowedHeaders, getFirstSortableHeader } from './utils'; /* eslint-disable react/no-array-index-key */ function ListView({ didDeleteData, entriesToDelete, onChangeBulk, onChangeBulkSelectall, onDeleteDataError, onDeleteDataSucceeded, onDeleteSeveralDataSucceeded, setModalLoadingState, showWarningDelete, showModalConfirmButtonLoading, showWarningDeleteAll, toggleModalDelete, toggleModalDeleteAll, data, displayedHeaders, getData, getDataSucceeded, isLoading, layout, onChangeListHeaders, onResetListHeaders, pagination: { total }, resetProps, setLayout, slug, }) { const { contentType: { attributes, settings: { defaultSortBy, defaultSortOrder, bulkable: isBulkable, filterable: isFilterable, searchable: isSearchable, pageSize: defaultPageSize, }, }, } = layout; const { emitEvent } = useGlobalContext(); const emitEventRef = useRef(emitEvent); const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]); const { isLoading: isLoadingForPermissions, allowedActions: { canCreate, canRead, canUpdate, canDelete }, } = useUserPermissions(viewPermissions); const defaultSort = `${defaultSortBy}:${defaultSortOrder}`; const initParams = useMemo(() => ({ page: 1, pageSize: defaultPageSize, _sort: defaultSort }), [ defaultPageSize, defaultSort, ]); const [{ query, rawQuery }, setQuery] = useQueryParams(initParams); const { pathname } = useLocation(); const { push } = useHistory(); const { formatMessage } = useIntl(); const [isFilterPickerOpen, setFilterPickerState] = useState(false); const [idToDelete, setIdToDelete] = useState(null); const contentType = layout.contentType; const hasDraftAndPublish = contentType.options.draftAndPublish; const allAllowedHeaders = useMemo(() => getAllAllowedHeaders(attributes), [attributes]); const filters = useMemo(() => { return formatFiltersFromQuery(query); }, [query]); const _sort = query._sort; const _q = query._q || ''; const label = contentType.info.label; const params = useMemo(() => { return rawQuery || `?${stringify(initParams, { encode: false })}`; }, [initParams, rawQuery]); const firstSortableHeader = useMemo(() => getFirstSortableHeader(displayedHeaders), [ displayedHeaders, ]); useEffect(() => { setLayout(layout); setFilterPickerState(false); return () => { resetProps(); }; }, [layout, setLayout, resetProps]); // Using a ref to avoid requests being fired multiple times on slug on change // We need it because the hook as mulitple dependencies so it may run before the permissions have checked const requestUrlRef = useRef(''); const fetchData = useCallback( async (endPoint, abortSignal = false) => { getData(); const signal = abortSignal || new AbortController().signal; try { const { results, pagination } = await request(endPoint, { method: 'GET', signal }); getDataSucceeded(pagination, results); } catch (err) { if (err.name !== 'AbortError') { console.error(err); strapi.notification.error(getTrad('error.model.fetch')); } } }, [getData, getDataSucceeded] ); const handleChangeListLabels = useCallback( ({ name, value }) => { // Display a notification if trying to remove the last displayed field if (value && displayedHeaders.length === 1) { strapi.notification.toggle({ type: 'warning', message: { id: 'content-manager.notification.error.displayedFields' }, }); } else { emitEventRef.current('didChangeDisplayedFields'); onChangeListHeaders({ name, value }); } }, [displayedHeaders, onChangeListHeaders] ); const handleConfirmDeleteAllData = useCallback(async () => { try { setModalLoadingState(); await request(getRequestUrl(`collection-types/${slug}/actions/bulkDelete`), { method: 'POST', body: { ids: entriesToDelete }, }); onDeleteSeveralDataSucceeded(); emitEventRef.current('didBulkDeleteEntries'); } catch (err) { strapi.notification.error(`${pluginId}.error.record.delete`); } }, [entriesToDelete, onDeleteSeveralDataSucceeded, slug, setModalLoadingState]); const handleConfirmDeleteData = useCallback(async () => { try { let trackerProperty = {}; if (hasDraftAndPublish) { const dataToDelete = data.find(obj => obj.id.toString() === idToDelete.toString()); const isDraftEntry = isEmpty(dataToDelete.published_at); const status = isDraftEntry ? 'draft' : 'published'; trackerProperty = { status }; } emitEventRef.current('willDeleteEntry', trackerProperty); setModalLoadingState(); await request(getRequestUrl(`collection-types/${slug}/${idToDelete}`), { method: 'DELETE', }); strapi.notification.toggle({ type: 'success', message: { id: `${pluginId}.success.record.delete` }, }); // Close the modal and refetch data onDeleteDataSucceeded(); emitEventRef.current('didDeleteEntry', trackerProperty); } catch (err) { const errorMessage = get( err, 'response.payload.message', formatMessage({ id: `${pluginId}.error.record.delete` }) ); strapi.notification.toggle({ type: 'warning', message: errorMessage, }); // Close the modal onDeleteDataError(); } }, [ hasDraftAndPublish, setModalLoadingState, slug, idToDelete, onDeleteDataSucceeded, data, formatMessage, onDeleteDataError, ]); useEffect(() => { const abortController = new AbortController(); const { signal } = abortController; const shouldSendRequest = !isLoadingForPermissions && canRead; const requestUrl = `/${pluginId}/collection-types/${slug}${params}`; if (shouldSendRequest && requestUrl.includes(requestUrlRef.current)) { fetchData(requestUrl, signal); } return () => { requestUrlRef.current = slug; abortController.abort(); }; }, [isLoadingForPermissions, canRead, getData, slug, params, getDataSucceeded, fetchData]); const handleClickDelete = id => { setIdToDelete(id); toggleModalDelete(); }; const handleModalClose = useCallback(() => { if (didDeleteData) { const requestUrl = `/${pluginId}/collection-types/${slug}${params}`; fetchData(requestUrl); } }, [fetchData, didDeleteData, slug, params]); const toggleFilterPickerState = useCallback(() => { setFilterPickerState(prevState => { if (!prevState) { emitEventRef.current('willFilterEntries'); } return !prevState; }); }, []); const headerAction = useMemo(() => { if (!canCreate) { return []; } return [ { label: formatMessage( { id: 'content-manager.containers.List.addAnEntry', }, { entity: label || 'Content Manager', } ), onClick: () => { const trackerProperty = hasDraftAndPublish ? { status: 'draft' } : {}; emitEventRef.current('willCreateEntry', trackerProperty); push({ pathname: `${pathname}/create`, }); }, color: 'primary', type: 'button', icon: true, style: { paddingLeft: 15, paddingRight: 15, fontWeight: 600, }, }, ]; }, [label, pathname, canCreate, formatMessage, hasDraftAndPublish, push]); const headerProps = useMemo(() => { /* eslint-disable indent */ return { title: { label: label || 'Content Manager', }, content: canRead ? formatMessage( { id: total > 1 ? `${pluginId}.containers.List.pluginHeaderDescription` : `${pluginId}.containers.List.pluginHeaderDescription.singular`, }, { label: total } ) : null, actions: headerAction, }; }, [total, headerAction, label, canRead, formatMessage]); const handleToggleModalDeleteAll = e => { emitEventRef.current('willBulkDeleteEntries'); toggleModalDeleteAll(e); }; return ( <> {!isFilterPickerOpen &&
} {isSearchable && canRead && ( )} {canRead && (
{isFilterable && ( <> {filters.map(({ filter: filterName, name, value }, key) => ( ))} )}
)} 1 ? '.all' : '' }` ), }} popUpWarningType="danger" onConfirm={handleConfirmDeleteAllData} onClosed={handleModalClose} isConfirmButtonLoading={showModalConfirmButtonLoading} /> ); } ListView.propTypes = { displayedHeaders: PropTypes.array.isRequired, data: PropTypes.array.isRequired, didDeleteData: PropTypes.bool.isRequired, entriesToDelete: PropTypes.array.isRequired, layout: PropTypes.exact({ components: PropTypes.object.isRequired, contentType: PropTypes.shape({ attributes: PropTypes.object.isRequired, info: PropTypes.shape({ label: PropTypes.string.isRequired }).isRequired, layouts: PropTypes.shape({ list: PropTypes.array.isRequired, }).isRequired, options: PropTypes.object.isRequired, settings: PropTypes.object.isRequired, }).isRequired, }).isRequired, isLoading: PropTypes.bool.isRequired, getData: PropTypes.func.isRequired, getDataSucceeded: PropTypes.func.isRequired, onChangeBulk: PropTypes.func.isRequired, onChangeBulkSelectall: PropTypes.func.isRequired, onChangeListHeaders: PropTypes.func.isRequired, onDeleteDataError: PropTypes.func.isRequired, onDeleteDataSucceeded: PropTypes.func.isRequired, onDeleteSeveralDataSucceeded: PropTypes.func.isRequired, onResetListHeaders: PropTypes.func.isRequired, pagination: PropTypes.shape({ total: PropTypes.number.isRequired }).isRequired, resetProps: PropTypes.func.isRequired, setModalLoadingState: PropTypes.func.isRequired, showModalConfirmButtonLoading: PropTypes.bool.isRequired, showWarningDelete: PropTypes.bool.isRequired, showWarningDeleteAll: PropTypes.bool.isRequired, slug: PropTypes.string.isRequired, toggleModalDelete: PropTypes.func.isRequired, toggleModalDeleteAll: PropTypes.func.isRequired, setLayout: PropTypes.func.isRequired, }; const mapStateToProps = makeSelectListView(); export function mapDispatchToProps(dispatch) { return bindActionCreators( { getData, getDataSucceeded, onChangeBulk, onChangeBulkSelectall, onChangeListHeaders, onDeleteDataError, onDeleteDataSucceeded, onDeleteSeveralDataSucceeded, onResetListHeaders, resetProps, setModalLoadingState, toggleModalDelete, toggleModalDeleteAll, setLayout, }, dispatch ); } const withConnect = connect(mapStateToProps, mapDispatchToProps); export default compose(withConnect)(memo(ListView, isEqual));