caracms 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +57 -2
- package/bin/caracms.mjs +174 -0
- package/bin/gate.mjs +275 -0
- package/bin/init.mjs +712 -0
- package/bin/media.mjs +662 -0
- package/bin/site-runner.mjs +61 -0
- package/bin/templates/Page.astro.txt +31 -0
- package/bin/templates/TextBlock.astro.txt +13 -0
- package/bin/templates/caracms.config.ts.txt +22 -0
- package/bin/templates/catch-all.astro.txt +25 -0
- package/bin/templates/health.ts.txt +6 -0
- package/bin/templates/railway.ts.txt +147 -0
- package/package.json +66 -4
- package/skills/caracms-adopt/SKILL.md +508 -0
- package/skills/caracms-adopt/look-same.mjs +957 -0
- package/skills/caracms-blocks/SKILL.md +376 -0
- package/skills/caracms-content/SKILL.md +201 -0
- package/skills/caracms-tina/SKILL.md +427 -0
- package/skills/caracms-tina/field-kinds.md +88 -0
- package/skills/caracms-tina/tina-to-caracms.mjs +1011 -0
- package/src/block-runs.ts +84 -0
- package/src/cards.ts +158 -0
- package/src/cli/schemas.ts +122 -0
- package/src/cli/site.ts +66 -0
- package/src/cli/validate.ts +471 -0
- package/src/code-routes-disk.ts +67 -0
- package/src/code-routes.ts +134 -0
- package/src/columns.ts +39 -0
- package/src/components/Block.astro +46 -0
- package/src/components/BlockRunTree.astro +45 -0
- package/src/components/Blocks.astro +139 -0
- package/src/components/Buttons.astro +116 -0
- package/src/components/Fonts.astro +14 -0
- package/src/components/Form.astro +221 -0
- package/src/components/Head.astro +62 -0
- package/src/components/Link.astro +68 -0
- package/src/components/Lockup.astro +116 -0
- package/src/components/Media.astro +124 -0
- package/src/components/MediaPlayer.astro +102 -0
- package/src/components/RenderDocument.astro +64 -0
- package/src/components/RichText.astro +59 -0
- package/src/components/Text.astro +81 -0
- package/src/components/attributes.ts +33 -0
- package/src/components/descriptor.ts +45 -0
- package/src/components/form-client.ts +182 -0
- package/src/components/media-player.ts +46 -0
- package/src/components/media-props.ts +35 -0
- package/src/components/sitemap.xml.ts +36 -0
- package/src/config.ts +825 -0
- package/src/content-schema.ts +166 -0
- package/src/content.ts +422 -0
- package/src/document.ts +579 -0
- package/src/editor/README.md +405 -0
- package/src/editor/addresses.ts +61 -0
- package/src/editor/brand.ts +118 -0
- package/src/editor/client/block-list/BlockList.tsx +342 -0
- package/src/editor/client/block-list/Picker.tsx +105 -0
- package/src/editor/client/block-list/UnknownBlock.tsx +119 -0
- package/src/editor/client/block-list/insert-points.ts +56 -0
- package/src/editor/client/block-list/link.ts +71 -0
- package/src/editor/client/block-list/reorder.ts +105 -0
- package/src/editor/client/block-list/summary.ts +118 -0
- package/src/editor/client/collections/CollectionGrid.tsx +143 -0
- package/src/editor/client/collections/CollectionTable.tsx +406 -0
- package/src/editor/client/collections/table.ts +270 -0
- package/src/editor/client/context.tsx +27 -0
- package/src/editor/client/documents/DocMenu.tsx +154 -0
- package/src/editor/client/documents/actions.ts +192 -0
- package/src/editor/client/documents/dialogs.tsx +687 -0
- package/src/editor/client/editor.ts +995 -0
- package/src/editor/client/fields/ButtonsField.tsx +87 -0
- package/src/editor/client/fields/DateField.tsx +200 -0
- package/src/editor/client/fields/FieldControl.tsx +61 -0
- package/src/editor/client/fields/FieldFrame.tsx +109 -0
- package/src/editor/client/fields/GroupField.tsx +113 -0
- package/src/editor/client/fields/HiddenToggle.tsx +51 -0
- package/src/editor/client/fields/LinkField.tsx +307 -0
- package/src/editor/client/fields/ListField.tsx +642 -0
- package/src/editor/client/fields/LockupField.tsx +365 -0
- package/src/editor/client/fields/MediaField.tsx +823 -0
- package/src/editor/client/fields/PageFields.tsx +82 -0
- package/src/editor/client/fields/ReferenceField.tsx +209 -0
- package/src/editor/client/fields/RichTextField.tsx +306 -0
- package/src/editor/client/fields/SelectField.tsx +63 -0
- package/src/editor/client/fields/SeoGroup.tsx +282 -0
- package/src/editor/client/fields/SimpleFields.tsx +89 -0
- package/src/editor/client/fields/TextField.tsx +51 -0
- package/src/editor/client/fields/date.ts +73 -0
- package/src/editor/client/fields/host.tsx +172 -0
- package/src/editor/client/fields/list-summary.ts +85 -0
- package/src/editor/client/fields/lockup-rules.ts +181 -0
- package/src/editor/client/fields/parts.tsx +458 -0
- package/src/editor/client/fields/rich-text-commands.ts +183 -0
- package/src/editor/client/fields/rich-text-doc.ts +156 -0
- package/src/editor/client/fields/rich-text-schema.ts +224 -0
- package/src/editor/client/fields/stale.ts +58 -0
- package/src/editor/client/inbox/InboxStage.tsx +272 -0
- package/src/editor/client/inbox/store.ts +311 -0
- package/src/editor/client/live/format.ts +77 -0
- package/src/editor/client/live/marks.tsx +90 -0
- package/src/editor/client/live/notices.tsx +213 -0
- package/src/editor/client/live/publish.tsx +617 -0
- package/src/editor/client/live/session.tsx +162 -0
- package/src/editor/client/main.tsx +12 -0
- package/src/editor/client/media/LibraryStage.tsx +75 -0
- package/src/editor/client/media/ReplaceDialog.tsx +355 -0
- package/src/editor/client/media/store.ts +618 -0
- package/src/editor/client/nav/DocRow.tsx +122 -0
- package/src/editor/client/nav/search.ts +71 -0
- package/src/editor/client/nav/tree.ts +93 -0
- package/src/editor/client/overlays/OverlayMount.tsx +26 -0
- package/src/editor/client/overlays/editor-marks.ts +159 -0
- package/src/editor/client/overlays/index.ts +50 -0
- package/src/editor/client/overlays/layer.ts +437 -0
- package/src/editor/client/overlays/overlay-css.ts +177 -0
- package/src/editor/client/overlays/rich-text-bar.ts +717 -0
- package/src/editor/client/pane/Pane.tsx +91 -0
- package/src/editor/client/pane/levels/BlockLevel.tsx +123 -0
- package/src/editor/client/pane/levels/CollectionLevel.tsx +115 -0
- package/src/editor/client/pane/levels/DocumentLevel.tsx +93 -0
- package/src/editor/client/pane/levels/DocumentsLevel.tsx +390 -0
- package/src/editor/client/pane/levels/FileLevel.tsx +132 -0
- package/src/editor/client/pane/levels/FormLevel.tsx +550 -0
- package/src/editor/client/pane/levels/MediaLevel.tsx +657 -0
- package/src/editor/client/pane/levels/SettingsM6.tsx +299 -0
- package/src/editor/client/pane/levels/SubmissionsLevel.tsx +185 -0
- package/src/editor/client/pane/parts.tsx +179 -0
- package/src/editor/client/pane/switcher.tsx +165 -0
- package/src/editor/client/preview/driver.ts +640 -0
- package/src/editor/client/preview/field-text.ts +71 -0
- package/src/editor/client/preview/follow-links.ts +234 -0
- package/src/editor/client/preview/layout.ts +37 -0
- package/src/editor/client/preview/link-target.ts +50 -0
- package/src/editor/client/preview/markers.ts +86 -0
- package/src/editor/client/preview/page-edit.ts +632 -0
- package/src/editor/client/preview/page-editor-registry.ts +63 -0
- package/src/editor/client/preview/page-editor.ts +235 -0
- package/src/editor/client/preview/render-sequencer.ts +113 -0
- package/src/editor/client/preview/selection.ts +116 -0
- package/src/editor/client/preview/warnings.ts +61 -0
- package/src/editor/client/shell/App.tsx +101 -0
- package/src/editor/client/shell/Bar.tsx +197 -0
- package/src/editor/client/shell/Brand.tsx +33 -0
- package/src/editor/client/shell/Icons.tsx +87 -0
- package/src/editor/client/shell/NarrowNote.tsx +47 -0
- package/src/editor/client/shell/Popovers.tsx +47 -0
- package/src/editor/client/shell/Stage.tsx +132 -0
- package/src/editor/client/shell/Toast.tsx +61 -0
- package/src/editor/client/state/document-store.ts +465 -0
- package/src/editor/client/state/keeper.ts +713 -0
- package/src/editor/client/state/live.ts +257 -0
- package/src/editor/client/state/schema.ts +42 -0
- package/src/editor/client/state/store.ts +39 -0
- package/src/editor/fonts/OFL-GeistMono.txt +93 -0
- package/src/editor/fonts/OFL-InstrumentSans.txt +93 -0
- package/src/editor/fonts/geist-mono-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/src/editor/fonts/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/src/editor/middleware.ts +62 -0
- package/src/editor/routes/_live.ts +26 -0
- package/src/editor/routes/admin.astro +80 -0
- package/src/editor/routes/create.ts +19 -0
- package/src/editor/routes/delete.ts +19 -0
- package/src/editor/routes/document.ts +39 -0
- package/src/editor/routes/documents.ts +30 -0
- package/src/editor/routes/github.ts +23 -0
- package/src/editor/routes/history.ts +9 -0
- package/src/editor/routes/lock.ts +9 -0
- package/src/editor/routes/login/assets.d.ts +10 -0
- package/src/editor/routes/login/login.ts +43 -0
- package/src/editor/routes/media.ts +16 -0
- package/src/editor/routes/move.ts +17 -0
- package/src/editor/routes/notice.ts +9 -0
- package/src/editor/routes/preview.astro +77 -0
- package/src/editor/routes/publish.ts +9 -0
- package/src/editor/routes/status.ts +9 -0
- package/src/editor/routes/submissions.ts +16 -0
- package/src/editor/routes/try-again.ts +9 -0
- package/src/editor/routes/write.ts +18 -0
- package/src/editor/server/admin-props.ts +151 -0
- package/src/editor/server/cards.ts +61 -0
- package/src/editor/server/create.ts +414 -0
- package/src/editor/server/drafts-store.ts +127 -0
- package/src/editor/server/media/alt.ts +107 -0
- package/src/editor/server/media/bucket.ts +295 -0
- package/src/editor/server/media/config.ts +78 -0
- package/src/editor/server/media/handler.ts +221 -0
- package/src/editor/server/media/library.ts +254 -0
- package/src/editor/server/media/memory.ts +184 -0
- package/src/editor/server/media/mux.ts +241 -0
- package/src/editor/server/media/sniff.ts +599 -0
- package/src/editor/server/media/upload.ts +175 -0
- package/src/editor/server/mode.ts +63 -0
- package/src/editor/server/move.ts +295 -0
- package/src/editor/server/render.tsx +9 -0
- package/src/editor/server/replace.ts +72 -0
- package/src/editor/server/settings.ts +35 -0
- package/src/editor/server/write.ts +366 -0
- package/src/editor/styles/editor.css +502 -0
- package/src/editor/styles/fonts.css +40 -0
- package/src/editor/styles/levels/README.md +4 -0
- package/src/editor/styles/levels/collections.css +99 -0
- package/src/editor/styles/levels/document-level.css +10 -0
- package/src/editor/styles/levels/documents.css +9 -0
- package/src/editor/styles/levels/fields.css +171 -0
- package/src/editor/styles/levels/files.css +95 -0
- package/src/editor/styles/levels/live.css +10 -0
- package/src/editor/styles/levels/m5a-documents.css +51 -0
- package/src/editor/styles/levels/m6.css +82 -0
- package/src/editor/styles/levels/media.css +175 -0
- package/src/editor/styles/levels/switcher.css +3 -0
- package/src/editor/styles/tokens.css +114 -0
- package/src/editor/tsconfig.json +16 -0
- package/src/editor/types.ts +881 -0
- package/src/editor/ui-state.ts +113 -0
- package/src/email.ts +67 -0
- package/src/fields.ts +1129 -0
- package/src/files-route.ts +201 -0
- package/src/files.ts +116 -0
- package/src/forms/email.ts +149 -0
- package/src/forms/endpoint.ts +347 -0
- package/src/forms/inbox.ts +273 -0
- package/src/forms/rate-limit.ts +130 -0
- package/src/forms/route.ts +24 -0
- package/src/forms/rules.ts +278 -0
- package/src/forms/store.ts +486 -0
- package/src/head.ts +125 -0
- package/src/index.ts +434 -0
- package/src/json-source.ts +116 -0
- package/src/links.ts +121 -0
- package/src/live/README.md +43 -0
- package/src/live/auth/allow-rules.ts +70 -0
- package/src/live/auth/allow.ts +128 -0
- package/src/live/auth/auth.ts +155 -0
- package/src/live/auth/email.ts +84 -0
- package/src/live/auth/guard.ts +111 -0
- package/src/live/auth/index.ts +68 -0
- package/src/live/auth/links.ts +101 -0
- package/src/live/auth/login.ts +245 -0
- package/src/live/auth/middleware.ts +53 -0
- package/src/live/auth/request.ts +56 -0
- package/src/live/auth/routes.ts +9 -0
- package/src/live/auth/session.ts +181 -0
- package/src/live/auth/views.ts +355 -0
- package/src/live/autosaves.ts +306 -0
- package/src/live/backend.ts +652 -0
- package/src/live/branches.ts +19 -0
- package/src/live/drafts-store.ts +591 -0
- package/src/live/editor.ts +465 -0
- package/src/live/git.ts +1021 -0
- package/src/live/github.ts +271 -0
- package/src/live/locals.ts +24 -0
- package/src/live/locks.ts +273 -0
- package/src/live/merge.ts +197 -0
- package/src/live/messages.ts +151 -0
- package/src/live/mode.ts +210 -0
- package/src/live/redis.ts +533 -0
- package/src/live/runtime.ts +164 -0
- package/src/live/status.ts +109 -0
- package/src/live/sweep.ts +228 -0
- package/src/live/timings.ts +47 -0
- package/src/live/webhook.ts +39 -0
- package/src/lockup-css.ts +58 -0
- package/src/measure.ts +145 -0
- package/src/media-value.ts +239 -0
- package/src/media.ts +255 -0
- package/src/read.ts +256 -0
- package/src/redirect-middleware.ts +49 -0
- package/src/redirects.ts +76 -0
- package/src/relink.ts +293 -0
- package/src/reserved.ts +24 -0
- package/src/rich-text.ts +690 -0
- package/src/schema-key.ts +63 -0
- package/src/seo.ts +124 -0
- package/src/serialize.ts +697 -0
- package/src/skipped.ts +35 -0
- package/src/slug.ts +121 -0
- package/src/text-styles.ts +62 -0
- package/src/virtual.d.ts +28 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bar's waiting count and Publish, and their popovers (M4 Decisions 10, 12 and
|
|
3
|
+
* 18; the mockup's pubHTML, waitingPop and historyPop: #waiting #nothing #publishing
|
|
4
|
+
* #live #failed #blocked #history). Live mode only.
|
|
5
|
+
*
|
|
6
|
+
* - **The bar's button**: "3 waiting" with who, and Publish; "Publishing…" with the
|
|
7
|
+
* running bar; "Live · 14:12"; "Publish failed" with Try again; "Nothing waiting".
|
|
8
|
+
* Off while the server can't be reached or you're signed out.
|
|
9
|
+
* - **The popover is the summary**: each waiting document, who changed it and when,
|
|
10
|
+
* "Editing now", uncommitted changes included (the status merges them), the merge
|
|
11
|
+
* notice where the draft was kept, then the final Publish button. Then publishing →
|
|
12
|
+
* live, or failed with Try again, or failed with the line that a developer can
|
|
13
|
+
* redeploy from Railway when there's no token (`hasToken: false`).
|
|
14
|
+
* - **History**: who published, when, which documents, and whether each went live,
|
|
15
|
+
* in the site's time zone, named once. Read when the waiting popover opens (on
|
|
16
|
+
* intent), so it's there when History is pressed, and the last read shows while
|
|
17
|
+
* it's read again.
|
|
18
|
+
* - **Images without alt text** (M5b Decision 9; #publish-alt, #fix-alt-list,
|
|
19
|
+
* #fix-alt-locked): each waiting document with an image missing alt text is marked,
|
|
20
|
+
* naming each slot by its label (`WaitingView.missingAlt`, worked out by the server
|
|
21
|
+
* from the document as it will publish), with **Add alt text**, which opens the
|
|
22
|
+
* document on that field (a list row expanded, the alt input focused; a document
|
|
23
|
+
* someone else holds opens read-only with the field shown). The bar's count gets a
|
|
24
|
+
* warning dot. **Publish stays on and one click**: nothing here holds it back.
|
|
25
|
+
*/
|
|
26
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
27
|
+
import { useEditor, useStore } from '../context.tsx';
|
|
28
|
+
import type { Editor } from '../editor.ts';
|
|
29
|
+
import { Icon } from '../shell/Icons.tsx';
|
|
30
|
+
import type { HistoryEntryView, MissingAltView, Person, StatusResponse, WaitingView } from '../../types.ts';
|
|
31
|
+
import { formKey, PAGE_SCOPE } from '../state/document-store.ts';
|
|
32
|
+
import { focusSidebarField } from '../preview/page-edit.ts';
|
|
33
|
+
import { clock, displayName, joinNames, samePerson, when, zoneLine } from './format.ts';
|
|
34
|
+
import { Avatar } from './marks.tsx';
|
|
35
|
+
import { docIcon } from '../nav/DocRow.tsx';
|
|
36
|
+
import { singular } from '../documents/actions.ts';
|
|
37
|
+
|
|
38
|
+
const histories = new WeakMap<Editor, { entries: HistoryEntryView[]; timeZone: string } | null>();
|
|
39
|
+
|
|
40
|
+
/** Read the history (at most once in flight), keeping the last read for the next open. */
|
|
41
|
+
function readHistory(editor: Editor, set?: (h: { entries: HistoryEntryView[]; timeZone: string }) => void) {
|
|
42
|
+
void editor.liveSession?.history().then((h) => {
|
|
43
|
+
if (!h) return;
|
|
44
|
+
histories.set(editor, h);
|
|
45
|
+
set?.(h);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The images without alt text across the waiting documents (a deleted one has none: the server leaves it out too). */
|
|
50
|
+
const altGaps = (waiting: readonly WaitingView[]): number => waiting.reduce((n, w) => n + (w.change === 'deleted' ? 0 : (w.missingAlt?.length ?? 0)), 0);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add alt text (Decision 9): open the document on the image's field. A list item opens its
|
|
54
|
+
* row, expanded; the alt input is focused. A document someone else holds opens read-only
|
|
55
|
+
* (M4's lock), its field shown but not focused.
|
|
56
|
+
*/
|
|
57
|
+
export async function openAtAlt(editor: Editor, item: Pick<WaitingView, 'collection' | 'path'>, gap: MissingAltView, options: { held?: boolean } = {}): Promise<void> {
|
|
58
|
+
if (item.collection === null || item.path === null) return;
|
|
59
|
+
editor.setPop(null);
|
|
60
|
+
await editor.openDocument({ collection: item.collection, path: item.path });
|
|
61
|
+
const session = editor.doc.get();
|
|
62
|
+
const slot = gap.path.replace(/\.alt$/, '');
|
|
63
|
+
if (gap.block === null && session.document.kind === 'sections') {
|
|
64
|
+
// M6c (G2): a page field's image, in the page's own group at its document level.
|
|
65
|
+
editor.view.update({ level: 'document', field: `${PAGE_SCOPE}.${slot}`, picked: false, pop: null });
|
|
66
|
+
} else {
|
|
67
|
+
const key = gap.block !== null && session.document.kind === 'sections' ? editor.doc.keyAt(gap.block) : formKey(session);
|
|
68
|
+
if (!key) return;
|
|
69
|
+
editor.openBlock(key, slot, { picked: false });
|
|
70
|
+
}
|
|
71
|
+
const held = () => !!options.held || editor.access.get().readOnly;
|
|
72
|
+
let tries = 30;
|
|
73
|
+
const attempt = () => {
|
|
74
|
+
const shown = '#side .side-level:not([hidden])';
|
|
75
|
+
const input = document.querySelector<HTMLInputElement>(`${shown} input[data-alt="${CSS.escape(slot)}"]`);
|
|
76
|
+
const wrap = document.querySelector<HTMLElement>(`${shown} [data-path="${CSS.escape(slot)}"]`);
|
|
77
|
+
if (wrap && held()) {
|
|
78
|
+
// Someone else holds it: read-only, the field shown but not focused.
|
|
79
|
+
wrap.scrollIntoView({ block: 'center' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (input && !input.disabled) {
|
|
83
|
+
input.scrollIntoView({ block: 'center' });
|
|
84
|
+
input.focus({ preventScroll: true });
|
|
85
|
+
if (document.activeElement === input) return;
|
|
86
|
+
}
|
|
87
|
+
if (tries-- > 0) requestAnimationFrame(attempt);
|
|
88
|
+
else if (!held()) focusSidebarField(slot);
|
|
89
|
+
};
|
|
90
|
+
requestAnimationFrame(attempt);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const people = (waiting: readonly WaitingView[]): Person[] => {
|
|
94
|
+
const out: Person[] = [];
|
|
95
|
+
for (const w of waiting) for (const e of w.editors) if (!out.some((p) => samePerson(p, e.person))) out.push(e.person);
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function PublishButton() {
|
|
100
|
+
const editor = useEditor();
|
|
101
|
+
const status = useStore(editor.liveState, (s) => s.status);
|
|
102
|
+
const reachable = useStore(editor.liveState, (s) => s.reachable);
|
|
103
|
+
const signedOut = useStore(editor.liveState, (s) => s.signedOut);
|
|
104
|
+
const keep = useStore(editor.keep, (k) => k.state);
|
|
105
|
+
const pop = useStore(editor.view, (v) => v.pop);
|
|
106
|
+
if (!status) return <span data-slot="publish" hidden />;
|
|
107
|
+
const expanded = pop === 'waiting' || pop === 'history';
|
|
108
|
+
const toggle = () => editor.setPop(expanded ? null : 'waiting');
|
|
109
|
+
const n = status.waiting.length;
|
|
110
|
+
const pub = status.publish;
|
|
111
|
+
const common = { type: 'button' as const, 'data-slot': 'publish', 'aria-haspopup': 'dialog' as const, 'aria-expanded': expanded, onClick: toggle };
|
|
112
|
+
if (pub.running || pub.state === 'publishing') {
|
|
113
|
+
return (
|
|
114
|
+
<button {...common} className="pub busy" data-pub="publishing">
|
|
115
|
+
<span className="wt">Publishing…</span>
|
|
116
|
+
<span className="pb off">Publish</span>
|
|
117
|
+
<span className="prog" aria-hidden="true" />
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (pub.state === 'live' && !n && pub.record) {
|
|
122
|
+
return (
|
|
123
|
+
<button {...common} className="pub live" data-pub="live">
|
|
124
|
+
<span className="wt">
|
|
125
|
+
<Icon name="check" size="sm" />
|
|
126
|
+
Live · {clock(pub.record.at, status.timeZone)}
|
|
127
|
+
</span>
|
|
128
|
+
<span className="pb off">Publish</span>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (pub.state === 'failed' && !n) {
|
|
133
|
+
return (
|
|
134
|
+
<button {...common} className="pub fail" data-pub="failed">
|
|
135
|
+
<span className="wt">
|
|
136
|
+
<Icon name="warn" size="sm" />
|
|
137
|
+
Publish failed
|
|
138
|
+
</span>
|
|
139
|
+
<span className={`pb${pub.canTryAgain ? '' : ' off'}`}>{pub.canTryAgain ? 'Try again' : 'Publish'}</span>
|
|
140
|
+
</button>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!n) {
|
|
144
|
+
return (
|
|
145
|
+
<button {...common} className="pub off" data-pub="nothing" aria-label="Nothing waiting. Nothing to publish">
|
|
146
|
+
<span className="wt">Nothing waiting</span>
|
|
147
|
+
<span className="pb">Publish</span>
|
|
148
|
+
</button>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const off = keep === 'offline' || !reachable || !!signedOut;
|
|
152
|
+
// M5b Decision 9: images without alt text put a warning dot on the count; Publish stays on.
|
|
153
|
+
const gaps = off ? 0 : altGaps(status.waiting);
|
|
154
|
+
const gw = `${gaps} image${gaps === 1 ? '' : 's'} without alt text`;
|
|
155
|
+
return (
|
|
156
|
+
<button {...common} className={`pub${off ? ' off' : ''}`} data-pub="waiting" data-alt-gaps={gaps || undefined} aria-label={`${n} change${n === 1 ? '' : 's'} waiting${gaps ? `, ${gw}` : ''}. Publish`}>
|
|
157
|
+
<span className="wt">
|
|
158
|
+
<span className="avs">
|
|
159
|
+
{people(status.waiting).map((p) => (
|
|
160
|
+
<Avatar key={p.email} person={p} size="sm" />
|
|
161
|
+
))}
|
|
162
|
+
</span>
|
|
163
|
+
<b>{n} waiting</b>
|
|
164
|
+
</span>
|
|
165
|
+
<span className="pb">
|
|
166
|
+
Publish{' '}
|
|
167
|
+
<span className="count" title={gaps ? gw : undefined}>
|
|
168
|
+
{n}
|
|
169
|
+
{gaps ? <span className="wdot" aria-hidden="true" data-dot="alt" /> : null}
|
|
170
|
+
</span>
|
|
171
|
+
</span>
|
|
172
|
+
</button>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A click outside a popover closes it (its trigger toggles it itself). */
|
|
177
|
+
function useOutside(close: () => void, trigger: string) {
|
|
178
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
const onDown = (event: MouseEvent) => {
|
|
181
|
+
const target = event.target as Element | null;
|
|
182
|
+
if (!target || ref.current?.contains(target) || target.closest?.(trigger)) return;
|
|
183
|
+
close();
|
|
184
|
+
};
|
|
185
|
+
document.addEventListener('mousedown', onDown);
|
|
186
|
+
return () => document.removeEventListener('mousedown', onDown);
|
|
187
|
+
}, [close, trigger]);
|
|
188
|
+
return ref;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function HistoryLink() {
|
|
192
|
+
const editor = useEditor();
|
|
193
|
+
return (
|
|
194
|
+
<button type="button" className="linkbtn subtle" data-act="history" onClick={() => editor.setPop('history')}>
|
|
195
|
+
<Icon name="history" size="sm" />
|
|
196
|
+
History
|
|
197
|
+
</button>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function WaitingPop() {
|
|
202
|
+
const editor = useEditor();
|
|
203
|
+
const status = useStore(editor.liveState, (s) => s.status);
|
|
204
|
+
const reachable = useStore(editor.liveState, (s) => s.reachable);
|
|
205
|
+
const signedOut = useStore(editor.liveState, (s) => s.signedOut);
|
|
206
|
+
const keep = useStore(editor.keep, (k) => k.state);
|
|
207
|
+
const [busy, setBusy] = useState(false);
|
|
208
|
+
const [tryError, setTryError] = useState<string | null>(null);
|
|
209
|
+
/** The last Publish press failed (a 502, the server away): said here, and Publish can be pressed again. */
|
|
210
|
+
const [publishError, setPublishError] = useState<string | null>(null);
|
|
211
|
+
const ref = useOutside(() => editor.setPop(null), '.pub');
|
|
212
|
+
// On intent: the history, so it's there when History is pressed.
|
|
213
|
+
useEffect(() => readHistory(editor), [editor]);
|
|
214
|
+
if (!status) return null;
|
|
215
|
+
const wrap = (body: ReactNode, width = 440) => (
|
|
216
|
+
<div ref={ref} className="pop" role="dialog" aria-label="Waiting changes and Publish" data-pop="waiting" style={{ right: 8, width }}>
|
|
217
|
+
{body}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
const n = status.waiting.length;
|
|
221
|
+
const pub = status.publish;
|
|
222
|
+
const record = pub.record;
|
|
223
|
+
const names = joinNames(record?.titles ?? []);
|
|
224
|
+
const by = record ? displayName(record.by) : '';
|
|
225
|
+
|
|
226
|
+
if ((pub.running || pub.state === 'publishing') && !n) {
|
|
227
|
+
return wrap(
|
|
228
|
+
<>
|
|
229
|
+
<div className="pop-h">
|
|
230
|
+
<div>
|
|
231
|
+
<h2>Publishing</h2>
|
|
232
|
+
<p className="sub">{record ? `${names} going live.` : 'Going live.'}</p>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="pop-b">
|
|
236
|
+
<div className="progress" role="progressbar" aria-label="Publishing" aria-valuetext="Publishing" />
|
|
237
|
+
{record ? (
|
|
238
|
+
<p className="field-help" style={{ marginBottom: 'var(--space-4)' }}>
|
|
239
|
+
Started at {clock(record.at, status.timeZone)} by {by}.
|
|
240
|
+
</p>
|
|
241
|
+
) : null}
|
|
242
|
+
</div>
|
|
243
|
+
<div className="pop-f">
|
|
244
|
+
<HistoryLink />
|
|
245
|
+
<span className="sp" />
|
|
246
|
+
<button type="button" className="btn lg" disabled>
|
|
247
|
+
Publish
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</>,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (pub.state === 'live' && !n && record) {
|
|
254
|
+
return wrap(
|
|
255
|
+
<>
|
|
256
|
+
<div className="pop-h">
|
|
257
|
+
<div>
|
|
258
|
+
<h2 style={{ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--status-live)' }}>
|
|
259
|
+
<Icon name="check" />
|
|
260
|
+
Live
|
|
261
|
+
</h2>
|
|
262
|
+
<p className="sub">
|
|
263
|
+
Published at {clock(record.at, status.timeZone)} by {by}: {names}.
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="pop-b">
|
|
268
|
+
<div className="empty" style={{ marginBottom: 'var(--space-4)' }}>
|
|
269
|
+
<b>Nothing waiting</b>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="pop-f">
|
|
273
|
+
<HistoryLink />
|
|
274
|
+
<span className="sp" />
|
|
275
|
+
<a className="btn" href="/" target="_blank" rel="noopener">
|
|
276
|
+
<Icon name="ext" size="sm" />
|
|
277
|
+
View the live site
|
|
278
|
+
</a>
|
|
279
|
+
</div>
|
|
280
|
+
</>,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (pub.state === 'failed' && !n) {
|
|
284
|
+
return wrap(
|
|
285
|
+
<>
|
|
286
|
+
<div className="pop-h">
|
|
287
|
+
<div>
|
|
288
|
+
<h2 style={{ color: 'var(--status-danger)' }}>The publish didn't go live</h2>
|
|
289
|
+
<p className="sub">
|
|
290
|
+
{record ? `${names} ${record.titles.length === 1 ? 'was' : 'were'} published at ${clock(record.at, status.timeZone)}, but` : 'The'} the live site couldn't be updated.
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="pop-b">
|
|
295
|
+
{pub.canTryAgain ? (
|
|
296
|
+
<div className="note danger" data-note="try-again">
|
|
297
|
+
<Icon name="warn" />
|
|
298
|
+
<div>
|
|
299
|
+
<b>Try again updates the live site</b>
|
|
300
|
+
<p>If it fails twice, tell your developer.</p>
|
|
301
|
+
{tryError ? <p>It didn't work: {tryError}</p> : null}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
) : (
|
|
305
|
+
<div className="note danger" data-note="no-token">
|
|
306
|
+
<Icon name="warn" />
|
|
307
|
+
<div>
|
|
308
|
+
<b>A developer can redeploy it from Railway</b>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
<div className="pop-f">
|
|
314
|
+
<HistoryLink />
|
|
315
|
+
<span className="sp" />
|
|
316
|
+
{pub.canTryAgain ? (
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
className="btn lg ink"
|
|
320
|
+
data-act="try-again"
|
|
321
|
+
disabled={busy}
|
|
322
|
+
onClick={async () => {
|
|
323
|
+
setBusy(true);
|
|
324
|
+
setTryError(null);
|
|
325
|
+
const r = await editor.tryAgain();
|
|
326
|
+
setBusy(false);
|
|
327
|
+
if (!r.ok) setTryError(r.error ?? 'the server said no.');
|
|
328
|
+
}}
|
|
329
|
+
>
|
|
330
|
+
Try again
|
|
331
|
+
</button>
|
|
332
|
+
) : null}
|
|
333
|
+
</div>
|
|
334
|
+
</>,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (!n) {
|
|
338
|
+
return wrap(
|
|
339
|
+
<>
|
|
340
|
+
<div className="pop-h">
|
|
341
|
+
<div>
|
|
342
|
+
<h2>Nothing to publish</h2>
|
|
343
|
+
<p className="sub">Everything is live.</p>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="pop-f">
|
|
347
|
+
<HistoryLink />
|
|
348
|
+
<span className="sp" />
|
|
349
|
+
<button type="button" className="btn lg" disabled>
|
|
350
|
+
Publish
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
</>,
|
|
354
|
+
380,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
if (keep === 'offline' || !reachable || signedOut) {
|
|
358
|
+
return wrap(
|
|
359
|
+
<>
|
|
360
|
+
<div className="pop-h">
|
|
361
|
+
<div>
|
|
362
|
+
<h2>
|
|
363
|
+
{n} change{n === 1 ? '' : 's'} waiting
|
|
364
|
+
</h2>
|
|
365
|
+
<p className="sub">
|
|
366
|
+
{signedOut ? 'Publishing needs you signed in.' : 'Publishing needs the server.'}
|
|
367
|
+
</p>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
<div className="pop-f">
|
|
371
|
+
<HistoryLink />
|
|
372
|
+
<span className="sp" />
|
|
373
|
+
<button type="button" className="btn lg" disabled>
|
|
374
|
+
Publish
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
</>,
|
|
378
|
+
380,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return wrap(
|
|
382
|
+
<>
|
|
383
|
+
<div className="pop-h">
|
|
384
|
+
<div>
|
|
385
|
+
<h2>
|
|
386
|
+
{n} change{n === 1 ? '' : 's'} waiting
|
|
387
|
+
</h2>
|
|
388
|
+
</div>
|
|
389
|
+
<span className="r">{whoCounts(status.waiting)}</span>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="pop-b">
|
|
392
|
+
<ul className="wlist">
|
|
393
|
+
{status.waiting.map((w) => (
|
|
394
|
+
<WaitItem key={w.file} item={w} status={status} />
|
|
395
|
+
))}
|
|
396
|
+
</ul>
|
|
397
|
+
{publishError ? (
|
|
398
|
+
<div className="note danger tight" role="alert" data-note="publish-failed">
|
|
399
|
+
<Icon name="warn" />
|
|
400
|
+
<div>
|
|
401
|
+
<b>It didn't publish</b>
|
|
402
|
+
<p>Nothing went live. ({publishError})</p>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
) : null}
|
|
406
|
+
</div>
|
|
407
|
+
<div className="pop-f">
|
|
408
|
+
<HistoryLink />
|
|
409
|
+
<span className="sp" />
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
className="btn lg accent"
|
|
413
|
+
data-act="publish"
|
|
414
|
+
disabled={busy}
|
|
415
|
+
onClick={async () => {
|
|
416
|
+
setBusy(true);
|
|
417
|
+
setPublishError(null);
|
|
418
|
+
const r = await editor.publish();
|
|
419
|
+
setBusy(false);
|
|
420
|
+
if (!r.ok) setPublishError(r.error ?? 'the server said no.');
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
Publish {n} change{n === 1 ? '' : 's'}
|
|
424
|
+
</button>
|
|
425
|
+
</div>
|
|
426
|
+
</>,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function whoCounts(waiting: readonly WaitingView[]): string {
|
|
431
|
+
const counts = new Map<string, { person: Person; n: number }>();
|
|
432
|
+
for (const w of waiting)
|
|
433
|
+
for (const e of w.editors) {
|
|
434
|
+
const k = e.person.email.toLowerCase();
|
|
435
|
+
const c = counts.get(k) ?? { person: e.person, n: 0 };
|
|
436
|
+
c.n += 1;
|
|
437
|
+
counts.set(k, c);
|
|
438
|
+
}
|
|
439
|
+
return [...counts.values()]
|
|
440
|
+
.sort((a, b) => b.n - a.n)
|
|
441
|
+
.map((c) => `${displayName(c.person)} ${c.n}`)
|
|
442
|
+
.join(' · ');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function WaitItem({ item, status }: { item: WaitingView; status: StatusResponse }) {
|
|
446
|
+
const editor = useEditor();
|
|
447
|
+
const others = item.editingNow.filter((p) => !samePerson(p, status.you));
|
|
448
|
+
const editing = item.editingNow.length > 0;
|
|
449
|
+
const notice = status.notices.find((n) => n.kind === 'draft-kept' && n.file === item.file);
|
|
450
|
+
const collection = editor.site.collections.find((c) => c.name === item.collection);
|
|
451
|
+
return (
|
|
452
|
+
<li className="wi" data-waiting={item.file}>
|
|
453
|
+
<div className="wi-top">
|
|
454
|
+
<Icon name={collection ? docIcon(collection) : 'page'} />
|
|
455
|
+
<b>{item.title}</b>
|
|
456
|
+
{editing ? <span className="chip waiting">Editing now</span> : null}
|
|
457
|
+
{/* M5a: a document that isn't on the live site yet, or that leaves it at this Publish. */}
|
|
458
|
+
{item.change === 'deleted' ? (
|
|
459
|
+
<span className="chip danger" data-change="deleted">
|
|
460
|
+
Deleted
|
|
461
|
+
</span>
|
|
462
|
+
) : item.movedFrom ? (
|
|
463
|
+
<span className="chip waiting" data-change="moved">
|
|
464
|
+
Moved
|
|
465
|
+
</span>
|
|
466
|
+
) : item.change === 'new' ? (
|
|
467
|
+
<span className="chip waiting" data-change="new">
|
|
468
|
+
New {collection ? singular(collection) : 'page'}
|
|
469
|
+
</span>
|
|
470
|
+
) : null}
|
|
471
|
+
<span className="when">
|
|
472
|
+
{item.editors.map((e) => (
|
|
473
|
+
<Avatar key={e.person.email} person={e.person} size="sm" />
|
|
474
|
+
))}
|
|
475
|
+
{item.editors.map((e) => displayName(e.person)).join(', ')}
|
|
476
|
+
{item.changedAt ? ` · ${when(item.changedAt, status.timeZone, status.now)}` : ''}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
{item.change === 'deleted' ? <div className="wi-what">It leaves the live site at the next Publish.</div> : null}
|
|
480
|
+
{item.movedFrom ? (
|
|
481
|
+
<div className="wi-what" data-moved-from={item.movedFrom}>
|
|
482
|
+
{item.movedRedirect ? `${item.movedFrom} redirects here.` : `Moved from ${item.movedFrom}.`}
|
|
483
|
+
</div>
|
|
484
|
+
) : null}
|
|
485
|
+
{collection?.kind === 'settings' ? (
|
|
486
|
+
<div className="chips">
|
|
487
|
+
<span className="chip live" data-chip="applies">
|
|
488
|
+
<Icon name="bolt" size="xs" />
|
|
489
|
+
Sign-in and branding already apply
|
|
490
|
+
</span>
|
|
491
|
+
</div>
|
|
492
|
+
) : null}
|
|
493
|
+
{others.length ? (
|
|
494
|
+
<div className="wi-what">
|
|
495
|
+
{joinNames(others.map(displayName))} {others.length === 1 ? 'is' : 'are'} still in it: it goes out as it stands, and they're told.
|
|
496
|
+
</div>
|
|
497
|
+
) : null}
|
|
498
|
+
{notice ? (
|
|
499
|
+
<div className="note warn tight" data-note="draft-kept">
|
|
500
|
+
<Icon name="warn" />
|
|
501
|
+
<div>
|
|
502
|
+
<b>{item.title} was also changed outside the editor.</b>
|
|
503
|
+
<p>Your draft was kept; check it before publishing.</p>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
) : null}
|
|
507
|
+
{item.change !== 'deleted' && item.missingAlt?.length ? <AltGaps item={item} held={others} /> : null}
|
|
508
|
+
</li>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** One waiting document's images without alt text (#publish-alt), each with Add alt text. It doesn't hold the Publish back. */
|
|
513
|
+
function AltGaps({ item, held }: { item: WaitingView; held: Person[] }) {
|
|
514
|
+
const editor = useEditor();
|
|
515
|
+
const gaps = item.missingAlt;
|
|
516
|
+
const fix = (gap: MissingAltView) => (
|
|
517
|
+
<button type="button" className="btn sm" data-act="fix-alt" data-alt-path={gap.path} onClick={() => void openAtAlt(editor, item, gap, { held: held.length > 0 })}>
|
|
518
|
+
Add alt text
|
|
519
|
+
</button>
|
|
520
|
+
);
|
|
521
|
+
const heldLine = held.length ? (
|
|
522
|
+
<p data-held="">
|
|
523
|
+
{joinNames(held.map(displayName))} {held.length === 1 ? 'is' : 'are'} editing {item.title}, so it opens read-only for now.
|
|
524
|
+
</p>
|
|
525
|
+
) : null;
|
|
526
|
+
return (
|
|
527
|
+
<div className="note warn tight" data-slot="alt-text" data-note="alt-text">
|
|
528
|
+
<Icon name="image" />
|
|
529
|
+
{gaps.length === 1 ? (
|
|
530
|
+
<div>
|
|
531
|
+
<b>{gaps[0]!.label} has no alt text</b>
|
|
532
|
+
<p>Doesn't block Publish.</p>
|
|
533
|
+
{heldLine}
|
|
534
|
+
<div className="acts">{fix(gaps[0]!)}</div>
|
|
535
|
+
</div>
|
|
536
|
+
) : (
|
|
537
|
+
<div>
|
|
538
|
+
<b>{gaps.length} images have no alt text</b>
|
|
539
|
+
<p>Doesn't block Publish.</p>
|
|
540
|
+
{heldLine}
|
|
541
|
+
<div className="alt-rows">
|
|
542
|
+
{gaps.map((gap) => (
|
|
543
|
+
<div key={gap.path + (gap.block ?? '')} className="alt-row" data-alt-row={gap.path}>
|
|
544
|
+
<span className="t">{gap.label}</span>
|
|
545
|
+
{fix(gap)}
|
|
546
|
+
</div>
|
|
547
|
+
))}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function HistoryPop() {
|
|
556
|
+
const editor = useEditor();
|
|
557
|
+
const [history, setHistory] = useState(() => histories.get(editor) ?? null);
|
|
558
|
+
const status = useStore(editor.liveState, (s) => s.status);
|
|
559
|
+
const ref = useOutside(() => editor.setPop(null), '.pub');
|
|
560
|
+
// Read again when opened (at most once a minute on the server); the last read shows meanwhile.
|
|
561
|
+
useEffect(() => readHistory(editor, setHistory), [editor]);
|
|
562
|
+
const timeZone = history?.timeZone ?? status?.timeZone ?? 'UTC';
|
|
563
|
+
const now = status?.now ?? new Date().toISOString();
|
|
564
|
+
const entries = history?.entries ?? [];
|
|
565
|
+
return (
|
|
566
|
+
<div ref={ref} className="pop" role="dialog" aria-label="Publish history" data-pop="history" style={{ right: 8, width: 480 }}>
|
|
567
|
+
<div className="pop-h">
|
|
568
|
+
<button type="button" className="ib" aria-label="Back to waiting changes" onClick={() => editor.setPop('waiting')}>
|
|
569
|
+
<Icon name="chev-l" />
|
|
570
|
+
</button>
|
|
571
|
+
<div>
|
|
572
|
+
<h2>Publish history</h2>
|
|
573
|
+
<p className="sub">{zoneLine(timeZone)}</p>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
<div className="pop-b">
|
|
577
|
+
{history && entries.length === 0 ? (
|
|
578
|
+
<div className="empty" style={{ marginBottom: 'var(--space-4)' }}>
|
|
579
|
+
<b>Nothing published yet</b>
|
|
580
|
+
</div>
|
|
581
|
+
) : (
|
|
582
|
+
<table className="hist">
|
|
583
|
+
<tbody>
|
|
584
|
+
{entries.map((h) => (
|
|
585
|
+
<tr key={h.commit} data-history={h.commit}>
|
|
586
|
+
<td className="when">{when(h.at, timeZone, now)}</td>
|
|
587
|
+
<td className="who">{h.by ? <span><Avatar person={h.by} size="sm" />{displayName(h.by)}</span> : null}</td>
|
|
588
|
+
<td className="docs">{joinNames(h.titles)}</td>
|
|
589
|
+
<td className="st">
|
|
590
|
+
{h.state === 'live' ? (
|
|
591
|
+
<span className="chip live">
|
|
592
|
+
<Icon name="check" size="xs" />
|
|
593
|
+
Live
|
|
594
|
+
</span>
|
|
595
|
+
) : h.state === 'failed' ? (
|
|
596
|
+
<span className="chip danger">
|
|
597
|
+
<Icon name="warn" size="xs" />
|
|
598
|
+
Failed
|
|
599
|
+
</span>
|
|
600
|
+
) : (
|
|
601
|
+
<span className="chip waiting">Publishing</span>
|
|
602
|
+
)}
|
|
603
|
+
</td>
|
|
604
|
+
</tr>
|
|
605
|
+
))}
|
|
606
|
+
</tbody>
|
|
607
|
+
</table>
|
|
608
|
+
)}
|
|
609
|
+
</div>
|
|
610
|
+
{history ? null : (
|
|
611
|
+
<div className="pop-f">
|
|
612
|
+
<span className="fine">Reading the history…</span>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
</div>
|
|
616
|
+
);
|
|
617
|
+
}
|