mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
30382dc366
- A book carries schemaRev. The reader that meets a newer one opens the book read-only with a reload banner and saves nothing, where it used to drop the elements it could not read on the next save. - The server refuses a save, a restore or a tool edit that would write an older revision over a newer book or one newer than itself (409 book_schema_newer), on REST and through every book tool alike. - normalizeBookDocument moves to book-normalize.ts. - Print waits for photos eagerly and up to 15 s for an export of more than 40 photos, and skips images without a source.
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { AlertTriangle, Check, CloudOff, Eye, RefreshCw } from 'lucide-react'
|
|
import type { BookRecord } from '@trek/shared'
|
|
import type { SaveState } from './useBookStore'
|
|
|
|
/**
|
|
* Whether the book is safe.
|
|
*
|
|
* Autosave without a sign of it is a promise you are asking someone to take on
|
|
* faith, and the moment they do not is the moment they close the tab to check.
|
|
* So: a tick when it is written, a spinner while it is being written, and — the
|
|
* one that matters — a real choice when someone else got there first.
|
|
*
|
|
* Quiet by design. This sits in the top bar of an editor, and a status that
|
|
* announces every successful save is a status people learn to stop reading,
|
|
* which is exactly the wrong habit for the one time it says something else.
|
|
*/
|
|
export function SaveIndicator({
|
|
state, t, onAcceptTheirs, onKeepMine, onRetry, onReload = reloadStudio,
|
|
}: {
|
|
state: SaveState
|
|
t: (k: string) => string
|
|
onAcceptTheirs: (current: BookRecord) => void
|
|
onKeepMine: (current: BookRecord) => void
|
|
onRetry: () => void
|
|
/** Load this Studio again, for a book a newer one saved. A full reload by default. */
|
|
onReload?: () => void
|
|
}) {
|
|
if (state.status === 'conflict') {
|
|
return (
|
|
<div className="st-save is-conflict" role="alert">
|
|
<AlertTriangle size={13} />
|
|
<span>{t('journey.studio.saveConflict')}</span>
|
|
{/*
|
|
Two ways out, both stated plainly. There is no merge here and
|
|
pretending otherwise would be worse than saying so: one of these
|
|
documents is going to be the book.
|
|
*/}
|
|
<button type="button" onClick={() => onAcceptTheirs(state.current)}>
|
|
{t('journey.studio.saveTakeTheirs')}
|
|
</button>
|
|
<button type="button" onClick={() => onKeepMine(state.current)}>
|
|
{t('journey.studio.saveKeepMine')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (state.status === 'newer') {
|
|
/*
|
|
* The one state with a way out the user has to take themselves: a newer
|
|
* Studio saved this book, this tab cannot write it without losing what
|
|
* that one added, and only a reload brings the newer Studio here. Loud
|
|
* like a conflict, because until then nothing done here is kept.
|
|
*/
|
|
return (
|
|
<div className="st-save is-conflict" role="alert">
|
|
<AlertTriangle size={13} />
|
|
<span>{t('journey.studio.saveNewer')}</span>
|
|
<button type="button" onClick={onReload}>
|
|
{t('journey.studio.saveReload')}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (state.status === 'readonly') {
|
|
/*
|
|
* Not an error and not retryable: this person was invited to read the
|
|
* journey, not to write it. Said once, plainly, and left standing — the
|
|
* alternative is an editor that looks like it is saving and is not.
|
|
*/
|
|
return (
|
|
<div className="st-save is-error" role="status">
|
|
<Eye size={13} />
|
|
<span>{t('journey.studio.saveReadOnly')}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (state.status === 'error') {
|
|
return (
|
|
<button type="button" className="st-save is-error" onClick={onRetry} title={t('journey.studio.saveRetry')}>
|
|
<CloudOff size={13} />
|
|
<span>{t('journey.studio.saveFailed')}</span>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
if (state.status === 'saving') {
|
|
return (
|
|
<div className="st-save">
|
|
<RefreshCw size={13} className="st-spin" />
|
|
<span>{t('journey.studio.saving')}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (state.status === 'saved') {
|
|
return (
|
|
<div className="st-save is-quiet">
|
|
<Check size={13} />
|
|
<span>{t('journey.studio.saved')}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/** The default way back to a current Studio: the page again, which loads the newest build. */
|
|
function reloadStudio() {
|
|
window.location.reload()
|
|
}
|