feat(backend-github): Open Authoring (#2430)

* Make filterPromises resolve entries before filtering

* Add filterPromisesWith & onlySuccessfulPromises to utility library

* Memoize user method in GitHub API

* Make storeMetadata safe to call concurrently in GitHub API

* Fork workflow: startup and authentication

* Fork workflow: backend support

* Fork workflow: disable unused UI elements

* Fork workflow: docs

* Fork workflow: fix deploy previews

* Suggested edits for fork workflow doc

* Change future tense to present

* Fork workflow: add beta status to docs

* remove debug statement

* rename fork workflow to Open Authoring
This commit is contained in:
Benaiah Mischenko
2019-07-24 15:20:41 -07:00
committed by Shawn Erquhart
parent 41559256d0
commit edf0a3afdc
16 changed files with 729 additions and 219 deletions

View File

@ -7,6 +7,7 @@ export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILURE = 'AUTH_FAILURE';
export const AUTH_REQUEST_DONE = 'AUTH_REQUEST_DONE';
export const USE_FORK_WORKFLOW = 'USE_FORK_WORKFLOW';
export const LOGOUT = 'LOGOUT';
export function authenticating() {
@ -36,6 +37,12 @@ export function doneAuthenticating() {
};
}
export function useForkWorkflow() {
return {
type: USE_FORK_WORKFLOW,
};
}
export function logout() {
return {
type: LOGOUT,
@ -52,6 +59,9 @@ export function authenticateUser() {
.currentUser()
.then(user => {
if (user) {
if (user.useForkWorkflow) {
dispatch(useForkWorkflow());
}
dispatch(authenticate(user));
} else {
dispatch(doneAuthenticating());
@ -73,6 +83,9 @@ export function loginUser(credentials) {
return backend
.authenticate(credentials)
.then(user => {
if (user.useForkWorkflow) {
dispatch(useForkWorkflow());
}
dispatch(authenticate(user));
})
.catch(error => {

View File

@ -250,25 +250,21 @@ export function loadMediaDisplayURL(file) {
) {
return Promise.resolve();
}
if (typeof url === 'string') {
if (typeof url === 'string' || typeof displayURL === 'string') {
dispatch(mediaDisplayURLRequest(id));
return dispatch(mediaDisplayURLSuccess(id, displayURL));
}
if (typeof displayURL === 'string') {
dispatch(mediaDisplayURLRequest(id));
return dispatch(mediaDisplayURLSuccess(id, displayURL));
dispatch(mediaDisplayURLSuccess(id, displayURL));
}
try {
const backend = currentBackend(state.config);
dispatch(mediaDisplayURLRequest(id));
const newURL = await backend.getMediaDisplayURL(displayURL);
if (newURL) {
return dispatch(mediaDisplayURLSuccess(id, newURL));
dispatch(mediaDisplayURLSuccess(id, newURL));
} else {
throw new Error('No display URL was returned!');
}
} catch (err) {
return dispatch(mediaDisplayURLFailure(id, err));
dispatch(mediaDisplayURLFailure(id, err));
}
};
}

View File

@ -61,6 +61,7 @@ class Editor extends React.Component {
newEntry: PropTypes.bool.isRequired,
displayUrl: PropTypes.string,
hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool,
unpublishedEntry: PropTypes.bool,
isModification: PropTypes.bool,
collectionEntriesLoaded: PropTypes.bool,
@ -350,6 +351,7 @@ class Editor extends React.Component {
hasChanged,
displayUrl,
hasWorkflow,
useForkWorkflow,
unpublishedEntry,
newEntry,
isModification,
@ -397,6 +399,7 @@ class Editor extends React.Component {
hasChanged={hasChanged}
displayUrl={displayUrl}
hasWorkflow={hasWorkflow}
useForkWorkflow={useForkWorkflow}
hasUnpublishedChanges={unpublishedEntry}
isNewEntry={newEntry}
isModification={isModification}
@ -410,7 +413,7 @@ class Editor extends React.Component {
}
function mapStateToProps(state, ownProps) {
const { collections, entryDraft, auth, config, entries } = state;
const { collections, entryDraft, auth, config, entries, globalUI } = state;
const slug = ownProps.match.params.slug;
const collection = collections.get(ownProps.match.params.name);
const collectionName = collection.get('name');
@ -422,6 +425,7 @@ function mapStateToProps(state, ownProps) {
const hasChanged = entryDraft.get('hasChanged');
const displayUrl = config.get('display_url');
const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
const useForkWorkflow = globalUI.get('useForkWorkflow', false);
const isModification = entryDraft.getIn(['entry', 'isModification']);
const collectionEntriesLoaded = !!entries.getIn(['pages', collectionName]);
const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug);
@ -441,6 +445,7 @@ function mapStateToProps(state, ownProps) {
hasChanged,
displayUrl,
hasWorkflow,
useForkWorkflow,
isModification,
collectionEntriesLoaded,
currentStatus,

View File

@ -166,6 +166,7 @@ class EditorInterface extends Component {
hasChanged,
displayUrl,
hasWorkflow,
useForkWorkflow,
hasUnpublishedChanges,
isNewEntry,
isModification,
@ -240,6 +241,7 @@ class EditorInterface extends Component {
displayUrl={displayUrl}
collection={collection}
hasWorkflow={hasWorkflow}
useForkWorkflow={useForkWorkflow}
hasUnpublishedChanges={hasUnpublishedChanges}
isNewEntry={isNewEntry}
isModification={isModification}
@ -293,6 +295,7 @@ EditorInterface.propTypes = {
hasChanged: PropTypes.bool,
displayUrl: PropTypes.string,
hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool,
hasUnpublishedChanges: PropTypes.bool,
isNewEntry: PropTypes.bool,
isModification: PropTypes.bool,

View File

@ -218,6 +218,7 @@ class EditorToolbar extends React.Component {
displayUrl: PropTypes.string,
collection: ImmutablePropTypes.map.isRequired,
hasWorkflow: PropTypes.bool,
useForkWorkflow: PropTypes.bool,
hasUnpublishedChanges: PropTypes.bool,
isNewEntry: PropTypes.bool,
isModification: PropTypes.bool,
@ -379,6 +380,7 @@ class EditorToolbar extends React.Component {
onPublishAndNew,
currentStatus,
isNewEntry,
useForkWorkflow,
t,
} = this.props;
if (currentStatus) {
@ -406,37 +408,45 @@ class EditorToolbar extends React.Component {
onClick={() => onChangeStatus('PENDING_REVIEW')}
icon={currentStatus === status.get('PENDING_REVIEW') && 'check'}
/>
<StatusDropdownItem
label={t('editor.editorToolbar.ready')}
onClick={() => onChangeStatus('PENDING_PUBLISH')}
icon={currentStatus === status.get('PENDING_PUBLISH') && 'check'}
/>
</ToolbarDropdown>
<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}
/>
{collection.get('create') ? (
<DropdownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPublishAndNew}
{useForkWorkflow ? (
''
) : (
<StatusDropdownItem
label={t('editor.editorToolbar.ready')}
onClick={() => onChangeStatus('PENDING_PUBLISH')}
icon={currentStatus === status.get('PENDING_PUBLISH') && 'check'}
/>
) : null}
)}
</ToolbarDropdown>
{useForkWorkflow ? (
''
) : (
<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}
/>
{collection.get('create') ? (
<DropdownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPublishAndNew}
/>
) : null}
</ToolbarDropdown>
)}
</>
);
}

View File

@ -55,6 +55,7 @@ class Workflow extends Component {
static propTypes = {
collections: ImmutablePropTypes.orderedMap,
isEditorialWorkflow: PropTypes.bool.isRequired,
isForkWorkflow: PropTypes.bool,
isFetching: PropTypes.bool,
unpublishedEntries: ImmutablePropTypes.map,
loadUnpublishedEntries: PropTypes.func.isRequired,
@ -74,6 +75,7 @@ class Workflow extends Component {
render() {
const {
isEditorialWorkflow,
isForkWorkflow,
isFetching,
unpublishedEntries,
updateUnpublishedEntryStatus,
@ -125,6 +127,7 @@ class Workflow extends Component {
handleChangeStatus={updateUnpublishedEntryStatus}
handlePublish={publishUnpublishedEntry}
handleDelete={deleteUnpublishedEntry}
isForkWorkflow={isForkWorkflow}
/>
</WorkflowContainer>
);
@ -132,9 +135,10 @@ class Workflow extends Component {
}
function mapStateToProps(state) {
const { collections } = state;
const isEditorialWorkflow = state.config.get('publish_mode') === EDITORIAL_WORKFLOW;
const returnObj = { collections, isEditorialWorkflow };
const { collections, config, globalUI } = state;
const isEditorialWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
const isForkWorkflow = globalUI.get('useForkWorkflow', false);
const returnObj = { collections, isEditorialWorkflow, isForkWorkflow };
if (isEditorialWorkflow) {
returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false);

View File

@ -17,6 +17,12 @@ const WorkflowListContainer = styled.div`
grid-template-columns: 33.3% 33.3% 33.3%;
`;
const WorkflowListContainerForkWorkflow = styled.div`
min-height: 60%;
display: grid;
grid-template-columns: 50% 50% 0%;
`;
const styles = {
columnPosition: idx =>
(idx === 0 &&
@ -58,6 +64,16 @@ const styles = {
columnHovered: css`
border-color: ${colors.active};
`,
hiddenColumn: css`
display: none;
`,
hiddenRightBorder: css`
&:not(:first-child):not(:last-child) {
&:after {
display: none;
}
}
`,
};
const ColumnHeader = styled.h2`
@ -118,6 +134,7 @@ class WorkflowList extends React.Component {
handlePublish: PropTypes.func.isRequired,
handleDelete: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
isForkWorkflow: PropTypes.bool,
};
handleChangeStatus = (newStatus, dragProps) => {
@ -145,6 +162,7 @@ class WorkflowList extends React.Component {
// eslint-disable-next-line react/display-name
renderColumns = (entries, column) => {
const { isForkWorkflow } = this.props;
if (!entries) return null;
if (!column) {
@ -162,6 +180,8 @@ class WorkflowList extends React.Component {
styles.column,
styles.columnPosition(idx),
isHovered && styles.columnHovered,
isForkWorkflow && currColumn === 'pending_publish' && styles.hiddenColumn,
isForkWorkflow && currColumn === 'pending_review' && styles.hiddenRightBorder,
]}
>
<ColumnHeader name={currColumn}>
@ -228,7 +248,10 @@ class WorkflowList extends React.Component {
render() {
const columns = this.renderColumns(this.props.entries);
return <WorkflowListContainer>{columns}</WorkflowListContainer>;
const ListContainer = this.props.isForkWorkflow
? WorkflowListContainerForkWorkflow
: WorkflowListContainer;
return <ListContainer>{columns}</ListContainer>;
}
}

View File

@ -1,13 +1,16 @@
import { Map } from 'immutable';
import { USE_FORK_WORKFLOW } from 'Actions/auth';
/*
* Reducer for some global UI state that we want to share between components
* */
const globalUI = (state = Map({ isFetching: false }), action) => {
const globalUI = (state = Map({ isFetching: false, useForkWorkflow: false }), action) => {
// Generic, global loading indicator
if (action.type.indexOf('REQUEST') > -1) {
return state.set('isFetching', true);
} else if (action.type.indexOf('SUCCESS') > -1 || action.type.indexOf('FAILURE') > -1) {
return state.set('isFetching', false);
} else if (action.type === USE_FORK_WORKFLOW) {
return state.set('useForkWorkflow', true);
}
return state;
};