diff --git a/docs/fleet/README.md b/docs/fleet/README.md
index 8d48324..a93aa50 100644
--- a/docs/fleet/README.md
+++ b/docs/fleet/README.md
@@ -908,6 +908,14 @@ which appears whenever the floor on screen has something waiting. It shows:
Everything except that button and the zone editor below is a read. Nothing you
do here changes any floor until you promote.
+**Leaving it** is `back to map` in the pane head, or Escape. Review is a mode
+rather than a fourth column — it stands the operations panes down at every
+width — and the tab bar exists only below 760 px, so on a desk the pane's own
+head is the way out. Both are held off while a zone edit is up, as the revision
+list is: an edit has no autosave, so `cancel` (or `save as candidate`) ends it
+first. A promotion leaves for you, the decision it was opened for having been
+made.
+
### Naming the rooms on it, before you promote it
`pixi run segment-map` finds the rooms of a map but cannot know what they are
diff --git a/mote_fleet/server/ui/app.mjs b/mote_fleet/server/ui/app.mjs
index 4a5ef3e..9292df4 100644
--- a/mote_fleet/server/ui/app.mjs
+++ b/mote_fleet/server/ui/app.mjs
@@ -288,11 +288,27 @@ function onReviewJump() {
if (state.mapKey) review.open(state.mapKey);
}
+// Out of it again, by the pane's own control or by Escape: above 760 px the tab
+// bar is hidden, so these are the only exits.
+function onReviewBack() {
+ if (!review.leavable()) return;
+ panes.show('map');
+}
+
+function onKey(event) {
+ if (event.key !== 'Escape') return;
+ if (panes.current() !== 'review') return;
+ onReviewBack();
+}
+
// A promotion happened in the review pane: this pane's basemap is now a
-// different map, so re-resolve it rather than keep drawing the old one.
-function onPromoted() {
+// different map, so re-resolve it rather than keep drawing the old one. The
+// review is then over — except when the announcement failed, whose note is
+// readable only in the pane that wrote it.
+function onPromoted(site, floor, revision, announced) {
state.mapKey = null;
scheduleRender();
+ if (announced) panes.show('map');
}
// -- rendering -----------------------------------------------------------
@@ -576,6 +592,7 @@ function bind() {
reviewCanvas: 'review-canvas',
reviewMapLabel: 'review-map-label',
reviewPromote: 'review-promote',
+ reviewBack: 'review-back',
reviewFit: 'review-fit',
reviewNote: 'review-note',
zonesEdit: 'zones-edit',
@@ -620,6 +637,7 @@ export async function boot() {
zoneSource: dom.reviewZoneSource,
mapLabel: dom.reviewMapLabel,
promote: dom.reviewPromote,
+ back: dom.reviewBack,
fit: dom.reviewFit,
note: dom.reviewNote,
// The zone editor's own controls. It lives in this pane because it edits
@@ -645,6 +663,8 @@ export async function boot() {
});
dom.zone.addEventListener('change', onZone);
dom.reviewJump.addEventListener('click', onReviewJump);
+ dom.reviewBack.addEventListener('click', onReviewBack);
+ document.addEventListener('keydown', onKey);
dom.fit.addEventListener('click', () => {
mapView.follow(null);
dom.follow.checked = false;
diff --git a/mote_fleet/server/ui/index.html b/mote_fleet/server/ui/index.html
index 2749f03..2bf71e3 100644
--- a/mote_fleet/server/ui/index.html
+++ b/mote_fleet/server/ui/index.html
@@ -63,6 +63,9 @@
map
is the map about to be promoted and not the one already published. -->
+
+
review
—
diff --git a/mote_fleet/server/ui/review.mjs b/mote_fleet/server/ui/review.mjs
index 525068d..df09f01 100644
--- a/mote_fleet/server/ui/review.mjs
+++ b/mote_fleet/server/ui/review.mjs
@@ -372,6 +372,10 @@ export class ReviewView {
return Boolean(this.selected && this.map.map);
}
+ leavable() {
+ return !this.editing;
+ }
+
// Editing is a mode on the selected revision, so while it is on, the things
// that would swap that revision out from under it are disabled rather than
// racing it. There is no autosave: an unsaved edit is lost to `cancel`, and
@@ -384,6 +388,7 @@ export class ReviewView {
this.dom.zoneSave.hidden = !this.editing;
this.dom.zoneCancel.hidden = !this.editing;
this.dom.floor.disabled = this.editing;
+ this.dom.back.disabled = this.editing;
for (const row of this.dom.revisions.querySelectorAll('button')) {
row.disabled = this.editing;
}
@@ -577,7 +582,7 @@ export class ReviewView {
: `promoted, but not announced: ${body.detail}`,
!body.announced,
);
- this.onPromoted(site, floor, revision);
+ this.onPromoted(site, floor, revision, Boolean(body.announced));
} catch (error) {
this.note(error.message, true);
}
diff --git a/mote_fleet/server/ui/style.css b/mote_fleet/server/ui/style.css
index 9f39023..8ecd826 100644
--- a/mote_fleet/server/ui/style.css
+++ b/mote_fleet/server/ui/style.css
@@ -792,9 +792,10 @@ main {
}
/* Editing holds the selected revision still, so the controls that would swap it
- are disabled — and have to *look* disabled, or the pane reads as ignoring
- clicks. */
+ or leave it are disabled — and have to *look* disabled, or the pane reads as
+ ignoring clicks. */
.zones-head button:disabled,
+#review-back:disabled,
.revision-row:disabled {
opacity: 0.5;
cursor: not-allowed;
diff --git a/mote_fleet/test/browser_check.mjs b/mote_fleet/test/browser_check.mjs
index c408231..5be50b3 100644
--- a/mote_fleet/test/browser_check.mjs
+++ b/mote_fleet/test/browser_check.mjs
@@ -283,6 +283,40 @@ try {
`${candidateDrawn} painted pixels`,
);
+ // The pane shipped with no exit above 760 px, where the tab bar is hidden: an
+ // operator who opened it left by resizing the window or reloading the page.
+ const exits = await session.evaluate(`(() => {
+ const active = () => [...document.querySelectorAll('.pane')]
+ .filter(pane => pane.classList.contains('active'))
+ .map(pane => pane.dataset.pane).join(',');
+ const out = { width: innerWidth, tabs: getComputedStyle(document.querySelector('.panes')).display };
+ document.getElementById('review-back').click();
+ out.button = active();
+ out.shown = getComputedStyle(document.querySelector('.map-pane')).display;
+ document.getElementById('review-jump').click();
+ out.reopened = active();
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+ out.escape = active();
+ return out;
+ })()`);
+ check(
+ 'the review pane can be left at desk width, by its button and by Escape',
+ exits.tabs === 'none' &&
+ exits.button === 'map' &&
+ exits.shown !== 'none' &&
+ exits.reopened === 'review' &&
+ exits.escape === 'map',
+ JSON.stringify(exits),
+ );
+
+ // Back in, for the rest of the review checks.
+ await session.evaluate(`document.getElementById('review-jump').click()`);
+ await settle(
+ session,
+ `document.getElementById('review-map-label').textContent`,
+ (label) => /\d{8}T\d{6}/.test(label),
+ );
+
// The fixture's candidate is the published map mirrored, so a review pane
// that fetched the canonical image — the defect this replaces — would draw a
// perfectly convincing map. The URL is what separates the two.
@@ -374,6 +408,22 @@ try {
JSON.stringify({ ends: editing.ends, adds: editing.adds }),
);
+ const held = await session.evaluate(`(() => {
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+ document.getElementById('review-back').click();
+ return {
+ disabled: document.getElementById('review-back').disabled,
+ pane: [...document.querySelectorAll('.pane')]
+ .filter(pane => pane.classList.contains('active'))
+ .map(pane => pane.dataset.pane).join(','),
+ };
+ })()`);
+ check(
+ 'an edit in progress holds the exit, by button and by Escape',
+ held.disabled && held.pane === 'review',
+ JSON.stringify(held),
+ );
+
// One list, one shape: the rows are the same rows, in the same place, the
// same height, with the same cells — `edit zones` puts controls in them and
// changes nothing else. Every part of that has been wrong at least once: a
@@ -605,6 +655,11 @@ try {
(note) => /is on \d{8}T\d{6}/.test(note),
);
check('the first promotion on a floor goes through', /is on /.test(promoted), promoted);
+
+ const landed = await session.evaluate(`[...document.querySelectorAll('.pane')]
+ .filter(pane => pane.classList.contains('active'))
+ .map(pane => pane.dataset.pane).join(',')`);
+ check('a promotion hands the screen back to the operations map', landed === 'map', landed);
}
// -- the phone ----------------------------------------------------------
diff --git a/mote_fleet/test/ui_test.mjs b/mote_fleet/test/ui_test.mjs
index f4d00e7..3e97a66 100644
--- a/mote_fleet/test/ui_test.mjs
+++ b/mote_fleet/test/ui_test.mjs
@@ -937,6 +937,33 @@ test('review is a mode: opening it stands the operations panes down', () => {
assert.match(css, /\.review-pane\.active\s*\{\s*display:\s*flex/);
});
+test('the review pane has a way out, and it leads to the map', () => {
+ const html = read('index.html');
+ const review = html.slice(
+ html.indexOf('class="pane review-pane"'),
+ html.indexOf('class="pane detail-pane"'),
+ );
+ assert.ok(review.includes('id="review-back"'), 'the review pane has no exit control');
+
+ // An exit only if it names a pane that is *not* this one: `show('review')`
+ // on a button labelled `back` looks right in the markup and does nothing.
+ const app = read('app.mjs');
+ const leave = app.slice(app.indexOf('function onReviewBack('));
+ assert.match(leave.slice(0, leave.indexOf('\n}')), /panes\.show\('map'\)/);
+ assert.match(app, /dom\.reviewBack\.addEventListener\('click', onReviewBack\)/);
+ const key = app.slice(app.indexOf('function onKey('));
+ const body = key.slice(0, key.indexOf('\n}'));
+ assert.match(body, /event\.key !== 'Escape'/);
+ assert.match(body, /onReviewBack\(\)/);
+});
+
+test('an edit in progress holds the exit, as it holds the revision list', () => {
+ const source = read('review.mjs');
+ const controls = source.slice(source.indexOf('renderEditControls()'));
+ assert.match(controls.slice(0, controls.indexOf('\n }')), /this\.dom\.back\.disabled = this\.editing/);
+ assert.match(source, /leavable\(\) \{\s*return !this\.editing;/);
+});
+
test('the review pane has every element app.mjs binds to it', () => {
const html = read('index.html');
for (const id of [
@@ -953,6 +980,7 @@ test('the review pane has every element app.mjs binds to it', () => {
'review-canvas',
'review-map-label',
'review-promote',
+ 'review-back',
'review-fit',
'review-note',
]) {