Skip to content

Commit

Permalink
feat: exploratory actions
Browse files Browse the repository at this point in the history
 - collect run button, result sections into one control div
 - show busy cursor while running
 - refactor state.action using get / set
  • Loading branch information
dckc committed Oct 24, 2020
1 parent ea9d981 commit 9c537aa
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 43 deletions.
28 changes: 15 additions & 13 deletions src/participate.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,23 @@ <h1>Liquid Democracy Participation Experiment</h1>
<!-- prettier-ignore -->
<textarea id='rholang-term' cols="80" rows="16"></textarea>
</div>
<button id="run">Run</button>
<div id="runControl">
<button id="run">Run</button>
<section id="resultSection" style="display: none">
<h2>Result</h2>
<pre id="result"></pre>
<h2>Block Info</h2>
<small>
<pre id="blockInfo"></pre>
</small>
</section>
<section id="problemSection" style="display: none">
<h3>Problem</h3>
<pre id="problem"></pre>
</section>
</div>
</fieldset>
</form>
<section id="resultSection" style="display: none">
<h2>Result</h2>
<pre id="result"></pre>
<h2>Block Info</h2>
<small>
<pre id="blockInfo"></pre>
</small>
</section>
<section id="problemSection" style="display: none">
<h3>Problem</h3>
<pre id="problem"></pre>
</section>
</div>
<hr />
<address>
Expand Down
188 changes: 158 additions & 30 deletions src/participate.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// @ts-check
import htm from 'htm';
import m from 'mithril';
import { getEthProvider, MetaMaskAccount, getAddrFromEth } from 'rchain-api';
import {
RNode,
RhoExpr,
getEthProvider,
MetaMaskAccount,
getAddrFromEth,
} from 'rchain-api';
import { actions } from './actions.js';
const { freeze, keys, values, entries } = Object;

Expand All @@ -26,15 +32,64 @@ document.addEventListener('DOMContentLoaded', () => {

buildUI({
html,
busy: makeBusy($),
formValue: (selector) => ckControl($(selector)).value,
fetch,
getEthProvider: () => getEthProvider({ window }),
mount: (selector, control) => m.mount($(selector), control),
redraw: () => m.redraw(),
});
});

/**
* @param { HTMLBuilder & EthSignAccess & MithrilMount } io
* @typedef {{
* formValue: (selector: string) => string,
* busy: (selector: string, p: Promise<T>) => Promise<T>,
* }} FormAccess
* @template T
*/

/**
* @param {(selector: string) => HTMLElement} $
*/
function makeBusy($) {
/**
* @param {string} selector
* @param {Promise<T>} p
* @return {Promise<T>}
*
* @template T
*/
async function busy(selector, p) {
$('form').style.cursor = 'wait';
const button = $(selector);
if (!(button instanceof HTMLButtonElement)) throw TypeError(String(button));
button.style.cursor = 'wait';
button.disabled = true;

try {
const result = await p;
m.redraw();
return result;
} finally {
button.disabled = false;
$('form').style.cursor = 'inherit';
button.style.cursor = 'inherit';
m.redraw();
}
}
return busy;
}

/**
* @param { HTMLBuilder & EthSignAccess & MithrilMount & WebAccess & FormAccess<any> } io
* @typedef {import('./actions').FieldSpec} FieldSpec
*
* @typedef {{
* fetch: typeof fetch
* }} WebAccess
*
*
* @typedef {{
* html: any, // TODO: htm(m) type
* }} HTMLBuilder
Expand All @@ -48,31 +103,60 @@ document.addEventListener('DOMContentLoaded', () => {
* }} EthSignAccess
* @typedef { import('rchain-api/src/ethProvider').MetaMaskProvider } MetaMaskProvider
*/
function buildUI({ html, getEthProvider, mount }) {
function buildUI({ html, formValue, busy, getEthProvider, mount, fetch }) {
let action = 'helloWorld';
const rnode = RNode(fetch);
let observer = rnode.observer(formValue('#nodeControl'));

const state = {
action: 'helloWorld',
get action() {
return action;
},
set action(value) {
if (typeof value !== 'string') return;
action = value;
state.fields = actions[state.action].fields || {};
const template = actions[state.action].template;
state.term = template;
if (keys(state.fields).length > 0) {
const exprs = values(state.fields).map(({ value, type }) =>
type === 'uri' ? `\`${value}\`` : JSON.stringify(value),
);
state.term = `match (${exprs.join(', ')}) {
(${keys(state.fields).join(', ')}) => {${
actions[state.action].template
}
}
}`;
}
},
/** @type {Record<string, FieldSpec>} */
fields: {},
term: '',
get observer() {
return observer;
},
result: undefined,
problem: undefined,
};
state.action = action; // compute initial term

mount('#actionControl', actionControl(state, { html, getEthProvider }));
mount('#runControl', runControl(state, { html, formValue, busy }));
}

/**
* @param { Event } event
* @returns { string }
* @param {unknown} ctrl
* @returns {HTMLInputElement | HTMLSelectElement}
*/
function eventValue(event) {
const ctrl = event.target;
function ckControl(ctrl) {
if (
!(ctrl instanceof HTMLInputElement) &&
!(ctrl instanceof HTMLSelectElement)
)
throw TypeError(String(ctrl));
return ctrl.value;
return ctrl;
}

/**
* @param {{
* action: string,
Expand All @@ -90,23 +174,6 @@ function actionControl(state, { html, getEthProvider }) {
</option>`,
);

function selectAction(/** @type { string } */ action) {
state.action = action;
state.fields = actions[state.action].fields || {};
if (keys(state.fields).length === 0) {
state.term = actions[state.action].template;
} else {
const exprs = values(state.fields).map(({ value, type }) =>
type === 'uri' ? `\`${value}\`` : JSON.stringify(value),
);
state.term = `match (${exprs.join(', ')}) {
(${keys(state.fields).join(', ')}) => ${
actions[state.action].template
}
}`;
}
}

const metaMaskP = getEthProvider().then((ethereum) =>
MetaMaskAccount(ethereum),
);
Expand All @@ -120,7 +187,7 @@ function actionControl(state, { html, getEthProvider }) {
name=${name}
value=${value}
onchange=${(/** @type {Event} */ event) => {
state.fields[name].value = eventValue(event);
state.fields[name].value = ckControl(event.target).value;
}}
/></label>
${type === 'walletRevAddr'
Expand All @@ -133,7 +200,7 @@ function actionControl(state, { html, getEthProvider }) {
const revAddr = getAddrFromEth(ethAddr);
if (!revAddr) throw new Error('bad ethAddr???');
state.fields[name].value = revAddr;
selectAction(state.action); // rebuild term. hm.
state.action = state.action; // rebuild term. hm.
m.redraw();
}),
);
Expand All @@ -152,7 +219,7 @@ function actionControl(state, { html, getEthProvider }) {
<select
name="action"
onchange=${(/** @type {Event} */ event) =>
selectAction(eventValue(event))}
(state.action = ckControl(event.target).value)}
>
${options(keys(actions))}
</select>
Expand All @@ -164,3 +231,64 @@ function actionControl(state, { html, getEthProvider }) {
},
});
}

/**
* @param {{
* observer: Observer,
* term: string,
* result?: RhoExpr[],
* problem?: string,
* }} state
* @param {HTMLBuilder & FormAccess<any>} io
*
* @typedef {import('rchain-api').RhoExpr} RhoExpr
* @typedef {import('rchain-api').Observer} Observer
*/
function runControl(state, { html, busy }) {
const hide = (/** @type { boolean } */ flag) =>
flag ? { style: 'display: none' } : {};

const pprint = (obj) => JSON.stringify(obj, null, 2);

async function run(/** @type {string} */ term) {
const obs = state.observer;
state.problem = undefined;
state.result = undefined;
try {
// TODO: busy()
const { expr, block } = await busy('#run', obs.exploratoryDeploy(term));
state.result = expr;
// TODO? $('#blockInfo').textContent = pprint(block);
} catch (err) {
state.problem = err.message;
}
}

return freeze({
view() {
return html`<button
id="run"
onclick=${async (/** @type {Event} */ event) => {
event.preventDefault();
run(state.term);
}}
>
Run
</button>
<section id="resultSection" ...${hide(!state.result)}>
<h2>Result</h2>
<pre id="result">
${state.result ? pprint(state.result.map(RhoExpr.parse)) : ''}</pre
>
<h2>Block Info</h2>
<small>
<pre id="blockInfo"></pre>
</small>
</section>
<section id="problemSection" ...${hide(!state.problem)}>
<h3>Problem</h3>
<pre id="problem">${state.problem ? state.problem : ''}</pre>
</section>`;
},
});
}

0 comments on commit 9c537aa

Please sign in to comment.