decap-cms-core 3.12.0 → 3.14.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 (46) hide show
  1. package/dist/decap-cms-core.js +18 -18
  2. package/dist/decap-cms-core.js.map +1 -1
  3. package/dist/esm/actions/config.js +14 -1
  4. package/dist/esm/actions/entries.js +15 -4
  5. package/dist/esm/backend.js +2 -0
  6. package/dist/esm/bootstrap.js +2 -2
  7. package/dist/esm/components/App/App.js +12 -5
  8. package/dist/esm/components/App/Header.js +18 -18
  9. package/dist/esm/components/Collection/Entries/EntryCard.js +30 -15
  10. package/dist/esm/components/Collection/NestedCollection.js +20 -11
  11. package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +22 -6
  12. package/dist/esm/components/UI/ErrorBoundary.js +2 -2
  13. package/dist/esm/components/UI/SettingsDropdown.js +25 -27
  14. package/dist/esm/constants/configSchema.js +7 -1
  15. package/dist/esm/lib/formatters.js +6 -4
  16. package/dist/esm/lib/registry.js +4 -1
  17. package/dist/esm/lib/urlHelper.js +26 -8
  18. package/dist/esm/reducers/entryDraft.js +36 -3
  19. package/index.d.ts +18 -1
  20. package/package.json +2 -2
  21. package/src/__tests__/backend.spec.js +214 -0
  22. package/src/actions/__tests__/config.spec.js +14 -0
  23. package/src/actions/__tests__/entries.spec.js +36 -1
  24. package/src/actions/config.ts +13 -1
  25. package/src/actions/entries.ts +22 -7
  26. package/src/backend.ts +2 -0
  27. package/src/components/App/App.js +22 -13
  28. package/src/components/App/Header.js +36 -11
  29. package/src/components/Collection/Entries/EntryCard.js +13 -3
  30. package/src/components/Collection/NestedCollection.js +14 -7
  31. package/src/components/Collection/__tests__/NestedCollection.spec.js +1 -1
  32. package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +0 -68
  33. package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +6 -5
  34. package/src/components/Editor/__tests__/__snapshots__/EditorToolbar.spec.js.snap +104 -72
  35. package/src/components/UI/SettingsDropdown.js +36 -9
  36. package/src/constants/__tests__/configSchema.spec.js +9 -6
  37. package/src/constants/configSchema.js +3 -1
  38. package/src/lib/__tests__/formatters.spec.js +76 -4
  39. package/src/lib/__tests__/registry.spec.js +3 -3
  40. package/src/lib/__tests__/urlHelper.spec.js +7 -0
  41. package/src/lib/formatters.ts +15 -5
  42. package/src/lib/registry.js +4 -1
  43. package/src/lib/urlHelper.ts +33 -9
  44. package/src/reducers/__tests__/entryDraft.spec.js +117 -0
  45. package/src/reducers/entryDraft.js +43 -3
  46. package/src/types/redux.ts +22 -2
@@ -295,8 +295,8 @@ describe('formatters', () => {
295
295
  };
296
296
 
297
297
  describe('slugFormatter', () => {
298
- const date = new Date('2020-01-01').valueOf();
299
- Date.now = jest.spyOn(Date, 'now').mockImplementation(() => date);
298
+ const date = new Date('2020-01-01T13:28:27.679Z').valueOf();
299
+ jest.spyOn(Date, 'now').mockImplementation(() => date);
300
300
 
301
301
  const { selectIdentifier } = require('../../reducers/collections');
302
302
 
@@ -333,10 +333,22 @@ describe('formatters', () => {
333
333
  ).toBe('entry-slug');
334
334
  });
335
335
 
336
+ it('should allow filters in slug templates', () => {
337
+ selectIdentifier.mockReturnValueOnce('published');
338
+
339
+ expect(
340
+ slugFormatter(
341
+ Map({ slug: "{{published | date('MM-DD')}}" }),
342
+ Map({ title: 'Post Title', published: new Date(date) }),
343
+ slugConfig,
344
+ ),
345
+ ).toBe('01-01');
346
+ });
347
+
336
348
  it('should see date filters applied to date from entry if it exists', () => {
337
349
  const { selectInferredField } = require('../../reducers/collections');
338
350
  selectInferredField.mockReturnValue('date');
339
- const entryDate = new Date('2026-10-20');
351
+ const entryDate = new Date('2026-10-20T13:28:27.679Z');
340
352
 
341
353
  expect(
342
354
  slugFormatter(
@@ -350,7 +362,7 @@ describe('formatters', () => {
350
362
  it('should see date filters applied to publishDate from entry if it exists', () => {
351
363
  const { selectInferredField } = require('../../reducers/collections');
352
364
  selectInferredField.mockReturnValue('publishDate');
353
- const entryDate = new Date('2026-10-20');
365
+ const entryDate = new Date('2026-10-20T13:28:27.679Z');
354
366
 
355
367
  expect(
356
368
  slugFormatter(
@@ -587,6 +599,66 @@ describe('formatters', () => {
587
599
  'Collection "posts" configuration error:\n `preview_path_date_field` must be a field with a valid date. Ignoring `preview_path`.',
588
600
  );
589
601
  });
602
+
603
+ it('should preserve slashes in value when configured', () => {
604
+ expect(
605
+ previewUrlFormatter(
606
+ 'https://www.example.com',
607
+ Map({
608
+ preview_path: 'prefix/{{value}}',
609
+ preview_path_preserve_slashes: true,
610
+ }),
611
+ 'backendSlug',
612
+ Map({ data: Map({ value: 'nested/value' }) }),
613
+ slugConfig,
614
+ ),
615
+ ).toBe('https://www.example.com/prefix/nested/value');
616
+ });
617
+
618
+ it('should sanitize slashes in value when not configured', () => {
619
+ expect(
620
+ previewUrlFormatter(
621
+ 'https://www.example.com',
622
+ Map({
623
+ preview_path: 'prefix/{{value}}',
624
+ }),
625
+ 'backendSlug',
626
+ Map({ data: Map({ value: 'nested/value' }) }),
627
+ slugConfig,
628
+ ),
629
+ ).toBe('https://www.example.com/prefix/nested-value');
630
+ });
631
+
632
+ it('should preserve slashes in value for nested collections by default', () => {
633
+ expect(
634
+ previewUrlFormatter(
635
+ 'https://www.example.com',
636
+ Map({
637
+ preview_path: 'prefix/{{value}}',
638
+ nested: { depth: 10 },
639
+ }),
640
+ 'backendSlug',
641
+ Map({ data: Map({ value: 'nested/value' }) }),
642
+ slugConfig,
643
+ ),
644
+ ).toBe('https://www.example.com/prefix/nested/value');
645
+ });
646
+
647
+ it('should sanitize slashes in value for nested collections when explicitly disabled', () => {
648
+ expect(
649
+ previewUrlFormatter(
650
+ 'https://www.example.com',
651
+ Map({
652
+ preview_path: 'prefix/{{value}}',
653
+ nested: { depth: 10 },
654
+ preview_path_preserve_slashes: false,
655
+ }),
656
+ 'backendSlug',
657
+ Map({ data: Map({ value: 'nested/value' }) }),
658
+ slugConfig,
659
+ ),
660
+ ).toBe('https://www.example.com/prefix/nested-value');
661
+ });
590
662
  });
591
663
 
592
664
  describe('summaryFormatter', () => {
@@ -200,7 +200,7 @@ describe('registry', () => {
200
200
  });
201
201
  });
202
202
 
203
- it(`should return an updated entry's DataMap`, async () => {
203
+ it(`should return the complete updated entry object`, async () => {
204
204
  const { registerEventListener, invokeEvent } = require('../registry');
205
205
 
206
206
  const event = 'preSave';
@@ -233,7 +233,7 @@ describe('registry', () => {
233
233
  expect(handler1).toHaveBeenCalledWith(data, options);
234
234
  expect(handler2).toHaveBeenCalledWith(dataAfterFirstHandlerExecution, options);
235
235
 
236
- expect(result).toEqual(dataAfterSecondHandlerExecution.entry.get('data'));
236
+ expect(result).toEqual(dataAfterSecondHandlerExecution.entry);
237
237
  });
238
238
 
239
239
  it('should allow multiple events to not return a value', async () => {
@@ -254,7 +254,7 @@ describe('registry', () => {
254
254
 
255
255
  expect(handler1).toHaveBeenCalledWith(data, options);
256
256
  expect(handler2).toHaveBeenCalledWith(data, options);
257
- expect(result).toEqual(data.entry.get('data'));
257
+ expect(result).toEqual(data.entry);
258
258
  });
259
259
  });
260
260
  });
@@ -125,6 +125,13 @@ describe('sanitizeSlug', () => {
125
125
  'test_test',
126
126
  );
127
127
  });
128
+
129
+ it('preserves slashes when requested', () => {
130
+ const input = '/this-is-a/nested/page';
131
+
132
+ expect(sanitizeSlug(input, slugConfig, false)).toEqual('this-is-a-nested-page');
133
+ expect(sanitizeSlug(input, slugConfig, true)).toEqual('this-is-a/nested/page');
134
+ });
128
135
  });
129
136
 
130
137
  describe('sanitizeChar', () => {
@@ -120,11 +120,19 @@ export function prepareSlug(slug: string) {
120
120
  );
121
121
  }
122
122
 
123
- export function getProcessSegment(slugConfig?: CmsSlug, ignoreValues?: string[]) {
123
+ export function getProcessSegment(
124
+ slugConfig?: CmsSlug,
125
+ ignoreValues?: string[],
126
+ preserveSlashes?: boolean,
127
+ ) {
124
128
  return (value: string) =>
125
129
  ignoreValues && ignoreValues.includes(value)
126
130
  ? value
127
- : flow([value => String(value), prepareSlug, partialRight(sanitizeSlug, slugConfig)])(value);
131
+ : flow([
132
+ value => String(value),
133
+ prepareSlug,
134
+ partialRight(sanitizeSlug, slugConfig, preserveSlashes),
135
+ ])(value);
128
136
  }
129
137
 
130
138
  export function slugFormatter(
@@ -205,19 +213,21 @@ export function previewUrlFormatter(
205
213
  fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder'));
206
214
  const dateFieldName = getDateField() || selectInferredField(collection, 'date');
207
215
  const date = parseDateFromEntry(entry as unknown as Map<string, unknown>, dateFieldName);
216
+ const previewPathPreserveSlashes = collection.get('preview_path_preserve_slashes');
217
+ const preserveSlashes = !!(previewPathPreserveSlashes ?? collection.has('nested'));
208
218
 
209
219
  // Prepare and sanitize slug variables only, leave the rest of the
210
220
  // `preview_path` template as is.
211
- const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')]);
221
+ const processSegment = getProcessSegment(slugConfig, [fields.get('dirname')], preserveSlashes);
212
222
  let compiledPath;
213
223
 
214
224
  try {
215
225
  compiledPath = compileStringTemplate(pathTemplate, date, slug, fields, processSegment);
216
- } catch (err) {
226
+ } catch (err: unknown) {
217
227
  // Print an error and ignore `preview_path` if both:
218
228
  // 1. Date is invalid (according to DayJs), and
219
229
  // 2. A date expression (eg. `{{year}}`) is used in `preview_path`
220
- if (err.name === SLUG_MISSING_REQUIRED_DATE) {
230
+ if (err instanceof Error && err.name === SLUG_MISSING_REQUIRED_DATE) {
221
231
  console.error(stripIndent`
222
232
  Collection "${collection.get('name')}" configuration error:
223
233
  \`preview_path_date_field\` must be a field with a valid date. Ignoring \`preview_path\`.
@@ -257,7 +257,10 @@ export async function invokeEvent({ name, data }) {
257
257
  _data = { ...data, entry };
258
258
  }
259
259
  }
260
- return _data.entry.get('data');
260
+ // Return the full entry object with all metadata (slug, path, meta, etc.)
261
+ // rather than just the data payload. Callers like invokePreSaveEvent expect
262
+ // the complete entry object to be preserved through the event handler chain.
263
+ return _data.entry;
261
264
  }
262
265
 
263
266
  export function removeEventListener({ name, handler }) {
@@ -51,7 +51,14 @@ function validIRIChar(char: string) {
51
51
  return uriChars.test(char) || ucsChars.test(char);
52
52
  }
53
53
 
54
- export function getCharReplacer(encoding: string, replacement: string) {
54
+ export function getCharReplacer(
55
+ encoding: string,
56
+ options: {
57
+ replacement: NonNullable<CmsSlug['sanitize_replacement']>;
58
+ preserveSlashes?: boolean;
59
+ },
60
+ ) {
61
+ const { replacement, preserveSlashes } = options;
55
62
  let validChar: (char: string) => boolean;
56
63
 
57
64
  if (encoding === 'unicode') {
@@ -67,14 +74,24 @@ export function getCharReplacer(encoding: string, replacement: string) {
67
74
  throw new Error('The replacement character(s) (options.replacement) is itself unsafe.');
68
75
  }
69
76
 
70
- return (char: string) => (validChar(char) ? char : replacement);
77
+ return (char: string, i = 0, arr: string[] = [char]) => {
78
+ if (preserveSlashes && char === '/' && i !== 0 && i !== arr.length - 1) {
79
+ return char;
80
+ }
81
+
82
+ return validChar(char) ? char : replacement;
83
+ };
71
84
  }
72
85
  // `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed.
73
86
  export function sanitizeURI(
74
87
  str: string,
75
- options?: { replacement: CmsSlug['sanitize_replacement']; encoding: CmsSlug['encoding'] },
88
+ options?: {
89
+ replacement: CmsSlug['sanitize_replacement'];
90
+ encoding: CmsSlug['encoding'];
91
+ preserveSlashes?: boolean;
92
+ },
76
93
  ) {
77
- const { replacement = '', encoding = 'unicode' } = options || {};
94
+ const { replacement = '', encoding = 'unicode', preserveSlashes } = options || {};
78
95
 
79
96
  if (!isString(str)) {
80
97
  throw new Error('The input slug must be a string.');
@@ -85,15 +102,15 @@ export function sanitizeURI(
85
102
 
86
103
  // `Array.from` must be used instead of `String.split` because
87
104
  // `split` converts things like emojis into UTF-16 surrogate pairs.
88
- return Array.from(str).map(getCharReplacer(encoding, replacement)).join('');
105
+ return Array.from(str).map(getCharReplacer(encoding, { replacement, preserveSlashes })).join('');
89
106
  }
90
107
 
91
108
  export function sanitizeChar(char: string, options?: CmsSlug) {
92
109
  const { encoding = 'unicode', sanitize_replacement: replacement = '' } = options || {};
93
- return getCharReplacer(encoding, replacement)(char);
110
+ return getCharReplacer(encoding, { replacement })(char);
94
111
  }
95
112
 
96
- export function sanitizeSlug(str: string, options?: CmsSlug) {
113
+ export function sanitizeSlug(str: string, options?: CmsSlug, preserveSlashes?: boolean) {
97
114
  if (!isString(str)) {
98
115
  throw new Error('The input slug must be a string.');
99
116
  }
@@ -106,8 +123,15 @@ export function sanitizeSlug(str: string, options?: CmsSlug) {
106
123
 
107
124
  const sanitizedSlug = flow([
108
125
  ...(stripDiacritics ? [diacritics.remove] : []),
109
- partialRight(sanitizeURI, { replacement, encoding }),
110
- partialRight(sanitizeFilename, { replacement }),
126
+ partialRight(sanitizeURI, { replacement, encoding, preserveSlashes }),
127
+ preserveSlashes
128
+ ? (slug: string) =>
129
+ slug
130
+ .split('/')
131
+ .filter(Boolean)
132
+ .map(part => sanitizeFilename(part, { replacement }))
133
+ .join('/')
134
+ : partialRight(sanitizeFilename, { replacement }),
111
135
  ])(str);
112
136
 
113
137
  // Remove any doubled or leading/trailing replacement characters (that were added in the sanitizers).
@@ -195,4 +195,121 @@ describe('entryDraft reducer', () => {
195
195
  });
196
196
  });
197
197
  });
198
+
199
+ describe('selectCustomPath', () => {
200
+ let selectCustomPath;
201
+ let selectHasMetaPath;
202
+ let selectFolderEntryExtension;
203
+
204
+ beforeEach(() => {
205
+ jest.resetModules();
206
+ selectHasMetaPath = jest.fn(
207
+ collection => collection.has('meta') && collection.get('meta').has('path'),
208
+ );
209
+ selectFolderEntryExtension = jest.fn(collection => collection.get('extension') || 'md');
210
+
211
+ jest.doMock('../collections', () => ({
212
+ selectHasMetaPath,
213
+ selectFolderEntryExtension,
214
+ }));
215
+
216
+ const entryDraftModule = require('../entryDraft');
217
+ selectCustomPath = entryDraftModule.selectCustomPath;
218
+ });
219
+
220
+ afterEach(() => {
221
+ jest.unmock('../collections');
222
+ });
223
+
224
+ it('should generate dynamic filename for new entries without index_file', () => {
225
+ const collection = fromJS({
226
+ folder: '_pages',
227
+ extension: 'md',
228
+ meta: { path: { label: 'Path', widget: 'string' } },
229
+ });
230
+ const entryDraft = fromJS({
231
+ entry: {
232
+ newRecord: true,
233
+ data: { title: 'My Great Article' },
234
+ meta: { path: 'blog' },
235
+ },
236
+ });
237
+
238
+ const result = selectCustomPath(collection, entryDraft);
239
+ expect(result).toBe('_pages/blog/my-great-article.md');
240
+ });
241
+
242
+ it('should preserve filename for existing entries without index_file', () => {
243
+ const collection = fromJS({
244
+ folder: '_pages',
245
+ extension: 'md',
246
+ meta: { path: { label: 'Path', widget: 'string' } },
247
+ });
248
+ const entryDraft = fromJS({
249
+ entry: {
250
+ newRecord: false,
251
+ path: '_pages/old-folder/existing-file.md',
252
+ data: { title: 'Updated Title' },
253
+ meta: { path: 'new-folder' },
254
+ },
255
+ });
256
+
257
+ const result = selectCustomPath(collection, entryDraft);
258
+ expect(result).toBe('_pages/new-folder/existing-file.md');
259
+ });
260
+
261
+ it('should use index_file when specified (backward compatibility)', () => {
262
+ const collection = fromJS({
263
+ folder: '_pages',
264
+ extension: 'md',
265
+ meta: { path: { label: 'Path', widget: 'string', index_file: 'index' } },
266
+ });
267
+ const entryDraft = fromJS({
268
+ entry: {
269
+ newRecord: true,
270
+ data: { title: 'My Article' },
271
+ meta: { path: 'blog' },
272
+ },
273
+ });
274
+
275
+ const result = selectCustomPath(collection, entryDraft);
276
+ expect(result).toBe('_pages/blog/index.md');
277
+ });
278
+
279
+ it('should return undefined when path is not set', () => {
280
+ const collection = fromJS({
281
+ folder: '_pages',
282
+ extension: 'md',
283
+ meta: { path: { label: 'Path', widget: 'string' } },
284
+ });
285
+ const entryDraft = fromJS({
286
+ entry: {
287
+ newRecord: true,
288
+ data: { title: 'My Article' },
289
+ meta: {},
290
+ },
291
+ });
292
+
293
+ const result = selectCustomPath(collection, entryDraft);
294
+ expect(result).toBeUndefined();
295
+ });
296
+
297
+ it('should preserve non-latin characters in generated filename', () => {
298
+ const collection = fromJS({
299
+ folder: '_pages',
300
+ extension: 'md',
301
+ meta: { path: { label: 'Path', widget: 'string' } },
302
+ });
303
+ const entryDraft = fromJS({
304
+ entry: {
305
+ newRecord: true,
306
+ data: { title: '日本語のタイトル' },
307
+ meta: { path: 'blog' },
308
+ },
309
+ });
310
+
311
+ const result = selectCustomPath(collection, entryDraft);
312
+ expect(result).toBe('_pages/blog/日本語のタイトル.md');
313
+ });
314
+ });
198
315
  });
@@ -1,8 +1,9 @@
1
1
  import { Map, List, fromJS } from 'immutable';
2
2
  import { v4 as uuid } from 'uuid';
3
3
  import get from 'lodash/get';
4
- import { join } from 'path';
4
+ import { join, basename } from 'path';
5
5
 
6
+ import { sanitizeSlug } from '../lib/urlHelper';
6
7
  import {
7
8
  DRAFT_CREATE_FROM_ENTRY,
8
9
  DRAFT_CREATE_EMPTY,
@@ -204,15 +205,54 @@ function entryDraftReducer(state = Map(), action) {
204
205
  }
205
206
  }
206
207
 
208
+ function cleanTitleForFilename(title) {
209
+ if (!title) return 'untitled';
210
+
211
+ const cleanedTitle = sanitizeSlug(title.toString().toLowerCase().trim(), {
212
+ sanitize_replacement: '-',
213
+ encoding: 'unicode',
214
+ });
215
+
216
+ return cleanedTitle || 'untitled';
217
+ }
218
+
207
219
  export function selectCustomPath(collection, entryDraft) {
208
220
  if (!selectHasMetaPath(collection)) {
209
221
  return;
210
222
  }
211
223
  const meta = entryDraft.getIn(['entry', 'meta']);
212
224
  const path = meta && meta.get('path');
213
- const indexFile = get(collection.toJS(), ['meta', 'path', 'index_file']);
225
+
226
+ if (!path) {
227
+ return;
228
+ }
229
+
214
230
  const extension = selectFolderEntryExtension(collection);
215
- const customPath = path && join(collection.get('folder'), path, `${indexFile}.${extension}`);
231
+ const indexFile = get(collection.toJS(), ['meta', 'path', 'index_file']);
232
+
233
+ // If index_file is specified, use the old behavior for backward compatibility
234
+ if (indexFile) {
235
+ const customPath = join(collection.get('folder'), path, `${indexFile}.${extension}`);
236
+ return customPath;
237
+ }
238
+
239
+ // New behavior: generate filename from entry title
240
+ const isNewEntry = entryDraft.getIn(['entry', 'newRecord']);
241
+ const currentPath = entryDraft.getIn(['entry', 'path']);
242
+
243
+ let filename;
244
+ if (isNewEntry || !currentPath) {
245
+ // For new entries, generate filename from title
246
+ const entryData = entryDraft.getIn(['entry', 'data']);
247
+ const title = entryData && entryData.get('title');
248
+ filename = cleanTitleForFilename(title);
249
+ } else {
250
+ // For existing entries, preserve the current filename
251
+ const currentFilename = basename(currentPath, `.${extension}`);
252
+ filename = currentFilename;
253
+ }
254
+
255
+ const customPath = join(collection.get('folder'), path, `${filename}.${extension}`);
216
256
  return customPath;
217
257
  }
218
258
 
@@ -185,6 +185,21 @@ export interface CmsFieldMarkdown {
185
185
  editorComponents?: string[];
186
186
  }
187
187
 
188
+ export interface CmsFieldRichText {
189
+ widget: 'richtext';
190
+ default?: string;
191
+
192
+ minimal?: boolean;
193
+ buttons?: CmsMarkdownWidgetButton[];
194
+ editor_components?: string[];
195
+ modes?: ('raw' | 'rich_text')[];
196
+
197
+ /**
198
+ * @deprecated Use editor_components instead
199
+ */
200
+ editorComponents?: string[];
201
+ }
202
+
188
203
  export interface CmsFieldNumber {
189
204
  widget: 'number';
190
205
  default?: string | number;
@@ -258,7 +273,7 @@ export interface CmsFieldMeta {
258
273
  label: string;
259
274
  widget: string;
260
275
  required: boolean;
261
- index_file: string;
276
+ index_file?: string;
262
277
  meta: boolean;
263
278
  }
264
279
 
@@ -272,6 +287,7 @@ export type CmsField = CmsFieldBase &
272
287
  | CmsFieldList
273
288
  | CmsFieldMap
274
289
  | CmsFieldMarkdown
290
+ | CmsFieldRichText
275
291
  | CmsFieldNumber
276
292
  | CmsFieldObject
277
293
  | CmsFieldRelation
@@ -290,6 +306,7 @@ export interface CmsCollectionFile {
290
306
  description?: string;
291
307
  preview_path?: string;
292
308
  preview_path_date_field?: string;
309
+ preview_path_preserve_slashes?: boolean;
293
310
  i18n?: boolean | CmsI18nConfig;
294
311
  media_folder?: string;
295
312
  public_folder?: string;
@@ -327,6 +344,7 @@ export interface CmsCollection {
327
344
  slug?: string;
328
345
  preview_path?: string;
329
346
  preview_path_date_field?: string;
347
+ preview_path_preserve_slashes?: boolean;
330
348
  create?: boolean;
331
349
  delete?: boolean;
332
350
  editor?: {
@@ -338,7 +356,7 @@ export interface CmsCollection {
338
356
  depth: number;
339
357
  };
340
358
  type: typeof FOLDER | typeof FILES;
341
- meta?: { path?: { label: string; widget: string; index_file: string } };
359
+ meta?: { path?: { label: string; widget: string; index_file?: string } };
342
360
 
343
361
  /**
344
362
  * It accepts the following values: yml, yaml, toml, json, md, markdown, html
@@ -578,6 +596,7 @@ export type EntryField = StaticallyTypedRecord<{
578
596
  name: string;
579
597
  default: string | null | boolean | List<unknown>;
580
598
  media_folder?: string;
599
+ multiple?: boolean;
581
600
  public_folder?: string;
582
601
  comment?: string;
583
602
  meta?: boolean;
@@ -634,6 +653,7 @@ type CollectionObject = {
634
653
  public_folder?: string;
635
654
  preview_path?: string;
636
655
  preview_path_date_field?: string;
656
+ preview_path_preserve_slashes?: boolean;
637
657
  summary?: string;
638
658
  filter?: FilterRule;
639
659
  type: 'file_based_collection' | 'folder_based_collection';