decap-cms-core 3.7.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,10 @@ import React from 'react';
3
3
  import ImmutablePropTypes from 'react-immutable-proptypes';
4
4
  import styled from '@emotion/styled';
5
5
  import { Waypoint } from 'react-waypoint';
6
- import { Map } from 'immutable';
6
+ import { Map, List } from 'immutable';
7
7
 
8
8
  import { selectFields, selectInferredField } from '../../../reducers/collections';
9
+ import { filterNestedEntries } from './EntriesCollection';
9
10
  import EntryCard from './EntryCard';
10
11
 
11
12
  const CardsGrid = styled.ul`
@@ -17,7 +18,7 @@ const CardsGrid = styled.ul`
17
18
  margin-bottom: 16px;
18
19
  `;
19
20
 
20
- export default class EntryListing extends React.Component {
21
+ class EntryListing extends React.Component {
21
22
  static propTypes = {
22
23
  collections: ImmutablePropTypes.iterable.isRequired,
23
24
  entries: ImmutablePropTypes.list,
@@ -25,6 +26,9 @@ export default class EntryListing extends React.Component {
25
26
  cursor: PropTypes.any.isRequired,
26
27
  handleCursorActions: PropTypes.func.isRequired,
27
28
  page: PropTypes.number,
29
+ getUnpublishedEntries: PropTypes.func.isRequired,
30
+ getWorkflowStatus: PropTypes.func.isRequired,
31
+ filterTerm: PropTypes.string,
28
32
  };
29
33
 
30
34
  componentDidMount() {
@@ -54,11 +58,58 @@ export default class EntryListing extends React.Component {
54
58
  return { titleField, descriptionField, imageField, remainingFields };
55
59
  };
56
60
 
61
+ getAllEntries = () => {
62
+ const { entries, collections, filterTerm } = this.props;
63
+ const collectionName = Map.isMap(collections) ? collections.get('name') : null;
64
+
65
+ if (!collectionName) {
66
+ return entries;
67
+ }
68
+
69
+ const unpublishedEntries = this.props.getUnpublishedEntries(collectionName);
70
+
71
+ if (!unpublishedEntries || unpublishedEntries.length === 0) {
72
+ return entries;
73
+ }
74
+
75
+ let unpublishedList = List(unpublishedEntries.map(entry => entry));
76
+
77
+ if (collections.has('nested') && filterTerm) {
78
+ const collectionFolder = collections.get('folder');
79
+ const subfolders = collections.get('nested').get('subfolders') !== false;
80
+
81
+ unpublishedList = filterNestedEntries(
82
+ filterTerm,
83
+ collectionFolder,
84
+ unpublishedList,
85
+ subfolders,
86
+ );
87
+ }
88
+
89
+ const publishedSlugs = entries.map(entry => entry.get('slug')).toSet();
90
+ const uniqueUnpublished = unpublishedList.filterNot(entry =>
91
+ publishedSlugs.has(entry.get('slug')),
92
+ );
93
+
94
+ return entries.concat(uniqueUnpublished);
95
+ };
96
+
57
97
  renderCardsForSingleCollection = () => {
58
- const { collections, entries, viewStyle } = this.props;
98
+ const { collections, viewStyle } = this.props;
99
+ const allEntries = this.getAllEntries();
59
100
  const inferredFields = this.inferFields(collections);
60
101
  const entryCardProps = { collection: collections, inferredFields, viewStyle };
61
- return entries.map((entry, idx) => <EntryCard {...entryCardProps} entry={entry} key={idx} />);
102
+
103
+ return allEntries.map((entry, idx) => {
104
+ const workflowStatus = this.props.getWorkflowStatus(
105
+ collections.get('name'),
106
+ entry.get('slug'),
107
+ );
108
+
109
+ return (
110
+ <EntryCard {...entryCardProps} entry={entry} workflowStatus={workflowStatus} key={idx} />
111
+ );
112
+ });
62
113
  };
63
114
 
64
115
  renderCardsForMultipleCollections = () => {
@@ -69,7 +120,14 @@ export default class EntryListing extends React.Component {
69
120
  const collection = collections.find(coll => coll.get('name') === collectionName);
70
121
  const collectionLabel = !isSingleCollectionInList && collection.get('label');
71
122
  const inferredFields = this.inferFields(collection);
72
- const entryCardProps = { collection, entry, inferredFields, collectionLabel };
123
+ const workflowStatus = this.props.getWorkflowStatus(collectionName, entry.get('slug'));
124
+ const entryCardProps = {
125
+ collection,
126
+ entry,
127
+ inferredFields,
128
+ collectionLabel,
129
+ workflowStatus,
130
+ };
73
131
  return <EntryCard {...entryCardProps} key={idx} />;
74
132
  });
75
133
  };
@@ -89,3 +147,5 @@ export default class EntryListing extends React.Component {
89
147
  );
90
148
  }
91
149
  }
150
+
151
+ export default EntryListing;
@@ -14,6 +14,19 @@ jest.mock('../Entries', () => 'mock-entries');
14
14
  const middlewares = [];
15
15
  const mockStore = configureStore(middlewares);
16
16
 
17
+ function createMockStore(collection, entriesArray, additionalState = {}) {
18
+ return mockStore({
19
+ entries: toEntriesState(collection, entriesArray),
20
+ cursors: fromJS({}),
21
+ config: fromJS({ publish_mode: 'simple' }),
22
+ collections: fromJS({ [collection.get('name')]: collection }),
23
+ editorialWorkflow: fromJS({
24
+ pages: { ids: [] },
25
+ }),
26
+ ...additionalState,
27
+ });
28
+ }
29
+
17
30
  function renderWithRedux(component, { store } = {}) {
18
31
  function Wrapper({ children }) {
19
32
  return <Provider store={store}>{children}</Provider>;
@@ -70,10 +83,18 @@ describe('EntriesCollection', () => {
70
83
  t: jest.fn(),
71
84
  loadEntries: jest.fn(),
72
85
  traverseCollectionCursor: jest.fn(),
86
+ loadUnpublishedEntries: jest.fn(),
73
87
  isFetching: false,
74
88
  cursor: {},
75
89
  collection,
90
+ collections: fromJS({ pages: collection }),
91
+ entriesLoaded: true,
92
+ unpublishedEntriesLoaded: true,
93
+ isEditorialWorkflowEnabled: false,
94
+ getWorkflowStatus: jest.fn(),
95
+ getUnpublishedEntries: jest.fn(() => []),
76
96
  };
97
+
77
98
  it('should render with entries', () => {
78
99
  const entries = fromJS([{ slug: 'index' }]);
79
100
  const { asFragment } = render(<EntriesCollection {...props} entries={entries} />);
@@ -88,10 +109,7 @@ describe('EntriesCollection', () => {
88
109
  { slug: 'dir2/index', path: 'src/pages/dir2/index.md', data: { title: 'File 2' } },
89
110
  ];
90
111
 
91
- const store = mockStore({
92
- entries: toEntriesState(collection, entriesArray),
93
- cursors: fromJS({}),
94
- });
112
+ const store = createMockStore(collection, entriesArray);
95
113
 
96
114
  const { asFragment } = renderWithRedux(<ConnectedEntriesCollection collection={collection} />, {
97
115
  store,
@@ -109,18 +127,13 @@ describe('EntriesCollection', () => {
109
127
  { slug: 'dir3/dir4/index', path: 'src/pages/dir3/dir4/index.md', data: { title: 'File 4' } },
110
128
  ];
111
129
 
112
- const store = mockStore({
113
- entries: toEntriesState(collection, entriesArray),
114
- cursors: fromJS({}),
115
- });
130
+ const store = createMockStore(collection, entriesArray);
116
131
 
117
132
  const { asFragment } = renderWithRedux(
118
133
  <ConnectedEntriesCollection
119
134
  collection={collection.set('nested', fromJS({ depth: 10, subfolders: false }))}
120
135
  />,
121
- {
122
- store,
123
- },
136
+ { store },
124
137
  );
125
138
 
126
139
  expect(asFragment()).toMatchSnapshot();
@@ -135,19 +148,14 @@ describe('EntriesCollection', () => {
135
148
  { slug: 'dir3/dir4/index', path: 'src/pages/dir3/dir4/index.md', data: { title: 'File 4' } },
136
149
  ];
137
150
 
138
- const store = mockStore({
139
- entries: toEntriesState(collection, entriesArray),
140
- cursors: fromJS({}),
141
- });
151
+ const store = createMockStore(collection, entriesArray);
142
152
 
143
153
  const { asFragment } = renderWithRedux(
144
154
  <ConnectedEntriesCollection
145
155
  collection={collection.set('nested', fromJS({ depth: 10, subfolders: false }))}
146
156
  filterTerm="dir3/dir4"
147
157
  />,
148
- {
149
- store,
150
- },
158
+ { store },
151
159
  );
152
160
 
153
161
  expect(asFragment()).toMatchSnapshot();
@@ -29,6 +29,7 @@ exports[`EntriesCollection should render with applied filter term for nested col
29
29
  collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10, \\"subfolders\\": false } }"
30
30
  cursor="[object Object]"
31
31
  entries="List [ Map { \\"slug\\": \\"dir3/dir4/index\\", \\"path\\": \\"src/pages/dir3/dir4/index.md\\", \\"data\\": Map { \\"title\\": \\"File 4\\" } } ]"
32
+ filterterm="dir3/dir4"
32
33
  />
33
34
  </DocumentFragment>
34
35
  `;
@@ -160,7 +160,15 @@ function getConfigSchema() {
160
160
  i18n: i18nRoot,
161
161
  site_url: { type: 'string', examples: ['https://example.com'] },
162
162
  display_url: { type: 'string', examples: ['https://example.com'] },
163
- logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
163
+ logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] }, // Deprecated, replaced by `logo.src`
164
+ logo: {
165
+ type: 'object',
166
+ properties: {
167
+ src: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
168
+ show_in_header: { type: 'boolean' },
169
+ },
170
+ required: ['src'],
171
+ },
164
172
  show_preview_links: { type: 'boolean' },
165
173
  media_folder: { type: 'string', examples: ['assets/uploads'] },
166
174
  public_folder: { type: 'string', examples: ['/uploads'] },
package/src/lib/i18n.ts CHANGED
@@ -221,6 +221,24 @@ export function formatI18nBackup(
221
221
  return i18n;
222
222
  }
223
223
 
224
+ function applyDefaultI18nValues(
225
+ collection: Collection,
226
+ value: EntryValue,
227
+ defaultLocaleValue: EntryValue,
228
+ ) {
229
+ if (collection.get('fields') === undefined) {
230
+ return;
231
+ }
232
+ collection.get('fields').forEach(field => {
233
+ if (field && field.get(I18N) === I18N_FIELD.DUPLICATE) {
234
+ const data = value.data[field.get('name')];
235
+ if (!data) {
236
+ value.data[field.get('name')] = defaultLocaleValue.data[field.get('name')];
237
+ }
238
+ }
239
+ });
240
+ }
241
+
224
242
  function mergeValues(
225
243
  collection: Collection,
226
244
  structure: I18N_STRUCTURE,
@@ -236,6 +254,9 @@ function mergeValues(
236
254
  .filter(e => e.locale !== defaultEntry!.locale)
237
255
  .reduce((acc, { locale, value }) => {
238
256
  const dataPath = getLocaleDataPath(locale);
257
+ if (defaultEntry) {
258
+ applyDefaultI18nValues(collection, value, defaultEntry.value);
259
+ }
239
260
  return set(acc, dataPath, value.data);
240
261
  }, {});
241
262
 
@@ -397,7 +397,11 @@ export interface CmsConfig {
397
397
  locale?: string;
398
398
  site_url?: string;
399
399
  display_url?: string;
400
- logo_url?: string;
400
+ logo_url?: string; // Deprecated, replaced by `logo.src`
401
+ logo?: {
402
+ src: string;
403
+ show_in_header?: boolean;
404
+ };
401
405
  show_preview_links?: boolean;
402
406
  media_folder?: string;
403
407
  public_folder?: string;