diff --git a/.gitignore b/.gitignore index 01ce5e521df5..5d55c107daf0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ coverage/ .temp/ storybook-static/ .nx +.claude/settings.local.json diff --git a/packages/decap-cms-core/src/components/Collection/Entries/Entries.js b/packages/decap-cms-core/src/components/Collection/Entries/Entries.js index 0ef11da57274..12edd6d0d1fd 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/Entries.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/Entries.js @@ -29,6 +29,7 @@ function Entries({ getWorkflowStatus, getUnpublishedEntries, filterTerm, + sortFields, }) { const loadingMessages = [ t('collection.entries.loadingEntries'), @@ -54,6 +55,7 @@ function Entries({ getWorkflowStatus={getWorkflowStatus} getUnpublishedEntries={getUnpublishedEntries} filterTerm={filterTerm} + sortFields={sortFields} /> {isFetching && page !== undefined && entries.size > 0 ? ( {t('collection.entries.loadingEntries')} @@ -77,6 +79,7 @@ Entries.propTypes = { getWorkflowStatus: PropTypes.func, getUnpublishedEntries: PropTypes.func, filterTerm: PropTypes.string, + sortFields: ImmutablePropTypes.list, }; export default translate()(Entries); diff --git a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js index 8847f3b7c242..b87188dd3759 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js @@ -18,6 +18,7 @@ import { selectEntriesLoaded, selectIsFetching, selectGroups, + selectEntriesSortFields, } from '../../../reducers/entries'; import { selectUnpublishedEntry, selectUnpublishedEntriesByStatus } from '../../../reducers'; import { selectCollectionEntriesCursor } from '../../../reducers/cursors'; @@ -144,6 +145,7 @@ export class EntriesCollection extends React.Component { getWorkflowStatus, getUnpublishedEntries, filterTerm, + sortFields, } = this.props; const EntriesToRender = ({ entries }) => { @@ -160,6 +162,7 @@ export class EntriesCollection extends React.Component { getWorkflowStatus={getWorkflowStatus} getUnpublishedEntries={getUnpublishedEntries} filterTerm={filterTerm} + sortFields={sortFields} /> ); }; @@ -206,6 +209,7 @@ function mapStateToProps(state, ownProps) { let entries = selectEntries(state.entries, collection); const groups = selectGroups(state.entries, collection); + const sortFields = selectEntriesSortFields(state.entries, collection.get('name')); if (collection.has('nested')) { const collectionFolder = collection.get('folder'); @@ -233,6 +237,7 @@ function mapStateToProps(state, ownProps) { page, entries, groups, + sortFields, entriesLoaded, isFetching, viewStyle, diff --git a/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js b/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js index c6d0e391457d..2ca6e277bc57 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js @@ -3,10 +3,18 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import styled from '@emotion/styled'; import { Waypoint } from 'react-waypoint'; -import { Map, List } from 'immutable'; - -import { selectFields, selectInferredField } from '../../../reducers/collections'; +import { Map, List, fromJS } from 'immutable'; +import { translate } from 'react-polyglot'; +import orderBy from 'lodash/orderBy'; +import { colors } from 'decap-cms-ui-default'; + +import { + selectFields, + selectInferredField, + selectSortDataPath, +} from '../../../reducers/collections'; import { filterNestedEntries } from './EntriesCollection'; +import { SortDirection } from '../../../types/redux'; import EntryCard from './EntryCard'; const CardsGrid = styled.ul` @@ -18,6 +26,20 @@ const CardsGrid = styled.ul` margin-bottom: 16px; `; +const SectionSeparator = styled.div` + width: 100%; + margin: 24px 0 16px 12px; + padding-top: 16px; + border-top: 2px solid ${colors.textFieldBorder}; +`; + +const SectionHeading = styled.h3` + font-size: 16px; + font-weight: 600; + color: ${colors.textLead}; + margin: 0 0 8px; +`; + class EntryListing extends React.Component { static propTypes = { collections: ImmutablePropTypes.iterable.isRequired, @@ -29,6 +51,8 @@ class EntryListing extends React.Component { getUnpublishedEntries: PropTypes.func.isRequired, getWorkflowStatus: PropTypes.func.isRequired, filterTerm: PropTypes.string, + sortFields: ImmutablePropTypes.list, + t: PropTypes.func.isRequired, }; componentDidMount() { @@ -58,18 +82,30 @@ class EntryListing extends React.Component { return { titleField, descriptionField, imageField, remainingFields }; }; - getAllEntries = () => { - const { entries, collections, filterTerm } = this.props; + sortEntries = (entries, sortFields, collections) => { + if (!sortFields || sortFields.size === 0) { + return entries; + } + + const keys = sortFields.map(v => selectSortDataPath(collections, v.get('key'))); + const orders = sortFields.map(v => + v.get('direction') === SortDirection.Ascending ? 'asc' : 'desc', + ); + return fromJS(orderBy(entries.toJS(), keys.toArray(), orders.toArray())); + }; + + getUnpublishedEntriesList = () => { + const { entries, collections, filterTerm, sortFields } = this.props; const collectionName = Map.isMap(collections) ? collections.get('name') : null; if (!collectionName) { - return entries; + return List(); } const unpublishedEntries = this.props.getUnpublishedEntries(collectionName); if (!unpublishedEntries || unpublishedEntries.length === 0) { - return entries; + return List(); } let unpublishedList = List(unpublishedEntries.map(entry => entry)); @@ -91,25 +127,61 @@ class EntryListing extends React.Component { publishedSlugs.has(entry.get('slug')), ); - return entries.concat(uniqueUnpublished); + return this.sortEntries(uniqueUnpublished, sortFields, collections); }; renderCardsForSingleCollection = () => { - const { collections, viewStyle } = this.props; - const allEntries = this.getAllEntries(); + const { collections, viewStyle, entries, t } = this.props; const inferredFields = this.inferFields(collections); const entryCardProps = { collection: collections, inferredFields, viewStyle }; - return allEntries.map((entry, idx) => { + const publishedCards = entries.map((entry, idx) => { + const workflowStatus = this.props.getWorkflowStatus( + collections.get('name'), + entry.get('slug'), + ); + + return ( + + ); + }); + + const unpublishedEntries = this.getUnpublishedEntriesList(); + + if (unpublishedEntries.size === 0) { + return publishedCards; + } + + const unpublishedCards = unpublishedEntries.map((entry, idx) => { const workflowStatus = this.props.getWorkflowStatus( collections.get('name'), entry.get('slug'), ); return ( - + ); }); + + return ( + + {publishedCards} + + {t('collection.entries.unpublishedHeader')} + + {unpublishedCards} + + ); }; renderCardsForMultipleCollections = () => { @@ -148,4 +220,4 @@ class EntryListing extends React.Component { } } -export default EntryListing; +export default translate()(EntryListing); diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap index 71e9ec1b5572..00b9bdb26969 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap +++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap @@ -7,6 +7,7 @@ exports[`EntriesCollection should render connected component 1`] = ` collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\" }" cursor="[object Object]" entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir2/index\\", \\"path\\": \\"src/pages/dir2/index.md\\", \\"data\\": Map { \\"title\\": \\"File 2\\" } } ]" + sortfields="" /> `; @@ -18,6 +19,7 @@ exports[`EntriesCollection should render show only immediate children for nested collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10, \\"subfolders\\": false } }" cursor="[object Object]" entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } } ]" + sortfields="" /> `; @@ -30,6 +32,7 @@ exports[`EntriesCollection should render with applied filter term for nested col cursor="[object Object]" entries="List [ Map { \\"slug\\": \\"dir3/dir4/index\\", \\"path\\": \\"src/pages/dir3/dir4/index.md\\", \\"data\\": Map { \\"title\\": \\"File 4\\" } } ]" filterterm="dir3/dir4" + sortfields="" /> `; diff --git a/packages/decap-cms-locales/src/en/index.js b/packages/decap-cms-locales/src/en/index.js index b41cd36de2fd..00148d7eb1d2 100644 --- a/packages/decap-cms-locales/src/en/index.js +++ b/packages/decap-cms-locales/src/en/index.js @@ -313,6 +313,17 @@ const en = { readyHeader: 'Ready', currentEntries: '%{smart_count} entry |||| %{smart_count} entries', }, + collection: { + sidebar: { + collections: 'Collections', + allCollections: 'All Collections', + searchAll: 'Search all', + searchIn: 'Search in', + }, + entries: { + unpublishedHeader: 'Unpublished Entries', + }, + }, }, };