decap-cms-core 3.8.1 → 3.10.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.
Files changed (37) hide show
  1. package/dist/decap-cms-core.js +23 -23
  2. package/dist/decap-cms-core.js.LICENSE.txt +10 -0
  3. package/dist/decap-cms-core.js.map +1 -1
  4. package/dist/esm/actions/config.js +19 -0
  5. package/dist/esm/actions/entries.js +10 -1
  6. package/dist/esm/bootstrap.js +2 -2
  7. package/dist/esm/components/App/StatusBar.js +41 -0
  8. package/dist/esm/components/Collection/Entries/Pagination.js +132 -0
  9. package/dist/esm/components/Editor/EditorNotesPane/AddNoteForm.js +111 -0
  10. package/dist/esm/components/Editor/EditorNotesPane/EditorNotesPane.js +138 -0
  11. package/dist/esm/components/Editor/EditorNotesPane/NoteItem.js +247 -0
  12. package/dist/esm/components/Editor/EditorNotesPane/NotesList.js +54 -0
  13. package/dist/esm/components/UI/ErrorBoundary.js +6 -9
  14. package/dist/esm/constants/configSchema.js +41 -23
  15. package/dist/esm/lib/entryCache.js +145 -0
  16. package/dist/esm/lib/entryHelpers.js +102 -0
  17. package/dist/esm/lib/formatters.js +2 -1
  18. package/dist/esm/lib/immutableHelpers.js +21 -0
  19. package/dist/esm/lib/indexFileHelper.js +36 -0
  20. package/dist/esm/lib/pagination.js +68 -0
  21. package/dist/esm/reducers/collections.js +54 -5
  22. package/dist/esm/reducers/entries.js +9 -4
  23. package/index.d.ts +10 -2
  24. package/package.json +2 -3
  25. package/src/actions/__tests__/config.spec.js +4 -4
  26. package/src/actions/config.ts +22 -0
  27. package/src/actions/entries.ts +11 -1
  28. package/src/components/UI/ErrorBoundary.js +1 -2
  29. package/src/constants/__tests__/configSchema.spec.js +84 -0
  30. package/src/constants/configSchema.js +34 -1
  31. package/src/lib/__tests__/formatters.spec.js +30 -2
  32. package/src/lib/formatters.ts +6 -1
  33. package/src/reducers/__tests__/collections.spec.js +39 -0
  34. package/src/reducers/__tests__/entries.spec.js +3 -3
  35. package/src/reducers/collections.ts +52 -5
  36. package/src/reducers/entries.ts +13 -5
  37. package/src/types/redux.ts +11 -3
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pagination Utilities
3
+ *
4
+ * This module provides helpers for determining if pagination is enabled for a collection,
5
+ * and for retrieving the effective pagination configuration (page size, enabled flag).
6
+ *
7
+ * Principles:
8
+ * - Pagination can be enabled/disabled per collection or globally via config.
9
+ * - If sorting, filtering, or grouping is active, pagination is handled client-side (all entries loaded).
10
+ * - If none are active, server-side pagination is used (only a page of entries loaded at a time).
11
+ * - The effective page size is determined by collection config, then global config, then a default.
12
+ *
13
+ * Usage:
14
+ * - Use isPaginationEnabled(collection, config) to check if pagination should be active.
15
+ * - Use getPaginationConfig(collection, config) to get the effective page size and enabled flag.
16
+ */
17
+ import { getValue, isImmutableMap } from './immutableHelpers';
18
+ const DEFAULT_PER_PAGE = 100;
19
+ export function isPaginationEnabled(collection, globalConfig) {
20
+ const pagination = isImmutableMap(collection) ? collection.get('pagination') : collection.pagination;
21
+ if (typeof pagination !== 'undefined') {
22
+ if (typeof pagination === 'boolean') return pagination;
23
+ if (pagination && typeof pagination === 'object') {
24
+ const enabled = getValue(pagination, 'enabled');
25
+ return enabled !== false;
26
+ }
27
+ return false;
28
+ }
29
+ if (globalConfig?.pagination) {
30
+ if (typeof globalConfig.pagination === 'boolean') return globalConfig.pagination;
31
+ if (typeof globalConfig.pagination === 'object') {
32
+ const enabled = getValue(globalConfig.pagination, 'enabled');
33
+ return enabled !== false;
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+ export function getPaginationConfig(collection, globalConfig) {
39
+ const defaults = {
40
+ enabled: false,
41
+ per_page: DEFAULT_PER_PAGE
42
+ };
43
+ if (globalConfig?.pagination) {
44
+ if (typeof globalConfig.pagination === 'boolean') {
45
+ defaults.enabled = globalConfig.pagination;
46
+ } else if (typeof globalConfig.pagination === 'object') {
47
+ const enabled = getValue(globalConfig.pagination, 'enabled');
48
+ defaults.enabled = enabled !== false;
49
+ const perPage = getValue(globalConfig.pagination, 'per_page');
50
+ defaults.per_page = typeof perPage === 'number' ? perPage : defaults.per_page;
51
+ }
52
+ }
53
+ const pagination = isImmutableMap(collection) ? collection.get('pagination') : collection.pagination;
54
+ if (pagination === true) {
55
+ return {
56
+ enabled: true,
57
+ per_page: defaults.per_page
58
+ };
59
+ } else if (typeof pagination === 'object' && pagination !== null) {
60
+ const perPage = getValue(pagination, 'per_page');
61
+ const enabled = getValue(pagination, 'enabled');
62
+ return {
63
+ enabled: enabled !== false,
64
+ per_page: typeof perPage === 'number' ? perPage : defaults.per_page
65
+ };
66
+ }
67
+ return defaults;
68
+ }
@@ -305,32 +305,58 @@ export function selectDefaultSortableFields(collection, backend, hasIntegration)
305
305
  // always have commit date by default
306
306
  defaultSortable = [COMMIT_DATE, ...defaultSortable];
307
307
  }
308
- return defaultSortable;
308
+
309
+ // Return as objects with field property
310
+ return defaultSortable.map(field => ({
311
+ field
312
+ }));
309
313
  }
310
314
  export function selectSortableFields(collection, t) {
311
- const fields = collection.get('sortable_fields').toArray().map(key => {
315
+ const fields = collection.get('sortable_fields').toArray().map(sortableField => {
316
+ // Extract the field name and custom label from the sortable field object
317
+ const key = sortableField.get('field');
318
+ const customLabel = sortableField.get('label');
312
319
  if (key === COMMIT_DATE) {
320
+ const label = customLabel || t('collection.defaultFields.updatedOn.label');
313
321
  return {
314
322
  key,
315
323
  field: {
316
324
  name: key,
317
- label: t('collection.defaultFields.updatedOn.label')
325
+ label
318
326
  }
319
327
  };
320
328
  }
321
329
  const field = selectField(collection, key);
322
330
  if (key === COMMIT_AUTHOR && !field) {
331
+ const label = customLabel || t('collection.defaultFields.author.label');
323
332
  return {
324
333
  key,
325
334
  field: {
326
335
  name: key,
327
- label: t('collection.defaultFields.author.label')
336
+ label
328
337
  }
329
338
  };
330
339
  }
340
+ let fieldObj = field?.toJS();
341
+
342
+ // If custom label is provided, override the field's label
343
+ if (fieldObj && customLabel) {
344
+ fieldObj = {
345
+ ...fieldObj,
346
+ label: customLabel
347
+ };
348
+ }
349
+
350
+ // If no label exists at all, use the field name
351
+ if (fieldObj && !fieldObj.label) {
352
+ fieldObj = {
353
+ ...fieldObj,
354
+ label: fieldObj.name || key
355
+ };
356
+ }
331
357
  return {
332
358
  key,
333
- field: field?.toJS()
359
+ field: fieldObj
334
360
  };
335
361
  }).filter(item => !!item.field).map(item => ({
336
362
  ...item.field,
@@ -338,6 +364,29 @@ export function selectSortableFields(collection, t) {
338
364
  }));
339
365
  return fields;
340
366
  }
367
+ export function selectDefaultSortField(collection) {
368
+ const sortableFields = collection.get('sortable_fields').toArray();
369
+ const defaultField = sortableFields.find(field => field.get('default_sort') !== undefined);
370
+ if (!defaultField) {
371
+ return null;
372
+ }
373
+ const fieldName = defaultField.get('field');
374
+ const defaultSortValue = defaultField.get('default_sort');
375
+
376
+ // Determine direction based on default_sort value
377
+ let direction;
378
+ if (defaultSortValue === true || defaultSortValue === 'asc') {
379
+ direction = 'asc';
380
+ } else if (defaultSortValue === 'desc') {
381
+ direction = 'desc';
382
+ } else {
383
+ direction = 'asc'; // fallback
384
+ }
385
+ return {
386
+ field: fieldName,
387
+ direction
388
+ };
389
+ }
341
390
  export function selectSortDataPath(collection, key) {
342
391
  if (key === COMMIT_DATE) {
343
392
  return 'updatedOn';
@@ -25,6 +25,12 @@ let page;
25
25
  let slug;
26
26
  const storageSortKey = 'decap-cms.entries.sort';
27
27
  const viewStyleKey = 'decap-cms.entries.viewStyle';
28
+ function normalizeDoubleSlashes(path) {
29
+ if (!path) {
30
+ return path;
31
+ }
32
+ return path.replace(/([^:]\/)\/+/g, '$1');
33
+ }
28
34
  const loadSort = once(() => {
29
35
  const sortString = localStorage.getItem(storageSortKey);
30
36
  if (sortString) {
@@ -429,7 +435,6 @@ export function selectEntriesLoaded(state, collection) {
429
435
  export function selectIsFetching(state, collection) {
430
436
  return state.getIn(['pages', collection, 'isFetching'], false);
431
437
  }
432
- const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
433
438
  function getFileField(collectionFiles, slug) {
434
439
  const file = collectionFiles.find(f => f?.get('name') === slug);
435
440
  return file;
@@ -528,7 +533,7 @@ export function selectMediaFolder(config, collection, entryMap, field) {
528
533
  mediaFolder = join(folder);
529
534
  } else {
530
535
  const entryPath = entryMap?.get('path');
531
- mediaFolder = entryPath ? join(dirname(entryPath), folder) : join(collection.get('folder'), DRAFT_MEDIA_FILES);
536
+ mediaFolder = entryPath ? join(dirname(entryPath), folder) : collection.get('folder');
532
537
  }
533
538
  }
534
539
  return trim(mediaFolder, '/');
@@ -545,10 +550,10 @@ export function selectMediaFilePublicPath(config, collection, mediaPath, entryMa
545
550
  return mediaPath;
546
551
  }
547
552
  const name = 'public_folder';
548
- let publicFolder = config[name];
553
+ let publicFolder = normalizeDoubleSlashes(config[name]);
549
554
  const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);
550
555
  if (customFolder) {
551
- publicFolder = evaluateFolder(name, config, collection, entryMap, field);
556
+ publicFolder = normalizeDoubleSlashes(evaluateFolder(name, config, collection, entryMap, field));
552
557
  }
553
558
  if (isAbsolutePath(publicFolder)) {
554
559
  return joinUrlPath(publicFolder, basename(mediaPath));
package/index.d.ts CHANGED
@@ -133,6 +133,8 @@ declare module 'decap-cms-core' {
133
133
  default?: any;
134
134
 
135
135
  allow_add?: boolean;
136
+ allow_remove?: boolean;
137
+ allow_reorder?: boolean;
136
138
  collapsed?: boolean;
137
139
  summary?: string;
138
140
  minimize_collapsed?: boolean;
@@ -290,6 +292,12 @@ declare module 'decap-cms-core' {
290
292
  pattern?: string;
291
293
  }
292
294
 
295
+ export interface SortableField {
296
+ field: string;
297
+ label?: string;
298
+ default_sort?: boolean | 'asc' | 'desc';
299
+ }
300
+
293
301
  export interface CmsCollection {
294
302
  name: string;
295
303
  label: string;
@@ -330,7 +338,7 @@ declare module 'decap-cms-core' {
330
338
  path?: string;
331
339
  media_folder?: string;
332
340
  public_folder?: string;
333
- sortable_fields?: string[];
341
+ sortable_fields?: (string | SortableField)[];
334
342
  view_filters?: ViewFilter[];
335
343
  view_groups?: ViewGroup[];
336
344
  i18n?: boolean | CmsI18nConfig;
@@ -338,7 +346,7 @@ declare module 'decap-cms-core' {
338
346
  /**
339
347
  * @deprecated Use sortable_fields instead
340
348
  */
341
- sortableFields?: string[];
349
+ sortableFields?: (string | SortableField)[];
342
350
  }
343
351
 
344
352
  export interface CmsBackend {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "decap-cms-core",
3
3
  "description": "Decap CMS core application, see decap-cms package for the main distribution.",
4
- "version": "3.8.1",
4
+ "version": "3.10.0",
5
5
  "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-core",
6
6
  "bugs": "https://github.com/decaporg/decap-cms/issues",
7
7
  "module": "dist/esm/index.js",
@@ -28,7 +28,6 @@
28
28
  "@reduxjs/toolkit": "^1.9.1",
29
29
  "@vercel/stega": "^0.1.2",
30
30
  "buffer": "^6.0.3",
31
- "clean-stack": "^5.2.0",
32
31
  "copy-text-to-clipboard": "^3.0.0",
33
32
  "dayjs": "^1.11.10",
34
33
  "deepmerge": "^4.2.2",
@@ -100,5 +99,5 @@
100
99
  "browser": {
101
100
  "path": "path-browserify"
102
101
  },
103
- "gitHead": "899dba82d1f396260e0f84c6977c1d2aee809b59"
102
+ "gitHead": "0c1b7c7d63ba6ddb3de3692fbb6b16ac6ebb44d5"
104
103
  }
@@ -1,5 +1,5 @@
1
1
  import { stripIndent } from 'common-tags';
2
- import { dump } from 'js-yaml';
2
+ import { stringify } from 'yaml';
3
3
 
4
4
  import {
5
5
  loadConfig,
@@ -480,7 +480,7 @@ describe('config', () => {
480
480
  ).toEqual({
481
481
  collections: [
482
482
  {
483
- sortable_fields: ['title'],
483
+ sortable_fields: [{ field: 'title', default_sort: undefined }],
484
484
  folder: 'src',
485
485
  type: 'folder_based_collection',
486
486
  view_filters: [],
@@ -934,7 +934,7 @@ describe('config', () => {
934
934
 
935
935
  global.fetch.mockResolvedValue({
936
936
  status: 200,
937
- text: () => Promise.resolve(dump({ backend: { repo: 'test-repo' } })),
937
+ text: () => Promise.resolve(stringify({ backend: { repo: 'test-repo' } })),
938
938
  headers: new Headers(),
939
939
  });
940
940
  await loadConfig()(dispatch);
@@ -962,7 +962,7 @@ describe('config', () => {
962
962
  document.querySelector.mockReturnValue({ type: 'text/yaml', href: 'custom-config.yml' });
963
963
  global.fetch.mockResolvedValue({
964
964
  status: 200,
965
- text: () => Promise.resolve(dump({ backend: { repo: 'github' } })),
965
+ text: () => Promise.resolve(stringify({ backend: { repo: 'github' } })),
966
966
  headers: new Headers(),
967
967
  });
968
968
  await loadConfig()(dispatch);
@@ -171,6 +171,20 @@ function hasIntegration(config: CmsConfig, collection: CmsCollection) {
171
171
  return !!integration;
172
172
  }
173
173
 
174
+ function normalizeSortableFields(
175
+ sortableFields: (
176
+ | string
177
+ | { field: string; label?: string; default_sort?: boolean | 'asc' | 'desc' }
178
+ )[],
179
+ ) {
180
+ return sortableFields.map(field => {
181
+ if (typeof field === 'string') {
182
+ return { field, default_sort: undefined };
183
+ }
184
+ return field;
185
+ });
186
+ }
187
+
174
188
  export function normalizeConfig(config: CmsConfig) {
175
189
  const { collections = [] } = config;
176
190
 
@@ -200,6 +214,14 @@ export function normalizeConfig(config: CmsConfig) {
200
214
  );
201
215
  }
202
216
 
217
+ // Normalize sortable_fields to consistent object format
218
+ if (normalizedCollection.sortable_fields) {
219
+ normalizedCollection = {
220
+ ...normalizedCollection,
221
+ sortable_fields: normalizeSortableFields(normalizedCollection.sortable_fields),
222
+ };
223
+ }
224
+
203
225
  return normalizedCollection;
204
226
  });
205
227
 
@@ -3,7 +3,7 @@ import isEqual from 'lodash/isEqual';
3
3
  import { Cursor } from 'decap-cms-lib-util';
4
4
 
5
5
  import { selectCollectionEntriesCursor } from '../reducers/cursors';
6
- import { selectFields, updateFieldByKey } from '../reducers/collections';
6
+ import { selectFields, updateFieldByKey, selectDefaultSortField } from '../reducers/collections';
7
7
  import { selectIntegration, selectPublishedSlugs } from '../reducers';
8
8
  import { getIntegrationProvider } from '../integrations';
9
9
  import { currentBackend } from '../backend';
@@ -579,11 +579,21 @@ export function loadEntries(collection: Collection, page = 0) {
579
579
  }
580
580
  const state = getState();
581
581
  const sortFields = selectEntriesSortFields(state.entries, collection.get('name'));
582
+
583
+ // If user has already set a sort, use it
582
584
  if (sortFields && sortFields.length > 0) {
583
585
  const field = sortFields[0];
584
586
  return dispatch(sortByField(collection, field.get('key'), field.get('direction')));
585
587
  }
586
588
 
589
+ // Otherwise, check for a default sort field in the collection configuration
590
+ const defaultSort = selectDefaultSortField(collection);
591
+ if (defaultSort) {
592
+ const direction =
593
+ defaultSort.direction === 'desc' ? SortDirection.Descending : SortDirection.Ascending;
594
+ return dispatch(sortByField(collection, defaultSort.field, direction));
595
+ }
596
+
587
597
  const backend = currentBackend(state.config);
588
598
  const integration = selectIntegration(state, collection.get('name'), 'listEntries');
589
599
  const provider = integration
@@ -7,7 +7,6 @@ import truncate from 'lodash/truncate';
7
7
  import copyToClipboard from 'copy-text-to-clipboard';
8
8
  import { localForage } from 'decap-cms-lib-util';
9
9
  import { buttons, colors } from 'decap-cms-ui-default';
10
- import cleanStack from 'clean-stack';
11
10
 
12
11
  const ISSUE_URL = 'https://github.com/decaporg/decap-cms/issues/new?';
13
12
 
@@ -145,7 +144,7 @@ export class ErrorBoundary extends React.Component {
145
144
  console.error(error);
146
145
  return {
147
146
  hasError: true,
148
- errorMessage: cleanStack(error.stack, { basePath: window.location.origin || '' }),
147
+ errorMessage: error.stack || error.toString(),
149
148
  errorTitle: error.toString(),
150
149
  };
151
150
  }
@@ -216,6 +216,90 @@ describe('config', () => {
216
216
  }).toThrowError("'collections[0]' must NOT be valid");
217
217
  });
218
218
 
219
+ it('should allow sortable_fields to have object format with field property', () => {
220
+ expect(() => {
221
+ validateConfig(
222
+ merge({}, validConfig, {
223
+ collections: [{ sortable_fields: [{ field: 'title' }] }],
224
+ }),
225
+ );
226
+ }).not.toThrow();
227
+ });
228
+
229
+ it('should allow sortable_fields with default_sort as boolean', () => {
230
+ expect(() => {
231
+ validateConfig(
232
+ merge({}, validConfig, {
233
+ collections: [{ sortable_fields: [{ field: 'title', default_sort: true }] }],
234
+ }),
235
+ );
236
+ }).not.toThrow();
237
+ });
238
+
239
+ it('should allow sortable_fields with default_sort as asc/desc', () => {
240
+ expect(() => {
241
+ validateConfig(
242
+ merge({}, validConfig, {
243
+ collections: [{ sortable_fields: ['title', { field: 'date', default_sort: 'desc' }] }],
244
+ }),
245
+ );
246
+ }).not.toThrow();
247
+ });
248
+
249
+ it('should allow sortable_fields with custom label', () => {
250
+ expect(() => {
251
+ validateConfig(
252
+ merge({}, validConfig, {
253
+ collections: [{ sortable_fields: [{ field: 'date', label: 'Publish Date' }] }],
254
+ }),
255
+ );
256
+ }).not.toThrow();
257
+ });
258
+
259
+ it('should allow sortable_fields with label and default_sort', () => {
260
+ expect(() => {
261
+ validateConfig(
262
+ merge({}, validConfig, {
263
+ collections: [
264
+ {
265
+ sortable_fields: [
266
+ 'title',
267
+ { field: 'date', label: 'Publish Date', default_sort: 'desc' },
268
+ ],
269
+ },
270
+ ],
271
+ }),
272
+ );
273
+ }).not.toThrow();
274
+ });
275
+
276
+ it('should allow mixed string and object format in sortable_fields', () => {
277
+ expect(() => {
278
+ validateConfig(
279
+ merge({}, validConfig, {
280
+ collections: [{ sortable_fields: ['title', { field: 'date', default_sort: true }] }],
281
+ }),
282
+ );
283
+ }).not.toThrow();
284
+ });
285
+
286
+ it('should throw if more than one sortable field has default_sort property', () => {
287
+ expect(() => {
288
+ validateConfig(
289
+ merge({}, validConfig, {
290
+ collections: [
291
+ {
292
+ sortable_fields: [
293
+ { field: 'title', default_sort: true },
294
+ { field: 'date', default_sort: true },
295
+ ],
296
+ },
297
+ ],
298
+ }),
299
+ );
300
+ }).toThrowError('only one sortable field can have the default_sort property');
301
+ });
302
+
219
303
  it('should throw if collection names are not unique', () => {
220
304
  expect(() => {
221
305
  validateConfig(
@@ -253,7 +253,21 @@ function getConfigSchema() {
253
253
  sortable_fields: {
254
254
  type: 'array',
255
255
  items: {
256
- type: 'string',
256
+ oneOf: [
257
+ { type: 'string' },
258
+ {
259
+ type: 'object',
260
+ properties: {
261
+ field: { type: 'string' },
262
+ label: { type: 'string' },
263
+ default_sort: {
264
+ oneOf: [{ type: 'boolean' }, { type: 'string', enum: ['asc', 'desc'] }],
265
+ },
266
+ },
267
+ required: ['field'],
268
+ additionalProperties: false,
269
+ },
270
+ ],
257
271
  },
258
272
  },
259
273
  sortableFields: {
@@ -405,4 +419,23 @@ export function validateConfig(config) {
405
419
  console.error('Config Errors', errors);
406
420
  throw new ConfigError(errors);
407
421
  }
422
+
423
+ // Custom validation: only one sortable field can have default_sort property
424
+ if (config.collections) {
425
+ config.collections.forEach((collection, index) => {
426
+ if (collection.sortable_fields) {
427
+ const defaultFields = collection.sortable_fields.filter(
428
+ field => typeof field === 'object' && field.default_sort !== undefined,
429
+ );
430
+ if (defaultFields.length > 1) {
431
+ const error = {
432
+ instancePath: `/collections/${index}/sortable_fields`,
433
+ message: 'only one sortable field can have the default_sort property',
434
+ };
435
+ console.error('Config Errors', [error]);
436
+ throw new ConfigError([error]);
437
+ }
438
+ }
439
+ });
440
+ }
408
441
  }
@@ -274,8 +274,8 @@ describe('formatters', () => {
274
274
  };
275
275
 
276
276
  describe('slugFormatter', () => {
277
- const date = new Date('2020-01-01');
278
- jest.spyOn(global, 'Date').mockImplementation(() => date);
277
+ const date = new Date('2020-01-01').valueOf();
278
+ Date.now = jest.spyOn(Date, 'now').mockImplementation(() => date);
279
279
 
280
280
  const { selectIdentifier } = require('../../reducers/collections');
281
281
 
@@ -312,6 +312,34 @@ describe('formatters', () => {
312
312
  ).toBe('entry-slug');
313
313
  });
314
314
 
315
+ it('should see date filters applied to date from entry if it exists', () => {
316
+ const { selectInferredField } = require('../../reducers/collections');
317
+ selectInferredField.mockReturnValue('date');
318
+ const entryDate = new Date('2026-10-20');
319
+
320
+ expect(
321
+ slugFormatter(
322
+ Map({ slug: '{{year}}-{{month}}-{{day}}-{{title}}' }),
323
+ Map({ date: entryDate, title: 'post title' }),
324
+ slugConfig,
325
+ ),
326
+ ).toBe('2026-10-20-post-title');
327
+ });
328
+
329
+ it('should see date filters applied to publishDate from entry if it exists', () => {
330
+ const { selectInferredField } = require('../../reducers/collections');
331
+ selectInferredField.mockReturnValue('publishDate');
332
+ const entryDate = new Date('2026-10-20');
333
+
334
+ expect(
335
+ slugFormatter(
336
+ Map({ slug: '{{year}}-{{month}}-{{day}}-{{title}}' }),
337
+ Map({ publishDate: entryDate, title: 'post title' }),
338
+ slugConfig,
339
+ ),
340
+ ).toBe('2026-10-20-post-title');
341
+ });
342
+
315
343
  it('should return slug', () => {
316
344
  selectIdentifier.mockReturnValueOnce('title');
317
345
 
@@ -21,6 +21,7 @@ import type { Map } from 'immutable';
21
21
  const {
22
22
  compileStringTemplate,
23
23
  parseDateFromEntry,
24
+ parseDateFromEntryData,
24
25
  SLUG_MISSING_REQUIRED_DATE,
25
26
  keyToPathArray,
26
27
  addFileTemplateFields,
@@ -129,7 +130,11 @@ export function slugFormatter(
129
130
  }
130
131
 
131
132
  const processSegment = getProcessSegment(slugConfig);
132
- const date = new Date();
133
+ const date =
134
+ parseDateFromEntryData(
135
+ entryData as unknown as Map<string, unknown>,
136
+ selectInferredField(collection, 'date'),
137
+ ) || new Date(Date.now());
133
138
  const slug = compileStringTemplate(slugTemplate, date, identifier, entryData, processSegment);
134
139
 
135
140
  if (!collection.has('path')) {
@@ -11,6 +11,7 @@ import collections, {
11
11
  getFieldsNames,
12
12
  selectField,
13
13
  updateFieldByKey,
14
+ selectInferredField,
14
15
  } from '../collections';
15
16
  import { FILES, FOLDER } from '../../constants/collectionTypes';
16
17
 
@@ -568,4 +569,42 @@ describe('collections', () => {
568
569
  );
569
570
  });
570
571
  });
572
+
573
+ describe("selectInferredField(collection, 'date')", () => {
574
+ it('should return publishDate if set', () => {
575
+ const collection = fromJS({
576
+ fields: [{ name: 'title' }, { name: 'publishDate', widget: 'datetime' }],
577
+ });
578
+
579
+ expect(selectInferredField(collection, 'date')).toEqual('publishDate');
580
+ });
581
+
582
+ it('should return publish_date if set', () => {
583
+ const collection = fromJS({
584
+ fields: [{ name: 'title' }, { name: 'publish_date', widget: 'datetime' }],
585
+ });
586
+
587
+ expect(selectInferredField(collection, 'date')).toEqual('publish_date');
588
+ });
589
+
590
+ it('should return date if set', () => {
591
+ const collection = fromJS({
592
+ fields: [{ name: 'title' }, { name: 'date', widget: 'datetime' }],
593
+ });
594
+
595
+ expect(selectInferredField(collection, 'date')).toEqual('date');
596
+ });
597
+
598
+ it('should return first date field if multiple synonyms are present', () => {
599
+ const collection = fromJS({
600
+ fields: [
601
+ { name: 'title' },
602
+ { name: 'publishDate', widget: 'datetime' },
603
+ { name: 'date', widget: 'datetime' },
604
+ ],
605
+ });
606
+
607
+ expect(selectInferredField(collection, 'date')).toEqual('publishDate');
608
+ });
609
+ });
571
610
  });
@@ -85,7 +85,7 @@ describe('entries', () => {
85
85
  ).toEqual('static/media');
86
86
  });
87
87
 
88
- it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
88
+ it('should return collection folder when collection specifies media_folder and entry is undefined', () => {
89
89
  expect(
90
90
  selectMediaFolder(
91
91
  { media_folder: 'static/media' },
@@ -93,7 +93,7 @@ describe('entries', () => {
93
93
  undefined,
94
94
  undefined,
95
95
  ),
96
- ).toEqual('posts/DRAFT_MEDIA_FILES');
96
+ ).toEqual('posts');
97
97
  });
98
98
 
99
99
  it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
@@ -364,7 +364,7 @@ describe('entries', () => {
364
364
  'image.png',
365
365
  undefined,
366
366
  ),
367
- ).toBe('posts/DRAFT_MEDIA_FILES/image.png');
367
+ ).toBe('posts/image.png');
368
368
  });
369
369
 
370
370
  it('should handle relative media_folder', () => {