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', ]) {