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 }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
port: env.int('PORT', 3000),
admin: {
auth: {
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 { get, isEmpty, isNull, isObject, toLower, toString } from 'lodash';
import moment from 'moment';
@ -67,26 +67,17 @@ const getDisplayedValue = (type, value, name) => {
};
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(
name => {
const type = get(schema, ['attributes', name, 'type'], 'string');
(name, type) => {
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 = [
{
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,
onClick: e => {
e.stopPropagation();
emitEvent('willDeleteEntryFromList');
emitEventRef.current('willDeleteEntryFromList');
onClickDelete(row.id);
},
},
@ -113,14 +104,16 @@ function Row({ canDelete, canUpdate, isBulkable, row, headers }) {
/>
</td>
)}
{headers.map(header => {
{headers.map(({ key, name, fieldSchema: { type }, cellFormatter }) => {
const isMedia = type === 'media';
return (
<td key={header.key || header.name}>
{isMedia(header) && <MediaPreviewList files={memoizedDisplayedValue(header.name)} />}
{header.cellFormatter && header.cellFormatter(row)}
{!isMedia(header) && !header.cellFormatter && (
<td key={key}>
{isMedia && <MediaPreviewList files={memoizedDisplayedValue(name, type)} />}
{cellFormatter && cellFormatter(row)}
{!isMedia && !cellFormatter && (
<Truncate>
<Truncated>{memoizedDisplayedValue(header.name)}</Truncated>
<Truncated>{memoizedDisplayedValue(name, type)}</Truncated>
</Truncate>
)}
</td>

View File

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

View File

@ -1,19 +1,55 @@
import React, { memo } from 'react';
import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useLocation, useHistory } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { upperFirst } from 'lodash';
import { LoadingIndicator } from 'strapi-helper-plugin';
import { FormattedMessage, useIntl } from 'react-intl';
import { upperFirst, isEmpty } from 'lodash';
import { LoadingIndicator, useGlobalContext } from 'strapi-helper-plugin';
import useListView from '../../hooks/useListView';
import { getTrad } from '../../utils';
import State from '../State';
import TableHeader from './TableHeader';
import { LoadingContainer, LoadingWrapper, Table, TableEmpty, TableRow } from './styledComponents';
import ActionCollapse from './ActionCollapse';
import Row from './Row';
const CustomTable = ({ canUpdate, canDelete, data, headers, isBulkable, showLoader }) => {
const { emitEvent, entriesToDelete, label, filters, _q } = useListView();
const CustomTable = ({
canUpdate,
canDelete,
data,
displayedHeaders,
hasDraftAndPublish,
isBulkable,
showLoader,
}) => {
const { formatMessage } = useIntl();
const { entriesToDelete, label, filters, _q } = useListView();
const { emitEvent } = useGlobalContext();
const { pathname } = useLocation();
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;
@ -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 = {
canDelete: PropTypes.bool,
canUpdate: PropTypes.bool,
data: PropTypes.array,
headers: PropTypes.array,
isBulkable: PropTypes.bool,
showLoader: PropTypes.bool,
canDelete: PropTypes.bool.isRequired,
canUpdate: PropTypes.bool.isRequired,
data: PropTypes.array.isRequired,
displayedHeaders: PropTypes.array.isRequired,
hasDraftAndPublish: PropTypes.bool.isRequired,
isBulkable: PropTypes.bool.isRequired,
showLoader: PropTypes.bool.isRequired,
};
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 { ButtonDropdown } from 'reactstrap';
import { FormattedMessage } from 'react-intl';
@ -13,15 +13,23 @@ import LayoutWrapper from './LayoutWrapper';
import MenuDropdown from './MenuDropdown';
import Toggle from './Toggle';
const DisplayedFieldsDropdown = ({
isOpen,
items,
onChange,
onClickReset,
slug,
toggle,
}) => {
const DisplayedFieldsDropdown = ({ displayedHeaders, items, onChange, onClickReset, slug }) => {
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 (
<DropdownWrapper>
@ -38,9 +46,7 @@ const DisplayedFieldsDropdown = ({
<FormattedMessage id="app.links.configure-view" />
</LayoutWrapper>
</DropdownItemLink>
<FormattedMessage
id={`${pluginId}.containers.ListPage.displayedFields`}
>
<FormattedMessage id={`${pluginId}.containers.ListPage.displayedFields`}>
{msg => (
<ItemDropdownReset onClick={onClickReset}>
<div
@ -55,21 +61,18 @@ const DisplayedFieldsDropdown = ({
</ItemDropdownReset>
)}
</FormattedMessage>
{items.map(item => (
<ItemDropdown
key={item.name}
toggle={false}
onClick={() => onChange(item)}
>
{items.map(headerName => {
const value = displayedHeaders.findIndex(({ name }) => name === headerName) !== -1;
const handleChange = () => onChange({ name: headerName, value });
return (
<ItemDropdown key={headerName} toggle={false} onClick={handleChange}>
<div>
<InputCheckbox
onChange={() => onChange(item)}
name={item.name}
value={item.value}
/>
<InputCheckbox onChange={handleChange} name={headerName} value={value} />
</div>
</ItemDropdown>
))}
);
})}
</MenuDropdown>
</ButtonDropdown>
</DropdownWrapper>
@ -77,12 +80,11 @@ const DisplayedFieldsDropdown = ({
};
DisplayedFieldsDropdown.propTypes = {
isOpen: PropTypes.bool.isRequired,
displayedHeaders: PropTypes.array.isRequired,
items: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onClickReset: PropTypes.func.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,
ON_CHANGE_BULK,
ON_CHANGE_BULK_SELECT_ALL,
//
ON_CHANGE_LIST_HEADERS,
ON_RESET_LIST_HEADERS,
//
ON_DELETE_DATA_ERROR,
ON_DELETE_DATA_SUCCEEDED,
ON_DELETE_SEVERAL_DATA_SUCCEEDED,
RESET_PROPS,
//
SET_LIST_LAYOUT,
//
SET_MODAL_LOADING_STATE,
TOGGLE_MODAL_DELETE,
TOGGLE_MODAL_DELETE_ALL,
@ -57,6 +64,8 @@ export function onDeleteSeveralDataSucceeded() {
};
}
export const onResetListHeaders = () => ({ type: ON_RESET_LIST_HEADERS });
export function resetProps() {
return { type: RESET_PROPS };
}
@ -78,3 +87,7 @@ export function toggleModalDelete() {
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 = 'ContentManager/ListView/TOGGLE_MODAL_DELETE';
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 { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { get, isEmpty, sortBy } from 'lodash';
import { get, isEmpty } from 'lodash';
import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
@ -17,12 +17,7 @@ import {
} from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import pluginPermissions from '../../permissions';
import {
checkIfAttributeIsDisplayable,
generatePermissionsObject,
getRequestUrl,
getTrad,
} from '../../utils';
import { generatePermissionsObject, getRequestUrl, getTrad } from '../../utils';
import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown';
import Container from '../../components/Container';
@ -30,9 +25,7 @@ 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';
@ -48,35 +41,36 @@ import {
setModalLoadingState,
toggleModalDelete,
toggleModalDeleteAll,
//
setLayout,
onChangeListHeaders,
onResetListHeaders,
} from './actions';
import makeSelectListView from './selectors';
import { getAllAllowedHeaders, getFirstSortableHeader } from './utils';
/* eslint-disable react/no-array-index-key */
const FilterPicker = () => <div>FILTER</div>;
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,
@ -87,7 +81,12 @@ function ListView({
toggleModalDeleteAll,
// NEW
// allAllowedHeaders,
displayedHeaders,
layout,
onChangeListHeaders,
onResetListHeaders,
setLayout,
}) {
const { emitEvent } = useGlobalContext();
const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]);
@ -101,14 +100,15 @@ function ListView({
const isFirstRender = useRef(true);
const { formatMessage } = useIntl();
const [isLabelPickerOpen, setLabelPickerState] = useState(false);
const [isFilterPickerOpen, setFilterPickerState] = useState(false);
const [idToDelete, setIdToDelete] = useState(null);
console.log({ layout });
const contentType = layout.contentType;
const {
contentType: {
attributes,
settings: {
defaultSortBy,
defaultSortOrder,
bulkable: isBulkable,
@ -116,21 +116,13 @@ function ListView({
searchable: isSearchable,
pageSize,
// mainField,
} = contentType.settings;
},
},
} = layout;
const hasDraftAndPublish = contentType.options.draftAndPublish;
const defaultSort = `${defaultSortBy}:${defaultSortOrder}`;
const listLayout = contentType.layouts.list;
console.log({ isBulkable });
const contentTypePath = useMemo(() => {
return [slug, 'contentType'];
}, [slug]);
// Related to the search
// const defaultSort = useMemo(() => {
// return `${getLayoutSetting('defaultSortBy')}:${getLayoutSetting('defaultSortOrder')}`;
// }, [getLayoutSetting]);
const allAllowedHeaders = getAllAllowedHeaders(attributes);
const filters = useMemo(() => {
const currentSearch = new URLSearchParams(search);
@ -157,6 +149,7 @@ function ListView({
// TODO
const _sort = query.get('_sort') || defaultSort;
const label = contentType.info.label;
const _start = useMemo(() => {
return (_page - 1) * parseInt(_limit, 10);
@ -182,94 +175,70 @@ function ListView({
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',
}),
]);
// 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);
// 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) {
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]);
useEffect(() => {
// TODO
const tableHeaders = useMemo(() => {
return listLayout;
// let headers = listLayout.map(label => {
// return { ...getMetaDatas([label, 'list']), name: label };
// });
console.log('up');
setLayout(layout);
}, [layout, setLayout]);
// 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 <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]);
const firstSortableHeader = useMemo(() => getFirstSortableHeader(displayedHeaders), [
displayedHeaders,
]);
useEffect(() => {
return () => {
@ -358,36 +327,32 @@ function ListView({
}
}, [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) {
const handleChangeListLabels = useCallback(
({ name, value }) => {
// const currentSort = _sort;
// // Display a notification if trying to remove the last displayed field
if (value && displayedHeaders.length === 1) {
strapi.notification.error('content-manager.notification.error.displayedFields');
return;
return false;
}
// Update the sort when removing the displayed one
if (currentSort.split(':')[0] === name && value) {
emitEvent('didChangeDisplayedFields');
handleChangeSearch({
target: {
name: '_sort',
value: `${getFirstSortableElement(name)}:ASC`,
},
});
}
// TODO
// // Update the sort when removing the displayed one
// if (currentSort.split(':')[0] === name && value) {
// emitEvent('didChangeDisplayedFields');
// handleChangeSearch({
// target: {
// name: '_sort',
// value: `${firstSortableHeader}:ASC`,
// },
// });
// }
// Update the Main reducer
onChangeListLabels({
target: {
name,
slug,
value: !value,
onChangeListHeaders({ name, value });
},
});
};
[displayedHeaders, onChangeListHeaders]
);
const handleChangeFilters = ({ target: { value } }) => {
const newSearch = new URLSearchParams();
@ -449,14 +414,6 @@ function ListView({
setFilterPickerState(prevState => !prevState);
};
const toggleLabelPickerState = () => {
if (!isLabelPickerOpen) {
emitEvent('willChangeListFieldsSettings');
}
setLabelPickerState(prevState => !prevState);
};
const filterPickerActions = [
{
label: `${pluginId}.components.FiltersPickWrapper.PluginHeader.actions.clearAll`,
@ -533,7 +490,6 @@ function ListView({
actions: headerAction,
};
/* eslint-enable indent */
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count, headerAction, label, canRead, formatMessage]);
return (
@ -543,13 +499,13 @@ function ListView({
count={count}
entriesToDelete={entriesToDelete}
emitEvent={emitEvent}
firstSortableElement={getFirstSortableElement()}
label={label}
onChangeBulk={onChangeBulk}
onChangeBulkSelectall={onChangeBulkSelectall}
onChangeSearch={handleChangeSearch}
onClickDelete={handleClickDelete}
schema={listSchema}
// schema={listSchema}
schema={{}}
slug={slug}
toggleModalDeleteAll={toggleModalDeleteAll}
_limit={_limit}
@ -557,6 +513,8 @@ function ListView({
filters={filters}
_q={_q}
_sort={_sort}
// to keep
firstSortableHeader={firstSortableHeader}
>
<FilterPicker
actions={filterPickerActions}
@ -587,7 +545,7 @@ function ListView({
changeParams={handleChangeFilters}
filters={filters}
index={key}
schema={listSchema}
schema={{}}
key={key}
toggleFilterPickerState={toggleFilterPickerState}
isFilterPickerOpen={isFilterPickerOpen}
@ -600,14 +558,12 @@ function ListView({
<div className="col-2">
<CheckPermissions permissions={pluginPermissions.collectionTypesConfigurations}>
<DisplayedFieldsDropdown
isOpen={isLabelPickerOpen}
items={allLabels}
displayedHeaders={displayedHeaders}
items={allAllowedHeaders}
// items={allAllowedHeaders}
onChange={handleChangeListLabels}
onClickReset={() => {
resetListLabels(slug);
}}
onClickReset={onResetListHeaders}
slug={slug}
toggle={toggleLabelPickerState}
/>
</CheckPermissions>
</div>
@ -618,7 +574,8 @@ function ListView({
data={data}
canDelete={canDelete}
canUpdate={canUpdate}
headers={tableHeaders}
displayedHeaders={displayedHeaders}
hasDraftAndPublish={hasDraftAndPublish}
isBulkable={isBulkable}
onChangeParams={handleChangeSearch}
showLoader={isLoading}
@ -664,9 +621,16 @@ ListView.defaultProps = {
};
ListView.propTypes = {
// allAllowedHeaders: PropTypes.array.isRequired,
displayedHeaders: 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,
@ -690,11 +654,11 @@ ListView.propTypes = {
// }).isRequired,
// onChangeBulk: PropTypes.func.isRequired,
// onChangeBulkSelectall: PropTypes.func.isRequired,
// onChangeListLabels: PropTypes.func.isRequired,
onChangeListHeaders: PropTypes.func.isRequired,
// onDeleteDataError: PropTypes.func.isRequired,
// onDeleteDataSucceeded: PropTypes.func.isRequired,
// onDeleteSeveralDataSucceeded: PropTypes.func.isRequired,
// resetListLabels: PropTypes.func.isRequired,
onResetListHeaders: PropTypes.func.isRequired,
// resetProps: PropTypes.func.isRequired,
// setModalLoadingState: PropTypes.func.isRequired,
// showModalConfirmButtonLoading: PropTypes.bool.isRequired,
@ -703,6 +667,7 @@ ListView.propTypes = {
// slug: PropTypes.string.isRequired,
// toggleModalDelete: PropTypes.func.isRequired,
// toggleModalDeleteAll: PropTypes.func.isRequired,
setLayout: PropTypes.func.isRequired,
};
const mapStateToProps = makeSelectListView();
@ -714,15 +679,16 @@ export function mapDispatchToProps(dispatch) {
getDataSucceeded,
onChangeBulk,
onChangeBulkSelectall,
onChangeListLabels,
onChangeListHeaders,
onDeleteDataError,
onDeleteDataSucceeded,
onDeleteSeveralDataSucceeded,
resetListLabels,
onResetListHeaders,
resetProps,
setModalLoadingState,
toggleModalDelete,
toggleModalDeleteAll,
setLayout,
},
dispatch
);

View File

@ -15,6 +15,11 @@ import {
ON_DELETE_SEVERAL_DATA_SUCCEEDED,
TOGGLE_MODAL_DELETE,
TOGGLE_MODAL_DELETE_ALL,
//
ON_CHANGE_LIST_HEADERS,
ON_RESET_LIST_HEADERS,
SET_LIST_LAYOUT,
//
SET_MODAL_LOADING_STATE,
} from './constants';
@ -27,6 +32,11 @@ export const initialState = {
showModalConfirmButtonLoading: false,
showWarningDelete: false,
showWarningDeleteAll: false,
//
contentType: {},
initialDisplayedHeaders: [],
displayedHeaders: [],
};
const listViewReducer = (state = initialState, action) =>
@ -66,6 +76,27 @@ const listViewReducer = (state = initialState, action) =>
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: {
drafState.didDeleteData = true;
drafState.showWarningDelete = false;
@ -81,6 +112,10 @@ const listViewReducer = (state = initialState, action) =>
drafState.showWarningDeleteAll = false;
break;
}
case ON_RESET_LIST_HEADERS: {
drafState.displayedHeaders = state.initialDisplayedHeaders;
break;
}
case RESET_PROPS: {
return initialState;
}
@ -113,6 +148,15 @@ const listViewReducer = (state = initialState, action) =>
break;
}
case SET_LIST_LAYOUT: {
const { contentType } = action.layout;
drafState.contentType = contentType;
drafState.displayedHeaders = contentType.layouts.list;
drafState.initialDisplayedHeaders = contentType.layouts.list;
break;
}
default:
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 {
// 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,
// };
// }
//
import { GET_DATA, GET_DATA_SUCCEEDED, RESET_PROPS } from './constants';
export const getData = () => ({
type: GET_DATA,

View File

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

View File

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

View File

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