flow: stuff

This commit is contained in:
cha0s 2019-03-05 23:20:26 -06:00
parent b32f879ccb
commit 89952ec168
41 changed files with 666 additions and 298 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/frontend/old

View File

@ -1,5 +1,3 @@
const {invokeHookSerial} = require('@truss/truss');
const {Responder} = require('./response.dev');
let responder = new Responder();
@ -7,24 +5,21 @@ responder.start();
module.exports = () => ({
'truss/http-get': (action) => {
const {payload: {headers}} = action;
delete headers.host;
delete headers.connection;
return responder.respond(action).then(({html, headers: resHeaders}) => {
return invokeHookSerial('truss/web-response', {
'@truss/http-get': async (action) => {
const {html, headers} = await responder.respond(action);
return {
status: 200,
headers: {...defaultResponseHeaders(), ...resHeaders},
html,
}, headers);
});
headers: {
...defaultResponseHeaders(),
...headers
},
response: html,
};
},
'truss/schema': () => {
return {executors: ['truss/http-get']};
},
'@truss/schema': () => ({
executors: ['@truss/http-get'],
}),
});
function defaultResponseHeaders() {

69
frontend/app/api.js Normal file
View File

@ -0,0 +1,69 @@
import {RSAA} from 'redux-api-middleware';
const PERSEA_FRONTHOST = '10.0.0.93';
export function createActionTypes(type, prefix) {
return ['REQUEST', 'SUCCESS', 'FAILURE'].reduce((actions, action) => {
return {
...actions,
[`${type}_${action}`]: `${prefix}/${type}_${action}`,
};
}, {});
}
export function createCall(uri, types, type) {
const prefix = `http://${PERSEA_FRONTHOST}/api`;
return {
[RSAA]: {
endpoint: `${prefix}${uri}`,
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
types: ['REQUEST', 'SUCCESS', 'FAILURE'].map((action) => {
return types[`${type}_${action}`]
}),
fetch: managedFetch,
},
};
}
export class ApiRequestHandler {
doesFetchEndpoint(input) {
return false;
}
async fetch(input, init) {
return new Response(null);
}
static normalizeRequestUrl(input) {
return ('string' === typeof input) ? input : input.url;
}
}
const handlers = [];
export function registerApiRequestHandler(handler) {
handlers.push(handler);
}
function handlerForInput(input) {
for (const handler of handlers) {
if (handler.doesFetchEndpoint(input)) {
return handler;
}
}
}
export function managedFetch(input, init) {
const handler = handlerForInput(input);
if (handler) {
return handler.fetch(input, init);
}
else {
return fetch(input, init);
}
}

45
frontend/app/component.js Normal file
View File

@ -0,0 +1,45 @@
import React from 'react';
import {hot} from 'react-hot-loader';
import {BrowserRouter} from 'react-router-dom';
import {compose} from 'redux';
import {connect} from 'react-redux';
import contempo from 'contempo';
import Editor from './editor/component';
import {ducks} from './index';
import {createMapDispatchToProps} from './ducks';
import DevTools from '../dev-tools';
const decorate = compose(
hot(module),
contempo(require('./component.scss')),
connect(
(state) => ({
currentWorkspace: state.workspace.current,
}),
createMapDispatchToProps(ducks)
),
)
function App({
currentWorkspace,
workspace,
}) {
return <React.Fragment>
<BrowserRouter>
<div className="app">
<Editor workspace={currentWorkspace} />
<button onClick={() => {
workspace.load('/workspace/test');
}}>Load</button>
</div>
</BrowserRouter>
<div className="dev-tools">
<DevTools />
</div>
</React.Fragment>;
};
export default decorate(App);

View File

@ -0,0 +1,58 @@
@import './forms.scss';
.app {
background-color: #222;
height: 100%;
width: 100%;
}
.aside {
font-size: 0.8em;
}
button {
&:hover {
background-color: #555;
}
}
button, input, select, option {
&[disabled] {
filter: grayscale(1);
opacity: 0.5;
&:hover {
background-color: transparent;
}
}
&:focus {
outline: none;
box-shadow: inset 0 0 1pt 0.5pt #CCC, 0 0 1pt 0.5pt #CCC
}
}
hr {
border: none;
border-bottom: 1px solid #333;
margin: 1em 0;
}
.loading {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
svg {
height: 25%;
width: 25%;
}
}
.editor.avocado-environment {
height: 100%;
width: 100%;
}

45
frontend/app/ducks.js Normal file
View File

@ -0,0 +1,45 @@
import {combineReducers} from 'redux';
import {all, call} from 'redux-saga/effects';
export function createMapDispatchToProps(ducks) {
return (dispatch) => {
const map = {};
for (const i in ducks) {
map[i] = {};
for (const j in ducks[i].actions.creators) {
map[i][j] = (...args) => {
return dispatch(ducks[i].actions.creators[j](...args));
}
}
}
return map;
};
}
export function createReducer(ducks) {
const map = {};
for (const i in ducks) {
map[i] = ducks[i].reducer;
}
return combineReducers(map);
}
export function createSaga(ducks) {
const sagas = [];
for (const i in ducks) {
if (!ducks[i].saga) {
continue;
}
sagas.push(call(ducks[i].saga));
}
return function* () {
yield all(sagas);
}
}
export function compose(ducks) {
return {
reducer: createReducer(ducks),
saga: createSaga(ducks),
}
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import {compose} from 'redux';
import {connect} from 'react-redux';
import Tabs from '../tabs/component';
import Tab from '../tabs/tab';
const decorate = compose(
connect(
(state) => ({
tabs: state.editor.tabs,
}),
)
);
function Editor({tabs}) {
return <div className="editor">
<Tabs>
<Tab icon="😂" basename="index.js" path="../editor" />
</Tabs>
</div>;
}
export default decorate(Editor);

View File

@ -0,0 +1,31 @@
import {combineReducers} from 'redux'
const types = {
EDITOR_CREATE_TAB: '@@persea/EDITOR_CREATE_TAB',
EDITOR_CLOSE_TAB: '@@persea/EDITOR_CLOSE_TAB',
};
const creators = {
createTab: (title, uri) => {
return {
type: types.EDITOR_CREATE_TAB,
payload: {
title,
uri,
},
};
},
};
export const actions = {types, creators};
export const reducer = combineReducers({
tabs: (state = [], action) => {
switch (action.type) {
case types.EDITOR_CREATE_TAB:
return [...state, action.payload];
default:
return state;
}
},
});

45
frontend/app/fixtures.js Normal file
View File

@ -0,0 +1,45 @@
import {ApiRequestHandler, registerApiRequestHandler} from './api';
export const data = {
'/api/config.json': {
workspace: '/workspace/test',
},
'/api/workspace/test': {
name: "Some workspace",
tree: [],
open: [
{
uri: ""
}
],
settings: {},
},
};
function lookupData(url) {
const {pathname} = new URL(url);
return data[pathname];
}
class FixtureApiRequestHandler extends ApiRequestHandler {
doesFetchEndpoint(input) {
const url = ApiRequestHandler.normalizeRequestUrl(input);
return !!lookupData(url);
}
fetch(input, init) {
const url = ApiRequestHandler.normalizeRequestUrl(input);
const data = lookupData(url);
return new Response(
JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
},
}
);
}
}
registerApiRequestHandler(new FixtureApiRequestHandler());

View File

@ -7,7 +7,7 @@ button, input, select {
button {
border: 1px solid #333;
padding: 0;
padding: 2em;
&:hover {
background-color: #555;

View File

@ -1,25 +1,54 @@
import React from 'react';
import {put, take} from 'redux-saga/effects';
import classnames from 'classnames';
import {Route, Switch} from 'react-router';
import {compose} from './ducks';
import * as editor from './editor';
import * as workspace from './workspace';
import Welcome from './welcome';
function App() {
import {createActionTypes, createCall} from './api';
return <div className={classnames({
app: true,
dev: 'production' !== process.env.NODE_ENV,
})}>
<Switch>
<Route path="/" component={Welcome} />
</Switch>
</div>;
const types = {
APPLICATION_START: '@@persea/APPLICATION_START',
...createActionTypes('APPLICATION_CONFIG_LOAD', '@@persea'),
};
const creators = {
loadConfig: () => {
return createCall('/config.json', types, 'APPLICATION_CONFIG_LOAD');
},
start: () => {
return {
type: types.APPLICATION_START,
payload: null,
};
},
};
export const actions = {types, creators};
export const ducks = {
editor,
workspace,
};
const {reducer, saga: ducksSaga} = compose(ducks);
export {reducer};
export function* saga() {
yield ducksSaga;
// Wait for application start.
yield take(types.APPLICATION_START);
// Load fixtures.
require('./fixtures');
// Read config.
yield put(creators.loadConfig());
const {payload: config} = yield take(types.APPLICATION_CONFIG_LOAD_SUCCESS);
// Workspace to load?
if (config.workspace) {
yield put(workspace.actions.creators.load(config.workspace));
}
// Put the dummy tab.
else {
yield put(editor.actions.creators.createTab('Welcome', '/foo/bar'));
}
}
import contempo from 'contempo';
App = contempo(App, require('./index.scss'));
import {hot} from 'react-hot-loader';
App = hot(module)(App);
export default App;

View File

@ -1,6 +1,58 @@
.app {
background-color: #444444;
color: #fff;
@import './forms.scss';
.app-container {
background-color: #222;
height: 100%;
width: 100%;
}
.aside {
font-size: 0.8em;
}
button {
&:hover {
background-color: #555;
}
}
button, input, select, option {
&[disabled] {
filter: grayscale(1);
opacity: 0.5;
&:hover {
background-color: transparent;
}
}
&:focus {
outline: none;
box-shadow: inset 0 0 1pt 0.5pt #CCC, 0 0 1pt 0.5pt #CCC
}
}
hr {
border: none;
border-bottom: 1px solid #333;
margin: 1em 0;
}
.loading {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
svg {
height: 25%;
width: 25%;
}
}
.editor.avocado-environment {
height: 100%;
width: 100%;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import {compose} from 'redux';
import contempo from 'contempo';
import Tab from './tab';
const decorate = compose(
contempo(require('./component.scss'))
);
function Tabs({
children,
}) {
return <div className="tabs">
{children}
</div>
}
export default decorate(Tabs);

View File

@ -0,0 +1,4 @@
.tabs {
width: 100%;
height: 3em;
}

View File

@ -0,0 +1,3 @@
// persea:///workspace
// http://api-server.com/workspace
// file:///persea-data-dir/workspace

23
frontend/app/tabs/tab.js Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import {compose} from 'redux';
import contempo from 'contempo';
const decorate = compose(
contempo(require('./tab.scss'))
);
function Tab({
basename,
icon,
path
}) {
return <div className="tab">
<span className="icon">{icon}</span>
<span className="basename">{basename}</span>
<span className="path">{path}</span>
<span className="close"></span>
</div>;
}
export default decorate(Tab);

View File

@ -0,0 +1,21 @@
.tab {
display: flex;
height: 3em;
float: left;
padding: 0.5em;
cursor: pointer;
align-items: center;
.close {
padding: 0 0.5em;
}
.icon {
padding-right: 0.5em;
}
.path {
padding-left: 0.5em;
color: #777777;
}
}

View File

@ -1,28 +0,0 @@
import React from 'react';
class Welcome extends React.Component {
render() {
return <div className="welcome">
<div className="content">
<h2>
<span>💕 </span>
Welcome back to <strong>Persea</strong>
<span> 💕</span>
</h2>
{this.thingsToDo()}
</div>
</div>;
}
thingsToDo() {
const things = <p>You have nothing to do!</p>;
return <div className="things-to-do">{things}</div>;
}
}
import contempo from 'contempo';
Welcome = contempo(Welcome, require('./welcome.scss'));
export default Welcome;

View File

@ -1,27 +0,0 @@
.welcome {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
h2 {
display: block;
font-size: 1.5em;
margin-bottom: 1.5em;
span {
color: red;
}
}
.content {
background-color: #333;
padding: 6em;
text-align: center;
}
.things-to-do div {
margin-bottom: 1em;
}

45
frontend/app/workspace.js Normal file
View File

@ -0,0 +1,45 @@
import {combineReducers} from 'redux'
import {createActionTypes, createCall} from './api';
const types = {
...createActionTypes('WORKSPACE_LOAD', '@@persea'),
};
const creators = {
load: (uri) => {
return createCall(uri, types, 'WORKSPACE_LOAD');
},
};
export const actions = {types, creators};
export const reducer = combineReducers({
current: (state = null, action) => {
switch (action.type) {
case types.WORKSPACE_LOAD_SUCCESS:
return action.payload;
default:
return state;
}
},
});
class Duck {
constructor(namespace) {
}
types() {
return {};
}
creators() {
return {};
}
*saga() {
}
}

View File

@ -7,18 +7,15 @@ import DockMonitor from 'redux-devtools-dock-monitor';
import FilterMonitor from 'redux-devtools-filter-actions';
const DevTools = createDevTools(
<DockMonitor
defaultIsVisible={false}
changePositionKey='ctrl-w'
toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
>
<FilterMonitor>
<LogMonitor
theme='tomorrow'
/>
</FilterMonitor>
</DockMonitor>
);

View File

@ -1,10 +1,19 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import Root from './root';
import createRootStore from './root/store';
import App from './app/component';
import {create as createStore} from './store';
const store = createStore();
// Application start.
import {actions} from './app';
store.dispatch(actions.creators.start());
ReactDOM.render(
<Root store={createRootStore()} />,
<Provider store={store}>
<App />
</Provider>,
document.querySelector('.root')
);

7
frontend/reducer.js Normal file
View File

@ -0,0 +1,7 @@
import {combineReducers} from 'redux'
import {reducer} from './app';
export default function createRootReducer() {
return reducer;
}

View File

@ -1,26 +0,0 @@
import React from 'react';
import {Provider} from 'react-redux';
import {BrowserRouter} from 'react-router-dom';
import DevTools from './dev-tools';
import App from '../app';
function Root ({store}) {
return <Provider store={store}>
<React.Fragment>
<BrowserRouter>
<App />
</BrowserRouter>
<div className="dev-tools">
<DevTools />
</div>
</React.Fragment>
</Provider>;
};
import contempo from 'contempo';
Root = contempo(Root, require('./index.scss'));
export default Root;

View File

@ -1,6 +0,0 @@
if ('production' === process.env.NODE_ENV) {
module.exports = require('./index.prod');
}
else {
module.exports = require('./index.dev');
}

View File

@ -1,19 +0,0 @@
import React from 'react';
import {Provider} from 'react-redux';
import {BrowserRouter} from 'react-router-dom';
import App from '../app';
function Root ({store}) {
return <Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>;
};
import contempo from 'contempo';
Root = contempo(Root, require('./index.scss'));
export default Root;

View File

@ -1,9 +0,0 @@
@media (max-width: 1023px) {
.testing {
margin: 0;
}
}
.dev-tools {
opacity: 0.9;
}

View File

@ -1,9 +0,0 @@
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form/immutable';
export default function createRootReducer() {
return combineReducers({
form: formReducer,
});
}

View File

@ -1,8 +0,0 @@
import {all, call} from 'redux-saga/effects'
export default function createRootSaga() {
return function*() {
return yield all([
].map(call));
};
}

View File

@ -1,28 +0,0 @@
import createSagaMiddleware from 'redux-saga';
import {compose, createStore, applyMiddleware} from 'redux';
import DevTools from './dev-tools';
import createRootReducer from './reducer';
import createRootSaga from './saga';
export default function createRootStore() {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
createRootReducer(),
compose(
applyMiddleware(sagaMiddleware),
DevTools.instrument()
)
);
module.hot.accept('./reducer', () => {
store.replaceReducer(require('./reducer').default());
});
sagaMiddleware.run(createRootSaga());
return store;
}

View File

@ -1,21 +0,0 @@
import createSagaMiddleware from 'redux-saga';
import {compose, createStore, applyMiddleware} from 'redux';
import createRootReducer from './reducer';
import createRootSaga from './saga';
export default function createRootStore() {
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
createRootReducer(),
compose(
applyMiddleware(sagaMiddleware)
)
);
sagaMiddleware.run(createRootSaga());
return store
}

7
frontend/saga.js Normal file
View File

@ -0,0 +1,7 @@
import {all} from 'redux-saga/effects';
import {saga} from './app';
export default function createRootSaga() {
return saga;
}

33
frontend/store.dev.js Normal file
View File

@ -0,0 +1,33 @@
import {compose, createStore, applyMiddleware} from 'redux';
import {apiMiddleware} from 'redux-api-middleware';
import createSagaMiddleware from 'redux-saga';
import DevTools from './dev-tools';
import {reducer, saga} from './app';
export function create() {
const sagaMiddleware = createSagaMiddleware({
onError: (error, options) => {
console.error(error);
if (options && options.sagaStack) {
console.error(`(above error originates from: ${options.sagaStack})`);
}
}
});
const middleware = applyMiddleware(apiMiddleware, sagaMiddleware);
const enhancer = compose(middleware, DevTools.instrument());
const store = createStore(reducer, enhancer);
let sagaTask = sagaMiddleware.run(saga);
if (module.hot) {
module.hot.accept('./app', () => {
const {reducer: newReducer, saga: newSaga} = require('./app');
store.replaceReducer(newReducer);
sagaTask.cancel();
sagaTask.done.then(() => {
sagaTask = sagaMiddleware.run(newSaga);
});
});
}
return store;
}

View File

@ -1 +1,22 @@
module.exports = () => ({});
module.exports = () => ({
ormCollections: () => {
return [
{
identity: 'project',
datastore: 'default',
primaryKey: 'id',
attributes: {
id: {
type: 'number',
autoMigrations: {
autoIncrement: true,
},
},
name: {
type: 'string',
},
}
},
];
},
});

View File

@ -1,4 +1,4 @@
import {createDispatcher} from '@truss/truss';
const {createDispatcher} = require('@truss/comm');
const dispatcher = createDispatcher();
dispatcher.lookupActions(require('./actions'));

View File

@ -4,14 +4,17 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "node -e '' -r '@truss/truss/task/build'",
"build": "node -e '' -r '@truss/webpack/task/build'",
"default": "yarn run dev",
"dev": "node -e '' -r '@truss/truss/task/scaffold'"
"dev": "node -e '' -r '@truss/webpack/task/scaffold'"
},
"author": "cha0s",
"license": "MIT",
"dependencies": {
"@truss/truss": "1.x",
"@persea/behavior": "1.x",
"@truss/comm": "1.x",
"@truss/webpack": "1.x",
"babel-plugin-redux-saga": "1.0.2",
"classnames": "^2.2.6",
"contempo": "1.x",
"debug": "3.1.0",
@ -23,7 +26,9 @@
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-split-pane": "^0.1.84",
"redux": "^4.0.0",
"redux-api-middleware": "3.0.1",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.3",
"redux-devtools-filter-actions": "^1.2.2",
@ -31,6 +36,7 @@
"redux-form": "^7.4.2",
"redux-saga": "^0.16.0",
"webpack-dev-middleware": "3.1.3",
"webpack-hot-middleware": "2.22.3"
"webpack-hot-middleware": "2.22.3",
"yallist": "^3.0.2"
}
}

View File

@ -71,11 +71,6 @@ exports.Responder = class Responder {
}
respond({payload: {headers, url}}) {
headers = {...headers};
delete headers.host;
delete headers.connection;
// forward to internal HTTP
return new Promise((resolve, reject) => {

View File

@ -1,55 +1,4 @@
html, body, .app, .root {
html, body, .root {
width: 100%;
height: 100%;
}
.aside {
font-size: 0.8em;
}
button {
&:hover {
background-color: #555;
}
}
button, input, select, option {
&[disabled] {
filter: grayscale(1);
opacity: 0.5;
&:hover {
background-color: transparent;
}
}
&:focus {
outline: none;
box-shadow: inset 0 0 1pt 0.5pt #CCC, 0 0 1pt 0.5pt #CCC
}
}
hr {
border: none;
border-bottom: 1px solid #333;
margin: 1em 0;
}
.loading {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
width: 100%;
svg {
height: 25%;
width: 25%;
}
}
.editor.avocado-environment {
height: 100%;
width: 100%;
}

View File

@ -37,16 +37,25 @@ module.exports.webpackConfig = function() {
style: [
path.join(cssPrefix, styleDirectory, 'reset.css'),
path.join(scssPrefix, styleDirectory, 'global.scss'),
path.join(scssPrefix, styleDirectory, 'forms.scss'),
],
},
optimization: {},
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(node_modules\/(?!contempo|@avocado|@truss))/,
name: 'vendor',
chunks: 'all',
}
}
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: [
/(node_modules\/(?!contempo))/,
/(node_modules\/(?!contempo|@avocado|@truss))/,
],
use: {
loader: 'babel-loader',
@ -55,7 +64,13 @@ module.exports.webpackConfig = function() {
OUTPUT_PATH,
],
plugins: [
'babel-plugin-redux-saga',
['@babel/plugin-proposal-decorators', {
legacy: true,
}],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'react-hot-loader/babel',
],
presets: [