diff --git a/packages/decap-cms-backend-gitlab/src/__tests__/gitlab.spec.js b/packages/decap-cms-backend-gitlab/src/__tests__/gitlab.spec.js index 4953ac3e4a0d..8df225359b10 100644 --- a/packages/decap-cms-backend-gitlab/src/__tests__/gitlab.spec.js +++ b/packages/decap-cms-backend-gitlab/src/__tests__/gitlab.spec.js @@ -432,6 +432,7 @@ describe('gitlab backend', () => { expect(entries).toEqual({ cursor: expect.any(Cursor), pagination: 1, + errors: [], entries: expect.arrayContaining( tree.map(file => expect.objectContaining({ path: file.path })), ), @@ -445,7 +446,7 @@ describe('gitlab backend', () => { tree.forEach(file => interceptFiles(backend, file.path)); interceptCollection(backend, collectionManyEntriesConfig, { repeat: 5 }); - const entries = await backend.listAllEntries(fromJS(collectionManyEntriesConfig)); + const { entries } = await backend.listAllEntries(fromJS(collectionManyEntriesConfig)); expect(entries).toEqual( expect.arrayContaining(tree.map(file => expect.objectContaining({ path: file.path }))), @@ -463,6 +464,7 @@ describe('gitlab backend', () => { entries: expect.arrayContaining( files.map(file => expect.objectContaining({ path: file.file })), ), + errors: [], }); expect(entries.entries).toHaveLength(2); }); diff --git a/packages/decap-cms-core/src/__tests__/backend.spec.js b/packages/decap-cms-core/src/__tests__/backend.spec.js index c673c19a01c4..5826c0ddb332 100644 --- a/packages/decap-cms-core/src/__tests__/backend.spec.js +++ b/packages/decap-cms-core/src/__tests__/backend.spec.js @@ -655,15 +655,15 @@ describe('Backend', () => { backend = new Backend(implementation, { config: {}, backendName: 'github' }); backend.listAllEntries = jest.fn(collection => { if (collection.get('name') === 'posts') { - return Promise.resolve(posts); + return Promise.resolve({ entries: posts }); } if (collection.get('name') === 'pages') { - return Promise.resolve(pages); + return Promise.resolve({ entries: pages }); } if (collection.get('name') === 'files') { - return Promise.resolve(files); + return Promise.resolve({ entries: files }); } - return Promise.resolve([]); + return Promise.resolve({ entries: [] }); }); }); diff --git a/packages/decap-cms-core/src/actions/entries.ts b/packages/decap-cms-core/src/actions/entries.ts index f9f52236d93c..f08c5d933df0 100644 --- a/packages/decap-cms-core/src/actions/entries.ts +++ b/packages/decap-cms-core/src/actions/entries.ts @@ -164,7 +164,7 @@ export async function getAllEntries(state: State, collection: Collection) { const provider: Backend = integration ? getIntegrationProvider(state.integrations, backend.getToken, integration) : backend; - const entries = await provider.listAllEntries(collection); + const { entries } = await provider.listAllEntries(collection); return entries; } @@ -609,9 +609,10 @@ export function loadEntries(collection: Collection, page = 0) { cursor: Cursor; pagination: number; entries: EntryValue[]; + errors?: string[]; } = await (loadAllEntries ? // nested collections require all entries to construct the tree - provider.listAllEntries(collection).then((entries: EntryValue[]) => ({ entries })) + provider.listAllEntries(collection) : provider.listEntries(collection, page)); response = { ...response, @@ -630,6 +631,19 @@ export function loadEntries(collection: Collection, page = 0) { : Cursor.create(response.cursor), }; + response.errors?.forEach(error => { + dispatch( + addNotification({ + message: { + details: error, + key: 'ui.toast.duplicateFrontmatterKey', + }, + type: 'warning', + dismissAfter: 8000, + }), + ); + }); + dispatch( entriesLoaded( collection, diff --git a/packages/decap-cms-core/src/backend.ts b/packages/decap-cms-core/src/backend.ts index 0025b26f4308..b0d0c2b25093 100644 --- a/packages/decap-cms-core/src/backend.ts +++ b/packages/decap-cms-core/src/backend.ts @@ -521,7 +521,12 @@ export class Backend { }, ), ); + const formattedEntries = entries.map(this.entryWithFormat(collection)); + const errors = formattedEntries + .filter(e => e.parseError) + .map(e => `${e.parseError}. In ${e.path}`); + // If this collection has a "filter" property, filter entries accordingly const collectionFilter = collection.get('filter'); const filteredEntries = collectionFilter @@ -531,10 +536,9 @@ export class Backend { if (hasI18n(collection)) { const extension = selectFolderEntryExtension(collection); const groupedEntries = groupEntries(collection, extension, filteredEntries); - return groupedEntries; + return { entries: groupedEntries, errors }; } - - return filteredEntries; + return { entries: filteredEntries, errors }; } async listEntries(collection: Collection) { @@ -574,10 +578,14 @@ export class Backend { cursorType: 'collectionEntries', collection, }); + + const { entries, errors } = this.processEntries(loadedEntries, collection); + return { - entries: this.processEntries(loadedEntries, collection), + entries, pagination: cursor.meta?.get('page'), cursor, + errors, }; } @@ -601,14 +609,17 @@ export class Backend { } const response = await this.listEntries(collection); - const { entries } = response; + const { entries, errors } = response; let { cursor } = response; while (cursor && cursor.actions!.includes('next')) { const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, 'next'); entries.push(...newEntries); cursor = newCursor; } - return entries; + return { + entries, + errors, + }; } async search(collections: Collection[], searchTerm: string) { @@ -646,7 +657,7 @@ export class Backend { ]; } const filteredSearchFields = searchFields.filter(Boolean) as string[]; - const collectionEntries = await this.listAllEntries(collection); + const collectionEntries = (await this.listAllEntries(collection)).entries; return fuzzy.filter(searchTerm, collectionEntries, { extract: extractSearchFields(uniq(filteredSearchFields)), }); @@ -680,7 +691,7 @@ export class Backend { file?: string, limit?: number, ) { - let entries = await this.listAllEntries(collection); + let { entries } = await this.listAllEntries(collection); if (file) { entries = entries.filter(e => e.slug === file); } @@ -710,7 +721,7 @@ export class Backend { const collection = data.get('collection') as Collection; return this.implementation!.traverseCursor!(unwrappedCursor, action).then( async ({ entries, cursor: newCursor }) => ({ - entries: this.processEntries(entries, collection), + entries: this.processEntries(entries, collection).entries, cursor: Cursor.create(newCursor).wrapData({ cursorType: 'collectionEntries', collection, @@ -877,7 +888,11 @@ export class Backend { const format = resolveFormat(collection, entry); if (entry && entry.raw !== undefined) { const data = (format && attempt(format.fromFile.bind(format, entry.raw))) || {}; - if (isError(data)) console.error(data); + if (isError(data)) { + console.warn(data.message, '\n', data.stack); + entry = Object.assign(entry, { parseError: data.message }); + } + return Object.assign(entry, { data: isError(data) ? {} : data }); } return format.fromFile(entry); diff --git a/packages/decap-cms-core/src/valueObjects/Entry.ts b/packages/decap-cms-core/src/valueObjects/Entry.ts index 23497db92ae5..aca0bdd62042 100644 --- a/packages/decap-cms-core/src/valueObjects/Entry.ts +++ b/packages/decap-cms-core/src/valueObjects/Entry.ts @@ -35,6 +35,7 @@ export interface EntryValue { updatedOn: string; status?: string; meta: { path?: string }; + parseError?: string; i18n?: { // eslint-disable-next-line @typescript-eslint/no-explicit-any [locale: string]: any; diff --git a/packages/decap-cms-locales/src/en/index.js b/packages/decap-cms-locales/src/en/index.js index 2e6c7b26b01f..f8af29a6fc83 100644 --- a/packages/decap-cms-locales/src/en/index.js +++ b/packages/decap-cms-locales/src/en/index.js @@ -286,6 +286,7 @@ const en = { onFailToDelete: 'Failed to delete entry: %{details}', onFailToUpdateStatus: 'Failed to update status: %{details}', missingRequiredField: "Oops, you've missed a required field. Please complete before saving.", + duplicateFrontmatterKey: 'Duplicate key in frontmatter. %{details}', entrySaved: 'Entry saved', entryPublished: 'Entry published', entryUnpublished: 'Entry unpublished',