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, sortBy } from 'lodash';
import { FormattedMessage, useIntl } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
import {
PopUpWarning,
generateFiltersFromSearch,
request,
CheckPermissions,
useGlobalContext,
useUserPermissions,
useQuery,
} from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import pluginPermissions from '../../permissions';
import {
checkIfAttributeIsDisplayable,
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 State from '../../components/State';
import ListViewProvider from '../ListViewProvider';
// import { onChangeListLabels, resetListLabels } from '../Main/actions';
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,
} from './actions';
import makeSelectListView from './selectors';
/* eslint-disable react/no-array-index-key */
const FilterPicker = () =>
FILTER
;
const onChangeListLabels = () => console.log('todo');
const resetListLabels = () => console.log('todo');
function ListView({
count,
data,
didDeleteData,
// emitEvent,
entriesToDelete,
isLoading,
location: { pathname },
getData,
getDataSucceeded,
layouts,
history: { push },
onChangeBulk,
onChangeBulkSelectall,
onChangeListLabels,
onDeleteDataError,
onDeleteDataSucceeded,
onDeleteSeveralDataSucceeded,
resetListLabels,
resetProps,
setModalLoadingState,
showWarningDelete,
showModalConfirmButtonLoading,
showWarningDeleteAll,
slug,
toggleModalDelete,
toggleModalDeleteAll,
}) {
const { emitEvent } = useGlobalContext();
const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]);
const {
isLoading: isLoadingForPermissions,
allowedActions: { canCreate, canRead, canUpdate, canDelete },
} = useUserPermissions(viewPermissions);
const query = useQuery();
const { search } = useLocation();
const isFirstRender = useRef(true);
const { formatMessage } = useIntl();
const [isLabelPickerOpen, setLabelPickerState] = useState(false);
const [isFilterPickerOpen, setFilterPickerState] = useState(false);
const [idToDelete, setIdToDelete] = useState(null);
const contentTypePath = useMemo(() => {
return [slug, 'contentType'];
}, [slug]);
const hasDraftAndPublish = useMemo(() => {
return get(layouts, [...contentTypePath, 'schema', 'options', 'draftAndPublish'], false);
}, [contentTypePath, layouts]);
const getLayoutSetting = useCallback(
settingName => {
return get(layouts, [...contentTypePath, 'settings', settingName], '');
},
[contentTypePath, layouts]
);
// Related to the search
const defaultSort = useMemo(() => {
return `${getLayoutSetting('defaultSortBy')}:${getLayoutSetting('defaultSortOrder')}`;
}, [getLayoutSetting]);
const filters = useMemo(() => {
const currentSearch = new URLSearchParams(search);
// Delete all params that are not related to the filters
const paramsToDelete = ['_limit', '_page', '_sort', '_q'];
for (let i = 0; i < paramsToDelete.length; i++) {
currentSearch.delete(paramsToDelete[i]);
}
return generateFiltersFromSearch(currentSearch.toString());
}, [search]);
const _limit = useMemo(() => {
return parseInt(query.get('_limit') || getLayoutSetting('pageSize'), 10);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getLayoutSetting, query.get('_limit')]);
const _q = useMemo(() => {
return query.get('_q') || '';
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.get('_q')]);
const _page = useMemo(() => {
return parseInt(query.get('_page') || 1, 10);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.get('_page')]);
const _sort = useMemo(() => {
return query.get('_sort') || defaultSort;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultSort, query.get('_sort')]);
const _start = useMemo(() => {
return (_page - 1) * parseInt(_limit, 10);
}, [_limit, _page]);
const searchToSendForRequest = useMemo(() => {
const currentSearch = new URLSearchParams(search);
currentSearch.set('_limit', _limit);
currentSearch.set('_sort', _sort);
currentSearch.set('_start', _start);
currentSearch.delete('_page');
return currentSearch.toString();
}, [_limit, _sort, _start, search]);
const getDataActionRef = useRef(getData);
const getDataSucceededRef = useRef(getDataSucceeded);
// Settings
const isBulkable = useMemo(() => {
return getLayoutSetting('bulkable');
}, [getLayoutSetting]);
const isFilterable = useMemo(() => {
return getLayoutSetting('filterable');
}, [getLayoutSetting]);
const isSearchable = useMemo(() => {
return getLayoutSetting('searchable');
}, [getLayoutSetting]);
const shouldSendRequest = useMemo(() => {
return !isLoadingForPermissions && canRead;
}, [canRead, isLoadingForPermissions]);
const fetchData = async (search = searchToSendForRequest) => {
try {
getDataActionRef.current();
const [{ count }, data] = await Promise.all([
request(getRequestUrl(`explorer/${slug}/count?${search}`), {
method: 'GET',
}),
request(getRequestUrl(`explorer/${slug}?${search}`), {
method: 'GET',
}),
]);
getDataSucceededRef.current(count, data);
} catch (err) {
strapi.notification.error(`${pluginId}.error.model.fetch`);
}
};
const getMetaDatas = useCallback(
(path = []) => {
return get(layouts, [...contentTypePath, 'metadatas', ...path], {});
},
[contentTypePath, layouts]
);
const listLayout = useMemo(() => {
return get(layouts, [...contentTypePath, 'layouts', 'list'], []);
}, [contentTypePath, layouts]);
const listSchema = useMemo(() => {
return get(layouts, [...contentTypePath, 'schema'], {});
}, [layouts, contentTypePath]);
const label = useMemo(() => {
return get(listSchema, ['info', 'name'], '');
}, [listSchema]);
const tableHeaders = useMemo(() => {
let headers = listLayout.map(label => {
return { ...getMetaDatas([label, 'list']), name: label };
});
if (hasDraftAndPublish) {
headers.push({
label: formatMessage({ id: getTrad('containers.ListPage.table-headers.published_at') }),
searchable: false,
sortable: true,
name: 'published_at',
key: '__published_at__',
cellFormatter: cellData => {
const isPublished = !isEmpty(cellData.published_at);
return ;
},
});
}
return headers;
}, [formatMessage, getMetaDatas, hasDraftAndPublish, listLayout]);
const getFirstSortableElement = useCallback(
(name = '') => {
return get(
listLayout.filter(h => {
return h !== name && getMetaDatas([h, 'list', 'sortable']) === true;
}),
['0'],
'id'
);
},
[getMetaDatas, listLayout]
);
const allLabels = useMemo(() => {
const filteredMetadatas = getMetaDatas();
return sortBy(
Object.keys(filteredMetadatas)
.filter(key => {
return checkIfAttributeIsDisplayable(get(listSchema, ['attributes', key], {}));
})
.map(label => ({
name: label,
value: listLayout.includes(label),
})),
['label', 'name']
);
}, [getMetaDatas, listLayout, listSchema]);
useEffect(() => {
return () => {
isFirstRender.current = true;
};
}, [slug]);
useEffect(() => {
if (!isFirstRender.current) {
fetchData(searchToSendForRequest);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchToSendForRequest]);
useEffect(() => {
return () => {
resetProps();
setFilterPickerState(false);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug]);
useEffect(() => {
if (shouldSendRequest) {
fetchData();
}
return () => {
isFirstRender.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldSendRequest]);
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 };
}
emitEvent('willDeleteEntry', trackerProperty);
setModalLoadingState();
await request(getRequestUrl(`explorer/${slug}/${idToDelete}`), {
method: 'DELETE',
});
strapi.notification.success(`${pluginId}.success.record.delete`);
// Close the modal and refetch data
onDeleteDataSucceeded();
emitEvent('didDeleteEntry', trackerProperty);
} catch (err) {
const errorMessage = get(
err,
'response.payload.message',
formatMessage({ id: `${pluginId}.error.record.delete` })
);
strapi.notification.error(errorMessage);
// Close the modal
onDeleteDataError();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setModalLoadingState, slug, idToDelete, onDeleteDataSucceeded, hasDraftAndPublish, data]);
const handleConfirmDeleteAllData = useCallback(async () => {
const params = Object.assign(entriesToDelete);
try {
setModalLoadingState();
await request(getRequestUrl(`explorer/deleteAll/${slug}`), {
method: 'DELETE',
params,
});
onDeleteSeveralDataSucceeded();
} catch (err) {
strapi.notification.error(`${pluginId}.error.record.delete`);
}
}, [entriesToDelete, onDeleteSeveralDataSucceeded, slug, setModalLoadingState]);
const handleChangeListLabels = ({ name, value }) => {
const currentSort = _sort;
// Display a notification if trying to remove the last displayed field
if (value && listLayout.length === 1) {
strapi.notification.error('content-manager.notification.error.displayedFields');
return;
}
// Update the sort when removing the displayed one
if (currentSort.split(':')[0] === name && value) {
emitEvent('didChangeDisplayedFields');
handleChangeSearch({
target: {
name: '_sort',
value: `${getFirstSortableElement(name)}:ASC`,
},
});
}
// Update the Main reducer
onChangeListLabels({
target: {
name,
slug,
value: !value,
},
});
};
const handleChangeFilters = ({ target: { value } }) => {
const newSearch = new URLSearchParams();
// Set the default params
newSearch.set('_limit', _limit);
newSearch.set('_sort', _sort);
newSearch.set('_page', 1);
value.forEach(({ filter, name, value: filterValue }) => {
const filterType = filter === '=' ? '' : filter;
const filterName = `${name}${filterType}`;
newSearch.append(filterName, filterValue);
});
push({ search: newSearch.toString() });
};
const handleChangeSearch = async ({ target: { name, value } }) => {
const currentSearch = new URLSearchParams(searchToSendForRequest);
// Pagination
currentSearch.delete('_start');
if (value === '') {
currentSearch.delete(name);
} else {
currentSearch.set(name, value);
}
const searchToString = currentSearch.toString();
push({ search: searchToString });
};
const handleClickDelete = id => {
setIdToDelete(id);
toggleModalDelete();
};
const handleModalClose = () => {
if (didDeleteData) {
fetchData();
}
};
const handleSubmit = (filters = []) => {
emitEvent('didFilterEntries');
toggleFilterPickerState();
handleChangeFilters({ target: { name: 'filters', value: filters } });
};
const toggleFilterPickerState = () => {
if (!isFilterPickerOpen) {
emitEvent('willFilterEntries');
}
setFilterPickerState(prevState => !prevState);
};
const toggleLabelPickerState = () => {
if (!isLabelPickerOpen) {
emitEvent('willChangeListFieldsSettings');
}
setLabelPickerState(prevState => !prevState);
};
const filterPickerActions = [
{
label: `${pluginId}.components.FiltersPickWrapper.PluginHeader.actions.clearAll`,
kind: 'secondary',
onClick: () => {
toggleFilterPickerState();
// Delete all filters
handleChangeFilters({ target: { name: 'filters', value: [] } });
},
},
{
label: `${pluginId}.components.FiltersPickWrapper.PluginHeader.actions.apply`,
kind: 'primary',
type: 'submit',
},
];
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' } : {};
emitEvent('willCreateEntry', trackerProperty);
push({
pathname: `${pathname}/create`,
});
},
color: 'primary',
type: 'button',
icon: true,
style: {
paddingLeft: 15,
paddingRight: 15,
fontWeight: 600,
},
},
];
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[label, pathname, search, canCreate, formatMessage, hasDraftAndPublish]
);
const headerProps = useMemo(() => {
/* eslint-disable indent */
return {
title: {
label: label || 'Content Manager',
},
content: canRead
? formatMessage(
{
id:
count > 1
? `${pluginId}.containers.List.pluginHeaderDescription`
: `${pluginId}.containers.List.pluginHeaderDescription.singular`,
},
{ label: count }
)
: null,
actions: headerAction,
};
/* eslint-enable indent */
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count, headerAction, label, canRead, formatMessage]);
return (
<>
{!isFilterPickerOpen && }
{isSearchable && canRead && (
)}
{canRead && (
{isFilterable && (
<>
{filters.map((filter, key) => (
))}
>
)}
{
resetListLabels(slug);
}}
slug={slug}
toggle={toggleLabelPickerState}
/>
)}
1 ? '.all' : ''
}`
),
}}
popUpWarningType="danger"
onConfirm={handleConfirmDeleteAllData}
onClosed={handleModalClose}
isConfirmButtonLoading={showModalConfirmButtonLoading}
/>
>
);
}
ListView.defaultProps = {
layouts: {},
};
ListView.propTypes = {
count: PropTypes.number.isRequired,
data: PropTypes.array.isRequired,
didDeleteData: PropTypes.bool.isRequired,
// emitEvent: PropTypes.func.isRequired,
entriesToDelete: PropTypes.array.isRequired,
isLoading: PropTypes.bool.isRequired,
layouts: PropTypes.object,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
search: PropTypes.string.isRequired,
}).isRequired,
// models: PropTypes.array.isRequired,
getData: PropTypes.func.isRequired,
getDataSucceeded: PropTypes.func.isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
onChangeBulk: PropTypes.func.isRequired,
onChangeBulkSelectall: PropTypes.func.isRequired,
onChangeListLabels: PropTypes.func.isRequired,
onDeleteDataError: PropTypes.func.isRequired,
onDeleteDataSucceeded: PropTypes.func.isRequired,
onDeleteSeveralDataSucceeded: PropTypes.func.isRequired,
resetListLabels: PropTypes.func.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,
};
const mapStateToProps = makeSelectListView();
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getData,
getDataSucceeded,
onChangeBulk,
onChangeBulkSelectall,
onChangeListLabels,
onDeleteDataError,
onDeleteDataSucceeded,
onDeleteSeveralDataSucceeded,
resetListLabels,
resetProps,
setModalLoadingState,
toggleModalDelete,
toggleModalDeleteAll,
},
dispatch
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(withConnect, memo)(ListView);