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:
committed by
Shawn Erquhart
parent
41559256d0
commit
edf0a3afdc
@ -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 => {
|
||||
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
Reference in New Issue
Block a user