feat: add publish configuration option to collections (#3467)
This commit is contained in:
parent
2f86d6fc36
commit
df33bc64a9
@ -23,6 +23,7 @@ import {
|
|||||||
duplicateEntry,
|
duplicateEntry,
|
||||||
} 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',
|
||||||
@ -145,4 +146,65 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
publishEntryInEditor(publishTypes.publishNow);
|
publishEntryInEditor(publishTypes.publishNow);
|
||||||
duplicateEntry(entry1);
|
duplicateEntry(entry1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('cannot publish when "publish" is false', () => {
|
||||||
|
cy.visit('/', {
|
||||||
|
onBeforeLoad: window => {
|
||||||
|
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);
|
||||||
|
cy.contains('span', 'Publish').should('not.exist');
|
||||||
|
exitEditor();
|
||||||
|
goToWorkflow();
|
||||||
|
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||||
|
cy.contains('button', 'Publish new entry').should('not.exist');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { attempt, isError, take, unset } from 'lodash';
|
import { attempt, isError, take, unset, isEmpty } from 'lodash';
|
||||||
import uuid from 'uuid/v4';
|
import uuid from 'uuid/v4';
|
||||||
import {
|
import {
|
||||||
EditorialWorkflowError,
|
EditorialWorkflowError,
|
||||||
@ -243,6 +243,7 @@ export default class TestBackend implements Implementation {
|
|||||||
},
|
},
|
||||||
slug,
|
slug,
|
||||||
mediaFiles: assetProxies.map(this.normalizeAsset),
|
mediaFiles: assetProxies.map(this.normalizeAsset),
|
||||||
|
isModification: !isEmpty(getFile(path)),
|
||||||
};
|
};
|
||||||
unpubStore.push(unpubEntry);
|
unpubStore.push(unpubEntry);
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,41 @@ describe('config', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('slug', () => {
|
||||||
|
it('should set default slug config if not set', () => {
|
||||||
|
expect(applyDefaults(fromJS({ collections: [] })).get('slug')).toEqual(
|
||||||
|
fromJS({ encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite slug encoding if set', () => {
|
||||||
|
expect(
|
||||||
|
applyDefaults(fromJS({ collections: [], slug: { encoding: 'ascii' } })).getIn([
|
||||||
|
'slug',
|
||||||
|
'encoding',
|
||||||
|
]),
|
||||||
|
).toEqual('ascii');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite slug clean_accents if set', () => {
|
||||||
|
expect(
|
||||||
|
applyDefaults(fromJS({ collections: [], slug: { clean_accents: true } })).getIn([
|
||||||
|
'slug',
|
||||||
|
'clean_accents',
|
||||||
|
]),
|
||||||
|
).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite slug sanitize_replacement if set', () => {
|
||||||
|
expect(
|
||||||
|
applyDefaults(fromJS({ collections: [], slug: { sanitize_replacement: '_' } })).getIn([
|
||||||
|
'slug',
|
||||||
|
'sanitize_replacement',
|
||||||
|
]),
|
||||||
|
).toEqual('_');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('collections', () => {
|
describe('collections', () => {
|
||||||
it('should strip leading slashes from collection folder', () => {
|
it('should strip leading slashes from collection folder', () => {
|
||||||
expect(
|
expect(
|
||||||
@ -62,8 +97,8 @@ describe('config', () => {
|
|||||||
fromJS({
|
fromJS({
|
||||||
collections: [{ folder: '/foo' }],
|
collections: [{ folder: '/foo' }],
|
||||||
}),
|
}),
|
||||||
).get('collections'),
|
).getIn(['collections', 0, 'folder']),
|
||||||
).toEqual(fromJS([{ folder: 'foo' }]));
|
).toEqual('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should strip leading slashes from collection files', () => {
|
it('should strip leading slashes from collection files', () => {
|
||||||
@ -72,43 +107,8 @@ describe('config', () => {
|
|||||||
fromJS({
|
fromJS({
|
||||||
collections: [{ files: [{ file: '/foo' }] }],
|
collections: [{ files: [{ file: '/foo' }] }],
|
||||||
}),
|
}),
|
||||||
).get('collections'),
|
).getIn(['collections', 0, 'files', 0, 'file']),
|
||||||
).toEqual(fromJS([{ files: [{ file: 'foo' }] }]));
|
).toEqual('foo');
|
||||||
});
|
|
||||||
|
|
||||||
describe('slug', () => {
|
|
||||||
it('should set default slug config if not set', () => {
|
|
||||||
expect(applyDefaults(fromJS({ collections: [] })).get('slug')).toEqual(
|
|
||||||
fromJS({ encoding: 'unicode', clean_accents: false, sanitize_replacement: '-' }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not overwrite slug encoding if set', () => {
|
|
||||||
expect(
|
|
||||||
applyDefaults(fromJS({ collections: [], slug: { encoding: 'ascii' } })).getIn([
|
|
||||||
'slug',
|
|
||||||
'encoding',
|
|
||||||
]),
|
|
||||||
).toEqual('ascii');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not overwrite slug clean_accents if set', () => {
|
|
||||||
expect(
|
|
||||||
applyDefaults(fromJS({ collections: [], slug: { clean_accents: true } })).getIn([
|
|
||||||
'slug',
|
|
||||||
'clean_accents',
|
|
||||||
]),
|
|
||||||
).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not overwrite slug sanitize_replacement if set', () => {
|
|
||||||
expect(
|
|
||||||
applyDefaults(fromJS({ collections: [], slug: { sanitize_replacement: '_' } })).getIn([
|
|
||||||
'slug',
|
|
||||||
'sanitize_replacement',
|
|
||||||
]),
|
|
||||||
).toEqual('_');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('public_folder and media_folder', () => {
|
describe('public_folder and media_folder', () => {
|
||||||
@ -118,16 +118,8 @@ describe('config', () => {
|
|||||||
fromJS({
|
fromJS({
|
||||||
collections: [{ folder: 'foo', media_folder: 'static/images/docs' }],
|
collections: [{ folder: 'foo', media_folder: 'static/images/docs' }],
|
||||||
}),
|
}),
|
||||||
).get('collections'),
|
).getIn(['collections', 0, 'public_folder']),
|
||||||
).toEqual(
|
).toEqual('static/images/docs');
|
||||||
fromJS([
|
|
||||||
{
|
|
||||||
folder: 'foo',
|
|
||||||
media_folder: 'static/images/docs',
|
|
||||||
public_folder: 'static/images/docs',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not overwrite collection public_folder if set', () => {
|
it('should not overwrite collection public_folder if set', () => {
|
||||||
@ -142,30 +134,42 @@ describe('config', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
).get('collections'),
|
).getIn(['collections', 0, 'public_folder']),
|
||||||
).toEqual(
|
).toEqual('images/docs');
|
||||||
fromJS([
|
|
||||||
{
|
|
||||||
folder: 'foo',
|
|
||||||
media_folder: 'static/images/docs',
|
|
||||||
public_folder: 'images/docs',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set collection media_folder and public_folder to an empty string when collection path exists, but collection media_folder doesn't", () => {
|
it("should set collection media_folder and public_folder to an empty string when collection path exists, but collection media_folder doesn't", () => {
|
||||||
|
const result = applyDefaults(
|
||||||
|
fromJS({
|
||||||
|
collections: [{ folder: 'foo', path: '{{slug}}/index' }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result.getIn(['collections', 0, 'media_folder'])).toEqual('');
|
||||||
|
expect(result.getIn(['collections', 0, 'public_folder'])).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('publish', () => {
|
||||||
|
it('should set publish to true if not set', () => {
|
||||||
expect(
|
expect(
|
||||||
applyDefaults(
|
applyDefaults(
|
||||||
fromJS({
|
fromJS({
|
||||||
collections: [{ folder: 'foo', path: '{{slug}}/index' }],
|
collections: [{ folder: 'foo', media_folder: 'static/images/docs' }],
|
||||||
}),
|
}),
|
||||||
).get('collections'),
|
).getIn(['collections', 0, 'publish']),
|
||||||
).toEqual(
|
).toEqual(true);
|
||||||
fromJS([
|
});
|
||||||
{ folder: 'foo', path: '{{slug}}/index', media_folder: '', public_folder: '' },
|
|
||||||
]),
|
it('should not override existing publish config', () => {
|
||||||
);
|
expect(
|
||||||
|
applyDefaults(
|
||||||
|
fromJS({
|
||||||
|
collections: [
|
||||||
|
{ folder: 'foo', media_folder: 'static/images/docs', publish: false },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).getIn(['collections', 0, 'publish']),
|
||||||
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -58,6 +58,10 @@ export function applyDefaults(config) {
|
|||||||
map.set(
|
map.set(
|
||||||
'collections',
|
'collections',
|
||||||
map.get('collections').map(collection => {
|
map.get('collections').map(collection => {
|
||||||
|
if (!collection.has('publish')) {
|
||||||
|
collection = collection.set('publish', true);
|
||||||
|
}
|
||||||
|
|
||||||
const folder = collection.get('folder');
|
const folder = collection.get('folder');
|
||||||
if (folder) {
|
if (folder) {
|
||||||
if (collection.has('path') && !collection.has('media_folder')) {
|
if (collection.has('path') && !collection.has('media_folder')) {
|
||||||
|
@ -301,48 +301,146 @@ class EditorToolbar extends React.Component {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSimplePublishControls = () => {
|
renderWorkflowStatusControls = () => {
|
||||||
const {
|
const { isUpdatingStatus, onChangeStatus, currentStatus, t, useOpenAuthoring } = this.props;
|
||||||
collection,
|
return (
|
||||||
onPersist,
|
<ToolbarDropdown
|
||||||
onPersistAndNew,
|
dropdownTopOverlap="40px"
|
||||||
onPersistAndDuplicate,
|
dropdownWidth="120px"
|
||||||
onDuplicate,
|
renderButton={() => (
|
||||||
isPersisting,
|
<StatusButton>
|
||||||
hasChanged,
|
{isUpdatingStatus
|
||||||
isNewEntry,
|
? t('editor.editorToolbar.updating')
|
||||||
t,
|
: t('editor.editorToolbar.setStatus')}
|
||||||
} = this.props;
|
</StatusButton>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StatusDropdownItem
|
||||||
|
label={t('editor.editorToolbar.draft')}
|
||||||
|
onClick={() => onChangeStatus('DRAFT')}
|
||||||
|
icon={currentStatus === status.get('DRAFT') ? 'check' : null}
|
||||||
|
/>
|
||||||
|
<StatusDropdownItem
|
||||||
|
label={t('editor.editorToolbar.inReview')}
|
||||||
|
onClick={() => onChangeStatus('PENDING_REVIEW')}
|
||||||
|
icon={currentStatus === status.get('PENDING_REVIEW') ? 'check' : null}
|
||||||
|
/>
|
||||||
|
{useOpenAuthoring ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<StatusDropdownItem
|
||||||
|
label={t('editor.editorToolbar.ready')}
|
||||||
|
onClick={() => onChangeStatus('PENDING_PUBLISH')}
|
||||||
|
icon={currentStatus === status.get('PENDING_PUBLISH') ? 'check' : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderNewEntryWorkflowPublishControls = ({ canCreate, canPublish }) => {
|
||||||
|
const { isPublishing, onPublish, onPublishAndNew, onPublishAndDuplicate, t } = this.props;
|
||||||
|
|
||||||
|
return canPublish ? (
|
||||||
|
<ToolbarDropdown
|
||||||
|
dropdownTopOverlap="40px"
|
||||||
|
dropdownWidth="150px"
|
||||||
|
renderButton={() => (
|
||||||
|
<PublishButton>
|
||||||
|
{isPublishing
|
||||||
|
? t('editor.editorToolbar.publishing')
|
||||||
|
: t('editor.editorToolbar.publish')}
|
||||||
|
</PublishButton>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.publishNow')}
|
||||||
|
icon="arrow"
|
||||||
|
iconDirection="right"
|
||||||
|
onClick={onPublish}
|
||||||
|
/>
|
||||||
|
{canCreate ? (
|
||||||
|
<>
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.publishAndCreateNew')}
|
||||||
|
icon="add"
|
||||||
|
onClick={onPublishAndNew}
|
||||||
|
/>
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.publishAndDuplicate')}
|
||||||
|
icon="add"
|
||||||
|
onClick={onPublishAndDuplicate}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderExistingEntryWorkflowPublishControls = ({ canCreate, canPublish }) => {
|
||||||
|
const { unPublish, onDuplicate, isPersisting, t } = this.props;
|
||||||
|
|
||||||
|
return canPublish || canCreate ? (
|
||||||
|
<ToolbarDropdown
|
||||||
|
dropdownTopOverlap="40px"
|
||||||
|
dropdownWidth="150px"
|
||||||
|
renderButton={() => (
|
||||||
|
<PublishedToolbarButton>
|
||||||
|
{isPersisting
|
||||||
|
? t('editor.editorToolbar.unpublishing')
|
||||||
|
: t('editor.editorToolbar.published')}
|
||||||
|
</PublishedToolbarButton>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{canPublish && (
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.unpublish')}
|
||||||
|
icon="arrow"
|
||||||
|
iconDirection="right"
|
||||||
|
onClick={unPublish}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{canCreate && (
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.duplicate')}
|
||||||
|
icon="add"
|
||||||
|
onClick={onDuplicate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderExistingEntrySimplePublishControls = ({ canCreate }) => {
|
||||||
|
const { onDuplicate, t } = this.props;
|
||||||
|
return canCreate ? (
|
||||||
|
<ToolbarDropdown
|
||||||
|
dropdownTopOverlap="40px"
|
||||||
|
dropdownWidth="150px"
|
||||||
|
renderButton={() => (
|
||||||
|
<PublishedToolbarButton>{t('editor.editorToolbar.published')}</PublishedToolbarButton>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<DropdownItem
|
||||||
|
label={t('editor.editorToolbar.duplicate')}
|
||||||
|
icon="add"
|
||||||
|
onClick={onDuplicate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</ToolbarDropdown>
|
||||||
|
) : (
|
||||||
|
<PublishedButton>{t('editor.editorToolbar.published')}</PublishedButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderNewEntrySimplePublishControls = ({ canCreate }) => {
|
||||||
|
const { onPersist, onPersistAndNew, onPersistAndDuplicate, isPersisting, t } = this.props;
|
||||||
|
|
||||||
const canCreate = collection.get('create');
|
|
||||||
if (!isNewEntry && !hasChanged) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
|
||||||
{canCreate ? (
|
|
||||||
<ToolbarDropdown
|
|
||||||
dropdownTopOverlap="40px"
|
|
||||||
dropdownWidth="150px"
|
|
||||||
renderButton={() => (
|
|
||||||
<PublishedToolbarButton>
|
|
||||||
{t('editor.editorToolbar.published')}
|
|
||||||
</PublishedToolbarButton>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.duplicate')}
|
|
||||||
icon="add"
|
|
||||||
onClick={onDuplicate}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</ToolbarDropdown>
|
|
||||||
) : (
|
|
||||||
<PublishedButton>{t('editor.editorToolbar.published')}</PublishedButton>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ToolbarDropdown
|
<ToolbarDropdown
|
||||||
@ -381,6 +479,21 @@ class EditorToolbar extends React.Component {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderSimplePublishControls = () => {
|
||||||
|
const { collection, hasChanged, isNewEntry, t } = this.props;
|
||||||
|
|
||||||
|
const canCreate = collection.get('create');
|
||||||
|
if (!isNewEntry && !hasChanged) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
||||||
|
{this.renderExistingEntrySimplePublishControls({ canCreate })}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.renderNewEntrySimplePublishControls({ canCreate });
|
||||||
|
};
|
||||||
|
|
||||||
renderWorkflowSaveControls = () => {
|
renderWorkflowSaveControls = () => {
|
||||||
const {
|
const {
|
||||||
onPersist,
|
onPersist,
|
||||||
@ -421,95 +534,17 @@ class EditorToolbar extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWorkflowPublishControls = () => {
|
renderWorkflowPublishControls = () => {
|
||||||
const {
|
const { collection, currentStatus, isNewEntry, useOpenAuthoring, t } = this.props;
|
||||||
collection,
|
|
||||||
isUpdatingStatus,
|
|
||||||
isPublishing,
|
|
||||||
onChangeStatus,
|
|
||||||
onPublish,
|
|
||||||
unPublish,
|
|
||||||
onDuplicate,
|
|
||||||
onPublishAndNew,
|
|
||||||
onPublishAndDuplicate,
|
|
||||||
currentStatus,
|
|
||||||
isNewEntry,
|
|
||||||
useOpenAuthoring,
|
|
||||||
isPersisting,
|
|
||||||
t,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const canCreate = collection.get('create');
|
const canCreate = collection.get('create');
|
||||||
|
const canPublish = collection.get('publish') && !useOpenAuthoring;
|
||||||
|
|
||||||
if (currentStatus) {
|
if (currentStatus) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployPreviewButtonLabel'))}
|
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployPreviewButtonLabel'))}
|
||||||
<ToolbarDropdown
|
{this.renderWorkflowStatusControls()}
|
||||||
dropdownTopOverlap="40px"
|
{this.renderNewEntryWorkflowPublishControls({ canCreate, canPublish })}
|
||||||
dropdownWidth="120px"
|
|
||||||
renderButton={() => (
|
|
||||||
<StatusButton>
|
|
||||||
{isUpdatingStatus
|
|
||||||
? t('editor.editorToolbar.updating')
|
|
||||||
: t('editor.editorToolbar.setStatus')}
|
|
||||||
</StatusButton>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<StatusDropdownItem
|
|
||||||
label={t('editor.editorToolbar.draft')}
|
|
||||||
onClick={() => onChangeStatus('DRAFT')}
|
|
||||||
icon={currentStatus === status.get('DRAFT') ? 'check' : null}
|
|
||||||
/>
|
|
||||||
<StatusDropdownItem
|
|
||||||
label={t('editor.editorToolbar.inReview')}
|
|
||||||
onClick={() => onChangeStatus('PENDING_REVIEW')}
|
|
||||||
icon={currentStatus === status.get('PENDING_REVIEW') ? 'check' : null}
|
|
||||||
/>
|
|
||||||
{useOpenAuthoring ? (
|
|
||||||
''
|
|
||||||
) : (
|
|
||||||
<StatusDropdownItem
|
|
||||||
label={t('editor.editorToolbar.ready')}
|
|
||||||
onClick={() => onChangeStatus('PENDING_PUBLISH')}
|
|
||||||
icon={currentStatus === status.get('PENDING_PUBLISH') ? 'check' : null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ToolbarDropdown>
|
|
||||||
{useOpenAuthoring ? (
|
|
||||||
''
|
|
||||||
) : (
|
|
||||||
<ToolbarDropdown
|
|
||||||
dropdownTopOverlap="40px"
|
|
||||||
dropdownWidth="150px"
|
|
||||||
renderButton={() => (
|
|
||||||
<PublishButton>
|
|
||||||
{isPublishing
|
|
||||||
? t('editor.editorToolbar.publishing')
|
|
||||||
: t('editor.editorToolbar.publish')}
|
|
||||||
</PublishButton>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.publishNow')}
|
|
||||||
icon="arrow"
|
|
||||||
iconDirection="right"
|
|
||||||
onClick={onPublish}
|
|
||||||
/>
|
|
||||||
{canCreate ? (
|
|
||||||
<>
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.publishAndCreateNew')}
|
|
||||||
icon="add"
|
|
||||||
onClick={onPublishAndNew}
|
|
||||||
/>
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.publishAndDuplicate')}
|
|
||||||
icon="add"
|
|
||||||
onClick={onPublishAndDuplicate}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</ToolbarDropdown>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -521,31 +556,7 @@ class EditorToolbar extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
||||||
<ToolbarDropdown
|
{this.renderExistingEntryWorkflowPublishControls({ canCreate, canPublish })}
|
||||||
dropdownTopOverlap="40px"
|
|
||||||
dropdownWidth="150px"
|
|
||||||
renderButton={() => (
|
|
||||||
<PublishedToolbarButton>
|
|
||||||
{isPersisting
|
|
||||||
? t('editor.editorToolbar.unpublishing')
|
|
||||||
: t('editor.editorToolbar.published')}
|
|
||||||
</PublishedToolbarButton>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.unpublish')}
|
|
||||||
icon="arrow"
|
|
||||||
iconDirection="right"
|
|
||||||
onClick={unPublish}
|
|
||||||
/>
|
|
||||||
{canCreate && (
|
|
||||||
<DropdownItem
|
|
||||||
label={t('editor.editorToolbar.duplicate')}
|
|
||||||
icon="add"
|
|
||||||
onClick={onDuplicate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ToolbarDropdown>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ const WorkflowTopDescription = styled.p`
|
|||||||
|
|
||||||
class Workflow extends Component {
|
class Workflow extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
collections: ImmutablePropTypes.orderedMap,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
isEditorialWorkflow: PropTypes.bool.isRequired,
|
isEditorialWorkflow: PropTypes.bool.isRequired,
|
||||||
isOpenAuthoring: PropTypes.bool,
|
isOpenAuthoring: PropTypes.bool,
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
|
@ -126,6 +126,7 @@ const WorkflowCard = ({
|
|||||||
editLink,
|
editLink,
|
||||||
timestamp,
|
timestamp,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
allowPublish,
|
||||||
canPublish,
|
canPublish,
|
||||||
onPublish,
|
onPublish,
|
||||||
t,
|
t,
|
||||||
@ -143,11 +144,13 @@ const WorkflowCard = ({
|
|||||||
? t('workflow.workflowCard.deleteChanges')
|
? t('workflow.workflowCard.deleteChanges')
|
||||||
: t('workflow.workflowCard.deleteNewEntry')}
|
: t('workflow.workflowCard.deleteNewEntry')}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
<PublishButton disabled={!canPublish} onClick={onPublish}>
|
{allowPublish && (
|
||||||
{isModification
|
<PublishButton disabled={!canPublish} onClick={onPublish}>
|
||||||
? t('workflow.workflowCard.publishChanges')
|
{isModification
|
||||||
: t('workflow.workflowCard.publishNewEntry')}
|
? t('workflow.workflowCard.publishChanges')
|
||||||
</PublishButton>
|
: t('workflow.workflowCard.publishNewEntry')}
|
||||||
|
</PublishButton>
|
||||||
|
)}
|
||||||
</CardButtonContainer>
|
</CardButtonContainer>
|
||||||
</WorkflowCardContainer>
|
</WorkflowCardContainer>
|
||||||
);
|
);
|
||||||
@ -161,6 +164,7 @@ WorkflowCard.propTypes = {
|
|||||||
editLink: PropTypes.string.isRequired,
|
editLink: PropTypes.string.isRequired,
|
||||||
timestamp: PropTypes.string.isRequired,
|
timestamp: PropTypes.string.isRequired,
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
|
allowPublish: PropTypes.bool.isRequired,
|
||||||
canPublish: PropTypes.bool.isRequired,
|
canPublish: PropTypes.bool.isRequired,
|
||||||
onPublish: PropTypes.func.isRequired,
|
onPublish: PropTypes.func.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
|
@ -134,7 +134,7 @@ class WorkflowList extends React.Component {
|
|||||||
handleDelete: PropTypes.func.isRequired,
|
handleDelete: PropTypes.func.isRequired,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
isOpenAuthoring: PropTypes.bool,
|
isOpenAuthoring: PropTypes.bool,
|
||||||
collections: ImmutablePropTypes.orderedMap,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChangeStatus = (newStatus, dragProps) => {
|
handleChangeStatus = (newStatus, dragProps) => {
|
||||||
@ -210,11 +210,15 @@ class WorkflowList extends React.Component {
|
|||||||
const editLink = `collections/${entry.getIn(['metaData', 'collection'])}/entries/${slug}`;
|
const editLink = `collections/${entry.getIn(['metaData', 'collection'])}/entries/${slug}`;
|
||||||
const ownStatus = entry.getIn(['metaData', 'status']);
|
const ownStatus = entry.getIn(['metaData', 'status']);
|
||||||
const collectionName = entry.getIn(['metaData', 'collection']);
|
const collectionName = entry.getIn(['metaData', 'collection']);
|
||||||
const collectionLabel = collections
|
const collection = collections.find(
|
||||||
?.find(collection => collection.get('name') === collectionName)
|
collection => collection.get('name') === collectionName,
|
||||||
?.get('label');
|
);
|
||||||
|
const collectionLabel = collection?.get('label');
|
||||||
const isModification = entry.get('isModification');
|
const isModification = entry.get('isModification');
|
||||||
|
|
||||||
|
const allowPublish = collection?.get('publish');
|
||||||
const canPublish = ownStatus === status.last() && !entry.get('isPersisting', false);
|
const canPublish = ownStatus === status.last() && !entry.get('isPersisting', false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragSource
|
<DragSource
|
||||||
namespace={DNDNamespace}
|
namespace={DNDNamespace}
|
||||||
@ -235,6 +239,7 @@ class WorkflowList extends React.Component {
|
|||||||
editLink={editLink}
|
editLink={editLink}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
onDelete={this.requestDelete.bind(this, collectionName, slug, ownStatus)}
|
onDelete={this.requestDelete.bind(this, collectionName, slug, ownStatus)}
|
||||||
|
allowPublish={allowPublish}
|
||||||
canPublish={canPublish}
|
canPublish={canPublish}
|
||||||
onPublish={this.requestPublish.bind(this, collectionName, slug, ownStatus)}
|
onPublish={this.requestPublish.bind(this, collectionName, slug, ownStatus)}
|
||||||
/>
|
/>
|
||||||
|
@ -152,5 +152,17 @@ describe('config', () => {
|
|||||||
);
|
);
|
||||||
}).not.toThrowError();
|
}).not.toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw if collection publish is not a boolean', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateConfig(merge(validConfig, { collections: [{ publish: 'false' }] }));
|
||||||
|
}).toThrowError("'collections[0].publish' should be boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw if collection publish is a boolean', () => {
|
||||||
|
expect(() => {
|
||||||
|
validateConfig(merge(validConfig, { collections: [{ publish: false }] }));
|
||||||
|
}).not.toThrowError();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -117,6 +117,7 @@ const getConfigSchema = () => ({
|
|||||||
preview_path: { type: 'string' },
|
preview_path: { type: 'string' },
|
||||||
preview_path_date_field: { type: 'string' },
|
preview_path_date_field: { type: 'string' },
|
||||||
create: { type: 'boolean' },
|
create: { type: 'boolean' },
|
||||||
|
publish: { type: 'boolean' },
|
||||||
editor: {
|
editor: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -194,6 +194,7 @@ The `collections` setting is the heart of your Netlify CMS configuration, as it
|
|||||||
* `files` or `folder` (requires one of these): specifies the collection type and location; details in [Collection Types](../collection-types)
|
* `files` or `folder` (requires one of these): specifies the collection type and location; details in [Collection Types](../collection-types)
|
||||||
* `filter`: optional filter for `folder` collections; details in [Collection Types](../collection-types)
|
* `filter`: optional filter for `folder` collections; details in [Collection Types](../collection-types)
|
||||||
* `create`: for `folder` collections only; `true` allows users to create new items in the collection; defaults to `false`
|
* `create`: for `folder` collections only; `true` allows users to create new items in the collection; defaults to `false`
|
||||||
|
* `publish`: for `publish_mode: editorial_workflow` only; `false` hides UI publishing controls for a collection; defaults to `true`
|
||||||
* `delete`: `false` prevents users from deleting items in a collection; defaults to `true`
|
* `delete`: `false` prevents users from deleting items in a collection; defaults to `true`
|
||||||
* `extension`: see detailed description below
|
* `extension`: see detailed description below
|
||||||
* `format`: see detailed description below
|
* `format`: see detailed description below
|
||||||
|
Loading…
x
Reference in New Issue
Block a user