Skip to content

Commit 2f5c54d

Browse files
ascender1729martinjagodicyanthomasdev
authored
fix: add separator and sorting for unpublished entries (#7624)
* 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 * 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 * fix: address comments * feat: split unpublished entries into its own list * fix: remove duplicate grouped entries --------- Co-authored-by: Martin Jagodic <jagodicmartin1@gmail.com> Co-authored-by: Yan <61414485+yanthomasdev@users.noreply.github.com>
1 parent 9cd6639 commit 2f5c54d

7 files changed

Lines changed: 203 additions & 30 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ coverage/
2121
.temp/
2222
storybook-static/
2323
.nx
24+
.claude/settings.local.json

packages/decap-cms-core/src/components/Collection/Entries/Entries.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,23 @@ function Entries({
3030
getWorkflowStatus,
3131
getUnpublishedEntries,
3232
filterTerm,
33+
sortFields,
34+
showPublishedEntries = true,
35+
showUnpublishedEntries = true,
3336
}) {
3437
const loadingMessages = [
3538
t('collection.entries.loadingEntries'),
3639
t('collection.entries.cachingEntries'),
3740
t('collection.entries.longerLoading'),
3841
];
3942

40-
if (isFetching && page === undefined) {
43+
if (showPublishedEntries && isFetching && page === undefined) {
4144
return <Loader active>{loadingMessages}</Loader>;
4245
}
4346

44-
const hasEntries = (entries && entries.size > 0) || cursor?.actions?.has('append_next');
45-
if (hasEntries) {
47+
const hasEntries =
48+
showPublishedEntries && ((entries && entries.size > 0) || cursor?.actions?.has('append_next'));
49+
if (hasEntries || !showPublishedEntries) {
4650
return (
4751
<>
4852
<EntryListing
@@ -55,8 +59,11 @@ function Entries({
5559
getWorkflowStatus={getWorkflowStatus}
5660
getUnpublishedEntries={getUnpublishedEntries}
5761
filterTerm={filterTerm}
62+
sortFields={sortFields}
63+
showPublishedEntries={showPublishedEntries}
64+
showUnpublishedEntries={showUnpublishedEntries}
5865
/>
59-
{isFetching && page !== undefined && entries.size > 0 ? (
66+
{showPublishedEntries && isFetching && page !== undefined && entries.size > 0 ? (
6067
<PaginationMessage>{t('collection.entries.loadingEntries')}</PaginationMessage>
6168
) : null}
6269
</>
@@ -78,6 +85,9 @@ Entries.propTypes = {
7885
getWorkflowStatus: PropTypes.func,
7986
getUnpublishedEntries: PropTypes.func,
8087
filterTerm: PropTypes.string,
88+
sortFields: PropTypes.array,
89+
showPublishedEntries: PropTypes.bool,
90+
showUnpublishedEntries: PropTypes.bool,
8191
};
8292

8393
export default translate()(Entries);

packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
selectEntriesLoaded,
1919
selectIsFetching,
2020
selectGroups,
21+
selectEntriesSortFields,
2122
} from '../../../reducers/entries';
2223
import { selectUnpublishedEntry, selectUnpublishedEntriesByStatus } from '../../../reducers';
2324
import { selectCollectionEntriesCursor } from '../../../reducers/cursors';
@@ -54,7 +55,10 @@ function withGroups(groups, entries, EntriesToRender, t) {
5455
return (
5556
<GroupContainer key={group.id} id={group.id}>
5657
<GroupHeading>{title}</GroupHeading>
57-
<EntriesToRender entries={getGroupEntries(entries, group.paths)} />
58+
<EntriesToRender
59+
entries={getGroupEntries(entries, group.paths)}
60+
showUnpublishedEntries={false}
61+
/>
5862
</GroupContainer>
5963
);
6064
});
@@ -144,9 +148,18 @@ export class EntriesCollection extends React.Component {
144148
getWorkflowStatus,
145149
getUnpublishedEntries,
146150
filterTerm,
151+
sortFields,
147152
} = this.props;
148153

149-
const EntriesToRender = ({ entries }) => {
154+
const EntriesToRender = ({ entries, showPublishedEntries, showUnpublishedEntries }) => {
155+
const visibilityProps = {};
156+
if (showPublishedEntries !== undefined) {
157+
visibilityProps.showPublishedEntries = showPublishedEntries;
158+
}
159+
if (showUnpublishedEntries !== undefined) {
160+
visibilityProps.showUnpublishedEntries = showUnpublishedEntries;
161+
}
162+
150163
return (
151164
<Entries
152165
collections={collection}
@@ -160,12 +173,19 @@ export class EntriesCollection extends React.Component {
160173
getWorkflowStatus={getWorkflowStatus}
161174
getUnpublishedEntries={getUnpublishedEntries}
162175
filterTerm={filterTerm}
176+
sortFields={sortFields}
177+
{...visibilityProps}
163178
/>
164179
);
165180
};
166181

167182
if (groups && groups.length > 0) {
168-
return withGroups(groups, entries, EntriesToRender, t);
183+
return (
184+
<React.Fragment>
185+
{withGroups(groups, entries, EntriesToRender, t)}
186+
<EntriesToRender entries={entries} showPublishedEntries={false} />
187+
</React.Fragment>
188+
);
169189
}
170190

171191
return <EntriesToRender entries={entries} />;
@@ -206,6 +226,7 @@ function mapStateToProps(state, ownProps) {
206226

207227
let entries = selectEntries(state.entries, collection);
208228
const groups = selectGroups(state.entries, collection);
229+
const sortFields = selectEntriesSortFields(state.entries, collection.get('name'));
209230

210231
if (collection.has('nested')) {
211232
const collectionFolder = collection.get('folder');
@@ -233,6 +254,7 @@ function mapStateToProps(state, ownProps) {
233254
page,
234255
entries,
235256
groups,
257+
sortFields,
236258
entriesLoaded,
237259
isFetching,
238260
viewStyle,

packages/decap-cms-core/src/components/Collection/Entries/EntryListing.js

Lines changed: 128 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ import React from 'react';
33
import ImmutablePropTypes from 'react-immutable-proptypes';
44
import styled from '@emotion/styled';
55
import { Waypoint } from 'react-waypoint';
6-
import { Map, List } from 'immutable';
6+
import { Map, List, fromJS } from 'immutable';
7+
import { translate } from 'react-polyglot';
8+
import orderBy from 'lodash/orderBy';
9+
import { colors, lengths } from 'decap-cms-ui-default';
710

8-
import { selectFields, selectInferredField } from '../../../reducers/collections';
11+
import {
12+
selectFields,
13+
selectInferredField,
14+
selectSortDataPath,
15+
} from '../../../reducers/collections';
916
import { filterNestedEntries } from './EntriesCollection';
17+
import { SortDirection } from '../../../types/redux';
1018
import EntryCard from './EntryCard';
1119

1220
const CardsGrid = styled.ul`
@@ -18,6 +26,20 @@ const CardsGrid = styled.ul`
1826
margin-bottom: 16px;
1927
`;
2028

29+
const SectionSeparator = styled.div`
30+
width: ${lengths.topCardWidth};
31+
margin: 24px 0 16px 12px;
32+
padding-top: 16px;
33+
border-top: 2px solid ${colors.textFieldBorder};
34+
`;
35+
36+
const SectionHeading = styled.p`
37+
font-size: 16px;
38+
font-weight: 600;
39+
color: ${colors.textLead};
40+
margin: 0 0 8px;
41+
`;
42+
2143
class EntryListing extends React.Component {
2244
static propTypes = {
2345
collections: ImmutablePropTypes.iterable.isRequired,
@@ -29,6 +51,15 @@ class EntryListing extends React.Component {
2951
getUnpublishedEntries: PropTypes.func.isRequired,
3052
getWorkflowStatus: PropTypes.func.isRequired,
3153
filterTerm: PropTypes.string,
54+
sortFields: PropTypes.array,
55+
showPublishedEntries: PropTypes.bool,
56+
showUnpublishedEntries: PropTypes.bool,
57+
t: PropTypes.func.isRequired,
58+
};
59+
60+
static defaultProps = {
61+
showPublishedEntries: true,
62+
showUnpublishedEntries: true,
3263
};
3364

3465
componentDidMount() {
@@ -58,18 +89,30 @@ class EntryListing extends React.Component {
5889
return { titleField, descriptionField, imageField, remainingFields };
5990
};
6091

61-
getAllEntries = () => {
62-
const { entries, collections, filterTerm } = this.props;
92+
sortEntries = (entries, sortFields, collections) => {
93+
if (!sortFields || sortFields.length === 0) {
94+
return entries;
95+
}
96+
97+
const keys = sortFields.map(v => selectSortDataPath(collections, v.get('key')));
98+
const orders = sortFields.map(v =>
99+
v.get('direction') === SortDirection.Ascending ? 'asc' : 'desc',
100+
);
101+
return fromJS(orderBy(entries.toJS(), keys, orders));
102+
};
103+
104+
getUnpublishedEntriesList = () => {
105+
const { entries, collections, filterTerm, sortFields } = this.props;
63106
const collectionName = Map.isMap(collections) ? collections.get('name') : null;
64107

65108
if (!collectionName) {
66-
return entries;
109+
return List();
67110
}
68111

69112
const unpublishedEntries = this.props.getUnpublishedEntries(collectionName);
70113

71114
if (!unpublishedEntries || unpublishedEntries.length === 0) {
72-
return entries;
115+
return List();
73116
}
74117

75118
let unpublishedList = List(unpublishedEntries.map(entry => entry));
@@ -91,31 +134,91 @@ class EntryListing extends React.Component {
91134
publishedSlugs.has(entry.get('slug')),
92135
);
93136

94-
return entries.concat(uniqueUnpublished);
137+
return this.sortEntries(uniqueUnpublished, sortFields, collections);
95138
};
96139

97140
renderCardsForSingleCollection = () => {
98-
const { collections, viewStyle } = this.props;
99-
const allEntries = this.getAllEntries();
141+
const {
142+
collections,
143+
viewStyle,
144+
entries,
145+
page,
146+
t,
147+
showPublishedEntries,
148+
showUnpublishedEntries,
149+
} = this.props;
100150
const inferredFields = this.inferFields(collections);
101151
const entryCardProps = { collection: collections, inferredFields, viewStyle };
102152

103-
return allEntries.map((entry, idx) => {
153+
const publishedCards = showPublishedEntries
154+
? entries.map((entry, idx) => {
155+
const workflowStatus = this.props.getWorkflowStatus(
156+
collections.get('name'),
157+
entry.get('slug'),
158+
);
159+
160+
return (
161+
<EntryCard
162+
{...entryCardProps}
163+
entry={entry}
164+
workflowStatus={workflowStatus}
165+
key={`published-${idx}`}
166+
/>
167+
);
168+
})
169+
: List();
170+
171+
const unpublishedEntries = showUnpublishedEntries ? this.getUnpublishedEntriesList() : List();
172+
173+
if (unpublishedEntries.size === 0) {
174+
if (!showPublishedEntries) {
175+
return null;
176+
}
177+
178+
return (
179+
<CardsGrid>
180+
{publishedCards}
181+
{this.hasMore() && <Waypoint key={page} onEnter={this.handleLoadMore} />}
182+
</CardsGrid>
183+
);
184+
}
185+
186+
const unpublishedCards = unpublishedEntries.map((entry, idx) => {
104187
const workflowStatus = this.props.getWorkflowStatus(
105188
collections.get('name'),
106189
entry.get('slug'),
107190
);
108191

109192
return (
110-
<EntryCard {...entryCardProps} entry={entry} workflowStatus={workflowStatus} key={idx} />
193+
<EntryCard
194+
{...entryCardProps}
195+
entry={entry}
196+
workflowStatus={workflowStatus}
197+
key={`unpublished-${idx}`}
198+
/>
111199
);
112200
});
201+
202+
return (
203+
<React.Fragment>
204+
{showPublishedEntries && (
205+
<CardsGrid>
206+
{publishedCards}
207+
{this.hasMore() && <Waypoint key={page} onEnter={this.handleLoadMore} />}
208+
</CardsGrid>
209+
)}
210+
<SectionSeparator>
211+
<SectionHeading>{t('collection.entries.unpublishedHeader')}</SectionHeading>
212+
</SectionSeparator>
213+
<CardsGrid>{unpublishedCards}</CardsGrid>
214+
</React.Fragment>
215+
);
113216
};
114217

115218
renderCardsForMultipleCollections = () => {
116-
const { collections, entries } = this.props;
219+
const { collections, entries, page } = this.props;
117220
const isSingleCollectionInList = collections.size === 1;
118-
return entries.map((entry, idx) => {
221+
const entryCards = entries.map((entry, idx) => {
119222
const collectionName = entry.get('collection');
120223
const collection = collections.find(coll => coll.get('name') === collectionName);
121224
const collectionLabel = !isSingleCollectionInList && collection.get('label');
@@ -130,22 +233,26 @@ class EntryListing extends React.Component {
130233
};
131234
return <EntryCard {...entryCardProps} key={idx} />;
132235
});
236+
237+
return (
238+
<CardsGrid>
239+
{entryCards}
240+
{this.hasMore() && <Waypoint key={page} onEnter={this.handleLoadMore} />}
241+
</CardsGrid>
242+
);
133243
};
134244

135245
render() {
136-
const { collections, page } = this.props;
246+
const { collections } = this.props;
137247

138248
return (
139249
<div>
140-
<CardsGrid>
141-
{Map.isMap(collections)
142-
? this.renderCardsForSingleCollection()
143-
: this.renderCardsForMultipleCollections()}
144-
{this.hasMore() && <Waypoint key={page} onEnter={this.handleLoadMore} />}
145-
</CardsGrid>
250+
{Map.isMap(collections)
251+
? this.renderCardsForSingleCollection()
252+
: this.renderCardsForMultipleCollections()}
146253
</div>
147254
);
148255
}
149256
}
150257

151-
export default EntryListing;
258+
export default translate()(EntryListing);

0 commit comments

Comments
 (0)