diff --git a/.gitignore b/.gitignore
index f1f4c9cfd15f..44f04aa17e62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,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 50e8d15fd5f1..97d463a68252 100644
--- a/packages/decap-cms-core/src/components/Collection/Entries/Entries.js
+++ b/packages/decap-cms-core/src/components/Collection/Entries/Entries.js
@@ -30,6 +30,9 @@ function Entries({
getWorkflowStatus,
getUnpublishedEntries,
filterTerm,
+ sortFields,
+ showPublishedEntries = true,
+ showUnpublishedEntries = true,
}) {
const loadingMessages = [
t('collection.entries.loadingEntries'),
@@ -37,12 +40,13 @@ function Entries({
t('collection.entries.longerLoading'),
];
- if (isFetching && page === undefined) {
+ if (showPublishedEntries && isFetching && page === undefined) {
return {loadingMessages};
}
- const hasEntries = (entries && entries.size > 0) || cursor?.actions?.has('append_next');
- if (hasEntries) {
+ const hasEntries =
+ showPublishedEntries && ((entries && entries.size > 0) || cursor?.actions?.has('append_next'));
+ if (hasEntries || !showPublishedEntries) {
return (
<>
- {isFetching && page !== undefined && entries.size > 0 ? (
+ {showPublishedEntries && isFetching && page !== undefined && entries.size > 0 ? (
{t('collection.entries.loadingEntries')}
) : null}
>
@@ -78,6 +85,9 @@ Entries.propTypes = {
getWorkflowStatus: PropTypes.func,
getUnpublishedEntries: PropTypes.func,
filterTerm: PropTypes.string,
+ sortFields: PropTypes.array,
+ showPublishedEntries: PropTypes.bool,
+ showUnpublishedEntries: PropTypes.bool,
};
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..db8c9bfbc8fc 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';
@@ -54,7 +55,10 @@ function withGroups(groups, entries, EntriesToRender, t) {
return (
{title}
-
+
);
});
@@ -144,9 +148,18 @@ export class EntriesCollection extends React.Component {
getWorkflowStatus,
getUnpublishedEntries,
filterTerm,
+ sortFields,
} = this.props;
- const EntriesToRender = ({ entries }) => {
+ const EntriesToRender = ({ entries, showPublishedEntries, showUnpublishedEntries }) => {
+ const visibilityProps = {};
+ if (showPublishedEntries !== undefined) {
+ visibilityProps.showPublishedEntries = showPublishedEntries;
+ }
+ if (showUnpublishedEntries !== undefined) {
+ visibilityProps.showUnpublishedEntries = showUnpublishedEntries;
+ }
+
return (
);
};
if (groups && groups.length > 0) {
- return withGroups(groups, entries, EntriesToRender, t);
+ return (
+
+ {withGroups(groups, entries, EntriesToRender, t)}
+
+
+ );
}
return ;
@@ -206,6 +226,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 +254,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..b28b2102d19c 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 { Map, List, fromJS } from 'immutable';
+import { translate } from 'react-polyglot';
+import orderBy from 'lodash/orderBy';
+import { colors, lengths } from 'decap-cms-ui-default';
-import { selectFields, selectInferredField } from '../../../reducers/collections';
+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: ${lengths.topCardWidth};
+ margin: 24px 0 16px 12px;
+ padding-top: 16px;
+ border-top: 2px solid ${colors.textFieldBorder};
+`;
+
+const SectionHeading = styled.p`
+ 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,15 @@ class EntryListing extends React.Component {
getUnpublishedEntries: PropTypes.func.isRequired,
getWorkflowStatus: PropTypes.func.isRequired,
filterTerm: PropTypes.string,
+ sortFields: PropTypes.array,
+ showPublishedEntries: PropTypes.bool,
+ showUnpublishedEntries: PropTypes.bool,
+ t: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ showPublishedEntries: true,
+ showUnpublishedEntries: true,
};
componentDidMount() {
@@ -58,18 +89,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.length === 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, orders));
+ };
+
+ 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,31 +134,91 @@ 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,
+ page,
+ t,
+ showPublishedEntries,
+ showUnpublishedEntries,
+ } = this.props;
const inferredFields = this.inferFields(collections);
const entryCardProps = { collection: collections, inferredFields, viewStyle };
- return allEntries.map((entry, idx) => {
+ const publishedCards = showPublishedEntries
+ ? entries.map((entry, idx) => {
+ const workflowStatus = this.props.getWorkflowStatus(
+ collections.get('name'),
+ entry.get('slug'),
+ );
+
+ return (
+
+ );
+ })
+ : List();
+
+ const unpublishedEntries = showUnpublishedEntries ? this.getUnpublishedEntriesList() : List();
+
+ if (unpublishedEntries.size === 0) {
+ if (!showPublishedEntries) {
+ return null;
+ }
+
+ return (
+
+ {publishedCards}
+ {this.hasMore() && }
+
+ );
+ }
+
+ const unpublishedCards = unpublishedEntries.map((entry, idx) => {
const workflowStatus = this.props.getWorkflowStatus(
collections.get('name'),
entry.get('slug'),
);
return (
-
+
);
});
+
+ return (
+
+ {showPublishedEntries && (
+
+ {publishedCards}
+ {this.hasMore() && }
+
+ )}
+
+ {t('collection.entries.unpublishedHeader')}
+
+ {unpublishedCards}
+
+ );
};
renderCardsForMultipleCollections = () => {
- const { collections, entries } = this.props;
+ const { collections, entries, page } = this.props;
const isSingleCollectionInList = collections.size === 1;
- return entries.map((entry, idx) => {
+ const entryCards = entries.map((entry, idx) => {
const collectionName = entry.get('collection');
const collection = collections.find(coll => coll.get('name') === collectionName);
const collectionLabel = !isSingleCollectionInList && collection.get('label');
@@ -130,22 +233,26 @@ class EntryListing extends React.Component {
};
return ;
});
+
+ return (
+
+ {entryCards}
+ {this.hasMore() && }
+
+ );
};
render() {
- const { collections, page } = this.props;
+ const { collections } = this.props;
return (
-
- {Map.isMap(collections)
- ? this.renderCardsForSingleCollection()
- : this.renderCardsForMultipleCollections()}
- {this.hasMore() && }
-
+ {Map.isMap(collections)
+ ? this.renderCardsForSingleCollection()
+ : this.renderCardsForMultipleCollections()}
);
}
}
-export default EntryListing;
+export default translate()(EntryListing);
diff --git a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js
index 9d719a8248c0..8ef282154b15 100644
--- a/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js
+++ b/packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js
@@ -1,6 +1,6 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { fromJS } from 'immutable';
+import { Set, fromJS } from 'immutable';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
@@ -8,8 +8,12 @@ import ConnectedEntriesCollection, {
EntriesCollection,
filterNestedEntries,
} from '../EntriesCollection';
+import Entries from '../Entries';
-jest.mock('../Entries', () => 'mock-entries');
+jest.mock('../Entries', () => {
+ const React = require('react');
+ return jest.fn(props => );
+});
const middlewares = [];
const mockStore = configureStore(middlewares);
@@ -102,6 +106,31 @@ describe('EntriesCollection', () => {
expect(asFragment()).toMatchSnapshot();
});
+ it('should render unpublished entries once when entries are grouped', () => {
+ Entries.mockClear();
+
+ const entries = fromJS([
+ { slug: 'one', path: 'src/pages/one.md' },
+ { slug: 'two', path: 'src/pages/two.md' },
+ ]);
+ const groups = [
+ { id: 'Groupone', label: 'Group', value: 'one', paths: Set(['src/pages/one.md']) },
+ { id: 'Grouptwo', label: 'Group', value: 'two', paths: Set(['src/pages/two.md']) },
+ ];
+
+ const { container } = render(
+ ,
+ );
+ const renderedEntries = container.querySelectorAll('mock-entries');
+
+ expect(renderedEntries).toHaveLength(3);
+ expect(Entries).toHaveBeenCalledTimes(3);
+ expect(Entries.mock.calls[0][0].showUnpublishedEntries).toBe(false);
+ expect(Entries.mock.calls[1][0].showUnpublishedEntries).toBe(false);
+ expect(Entries.mock.calls[2][0].showPublishedEntries).toBe(false);
+ expect(Entries.mock.calls[2][0].entries).toBe(entries);
+ });
+
it('should render connected component', () => {
const entriesArray = [
{ slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
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 eea5209a5878..ebb49934b363 100644
--- a/packages/decap-cms-locales/src/en/index.js
+++ b/packages/decap-cms-locales/src/en/index.js
@@ -59,6 +59,7 @@ const en = {
cachingEntries: 'Caching Entries...',
longerLoading: 'This might take several minutes',
noEntries: 'No Entries',
+ unpublishedHeader: 'Unpublished Entries',
},
groups: {
other: 'Other',