Test: add editor test coverage (#3598)
This commit is contained in:
parent
36ae69c96e
commit
166b070cb1
@ -21,9 +21,12 @@ import {
|
|||||||
unpublishEntry,
|
unpublishEntry,
|
||||||
publishEntryInEditor,
|
publishEntryInEditor,
|
||||||
duplicateEntry,
|
duplicateEntry,
|
||||||
|
goToEntry,
|
||||||
|
populateEntry,
|
||||||
|
publishAndCreateNewEntryInEditor,
|
||||||
|
publishAndDuplicateEntryInEditor,
|
||||||
} from '../utils/steps';
|
} from '../utils/steps';
|
||||||
import { setting1, setting2, workflowStatus, editorStatus, publishTypes } from '../utils/constants';
|
import { setting1, setting2, workflowStatus, editorStatus, publishTypes } from '../utils/constants';
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
|
|
||||||
const entry1 = {
|
const entry1 = {
|
||||||
title: 'first title',
|
title: 'first title',
|
||||||
@ -54,7 +57,17 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
|
|
||||||
it('can create an entry', () => {
|
it('can create an entry', () => {
|
||||||
login();
|
login();
|
||||||
createPostAndExit(entry1);
|
createPost(entry1);
|
||||||
|
|
||||||
|
// new entry should show 'Delete unpublished entry'
|
||||||
|
cy.contains('button', 'Delete unpublished entry');
|
||||||
|
cy.url().should(
|
||||||
|
'eq',
|
||||||
|
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/, '-')}`,
|
||||||
|
);
|
||||||
|
exitEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can validate object fields', () => {
|
it('can validate object fields', () => {
|
||||||
@ -80,6 +93,27 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
publishWorkflowEntry(entry1);
|
publishWorkflowEntry(entry1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can update an entry', () => {
|
||||||
|
login();
|
||||||
|
createPostAndExit(entry1);
|
||||||
|
goToWorkflow();
|
||||||
|
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||||
|
publishWorkflowEntry(entry1);
|
||||||
|
|
||||||
|
goToEntry(entry1);
|
||||||
|
populateEntry(entry2);
|
||||||
|
// existing entry should show 'Delete unpublished changes'
|
||||||
|
cy.contains('button', 'Delete unpublished changes');
|
||||||
|
// existing entry slug should remain the same after save'
|
||||||
|
cy.url().should(
|
||||||
|
'eq',
|
||||||
|
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/, '-')}`,
|
||||||
|
);
|
||||||
|
exitEditor();
|
||||||
|
});
|
||||||
|
|
||||||
it('can change workflow status', () => {
|
it('can change workflow status', () => {
|
||||||
login();
|
login();
|
||||||
createPostAndExit(entry1);
|
createPostAndExit(entry1);
|
||||||
@ -148,58 +182,8 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('cannot publish when "publish" is false', () => {
|
it('cannot publish when "publish" is false', () => {
|
||||||
cy.visit('/', {
|
cy.task('updateConfig', { collections: [{ publish: false }] });
|
||||||
onBeforeLoad: window => {
|
login();
|
||||||
window.CMS_MANUAL_INIT = true;
|
|
||||||
},
|
|
||||||
onLoad: window => {
|
|
||||||
window.CMS.init({
|
|
||||||
config: fromJS({
|
|
||||||
backend: {
|
|
||||||
name: 'test-repo',
|
|
||||||
},
|
|
||||||
publish_mode: 'editorial_workflow',
|
|
||||||
load_config_file: false,
|
|
||||||
media_folder: 'assets/uploads',
|
|
||||||
collections: [
|
|
||||||
{
|
|
||||||
label: 'Posts',
|
|
||||||
name: 'post',
|
|
||||||
folder: '_posts',
|
|
||||||
label_singular: 'Post',
|
|
||||||
create: true,
|
|
||||||
publish: false,
|
|
||||||
fields: [
|
|
||||||
{ label: 'Title', name: 'title', widget: 'string', tagname: 'h1' },
|
|
||||||
{
|
|
||||||
label: 'Publish Date',
|
|
||||||
name: 'date',
|
|
||||||
widget: 'datetime',
|
|
||||||
dateFormat: 'YYYY-MM-DD',
|
|
||||||
timeFormat: 'HH:mm',
|
|
||||||
format: 'YYYY-MM-DD HH:mm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cover Image',
|
|
||||||
name: 'image',
|
|
||||||
widget: 'image',
|
|
||||||
required: false,
|
|
||||||
tagname: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Body',
|
|
||||||
name: 'body',
|
|
||||||
widget: 'markdown',
|
|
||||||
hint: 'Main content goes here.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
cy.contains('button', 'Login').click();
|
|
||||||
createPost(entry1);
|
createPost(entry1);
|
||||||
cy.contains('span', 'Publish').should('not.exist');
|
cy.contains('span', 'Publish').should('not.exist');
|
||||||
exitEditor();
|
exitEditor();
|
||||||
@ -207,4 +191,19 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||||
cy.contains('button', 'Publish new entry').should('not.exist');
|
cy.contains('button', 'Publish new entry').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.only('can create a new entry, publish and create new', () => {
|
||||||
|
login();
|
||||||
|
createPost(entry1);
|
||||||
|
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||||
|
|
||||||
|
publishAndCreateNewEntryInEditor(entry1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only('can create a new entry, publish and duplicate', () => {
|
||||||
|
login();
|
||||||
|
createPost(entry1);
|
||||||
|
updateWorkflowStatusInEditor(editorStatus.ready);
|
||||||
|
publishAndDuplicateEntryInEditor(entry1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
101
cypress/integration/simple_workflow_spec_test_backend.js
Normal file
101
cypress/integration/simple_workflow_spec_test_backend.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import {
|
||||||
|
login,
|
||||||
|
newPost,
|
||||||
|
populateEntry,
|
||||||
|
exitEditor,
|
||||||
|
createPostAndPublish,
|
||||||
|
assertPublishedEntry,
|
||||||
|
editPostAndPublish,
|
||||||
|
createPostPublishAndCreateNew,
|
||||||
|
createPostPublishAndDuplicate,
|
||||||
|
editPostPublishAndCreateNew,
|
||||||
|
editPostPublishAndDuplicate,
|
||||||
|
duplicatePostAndPublish,
|
||||||
|
} from '../utils/steps';
|
||||||
|
|
||||||
|
const entry1 = {
|
||||||
|
title: 'first title',
|
||||||
|
body: 'first body',
|
||||||
|
};
|
||||||
|
const entry2 = {
|
||||||
|
title: 'second title',
|
||||||
|
body: 'second body',
|
||||||
|
};
|
||||||
|
|
||||||
|
const backend = 'test';
|
||||||
|
|
||||||
|
describe('Test Backend Simple Workflow', () => {
|
||||||
|
before(() => {
|
||||||
|
Cypress.config('defaultCommandTimeout', 4000);
|
||||||
|
cy.task('setupBackend', { backend, options: { publish_mode: 'simple' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
cy.task('teardownBackend', { backend });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully loads', () => {
|
||||||
|
login();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can create a new entry', () => {
|
||||||
|
login();
|
||||||
|
newPost();
|
||||||
|
populateEntry(entry1, () => {});
|
||||||
|
|
||||||
|
// new entry should show 'Unsaved changes'
|
||||||
|
cy.contains('div', 'Unsaved Changes');
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
exitEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can publish a new entry', () => {
|
||||||
|
login();
|
||||||
|
createPostAndPublish(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can publish a new entry and create new', () => {
|
||||||
|
login();
|
||||||
|
createPostPublishAndCreateNew(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can publish a new entry and duplicate', () => {
|
||||||
|
login();
|
||||||
|
createPostPublishAndDuplicate(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit an existing entry and publish', () => {
|
||||||
|
login();
|
||||||
|
createPostAndPublish(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
|
||||||
|
editPostAndPublish(entry1, entry2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit an existing entry, publish and create new', () => {
|
||||||
|
login();
|
||||||
|
createPostAndPublish(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
|
||||||
|
editPostPublishAndCreateNew(entry1, entry2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can edit an existing entry, publish and duplicate', () => {
|
||||||
|
login();
|
||||||
|
createPostAndPublish(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
|
||||||
|
editPostPublishAndDuplicate(entry1, entry2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can duplicate an existing entry', () => {
|
||||||
|
login();
|
||||||
|
createPostAndPublish(entry1);
|
||||||
|
assertPublishedEntry(entry1);
|
||||||
|
|
||||||
|
duplicatePostAndPublish(entry1);
|
||||||
|
});
|
||||||
|
});
|
@ -11,6 +11,7 @@
|
|||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
// the project's config changing)
|
// the project's config changing)
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
const { merge } = require('lodash');
|
||||||
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
|
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -34,8 +35,8 @@ const {
|
|||||||
teardownBitBucketTest,
|
teardownBitBucketTest,
|
||||||
} = require('./bitbucket');
|
} = require('./bitbucket');
|
||||||
const { setupProxy, teardownProxy, setupProxyTest, teardownProxyTest } = require('./proxy');
|
const { setupProxy, teardownProxy, setupProxyTest, teardownProxyTest } = require('./proxy');
|
||||||
|
const { setupTestBackend } = require('./testBackend');
|
||||||
const { copyBackendFiles, switchVersion } = require('../utils/config');
|
const { copyBackendFiles, switchVersion, updateConfig } = require('../utils/config');
|
||||||
|
|
||||||
module.exports = async (on, config) => {
|
module.exports = async (on, config) => {
|
||||||
// `on` is used to hook into various events Cypress emits
|
// `on` is used to hook into various events Cypress emits
|
||||||
@ -61,6 +62,9 @@ module.exports = async (on, config) => {
|
|||||||
case 'proxy':
|
case 'proxy':
|
||||||
result = await setupProxy(options);
|
result = await setupProxy(options);
|
||||||
break;
|
break;
|
||||||
|
case 'test':
|
||||||
|
result = await setupTestBackend(options);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -161,6 +165,13 @@ module.exports = async (on, config) => {
|
|||||||
|
|
||||||
await switchVersion(version);
|
await switchVersion(version);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
async updateConfig(config) {
|
||||||
|
await updateConfig(current => {
|
||||||
|
merge(current, config);
|
||||||
|
});
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
14
cypress/plugins/testBackend.js
Normal file
14
cypress/plugins/testBackend.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const { updateConfig } = require('../utils/config');
|
||||||
|
const { merge } = require('lodash');
|
||||||
|
|
||||||
|
async function setupTestBackend(options) {
|
||||||
|
await updateConfig(current => {
|
||||||
|
merge(current, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
setupTestBackend,
|
||||||
|
};
|
@ -2,7 +2,7 @@ const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
|
|||||||
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
||||||
const setting1 = { limit: 10, author: 'John Doe' };
|
const setting1 = { limit: 10, author: 'John Doe' };
|
||||||
const setting2 = { name: 'Jane Doe', description: 'description' };
|
const setting2 = { name: 'Jane Doe', description: 'description' };
|
||||||
const publishTypes = { publishNow: 'Publish now' };
|
const publishTypes = { publishNow: 'Publish now', publishAndCreateNew: 'Publish and create new', publishAndDuplicate: 'Publish and duplicate' };
|
||||||
const notifications = {
|
const notifications = {
|
||||||
saved: 'Entry saved',
|
saved: 'Entry saved',
|
||||||
published: 'Entry published',
|
published: 'Entry published',
|
||||||
|
@ -169,6 +169,20 @@ function publishEntryInEditor(publishType) {
|
|||||||
assertNotification(notifications.published);
|
assertNotification(notifications.published);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function publishAndCreateNewEntryInEditor() {
|
||||||
|
selectDropdownItem('Publish', publishTypes.publishAndCreateNew);
|
||||||
|
assertNotification(notifications.published);
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishAndDuplicateEntryInEditor(entry) {
|
||||||
|
selectDropdownItem('Publish', publishTypes.publishAndDuplicate);
|
||||||
|
assertNotification(notifications.published);
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', entry.title);
|
||||||
|
}
|
||||||
|
|
||||||
function selectDropdownItem(label, item) {
|
function selectDropdownItem(label, item) {
|
||||||
cy.contains('[role="button"]', label).as('dropDownButton');
|
cy.contains('[role="button"]', label).as('dropDownButton');
|
||||||
cy.get('@dropDownButton')
|
cy.get('@dropDownButton')
|
||||||
@ -205,12 +219,11 @@ function populateEntry(entry, onDone = flushClockAndSave) {
|
|||||||
if (key === 'body') {
|
if (key === 'body') {
|
||||||
cy.getMarkdownEditor()
|
cy.getMarkdownEditor()
|
||||||
.click()
|
.click()
|
||||||
.clear()
|
.clear({ force: true })
|
||||||
.type(value);
|
.type(value, { force: true });
|
||||||
} else {
|
} else {
|
||||||
cy.get(`[id^="${key}-field"]`)
|
cy.get(`[id^="${key}-field"]`).clear({ force: true });
|
||||||
.clear()
|
cy.get(`[id^="${key}-field"]`).type(value, { force: true });
|
||||||
.type(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +244,7 @@ function createPostAndExit(entry) {
|
|||||||
exitEditor();
|
exitEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
function publishEntry() {
|
function publishEntry({ createNew = false, duplicate = false } = {}) {
|
||||||
cy.clock().then(clock => {
|
cy.clock().then(clock => {
|
||||||
// some input fields are de-bounced thus require advancing the clock
|
// some input fields are de-bounced thus require advancing the clock
|
||||||
if (clock) {
|
if (clock) {
|
||||||
@ -242,23 +255,91 @@ function publishEntry() {
|
|||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.contains('[role="button"]', 'Publish').as('publishButton');
|
if (createNew) {
|
||||||
cy.get('@publishButton')
|
selectDropdownItem('Publish', publishTypes.publishAndCreateNew);
|
||||||
.parent()
|
} else if (duplicate) {
|
||||||
.within(() => {
|
selectDropdownItem('Publish', publishTypes.publishAndDuplicate);
|
||||||
cy.get('@publishButton').click();
|
} else {
|
||||||
cy.contains('[role="menuitem"] span', 'Publish now').click();
|
selectDropdownItem('Publish', publishTypes.publishNow);
|
||||||
});
|
}
|
||||||
|
|
||||||
assertNotification(notifications.saved);
|
assertNotification(notifications.saved);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPostAndPublish(entry) {
|
function createPostAndPublish(entry) {
|
||||||
cy.contains('a', 'New Post').click();
|
newPost();
|
||||||
populateEntry(entry, publishEntry);
|
populateEntry(entry, publishEntry);
|
||||||
exitEditor();
|
exitEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPostPublishAndCreateNew(entry) {
|
||||||
|
newPost();
|
||||||
|
populateEntry(entry, () => publishEntry({ createNew: true }));
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', '');
|
||||||
|
|
||||||
|
exitEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPostPublishAndDuplicate(entry) {
|
||||||
|
newPost();
|
||||||
|
populateEntry(entry, () => publishEntry({ duplicate: true }));
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', entry.title);
|
||||||
|
|
||||||
|
exitEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPostAndPublish(entry1, entry2) {
|
||||||
|
goToEntry(entry1);
|
||||||
|
cy.contains('button', 'Delete entry');
|
||||||
|
cy.contains('span', 'Published');
|
||||||
|
|
||||||
|
populateEntry(entry2, publishEntry);
|
||||||
|
// existing entry slug should remain the same after save
|
||||||
|
cy.url().should(
|
||||||
|
'eq',
|
||||||
|
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/, '-')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPostPublishAndCreateNew(entry1, entry2) {
|
||||||
|
goToEntry(entry1);
|
||||||
|
cy.contains('button', 'Delete entry');
|
||||||
|
cy.contains('span', 'Published');
|
||||||
|
|
||||||
|
populateEntry(entry2, () => publishEntry({ createNew: true }));
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPostPublishAndDuplicate(entry1, entry2) {
|
||||||
|
goToEntry(entry1);
|
||||||
|
cy.contains('button', 'Delete entry');
|
||||||
|
cy.contains('span', 'Published');
|
||||||
|
|
||||||
|
populateEntry(entry2, () => publishEntry({ duplicate: true }));
|
||||||
|
cy.url().should('eq', `http://localhost:8080/#/collections/posts/new`);
|
||||||
|
cy.get('[id^="title-field"]').should('have.value', entry2.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicatePostAndPublish(entry1) {
|
||||||
|
goToEntry(entry1);
|
||||||
|
cy.contains('button', 'Delete entry');
|
||||||
|
selectDropdownItem('Published', 'Duplicate');
|
||||||
|
publishEntry();
|
||||||
|
|
||||||
|
cy.url().should(
|
||||||
|
'eq',
|
||||||
|
`http://localhost:8080/#/collections/posts/entries/1970-01-01-${entry1.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/, '-')}-1`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function updateExistingPostAndExit(fromEntry, toEntry) {
|
function updateExistingPostAndExit(fromEntry, toEntry) {
|
||||||
goToWorkflow();
|
goToWorkflow();
|
||||||
cy.contains('h2', fromEntry.title)
|
cy.contains('h2', fromEntry.title)
|
||||||
@ -393,4 +474,13 @@ module.exports = {
|
|||||||
newPost,
|
newPost,
|
||||||
populateEntry,
|
populateEntry,
|
||||||
goToEntry,
|
goToEntry,
|
||||||
|
publishEntry,
|
||||||
|
createPostPublishAndCreateNew,
|
||||||
|
createPostPublishAndDuplicate,
|
||||||
|
editPostAndPublish,
|
||||||
|
editPostPublishAndCreateNew,
|
||||||
|
editPostPublishAndDuplicate,
|
||||||
|
duplicatePostAndPublish,
|
||||||
|
publishAndCreateNewEntryInEditor,
|
||||||
|
publishAndDuplicateEntryInEditor,
|
||||||
};
|
};
|
||||||
|
File diff suppressed because one or more lines are too long
@ -33,7 +33,7 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
required: false
|
required: false
|
||||||
tagname: ''
|
tagname: ''
|
||||||
|
|
||||||
- { editorComponents: ['youtube'], label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
|
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
|
||||||
meta:
|
meta:
|
||||||
- { label: 'SEO Description', name: 'description', widget: 'text' }
|
- { label: 'SEO Description', name: 'description', widget: 'text' }
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user