Changed displayed headers behaviour, simplify main reducer

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-11-04 18:37:19 +01:00
parent 78534fca2e
commit 5792cfeea8
19 changed files with 378 additions and 390 deletions

View File

@ -1,6 +1,6 @@
module.exports = ({ env }) => ({ module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'), host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337), port: env.int('PORT', 3000),
admin: { admin: {
auth: { auth: {
secret: env('ADMIN_JWT_SECRET', 'example-token'), secret: env('ADMIN_JWT_SECRET', 'example-token'),

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback } from 'react'; import React, { memo, useCallback, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { get, isEmpty, isNull, isObject, toLower, toString } from 'lodash'; import { get, isEmpty, isNull, isObject, toLower, toString } from 'lodash';
import moment from 'moment'; import moment from 'moment';
@ -67,26 +67,17 @@ const getDisplayedValue = (type, value, name) => {
}; };
function Row({ canDelete, canUpdate, isBulkable, row, headers }) { function Row({ canDelete, canUpdate, isBulkable, row, headers }) {
const { entriesToDelete, onChangeBulk, onClickDelete, schema } = useListView(); const { entriesToDelete, onChangeBulk, onClickDelete } = useListView();
const { emitEvent } = useGlobalContext();
const emitEventRef = useRef(emitEvent);
const memoizedDisplayedValue = useCallback( const memoizedDisplayedValue = useCallback(
name => { (name, type) => {
const type = get(schema, ['attributes', name, 'type'], 'string');
return getDisplayedValue(type, row[name], name); return getDisplayedValue(type, row[name], name);
}, },
[row, schema] [row]
); );
const isMedia = useCallback(
header => {
return get(schema, ['attributes', header.name, 'type']) === 'media';
},
[schema]
);
const { emitEvent } = useGlobalContext();
const links = [ const links = [
{ {
icon: canUpdate ? <FontAwesomeIcon icon="pencil-alt" /> : null, icon: canUpdate ? <FontAwesomeIcon icon="pencil-alt" /> : null,
@ -95,7 +86,7 @@ function Row({ canDelete, canUpdate, isBulkable, row, headers }) {
icon: canDelete ? <FontAwesomeIcon icon="trash-alt" /> : null, icon: canDelete ? <FontAwesomeIcon icon="trash-alt" /> : null,
onClick: e => { onClick: e => {
e.stopPropagation(); e.stopPropagation();
emitEvent('willDeleteEntryFromList'); emitEventRef.current('willDeleteEntryFromList');
onClickDelete(row.id); onClickDelete(row.id);
}, },
}, },
@ -113,14 +104,16 @@ function Row({ canDelete, canUpdate, isBulkable, row, headers }) {
/> />
</td> </td>
)} )}
{headers.map(header => { {headers.map(({ key, name, fieldSchema: { type }, cellFormatter }) => {
const isMedia = type === 'media';
return ( return (
<td key={header.key || header.name}> <td key={key}>
{isMedia(header) && <MediaPreviewList files={memoizedDisplayedValue(header.name)} />} {isMedia && <MediaPreviewList files={memoizedDisplayedValue(name, type)} />}
{header.cellFormatter && header.cellFormatter(row)} {cellFormatter && cellFormatter(row)}
{!isMedia(header) && !header.cellFormatter && ( {!isMedia && !cellFormatter && (
<Truncate> <Truncate>
<Truncated>{memoizedDisplayedValue(header.name)}</Truncated> <Truncated>{memoizedDisplayedValue(name, type)}</Truncated>
</Truncate> </Truncate>
)} )}
</td> </td>

View File

@ -1,7 +1,8 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useGlobalContext } from 'strapi-helper-plugin'; import { useGlobalContext } from 'strapi-helper-plugin';
import useListView from '../../hooks/useListView'; import { useListView } from '../../hooks';
import CustomInputCheckbox from '../CustomInputCheckbox'; import CustomInputCheckbox from '../CustomInputCheckbox';
import { Arrow, Thead } from './styledComponents'; import { Arrow, Thead } from './styledComponents';
@ -11,10 +12,11 @@ function TableHeader({ headers, isBulkable }) {
const { const {
data, data,
entriesToDelete, entriesToDelete,
firstSortableElement,
onChangeBulkSelectall, onChangeBulkSelectall,
onChangeSearch, onChangeSearch,
_sort, _sort,
// to keep
firstSortableHeader,
} = useListView(); } = useListView();
const { emitEvent } = useGlobalContext(); const { emitEvent } = useGlobalContext();
const [sortBy, sortOrder] = _sort.split(':'); const [sortBy, sortOrder] = _sort.split(':');
@ -33,19 +35,19 @@ function TableHeader({ headers, isBulkable }) {
/> />
</th> </th>
)} )}
{headers.map(header => { {headers.map(({ key, name, metadatas: { label, sortable } }) => {
return ( return (
<th <th
key={header.key || header.name} key={key}
onClick={() => { onClick={() => {
if (header.sortable) { if (sortable) {
emitEvent('didSortEntries'); emitEvent('didSortEntries');
const isCurrentSort = header.name === sortBy; const isCurrentSort = name === sortBy;
const nextOrder = isCurrentSort && sortOrder === 'ASC' ? 'DESC' : 'ASC'; const nextOrder = isCurrentSort && sortOrder === 'ASC' ? 'DESC' : 'ASC';
let value = `${header.name}:${nextOrder}`; let value = `${name}:${nextOrder}`;
if (isCurrentSort && sortOrder === 'DESC') { if (isCurrentSort && sortOrder === 'DESC') {
value = `${firstSortableElement}:ASC`; value = `${firstSortableHeader}:ASC`;
} }
onChangeSearch({ onChangeSearch({
@ -57,12 +59,10 @@ function TableHeader({ headers, isBulkable }) {
} }
}} }}
> >
<span className={header.sortable ? 'sortable' : ''}> <span className={sortable ? 'sortable' : ''}>
{header.label} {label}
{sortBy === header.name && ( {sortBy === name && <Arrow fill="#212529" isUp={sortOrder === 'ASC' && 'isAsc'} />}
<Arrow fill="#212529" isUp={sortOrder === 'ASC' && 'isAsc'} />
)}
</span> </span>
</th> </th>
); );

View File

@ -1,19 +1,55 @@
import React, { memo } from 'react'; import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useLocation, useHistory } from 'react-router-dom'; import { useLocation, useHistory } from 'react-router-dom';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { upperFirst } from 'lodash'; import { upperFirst, isEmpty } from 'lodash';
import { LoadingIndicator } from 'strapi-helper-plugin'; import { LoadingIndicator, useGlobalContext } from 'strapi-helper-plugin';
import useListView from '../../hooks/useListView'; import useListView from '../../hooks/useListView';
import { getTrad } from '../../utils';
import State from '../State';
import TableHeader from './TableHeader'; import TableHeader from './TableHeader';
import { LoadingContainer, LoadingWrapper, Table, TableEmpty, TableRow } from './styledComponents'; import { LoadingContainer, LoadingWrapper, Table, TableEmpty, TableRow } from './styledComponents';
import ActionCollapse from './ActionCollapse'; import ActionCollapse from './ActionCollapse';
import Row from './Row'; import Row from './Row';
const CustomTable = ({ canUpdate, canDelete, data, headers, isBulkable, showLoader }) => { const CustomTable = ({
const { emitEvent, entriesToDelete, label, filters, _q } = useListView(); canUpdate,
canDelete,
data,
displayedHeaders,
hasDraftAndPublish,
isBulkable,
showLoader,
}) => {
const { formatMessage } = useIntl();
const { entriesToDelete, label, filters, _q } = useListView();
const { emitEvent } = useGlobalContext();
const { pathname } = useLocation(); const { pathname } = useLocation();
const { push } = useHistory(); const { push } = useHistory();
const headers = useMemo(() => {
if (hasDraftAndPublish) {
return [
...displayedHeaders,
{
key: '__published_at_temp_key__',
name: 'published_at',
fieldSchema: {},
metadatas: {
label: formatMessage({ id: getTrad('containers.ListPage.table-headers.published_at') }),
searchable: false,
sortable: true,
},
cellFormatter: cellData => {
const isPublished = !isEmpty(cellData.published_at);
return <State isPublished={isPublished} />;
},
},
];
}
return displayedHeaders;
}, [formatMessage, hasDraftAndPublish, displayedHeaders]);
const colSpanLength = isBulkable && canDelete ? headers.length + 2 : headers.length + 1; const colSpanLength = isBulkable && canDelete ? headers.length + 2 : headers.length + 1;
@ -93,22 +129,14 @@ const CustomTable = ({ canUpdate, canDelete, data, headers, isBulkable, showLoad
); );
}; };
CustomTable.defaultProps = {
canDelete: false,
canUpdate: false,
data: [],
headers: [],
isBulkable: true,
showLoader: false,
};
CustomTable.propTypes = { CustomTable.propTypes = {
canDelete: PropTypes.bool, canDelete: PropTypes.bool.isRequired,
canUpdate: PropTypes.bool, canUpdate: PropTypes.bool.isRequired,
data: PropTypes.array, data: PropTypes.array.isRequired,
headers: PropTypes.array, displayedHeaders: PropTypes.array.isRequired,
isBulkable: PropTypes.bool, hasDraftAndPublish: PropTypes.bool.isRequired,
showLoader: PropTypes.bool, isBulkable: PropTypes.bool.isRequired,
showLoader: PropTypes.bool.isRequired,
}; };
export default memo(CustomTable); export default memo(CustomTable);

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { memo, useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ButtonDropdown } from 'reactstrap'; import { ButtonDropdown } from 'reactstrap';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -13,15 +13,23 @@ import LayoutWrapper from './LayoutWrapper';
import MenuDropdown from './MenuDropdown'; import MenuDropdown from './MenuDropdown';
import Toggle from './Toggle'; import Toggle from './Toggle';
const DisplayedFieldsDropdown = ({ const DisplayedFieldsDropdown = ({ displayedHeaders, items, onChange, onClickReset, slug }) => {
isOpen,
items,
onChange,
onClickReset,
slug,
toggle,
}) => {
const { emitEvent } = useGlobalContext(); const { emitEvent } = useGlobalContext();
const emitEventRef = useRef(emitEvent);
const [isOpen, setIsOpen] = useState(false);
const toggle = useCallback(
() =>
setIsOpen(prev => {
if (prev === false) {
emitEventRef.current('willChangeListFieldsSettings');
}
return !prev;
}),
[]
);
return ( return (
<DropdownWrapper> <DropdownWrapper>
@ -38,9 +46,7 @@ const DisplayedFieldsDropdown = ({
<FormattedMessage id="app.links.configure-view" /> <FormattedMessage id="app.links.configure-view" />
</LayoutWrapper> </LayoutWrapper>
</DropdownItemLink> </DropdownItemLink>
<FormattedMessage <FormattedMessage id={`${pluginId}.containers.ListPage.displayedFields`}>
id={`${pluginId}.containers.ListPage.displayedFields`}
>
{msg => ( {msg => (
<ItemDropdownReset onClick={onClickReset}> <ItemDropdownReset onClick={onClickReset}>
<div <div
@ -55,21 +61,18 @@ const DisplayedFieldsDropdown = ({
</ItemDropdownReset> </ItemDropdownReset>
)} )}
</FormattedMessage> </FormattedMessage>
{items.map(item => ( {items.map(headerName => {
<ItemDropdown const value = displayedHeaders.findIndex(({ name }) => name === headerName) !== -1;
key={item.name} const handleChange = () => onChange({ name: headerName, value });
toggle={false}
onClick={() => onChange(item)} return (
> <ItemDropdown key={headerName} toggle={false} onClick={handleChange}>
<div> <div>
<InputCheckbox <InputCheckbox onChange={handleChange} name={headerName} value={value} />
onChange={() => onChange(item)}
name={item.name}
value={item.value}
/>
</div> </div>
</ItemDropdown> </ItemDropdown>
))} );
})}
</MenuDropdown> </MenuDropdown>
</ButtonDropdown> </ButtonDropdown>
</DropdownWrapper> </DropdownWrapper>
@ -77,12 +80,11 @@ const DisplayedFieldsDropdown = ({
}; };
DisplayedFieldsDropdown.propTypes = { DisplayedFieldsDropdown.propTypes = {
isOpen: PropTypes.bool.isRequired, displayedHeaders: PropTypes.array.isRequired,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onClickReset: PropTypes.func.isRequired, onClickReset: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired, slug: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
}; };
export default DisplayedFieldsDropdown; export default memo(DisplayedFieldsDropdown);

View File

@ -3,10 +3,17 @@ import {
GET_DATA_SUCCEEDED, GET_DATA_SUCCEEDED,
ON_CHANGE_BULK, ON_CHANGE_BULK,
ON_CHANGE_BULK_SELECT_ALL, ON_CHANGE_BULK_SELECT_ALL,
//
ON_CHANGE_LIST_HEADERS,
ON_RESET_LIST_HEADERS,
//
ON_DELETE_DATA_ERROR, ON_DELETE_DATA_ERROR,
ON_DELETE_DATA_SUCCEEDED, ON_DELETE_DATA_SUCCEEDED,
ON_DELETE_SEVERAL_DATA_SUCCEEDED, ON_DELETE_SEVERAL_DATA_SUCCEEDED,
RESET_PROPS, RESET_PROPS,
//
SET_LIST_LAYOUT,
//
SET_MODAL_LOADING_STATE, SET_MODAL_LOADING_STATE,
TOGGLE_MODAL_DELETE, TOGGLE_MODAL_DELETE,
TOGGLE_MODAL_DELETE_ALL, TOGGLE_MODAL_DELETE_ALL,
@ -57,6 +64,8 @@ export function onDeleteSeveralDataSucceeded() {
}; };
} }
export const onResetListHeaders = () => ({ type: ON_RESET_LIST_HEADERS });
export function resetProps() { export function resetProps() {
return { type: RESET_PROPS }; return { type: RESET_PROPS };
} }
@ -78,3 +87,7 @@ export function toggleModalDelete() {
type: TOGGLE_MODAL_DELETE, type: TOGGLE_MODAL_DELETE,
}; };
} }
export const setLayout = layout => ({ layout, type: SET_LIST_LAYOUT });
export const onChangeListHeaders = target => ({ type: ON_CHANGE_LIST_HEADERS, target });

View File

@ -10,3 +10,7 @@ export const RESET_PROPS = 'ContentManager/ListView/RESET_PROPS';
export const TOGGLE_MODAL_DELETE_ALL = 'ContentManager/ListView/TOGGLE_MODAL_DELETE_ALL'; export const TOGGLE_MODAL_DELETE_ALL = 'ContentManager/ListView/TOGGLE_MODAL_DELETE_ALL';
export const TOGGLE_MODAL_DELETE = 'ContentManager/ListView/TOGGLE_MODAL_DELETE'; export const TOGGLE_MODAL_DELETE = 'ContentManager/ListView/TOGGLE_MODAL_DELETE';
export const SET_MODAL_LOADING_STATE = 'ContentManager/ListView/SET_MODAL_LOADING_STATE'; export const SET_MODAL_LOADING_STATE = 'ContentManager/ListView/SET_MODAL_LOADING_STATE';
export const ON_CHANGE_LIST_HEADERS = 'ContentManager/ListView/ON_CHANGE_LIST_HEADERS ';
export const ON_RESET_LIST_HEADERS = 'ContentManager/ListView/ON_RESET_LIST_HEADERS ';
export const SET_LIST_LAYOUT = 'ContentManager/ListView/SET_LIST_LAYOUT ';

View File

@ -2,7 +2,7 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux'; import { bindActionCreators, compose } from 'redux';
import { get, isEmpty, sortBy } from 'lodash'; import { get, isEmpty } from 'lodash';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom'; import { Header } from '@buffetjs/custom';
@ -17,12 +17,7 @@ import {
} from 'strapi-helper-plugin'; } from 'strapi-helper-plugin';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
import pluginPermissions from '../../permissions'; import pluginPermissions from '../../permissions';
import { import { generatePermissionsObject, getRequestUrl, getTrad } from '../../utils';
checkIfAttributeIsDisplayable,
generatePermissionsObject,
getRequestUrl,
getTrad,
} from '../../utils';
import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown'; import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown';
import Container from '../../components/Container'; import Container from '../../components/Container';
@ -30,9 +25,7 @@ import CustomTable from '../../components/CustomTable';
// import FilterPicker from '../../components/FilterPicker'; // import FilterPicker from '../../components/FilterPicker';
import Search from '../../components/Search'; import Search from '../../components/Search';
import State from '../../components/State';
import ListViewProvider from '../ListViewProvider'; import ListViewProvider from '../ListViewProvider';
// import { onChangeListLabels, resetListLabels } from '../Main/actions';
import { AddFilterCta, FilterIcon, Wrapper } from './components'; import { AddFilterCta, FilterIcon, Wrapper } from './components';
import Filter from './Filter'; import Filter from './Filter';
import Footer from './Footer'; import Footer from './Footer';
@ -48,35 +41,36 @@ import {
setModalLoadingState, setModalLoadingState,
toggleModalDelete, toggleModalDelete,
toggleModalDeleteAll, toggleModalDeleteAll,
//
setLayout,
onChangeListHeaders,
onResetListHeaders,
} from './actions'; } from './actions';
import makeSelectListView from './selectors'; import makeSelectListView from './selectors';
import { getAllAllowedHeaders, getFirstSortableHeader } from './utils';
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
const FilterPicker = () => <div>FILTER</div>; const FilterPicker = () => <div>FILTER</div>;
const onChangeListLabels = () => console.log('todo');
const resetListLabels = () => console.log('todo');
function ListView({ function ListView({
count, count,
data, data,
didDeleteData, didDeleteData,
// emitEvent,
entriesToDelete, entriesToDelete,
isLoading, isLoading,
// location: { pathname }, // location: { pathname },
getData, getData,
getDataSucceeded, getDataSucceeded,
layouts,
// history: { push },
onChangeBulk, onChangeBulk,
onChangeBulkSelectall, onChangeBulkSelectall,
onChangeListLabels,
onDeleteDataError, onDeleteDataError,
onDeleteDataSucceeded, onDeleteDataSucceeded,
onDeleteSeveralDataSucceeded, onDeleteSeveralDataSucceeded,
resetListLabels,
resetProps, resetProps,
setModalLoadingState, setModalLoadingState,
showWarningDelete, showWarningDelete,
@ -87,7 +81,12 @@ function ListView({
toggleModalDeleteAll, toggleModalDeleteAll,
// NEW // NEW
// allAllowedHeaders,
displayedHeaders,
layout, layout,
onChangeListHeaders,
onResetListHeaders,
setLayout,
}) { }) {
const { emitEvent } = useGlobalContext(); const { emitEvent } = useGlobalContext();
const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]); const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]);
@ -101,14 +100,15 @@ function ListView({
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [isLabelPickerOpen, setLabelPickerState] = useState(false);
const [isFilterPickerOpen, setFilterPickerState] = useState(false); const [isFilterPickerOpen, setFilterPickerState] = useState(false);
const [idToDelete, setIdToDelete] = useState(null); const [idToDelete, setIdToDelete] = useState(null);
console.log({ layout });
const contentType = layout.contentType; const contentType = layout.contentType;
const { const {
contentType: {
attributes,
settings: {
defaultSortBy, defaultSortBy,
defaultSortOrder, defaultSortOrder,
bulkable: isBulkable, bulkable: isBulkable,
@ -116,21 +116,13 @@ function ListView({
searchable: isSearchable, searchable: isSearchable,
pageSize, pageSize,
// mainField, // mainField,
} = contentType.settings; },
},
} = layout;
const hasDraftAndPublish = contentType.options.draftAndPublish; const hasDraftAndPublish = contentType.options.draftAndPublish;
const defaultSort = `${defaultSortBy}:${defaultSortOrder}`; const defaultSort = `${defaultSortBy}:${defaultSortOrder}`;
const listLayout = contentType.layouts.list; const allAllowedHeaders = getAllAllowedHeaders(attributes);
console.log({ isBulkable });
const contentTypePath = useMemo(() => {
return [slug, 'contentType'];
}, [slug]);
// Related to the search
// const defaultSort = useMemo(() => {
// return `${getLayoutSetting('defaultSortBy')}:${getLayoutSetting('defaultSortOrder')}`;
// }, [getLayoutSetting]);
const filters = useMemo(() => { const filters = useMemo(() => {
const currentSearch = new URLSearchParams(search); const currentSearch = new URLSearchParams(search);
@ -157,6 +149,7 @@ function ListView({
// TODO // TODO
const _sort = query.get('_sort') || defaultSort; const _sort = query.get('_sort') || defaultSort;
const label = contentType.info.label;
const _start = useMemo(() => { const _start = useMemo(() => {
return (_page - 1) * parseInt(_limit, 10); return (_page - 1) * parseInt(_limit, 10);
@ -182,94 +175,70 @@ function ListView({
const fetchData = async (search = searchToSendForRequest) => { const fetchData = async (search = searchToSendForRequest) => {
try { try {
getDataActionRef.current(); // getDataActionRef.current();
const [{ count }, data] = await Promise.all([ // const [{ count }, data] = await Promise.all([
request(getRequestUrl(`explorer/${slug}/count?${search}`), { // request(getRequestUrl(`explorer/${slug}/count?${search}`), {
method: 'GET', // method: 'GET',
}), // }),
request(getRequestUrl(`explorer/${slug}?${search}`), { // request(getRequestUrl(`explorer/${slug}?${search}`), {
method: 'GET', // method: 'GET',
}), // }),
]); // ]);
getDataSucceededRef.current(count, data); // const c = await request(getRequestUrl(`collection-types/${slug}?${search}`));
// console.log({ c });
const data = [
{
id: 16,
postal_coder: 'kkkk',
city: 'kljkojihv',
created_by: {
id: 1,
firstname: 'cyril',
lastname: 'lopez',
username: null,
email: 'cyril@strapi.io',
resetPasswordToken: null,
registrationToken: null,
isActive: true,
blocked: null,
},
updated_by: {
id: 1,
firstname: 'cyril',
lastname: 'lopez',
username: null,
email: 'cyril@strapi.io',
resetPasswordToken: null,
registrationToken: null,
isActive: true,
blocked: null,
},
created_at: '2020-10-28T09:03:20.905Z',
updated_at: '2020-10-28T13:51:35.381Z',
published_at: '2020-10-28T13:51:35.351Z',
cover: null,
images: [],
categories: [],
likes: [],
},
];
getDataSucceededRef.current(1, data);
} catch (err) { } catch (err) {
strapi.notification.error(`${pluginId}.error.model.fetch`); strapi.notification.error(`${pluginId}.error.model.fetch`);
} }
}; };
const getMetaDatas = useCallback( useEffect(() => {
(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]);
// TODO // TODO
const tableHeaders = useMemo(() => { console.log('up');
return listLayout; setLayout(layout);
// let headers = listLayout.map(label => { }, [layout, setLayout]);
// return { ...getMetaDatas([label, 'list']), name: label };
// });
// if (hasDraftAndPublish) { const firstSortableHeader = useMemo(() => getFirstSortableHeader(displayedHeaders), [
// headers.push({ displayedHeaders,
// 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 <State isPublished={isPublished} />;
// },
// });
// }
// 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(() => { useEffect(() => {
return () => { return () => {
@ -358,36 +327,32 @@ function ListView({
} }
}, [entriesToDelete, onDeleteSeveralDataSucceeded, slug, setModalLoadingState]); }, [entriesToDelete, onDeleteSeveralDataSucceeded, slug, setModalLoadingState]);
const handleChangeListLabels = ({ name, value }) => { const handleChangeListLabels = useCallback(
const currentSort = _sort; ({ name, value }) => {
// const currentSort = _sort;
// Display a notification if trying to remove the last displayed field // // Display a notification if trying to remove the last displayed field
if (value && listLayout.length === 1) { if (value && displayedHeaders.length === 1) {
strapi.notification.error('content-manager.notification.error.displayedFields'); strapi.notification.error('content-manager.notification.error.displayedFields');
return; return false;
} }
// Update the sort when removing the displayed one // TODO
if (currentSort.split(':')[0] === name && value) { // // Update the sort when removing the displayed one
emitEvent('didChangeDisplayedFields'); // if (currentSort.split(':')[0] === name && value) {
handleChangeSearch({ // emitEvent('didChangeDisplayedFields');
target: { // handleChangeSearch({
name: '_sort', // target: {
value: `${getFirstSortableElement(name)}:ASC`, // name: '_sort',
}, // value: `${firstSortableHeader}:ASC`,
}); // },
} // });
// }
// Update the Main reducer onChangeListHeaders({ name, value });
onChangeListLabels({
target: {
name,
slug,
value: !value,
}, },
}); [displayedHeaders, onChangeListHeaders]
}; );
const handleChangeFilters = ({ target: { value } }) => { const handleChangeFilters = ({ target: { value } }) => {
const newSearch = new URLSearchParams(); const newSearch = new URLSearchParams();
@ -449,14 +414,6 @@ function ListView({
setFilterPickerState(prevState => !prevState); setFilterPickerState(prevState => !prevState);
}; };
const toggleLabelPickerState = () => {
if (!isLabelPickerOpen) {
emitEvent('willChangeListFieldsSettings');
}
setLabelPickerState(prevState => !prevState);
};
const filterPickerActions = [ const filterPickerActions = [
{ {
label: `${pluginId}.components.FiltersPickWrapper.PluginHeader.actions.clearAll`, label: `${pluginId}.components.FiltersPickWrapper.PluginHeader.actions.clearAll`,
@ -533,7 +490,6 @@ function ListView({
actions: headerAction, actions: headerAction,
}; };
/* eslint-enable indent */ /* eslint-enable indent */
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count, headerAction, label, canRead, formatMessage]); }, [count, headerAction, label, canRead, formatMessage]);
return ( return (
@ -543,13 +499,13 @@ function ListView({
count={count} count={count}
entriesToDelete={entriesToDelete} entriesToDelete={entriesToDelete}
emitEvent={emitEvent} emitEvent={emitEvent}
firstSortableElement={getFirstSortableElement()}
label={label} label={label}
onChangeBulk={onChangeBulk} onChangeBulk={onChangeBulk}
onChangeBulkSelectall={onChangeBulkSelectall} onChangeBulkSelectall={onChangeBulkSelectall}
onChangeSearch={handleChangeSearch} onChangeSearch={handleChangeSearch}
onClickDelete={handleClickDelete} onClickDelete={handleClickDelete}
schema={listSchema} // schema={listSchema}
schema={{}}
slug={slug} slug={slug}
toggleModalDeleteAll={toggleModalDeleteAll} toggleModalDeleteAll={toggleModalDeleteAll}
_limit={_limit} _limit={_limit}
@ -557,6 +513,8 @@ function ListView({
filters={filters} filters={filters}
_q={_q} _q={_q}
_sort={_sort} _sort={_sort}
// to keep
firstSortableHeader={firstSortableHeader}
> >
<FilterPicker <FilterPicker
actions={filterPickerActions} actions={filterPickerActions}
@ -587,7 +545,7 @@ function ListView({
changeParams={handleChangeFilters} changeParams={handleChangeFilters}
filters={filters} filters={filters}
index={key} index={key}
schema={listSchema} schema={{}}
key={key} key={key}
toggleFilterPickerState={toggleFilterPickerState} toggleFilterPickerState={toggleFilterPickerState}
isFilterPickerOpen={isFilterPickerOpen} isFilterPickerOpen={isFilterPickerOpen}
@ -600,14 +558,12 @@ function ListView({
<div className="col-2"> <div className="col-2">
<CheckPermissions permissions={pluginPermissions.collectionTypesConfigurations}> <CheckPermissions permissions={pluginPermissions.collectionTypesConfigurations}>
<DisplayedFieldsDropdown <DisplayedFieldsDropdown
isOpen={isLabelPickerOpen} displayedHeaders={displayedHeaders}
items={allLabels} items={allAllowedHeaders}
// items={allAllowedHeaders}
onChange={handleChangeListLabels} onChange={handleChangeListLabels}
onClickReset={() => { onClickReset={onResetListHeaders}
resetListLabels(slug);
}}
slug={slug} slug={slug}
toggle={toggleLabelPickerState}
/> />
</CheckPermissions> </CheckPermissions>
</div> </div>
@ -618,7 +574,8 @@ function ListView({
data={data} data={data}
canDelete={canDelete} canDelete={canDelete}
canUpdate={canUpdate} canUpdate={canUpdate}
headers={tableHeaders} displayedHeaders={displayedHeaders}
hasDraftAndPublish={hasDraftAndPublish}
isBulkable={isBulkable} isBulkable={isBulkable}
onChangeParams={handleChangeSearch} onChangeParams={handleChangeSearch}
showLoader={isLoading} showLoader={isLoading}
@ -664,9 +621,16 @@ ListView.defaultProps = {
}; };
ListView.propTypes = { ListView.propTypes = {
// allAllowedHeaders: PropTypes.array.isRequired,
displayedHeaders: PropTypes.array.isRequired,
layout: PropTypes.exact({ layout: PropTypes.exact({
components: PropTypes.object.isRequired, components: PropTypes.object.isRequired,
contentType: PropTypes.shape({ 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, options: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
}).isRequired, }).isRequired,
@ -690,11 +654,11 @@ ListView.propTypes = {
// }).isRequired, // }).isRequired,
// onChangeBulk: PropTypes.func.isRequired, // onChangeBulk: PropTypes.func.isRequired,
// onChangeBulkSelectall: PropTypes.func.isRequired, // onChangeBulkSelectall: PropTypes.func.isRequired,
// onChangeListLabels: PropTypes.func.isRequired, onChangeListHeaders: PropTypes.func.isRequired,
// onDeleteDataError: PropTypes.func.isRequired, // onDeleteDataError: PropTypes.func.isRequired,
// onDeleteDataSucceeded: PropTypes.func.isRequired, // onDeleteDataSucceeded: PropTypes.func.isRequired,
// onDeleteSeveralDataSucceeded: PropTypes.func.isRequired, // onDeleteSeveralDataSucceeded: PropTypes.func.isRequired,
// resetListLabels: PropTypes.func.isRequired, onResetListHeaders: PropTypes.func.isRequired,
// resetProps: PropTypes.func.isRequired, // resetProps: PropTypes.func.isRequired,
// setModalLoadingState: PropTypes.func.isRequired, // setModalLoadingState: PropTypes.func.isRequired,
// showModalConfirmButtonLoading: PropTypes.bool.isRequired, // showModalConfirmButtonLoading: PropTypes.bool.isRequired,
@ -703,6 +667,7 @@ ListView.propTypes = {
// slug: PropTypes.string.isRequired, // slug: PropTypes.string.isRequired,
// toggleModalDelete: PropTypes.func.isRequired, // toggleModalDelete: PropTypes.func.isRequired,
// toggleModalDeleteAll: PropTypes.func.isRequired, // toggleModalDeleteAll: PropTypes.func.isRequired,
setLayout: PropTypes.func.isRequired,
}; };
const mapStateToProps = makeSelectListView(); const mapStateToProps = makeSelectListView();
@ -714,15 +679,16 @@ export function mapDispatchToProps(dispatch) {
getDataSucceeded, getDataSucceeded,
onChangeBulk, onChangeBulk,
onChangeBulkSelectall, onChangeBulkSelectall,
onChangeListLabels, onChangeListHeaders,
onDeleteDataError, onDeleteDataError,
onDeleteDataSucceeded, onDeleteDataSucceeded,
onDeleteSeveralDataSucceeded, onDeleteSeveralDataSucceeded,
resetListLabels, onResetListHeaders,
resetProps, resetProps,
setModalLoadingState, setModalLoadingState,
toggleModalDelete, toggleModalDelete,
toggleModalDeleteAll, toggleModalDeleteAll,
setLayout,
}, },
dispatch dispatch
); );

View File

@ -15,6 +15,11 @@ import {
ON_DELETE_SEVERAL_DATA_SUCCEEDED, ON_DELETE_SEVERAL_DATA_SUCCEEDED,
TOGGLE_MODAL_DELETE, TOGGLE_MODAL_DELETE,
TOGGLE_MODAL_DELETE_ALL, TOGGLE_MODAL_DELETE_ALL,
//
ON_CHANGE_LIST_HEADERS,
ON_RESET_LIST_HEADERS,
SET_LIST_LAYOUT,
//
SET_MODAL_LOADING_STATE, SET_MODAL_LOADING_STATE,
} from './constants'; } from './constants';
@ -27,6 +32,11 @@ export const initialState = {
showModalConfirmButtonLoading: false, showModalConfirmButtonLoading: false,
showWarningDelete: false, showWarningDelete: false,
showWarningDeleteAll: false, showWarningDeleteAll: false,
//
contentType: {},
initialDisplayedHeaders: [],
displayedHeaders: [],
}; };
const listViewReducer = (state = initialState, action) => const listViewReducer = (state = initialState, action) =>
@ -66,6 +76,27 @@ const listViewReducer = (state = initialState, action) =>
break; break;
} }
case ON_CHANGE_LIST_HEADERS: {
const {
target: { name, value },
} = action;
if (!value) {
const { metadatas, attributes } = state.contentType;
drafState.displayedHeaders.push({
name,
fieldSchema: attributes[name],
metadatas: metadatas[name].list,
key: `__${name}_key__`,
});
} else {
drafState.displayedHeaders = state.displayedHeaders.filter(
header => header.name !== name
);
}
break;
}
case ON_DELETE_DATA_SUCCEEDED: { case ON_DELETE_DATA_SUCCEEDED: {
drafState.didDeleteData = true; drafState.didDeleteData = true;
drafState.showWarningDelete = false; drafState.showWarningDelete = false;
@ -81,6 +112,10 @@ const listViewReducer = (state = initialState, action) =>
drafState.showWarningDeleteAll = false; drafState.showWarningDeleteAll = false;
break; break;
} }
case ON_RESET_LIST_HEADERS: {
drafState.displayedHeaders = state.initialDisplayedHeaders;
break;
}
case RESET_PROPS: { case RESET_PROPS: {
return initialState; return initialState;
} }
@ -113,6 +148,15 @@ const listViewReducer = (state = initialState, action) =>
break; break;
} }
case SET_LIST_LAYOUT: {
const { contentType } = action.layout;
drafState.contentType = contentType;
drafState.displayedHeaders = contentType.layouts.list;
drafState.initialDisplayedHeaders = contentType.layouts.list;
break;
}
default: default:
return drafState; return drafState;

View File

@ -0,0 +1,18 @@
import { sortBy } from 'lodash';
import { checkIfAttributeIsDisplayable } from '../../../utils';
const getAllAllowedHeaders = attributes => {
const allowedAttributes = Object.keys(attributes).reduce((acc, current) => {
const attribute = attributes[current];
if (checkIfAttributeIsDisplayable(attribute)) {
acc.push(current);
}
return acc;
}, []);
return sortBy(allowedAttributes, ['name']);
};
export default getAllAllowedHeaders;

View File

@ -0,0 +1,9 @@
import { get } from 'lodash';
const getFirstSortableHeader = headers => {
const matched = headers.find(header => header.metadatas.sortable === true);
return get(matched, 'name', 'id');
};
export default getFirstSortableHeader;

View File

@ -0,0 +1,2 @@
export { default as getAllAllowedHeaders } from './getAllAllowedHeaders';
export { default as getFirstSortableHeader } from './getFirstSortableHeader';

View File

@ -0,0 +1,26 @@
import getFirstSortableHeader from '../getFirstSortableHeader';
describe('CONTENT MANAGER | containers | ListView | utils | getFirstSortableHeader', () => {
it('should return id if the array is empty', () => {
expect(getFirstSortableHeader([])).toEqual('id');
});
it('should return the first sortable element', () => {
const headers = [
{
name: 'un',
metadatas: { sortable: false },
},
{
name: 'two',
metadatas: { sortable: true },
},
{
name: 'three',
metadatas: { sortable: true },
},
];
expect(getFirstSortableHeader(headers)).toBe('two');
});
});

View File

@ -1,66 +1,4 @@
import { import { GET_DATA, GET_DATA_SUCCEEDED, RESET_PROPS } from './constants';
// DELETE_LAYOUT,
// DELETE_LAYOUTS,
GET_DATA,
GET_DATA_SUCCEEDED,
// GET_LAYOUT_SUCCEEDED,
// ON_CHANGE_LIST_LABELS,
// RESET_LIST_LABELS,
RESET_PROPS,
} from './constants';
// export function deleteLayout(uid) {
// return {
// type: DELETE_LAYOUT,
// uid,
// };
// }
// export function deleteLayouts() {
// return {
// type: DELETE_LAYOUTS,
// };
// }
// export function getDataSucceeded(components, models, mainFields) {
// return {
// type: GET_DATA_SUCCEEDED,
// components,
// models: models.filter(model => model.isDisplayed === true),
// mainFields,
// };
// }
// export function getLayoutSucceeded(layout, uid) {
// return {
// type: GET_LAYOUT_SUCCEEDED,
// layout,
// uid,
// };
// }
// export function onChangeListLabels({ target: { name, slug, value } }) {
// return {
// type: ON_CHANGE_LIST_LABELS,
// name,
// slug,
// value,
// };
// }
// export function resetListLabels(slug) {
// return {
// type: RESET_LIST_LABELS,
// slug,
// };
// }
// export function resetProps() {
// return {
// type: RESET_PROPS,
// };
// }
//
export const getData = () => ({ export const getData = () => ({
type: GET_DATA, type: GET_DATA,

View File

@ -1,9 +1,3 @@
export const GET_DATA = 'ContentManager/Main/GET_DATA'; export const GET_DATA = 'ContentManager/Main/GET_DATA';
export const GET_DATA_SUCCEEDED = 'ContentManager/Main/GET_DATA_SUCCEEDED'; export const GET_DATA_SUCCEEDED = 'ContentManager/Main/GET_DATA_SUCCEEDED';
export const RESET_PROPS = 'ContentManager/Main/RESET_PROPS'; export const RESET_PROPS = 'ContentManager/Main/RESET_PROPS';
// export const DELETE_LAYOUT = 'ContentManager/Main/DELETE_LAYOUT';
// export const DELETE_LAYOUTS = 'ContentManager/Main/DELETE_LAYOUTS';
// export const GET_LAYOUT_SUCCEEDED = 'ContentManager/Main/GET_LAYOUT_SUCCEEDED';
// export const ON_CHANGE_LIST_LABELS = 'ContentManager/Main/ON_CHANGE_LIST_LABELS';
// export const RESET_LIST_LABELS = 'ContentManager/Main/RESET_LIST_LABELS';

View File

@ -4,17 +4,7 @@
*/ */
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
import produce from 'immer'; import produce from 'immer';
import { import { GET_DATA, GET_DATA_SUCCEEDED, RESET_PROPS } from './constants';
// DELETE_LAYOUT,
// DELETE_LAYOUTS,
GET_DATA,
GET_DATA_SUCCEEDED,
// GET_LAYOUT_SUCCEEDED,
// ON_CHANGE_LIST_LABELS,
// RESET_LIST_LABELS,
RESET_PROPS,
} from './constants';
// import { fromJS } from 'immutable';
const initialState = { const initialState = {
// TODO! // TODO!
@ -47,52 +37,5 @@ const mainReducer = (state = initialState, action) =>
} }
}); });
// export const initialState = fromJS({
// componentsAndModelsMainPossibleMainFields: {},
// components: [],
// initialLayouts: {},
// isLoading: true,
// layouts: {},
// models: [],
// });
// function mainReducer(state = initialState, action) {
// switch (action.type) {
// case DELETE_LAYOUT:
// return state.removeIn(['layouts', action.uid]);
// case DELETE_LAYOUTS:
// return state.update('layouts', () => fromJS({}));
// case GET_DATA_SUCCEEDED:
// return state
// .update('components', () => fromJS(action.components))
// .update('models', () => fromJS(action.models))
// .update('componentsAndModelsMainPossibleMainFields', () => fromJS(action.mainFields))
// .update('isLoading', () => false);
// case GET_LAYOUT_SUCCEEDED:
// return state
// .updateIn(['layouts', action.uid], () => fromJS(action.layout))
// .updateIn(['initialLayouts', action.uid], () => fromJS(action.layout));
// case ON_CHANGE_LIST_LABELS: {
// const { name, slug, value } = action;
// return state.updateIn(['layouts', slug, 'contentType', 'layouts', 'list'], list => {
// if (value) {
// return list.push(name);
// }
// return list.filter(l => l !== name);
// });
// }
// case RESET_LIST_LABELS:
// return state.updateIn(['layouts', action.slug], () =>
// state.getIn(['initialLayouts', action.slug])
// );
// case RESET_PROPS:
// return initialState;
// default:
// return state;
// }
// }
export default mainReducer; export default mainReducer;
export { initialState }; export { initialState };

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useReducer } from 'react'; import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { request } from 'strapi-helper-plugin'; import { request } from 'strapi-helper-plugin';
import formatLayouts from './utils/formatLayouts'; import formatLayouts from './utils/formatLayouts';
@ -9,6 +9,7 @@ const useFetchContentTypeLayout = contentTypeUID => {
const [{ error, isLoading, layout, layouts }, dispatch] = useReducer(reducer, initialState); const [{ error, isLoading, layout, layouts }, dispatch] = useReducer(reducer, initialState);
const schemasSelector = useMemo(makeSelectModelAndComponentSchemas, []); const schemasSelector = useMemo(makeSelectModelAndComponentSchemas, []);
const { schemas } = useSelector(state => schemasSelector(state), []); const { schemas } = useSelector(state => schemasSelector(state), []);
const isMounted = useRef(true);
const getData = useCallback( const getData = useCallback(
async (uid, abortSignal = false) => { async (uid, abortSignal = false) => {
@ -33,13 +34,20 @@ const useFetchContentTypeLayout = contentTypeUID => {
data: formatLayouts(data, schemas), data: formatLayouts(data, schemas),
}); });
} catch (error) { } catch (error) {
console.error(error); if (isMounted.current && error.name !== 'AbortError') {
dispatch({ type: 'GET_DATA_ERROR', error }); dispatch({ type: 'GET_DATA_ERROR', error });
} }
}
}, },
[schemas, layouts] [schemas, layouts]
); );
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => { useEffect(() => {
const abortController = new AbortController(); const abortController = new AbortController();
const { signal } = abortController; const { signal } = abortController;

View File

@ -20,9 +20,9 @@ const reducer = (state, action) =>
case 'GET_DATA_SUCCEEDED': { case 'GET_DATA_SUCCEEDED': {
const contentTypeUid = action.data.contentType.uid; const contentTypeUid = action.data.contentType.uid;
draftState.isLoading = false;
draftState.layout = action.data; draftState.layout = action.data;
draftState.layouts[contentTypeUid] = action.data; draftState.layouts[contentTypeUid] = action.data;
draftState.isLoading = false;
break; break;
} }
case 'GET_DATA_ERROR': { case 'GET_DATA_ERROR': {

View File

@ -94,7 +94,7 @@ const formatListLayoutWithMetas = obj => {
const fieldSchema = get(obj, ['attributes', current], {}); const fieldSchema = get(obj, ['attributes', current], {});
const metadatas = get(obj, ['metadatas', current, 'list'], {}); const metadatas = get(obj, ['metadatas', current, 'list'], {});
acc.push({ name: current, fieldSchema, metadatas }); acc.push({ key: `__${current}_key__`, name: current, fieldSchema, metadatas });
return acc; return acc;
}, []); }, []);