Skip to content

Commit 4bee60f

Browse files
zaerlclaude
andauthored
Draw the window's confirmations as the design system's notices, and say a failed setup there (#602)
## Why Part of the redesign in #542, item 8 of #557: the toasts. The window's confirmations were the older component library's snackbars, restyled by hand in the page's own stylesheet, and only one of their sentences could be translated. One thing they could not say at all: why a site failed to set up. That was shown only if the create-site dialog happened to be open when the setup failed, and lost otherwise (#600 left it for here). ## What changes **The confirmations are the design system's notices**, stacked in the window's bottom corner as the prototype has them: green for what worked, red for what did not, each with a **Dismiss** button. What they do is what the snackbars did: - One for something that worked is said politely and goes by itself after six seconds. - One for something that did not is said at once and does not time out. - A repeat of the newest is not shown or said twice, and three are kept at most. What is different: - **Every notice has a Dismiss button.** A snackbar was dismissed by pressing it anywhere. Dismissing one leaves the focus on the stack, as the snackbars' list had it. - **The stack is a region named "Notifications"**, which a screen reader can go to. - **A site that could not be set up is said there**, as an error. The create-site dialog's message is its own again: which answer is missing. Its two ways in no longer differ over a message left behind, since none is. - **The sentences can be translated.** The ones a value or a branch chooses are worded in `confirmations.cjs`, with unit tests: what the apply flow did, a saved patch, an opened pull request, a deletion or a setup that failed. - **Tooltips of the design system are drawn over everything.** They were under the stack, so the name of a notice's button was hidden by the notice above it. Not in this pull request, by decision: **no new confirmations.** The prototype raises a toast for many things the app does not confirm (a rename, a deletion, a server starting). The app says what it said before, plus the failed setup. ## How to test this Platforms: any. The change is in the renderer only. The journeys walk the stack on both platforms. From the repository's root: ```bash npm run build:once && npx playwright test --project=journeys toasts.spec create-site.spec i18n.spec ``` `toasts.spec.js` is new: a confirmation is shown and said once, goes at its time and not before, can be dismissed; an error is said at once, stays, and is joined by a later success without going with it; a notice's tooltip is over the notice above it. `create-site.spec.js` covers the failed setup, with a dialog open and with none. Each claim was checked by breaking it: 23 breaks, all red. By hand, on the current head, on either platform: **Starting state:** a site that is set up. 1. Open the site's menu (⋮, top right) and choose **Copy path**. - A green notice "Copied the path" appears in the bottom right corner, above the footer. It goes after about six seconds. 2. Choose **Copy path** again and click the notice's **✕**. - It goes at once. 3. Turn the network off, click **Create new site**, fill the dialog in and click **Create site**. - The dialog closes. When the clone fails, a red notice says "The site could not be created." and why. It is still there a minute later. 4. While it is showing, choose **Copy path** again and rest the pointer on the green notice's **✕**. - The green notice is under the red one. A "Dismiss" tooltip shows over the red notice, not behind it. 5. Click the red notice's **✕**. - It goes. **What must not have happened:** - Step 3 left nothing in the sites list for the site that failed, and no dialog open. - The red notice did not go by itself in step 4, when the green one did. - A notice never stopped a click on what is beside it: the stack takes a press only where a notice is. ## Risks and limitations - **Review:** two passes by a separate context. 3 to fix here on each, all fixed. The first pass caught a mistake of mine: I had set the lifetime to ten seconds, believing that was the snackbars'. It was six, and is six. Details below. - **Checked on macOS only**, at the default size and in a 1024×728 window. Windows is CI's. - **With the site's status read slowed by a second, two journeys fail that this change does not touch**: `site-header.spec.js` (a viewport check) and `trunk-update.spec.js` (an update asked for straight after a ticket is linked). Both fail the same way on `redesign/wpds` without this change; I checked. They pass at normal speed and have been passing on the Windows runner. The eight other files that read a toast pass slowed, 25 of 25. - **The manual steps above were not run by a person.** I looked at screenshots of one notice, of two, and of the tooltip. - **A failed setup's notice shows the error as the main process words it** ("Error invoking remote method 'wordpress:setup': Error: …"). The dialog showed the same text. It is now in a notice that stays in front of a newcomer. - **Three newer notices push out the oldest, an error included.** As before. A reducer that drops successes first would be a change of behaviour. - **A notice raised while a dialog is open cannot be reached by keyboard** until the dialog closes: the dialog keeps the focus. It is still said, and can be dismissed with the pointer, which leaves the dialog open; I ran that with the rename dialog. - **The "Notifications" region is always there**, empty when nothing is said. - **The five sentences worded where they are raised** ("Updated to the latest trunk", "This site is ready to work on", and three more) go through the translator, but no test would notice if one stopped: the unit tests reach the module's, and the pseudo-locale journey reads "Copied the path". - **The guide's other pages** still describe the redesign's earlier screens (#561). One sentence is corrected here because this change made it false. ## Related Part of #557. Part of #542. Follows #601. Settles what #600 left about a failed setup. --- <details> <summary>Design decisions and alternatives considered</summary> - **Six seconds, not the prototype's 3.2.** The prototype's is short for a sentence, and the app's has not changed. - **A Dismiss button on every notice.** The prototype's notices have none, and its errors time out like the rest. An error that stays needs a way to go, and a success could always be sent away early. - **The dialog no longer shows why a setup failed.** Two places saying it, one of them only sometimes, is what lost it. With the notice saying it always, the dialog's line is its own and needs no owner outside it. - **No shadow in the prototype; one here.** A notice over a page of cards reads as one more card without it. It is the design system's elevation token. - **The journeys find the stack by its label, not its role.** A confirmation can be raised while a dialog is open, for something done inside it, and an open dialog hides everything else from the roles. - **Every lookup of a toast's text goes through the stack.** A notice is said through a live region elsewhere in the document, which holds the same text; a search of the whole page finds both. </details> <details> <summary>Review outcome (required — see AGENTS.md)</summary> 3 [fix here] · 3 [follow-up] on the first pass, all 3 fixed. 3 [fix here] · 2 [follow-up] on the second, all 3 fixed. - **Review:** completed — a separate agent context, against `.github/instructions/code-review.instructions.md`; head `3297f74`, base `6c7e6792026c533f8e91de7105c61ef5505d6e2b`; it read the diff and the libraries' source and ran lint and the unit tests, not the journeys. Fixed: 1. The lifetime was ten seconds, said to be the snackbars'. Theirs was six (`NOTICE_TIMEOUT` in `@wordpress/components` 41). It is six. 2. Dismissing a notice left the focus at the top of the document. It is put on the stack. 3. The guide said a failed setup's reason goes to the log and not to a dialog. - Follow-ups taken: the design system's tooltips under the stack; two steps of the journey. Not reproduced when run: dismissing a notice closing an open dialog. - **Second pass:** completed — a separate agent context; head `953f4a32e08a9b3a274a7a9979a4382d7f87b755`, same base. No finding in the app's code. Fixed, all in `toasts.spec.js`: 1. The check that a confirmation is still there depended on how fast the machine was. The clock is stopped under it. 2. The check that a repeat is not shown twice could be read before a second one had appeared. It is told by what is left after one is dismissed, and by what was said by the end. 3. The tooltip's place could be read before the tooltip had been moved to its button. It is read once it reaches the notice above. - Also taken: the guide's sentence said the notice stays until dismissed, which three newer ones can undo. Left: the main process's wording in the notice; in Risks. - **Since review:** `953f4a3` → `ba41d4e633b8c7f8e001a5e3ef9930907302f67e`: the journey and one sentence of the guide. No source under `src/` changed. Not reviewed again; every suite was run on it, and eight breaks go red against the rewritten journey. Checks on `ba41d4e633b8c7f8e001a5e3ef9930907302f67e`, macOS: | Check | Result | |---|---| | `npx eslint . --max-warnings=0` | clean | | `npm test` | 1846 pass, 2 skipped | | `npm run test:electron` | 1847 pass, 1 skipped | | `npm run test:e2e` | 95 of 95 | | `npm run test:e2e:packaged` | 15 of 15, on an unsigned build | | Ten journey files in a 1024×728 window | 33 of 33 | | Eight journey files with the status read slowed | 25 of 25 | </details> <details> <summary>Implementation notes</summary> - `src/renderer/components/toast-stack.jsx` (new): `ToastStack`. - `src/renderer/confirmations.cjs`: the reducer is as it was. New: `toastView`, `TOAST_LIFETIME_MS`, `setupFailureMessage`, `applyDoneMessage`, `patchSavedMessage`, `savedAndResetMessage`. The module now needs `@wordpress/i18n`. - `src/renderer/index.jsx`: the stack in the same portal on `body`; the failed setup's notice; `createSiteError` gone. - `src/renderer/index.html`: the snackbars' rules are gone. `src/renderer/shell.css`: the stack, and `--wp-ui-tooltip-z-index` on `:root`, which is where the design system reads it from. - `tests/e2e/helpers/ui.cjs`: `toasts`, `toast`. Eight journey files used `getByTestId( 'snackbar' )` or a search of the whole page. - The toasts journey uses Playwright's clock. Stopped, it also stops the app's menus from being pressed, so it is stopped only between a notice appearing and its time being up. </details> <details> <summary>Screenshots or recording</summary> Not attached: I have no way to upload an image from here. What the screenshots I took show: - **Two notices** in the bottom right corner, above the footer, 360 pixels wide: a red one with a warning icon and four lines of bold text, and under it a green one with a tick, "Copied the path". Each has a ✕ at its right. - **The tooltip:** a small dark "Dismiss" label above the green notice's ✕, drawn over the lower edge of the red notice. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6c7e679 commit 4bee60f

23 files changed

Lines changed: 563 additions & 145 deletions

‎TESTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ Every matrix uses `fail-fast: false`, so a Windows failure never hides the macOS
221221
- **`npm test` is separated from the end-to-end suite by filename, not by directory.** `node --test` is run with no path at all, so it walks the whole repository and collects by name: `*.test.cjs` and four other shapes Node treats as test files — `*-test.cjs`, `*_test.cjs`, `test-*.cjs`, `test.cjs`. The journeys are `*.spec.js`, which matches none of them. Now that both suites live under `tests/`, that naming is the only thing keeping them apart, and a file under `tests/e2e/` named any of the five would silently join the fast suite and cost it an app launch. **End-to-end files are `.spec.js`.** The separation is worth defending: the fast suite has to stay something you run without thinking.
222222
- End-to-end selectors read the text and roles already on screen. No `data-testid` — the visible copy is the contract, and renaming a button is a change worth noticing.
223223
- Journeys run in the `en-US` locale, the source language, so no translation catalog loads and they read English text as written. The exception is `tests/e2e/journeys/i18n.spec.js`, which launches with `lang: 'en-XA'` (Chromium's pseudo-locale) and fails on any visible text that did not go through `__()`. See [Translatable strings](CONTRIBUTING.md#translatable-strings).
224-
- What is on screen is named once, in `tests/e2e/helpers/ui.cjs`, for the journeys, the other end-to-end specs and `scripts/screenshots/` alike, so renaming a button or rebuilding a card is one change. It holds every control that more than one of those files reaches, and the locators that find a part of the screen rather than something to press: the rendered app, an entry of the sites list, what a window with no site says, the open site's heading, a card, the work-item card with its heading and its ticket's number, and a ticket's row. A sentence one journey asserts, a button only one journey presses, and a selector only one file needs stay in that file: they are what it is about.
224+
- What is on screen is named once, in `tests/e2e/helpers/ui.cjs`, for the journeys, the other end-to-end specs and `scripts/screenshots/` alike, so renaming a button or rebuilding a card is one change. It holds every control that more than one of those files reaches, and the locators that find a part of the screen rather than something to press: the rendered app, an entry of the sites list, what a window with no site says, the open site's heading, a card, the work-item card with its heading and its ticket's number, a ticket's row, the question asked before a deletion, and the window's confirmations. A sentence one journey asserts, a button only one journey presses, and a selector only one file needs stay in that file: they are what it is about.
225225
- No test reads a component's source, the `.jsx` under `src/renderer/`, as text. A test that greps the component for a call, a literal or the order of two strings pins the file rather than the screen: it goes red when the code moves and stays green when the screen is wrong. What is on the page and where, a journey asserts, with order read as document order rather than from bounding boxes (the page scrolls as cards appear). How a component is allowed to be written, a lint rule asserts: `react-hooks/refs` is on for the renderer for that reason.
226226
- Journeys mark each assertion as an **invariant** (must hold under any model of how work is stored) or a **characterisation** (true because of how the app stores things today). A red invariant is a bug; a red characterisation is a prompt to read it and update it deliberately.
227227
- A journey that needs something only a running process would say, a mail caught or a server's address, says it itself: `webContents.send` from `app.evaluate`, on the channel and in the shape the real sender uses. Check the payload against both ends, the handler in `src/preload.js` and the place `src/main.js` sends it, and say in the test what the stand-in leaves out, such as the store write the real sender makes first. To assert that the page did *not* react, wait until it has heard: after the telling, ask the main process one more question from the page (`window.api.getSitesWithMeta()` is a plain read) and assert once that answer is back. An absence read before then passes whatever the app does.

‎docs/guide/creating-a-site.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ While the clone runs, the site view shows a **Setting up new site…** card with
2828

2929
The clone is the first step of the [initial setup checklist](./setup-wizard); the remaining steps stay locked until it finishes. Then the app carries on by itself — installing the dependencies and running the first build without waiting for you — so the only step left to click is starting the dev server.
3030

31-
If setup fails, the half-created site is removed from the list — the row simply disappears. The reason goes to the application log rather than to a dialog, so **Help → Open App Log** is where to look when a site never finishes.
31+
If setup fails, the half-created site is removed from the list — the row simply disappears. The reason is said in a red notice in the bottom corner of the window. Unlike a green one, it does not go away after a few seconds: it waits to be dismissed. The setup's own output is in the application log: **Help → Open App Log** is where to look for the detail.
3232

3333
## Where sites live on disk
3434

‎src/renderer/components/create-site-dialog.jsx‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ function FolderField({ value, disabled, onChoose, onFiles }) {
5151
// the dialog's popup, which is there while the dialog is open, and for the
5252
// moment it takes to fade once it is closed, and not otherwise. So the
5353
// answers are empty every time, on Core, however the last ones were left.
54-
function CreateSiteForm({ formId, submitting, error, onError, onCreate }) {
54+
function CreateSiteForm({ formId, submitting, onCreate }) {
55+
// Which answer is missing, said under the form.
56+
const [error, setError] = useState('');
5557
const [name, setName] = useState('');
5658
const [dir, setDir] = useState('');
5759
const [type, setType] = useState(DEFAULT_PROJECT_TYPE);
@@ -62,10 +64,10 @@ function CreateSiteForm({ formId, submitting, error, onError, onCreate }) {
6264
const chosen = await window.api.chooseDirectory();
6365
if (chosen) {
6466
setDir(chosen);
65-
onError('');
67+
setError('');
6668
}
6769
} catch {}
68-
}, [onError]);
70+
}, []);
6971

7072
// Not reached by the intended route, which is the system's dialog above:
7173
// a folder dropped on the input arrives here.
@@ -77,16 +79,16 @@ function CreateSiteForm({ formId, submitting, error, onError, onCreate }) {
7779
setDir(resolved);
7880
// Clearing the error only when there is a directory: a selection that
7981
// resolved to nothing has not fixed anything the message was about.
80-
if (resolved) onError('');
82+
if (resolved) setError('');
8183
}
8284
input.value = '';
83-
}, [onError]);
85+
}, []);
8486

8587
const submit = (event) => {
8688
event.preventDefault();
8789
const problem = createSiteProblem({ name, dir });
8890
if (problem) {
89-
onError(problem);
91+
setError(problem);
9092
return;
9193
}
9294
onCreate({ name: name.trim(), dir, projectType: type });
@@ -145,22 +147,18 @@ function CreateSiteForm({ formId, submitting, error, onError, onCreate }) {
145147
* one, from the notice on a site the old engine made, and is then inert and
146148
* stays until the setup has ended, as it did before it was redrawn.
147149
*
148-
* The one message under the form is the caller's, not the dialog's: `error`
149-
* is shown and `onError` sets it. The dialog's complaint about a missing
150-
* answer and the reason a setup failed are the same line, and the second can
151-
* arrive while the dialog is open, or after it has closed and before it
152-
* opens again. Two copies of it, one here and one there, lose one or the
153-
* other.
150+
* The message under the form is the dialog's own complaint about a missing
151+
* answer. Why a setup failed is not said here: the dialog that asked for it
152+
* closed as it began, and the window says it in its corner (#557), whether
153+
* or not a dialog happens to be open by then.
154154
*
155155
* @param {Object} props
156156
* @param {boolean} props.open Whether the dialog is open.
157157
* @param {boolean} props.submitting A setup is running.
158-
* @param {string} props.error The message under the form, or ''.
159-
* @param {Function} props.onError Sets that message.
160158
* @param {Function} props.onCreate Given `{ name, dir, projectType }` once every answer is there.
161159
* @param {Function} props.onClose Asked for by the close button, Escape, or a press outside.
162160
*/
163-
export function CreateSiteDialog({ open, submitting, error, onError, onCreate, onClose }) {
161+
export function CreateSiteDialog({ open, submitting, onCreate, onClose }) {
164162
const formId = useId();
165163
return (
166164
<Dialog.Root
@@ -181,7 +179,7 @@ export function CreateSiteDialog({ open, submitting, error, onError, onCreate, o
181179
<Dialog.Title>{__('Create site')}</Dialog.Title>
182180
{submitting ? null : <Dialog.CloseIcon />}
183181
</Dialog.Header>
184-
<CreateSiteForm formId={formId} submitting={submitting} error={error} onError={onError} onCreate={onCreate} />
182+
<CreateSiteForm formId={formId} submitting={submitting} onCreate={onCreate} />
185183
</Dialog.Popup>
186184
</Dialog.Root>
187185
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useCallback, useEffect, useRef } from 'react';
2+
import { __ } from '@wordpress/i18n';
3+
import { Notice } from '@wordpress/ui';
4+
import { toastView } from '../confirmations.cjs';
5+
6+
// One confirmation. It says itself as it appears, politely or at once as the
7+
// queue decided, and one that clears itself does so when its time is up. Any
8+
// of them can be dismissed before that.
9+
function Toast({ notice, onRemove, onDismiss }) {
10+
const { intent, lifetime } = toastView(notice);
11+
useEffect(() => {
12+
if (!lifetime) return undefined;
13+
const timer = setTimeout(() => onRemove(notice.id), lifetime);
14+
return () => clearTimeout(timer);
15+
}, [lifetime, notice.id, onRemove]);
16+
return (
17+
<Notice.Root intent={intent} politeness={notice.politeness} spokenMessage={notice.content}>
18+
<Notice.Title>{notice.content}</Notice.Title>
19+
<Notice.CloseIcon onClick={() => onDismiss(notice.id)} />
20+
</Notice.Root>
21+
);
22+
}
23+
24+
/**
25+
* The window's confirmations (#253, #557), stacked in its corner: that an
26+
* action worked, or that one the contributor is no longer looking at did
27+
* not. The newest is the nearest to the corner.
28+
*
29+
* It draws the queue it is given. Which messages are in it, how each speaks
30+
* and whether it stays are decided in confirmations.cjs.
31+
*
32+
* @param {Object} props
33+
* @param {Array} props.notices The queue's notices, oldest first.
34+
* @param {Function} props.onRemove Takes a notice out of the queue, by its id.
35+
*/
36+
export function ToastStack({ notices, onRemove }) {
37+
const stack = useRef(null);
38+
// The button that dismisses a notice goes with it, and the focus would go
39+
// to the top of the document. It is put on the stack, which is where the
40+
// next notice's button is. A notice that runs out of time takes no focus
41+
// from anywhere but itself, and moves none.
42+
const dismiss = useCallback((id) => {
43+
if (stack.current) stack.current.focus();
44+
onRemove(id);
45+
}, [onRemove]);
46+
return (
47+
<section ref={stack} tabIndex={-1} className="toast-stack" aria-label={__('Notifications')}>
48+
{notices.map((notice) => <Toast key={notice.id} notice={notice} onRemove={onRemove} onDismiss={dismiss} />)}
49+
</section>
50+
);
51+
}

‎src/renderer/confirmations.cjs‎

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
* Actions used to complete silently, or leave an inline sentence the
77
* contributor may not be looking at, and none of the success notices reached a
88
* screen reader. This is the one place a completed action is confirmed: a
9-
* transient message that `SnackbarList` renders and speaks. The reducer holds
10-
* the list; the renderer only dispatches and paints.
9+
* transient message that the toast stack renders and speaks (#557). The
10+
* reducer holds the list; the renderer only dispatches and paints. What the
11+
* messages say is here too, where a branch or a value chooses it.
1112
*
12-
* Kept as a pure, dependency-free module so it can be unit tested without a
13-
* DOM: the renderer bundle imports it, `node --test` requires it directly. The
14-
* `id` comes from a running counter rather than a timestamp or random value so
15-
* the reducer stays deterministic under test.
13+
* Kept as a pure module so it can be unit tested without a DOM: the renderer
14+
* bundle imports it, `node --test` requires it directly. The `id` comes from a
15+
* running counter rather than a timestamp or random value so the reducer
16+
* stays deterministic under test.
1617
*
1718
* Tone drives accessibility, not just colour:
1819
* - `success` speaks politely and clears itself on a timer — a confirmation
@@ -23,6 +24,8 @@
2324
* that half-happened (#381).
2425
*/
2526

27+
const { __, sprintf } = require('@wordpress/i18n');
28+
2629
// At most this many confirmations are kept on screen at once. A burst — a
2730
// double-click, a chain of steps finishing together — collapses to the most
2831
// recent few rather than stacking into a wall.
@@ -86,13 +89,16 @@ function confirmationReducer(state = initialConfirmations, action = {}) {
8689
* @param {{ ok?: boolean, dryRun?: boolean, number?: number, url?: string }} res The main process's result.
8790
*/
8891
function prConfirmationMessage(res = {}) {
89-
if (res.dryRun) return 'Dry run — branch created, no pull request opened';
92+
if (res.dryRun) return __('Dry run — branch created, no pull request opened');
9093
// Named because two repositories are possible now (#251): a number alone
9194
// does not say whether it landed on wordpress-develop or gutenberg. Read
9295
// from the pull request's own URL, not from the site's type: with the
9396
// sandbox override set the two differ, and the URL is where it went.
9497
const match = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(String(res.url || ''));
95-
return `Opened pull request #${res.number}${match ? ` on ${match[1]}` : ''}`;
98+
// translators: 1: the number of a pull request. 2: the repository it was opened on, such as WordPress/gutenberg.
99+
if (match) return sprintf(__('Opened pull request #%1$s on %2$s'), res.number, match[1]);
100+
// translators: %s: the number of a pull request.
101+
return sprintf(__('Opened pull request #%s'), res.number);
96102
}
97103

98104
/**
@@ -108,8 +114,88 @@ function prConfirmationMessage(res = {}) {
108114
*/
109115
function deleteFailureMessage(res = {}) {
110116
if (res.ok !== false || res.reason !== 'remove-failed') return null;
111-
const code = res.code ? ` (${res.code})` : '';
112-
return `The site is still listed because its folder could not be deleted${code}. Close anything using it, then try again. Folder: ${res.path}`;
117+
if (res.code) {
118+
// translators: 1: an error code of the system, such as EBUSY. 2: the path of a folder.
119+
return sprintf(__('The site is still listed because its folder could not be deleted (%1$s). Close anything using it, then try again. Folder: %2$s'), res.code, res.path);
120+
}
121+
// translators: %s: the path of a folder.
122+
return sprintf(__('The site is still listed because its folder could not be deleted. Close anything using it, then try again. Folder: %s'), res.path);
123+
}
124+
125+
/**
126+
* The notice for a site that could not be set up (#557): that it could not,
127+
* and what went wrong, as it was reported. The setup runs for minutes after
128+
* the dialog that asked for it has closed, so this is the one place its
129+
* failure is said.
130+
*
131+
* @param {*} error What the setup was rejected with.
132+
* @return {string} The sentence.
133+
*/
134+
function setupFailureMessage(error) {
135+
// translators: %s: what went wrong, as the system reported it.
136+
return sprintf(__('The site could not be created. %s'), String(error));
137+
}
138+
139+
/**
140+
* The confirmation for a patch or a pull request that is in the checkout and
141+
* built, or taken back out. The apply flow names what it did with a verb and
142+
* a noun, which it also prints in the terminal; here each pair it has is a
143+
* sentence of its own, so that it can be translated whole.
144+
*
145+
* @param {string} verb 'Applied', 'Reverted', 'Checked out' or 'Restored'.
146+
* @param {string} noun 'patch', 'pull request', 'previous branch' or 'saved work'.
147+
* @return {string} The sentence.
148+
*/
149+
function applyDoneMessage(verb, noun) {
150+
const sentences = {
151+
'Applied patch': __('Applied the patch'),
152+
'Reverted patch': __('Reverted the patch'),
153+
'Checked out pull request': __('Checked out the pull request'),
154+
'Restored previous branch': __('Restored the previous branch'),
155+
'Restored saved work': __('Restored the saved work')
156+
};
157+
return sentences[`${verb} ${noun}`] || `${verb} the ${noun}`;
158+
}
159+
160+
/**
161+
* The confirmation for a patch saved to a file.
162+
*
163+
* @param {string} fileName The file's name, without its folder.
164+
* @return {string} The sentence.
165+
*/
166+
function patchSavedMessage(fileName) {
167+
// translators: %s: the name of a file.
168+
return sprintf(__('Patch saved to %s'), fileName);
169+
}
170+
171+
/**
172+
* The confirmation for edits saved to a file before trunk was updated over
173+
* them.
174+
*
175+
* @param {string} fileName The file's name, without its folder.
176+
* @return {string} The sentence.
177+
*/
178+
function savedAndResetMessage(fileName) {
179+
// translators: %s: the name of a file.
180+
return sprintf(__('Saved your changes to %s and reset the working tree'), fileName);
181+
}
182+
183+
// How long a confirmation that clears itself is on screen: what the
184+
// snackbars before it had (#253).
185+
const TOAST_LIFETIME_MS = 6000;
186+
187+
/**
188+
* How a confirmation is drawn as a toast: the notice's colour, and how long
189+
* it stays. One that has to be dismissed stays until it is.
190+
*
191+
* @param {Object} notice A notice of the queue: `{ tone, explicitDismiss }`.
192+
* @return {{intent: string, lifetime: ?number}} The notice's intent, and its lifetime in milliseconds or null.
193+
*/
194+
function toastView(notice = {}) {
195+
return {
196+
intent: notice.tone === 'error' ? 'error' : 'success',
197+
lifetime: notice.explicitDismiss ? null : TOAST_LIFETIME_MS
198+
};
113199
}
114200

115-
module.exports = { initialConfirmations, confirmationReducer, prConfirmationMessage, deleteFailureMessage, MAX_NOTICES };
201+
module.exports = { initialConfirmations, confirmationReducer, prConfirmationMessage, deleteFailureMessage, setupFailureMessage, applyDoneMessage, patchSavedMessage, savedAndResetMessage, toastView, MAX_NOTICES, TOAST_LIFETIME_MS };

0 commit comments

Comments
 (0)