diff --git a/examples/getstarted/config/server.js b/examples/getstarted/config/server.js index d1989c8ec7..acd681603b 100644 --- a/examples/getstarted/config/server.js +++ b/examples/getstarted/config/server.js @@ -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'), diff --git a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js index 2169b342c6..7517a80ee9 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js @@ -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 ? : null, @@ -95,7 +86,7 @@ function Row({ canDelete, canUpdate, isBulkable, row, headers }) { icon: canDelete ? : null, onClick: e => { e.stopPropagation(); - emitEvent('willDeleteEntryFromList'); + emitEventRef.current('willDeleteEntryFromList'); onClickDelete(row.id); }, }, @@ -113,14 +104,16 @@ function Row({ canDelete, canUpdate, isBulkable, row, headers }) { /> )} - {headers.map(header => { + {headers.map(({ key, name, fieldSchema: { type }, cellFormatter }) => { + const isMedia = type === 'media'; + return ( - - {isMedia(header) && } - {header.cellFormatter && header.cellFormatter(row)} - {!isMedia(header) && !header.cellFormatter && ( + + {isMedia && } + {cellFormatter && cellFormatter(row)} + {!isMedia && !cellFormatter && ( - {memoizedDisplayedValue(header.name)} + {memoizedDisplayedValue(name, type)} )} diff --git a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/TableHeader.js b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/TableHeader.js index 4898c51acd..b865293ebc 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/TableHeader.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/TableHeader.js @@ -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 }) { /> )} - {headers.map(header => { + {headers.map(({ key, name, metadatas: { label, sortable } }) => { return ( { - 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 }) { } }} > - - {header.label} + + {label} - {sortBy === header.name && ( - - )} + {sortBy === name && } ); diff --git a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js index 804904fe83..900a6cf3da 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js @@ -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 ; + }, + }, + ]; + } + + 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); diff --git a/packages/strapi-plugin-content-manager/admin/src/components/DisplayedFieldsDropdown/index.js b/packages/strapi-plugin-content-manager/admin/src/components/DisplayedFieldsDropdown/index.js index 0663df0246..10d2f9ef97 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/DisplayedFieldsDropdown/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/DisplayedFieldsDropdown/index.js @@ -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 ( @@ -38,9 +46,7 @@ const DisplayedFieldsDropdown = ({ - + {msg => (
)} - {items.map(item => ( - onChange(item)} - > -
- onChange(item)} - name={item.name} - value={item.value} - /> -
-
- ))} + {items.map(headerName => { + const value = displayedHeaders.findIndex(({ name }) => name === headerName) !== -1; + const handleChange = () => onChange({ name: headerName, value }); + + return ( + +
+ +
+
+ ); + })} @@ -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); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/actions.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/actions.js index c61c781b94..69ffbeeb9e 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/actions.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/actions.js @@ -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 }); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/constants.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/constants.js index bba1d0f742..1a60f4a514 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/constants.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/constants.js @@ -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 '; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js index a3b5e8fcfb..e0fda77a2c 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/index.js @@ -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 = () =>
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, @@ -87,7 +81,12 @@ function ListView({ toggleModalDeleteAll, // NEW + // allAllowedHeaders, + displayedHeaders, layout, + onChangeListHeaders, + onResetListHeaders, + setLayout, }) { const { emitEvent } = useGlobalContext(); const viewPermissions = useMemo(() => generatePermissionsObject(slug), [slug]); @@ -101,36 +100,29 @@ 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 { - defaultSortBy, - defaultSortOrder, - bulkable: isBulkable, - filterable: isFilterable, - searchable: isSearchable, - pageSize, - // mainField, - } = contentType.settings; + contentType: { + attributes, + settings: { + defaultSortBy, + defaultSortOrder, + bulkable: isBulkable, + filterable: isFilterable, + searchable: isSearchable, + pageSize, + // mainField, + }, + }, + } = 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] - ); + useEffect(() => { + // TODO + console.log('up'); + setLayout(layout); + }, [layout, setLayout]); - // 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 - const tableHeaders = useMemo(() => { - return listLayout; - // 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]); + 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; + 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'); - // 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 false; + } - return; - } + // 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 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, - }, - }); - }; + 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} > { - resetListLabels(slug); - }} + onClickReset={onResetListHeaders} slug={slug} - toggle={toggleLabelPickerState} />
@@ -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 ); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/reducer.js index 7bec9e4a29..e35e7f25a4 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/reducer.js @@ -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; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getAllAllowedHeaders.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getAllAllowedHeaders.js new file mode 100644 index 0000000000..5150097d4f --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getAllAllowedHeaders.js @@ -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; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getFirstSortableHeader.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getFirstSortableHeader.js new file mode 100644 index 0000000000..ec0a1d11ec --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/getFirstSortableHeader.js @@ -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; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/index.js new file mode 100644 index 0000000000..7c5cfa3bd8 --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/index.js @@ -0,0 +1,2 @@ +export { default as getAllAllowedHeaders } from './getAllAllowedHeaders'; +export { default as getFirstSortableHeader } from './getFirstSortableHeader'; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/tests/getFirstSortableHeader.test.js b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/tests/getFirstSortableHeader.test.js new file mode 100644 index 0000000000..d5d61be394 --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/containers/ListView/utils/tests/getFirstSortableHeader.test.js @@ -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'); + }); +}); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Main/actions.js b/packages/strapi-plugin-content-manager/admin/src/containers/Main/actions.js index 7a7d934111..c416f9cd1d 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Main/actions.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Main/actions.js @@ -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, diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Main/constants.js b/packages/strapi-plugin-content-manager/admin/src/containers/Main/constants.js index 92672bfc5d..47237d155d 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Main/constants.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Main/constants.js @@ -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'; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Main/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/Main/reducer.js index 52b30da2f0..36c26cce36 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Main/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Main/reducer.js @@ -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 }; diff --git a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/index.js b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/index.js index f331f2e827..90b3fdf3f8 100644 --- a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/index.js @@ -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); - dispatch({ type: 'GET_DATA_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; diff --git a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/reducer.js b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/reducer.js index 6d0e6c48c6..fce858d1d8 100644 --- a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/reducer.js @@ -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': { diff --git a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js index bd288559f9..9803481f2b 100644 --- a/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js +++ b/packages/strapi-plugin-content-manager/admin/src/hooks/useFetchContentTypeLayout/utils/formatLayouts.js @@ -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; }, []);