decap-cms-core 3.13.0 → 3.15.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.
- package/dist/decap-cms-core.js +9 -9
- package/dist/decap-cms-core.js.map +1 -1
- package/dist/esm/actions/config.js +14 -1
- package/dist/esm/actions/entries.js +15 -4
- package/dist/esm/backend.js +2 -0
- package/dist/esm/bootstrap.js +2 -2
- package/dist/esm/components/App/App.js +15 -8
- package/dist/esm/components/App/Header.js +18 -18
- package/dist/esm/components/Collection/Collection.js +7 -7
- package/dist/esm/components/Collection/CollectionControls.js +1 -1
- package/dist/esm/components/Collection/CollectionTop.js +8 -8
- package/dist/esm/components/Collection/ControlButton.js +2 -2
- package/dist/esm/components/Collection/Entries/Entries.js +2 -2
- package/dist/esm/components/Collection/Entries/EntryCard.js +30 -15
- package/dist/esm/components/Collection/NestedCollection.js +20 -11
- package/dist/esm/components/Collection/Sidebar.js +6 -6
- package/dist/esm/components/Editor/EditorPreviewPane/EditorPreviewPane.js +22 -6
- package/dist/esm/components/MediaLibrary/MediaLibraryButtons.js +48 -9
- package/dist/esm/components/MediaLibrary/MediaLibraryCard.js +7 -9
- package/dist/esm/components/MediaLibrary/MediaLibraryCardGrid.js +5 -5
- package/dist/esm/components/MediaLibrary/MediaLibraryHeader.js +15 -3
- package/dist/esm/components/MediaLibrary/MediaLibraryModal.js +4 -1
- package/dist/esm/components/MediaLibrary/MediaLibrarySearch.js +6 -6
- package/dist/esm/components/MediaLibrary/MediaLibraryTop.js +14 -24
- package/dist/esm/components/UI/ErrorBoundary.js +2 -2
- package/dist/esm/components/UI/Modal.js +11 -4
- package/dist/esm/components/UI/SettingsDropdown.js +25 -27
- package/dist/esm/components/Workflow/Workflow.js +5 -5
- package/dist/esm/components/Workflow/WorkflowCard.js +14 -14
- package/dist/esm/components/Workflow/WorkflowList.js +18 -42
- package/dist/esm/constants/configSchema.js +2 -3
- package/dist/esm/lib/registry.js +4 -1
- package/dist/esm/reducers/entries.js +9 -7
- package/dist/esm/reducers/entryDraft.js +39 -7
- package/dist/esm/reducers/mediaLibrary.js +2 -3
- package/dist/esm/reducers/notifications.js +1 -2
- package/index.d.ts +19 -3
- package/package.json +7 -8
- package/src/__tests__/backend.spec.js +214 -0
- package/src/actions/__tests__/config.spec.js +14 -0
- package/src/actions/__tests__/editorialWorkflow.spec.js +1 -3
- package/src/actions/__tests__/entries.spec.js +36 -1
- package/src/actions/config.ts +13 -1
- package/src/actions/entries.ts +22 -7
- package/src/backend.ts +2 -0
- package/src/components/App/App.js +22 -14
- package/src/components/App/Header.js +36 -11
- package/src/components/Collection/Collection.js +10 -3
- package/src/components/Collection/CollectionControls.js +4 -3
- package/src/components/Collection/CollectionTop.js +3 -2
- package/src/components/Collection/ControlButton.js +1 -0
- package/src/components/Collection/Entries/Entries.js +1 -0
- package/src/components/Collection/Entries/EntryCard.js +13 -3
- package/src/components/Collection/NestedCollection.js +14 -7
- package/src/components/Collection/Sidebar.js +8 -7
- package/src/components/Collection/__tests__/NestedCollection.spec.js +1 -1
- package/src/components/Collection/__tests__/__snapshots__/Collection.spec.js.snap +76 -12
- package/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap +0 -68
- package/src/components/Collection/__tests__/__snapshots__/Sidebar.spec.js.snap +76 -16
- package/src/components/Editor/EditorPreviewPane/EditorPreviewPane.js +6 -5
- package/src/components/Editor/__tests__/__snapshots__/EditorToolbar.spec.js.snap +104 -72
- package/src/components/MediaLibrary/MediaLibraryButtons.js +39 -7
- package/src/components/MediaLibrary/MediaLibraryCard.js +1 -3
- package/src/components/MediaLibrary/MediaLibraryCardGrid.js +2 -2
- package/src/components/MediaLibrary/MediaLibraryHeader.js +32 -17
- package/src/components/MediaLibrary/MediaLibraryModal.js +8 -4
- package/src/components/MediaLibrary/MediaLibrarySearch.js +3 -1
- package/src/components/MediaLibrary/MediaLibraryTop.js +19 -1
- package/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +6 -6
- package/src/components/UI/Modal.js +10 -3
- package/src/components/UI/SettingsDropdown.js +36 -9
- package/src/components/Workflow/Workflow.js +7 -1
- package/src/components/Workflow/WorkflowCard.js +52 -14
- package/src/components/Workflow/WorkflowList.js +50 -31
- package/src/constants/__tests__/configSchema.spec.js +9 -6
- package/src/constants/configSchema.js +2 -3
- package/src/lib/__tests__/formatters.spec.js +16 -4
- package/src/lib/__tests__/registry.spec.js +3 -3
- package/src/lib/registry.js +4 -1
- package/src/reducers/__tests__/entryDraft.spec.js +118 -1
- package/src/reducers/__tests__/mediaLibrary.spec.js +0 -1
- package/src/reducers/entries.ts +9 -7
- package/src/reducers/entryDraft.js +46 -7
- package/src/reducers/mediaLibrary.ts +2 -3
- package/src/reducers/notifications.ts +1 -2
- package/src/types/redux.ts +22 -5
|
@@ -15,47 +15,57 @@ import { selectEntryCollectionTitle } from '../../reducers/collections';
|
|
|
15
15
|
const WorkflowListContainer = styled.div`
|
|
16
16
|
min-height: 60%;
|
|
17
17
|
display: grid;
|
|
18
|
-
grid-template-columns:
|
|
18
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
19
|
+
gap: 8px;
|
|
20
|
+
@media (min-width: 500px) {
|
|
21
|
+
gap: 14px;
|
|
22
|
+
}
|
|
23
|
+
@media (min-width: 800px) {
|
|
24
|
+
gap: 40px;
|
|
25
|
+
}
|
|
19
26
|
`;
|
|
20
27
|
|
|
21
28
|
const WorkflowListContainerOpenAuthoring = styled.div`
|
|
22
29
|
min-height: 60%;
|
|
23
30
|
display: grid;
|
|
24
|
-
grid-template-columns:
|
|
31
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
32
|
+
gap: 8px;
|
|
33
|
+
@media (min-width: 500px) {
|
|
34
|
+
gap: 14px;
|
|
35
|
+
}
|
|
36
|
+
@media (min-width: 800px) {
|
|
37
|
+
gap: 40px;
|
|
38
|
+
}
|
|
25
39
|
`;
|
|
26
40
|
|
|
27
41
|
const styles = {
|
|
28
42
|
columnPosition: idx =>
|
|
29
|
-
|
|
30
|
-
css`
|
|
31
|
-
margin-left: 0;
|
|
32
|
-
`) ||
|
|
33
|
-
(idx === 2 &&
|
|
34
|
-
css`
|
|
35
|
-
margin-right: 0;
|
|
36
|
-
`) ||
|
|
43
|
+
idx > 0 &&
|
|
37
44
|
css`
|
|
38
|
-
&:before
|
|
39
|
-
&:after {
|
|
45
|
+
&:before {
|
|
40
46
|
content: '';
|
|
41
47
|
display: block;
|
|
42
48
|
position: absolute;
|
|
43
49
|
width: 2px;
|
|
44
50
|
height: 80%;
|
|
45
|
-
top:
|
|
51
|
+
top: 36px;
|
|
46
52
|
background-color: ${colors.textFieldBorder};
|
|
53
|
+
@media (min-width: 800px) {
|
|
54
|
+
top: 76px;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
&:before {
|
|
50
|
-
left: -
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
left: -7px;
|
|
60
|
+
@media (min-width: 500px) {
|
|
61
|
+
left: -10px;
|
|
62
|
+
}
|
|
63
|
+
@media (min-width: 800px) {
|
|
64
|
+
left: -23px;
|
|
65
|
+
}
|
|
55
66
|
}
|
|
56
67
|
`,
|
|
57
68
|
column: css`
|
|
58
|
-
margin: 0 20px;
|
|
59
69
|
transition: background-color 0.5s ease;
|
|
60
70
|
border: 2px dashed transparent;
|
|
61
71
|
border-radius: 4px;
|
|
@@ -68,21 +78,21 @@ const styles = {
|
|
|
68
78
|
hiddenColumn: css`
|
|
69
79
|
display: none;
|
|
70
80
|
`,
|
|
71
|
-
hiddenRightBorder: css`
|
|
72
|
-
&:not(:first-child):not(:last-child) {
|
|
73
|
-
&:after {
|
|
74
|
-
display: none;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
`,
|
|
78
81
|
};
|
|
79
82
|
|
|
80
83
|
const ColumnHeader = styled.h2`
|
|
81
|
-
font-size:
|
|
84
|
+
font-size: 16px;
|
|
82
85
|
font-weight: normal;
|
|
83
|
-
padding:
|
|
86
|
+
padding: 2px 6px;
|
|
84
87
|
border-radius: ${lengths.borderRadius};
|
|
85
|
-
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
margin-bottom: 6px;
|
|
90
|
+
|
|
91
|
+
@media (min-width: 800px) {
|
|
92
|
+
font-size: 20px;
|
|
93
|
+
padding: 4px 12px;
|
|
94
|
+
margin-bottom: 28px;
|
|
95
|
+
}
|
|
86
96
|
|
|
87
97
|
${props =>
|
|
88
98
|
props.name === 'draft' &&
|
|
@@ -107,11 +117,21 @@ const ColumnHeader = styled.h2`
|
|
|
107
117
|
`;
|
|
108
118
|
|
|
109
119
|
const ColumnCount = styled.p`
|
|
110
|
-
font-size:
|
|
120
|
+
font-size: 11px;
|
|
111
121
|
font-weight: 500;
|
|
112
122
|
color: ${colors.text};
|
|
113
123
|
text-transform: uppercase;
|
|
114
124
|
margin-bottom: 6px;
|
|
125
|
+
padding-inline: 6px;
|
|
126
|
+
white-space: nowrap;
|
|
127
|
+
text-overflow: ellipsis;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
|
|
131
|
+
@media (min-width: 800px) {
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
padding-inline: 12px;
|
|
134
|
+
}
|
|
115
135
|
`;
|
|
116
136
|
|
|
117
137
|
// This is a namespace so that we can only drop these elements on a DropTarget with the same
|
|
@@ -188,7 +208,6 @@ class WorkflowList extends React.Component {
|
|
|
188
208
|
styles.columnPosition(idx),
|
|
189
209
|
isHovered && styles.columnHovered,
|
|
190
210
|
isOpenAuthoring && currColumn === 'pending_publish' && styles.hiddenColumn,
|
|
191
|
-
isOpenAuthoring && currColumn === 'pending_review' && styles.hiddenRightBorder,
|
|
192
211
|
]}
|
|
193
212
|
>
|
|
194
213
|
<ColumnHeader name={currColumn}>
|
|
@@ -477,22 +477,25 @@ describe('config', () => {
|
|
|
477
477
|
merge({}, validConfig, { collections: [{ meta: { path: { label: 'Label' } } }] }),
|
|
478
478
|
);
|
|
479
479
|
}).toThrowError("'collections[0].meta.path' must have required property 'widget'");
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should allow collection meta to have a path configuration with index_file', () => {
|
|
480
483
|
expect(() => {
|
|
481
484
|
validateConfig(
|
|
482
485
|
merge({}, validConfig, {
|
|
483
|
-
collections: [
|
|
486
|
+
collections: [
|
|
487
|
+
{ meta: { path: { label: 'Path', widget: 'string', index_file: 'index' } } },
|
|
488
|
+
],
|
|
484
489
|
}),
|
|
485
490
|
);
|
|
486
|
-
}).
|
|
491
|
+
}).not.toThrow();
|
|
487
492
|
});
|
|
488
493
|
|
|
489
|
-
it('should allow collection meta to have a path configuration', () => {
|
|
494
|
+
it('should allow collection meta to have a path configuration without index_file', () => {
|
|
490
495
|
expect(() => {
|
|
491
496
|
validateConfig(
|
|
492
497
|
merge({}, validConfig, {
|
|
493
|
-
collections: [
|
|
494
|
-
{ meta: { path: { label: 'Path', widget: 'string', index_file: 'index' } } },
|
|
495
|
-
],
|
|
498
|
+
collections: [{ meta: { path: { label: 'Path', widget: 'string' } } }],
|
|
496
499
|
}),
|
|
497
500
|
);
|
|
498
501
|
}).not.toThrow();
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
prohibited,
|
|
7
7
|
} from 'ajv-keywords/dist/keywords';
|
|
8
8
|
import ajvErrors from 'ajv-errors';
|
|
9
|
-
import { v4 as uuid } from 'uuid';
|
|
10
9
|
|
|
11
10
|
import { frontmatterFormats, extensionFormatters } from '../formats/formats';
|
|
12
11
|
import { getWidgets } from '../lib/registry';
|
|
@@ -45,7 +44,7 @@ const i18nField = {
|
|
|
45
44
|
* Config for fields in both file and folder collections.
|
|
46
45
|
*/
|
|
47
46
|
function fieldsConfig() {
|
|
48
|
-
const id =
|
|
47
|
+
const id = crypto.randomUUID();
|
|
49
48
|
return {
|
|
50
49
|
$id: `fields_${id}`,
|
|
51
50
|
type: 'array',
|
|
@@ -305,7 +304,7 @@ function getConfigSchema() {
|
|
|
305
304
|
widget: { type: 'string' },
|
|
306
305
|
index_file: { type: 'string' },
|
|
307
306
|
},
|
|
308
|
-
required: ['label', 'widget'
|
|
307
|
+
required: ['label', 'widget'],
|
|
309
308
|
},
|
|
310
309
|
},
|
|
311
310
|
additionalProperties: false,
|
|
@@ -295,8 +295,8 @@ describe('formatters', () => {
|
|
|
295
295
|
};
|
|
296
296
|
|
|
297
297
|
describe('slugFormatter', () => {
|
|
298
|
-
const date = new Date('2020-01-
|
|
299
|
-
|
|
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-
|
|
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-
|
|
365
|
+
const entryDate = new Date('2026-10-20T13:28:27.679Z');
|
|
354
366
|
|
|
355
367
|
expect(
|
|
356
368
|
slugFormatter(
|
|
@@ -200,7 +200,7 @@ describe('registry', () => {
|
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
it(`should return
|
|
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
|
|
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
|
|
257
|
+
expect(result).toEqual(data.entry);
|
|
258
258
|
});
|
|
259
259
|
});
|
|
260
260
|
});
|
package/src/lib/registry.js
CHANGED
|
@@ -257,7 +257,10 @@ export async function invokeEvent({ name, data }) {
|
|
|
257
257
|
_data = { ...data, entry };
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
|
|
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 }) {
|
|
@@ -3,7 +3,7 @@ import { Map, fromJS } from 'immutable';
|
|
|
3
3
|
import * as actions from '../../actions/entries';
|
|
4
4
|
import reducer from '../entryDraft';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
global.crypto.randomUUID = jest.fn(() => '1');
|
|
7
7
|
|
|
8
8
|
const initialState = Map({
|
|
9
9
|
entry: Map(),
|
|
@@ -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
|
});
|
package/src/reducers/entries.ts
CHANGED
|
@@ -474,14 +474,16 @@ function getGroup(entry: EntryMap, selectedGroup: GroupMap) {
|
|
|
474
474
|
if (selectedGroup.has('pattern')) {
|
|
475
475
|
const pattern = selectedGroup.get('pattern');
|
|
476
476
|
let value = '';
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
477
|
+
if (pattern !== undefined) {
|
|
478
|
+
try {
|
|
479
|
+
const regex = new RegExp(pattern);
|
|
480
|
+
const matched = dataAsString.match(regex);
|
|
481
|
+
if (matched) {
|
|
482
|
+
value = matched[0];
|
|
483
|
+
}
|
|
484
|
+
} catch (e) {
|
|
485
|
+
console.warn(`Invalid view group pattern '${pattern}' for field '${field}'`, e);
|
|
482
486
|
}
|
|
483
|
-
} catch (e) {
|
|
484
|
-
console.warn(`Invalid view group pattern '${pattern}' for field '${field}'`, e);
|
|
485
487
|
}
|
|
486
488
|
return {
|
|
487
489
|
id: `${label}${value}`,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Map, List, fromJS } from 'immutable';
|
|
2
|
-
import { v4 as uuid } from 'uuid';
|
|
3
2
|
import get from 'lodash/get';
|
|
4
|
-
import { join } from 'path';
|
|
3
|
+
import { join, basename } from 'path';
|
|
5
4
|
|
|
5
|
+
import { sanitizeSlug } from '../lib/urlHelper';
|
|
6
6
|
import {
|
|
7
7
|
DRAFT_CREATE_FROM_ENTRY,
|
|
8
8
|
DRAFT_CREATE_EMPTY,
|
|
@@ -52,7 +52,7 @@ function entryDraftReducer(state = Map(), action) {
|
|
|
52
52
|
state.set('fieldsMetaData', Map());
|
|
53
53
|
state.set('fieldsErrors', Map());
|
|
54
54
|
state.set('hasChanged', false);
|
|
55
|
-
state.set('key',
|
|
55
|
+
state.set('key', crypto.randomUUID());
|
|
56
56
|
});
|
|
57
57
|
case DRAFT_CREATE_EMPTY:
|
|
58
58
|
// New Entry
|
|
@@ -62,7 +62,7 @@ function entryDraftReducer(state = Map(), action) {
|
|
|
62
62
|
state.set('fieldsMetaData', Map());
|
|
63
63
|
state.set('fieldsErrors', Map());
|
|
64
64
|
state.set('hasChanged', false);
|
|
65
|
-
state.set('key',
|
|
65
|
+
state.set('key', crypto.randomUUID());
|
|
66
66
|
});
|
|
67
67
|
case DRAFT_CREATE_FROM_LOCAL_BACKUP:
|
|
68
68
|
// Local Backup
|
|
@@ -75,7 +75,7 @@ function entryDraftReducer(state = Map(), action) {
|
|
|
75
75
|
state.set('fieldsMetaData', Map());
|
|
76
76
|
state.set('fieldsErrors', Map());
|
|
77
77
|
state.set('hasChanged', true);
|
|
78
|
-
state.set('key',
|
|
78
|
+
state.set('key', crypto.randomUUID());
|
|
79
79
|
});
|
|
80
80
|
case DRAFT_CREATE_DUPLICATE_FROM_ENTRY:
|
|
81
81
|
// Duplicate Entry
|
|
@@ -204,15 +204,54 @@ function entryDraftReducer(state = Map(), action) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function cleanTitleForFilename(title) {
|
|
208
|
+
if (!title) return 'untitled';
|
|
209
|
+
|
|
210
|
+
const cleanedTitle = sanitizeSlug(title.toString().toLowerCase().trim(), {
|
|
211
|
+
sanitize_replacement: '-',
|
|
212
|
+
encoding: 'unicode',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return cleanedTitle || 'untitled';
|
|
216
|
+
}
|
|
217
|
+
|
|
207
218
|
export function selectCustomPath(collection, entryDraft) {
|
|
208
219
|
if (!selectHasMetaPath(collection)) {
|
|
209
220
|
return;
|
|
210
221
|
}
|
|
211
222
|
const meta = entryDraft.getIn(['entry', 'meta']);
|
|
212
223
|
const path = meta && meta.get('path');
|
|
213
|
-
|
|
224
|
+
|
|
225
|
+
if (!path) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
214
229
|
const extension = selectFolderEntryExtension(collection);
|
|
215
|
-
const
|
|
230
|
+
const indexFile = get(collection.toJS(), ['meta', 'path', 'index_file']);
|
|
231
|
+
|
|
232
|
+
// If index_file is specified, use the old behavior for backward compatibility
|
|
233
|
+
if (indexFile) {
|
|
234
|
+
const customPath = join(collection.get('folder'), path, `${indexFile}.${extension}`);
|
|
235
|
+
return customPath;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// New behavior: generate filename from entry title
|
|
239
|
+
const isNewEntry = entryDraft.getIn(['entry', 'newRecord']);
|
|
240
|
+
const currentPath = entryDraft.getIn(['entry', 'path']);
|
|
241
|
+
|
|
242
|
+
let filename;
|
|
243
|
+
if (isNewEntry || !currentPath) {
|
|
244
|
+
// For new entries, generate filename from title
|
|
245
|
+
const entryData = entryDraft.getIn(['entry', 'data']);
|
|
246
|
+
const title = entryData && entryData.get('title');
|
|
247
|
+
filename = cleanTitleForFilename(title);
|
|
248
|
+
} else {
|
|
249
|
+
// For existing entries, preserve the current filename
|
|
250
|
+
const currentFilename = basename(currentPath, `.${extension}`);
|
|
251
|
+
filename = currentFilename;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const customPath = join(collection.get('folder'), path, `${filename}.${extension}`);
|
|
216
255
|
return customPath;
|
|
217
256
|
}
|
|
218
257
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Map, List } from 'immutable';
|
|
2
|
-
import { v4 as uuid } from 'uuid';
|
|
3
2
|
import { dirname } from 'path';
|
|
4
3
|
|
|
5
4
|
import {
|
|
@@ -150,7 +149,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
|
|
|
150
149
|
return state;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
const filesWithKeys = files.map(file => ({ ...file, key:
|
|
152
|
+
const filesWithKeys = files.map(file => ({ ...file, key: crypto.randomUUID() }));
|
|
154
153
|
return state.withMutations(map => {
|
|
155
154
|
map.set('isLoading', false);
|
|
156
155
|
map.set('isPaginating', false);
|
|
@@ -186,7 +185,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) {
|
|
|
186
185
|
return state;
|
|
187
186
|
}
|
|
188
187
|
return state.withMutations(map => {
|
|
189
|
-
const fileWithKey = { ...file, key:
|
|
188
|
+
const fileWithKey = { ...file, key: crypto.randomUUID() };
|
|
190
189
|
const files = map.get('files') as MediaFile[];
|
|
191
190
|
const updatedFiles = [fileWithKey, ...files];
|
|
192
191
|
map.set('files', updatedFiles);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { produce } from 'immer';
|
|
2
|
-
import { v4 as uuid } from 'uuid';
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
NOTIFICATION_SEND,
|
|
@@ -41,7 +40,7 @@ const notifications = produce((state: NotificationsState, action: NotificationsA
|
|
|
41
40
|
state.notifications = [
|
|
42
41
|
...state.notifications,
|
|
43
42
|
{
|
|
44
|
-
id:
|
|
43
|
+
id: crypto.randomUUID(),
|
|
45
44
|
...(action.payload as NotificationPayload),
|
|
46
45
|
},
|
|
47
46
|
];
|
package/src/types/redux.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface CmsFieldBase {
|
|
|
72
72
|
label?: string;
|
|
73
73
|
required?: boolean;
|
|
74
74
|
hint?: string;
|
|
75
|
-
pattern?: [string, string];
|
|
75
|
+
pattern?: [string | RegExp, string];
|
|
76
76
|
i18n?: boolean | 'translate' | 'duplicate' | 'none';
|
|
77
77
|
media_folder?: string;
|
|
78
78
|
public_folder?: string;
|
|
@@ -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
|
|
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
|
|
@@ -299,14 +315,14 @@ export interface CmsCollectionFile {
|
|
|
299
315
|
export interface ViewFilter {
|
|
300
316
|
label: string;
|
|
301
317
|
field: string;
|
|
302
|
-
pattern: string;
|
|
318
|
+
pattern: string | boolean;
|
|
303
319
|
id: string;
|
|
304
320
|
}
|
|
305
321
|
|
|
306
322
|
export interface ViewGroup {
|
|
307
323
|
label: string;
|
|
308
324
|
field: string;
|
|
309
|
-
pattern
|
|
325
|
+
pattern?: string;
|
|
310
326
|
id: string;
|
|
311
327
|
}
|
|
312
328
|
|
|
@@ -340,7 +356,7 @@ export interface CmsCollection {
|
|
|
340
356
|
depth: number;
|
|
341
357
|
};
|
|
342
358
|
type: typeof FOLDER | typeof FILES;
|
|
343
|
-
meta?: { path?: { label: string; widget: string; index_file
|
|
359
|
+
meta?: { path?: { label: string; widget: string; index_file?: string } };
|
|
344
360
|
|
|
345
361
|
/**
|
|
346
362
|
* It accepts the following values: yml, yaml, toml, json, md, markdown, html
|
|
@@ -580,6 +596,7 @@ export type EntryField = StaticallyTypedRecord<{
|
|
|
580
596
|
name: string;
|
|
581
597
|
default: string | null | boolean | List<unknown>;
|
|
582
598
|
media_folder?: string;
|
|
599
|
+
multiple?: boolean;
|
|
583
600
|
public_folder?: string;
|
|
584
601
|
comment?: string;
|
|
585
602
|
meta?: boolean;
|