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
parent
4b8d4c495b
commit
4494b0c05b
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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 === '?') {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue