提交 68c062f4 编写于 作者: J Jason Park

Refactor redux stores

上级 a056b266
......@@ -7,7 +7,7 @@ const sandbox = code => {
eval(code);
};
onmessage = e => { // TODO: stop after the first delay() on the initial run
onmessage = e => {
const lines = e.data.split('\n').map((line, i) => line.replace(/(.+\. *delay *)(\( *\))/g, `$1(${i})`));
const { code } = Babel.transform(lines.join('\n'), { presets: ['es2015'] });
sandbox(code);
......
......@@ -30,6 +30,14 @@ const createProjectFile = (name, content) => createFile(name, content, [{
const createUserFile = (name, content) => createFile(name, content, undefined);
const isSaved = ({ titles, files, lastTitles, lastFiles }) => {
const serialize = (titles, files) => JSON.stringify({
titles,
files: files.map(({ name, content }) => ({ name, content })),
});
return serialize(titles, files) === serialize(lastTitles, lastFiles);
};
export {
classes,
distance,
......@@ -38,4 +46,5 @@ export {
createFile,
createProjectFile,
createUserFile,
isSaved,
};
......@@ -3,10 +3,7 @@ import Cookies from 'js-cookie';
import { connect } from 'react-redux';
import Promise from 'bluebird';
import { Helmet } from 'react-helmet';
import AutosizeInput from 'react-input-autosize';
import queryString from 'query-string';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faPlus from '@fortawesome/fontawesome-free-solid/faPlus';
import {
BaseComponent,
CodeEditor,
......@@ -32,7 +29,6 @@ class App extends BaseComponent {
this.state = {
navigatorOpened: true,
workspaceWeights: [1, 2, 2],
editorTabIndex: -1,
};
this.codeEditorRef = React.createRef();
......@@ -79,11 +75,12 @@ class App extends BaseComponent {
toggleHistoryBlock(enable = !this.unblock) {
if (enable) {
const { saved } = this.props.current;
const warningMessage = 'Are you sure you want to discard changes?';
window.onbeforeunload = () => this.isSaved() ? undefined : warningMessage;
window.onbeforeunload = () => saved ? undefined : warningMessage;
this.unblock = this.props.history.block((location) => {
if (location.pathname === this.props.location.pathname) return;
if (!this.isSaved()) return warningMessage;
if (!saved) return warningMessage;
});
} else {
window.onbeforeunload = undefined;
......@@ -189,11 +186,11 @@ class App extends BaseComponent {
selectDefaultTab() {
const { ext } = this.props.env;
const { files } = this.props.current;
let editorTabIndex = files.findIndex(file => extension(file.name) === 'json');
if (!~editorTabIndex) files.findIndex(file => extension(file.name) === ext);
if (!~editorTabIndex) editorTabIndex = files.findIndex(file => exts.includes(extension(file.name)));
if (!~editorTabIndex) editorTabIndex = Math.min(0, files.length - 1);
this.handleChangeEditorTabIndex(editorTabIndex);
const editingFile = files.find(file => extension(file.name) === 'json') ||
files.find(file => extension(file.name) === ext) ||
files.find(file => exts.includes(extension(file.name))) ||
files[files.length - 1];
this.props.setEditingFile(editingFile);
}
handleChangeWorkspaceWeights(workspaceWeights) {
......@@ -201,67 +198,15 @@ class App extends BaseComponent {
this.codeEditorRef.current.getWrappedInstance().handleResize();
}
handleChangeEditorTabIndex(editorTabIndex) {
const { files } = this.props.current;
if (editorTabIndex === files.length) this.handleAddFile();
this.setState({ editorTabIndex });
this.props.shouldBuild();
}
handleAddFile() {
const { ext } = this.props.env;
const { files } = this.props.current;
const language = languages.find(language => language.ext === ext);
const file = { ...language.skeleton };
let count = 0;
while (files.some(existingFile => existingFile.name === file.name)) file.name = `code-${++count}.${ext}`;
this.props.addFile(file);
}
handleRenameFile(e) {
const { value } = e.target;
const { editorTabIndex } = this.state;
this.props.renameFile(editorTabIndex, value);
}
handleDeleteFile() {
const { editorTabIndex } = this.state;
const { files } = this.props.current;
this.handleChangeEditorTabIndex(Math.min(editorTabIndex, files.length - 2));
this.props.deleteFile(editorTabIndex);
}
toggleNavigatorOpened(navigatorOpened = !this.state.navigatorOpened) {
this.setState({ navigatorOpened });
}
isSaved() {
const { titles, files, lastTitles, lastFiles } = this.props.current;
const serialize = (titles, files) => JSON.stringify({
titles,
files: files.map(({ name, content }) => ({ name, content })),
});
return serialize(titles, files) === serialize(lastTitles, lastFiles);
}
render() {
const { navigatorOpened, workspaceWeights, editorTabIndex } = this.state;
const { navigatorOpened, workspaceWeights } = this.state;
const { files, titles, description } = this.props.current;
const saved = this.isSaved();
const { titles, description, saved } = this.props.current;
const title = `${saved ? '' : '(Unsaved) '}${titles.join(' - ')}`;
const file = files[editorTabIndex];
const editorTitles = files.map(file => file.name);
if (file) {
editorTitles[editorTabIndex] = (
<AutosizeInput className={styles.input_title} value={file.name}
onClick={e => e.stopPropagation()} onChange={e => this.handleRenameFile(e)} />
);
}
editorTitles.push(
<FontAwesomeIcon fixedWidth icon={faPlus} />,
);
return (
<div className={styles.app}>
......@@ -269,17 +214,16 @@ class App extends BaseComponent {
<title>{title}</title>
<meta name="description" content={description} />
</Helmet>
<Header className={styles.header} onClickTitleBar={() => this.toggleNavigatorOpened()} saved={saved}
navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()} file={file}
<Header className={styles.header} onClickTitleBar={() => this.toggleNavigatorOpened()}
navigatorOpened={navigatorOpened} loadScratchPapers={() => this.loadScratchPapers()}
ignoreHistoryBlock={this.ignoreHistoryBlock} />
<ResizableContainer className={styles.workspace} horizontal weights={workspaceWeights}
visibles={[navigatorOpened, true, true]}
onChangeWeights={weights => this.handleChangeWorkspaceWeights(weights)}>
<Navigator />
<VisualizationViewer className={styles.visualization_viewer} />
<TabContainer className={styles.editor_tab_container} titles={editorTitles} tabIndex={editorTabIndex}
onChangeTabIndex={tabIndex => this.handleChangeEditorTabIndex(tabIndex)}>
<CodeEditor ref={this.codeEditorRef} file={file} onClickDelete={() => this.handleDeleteFile()} />
<TabContainer className={styles.editor_tab_container}>
<CodeEditor ref={this.codeEditorRef} />
</TabContainer>
</ResizableContainer>
<ToastContainer className={styles.toast_container} />
......
......@@ -60,16 +60,6 @@ button {
}
.editor_tab_container {
.input_title {
input {
&:hover,
&:focus {
margin: -4px;
padding: 4px;
background-color: $theme-normal;
}
}
}
}
}
......
......@@ -8,7 +8,7 @@ import { languages } from '/common/config';
import { Button, Ellipsis, FoldableAceEditor } from '/components';
import styles from './stylesheet.scss';
@connect(({ env, player }) => ({ env, player }), actions, null, { withRef: true })
@connect(({ current, env, player }) => ({ current, env, player }), actions, null, { withRef: true })
class CodeEditor extends React.Component {
constructor(props) {
super(props);
......@@ -16,24 +16,19 @@ class CodeEditor extends React.Component {
this.aceEditorRef = React.createRef();
}
handleChangeCode(code) {
const { file } = this.props;
this.props.modifyFile({ ...file, content: code });
if (extension(file.name) === 'md') this.props.shouldBuild();
}
handleResize() {
this.aceEditorRef.current.editor.resize();
}
render() {
const { className, file, onClickDelete } = this.props;
const { className } = this.props;
const { editingFile } = this.props.current;
const { user } = this.props.env;
const { lineIndicator, buildAt } = this.props.player;
const { lineIndicator } = this.props.player;
if (!file) return null;
if (!editingFile) return null;
const fileExt = extension(file.name);
const fileExt = extension(editingFile.name);
const language = languages.find(language => language.ext === fileExt);
const mode = language ? language.mode :
fileExt === 'md' ? 'markdown' :
......@@ -49,7 +44,7 @@ class CodeEditor extends React.Component {
theme="tomorrow_night_eighties"
name="code_editor"
editorProps={{ $blockScrolling: true }}
onChange={code => this.handleChangeCode(code)}
onChange={code => this.props.modifyFile(editingFile, code)}
markers={lineIndicator ? [{
startRow: lineIndicator.lineNumber,
startCol: 0,
......@@ -60,12 +55,11 @@ class CodeEditor extends React.Component {
inFront: true,
_key: lineIndicator.cursor,
}] : []}
foldAt={buildAt}
value={file.content} />
value={editingFile.content} />
<div className={classes(styles.contributors_viewer, className)}>
<span className={classes(styles.contributor, styles.label)}>Contributed by</span>
{
(file.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => (
(editingFile.contributors || [user || { login: 'guest', avatar_url: faUser }]).map(contributor => (
<Button className={styles.contributor} icon={contributor.avatar_url} key={contributor.login}
href={`https://github.com/${contributor.login}`}>
{contributor.login}
......@@ -74,8 +68,8 @@ class CodeEditor extends React.Component {
}
<div className={styles.empty}>
<div className={styles.empty} />
<Button className={styles.delete} icon={faTrashAlt} primary onClick={onClickDelete}
confirmNeeded>
<Button className={styles.delete} icon={faTrashAlt} primary confirmNeeded
onClick={() => this.props.deleteFile(editingFile)}>
<Ellipsis>Delete File</Ellipsis>
</Button>
</div>
......
import React from 'react';
import { connect } from 'react-redux';
import AceEditor from 'react-ace';
import 'brace/mode/plain_text';
import 'brace/mode/markdown';
......@@ -8,19 +9,23 @@ import 'brace/mode/c_cpp';
import 'brace/mode/java';
import 'brace/theme/tomorrow_night_eighties';
import 'brace/ext/searchbox';
import { actions } from '/reducers';
@connect(({ current }) => ({ current }), actions)
class FoldableAceEditor extends AceEditor {
componentDidMount() {
super.componentDidMount();
this.foldTracers();
const { shouldBuild } = this.props.current;
if (shouldBuild) this.foldTracers();
}
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
if (prevProps.foldAt !== this.props.foldAt) {
this.foldTracers();
const { editingFile, shouldBuild } = this.props.current;
if (editingFile !== prevProps.current.editingFile) {
if (shouldBuild) this.foldTracers();
}
}
......
......@@ -110,8 +110,8 @@ class Header extends BaseComponent {
}
render() {
const { className, onClickTitleBar, navigatorOpened, saved, file } = this.props;
const { scratchPaper, titles } = this.props.current;
const { className, onClickTitleBar, navigatorOpened } = this.props;
const { scratchPaper, titles, saved } = this.props.current;
const { ext, user } = this.props.env;
const permitted = this.hasPermission();
......@@ -174,7 +174,7 @@ class Header extends BaseComponent {
</div>
</Button>
</div>
<Player className={styles.section} file={file} />
<Player className={styles.section} />
</div>
</header>
);
......
......@@ -13,7 +13,7 @@ import { actions } from '/reducers';
import { BaseComponent, Button, ProgressBar } from '/components';
import styles from './stylesheet.scss';
@connect(({ player }) => ({ player }), actions)
@connect(({ current, player }) => ({ current, player }), actions)
class Player extends BaseComponent {
constructor(props) {
super(props);
......@@ -30,15 +30,14 @@ class Player extends BaseComponent {
}
componentDidMount() {
const { file } = this.props;
this.build(file);
const { editingFile, shouldBuild } = this.props.current;
if (shouldBuild) this.build(editingFile);
}
componentWillReceiveProps(nextProps) {
const { file } = nextProps;
const { buildAt } = nextProps.player;
if (buildAt !== this.props.player.buildAt) {
this.build(file);
const { editingFile, shouldBuild } = nextProps.current;
if (editingFile !== this.props.current.editingFile) {
if (shouldBuild) this.build(editingFile);
}
}
......@@ -145,13 +144,15 @@ class Player extends BaseComponent {
}
render() {
const { className, file } = this.props;
const { className } = this.props;
const { editingFile } = this.props.current;
const { chunks, cursor } = this.props.player;
const { speed, playing, building } = this.state;
return (
<div className={classes(styles.player, className)}>
<Button icon={faWrench} primary disabled={building} inProgress={building} onClick={() => this.build(file)}>
<Button icon={faWrench} primary disabled={building} inProgress={building}
onClick={() => this.build(editingFile)}>
{building ? 'Building' : 'Build'}
</Button>
{
......
import React from 'react';
import { connect } from 'react-redux';
import AutosizeInput from 'react-input-autosize';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import faPlus from '@fortawesome/fontawesome-free-solid/faPlus';
import { classes } from '/common/util';
import { actions } from '/reducers';
import { languages } from '/common/config';
import styles from './stylesheet.scss';
@connect(({ current, env }) => ({ current, env }), actions)
class TabContainer extends React.Component {
handleAddFile() {
const { ext } = this.props.env;
const { files } = this.props.current;
const language = languages.find(language => language.ext === ext);
const newFile = { ...language.skeleton };
let count = 0;
while (files.some(file => file.name === newFile.name)) newFile.name = `code-${++count}.${ext}`;
this.props.addFile(newFile);
}
render() {
const { className, children, titles, tabIndex, onChangeTabIndex } = this.props;
const { className, children } = this.props;
const { editingFile, files } = this.props.current;
return (
<div className={classes(styles.tab_container, className)}>
<div className={styles.tab_bar}>
<div className={classes(styles.title, styles.fake)} />
{
titles.map((title, i) => {
const selected = i === tabIndex;
return (
<div className={classes(styles.title, selected && styles.selected)} key={i}
onClick={() => onChangeTabIndex(i)}>
{title}
</div>
);
})
files.map((file, i) => file === editingFile ? (
<div className={classes(styles.title, styles.selected)} key={i}
onClick={() => this.props.setEditingFile(file)}>
<AutosizeInput className={styles.input_title} value={file.name}
onClick={e => e.stopPropagation()}
onChange={e => this.props.renameFile(file, e.target.value)} />
</div>
) : (
<div className={styles.title} key={i} onClick={() => this.props.setEditingFile(file)}>
{file.name}
</div>
))
}
<div className={styles.title} onClick={() => this.handleAddFile()}>
<FontAwesomeIcon fixedWidth icon={faPlus} />
</div>
<div className={classes(styles.title, styles.fake)} />
</div>
<div className={styles.content}>
......
......@@ -25,6 +25,17 @@
margin: 0;
border-bottom: 1px solid $theme-light;
.input_title {
input {
&:hover,
&:focus {
margin: -4px;
padding: 4px;
background-color: $theme-normal;
}
}
}
&.selected {
border-left: 1px solid $theme-light;
border-right: 1px solid $theme-light;
......@@ -56,4 +67,4 @@
background-color: $theme-dark;
overflow: hidden;
}
}
\ No newline at end of file
}
import { combineActions, createAction, handleActions } from 'redux-actions';
import { README_MD } from '/files';
import { extension, isSaved } from '/common/util';
const prefix = 'CURRENT';
const setHome = createAction(`${prefix}/SET_HOME`, () => defaultState);
const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => {
const titles = [categoryName, algorithmName];
return {
algorithm: { categoryKey, algorithmKey },
scratchPaper: undefined,
titles,
files,
lastTitles: titles,
lastFiles: files,
description,
};
});
const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => {
const titles = ['Scratch Paper', title];
return {
algorithm: undefined,
scratchPaper: { login, gistId },
titles,
files,
lastTitles: titles,
lastFiles: files,
description: homeDescription,
};
});
const setAlgorithm = createAction(`${prefix}/SET_ALGORITHM`, ({ categoryKey, categoryName, algorithmKey, algorithmName, files, description }) => ({
algorithm: { categoryKey, algorithmKey },
titles: [categoryName, algorithmName],
files,
description,
}));
const setScratchPaper = createAction(`${prefix}/SET_SCRATCH_PAPER`, ({ login, gistId, title, files }) => ({
scratchPaper: { login, gistId },
titles: ['Scratch Paper', title],
files,
description: homeDescription,
}));
const setEditingFile = createAction(`${prefix}/SET_EDITING_FILE`, file => ({ file }));
const modifyTitle = createAction(`${prefix}/MODIFY_TITLE`, title => ({ title }));
const addFile = createAction(`${prefix}/ADD_FILE`, file => ({ file }));
const modifyFile = createAction(`${prefix}/MODIFY_FILE`, file => ({ file }));
const deleteFile = createAction(`${prefix}/DELETE_FILE`, index => ({ index }));
const renameFile = createAction(`${prefix}/RENAME_FILE`, (index, name) => ({ index, name }));
const renameFile = createAction(`${prefix}/RENAME_FILE`, (file, name) => ({ file, name }));
const modifyFile = createAction(`${prefix}/MODIFY_FILE`, (file, content) => ({ file, content }));
const deleteFile = createAction(`${prefix}/DELETE_FILE`, file => ({ file }));
export const actions = {
setHome,
setAlgorithm,
setScratchPaper,
setEditingFile,
modifyTitle,
addFile,
modifyFile,
......@@ -59,6 +50,9 @@ const defaultState = {
lastTitles: homeTitles,
lastFiles: homeFiles,
description: homeDescription,
editingFile: undefined,
shouldBuild: true,
saved: true,
};
export default handleActions({
......@@ -66,35 +60,85 @@ export default handleActions({
setHome,
setAlgorithm,
setScratchPaper,
)]: (state, { payload }) => ({
...state,
...payload,
}),
)]: (state, { payload }) => {
const { algorithm, scratchPaper, titles, files, description } = payload;
return {
...state,
algorithm,
scratchPaper,
titles,
files,
lastTitles: titles,
lastFiles: files,
description,
editingFile: undefined,
shouldBuild: true,
saved: true,
};
},
[setEditingFile]: (state, { payload }) => {
const { file } = payload;
return {
...state,
editingFile: file,
shouldBuild: true,
};
},
[modifyTitle]: (state, { payload }) => {
const { title } = payload;
return {
const newState = {
...state,
titles: [state.titles[0], title],
};
return {
...newState,
saved: isSaved(newState),
};
},
[addFile]: (state, { payload }) => {
const { file } = payload;
const files = [...state.files, file];
return { ...state, files };
const newState = {
...state,
files: [...state.files, file],
editingFile: file,
shouldBuild: true,
};
return {
...newState,
saved: isSaved(newState),
};
},
[modifyFile]: (state, { payload }) => {
const { file } = payload;
const files = state.files.map(oldFile => oldFile.name === file.name ? file : oldFile);
return { ...state, files };
[combineActions(
renameFile,
modifyFile,
)]: (state, { payload }) => {
const { file, ...update } = payload;
const editingFile = { ...file, ...update };
const newState = {
...state,
files: state.files.map(oldFile => oldFile === file ? editingFile : oldFile),
editingFile,
shouldBuild: extension(editingFile.name) === 'md',
};
return {
...newState,
saved: isSaved(newState),
};
},
[deleteFile]: (state, { payload }) => {
const { index } = payload;
const files = state.files.filter((file, i) => i !== index);
return { ...state, files };
},
[renameFile]: (state, { payload }) => {
const { index, name } = payload;
const files = state.files.map((file, i) => i === index ? { ...file, name } : file);
return { ...state, files };
const { file } = payload;
const index = state.files.indexOf(file);
const files = state.files.filter(oldFile => oldFile !== file);
const editingFile = files[Math.min(index, files.length - 1)];
const newState = {
...state,
files,
editingFile,
shouldBuild: true,
};
return {
...newState,
saved: isSaved(newState),
};
},
}, defaultState);
......@@ -2,20 +2,17 @@ import { combineActions, createAction, handleActions } from 'redux-actions';
const prefix = 'PLAYER';
const shouldBuild = createAction(`${prefix}/SHOULD_BUILD`, () => ({ buildAt: Date.now() }));
const setChunks = createAction(`${prefix}/SET_CHUNKS`, chunks => ({ chunks }));
const setCursor = createAction(`${prefix}/SET_CURSOR`, cursor => ({ cursor }));
const setLineIndicator = createAction(`${prefix}/SET_LINE_INDICATOR`, lineIndicator => ({ lineIndicator }));
export const actions = {
shouldBuild,
setChunks,
setCursor,
setLineIndicator,
};
const defaultState = {
buildAt: 0,
chunks: [],
cursor: 0,
lineIndicator: undefined,
......@@ -23,7 +20,6 @@ const defaultState = {
export default handleActions({
[combineActions(
shouldBuild,
setChunks,
setCursor,
setLineIndicator,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册