From aafe18967f66a351bd5274b59207ddb4360ba181 Mon Sep 17 00:00:00 2001 From: ascender1729 Date: Tue, 7 Oct 2025 19:07:58 +0530 Subject: [PATCH 1/2] fix: add separator and sorting for unpublished entries Implements proper sorting and visual separation for unpublished entries in collections when editorial workflow is enabled. Changes: - Add visual separator with "Unpublished Entries" heading - Implement sorting for unpublished entries using collection sort config - Extract unpublished entries logic into separate method - Add translation key for internationalization support - Pass sortFields through component chain Technical implementation: - Modify EntryListing to render published/unpublished separately - Create sortEntries() method for applying sort configuration - Add styled components for visual separation - Enhance component props to include sortFields Fixes #7542 --- .../components/Collection/Entries/Entries.js | 3 + .../Collection/Entries/EntriesCollection.js | 5 + .../Collection/Entries/EntryListing.js | 96 ++++++++++++++++--- .../EntriesCollection.spec.js.snap | 3 + packages/decap-cms-locales/src/en/index.js | 11 +++ 5 files changed, 105 insertions(+), 13 deletions(-) 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..7ced774e857e 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,59 @@ 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.toArray(), + + {t('collection.entries.unpublishedHeader')} + , + ...unpublishedCards.toArray(), + ]; }; renderCardsForMultipleCollections = () => { @@ -148,4 +218,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 5cba0d853d43..739dd4683db9 100644 --- a/packages/decap-cms-locales/src/en/index.js +++ b/packages/decap-cms-locales/src/en/index.js @@ -312,6 +312,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', + }, + }, }, }; From 7edb37fb245fd92f4c4dcb6381ae9f542e1503f6 Mon Sep 17 00:00:00 2001 From: ascender1729 Date: Wed, 8 Oct 2025 18:18:41 +0530 Subject: [PATCH 2/2] refactor: address PR review feedback - Add .claude/settings.local.json to .gitignore - Refactor EntryListing to use React.Fragment instead of array spread for better readability --- .gitignore | 1 + .../Collection/Entries/EntryListing.js | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) 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/EntryListing.js b/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js index 7ced774e857e..2ca6e277bc57 100644 --- a/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js +++ b/packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js @@ -173,13 +173,15 @@ class EntryListing extends React.Component { ); }); - return [ - ...publishedCards.toArray(), - - {t('collection.entries.unpublishedHeader')} - , - ...unpublishedCards.toArray(), - ]; + return ( + + {publishedCards} + + {t('collection.entries.unpublishedHeader')} + + {unpublishedCards} + + ); }; renderCardsForMultipleCollections = () => {