decap-cms-core 3.9.0 → 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.
- package/dist/decap-cms-core.js +23 -23
- package/dist/decap-cms-core.js.LICENSE.txt +10 -0
- package/dist/decap-cms-core.js.map +1 -1
- package/dist/esm/actions/config.js +19 -0
- package/dist/esm/actions/entries.js +10 -1
- package/dist/esm/bootstrap.js +2 -2
- package/dist/esm/components/App/StatusBar.js +41 -0
- package/dist/esm/components/Collection/Entries/Pagination.js +132 -0
- package/dist/esm/components/Editor/EditorNotesPane/AddNoteForm.js +17 -14
- package/dist/esm/components/UI/ErrorBoundary.js +6 -9
- package/dist/esm/constants/configSchema.js +41 -23
- package/dist/esm/lib/entryCache.js +145 -0
- package/dist/esm/lib/entryHelpers.js +102 -0
- package/dist/esm/lib/formatters.js +2 -1
- package/dist/esm/lib/immutableHelpers.js +21 -0
- package/dist/esm/lib/indexFileHelper.js +36 -0
- package/dist/esm/lib/pagination.js +68 -0
- package/dist/esm/reducers/collections.js +54 -5
- package/dist/esm/reducers/entries.js +8 -2
- package/index.d.ts +8 -2
- package/package.json +2 -3
- package/src/actions/__tests__/config.spec.js +4 -4
- package/src/actions/config.ts +22 -0
- package/src/actions/entries.ts +11 -1
- package/src/components/UI/ErrorBoundary.js +1 -2
- package/src/constants/__tests__/configSchema.spec.js +84 -0
- package/src/constants/configSchema.js +34 -1
- package/src/lib/__tests__/formatters.spec.js +30 -2
- package/src/lib/formatters.ts +6 -1
- package/src/reducers/__tests__/collections.spec.js +39 -0
- package/src/reducers/collections.ts +52 -5
- package/src/reducers/entries.ts +12 -2
- package/src/types/redux.ts +9 -3
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
function _extendableBuiltin(cls) {
|
|
2
|
-
function ExtendableBuiltin() {
|
|
3
|
-
var instance = Reflect.construct(cls, Array.from(arguments));
|
|
4
|
-
Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
|
|
5
|
-
return instance;
|
|
6
|
-
}
|
|
7
|
-
ExtendableBuiltin.prototype = Object.create(cls.prototype, {
|
|
8
|
-
constructor: {
|
|
9
|
-
value: cls,
|
|
10
|
-
enumerable: false,
|
|
11
|
-
writable: true,
|
|
12
|
-
configurable: true
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
if (Object.setPrototypeOf) {
|
|
16
|
-
Object.setPrototypeOf(ExtendableBuiltin, cls);
|
|
17
|
-
} else {
|
|
18
|
-
ExtendableBuiltin.__proto__ = cls;
|
|
19
|
-
}
|
|
20
|
-
return ExtendableBuiltin;
|
|
21
|
-
}
|
|
22
1
|
import AJV from 'ajv';
|
|
23
2
|
import { select, uniqueItemProperties, instanceof as instanceOf, prohibited } from 'ajv-keywords/dist/keywords';
|
|
24
3
|
import ajvErrors from 'ajv-errors';
|
|
@@ -410,7 +389,29 @@ function getConfigSchema() {
|
|
|
410
389
|
sortable_fields: {
|
|
411
390
|
type: 'array',
|
|
412
391
|
items: {
|
|
413
|
-
|
|
392
|
+
oneOf: [{
|
|
393
|
+
type: 'string'
|
|
394
|
+
}, {
|
|
395
|
+
type: 'object',
|
|
396
|
+
properties: {
|
|
397
|
+
field: {
|
|
398
|
+
type: 'string'
|
|
399
|
+
},
|
|
400
|
+
label: {
|
|
401
|
+
type: 'string'
|
|
402
|
+
},
|
|
403
|
+
default_sort: {
|
|
404
|
+
oneOf: [{
|
|
405
|
+
type: 'boolean'
|
|
406
|
+
}, {
|
|
407
|
+
type: 'string',
|
|
408
|
+
enum: ['asc', 'desc']
|
|
409
|
+
}]
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
required: ['field'],
|
|
413
|
+
additionalProperties: false
|
|
414
|
+
}]
|
|
414
415
|
}
|
|
415
416
|
},
|
|
416
417
|
sortableFields: {
|
|
@@ -523,7 +524,7 @@ function getWidgetSchemas() {
|
|
|
523
524
|
}));
|
|
524
525
|
return Object.assign(...schemas);
|
|
525
526
|
}
|
|
526
|
-
class ConfigError extends
|
|
527
|
+
class ConfigError extends Error {
|
|
527
528
|
constructor(errors, ...args) {
|
|
528
529
|
const message = errors.map(({
|
|
529
530
|
message,
|
|
@@ -602,4 +603,21 @@ export function validateConfig(config) {
|
|
|
602
603
|
console.error('Config Errors', errors);
|
|
603
604
|
throw new ConfigError(errors);
|
|
604
605
|
}
|
|
606
|
+
|
|
607
|
+
// Custom validation: only one sortable field can have default_sort property
|
|
608
|
+
if (config.collections) {
|
|
609
|
+
config.collections.forEach((collection, index) => {
|
|
610
|
+
if (collection.sortable_fields) {
|
|
611
|
+
const defaultFields = collection.sortable_fields.filter(field => typeof field === 'object' && field.default_sort !== undefined);
|
|
612
|
+
if (defaultFields.length > 1) {
|
|
613
|
+
const error = {
|
|
614
|
+
instancePath: `/collections/${index}/sortable_fields`,
|
|
615
|
+
message: 'only one sortable field can have the default_sort property'
|
|
616
|
+
};
|
|
617
|
+
console.error('Config Errors', [error]);
|
|
618
|
+
throw new ConfigError([error]);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
605
623
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry Cache Module
|
|
3
|
+
*
|
|
4
|
+
* Provides localStorage-based caching for collection entries to improve performance
|
|
5
|
+
* when navigating between pages, sorting, or filtering.
|
|
6
|
+
*
|
|
7
|
+
* Cache Strategy:
|
|
8
|
+
* - Store fetched entries with timestamps
|
|
9
|
+
* - Invalidate on entry changes (persist, delete)
|
|
10
|
+
* - Time-based expiration (default: 5 minutes)
|
|
11
|
+
* - Collection-specific cache keys
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import localForage from 'decap-cms-lib-util/src/localForage';
|
|
15
|
+
const CACHE_PREFIX = 'decap_entry_cache_';
|
|
16
|
+
const CACHE_EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate cache key for a collection
|
|
20
|
+
*/
|
|
21
|
+
function getCacheKey(collectionName) {
|
|
22
|
+
return `${CACHE_PREFIX}${collectionName}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if cached data is still valid
|
|
27
|
+
*/
|
|
28
|
+
function isCacheValid(cacheEntry) {
|
|
29
|
+
if (!cacheEntry) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const age = now - cacheEntry.timestamp;
|
|
34
|
+
return age < CACHE_EXPIRATION_MS;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get cached entries for a collection
|
|
39
|
+
*
|
|
40
|
+
* @param collectionName - Name of the collection
|
|
41
|
+
* @returns Cached entries or null if cache miss/expired
|
|
42
|
+
*/
|
|
43
|
+
export async function getCachedEntries(collectionName) {
|
|
44
|
+
try {
|
|
45
|
+
const cacheKey = getCacheKey(collectionName);
|
|
46
|
+
const cached = await localForage.getItem(cacheKey);
|
|
47
|
+
if (cached && isCacheValid(cached)) {
|
|
48
|
+
console.log(`[EntryCache] Cache HIT for collection: ${collectionName}`);
|
|
49
|
+
return cached.entries;
|
|
50
|
+
}
|
|
51
|
+
console.log(`[EntryCache] Cache MISS for collection: ${collectionName}`);
|
|
52
|
+
return null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn('[EntryCache] Error reading cache:', error);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Store entries in cache
|
|
61
|
+
*
|
|
62
|
+
* @param collectionName - Name of the collection
|
|
63
|
+
* @param entries - Entries to cache
|
|
64
|
+
*/
|
|
65
|
+
export async function setCachedEntries(collectionName, entries) {
|
|
66
|
+
try {
|
|
67
|
+
const cacheKey = getCacheKey(collectionName);
|
|
68
|
+
const cacheEntry = {
|
|
69
|
+
entries,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
collectionName,
|
|
72
|
+
version: 1
|
|
73
|
+
};
|
|
74
|
+
await localForage.setItem(cacheKey, cacheEntry);
|
|
75
|
+
console.log(`[EntryCache] Cached ${entries.length} entries for collection: ${collectionName}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn('[EntryCache] Error writing cache:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Invalidate cache for a specific collection
|
|
83
|
+
*
|
|
84
|
+
* Should be called when:
|
|
85
|
+
* - Entry is created
|
|
86
|
+
* - Entry is updated
|
|
87
|
+
* - Entry is deleted
|
|
88
|
+
*
|
|
89
|
+
* @param collectionName - Name of the collection to invalidate
|
|
90
|
+
*/
|
|
91
|
+
export async function invalidateCollectionCache(collectionName) {
|
|
92
|
+
try {
|
|
93
|
+
const cacheKey = getCacheKey(collectionName);
|
|
94
|
+
await localForage.removeItem(cacheKey);
|
|
95
|
+
console.log(`[EntryCache] Invalidated cache for collection: ${collectionName}`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('[EntryCache] Error invalidating cache:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clear all entry caches
|
|
103
|
+
*
|
|
104
|
+
* Useful for logout or manual cache clearing
|
|
105
|
+
*/
|
|
106
|
+
export async function clearAllEntryCaches() {
|
|
107
|
+
try {
|
|
108
|
+
const keys = await localForage.keys();
|
|
109
|
+
const cacheKeys = keys.filter(key => key.startsWith(CACHE_PREFIX));
|
|
110
|
+
await Promise.all(cacheKeys.map(key => localForage.removeItem(key)));
|
|
111
|
+
console.log(`[EntryCache] Cleared ${cacheKeys.length} collection caches`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn('[EntryCache] Error clearing all caches:', error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get cache statistics for debugging
|
|
119
|
+
*/
|
|
120
|
+
export async function getCacheStats() {
|
|
121
|
+
try {
|
|
122
|
+
const keys = await localForage.keys();
|
|
123
|
+
const cacheKeys = keys.filter(key => key.startsWith(CACHE_PREFIX));
|
|
124
|
+
const cacheEntries = await Promise.all(cacheKeys.map(async key => {
|
|
125
|
+
const entry = await localForage.getItem(key);
|
|
126
|
+
return entry;
|
|
127
|
+
}));
|
|
128
|
+
const validCaches = cacheEntries.filter(entry => entry !== null);
|
|
129
|
+
const timestamps = validCaches.map(c => c.timestamp);
|
|
130
|
+
return {
|
|
131
|
+
collections: validCaches.map(c => c.collectionName),
|
|
132
|
+
totalEntries: validCaches.reduce((sum, c) => sum + c.entries.length, 0),
|
|
133
|
+
oldestCache: timestamps.length > 0 ? Math.min(...timestamps) : null,
|
|
134
|
+
newestCache: timestamps.length > 0 ? Math.max(...timestamps) : null
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn('[EntryCache] Error getting cache stats:', error);
|
|
138
|
+
return {
|
|
139
|
+
collections: [],
|
|
140
|
+
totalEntries: 0,
|
|
141
|
+
oldestCache: null,
|
|
142
|
+
newestCache: null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for working with entries, filters, sorts, and groups
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if any filters are active in the Redux state
|
|
7
|
+
*/
|
|
8
|
+
export function hasActiveFilters(activeFilters) {
|
|
9
|
+
if (!activeFilters) return false;
|
|
10
|
+
|
|
11
|
+
// Check if it's an Immutable collection with a 'some' method
|
|
12
|
+
if (typeof activeFilters === 'object' &&
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
typeof activeFilters.some === 'function') {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
return activeFilters.some(f => f.get('active') === true);
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if any groups are active in the Redux state
|
|
23
|
+
*/
|
|
24
|
+
export function hasActiveGroups(activeGroups) {
|
|
25
|
+
if (!activeGroups) return false;
|
|
26
|
+
|
|
27
|
+
// Check if it's an Immutable collection with a 'some' method
|
|
28
|
+
if (typeof activeGroups === 'object' &&
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
typeof activeGroups.some === 'function') {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
return activeGroups.some(g => g.get('active') === true);
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if any sorts are active in the Redux state
|
|
39
|
+
*/
|
|
40
|
+
export function hasActiveSorts(activeSorts) {
|
|
41
|
+
if (!activeSorts) return false;
|
|
42
|
+
|
|
43
|
+
// Check if it's an Immutable collection with a 'size' property
|
|
44
|
+
if (typeof activeSorts === 'object' &&
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
typeof activeSorts.size === 'number') {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
return activeSorts.size > 0;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get value from a nested field path (e.g., "data.title" or "data.nested.field")
|
|
55
|
+
*/
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
export function getFieldValue(obj, fieldPath) {
|
|
58
|
+
const pathParts = fieldPath.split('.');
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
let value = obj;
|
|
61
|
+
for (const part of pathParts) {
|
|
62
|
+
value = value?.[part];
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract active filters from Immutable collection into plain array
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
export function extractActiveFilters(activeFilters) {
|
|
72
|
+
const filters = [];
|
|
73
|
+
if (!activeFilters) return filters;
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
if (typeof activeFilters.forEach === 'function') {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
activeFilters.forEach(f => {
|
|
79
|
+
if (f.get('active') === true) {
|
|
80
|
+
filters.push({
|
|
81
|
+
pattern: f.get('pattern'),
|
|
82
|
+
field: f.get('field')
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return filters;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Apply filters to an entry
|
|
92
|
+
*/
|
|
93
|
+
export function matchesFilters(entry, filters) {
|
|
94
|
+
return filters.every(({
|
|
95
|
+
pattern,
|
|
96
|
+
field
|
|
97
|
+
}) => {
|
|
98
|
+
const data = ('data' in entry ? entry.data : entry) || {};
|
|
99
|
+
const value = getFieldValue(data, field);
|
|
100
|
+
return value !== undefined && new RegExp(String(pattern)).test(String(value));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -11,6 +11,7 @@ import { COMMIT_AUTHOR, COMMIT_DATE } from '../constants/commitProps';
|
|
|
11
11
|
const {
|
|
12
12
|
compileStringTemplate,
|
|
13
13
|
parseDateFromEntry,
|
|
14
|
+
parseDateFromEntryData,
|
|
14
15
|
SLUG_MISSING_REQUIRED_DATE,
|
|
15
16
|
keyToPathArray,
|
|
16
17
|
addFileTemplateFields
|
|
@@ -91,7 +92,7 @@ export function slugFormatter(collection, entryData, slugConfig) {
|
|
|
91
92
|
throw new Error('Collection must have a field name that is a valid entry identifier, or must have `identifier_field` set');
|
|
92
93
|
}
|
|
93
94
|
const processSegment = getProcessSegment(slugConfig);
|
|
94
|
-
const date = new Date();
|
|
95
|
+
const date = parseDateFromEntryData(entryData, selectInferredField(collection, 'date')) || new Date(Date.now());
|
|
95
96
|
const slug = compileStringTemplate(slugTemplate, date, identifier, entryData, processSegment);
|
|
96
97
|
if (!collection.has('path')) {
|
|
97
98
|
return slug;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Map } from 'immutable';
|
|
2
|
+
/**
|
|
3
|
+
* Type guard to check if an object is an Immutable.js Map
|
|
4
|
+
*/
|
|
5
|
+
export function isImmutableMap(obj) {
|
|
6
|
+
return Map.isMap(obj);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper to safely get a value from either an Immutable Map or plain object
|
|
11
|
+
*/
|
|
12
|
+
export function getValue(obj, key) {
|
|
13
|
+
if (!obj) return undefined;
|
|
14
|
+
if (isImmutableMap(obj)) {
|
|
15
|
+
return obj.get(key);
|
|
16
|
+
}
|
|
17
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
18
|
+
return obj[key];
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines if a file slug matches the index file pattern for a collection.
|
|
3
|
+
* @param filePath - The file slug or path to check
|
|
4
|
+
* @param pattern - The regex pattern from the collection's index_file config
|
|
5
|
+
* @param nested - Whether the collection is nested (affects how the slug is extracted)
|
|
6
|
+
* @returns True if the file matches the index file pattern
|
|
7
|
+
*/
|
|
8
|
+
export function isIndexFile(filePath, pattern, nested) {
|
|
9
|
+
const fileSlug = nested ? filePath?.split('/').pop() : filePath;
|
|
10
|
+
return !!(fileSlug && new RegExp(pattern).test(fileSlug));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Determines if an entry is an index file entry.
|
|
15
|
+
* Checks both the meta.path_type field (set by the backend) and the slug pattern.
|
|
16
|
+
* @param entry - The entry to check
|
|
17
|
+
* @param collection - The collection configuration containing index_file pattern
|
|
18
|
+
* @returns True if the entry is an index file
|
|
19
|
+
*/
|
|
20
|
+
export function isIndexFileEntry(entry, collection) {
|
|
21
|
+
if (!entry) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const pathType = entry.getIn(['meta', 'path_type']);
|
|
25
|
+
if (pathType === 'index') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const indexFileConfig = collection.get('index_file');
|
|
29
|
+
if (!indexFileConfig) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const slug = entry.get('slug');
|
|
33
|
+
const pattern = indexFileConfig.get('pattern');
|
|
34
|
+
const nested = !!collection.get('nested');
|
|
35
|
+
return isIndexFile(slug, pattern, nested);
|
|
36
|
+
}
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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:
|
|
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) {
|
|
@@ -544,10 +550,10 @@ export function selectMediaFilePublicPath(config, collection, mediaPath, entryMa
|
|
|
544
550
|
return mediaPath;
|
|
545
551
|
}
|
|
546
552
|
const name = 'public_folder';
|
|
547
|
-
let publicFolder = config[name];
|
|
553
|
+
let publicFolder = normalizeDoubleSlashes(config[name]);
|
|
548
554
|
const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field);
|
|
549
555
|
if (customFolder) {
|
|
550
|
-
publicFolder = evaluateFolder(name, config, collection, entryMap, field);
|
|
556
|
+
publicFolder = normalizeDoubleSlashes(evaluateFolder(name, config, collection, entryMap, field));
|
|
551
557
|
}
|
|
552
558
|
if (isAbsolutePath(publicFolder)) {
|
|
553
559
|
return joinUrlPath(publicFolder, basename(mediaPath));
|
package/index.d.ts
CHANGED
|
@@ -292,6 +292,12 @@ declare module 'decap-cms-core' {
|
|
|
292
292
|
pattern?: string;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
export interface SortableField {
|
|
296
|
+
field: string;
|
|
297
|
+
label?: string;
|
|
298
|
+
default_sort?: boolean | 'asc' | 'desc';
|
|
299
|
+
}
|
|
300
|
+
|
|
295
301
|
export interface CmsCollection {
|
|
296
302
|
name: string;
|
|
297
303
|
label: string;
|
|
@@ -332,7 +338,7 @@ declare module 'decap-cms-core' {
|
|
|
332
338
|
path?: string;
|
|
333
339
|
media_folder?: string;
|
|
334
340
|
public_folder?: string;
|
|
335
|
-
sortable_fields?: string[];
|
|
341
|
+
sortable_fields?: (string | SortableField)[];
|
|
336
342
|
view_filters?: ViewFilter[];
|
|
337
343
|
view_groups?: ViewGroup[];
|
|
338
344
|
i18n?: boolean | CmsI18nConfig;
|
|
@@ -340,7 +346,7 @@ declare module 'decap-cms-core' {
|
|
|
340
346
|
/**
|
|
341
347
|
* @deprecated Use sortable_fields instead
|
|
342
348
|
*/
|
|
343
|
-
sortableFields?: string[];
|
|
349
|
+
sortableFields?: (string | SortableField)[];
|
|
344
350
|
}
|
|
345
351
|
|
|
346
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.
|
|
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": "
|
|
102
|
+
"gitHead": "0c1b7c7d63ba6ddb3de3692fbb6b16ac6ebb44d5"
|
|
104
103
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stripIndent } from 'common-tags';
|
|
2
|
-
import {
|
|
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(
|
|
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(
|
|
965
|
+
text: () => Promise.resolve(stringify({ backend: { repo: 'github' } })),
|
|
966
966
|
headers: new Headers(),
|
|
967
967
|
});
|
|
968
968
|
await loadConfig()(dispatch);
|