@volter/world-console 3.0.11 → 3.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/console.css +3 -0
- package/client/console.tsx +62 -26
- package/dist/client/console.bundle.js +13 -13
- package/dist/client/console.css +3 -0
- package/dist/client/console.tsx +62 -26
- package/dist/client/main.bundle.js +66 -66
- package/package.json +3 -3
package/dist/client/console.css
CHANGED
|
@@ -645,6 +645,9 @@ main.app.home { max-width: none; }
|
|
|
645
645
|
.float.compare-panel { max-height: calc(100vh - 32px); overflow-y: auto; padding: 0; }
|
|
646
646
|
.compare { display: grid; gap: 12px; padding: 12px 14px; width: 900px; max-width: calc(100vw - 320px); }
|
|
647
647
|
.compare-head { display: flex; align-items: center; gap: 8px; }
|
|
648
|
+
.compare-base { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
649
|
+
.compare-base select { font: inherit; max-width: 22ch; }
|
|
650
|
+
.compare-acts-head { margin: 12px 0 0; font-size: 12px; }
|
|
648
651
|
.compare-head .mode { flex: 1; }
|
|
649
652
|
.compare-sides { display: flex; gap: 16px; flex-wrap: wrap; }
|
|
650
653
|
.compare-sides figure { margin: 0; display: grid; gap: 6px; }
|
package/dist/client/console.tsx
CHANGED
|
@@ -1707,7 +1707,8 @@ function ScreenFrame({ data }: NodeProps<Node<Screen>>): React.JSX.Element {
|
|
|
1707
1707
|
{/* its name always, as a canvas app labels its frames; what can be done to it once it is selected */}
|
|
1708
1708
|
<NodeToolbar isVisible position={Position.Top} align="start" className="frame-label">
|
|
1709
1709
|
<Mark vendor={frame.vendor} /><span>{frame.title}</span>
|
|
1710
|
-
{frame.changes ? <button type="button" className="frame-changed nodrag" onClick={() => compare(key)} title={`${counted(frame.changes, 'change')} here, not in a changeset: compare before and now`}><Icon name="square-dot" size={14} />{frame.changes}</button>
|
|
1710
|
+
{frame.changes ? <button type="button" className="frame-changed nodrag" onClick={() => compare(key)} title={`${counted(frame.changes, 'change')} here, not in a changeset: compare before and now`}><Icon name="square-dot" size={14} />{frame.changes}</button>
|
|
1711
|
+
: picked ? <button type="button" className="ghost nodrag" onClick={() => compare(key)} title="Compare with a branch of this World" aria-label={`Compare ${frame.title} with a branch`}><Icon name="file-diff" size={14} /></button> : null}
|
|
1711
1712
|
{picked && others.length ? <select className="nodrag frame-add" value="" aria-label={`Add ${frame.title} to a board`} onChange={(e) => { if (e.target.value) addTo(e.target.value, key); }}><option value="">Add to board…</option>{others.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}</select> : null}
|
|
1712
1713
|
{picked && remove ? <button type="button" className="ghost nodrag" onClick={() => remove.act(key)} title={remove.label} aria-label={remove.label}>✕</button> : null}
|
|
1713
1714
|
</NodeToolbar>
|
|
@@ -1752,55 +1753,89 @@ function Thumb({ url, title }: { url: string; title: string }): React.JSX.Elemen
|
|
|
1752
1753
|
* was the moment before the first of them beside how it is now. "Before" is a read-only copy of the World as of then (a
|
|
1753
1754
|
* branch the World makes for an hour, removed when this closes), its site read at the copy's own address. */
|
|
1754
1755
|
function Compare({ name, token, frame, onClose, reads }: { name: string; token: string; frame: BoardFrame; onClose: () => void; /** the board's reads so far: its changes are read again with each */ reads: number }): React.JSX.Element {
|
|
1755
|
-
|
|
1756
|
+
// what "before" is: the World as it was the moment before this frame's first change not yet cut (a copy made for the
|
|
1757
|
+
// comparison), or any branch of the World chosen here (a pull request's preview, a named branch), as a compare view
|
|
1758
|
+
// sets one branch beside another
|
|
1759
|
+
const [base, setBase] = useState<string>(frame.since ? 'before' : '');
|
|
1760
|
+
const [before, setBefore] = useState<{ state: 'opening' } | { state: 'ok'; url: string; at?: string } | { state: 'none'; why: string }>({ state: 'opening' });
|
|
1761
|
+
const [made, setMade] = useState<string | null>(null);
|
|
1762
|
+
// the copies this panel made, kept out of the choices after they are gone (the list is read again only every so often)
|
|
1763
|
+
const mine = useRef(new Set<string>());
|
|
1764
|
+
const branches = useDoor<{ branches: Array<{ name: string; origin?: string; sites?: string; at: { instant?: string; label?: string; live?: boolean } | null; expiresAt: string | null }> }>(token, `/-/${name}/branches`, reads);
|
|
1765
|
+
const choices = branches.state === 'ok' ? branches.data.branches.filter((b) => b.name !== made && !mine.current.has(b.name)) : [];
|
|
1756
1766
|
// its changes, read for this comparison alone
|
|
1757
1767
|
const listed = useDoor<{ total: number; acts: BoardAct[] }>(token, `/-/${name}/board/changes?frame=${encodeURIComponent(frameKey(frame))}`, reads);
|
|
1768
|
+
// a branch's page beside this World's: a site at the branch's own site address, a vendor's page on the branch's own
|
|
1769
|
+
// content plane through this World's branch door (a one-time read pass), never running on this World's origin
|
|
1770
|
+
const pageOf = async (branch: string, sites: string | null, isClosed: () => boolean): Promise<{ url: string } | { why: string }> => {
|
|
1771
|
+
const shown = frame.at!;
|
|
1772
|
+
if ('site' in shown) {
|
|
1773
|
+
// the branch's site at its address, where it nests under the branch on this machine; a hosted World's sites are
|
|
1774
|
+
// private to its people and open through the World's own address
|
|
1775
|
+
const u = new URL(frame.url);
|
|
1776
|
+
const nested = sites && /^https?:\/\/\{host\}\./.test(sites) ? sites.replace('{host}', u.hostname.toLowerCase()) : null;
|
|
1777
|
+
return nested ? { url: `${nested}${u.pathname}${u.search}` } : { why: "This World shows its branches' sites only on this machine." };
|
|
1778
|
+
}
|
|
1779
|
+
const beside = `${WORLDS}/-/${name}/branches/${branch}/pages?${new URLSearchParams({ land: `/${branch}/${frame.vendor}${shown.screen}` }).toString()}`;
|
|
1780
|
+
// the door answers a navigation with the branch's address; a host that shows no content plane says so instead
|
|
1781
|
+
const asked = await fetch(beside, { credentials: 'same-origin', redirect: 'manual' }).catch(() => null);
|
|
1782
|
+
if (isClosed()) return { why: '' };
|
|
1783
|
+
return asked?.type === 'opaqueredirect' ? { url: beside } : { why: `This World does not show its branches' pages${asked ? `: ${await failure(asked)}` : ''}.` };
|
|
1784
|
+
};
|
|
1758
1785
|
useEffect(() => {
|
|
1759
|
-
if (!frame.since) { setBefore({ state: 'none', why: 'Nothing here has changed since the World was last cut.' }); return; }
|
|
1760
1786
|
if (!frame.at) { setBefore({ state: 'none', why: `${vendorName(frame.vendor)}'s twin draws no page for this ${frame.kind} yet; its changes are listed below.` }); return; }
|
|
1761
|
-
|
|
1787
|
+
if (base === '') { setBefore({ state: 'none', why: 'Choose a branch of this World to set beside it.' }); return; }
|
|
1788
|
+
let closed = false;
|
|
1789
|
+
if (base !== 'before') {
|
|
1790
|
+
setBefore({ state: 'opening' });
|
|
1791
|
+
const row = branches.state === 'ok' ? branches.data.branches.find((b) => b.name === base) : undefined;
|
|
1792
|
+
// a moment's wait, so passing over a branch on the way to another opens nothing
|
|
1793
|
+
const wait = setTimeout(() => void pageOf(base, row?.sites ?? null, () => closed).then((r) => { if (!closed) setBefore('url' in r ? { state: 'ok', url: r.url } : { state: 'none', why: r.why }); }), 300);
|
|
1794
|
+
return () => { closed = true; clearTimeout(wait); };
|
|
1795
|
+
}
|
|
1796
|
+
if (!frame.since) { setBefore({ state: 'none', why: 'Nothing here has changed since the World was last cut.' }); return; }
|
|
1797
|
+
setBefore({ state: 'opening' });
|
|
1762
1798
|
const at = new Date(Date.parse(frame.since) - 1).toISOString();
|
|
1763
|
-
let
|
|
1799
|
+
let copy: string | null = null;
|
|
1764
1800
|
// a copy is made once the comparison has stayed open a moment: one passed through on the way to another makes none
|
|
1765
1801
|
const wait = setTimeout(() => void (async () => {
|
|
1766
1802
|
const r = await door(token, `/-/${name}/branches`, { method: 'POST', body: JSON.stringify({ at: { instant: at }, ttl: 3600 }), signal: AbortSignal.timeout(60000) }).catch(() => null);
|
|
1767
1803
|
if (!r?.ok) { setBefore({ state: 'none', why: `The World made no copy of itself as it was then${r ? `: ${await failure(r)}` : ''}.` }); return; }
|
|
1768
|
-
const b = (await r.json()) as { name: string; origin?: string;
|
|
1769
|
-
|
|
1804
|
+
const b = (await r.json()) as { name: string; origin?: string; sites?: string };
|
|
1805
|
+
copy = b.name; setMade(b.name); mine.current.add(b.name);
|
|
1770
1806
|
if (closed) { void door(token, `/-/${name}/branches/${b.name}`, { method: 'DELETE' }).catch(() => null); return; }
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
if ('site' in shown) {
|
|
1774
|
-
const o = httpOrigin(b.origin);
|
|
1775
|
-
if (!o) { setBefore({ state: 'none', why: 'This World shows sites only at its own local address.' }); return; }
|
|
1776
|
-
const u = new URL(frame.url); setBefore({ state: 'ok', url: `${o.protocol}//${u.hostname}.${o.host}${u.pathname}${u.search}`, at }); return;
|
|
1777
|
-
}
|
|
1778
|
-
const beside = `${WORLDS}/-/${name}/branches/${b.name}/pages?${new URLSearchParams({ land: `/${b.name}/${frame.vendor}${shown.screen}` }).toString()}`;
|
|
1779
|
-
// the door answers a navigation with the copy's address; a host that shows no content plane says so instead
|
|
1780
|
-
const asked = await fetch(beside, { credentials: 'same-origin', redirect: 'manual' }).catch(() => null);
|
|
1781
|
-
if (closed) return;
|
|
1782
|
-
if (asked?.type !== 'opaqueredirect') { setBefore({ state: 'none', why: `This World does not show its copy's pages${asked ? `: ${await failure(asked)}` : ''}.` }); return; }
|
|
1783
|
-
setBefore({ state: 'ok', url: beside, at });
|
|
1807
|
+
const page = await pageOf(b.name, b.sites ?? null, () => closed);
|
|
1808
|
+
if (!closed) setBefore('url' in page ? { state: 'ok', url: page.url, at } : { state: 'none', why: page.why });
|
|
1784
1809
|
})(), 500);
|
|
1785
1810
|
// the copy goes with the comparison, and with the page: a reload or a closed tab runs no cleanup of its own
|
|
1786
|
-
const drop = (): void => { if (
|
|
1811
|
+
const drop = (): void => { if (copy) void door(token, `/-/${name}/branches/${copy}`, { method: 'DELETE', keepalive: true }).catch(() => null); copy = null; };
|
|
1787
1812
|
// a page kept for back and forward keeps its copy: it may be shown again
|
|
1788
1813
|
const left = (e: PageTransitionEvent): void => { if (!e.persisted) drop(); };
|
|
1789
1814
|
window.addEventListener('pagehide', left);
|
|
1790
|
-
return () => { closed = true; clearTimeout(wait); window.removeEventListener('pagehide', left); drop(); };
|
|
1791
|
-
}, [name, token, frame.vendor, frame.id, frame.since]);
|
|
1815
|
+
return () => { closed = true; clearTimeout(wait); window.removeEventListener('pagehide', left); drop(); setMade(null); };
|
|
1816
|
+
}, [name, token, frame.vendor, frame.id, frame.since, base]);
|
|
1792
1817
|
const now = frameUrl(name, frame);
|
|
1818
|
+
// nothing to choose: said, rather than an empty list
|
|
1819
|
+
const noBranches = !frame.at || base !== '' ? null : branches.state === 'ok' && choices.length === 0 ? 'This World has no branches to set beside it: make one under Branches.' : branches.state === 'error' ? `The World's branches could not be read: ${branches.error}` : null;
|
|
1793
1820
|
return (
|
|
1794
1821
|
<div className="compare" role="dialog" aria-label={`Changes to ${frame.title}`}>
|
|
1795
1822
|
<div className="compare-head">
|
|
1796
1823
|
<Mark vendor={frame.vendor} /><strong>{frame.title}</strong>
|
|
1797
|
-
<span className="mode">{counted(frame.changes, 'change')} not yet cut{frame.since ? `, since ${when(frame.since)}` : ''}</span>
|
|
1824
|
+
<span className="mode">{frame.changes ? `${counted(frame.changes, 'change')} not yet cut${frame.since ? `, since ${when(frame.since)}` : ''}` : 'No changes here not yet cut'}</span>
|
|
1825
|
+
{frame.at ? <label className="compare-base">Compare with{' '}
|
|
1826
|
+
<select value={base} onChange={(e) => setBase(e.target.value)}>
|
|
1827
|
+
{base === '' ? <option value="">Choose a branch…</option> : null}
|
|
1828
|
+
{frame.since || base === 'before' ? <option value="before">Before these changes</option> : null}
|
|
1829
|
+
{base && base !== 'before' && !choices.some((b) => b.name === base) ? <option value={base}>{base.split('/')[1]}</option> : null}
|
|
1830
|
+
{choices.map((b) => <option key={b.name} value={b.name}>{b.name.split('/')[1]}{b.at?.label ? ` (${b.at.label})` : b.at?.instant ? ` (as of ${when(b.at.instant)})` : b.at?.live ? ' (live)' : ''}</option>)}
|
|
1831
|
+
</select>
|
|
1832
|
+
</label> : null}
|
|
1798
1833
|
<button type="button" className="ghost" onClick={onClose} aria-label="Close the comparison">✕</button>
|
|
1799
1834
|
</div>
|
|
1800
1835
|
<div className="compare-sides">
|
|
1801
1836
|
<figure>
|
|
1802
|
-
<figcaption>Before{before.state === 'ok' ? ` · as of ${when(before.at)}` : ''}</figcaption>
|
|
1803
|
-
{before.state === 'ok' ? <Thumb url={before.url} title={`${frame.title} before`} /> : before.state === 'opening' ? <p className="mode compare-note" role="status">Opening the World as it was…</p> : <p className="mode compare-note">{before.why}</p>}
|
|
1837
|
+
<figcaption>{base === 'before' ? `Before${before.state === 'ok' && before.at ? ` · as of ${when(before.at)}` : ''}` : base ? base.split('/')[1] : 'A branch'}</figcaption>
|
|
1838
|
+
{noBranches ? <p className="mode compare-note">{noBranches}</p> : before.state === 'ok' ? <Thumb url={before.url} title={`${frame.title} before`} /> : before.state === 'opening' ? <p className="mode compare-note" role="status">Opening the World as it was…</p> : <p className="mode compare-note">{before.why}</p>}
|
|
1804
1839
|
</figure>
|
|
1805
1840
|
<figure>
|
|
1806
1841
|
<figcaption>Now</figcaption>
|
|
@@ -1808,6 +1843,7 @@ function Compare({ name, token, frame, onClose, reads }: { name: string; token:
|
|
|
1808
1843
|
</figure>
|
|
1809
1844
|
</div>
|
|
1810
1845
|
{listed.state === 'error' ? <p role="alert" className="mode">Its changes could not be read: {listed.error}</p> : null}
|
|
1846
|
+
{base !== 'before' && base && listed.state === 'ok' && listed.data.acts.length ? <p className="mode compare-acts-head">This World's own changes here, not yet cut:</p> : null}
|
|
1811
1847
|
<ol className="compare-acts">
|
|
1812
1848
|
{(listed.state === 'ok' ? listed.data.acts : []).map((a) => {
|
|
1813
1849
|
const first = a.entries[0]!; const words = said({ ...first, service: frame.vendor, id: '', occurredAt: a.at } as unknown as Entry);
|