fix(UI): keep portal preview open (#48451)

* moved portal states to redux

stored a global window

open window if does not exist clear htmlif already exists

moved close window to desktop-layout

close portal if user navigates to route outside of challenges

naming conventions, specify url

dispatch setIsAdvancing and use in portal preview

* fix: close portal if main window closes

* fix: refactor it

* Revert "fix: refactor it"

This reverts commit 197a40a3a6.

* feat: add isAdvancing to hotkeys

* fix: revert to pane on first step of block

* fix: safari issues

* fix: revert to pane when not advancing

---------

Co-authored-by: kravmaguy <flex4lease@gmail.com>
Co-authored-by: moT01 <20648924+moT01@users.noreply.github.com>
pull/49272/head
abe 2023-02-06 01:18:46 -06:00 committed by GitHub
parent 4b8d4c495b
commit 4494b0c05b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 199 additions and 54 deletions

View File

@ -277,6 +277,7 @@ export type ChallengeMeta = {
block: string; block: string;
id: string; id: string;
introPath: string; introPath: string;
isFirstStep: boolean;
nextChallengePath: string; nextChallengePath: string;
prevChallengePath: string; prevChallengePath: string;
removeComments: boolean; removeComments: boolean;

View File

@ -1,6 +1,8 @@
import { first } from 'lodash-es'; import { first } from 'lodash-es';
import React, { useState, ReactElement } from 'react'; import React, { useState, useEffect, ReactElement } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles';
import { challengeTypes } from '../../../../utils/challenge-types'; import { challengeTypes } from '../../../../utils/challenge-types';
import { import {
@ -8,6 +10,13 @@ import {
ChallengeFiles, ChallengeFiles,
ResizeProps ResizeProps
} from '../../../redux/prop-types'; } from '../../../redux/prop-types';
import { setShowPreviewPortal, setShowPreviewPane } from '../redux/actions';
import {
portalWindowSelector,
showPreviewPortalSelector,
showPreviewPaneSelector,
isAdvancingToChallengeSelector
} from '../redux/selectors';
import PreviewPortal from '../components/preview-portal'; import PreviewPortal from '../components/preview-portal';
import ActionRow from './action-row'; import ActionRow from './action-row';
@ -21,6 +30,8 @@ interface DesktopLayoutProps {
hasNotes: boolean; hasNotes: boolean;
hasPreview: boolean; hasPreview: boolean;
instructions: ReactElement; instructions: ReactElement;
isAdvancing: boolean;
isFirstStep: boolean;
layoutState: { layoutState: {
codePane: Pane; codePane: Pane;
editorPane: Pane; editorPane: Pane;
@ -34,16 +45,51 @@ interface DesktopLayoutProps {
resizeProps: ResizeProps; resizeProps: ResizeProps;
testOutput: ReactElement; testOutput: ReactElement;
windowTitle: string; windowTitle: string;
showPreviewPortal: boolean;
showPreviewPane: boolean;
setShowPreviewPortal: (arg: boolean) => void;
setShowPreviewPane: (arg: boolean) => void;
portalWindow: null | Window;
} }
const reflexProps = { const reflexProps = {
propagateDimensions: true propagateDimensions: true
}; };
const mapDispatchToProps = {
setShowPreviewPortal,
setShowPreviewPane
};
const mapStateToProps = createSelector(
isAdvancingToChallengeSelector,
showPreviewPortalSelector,
showPreviewPaneSelector,
portalWindowSelector,
(
isAdvancing: boolean,
showPreviewPortal: boolean,
showPreviewPane: boolean,
portalWindow: null | Window
) => ({
isAdvancing,
showPreviewPortal,
showPreviewPane,
portalWindow
})
);
const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => { const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const {
showPreviewPane,
showPreviewPortal,
setShowPreviewPane,
setShowPreviewPortal,
portalWindow
} = props;
const [showNotes, setShowNotes] = useState(false); const [showNotes, setShowNotes] = useState(false);
const [showPreviewPane, setShowPreviewPane] = useState(true);
const [showPreviewPortal, setShowPreviewPortal] = useState(false);
const [showConsole, setShowConsole] = useState(false); const [showConsole, setShowConsole] = useState(false);
const [showInstructions, setShowInstuctions] = useState(true); const [showInstructions, setShowInstuctions] = useState(true);
@ -52,10 +98,12 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
case 'showPreviewPane': case 'showPreviewPane':
if (!showPreviewPane && showPreviewPortal) setShowPreviewPortal(false); if (!showPreviewPane && showPreviewPortal) setShowPreviewPortal(false);
setShowPreviewPane(!showPreviewPane); setShowPreviewPane(!showPreviewPane);
portalWindow?.close();
break; break;
case 'showPreviewPortal': case 'showPreviewPortal':
if (!showPreviewPortal && showPreviewPane) setShowPreviewPane(false); if (!showPreviewPortal && showPreviewPane) setShowPreviewPane(false);
setShowPreviewPortal(!showPreviewPortal); setShowPreviewPortal(!showPreviewPortal);
if (showPreviewPortal) portalWindow?.close();
break; break;
case 'showConsole': case 'showConsole':
setShowConsole(!showConsole); setShowConsole(!showConsole);
@ -88,6 +136,8 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
testOutput, testOutput,
hasNotes, hasNotes,
hasPreview, hasPreview,
isAdvancing,
isFirstStep,
layoutState, layoutState,
notes, notes,
preview, preview,
@ -95,6 +145,18 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
windowTitle windowTitle
} = props; } = props;
// on mount
useEffect(() => {
if (isFirstStep) {
setShowPreviewPortal(false);
portalWindow?.close();
setShowPreviewPane(true);
} else if (!isAdvancing && !showPreviewPane && !showPreviewPortal) {
togglePane('showPreviewPane');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const challengeFile = getChallengeFile(); const challengeFile = getChallengeFile();
const projectBasedChallenge = hasEditableBoundaries; const projectBasedChallenge = hasEditableBoundaries;
const isMultifileCertProject = const isMultifileCertProject =
@ -197,9 +259,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
)} )}
</ReflexContainer> </ReflexContainer>
{displayPreviewPortal && ( {displayPreviewPortal && (
<PreviewPortal togglePane={togglePane} windowTitle={windowTitle}> <PreviewPortal windowTitle={windowTitle}>{preview}</PreviewPortal>
{preview}
</PreviewPortal>
)} )}
</div> </div>
); );
@ -207,4 +267,4 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
DesktopLayout.displayName = 'DesktopLayout'; DesktopLayout.displayName = 'DesktopLayout';
export default DesktopLayout; export default connect(mapStateToProps, mapDispatchToProps)(DesktopLayout);

View File

@ -46,7 +46,8 @@ import {
previewMounted, previewMounted,
updateChallengeMeta, updateChallengeMeta,
openModal, openModal,
setEditorFocusability setEditorFocusability,
setIsAdvancing
} from '../redux/actions'; } from '../redux/actions';
import { import {
challengeFilesSelector, challengeFilesSelector,
@ -84,7 +85,8 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
cancelTests, cancelTests,
previewMounted, previewMounted,
openModal, openModal,
setEditorFocusability setEditorFocusability,
setIsAdvancing
}, },
dispatch dispatch
); );
@ -113,6 +115,7 @@ interface ShowClassicProps {
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
openModal: (modal: string) => void; openModal: (modal: string) => void;
setEditorFocusability: (canFocus: boolean) => void; setEditorFocusability: (canFocus: boolean) => void;
setIsAdvancing: (arg: boolean) => void;
previewMounted: () => void; previewMounted: () => void;
savedChallenges: CompletedChallenge[]; savedChallenges: CompletedChallenge[];
} }
@ -292,6 +295,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
openModal, openModal,
setIsAdvancing,
savedChallenges, savedChallenges,
data: { data: {
challengeNode: { challengeNode: {
@ -327,6 +331,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
helpCategory helpCategory
}); });
challengeMounted(challengeMeta.id); challengeMounted(challengeMeta.id);
setIsAdvancing(false);
} }
componentWillUnmount() { componentWillUnmount() {
@ -480,7 +485,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
const { const {
executeChallenge, executeChallenge,
pageContext: { pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }, challengeMeta: { isFirstStep, nextChallengePath, prevChallengePath },
projectPreview: { challengeData, showProjectPreview } projectPreview: { challengeData, showProjectPreview }
}, },
challengeFiles, challengeFiles,
@ -539,6 +544,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
instructions={this.renderInstructionsPanel({ instructions={this.renderInstructionsPanel({
showToolPanel: true showToolPanel: true
})} })}
isFirstStep={isFirstStep}
layoutState={this.state.layout} layoutState={this.state.layout}
notes={this.renderNotes(notes)} notes={this.renderNotes(notes)}
preview={this.renderPreview()} preview={this.renderPreview()}

View File

@ -4,12 +4,14 @@ import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ChallengeFiles, Test, User } from '../../../redux/prop-types'; import { ChallengeFiles, Test, User } from '../../../redux/prop-types';
import { isChallenge } from '../../../utils/path-parsers';
import { userSelector } from '../../../redux/selectors'; import { userSelector } from '../../../redux/selectors';
import { import {
setEditorFocusability, setEditorFocusability,
submitChallenge, submitChallenge,
openModal openModal,
setIsAdvancing
} from '../redux/actions'; } from '../redux/actions';
import { import {
canFocusEditorSelector, canFocusEditorSelector,
@ -40,7 +42,8 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = { const mapDispatchToProps = {
setEditorFocusability, setEditorFocusability,
submitChallenge, submitChallenge,
openShortcutsModal: () => openModal('shortcuts') openShortcutsModal: () => openModal('shortcuts'),
setIsAdvancing
}; };
const keyMap = { const keyMap = {
@ -66,6 +69,7 @@ interface HotkeysProps {
nextChallengePath: string; nextChallengePath: string;
prevChallengePath: string; prevChallengePath: string;
setEditorFocusability: (arg0: boolean) => void; setEditorFocusability: (arg0: boolean) => void;
setIsAdvancing: (arg0: boolean) => void;
tests: Test[]; tests: Test[];
usesMultifileEditor?: boolean; usesMultifileEditor?: boolean;
openShortcutsModal: () => void; openShortcutsModal: () => void;
@ -83,6 +87,7 @@ function Hotkeys({
nextChallengePath, nextChallengePath,
prevChallengePath, prevChallengePath,
setEditorFocusability, setEditorFocusability,
setIsAdvancing,
submitChallenge, submitChallenge,
tests, tests,
usesMultifileEditor, usesMultifileEditor,
@ -130,10 +135,16 @@ function Hotkeys({
}, },
navigationMode: () => setEditorFocusability(false), navigationMode: () => setEditorFocusability(false),
navigatePrev: () => { navigatePrev: () => {
if (!canFocusEditor) void navigate(prevChallengePath); if (!canFocusEditor) {
if (isChallenge(prevChallengePath)) setIsAdvancing(true);
void navigate(prevChallengePath);
}
}, },
navigateNext: () => { navigateNext: () => {
if (!canFocusEditor) void navigate(nextChallengePath); if (!canFocusEditor) {
if (isChallenge(nextChallengePath)) setIsAdvancing(true);
void navigate(nextChallengePath);
}
}, },
showShortcuts: (e: React.KeyboardEvent) => { showShortcuts: (e: React.KeyboardEvent) => {
if (!canFocusEditor && e.shiftKey && e.key === '?') { if (!canFocusEditor && e.shiftKey && e.key === '?') {

View File

@ -2,35 +2,60 @@ import { Component, ReactElement } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { TFunction, withTranslation } from 'react-i18next'; import { TFunction, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { storePortalDocument, removePortalDocument } from '../redux/actions'; import { createSelector } from 'reselect';
import {
storePortalWindow,
removePortalWindow,
setShowPreviewPortal,
setIsAdvancing
} from '../redux/actions';
import {
portalWindowSelector,
isAdvancingToChallengeSelector
} from '../redux/selectors';
interface PreviewPortalProps { interface PreviewPortalProps {
children: ReactElement | null; children: ReactElement | null;
togglePane: (pane: string) => void;
windowTitle: string; windowTitle: string;
t: TFunction; t: TFunction;
storePortalDocument: (document: Document | undefined) => void; storePortalWindow: (window: Window | null) => void;
removePortalDocument: () => void; removePortalWindow: () => void;
portalWindow: null | Window;
setShowPreviewPortal: (arg: boolean) => void;
setIsAdvancing: (arg: boolean) => void;
isAdvancing: boolean;
} }
const mapDispatchToProps = { const mapDispatchToProps = {
storePortalDocument, storePortalWindow,
removePortalDocument removePortalWindow,
setShowPreviewPortal,
setIsAdvancing
}; };
const mapStateToProps = createSelector(
isAdvancingToChallengeSelector,
portalWindowSelector,
(isAdvancing: boolean, portalWindow: null | Window) => ({
isAdvancing,
portalWindow
})
);
class PreviewPortal extends Component<PreviewPortalProps> { class PreviewPortal extends Component<PreviewPortalProps> {
static displayName = 'PreviewPortal'; static displayName = 'PreviewPortal';
mainWindow: Window; mainWindow: Window;
externalWindow: Window | null = null; externalWindow: Window | null = null;
isAdvancing: boolean;
containerEl; containerEl;
titleEl; titleEl;
styleEl; styleEl;
constructor(props: PreviewPortalProps) { constructor(props: PreviewPortalProps) {
super(props); super(props);
this.mainWindow = window; this.mainWindow = window;
this.externalWindow = null; this.externalWindow = this.props.portalWindow;
this.isAdvancing = this.props.isAdvancing;
this.containerEl = document.createElement('div'); this.containerEl = document.createElement('div');
this.titleEl = document.createElement('title'); this.titleEl = document.createElement('title');
this.styleEl = document.createElement('style'); this.styleEl = document.createElement('style');
@ -39,6 +64,17 @@ class PreviewPortal extends Component<PreviewPortalProps> {
componentDidMount() { componentDidMount() {
const { t, windowTitle } = this.props; const { t, windowTitle } = this.props;
if (!this.externalWindow) {
this.externalWindow = window.open(
'',
'',
'width=960,height=540,left=100,top=100'
);
} else {
this.externalWindow.document.head.innerHTML = '';
this.externalWindow.document.body.innerHTML = '';
}
this.titleEl.innerText = `${t( this.titleEl.innerText = `${t(
'learn.editor-tabs.preview' 'learn.editor-tabs.preview'
)} | ${windowTitle}`; )} | ${windowTitle}`;
@ -51,12 +87,6 @@ class PreviewPortal extends Component<PreviewPortalProps> {
} }
`; `;
this.externalWindow = window.open(
'',
'',
'width=960,height=540,left=100,top=100'
);
this.externalWindow?.document.head.appendChild(this.titleEl); this.externalWindow?.document.head.appendChild(this.titleEl);
this.externalWindow?.document.head.appendChild(this.styleEl); this.externalWindow?.document.head.appendChild(this.styleEl);
this.externalWindow?.document.body.setAttribute( this.externalWindow?.document.body.setAttribute(
@ -69,19 +99,23 @@ class PreviewPortal extends Component<PreviewPortalProps> {
); );
this.externalWindow?.document.body.appendChild(this.containerEl); this.externalWindow?.document.body.appendChild(this.containerEl);
this.externalWindow?.addEventListener('beforeunload', () => { this.externalWindow?.addEventListener('beforeunload', () => {
this.props.togglePane('showPreviewPortal'); this.props.setShowPreviewPortal(false);
}); });
this.props.storePortalDocument(this.externalWindow?.document); this.props.storePortalWindow(this.externalWindow);
// close the portal if the main window closes
this.mainWindow?.addEventListener('beforeunload', () => { this.mainWindow?.addEventListener('beforeunload', () => {
this.externalWindow?.close(); this.externalWindow?.close();
}); });
} }
componentWillUnmount() { componentWillUnmount() {
this.externalWindow?.close(); if (!this.props.isAdvancing) {
this.props.removePortalDocument(); this.externalWindow?.close();
}
this.props.removePortalWindow();
this.props.setIsAdvancing(false);
} }
render() { render() {
@ -92,6 +126,6 @@ class PreviewPortal extends Component<PreviewPortalProps> {
PreviewPortal.displayName = 'PreviewPortal'; PreviewPortal.displayName = 'PreviewPortal';
export default connect( export default connect(
null, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(withTranslation()(PreviewPortal)); )(withTranslation()(PreviewPortal));

View File

@ -28,14 +28,16 @@ export const actionTypes = createTypes(
'storedCodeFound', 'storedCodeFound',
'noStoredCodeFound', 'noStoredCodeFound',
'saveEditorContent', 'saveEditorContent',
'setShowPreviewPane',
'setShowPreviewPortal',
'closeModal', 'closeModal',
'openModal', 'openModal',
'setIsAdvancing',
'previewMounted', 'previewMounted',
'projectPreviewMounted', 'projectPreviewMounted',
'storePortalDocument', 'storePortalWindow',
'removePortalDocument', 'removePortalWindow',
'challengeMounted', 'challengeMounted',
'checkChallenge', 'checkChallenge',
'executeChallenge', 'executeChallenge',

View File

@ -22,7 +22,6 @@ export const createQuestion = createAction(actionTypes.createQuestion);
export const initTests = createAction(actionTypes.initTests); export const initTests = createAction(actionTypes.initTests);
export const updateTests = createAction(actionTypes.updateTests); export const updateTests = createAction(actionTypes.updateTests);
export const cancelTests = createAction(actionTypes.cancelTests); export const cancelTests = createAction(actionTypes.cancelTests);
export const initConsole = createAction(actionTypes.initConsole); export const initConsole = createAction(actionTypes.initConsole);
export const initLogs = createAction(actionTypes.initLogs); export const initLogs = createAction(actionTypes.initLogs);
export const updateChallengeMeta = createAction( export const updateChallengeMeta = createAction(
@ -37,6 +36,10 @@ export const updateSolutionFormValues = createAction(
export const updateSuccessMessage = createAction( export const updateSuccessMessage = createAction(
actionTypes.updateSuccessMessage actionTypes.updateSuccessMessage
); );
export const setShowPreviewPortal = createAction(
actionTypes.setShowPreviewPortal
);
export const setShowPreviewPane = createAction(actionTypes.setShowPreviewPane);
export const logsToConsole = createAction(actionTypes.logsToConsole); export const logsToConsole = createAction(actionTypes.logsToConsole);
@ -48,7 +51,7 @@ export const disableBuildOnError = createAction(
export const storedCodeFound = createAction(actionTypes.storedCodeFound); export const storedCodeFound = createAction(actionTypes.storedCodeFound);
export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound); export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound);
export const saveEditorContent = createAction(actionTypes.saveEditorContent); export const saveEditorContent = createAction(actionTypes.saveEditorContent);
export const setIsAdvancing = createAction(actionTypes.setIsAdvancing);
export const closeModal = createAction(actionTypes.closeModal); export const closeModal = createAction(actionTypes.closeModal);
export const openModal = createAction(actionTypes.openModal); export const openModal = createAction(actionTypes.openModal);
@ -57,12 +60,8 @@ export const projectPreviewMounted = createAction(
actionTypes.projectPreviewMounted actionTypes.projectPreviewMounted
); );
export const storePortalDocument = createAction( export const storePortalWindow = createAction(actionTypes.storePortalWindow);
actionTypes.storePortalDocument export const removePortalWindow = createAction(actionTypes.removePortalWindow);
);
export const removePortalDocument = createAction(
actionTypes.removePortalDocument
);
export const challengeMounted = createAction(actionTypes.challengeMounted); export const challengeMounted = createAction(actionTypes.challengeMounted);
export const checkChallenge = createAction(actionTypes.checkChallenge); export const checkChallenge = createAction(actionTypes.checkChallenge);

View File

@ -3,7 +3,7 @@ import { omit } from 'lodash-es';
import { ofType } from 'redux-observable'; import { ofType } from 'redux-observable';
import { empty, of } from 'rxjs'; import { empty, of } from 'rxjs';
import { catchError, concat, retry, switchMap, tap } from 'rxjs/operators'; import { catchError, concat, retry, switchMap, tap } from 'rxjs/operators';
import { isChallenge } from '../../../utils/path-parsers';
import { challengeTypes, submitTypes } from '../../../../utils/challenge-types'; import { challengeTypes, submitTypes } from '../../../../utils/challenge-types';
import { actionTypes as submitActionTypes } from '../../../redux/action-types'; import { actionTypes as submitActionTypes } from '../../../redux/action-types';
import { import {
@ -16,7 +16,11 @@ import { mapFilesToChallengeFiles } from '../../../utils/ajax';
import { standardizeRequestBody } from '../../../utils/challenge-request-helpers'; import { standardizeRequestBody } from '../../../utils/challenge-request-helpers';
import postUpdate$ from '../utils/post-update'; import postUpdate$ from '../utils/post-update';
import { actionTypes } from './action-types'; import { actionTypes } from './action-types';
import { closeModal, updateSolutionFormValues } from './actions'; import {
closeModal,
updateSolutionFormValues,
setIsAdvancing
} from './actions';
import { import {
challengeFilesSelector, challengeFilesSelector,
challengeMetaSelector, challengeMetaSelector,
@ -174,6 +178,7 @@ export default function completionEpic(action$, state$) {
}; };
return submitter(type, state).pipe( return submitter(type, state).pipe(
concat(of(setIsAdvancing(isChallenge(pathToNavigateTo())))),
tap(res => { tap(res => {
if (res.type !== submitActionTypes.updateFailed) { if (res.type !== submitActionTypes.updateFailed) {
navigate(pathToNavigateTo()); navigate(pathToNavigateTo());

View File

@ -41,9 +41,12 @@ const initialState = {
projectPreview: false, projectPreview: false,
shortcuts: false shortcuts: false
}, },
portalDocument: false, portalWindow: null,
showPreviewPortal: false,
showPreviewPane: true,
projectFormValues: {}, projectFormValues: {},
successMessage: 'Happy Coding!' successMessage: 'Happy Coding!',
isAdvancing: false
}; };
export const epics = [ export const epics = [
@ -178,18 +181,30 @@ export const reducer = handleActions(
...state, ...state,
isBuildEnabled: false isBuildEnabled: false
}), }),
[actionTypes.storePortalDocument]: (state, { payload }) => ({ [actionTypes.setShowPreviewPortal]: (state, { payload }) => ({
...state, ...state,
portalDocument: payload showPreviewPortal: payload
}), }),
[actionTypes.removePortalDocument]: state => ({ [actionTypes.setShowPreviewPane]: (state, { payload }) => ({
...state, ...state,
portalDocument: false showPreviewPane: payload
}),
[actionTypes.storePortalWindow]: (state, { payload }) => ({
...state,
portalWindow: payload
}),
[actionTypes.removePortalWindow]: state => ({
...state,
portalWindow: null
}), }),
[actionTypes.updateSuccessMessage]: (state, { payload }) => ({ [actionTypes.updateSuccessMessage]: (state, { payload }) => ({
...state, ...state,
successMessage: payload successMessage: payload
}), }),
[actionTypes.setIsAdvancing]: (state, { payload }) => ({
...state,
isAdvancing: payload
}),
[actionTypes.closeModal]: (state, { payload }) => ({ [actionTypes.closeModal]: (state, { payload }) => ({
...state, ...state,
modal: { modal: {

View File

@ -29,8 +29,9 @@ export const successMessageSelector = state => state[ns].successMessage;
export const projectFormValuesSelector = state => export const projectFormValuesSelector = state =>
state[ns].projectFormValues || {}; state[ns].projectFormValues || {};
export const isAdvancingToChallengeSelector = state => state[ns].isAdvancing;
export const portalDocumentSelector = state => state[ns].portalDocument; export const portalDocumentSelector = state => state[ns].portalWindow?.document;
export const portalWindowSelector = state => state[ns].portalWindow;
export const challengeDataSelector = state => { export const challengeDataSelector = state => {
const { challengeType } = challengeMetaSelector(state); const { challengeType } = challengeMetaSelector(state);
@ -84,3 +85,5 @@ export const challengeDataSelector = state => {
export const attemptsSelector = state => state[ns].attempts; export const attemptsSelector = state => state[ns].attempts;
export const canFocusEditorSelector = state => state[ns].canFocusEditor; export const canFocusEditorSelector = state => state[ns].canFocusEditor;
export const visibleEditorsSelector = state => state[ns].visibleEditors; export const visibleEditorsSelector = state => state[ns].visibleEditors;
export const showPreviewPortalSelector = state => state[ns].showPreviewPortal;
export const showPreviewPaneSelector = state => state[ns].showPreviewPane;

View File

@ -48,6 +48,14 @@ const views = {
// quiz: Quiz // quiz: Quiz
}; };
function getIsFirstStep(_node, index, nodeArray) {
const current = nodeArray[index];
const previous = nodeArray[index - 1];
if (!previous) return true;
return previous.node.challenge.block !== current.node.challenge.block;
}
function getNextChallengePath(_node, index, nodeArray) { function getNextChallengePath(_node, index, nodeArray) {
const next = nodeArray[index + 1]; const next = nodeArray[index + 1];
return next ? next.node.challenge.fields.slug : '/learn'; return next ? next.node.challenge.fields.slug : '/learn';
@ -85,6 +93,7 @@ exports.createChallengePages = function (createPage) {
certification, certification,
superBlock, superBlock,
block, block,
isFirstStep: getIsFirstStep(challenge, index, allChallengeEdges),
template, template,
required, required,
nextChallengePath: getNextChallengePath( nextChallengePath: getNextChallengePath(