@goodfellow-cms/admin 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as useDebounced, A as useRoute, At as redirectsFileChange, B as FrameServicesProvider, C as AppLink, Ct as menusFileChange, D as hasUnsavedChanges, Dt as newPageData, E as entryEditorHref, Et as newCollectionSettings, F as PluginScreenFrame, H as useFrame, I as usePluginFrame, It as uniqueName, J as Dialog, K as draftStarted, L as usePluginHost, Lt as updateSettingsFileChange, M as prepareUpload, Mt as slugify, N as useOutsideHost, Nt as storedCss, O as navigate, Ot as pageFileChange, P as PublishFailure, Pt as storedData, Q as TextField, R as createMediaPreviews, S as MediaScreen, St as mediaFileChange, T as collectionHref, Tt as movePageChanges, U as useFrameServices, V as reloadFrames, W as useFrameUpdates, X as Field, Y as ErrorMessage, Z as PublishedNotice, _ as scheduleSettingsChanges, _t as customCssFileChange, bt as entryFileChange$1, c as useCanChangeSettings, ct as mediaUrl, d as useApplyThemeChoice, dt as addressPatternFor, et as StringsProvider, f as useDarkMode, ft as checkEntrySlug, g as ScheduleStatus, gt as collectionSettingsChanges, h as ScheduleFields, ht as collectionFileChange$1, it as describeContrast, j as useUnsavedChanges, jt as siteSettingsFileChange, k as pageEditorHref, kt as pageTitle$1, l as useSiteContent, lt as siteImages, m as ComingUp, mt as codeFileChanges, n as formValues, nt as useStringTable, o as AdminProvider, ot as allMedia, p as useThemeChoice$1, pt as checkPageAddress, q as Button, r as PreviewFrame, rt as useStrings, s as useAdmin, st as fileName, t as SCHEDULE_PROP, tt as defaultStrings, u as setThemeChoice, ut as uploadName, v as REGISTRY_VERSION, vt as deleteCollectionChanges, w as SETTINGS_SECTIONS, wt as moveEntryChanges, x as MediaField, xt as layoutFileChange, yt as duplicatePageData } from "./entry-values-BrY3L0Ai.js";
2
- import { APP_API_HEADER, BROKER_AI_ADDRESS, BROKER_CONNECT_MESSAGE, BROKER_READY_MESSAGE, CODE_THEMES, COLLECTIONS_DIR, CONFIG_FILE, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DomainError, EditorsError, FIELD_TYPES, FOOTER_FILE, GOODFELLOW_VERSION, HEADER_FILE, HeadCodeError, INSTALLED_PLUGINS_FILE, INSTALLED_RECORD_FILE, MEDIA_DIR, NIGHTLY_MODES, NPM_REGISTRY, PackageError, PluginInstallError, PluginTaskError, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RELEASE_AGE_DAYS, RegistryError, SetupError, SignInError, THEME_COLORS, THEME_FILE, THEME_RELEASE_AGE_DAYS, TITLE_FIELD, ThemeError, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, addressPatternProblem, adminOrigin, allPages, availableBlocks, availableUpdates, blockUses, brokerAddressProblem, brokerOrigin, brokerReleaseUrl, checkAdminAddress, checkEditedData, checkRecords, chooseRelease, chooseThemeRelease, collectionFileSchema, compareReleases, configAdmin, configBroker, contentInLanguage, contrastProblems, credentialStorage, decodeBase64, demoContentStore, demoStore, describeSiteChanges, describedConfig, domainZone, encodeBase64Bytes, entryAddress, entryLanguage, entryPathIn, entrySourceHash, entryTitle, entryTranslations, eventValueProblem, fetchBrokerRelease, fetchPluginPackage, fetchTheme, findEntry, formTarget, getPageHead, hasLanguages, inRange, isAddressSegment, isEmptyValue, isLanguageCode, isLanguageHome, isReservedPagePath, languageLayoutFile, languageName, languagePrefix, loadSiteContent, mainLanguage, menusFileSchema, normalizeDomain, pageLanguage, pageLinks, pageSourceHash, pageTitle, pageTranslations, parseDemoContent, parseHeadCode, parseInstalledPlugins, parseInstalledRecord, parseThemeRecord, parseUpdateSettings, pathLanguage, permissionChanges, planBlockChanges, planBroker, planPluginInstall, planPluginRemove, planTheme, pluginReleases, pluginsOutOfRange, recordsInPlace, redirectFrom, redirectProblems, registrarName, releaseVersions, remoteBackend, reviewPluginPackages, scheduleOf, scheduleProblem, searchPlugins, searchThemes, serializeContent, siteChangesSince, siteHistory, siteLanguages, siteRestoreChanges, siteSettingsSchema, siteVersion, storedRedirects, themeFileUrl, themeLabel, themeLoads, themeReleases, themeToCss, themeUnknownBlocks, todayIn, translationState, trimChars, withAdminConfig, withBrokerConfig, withSchedule, writesLockfile } from "@goodfellow-cms/core";
1
+ import { $ as PublishedNotice, A as useRoute, At as pageFileChange, B as mayChange, C as AppLink, Ct as layoutFileChange, D as hasUnsavedChanges, Dt as movePageChanges, E as entryEditorHref, Et as moveEntryChanges, F as PluginScreenFrame, Ft as storedCss, G as useFrameServices, H as FrameServicesProvider, I as usePluginFrame, It as storedData, J as draftStarted, K as useFrameUpdates, L as usePluginHost, M as prepareUpload, Mt as redirectsFileChange, N as useOutsideHost, Nt as siteSettingsFileChange, O as navigate, Ot as newCollectionSettings, P as PublishFailure, Pt as slugify, Q as Field, R as createMediaPreviews, Rt as uniqueName, S as MediaScreen, St as entryFileChange$1, T as collectionHref, Tt as menusFileChange, U as reloadFrames, V as useMayChange, W as useFrame, X as Dialog, Y as Button, Z as ErrorMessage, _ as scheduleSettingsChanges, _t as collectionFileChange$1, at as useStrings, bt as deleteCollectionChanges, c as useCanChangeSettings, ct as allMedia, d as useApplyThemeChoice, dt as siteImages, et as TextField, f as useDarkMode, ft as uploadName, g as ScheduleStatus, gt as codeFileChanges, h as ScheduleFields, ht as checkPageAddress, it as useStringTable, j as useUnsavedChanges, jt as pageTitle$1, k as pageEditorHref, kt as newPageData, l as useSiteContent, lt as fileName, m as ComingUp, mt as checkEntrySlug, n as formValues, nt as StringsProvider, o as AdminProvider, ot as describeContrast, p as useThemeChoice$1, pt as addressPatternFor, r as PreviewFrame, rt as defaultStrings, s as useAdmin, t as SCHEDULE_PROP, tt as useDebounced, u as setThemeChoice, ut as mediaUrl, v as REGISTRY_VERSION, vt as collectionSettingsChanges, w as SETTINGS_SECTIONS, wt as mediaFileChange, x as MediaField, xt as duplicatePageData, yt as customCssFileChange, zt as updateSettingsFileChange } from "./entry-values-DO01xZ9L.js";
2
+ import { APP_API_HEADER, BROKER_AI_ADDRESS, BROKER_CONNECT_MESSAGE, BROKER_READY_MESSAGE, CODE_THEMES, COLLECTIONS_DIR, CONFIG_FILE, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DomainError, EditorsError, FIELD_TYPES, FOOTER_FILE, GOODFELLOW_VERSION, HEADER_FILE, HeadCodeError, INSTALLED_PLUGINS_FILE, INSTALLED_RECORD_FILE, MEDIA_DIR, NIGHTLY_MODES, NPM_REGISTRY, PackageError, PluginInstallError, PluginTaskError, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RELEASE_AGE_DAYS, RegistryError, SetupError, SignInError, THEME_COLORS, THEME_FILE, THEME_RELEASE_AGE_DAYS, TITLE_FIELD, ThemeError, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, addressPatternProblem, adminOrigin, allPages, availableBlocks, availableUpdates, blockUses, brokerAddressProblem, brokerOrigin, brokerReleaseUrl, checkAdminAddress, checkEditedData, checkRecords, chooseRelease, chooseThemeRelease, collectionFileSchema, compareReleases, configAdmin, configBroker, contentInLanguage, contrastProblems, credentialStorage, decodeBase64, demoContentStore, demoStore, describeSiteChanges, describedConfig, domainZone, encodeBase64Bytes, entryAddress, entryLanguage, entryPathIn, entrySourceHash, entryTitle, entryTranslations, eventValueProblem, fetchBrokerRelease, fetchPluginPackage, fetchTheme, findEntry, formTarget, getPageHead, googleFontsUrl, hasLanguages, inRange, isAddressSegment, isEmptyValue, isLanguageCode, isLanguageHome, isReservedPagePath, languageLayoutFile, languageName, languagePrefix, loadSiteContent, mainLanguage, menusFileSchema, normalizeDomain, pageLanguage, pageLinks, pageSourceHash, pageTitle, pageTranslations, parseDemoContent, parseHeadCode, parseInstalledPlugins, parseInstalledRecord, parseThemeRecord, parseUpdateSettings, pathLanguage, permissionChanges, permissionsFileChange, planBlockChanges, planBroker, planPluginInstall, planPluginRemove, planTheme, pluginReleases, pluginsOutOfRange, recordsInPlace, redirectFrom, redirectProblems, registrarName, releaseVersions, remoteBackend, reviewPluginPackages, scheduleOf, scheduleProblem, searchPlugins, searchThemes, serializeContent, siteChangesSince, siteHistory, siteLanguages, siteRestoreChanges, siteSettingsSchema, siteVersion, storedRedirects, themeFileUrl, themeLabel, themeLoads, themeReleases, themeToCss, themeUnknownBlocks, todayIn, translationState, trimChars, withAdminConfig, withBrokerConfig, withSchedule, writesLockfile } from "@goodfellow-cms/core";
3
3
  import { Fragment, useCallback, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -2033,14 +2033,17 @@ function PluginEditorFrame({ plugin, target, data, onOpen, onChange }) {
2033
2033
  themeCss
2034
2034
  };
2035
2035
  const opening = useRef(void 0);
2036
+ const [ready, setReady] = useState(false);
2036
2037
  const hooks = useMemo(() => ({
2037
2038
  open: () => {
2038
2039
  const { target, data, content, description } = latest.current;
2039
2040
  const opened = data();
2040
2041
  opening.current = opened;
2042
+ setReady(false);
2041
2043
  return editorOpen(target, opened, content, description);
2042
2044
  },
2043
2045
  onOpened: (opened, channel) => {
2046
+ setReady(true);
2044
2047
  latest.current.onOpen(editorHandle(opened, channel), opening.current ?? latest.current.data());
2045
2048
  },
2046
2049
  onChange: ({ data, state }) => {
@@ -2064,19 +2067,31 @@ function PluginEditorFrame({ plugin, target, data, onOpen, onChange }) {
2064
2067
  },
2065
2068
  styles: async () => {
2066
2069
  const { services, content, themeCss } = latest.current;
2070
+ const fonts = googleFontsUrl(content.settings.theme);
2067
2071
  return {
2068
2072
  stylesheets: [...await services.siteCss(), themeToCss(content.settings.theme)],
2069
- tailwind: `${themeCss}\n${content.customCss}`
2073
+ tailwind: `${themeCss}\n${content.customCss}`,
2074
+ ...fonts && { fonts }
2070
2075
  };
2071
2076
  }
2072
2077
  }), []);
2073
2078
  const { failed, frameProps } = usePluginFrame(plugin, host, "editor", void 0, hooks);
2074
2079
  const name = plugin.editor?.title ?? plugin.title;
2075
2080
  if (failed) return /* @__PURE__ */ jsx(ErrorMessage, { message: t("editor.plugin.failed", { editor: name }) });
2076
- return /* @__PURE__ */ jsx("iframe", {
2077
- className: "gfa-editor-frame",
2078
- title: t("editor.frame", { title: target.title }),
2079
- ...frameProps
2081
+ return /* @__PURE__ */ jsxs("div", {
2082
+ className: "gfa-plugin-editor-wrap",
2083
+ children: [/* @__PURE__ */ jsx("iframe", {
2084
+ className: "gfa-editor-frame",
2085
+ title: t("editor.frame", { title: target.title }),
2086
+ ...frameProps
2087
+ }), !ready && /* @__PURE__ */ jsxs("div", {
2088
+ className: "gfa-editor-loading",
2089
+ role: "status",
2090
+ children: [/* @__PURE__ */ jsx("span", {
2091
+ className: "gfa-spinner",
2092
+ "aria-hidden": "true"
2093
+ }), t("editor.plugin.loading", { editor: name })]
2094
+ })]
2080
2095
  });
2081
2096
  }
2082
2097
  /** What the header asks the editor's frame for, as far as it said it can. */
@@ -2154,6 +2169,18 @@ function sharePreview(content, path, root) {
2154
2169
  * can do, and the AI assistant, which keeps editors' keys out of the frame.
2155
2170
  */
2156
2171
  /** Where the editors' sidebars' widths are remembered: items' fields have a wider one of their own. */
2172
+ /** One thing an editor plugin gave back that the site can't keep, in words: the block and setting, then what's wrong. */
2173
+ function problemLine(t, problem) {
2174
+ const where = problem.block ?? t("editor.plugin.pageSettings");
2175
+ return problem.field ? t("editor.plugin.problemField", {
2176
+ block: where,
2177
+ field: problem.field,
2178
+ message: problem.message
2179
+ }) : t("editor.plugin.problem", {
2180
+ block: where,
2181
+ message: problem.message
2182
+ });
2183
+ }
2157
2184
  const WIDTHS_KEY = "goodfellow-editor-sidebar-widths";
2158
2185
  const ENTRY_WIDTHS_KEY = "goodfellow-entry-sidebar-widths";
2159
2186
  function readWidths(key) {
@@ -2188,6 +2215,20 @@ const VIEWPORTS = [
2188
2215
  function PuckEditor({ kind, path, title, data, collection, entry, validate, toChanges, actions, notice, language }) {
2189
2216
  const t = useStrings();
2190
2217
  const { site, publish, reload, drafts } = useAdmin();
2218
+ const readOnly = !useMayChange(kind === "page" ? {
2219
+ kind,
2220
+ path
2221
+ } : kind === "entry" ? {
2222
+ kind,
2223
+ collection: collection?.id ?? "",
2224
+ entry: entry ?? ""
2225
+ } : kind === "template" ? {
2226
+ kind,
2227
+ collection: collection?.id ?? ""
2228
+ } : {
2229
+ kind,
2230
+ ...language && { language }
2231
+ });
2191
2232
  const editors = useEditorChoice(site.description, kind);
2192
2233
  const plugin = editors.chosen;
2193
2234
  const editorName = plugin?.editor?.title ?? plugin?.title ?? "";
@@ -2211,7 +2252,8 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2211
2252
  data,
2212
2253
  ...collection && { collection: collection.id },
2213
2254
  ...entry && { entry },
2214
- ...language && { language }
2255
+ ...language && { language },
2256
+ ...readOnly && { readOnly }
2215
2257
  }), [
2216
2258
  kind,
2217
2259
  path,
@@ -2219,7 +2261,8 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2219
2261
  data,
2220
2262
  collection,
2221
2263
  entry,
2222
- language
2264
+ language,
2265
+ readOnly
2223
2266
  ]);
2224
2267
  const onConnect = useCallback(async (client) => {
2225
2268
  client.on("editor.state", (next) => {
@@ -2285,12 +2328,23 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2285
2328
  rightSideBarVisible: false
2286
2329
  });
2287
2330
  }, []);
2288
- /** Changes editor, keeping what's edited: from an editor plugin, only what the site can publish. */
2331
+ /**
2332
+ * Changes editor, keeping what's edited: from Puck, as it is when the choice is made, even if its frame hasn't
2333
+ * said so yet; from an editor plugin, only what the site can publish, and where some of it can't be, only once
2334
+ * the editor has agreed to leave those changes behind.
2335
+ */
2289
2336
  const chooseEditor = async (name) => {
2290
2337
  if (plugin && pluginEditor) {
2291
2338
  const checked = await pluginData(pluginEditor, latest.current.data);
2339
+ if (!checked.ok) {
2340
+ const problems = checked.problems.map((problem) => problemLine(t, problem)).join("\n");
2341
+ if (!window.confirm(t("editor.plugin.switchLoses", {
2342
+ editor: editorName,
2343
+ problems
2344
+ }))) return;
2345
+ }
2292
2346
  latest.current.data = checked.ok ? checked.data : pluginEditor.opened;
2293
- }
2347
+ } else if (!plugin && client) latest.current.data = await client.call("editor.data").catch(() => latest.current.data);
2294
2348
  setPluginEditor(void 0);
2295
2349
  editors.choose(name);
2296
2350
  };
@@ -2305,7 +2359,7 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2305
2359
  setStatus({
2306
2360
  type: "invalid",
2307
2361
  message: t("editor.plugin.invalid", { editor: editorName }),
2308
- details: checked.problems.map((problem) => `${problem.path}: ${problem.message}`).join("\n")
2362
+ details: checked.problems.map((problem) => problemLine(t, problem)).join("\n")
2309
2363
  });
2310
2364
  return;
2311
2365
  }
@@ -2381,6 +2435,10 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2381
2435
  className: "gfa-editor-notice",
2382
2436
  children: notice
2383
2437
  }),
2438
+ readOnly && /* @__PURE__ */ jsx("div", {
2439
+ className: "gfa-editor-notice",
2440
+ children: t("limits.readOnly")
2441
+ }),
2384
2442
  /* @__PURE__ */ jsxs("div", {
2385
2443
  className: "gfa-editor-header",
2386
2444
  children: [
@@ -2461,13 +2519,13 @@ function PuckEditor({ kind, path, title, data, collection, entry, validate, toCh
2461
2519
  onClick: () => setAiOpen(!aiOpen),
2462
2520
  children: t("ai.label")
2463
2521
  }),
2464
- !drafts.current && /* @__PURE__ */ jsx(Button, {
2522
+ !drafts.current && !readOnly && /* @__PURE__ */ jsx(Button, {
2465
2523
  title: t("draft.addHint"),
2466
2524
  disabled: !state || status.type === "publishing",
2467
2525
  onClick: () => void onPublish(true),
2468
2526
  children: t("draft.add")
2469
2527
  }),
2470
- /* @__PURE__ */ jsx(Button, {
2528
+ !readOnly && /* @__PURE__ */ jsx(Button, {
2471
2529
  variant: "primary",
2472
2530
  disabled: !state || status.type === "publishing",
2473
2531
  onClick: () => void onPublish(),
@@ -5013,7 +5071,7 @@ async function answers(address) {
5013
5071
  /** Connects a custom domain to a site published with its git host's own Pages, and follows it until it works. */
5014
5072
  function DomainScreen() {
5015
5073
  const t = useStrings();
5016
- const { pages, account, demo, publish } = useAdmin();
5074
+ const { pages, account, demo, publish, site: about } = useAdmin();
5017
5075
  const { content } = useSiteContent();
5018
5076
  const host = account?.hostName ?? "";
5019
5077
  const outside = useOutsideHost();
@@ -5075,6 +5133,10 @@ function DomainScreen() {
5075
5133
  className: "gfa-form",
5076
5134
  children: [
5077
5135
  /* @__PURE__ */ jsx("p", { children: t("domain.intro") }),
5136
+ about.copy?.url && /* @__PURE__ */ jsx("p", {
5137
+ className: "gfa-hint",
5138
+ children: t("domain.copy", { address: about.copy.url })
5139
+ }),
5078
5140
  /* @__PURE__ */ jsx(OwnerTokenActive, { onForget: loadSite }),
5079
5141
  body,
5080
5142
  !demo && (pages || outside) && /* @__PURE__ */ jsx(AdminAddressSection, { outside })
@@ -5493,6 +5555,301 @@ function LayoutEditorScreen({ part, language }) {
5493
5555
  });
5494
5556
  }
5495
5557
  //#endregion
5558
+ //#region src/permissions-section.tsx
5559
+ /** Limits a new person starts with: nothing, until the owner ticks what they may change. */
5560
+ const NO_LIMITS = {
5561
+ pages: [],
5562
+ collections: []
5563
+ };
5564
+ /** The page sections an owner can choose from: every page, then each page at the top of the main language's addresses. */
5565
+ function pageSections(content) {
5566
+ const main = mainLanguage(content.settings);
5567
+ const sections = /* @__PURE__ */ new Set();
5568
+ for (const page of content.pages) {
5569
+ if (pathLanguage(content.settings, page.path) !== main) continue;
5570
+ const first = page.path.split("/")[1];
5571
+ if (first) sections.add(`/${first}`);
5572
+ }
5573
+ return ["/", ...[...sections].sort()];
5574
+ }
5575
+ function toggled(list, value, on) {
5576
+ const without = (list ?? []).filter((item) => item !== value);
5577
+ return on ? [...without, value] : without;
5578
+ }
5579
+ /**
5580
+ * What each limited person may change, and who reviews the rest, written to
5581
+ * `content/permissions.json`. Anyone it doesn't list may change everything.
5582
+ */
5583
+ function PermissionsSection() {
5584
+ const t = useStrings();
5585
+ const { publish, account } = useAdmin();
5586
+ const { content } = useSiteContent();
5587
+ const permissions = content.permissions ?? { people: {} };
5588
+ const [editing, setEditing] = useState();
5589
+ const [login, setLogin] = useState("");
5590
+ const [busy, setBusy] = useState(false);
5591
+ const [reviewers, setReviewers] = useState((permissions.reviewers ?? []).join(", "));
5592
+ const people = Object.entries(permissions.people).sort(([a], [b]) => a.localeCompare(b));
5593
+ const save = async (next, message) => {
5594
+ setBusy(true);
5595
+ const result = await publish([permissionsFileChange(next)], message, { checks: false });
5596
+ setBusy(false);
5597
+ return result.ok;
5598
+ };
5599
+ const remove = (person) => {
5600
+ const { [person]: _, ...rest } = permissions.people;
5601
+ save({
5602
+ ...permissions,
5603
+ people: rest
5604
+ }, t("permissions.removeMessage", { name: person }));
5605
+ };
5606
+ const named = reviewers.split(/[\s,]+/).map((name) => name.trim()).filter(Boolean);
5607
+ return /* @__PURE__ */ jsxs("section", {
5608
+ className: "gfa-form",
5609
+ "aria-labelledby": "gfa-permissions-title",
5610
+ children: [
5611
+ /* @__PURE__ */ jsx("h2", {
5612
+ id: "gfa-permissions-title",
5613
+ className: "gfa-section-title",
5614
+ children: t("permissions.title")
5615
+ }),
5616
+ /* @__PURE__ */ jsx("p", {
5617
+ className: "gfa-hint",
5618
+ children: t("permissions.intro")
5619
+ }),
5620
+ /* @__PURE__ */ jsx("p", {
5621
+ className: "gfa-hint",
5622
+ children: account?.role ? t("permissions.whereServer") : t("permissions.where", { host: account?.hostName || t("permissions.host") })
5623
+ }),
5624
+ people.length > 0 && /* @__PURE__ */ jsx("ul", {
5625
+ className: "gfa-check-list",
5626
+ children: people.map(([person, limits]) => /* @__PURE__ */ jsxs("li", { children: [
5627
+ /* @__PURE__ */ jsx("strong", { children: person }),
5628
+ " ",
5629
+ /* @__PURE__ */ jsx("span", {
5630
+ className: "gfa-hint",
5631
+ children: describeLimits(t, limits)
5632
+ }),
5633
+ " ",
5634
+ /* @__PURE__ */ jsx(Button, {
5635
+ variant: "ghost",
5636
+ disabled: busy,
5637
+ onClick: () => setEditing({
5638
+ login: person,
5639
+ limits
5640
+ }),
5641
+ children: t("permissions.change")
5642
+ }),
5643
+ /* @__PURE__ */ jsx(Button, {
5644
+ variant: "ghost",
5645
+ disabled: busy,
5646
+ onClick: () => remove(person),
5647
+ children: t("permissions.remove")
5648
+ })
5649
+ ] }, person))
5650
+ }),
5651
+ /* @__PURE__ */ jsxs("form", {
5652
+ className: "gfa-form",
5653
+ onSubmit: (event) => {
5654
+ event.preventDefault();
5655
+ const name = login.trim();
5656
+ if (!name) return;
5657
+ setEditing({
5658
+ login: name,
5659
+ limits: permissions.people[name] ?? NO_LIMITS
5660
+ });
5661
+ },
5662
+ children: [/* @__PURE__ */ jsx(TextField, {
5663
+ label: t("permissions.person"),
5664
+ hint: t("permissions.personHint"),
5665
+ value: login,
5666
+ autoComplete: "off",
5667
+ onChange: setLogin
5668
+ }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
5669
+ type: "submit",
5670
+ disabled: busy || !login.trim(),
5671
+ children: t("permissions.add")
5672
+ }) })]
5673
+ }),
5674
+ /* @__PURE__ */ jsxs("form", {
5675
+ className: "gfa-form",
5676
+ onSubmit: (event) => {
5677
+ event.preventDefault();
5678
+ const { reviewers: _, ...rest } = permissions;
5679
+ save(named.length > 0 ? {
5680
+ ...rest,
5681
+ reviewers: named
5682
+ } : rest, t("permissions.reviewersMessage"));
5683
+ },
5684
+ children: [/* @__PURE__ */ jsx(TextField, {
5685
+ label: t("permissions.reviewers"),
5686
+ hint: t("permissions.reviewersHint"),
5687
+ value: reviewers,
5688
+ autoComplete: "off",
5689
+ onChange: setReviewers
5690
+ }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
5691
+ type: "submit",
5692
+ disabled: busy || named.join(",") === (permissions.reviewers ?? []).join(","),
5693
+ children: t("permissions.saveReviewers")
5694
+ }) })]
5695
+ }),
5696
+ editing && /* @__PURE__ */ jsx(LimitsEditor, {
5697
+ login: editing.login,
5698
+ limits: editing.limits,
5699
+ busy,
5700
+ onClose: () => setEditing(void 0),
5701
+ onSave: async (limits) => {
5702
+ if (await save({
5703
+ ...permissions,
5704
+ people: {
5705
+ ...permissions.people,
5706
+ [editing.login]: limits
5707
+ }
5708
+ }, t("permissions.message", { name: editing.login }))) {
5709
+ setEditing(void 0);
5710
+ setLogin("");
5711
+ }
5712
+ }
5713
+ })
5714
+ ]
5715
+ });
5716
+ }
5717
+ /** Someone's limits in a few words, for the list. */
5718
+ function describeLimits(t, limits) {
5719
+ const parts = [
5720
+ ...(limits.pages ?? []).map((section) => section === "/" ? t("permissions.allPages") : section),
5721
+ ...limits.collections ?? [],
5722
+ ...limits.media ? [t("limits.media")] : [],
5723
+ ...limits.layout ? [t("limits.layout")] : [],
5724
+ ...limits.menus ? [t("limits.menus")] : []
5725
+ ];
5726
+ const text = parts.length > 0 ? parts.join(", ") : t("permissions.nothing");
5727
+ return limits.languages ? `${text} (${limits.languages.join(", ")})` : text;
5728
+ }
5729
+ function Check({ label, checked, onChange }) {
5730
+ return /* @__PURE__ */ jsxs("label", {
5731
+ className: "gfa-checkbox",
5732
+ children: [/* @__PURE__ */ jsx("input", {
5733
+ type: "checkbox",
5734
+ checked,
5735
+ onChange: (event) => onChange(event.target.checked)
5736
+ }), label]
5737
+ });
5738
+ }
5739
+ function LimitsEditor({ login, limits: initial, busy, onClose, onSave }) {
5740
+ const t = useStrings();
5741
+ const { content } = useSiteContent();
5742
+ const [limits, setLimits] = useState(initial);
5743
+ const id = useId();
5744
+ const sections = useMemo(() => {
5745
+ const found = pageSections(content);
5746
+ return [...found, ...(initial.pages ?? []).filter((section) => !found.includes(section))];
5747
+ }, [content, initial.pages]);
5748
+ const languages = [{
5749
+ code: mainLanguage(content.settings),
5750
+ name: void 0
5751
+ }, ...content.settings.languages ?? []];
5752
+ const set = (change) => setLimits((current) => ({
5753
+ ...current,
5754
+ ...change
5755
+ }));
5756
+ return /* @__PURE__ */ jsxs(Dialog, {
5757
+ title: t("permissions.editTitle", { name: login }),
5758
+ onClose,
5759
+ children: [
5760
+ /* @__PURE__ */ jsx("p", { children: t("permissions.editIntro") }),
5761
+ /* @__PURE__ */ jsxs("fieldset", {
5762
+ className: "gfa-fieldset",
5763
+ "aria-describedby": `${id}-pages`,
5764
+ children: [
5765
+ /* @__PURE__ */ jsx("legend", { children: t("permissions.pages") }),
5766
+ /* @__PURE__ */ jsx("p", {
5767
+ id: `${id}-pages`,
5768
+ className: "gfa-hint",
5769
+ children: t("permissions.pagesHint")
5770
+ }),
5771
+ sections.map((section) => /* @__PURE__ */ jsx(Check, {
5772
+ label: section === "/" ? t("permissions.allPages") : section,
5773
+ checked: (limits.pages ?? []).includes(section),
5774
+ onChange: (on) => set({ pages: toggled(limits.pages, section, on) })
5775
+ }, section))
5776
+ ]
5777
+ }),
5778
+ content.collections.length > 0 && /* @__PURE__ */ jsxs("fieldset", {
5779
+ className: "gfa-fieldset",
5780
+ children: [/* @__PURE__ */ jsx("legend", { children: t("permissions.collections") }), content.collections.map((collection) => /* @__PURE__ */ jsx(Check, {
5781
+ label: collection.settings.name,
5782
+ checked: (limits.collections ?? []).includes(collection.id),
5783
+ onChange: (on) => set({ collections: toggled(limits.collections, collection.id, on) })
5784
+ }, collection.id))]
5785
+ }),
5786
+ /* @__PURE__ */ jsxs("fieldset", {
5787
+ className: "gfa-fieldset",
5788
+ children: [
5789
+ /* @__PURE__ */ jsx("legend", { children: t("permissions.more") }),
5790
+ /* @__PURE__ */ jsx(Check, {
5791
+ label: t("limits.media"),
5792
+ checked: limits.media === true,
5793
+ onChange: (on) => set({ media: on })
5794
+ }),
5795
+ /* @__PURE__ */ jsx(Check, {
5796
+ label: t("limits.layout"),
5797
+ checked: limits.layout === true,
5798
+ onChange: (on) => set({ layout: on })
5799
+ }),
5800
+ /* @__PURE__ */ jsx(Check, {
5801
+ label: t("limits.menus"),
5802
+ checked: limits.menus === true,
5803
+ onChange: (on) => set({ menus: on })
5804
+ })
5805
+ ]
5806
+ }),
5807
+ languages.length > 1 && /* @__PURE__ */ jsxs("fieldset", {
5808
+ className: "gfa-fieldset",
5809
+ children: [
5810
+ /* @__PURE__ */ jsx("legend", { children: t("permissions.languages") }),
5811
+ /* @__PURE__ */ jsx(Check, {
5812
+ label: t("permissions.everyLanguage"),
5813
+ checked: !limits.languages,
5814
+ onChange: (on) => setLimits(({ languages: _, ...rest }) => on ? rest : {
5815
+ ...rest,
5816
+ languages: []
5817
+ })
5818
+ }),
5819
+ limits.languages !== void 0 && languages.map(({ code, name }) => /* @__PURE__ */ jsx(Check, {
5820
+ label: name ? `${name} (${code})` : code,
5821
+ checked: limits.languages?.includes(code) === true,
5822
+ onChange: (on) => set({ languages: toggled(limits.languages, code, on) })
5823
+ }, code))
5824
+ ]
5825
+ }),
5826
+ /* @__PURE__ */ jsxs("div", {
5827
+ className: "gfa-dialog-actions",
5828
+ children: [/* @__PURE__ */ jsx(Button, {
5829
+ onClick: onClose,
5830
+ children: t("action.cancel")
5831
+ }), /* @__PURE__ */ jsx(Button, {
5832
+ variant: "primary",
5833
+ disabled: busy,
5834
+ onClick: () => void onSave(cleaned(limits)),
5835
+ children: t(busy ? "publish.publishing" : "permissions.save")
5836
+ })]
5837
+ })
5838
+ ]
5839
+ });
5840
+ }
5841
+ /** Limits as the file keeps them: no `false`, and no empty lists but pages and collections. */
5842
+ function cleaned(limits) {
5843
+ return {
5844
+ pages: limits.pages ?? [],
5845
+ collections: limits.collections ?? [],
5846
+ ...limits.media && { media: true },
5847
+ ...limits.layout && { layout: true },
5848
+ ...limits.menus && { menus: true },
5849
+ ...limits.languages && { languages: limits.languages }
5850
+ };
5851
+ }
5852
+ //#endregion
5496
5853
  //#region src/editors-screen.tsx
5497
5854
  const ROLE_LABELS = {
5498
5855
  owner: "editors.role.owner",
@@ -5506,16 +5863,21 @@ function problemText(error) {
5506
5863
  /** Who edits the site: inviting people, changing what they can do, and removing them. */
5507
5864
  function EditorsScreen() {
5508
5865
  const t = useStrings();
5509
- const { editors, demo } = useAdmin();
5866
+ const { editors, demo, limits } = useAdmin();
5867
+ const owner = useCanChangeSettings() && !limits;
5510
5868
  return /* @__PURE__ */ jsxs("div", {
5511
5869
  className: "gfa-screen",
5512
- children: [/* @__PURE__ */ jsx("div", {
5513
- className: "gfa-screen-header",
5514
- children: /* @__PURE__ */ jsx("h1", { children: t("settings.tab.editors") })
5515
- }), /* @__PURE__ */ jsxs("div", {
5516
- className: "gfa-form",
5517
- children: [/* @__PURE__ */ jsx("p", { children: t("editors.intro") }), demo ? /* @__PURE__ */ jsx("p", { children: t("editors.demo") }) : editors ? /* @__PURE__ */ jsx(EditorsPanel, {}) : /* @__PURE__ */ jsx("p", { children: t("editors.local") })]
5518
- })]
5870
+ children: [
5871
+ /* @__PURE__ */ jsx("div", {
5872
+ className: "gfa-screen-header",
5873
+ children: /* @__PURE__ */ jsx("h1", { children: t("settings.tab.editors") })
5874
+ }),
5875
+ /* @__PURE__ */ jsxs("div", {
5876
+ className: "gfa-form",
5877
+ children: [/* @__PURE__ */ jsx("p", { children: t("editors.intro") }), demo ? /* @__PURE__ */ jsx("p", { children: t("editors.demo") }) : editors ? /* @__PURE__ */ jsx(EditorsPanel, {}) : /* @__PURE__ */ jsx("p", { children: t("editors.local") })]
5878
+ }),
5879
+ owner && /* @__PURE__ */ jsx(PermissionsSection, {})
5880
+ ]
5519
5881
  });
5520
5882
  }
5521
5883
  function EditorsPanel() {
@@ -6482,7 +6844,7 @@ function DeletePageDialog({ page, onClose }) {
6482
6844
  }
6483
6845
  function PagesScreen() {
6484
6846
  const t = useStrings();
6485
- const { siteUrl } = useAdmin();
6847
+ const { siteUrl, limits } = useAdmin();
6486
6848
  const { content } = useSiteContent();
6487
6849
  const [dialog, setDialog] = useState(null);
6488
6850
  const close = () => setDialog(null);
@@ -6507,74 +6869,79 @@ function PagesScreen() {
6507
6869
  className: "gfa-visually-hidden",
6508
6870
  children: t("pages.edit")
6509
6871
  }) })
6510
- ] }) }), /* @__PURE__ */ jsx("tbody", { children: content.pages.map((page) => /* @__PURE__ */ jsxs("tr", { children: [
6511
- /* @__PURE__ */ jsxs("td", { children: [
6512
- /* @__PURE__ */ jsx("a", {
6513
- href: pageEditorHref(page.path),
6514
- className: "gfa-link-strong",
6515
- children: pageTitle$1(page)
6516
- }),
6517
- /* @__PURE__ */ jsx(ScheduleStatus, {
6518
- schedule: page.content,
6519
- content
6520
- }),
6521
- /* @__PURE__ */ jsx(PageTranslations, { page })
6522
- ] }),
6523
- /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: page.path }) }),
6524
- /* @__PURE__ */ jsxs("td", {
6525
- className: "gfa-row-actions",
6526
- children: [
6872
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: content.pages.map((page) => {
6873
+ const changeable = mayChange(content, limits, {
6874
+ kind: "page",
6875
+ path: page.path
6876
+ });
6877
+ return /* @__PURE__ */ jsxs("tr", { children: [
6878
+ /* @__PURE__ */ jsxs("td", { children: [
6527
6879
  /* @__PURE__ */ jsx("a", {
6528
- className: "gfa-button gfa-button-secondary",
6529
6880
  href: pageEditorHref(page.path),
6530
- children: t("pages.edit")
6881
+ className: "gfa-link-strong",
6882
+ children: pageTitle$1(page)
6531
6883
  }),
6532
- /* @__PURE__ */ jsx("a", {
6533
- className: "gfa-button gfa-button-ghost",
6534
- href: `${siteUrl.replace(/\/$/, "")}${page.path}`,
6535
- target: "_blank",
6536
- rel: "noreferrer",
6537
- children: t("pages.view")
6884
+ /* @__PURE__ */ jsx(ScheduleStatus, {
6885
+ schedule: page.content,
6886
+ content
6538
6887
  }),
6539
- /* @__PURE__ */ jsx(Button, {
6540
- variant: "ghost",
6541
- onClick: () => setDialog({
6542
- type: "move",
6543
- page
6888
+ /* @__PURE__ */ jsx(PageTranslations, { page })
6889
+ ] }),
6890
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("code", { children: page.path }) }),
6891
+ /* @__PURE__ */ jsxs("td", {
6892
+ className: "gfa-row-actions",
6893
+ children: [
6894
+ /* @__PURE__ */ jsx("a", {
6895
+ className: "gfa-button gfa-button-secondary",
6896
+ href: pageEditorHref(page.path),
6897
+ children: t(changeable ? "pages.edit" : "pages.look")
6544
6898
  }),
6545
- disabled: page.path === "/",
6546
- children: t("pages.move")
6547
- }),
6548
- /* @__PURE__ */ jsx(Button, {
6549
- variant: "ghost",
6550
- onClick: () => setDialog({
6551
- type: "duplicate",
6552
- page
6899
+ /* @__PURE__ */ jsx("a", {
6900
+ className: "gfa-button gfa-button-ghost",
6901
+ href: `${siteUrl.replace(/\/$/, "")}${page.path}`,
6902
+ target: "_blank",
6903
+ rel: "noreferrer",
6904
+ children: t("pages.view")
6553
6905
  }),
6554
- children: t("pages.duplicate")
6555
- }),
6556
- /* @__PURE__ */ jsx(Button, {
6557
- variant: "ghost",
6558
- onClick: () => setDialog({
6559
- type: "schedule",
6560
- page
6906
+ changeable && /* @__PURE__ */ jsx(Button, {
6907
+ variant: "ghost",
6908
+ onClick: () => setDialog({
6909
+ type: "move",
6910
+ page
6911
+ }),
6912
+ disabled: page.path === "/",
6913
+ children: t("pages.move")
6561
6914
  }),
6562
- disabled: page.path === "/",
6563
- children: t("pages.schedule")
6564
- }),
6565
- /* @__PURE__ */ jsx(Button, {
6566
- variant: "ghost",
6567
- onClick: () => setDialog({
6568
- type: "delete",
6569
- page
6915
+ /* @__PURE__ */ jsx(Button, {
6916
+ variant: "ghost",
6917
+ onClick: () => setDialog({
6918
+ type: "duplicate",
6919
+ page
6920
+ }),
6921
+ children: t("pages.duplicate")
6570
6922
  }),
6571
- disabled: page.path === "/",
6572
- title: page.path === "/" ? t("pages.homeCantDelete") : void 0,
6573
- children: t("pages.delete")
6574
- })
6575
- ]
6576
- })
6577
- ] }, page.path)) })]
6923
+ changeable && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
6924
+ variant: "ghost",
6925
+ onClick: () => setDialog({
6926
+ type: "schedule",
6927
+ page
6928
+ }),
6929
+ disabled: page.path === "/",
6930
+ children: t("pages.schedule")
6931
+ }), /* @__PURE__ */ jsx(Button, {
6932
+ variant: "ghost",
6933
+ onClick: () => setDialog({
6934
+ type: "delete",
6935
+ page
6936
+ }),
6937
+ disabled: page.path === "/",
6938
+ title: page.path === "/" ? t("pages.homeCantDelete") : void 0,
6939
+ children: t("pages.delete")
6940
+ })] })
6941
+ ]
6942
+ })
6943
+ ] }, page.path);
6944
+ }) })]
6578
6945
  }),
6579
6946
  dialog?.type === "new" && /* @__PURE__ */ jsx(NewPageDialog, { onClose: close }),
6580
6947
  dialog?.type === "duplicate" && /* @__PURE__ */ jsx(NewPageDialog, {
@@ -9151,6 +9518,16 @@ function validate(draft) {
9151
9518
  };
9152
9519
  return { errors: Object.fromEntries(result.error.issues.map((issue) => [issue.path.join("."), issue.message])) };
9153
9520
  }
9521
+ /**
9522
+ * The loaded settings and redirects as the form would save them, serialized
9523
+ * to compare with. Settings the form can't save as they are stay as loaded.
9524
+ */
9525
+ function loadedForm(content) {
9526
+ return {
9527
+ settings: serializeContent(validate(content.settings).settings ?? content.settings),
9528
+ redirects: serializeContent(storedRedirects(filledRedirects(content.redirects ?? [])))
9529
+ };
9530
+ }
9154
9531
  /** The time zones this browser knows, such as `America/New_York`. */
9155
9532
  function timeZones(current) {
9156
9533
  const known = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : [];
@@ -9752,17 +10129,18 @@ function SettingsScreen({ tab, section }) {
9752
10129
  const headError = useMemo(() => headCodeProblem(t, code.head), [t, code.head]);
9753
10130
  const formsError = useMemo(() => formsProblem(t, settings.forms), [t, settings.forms]);
9754
10131
  const savedRedirects = useMemo(() => storedRedirects(filledRedirects(redirects)), [redirects]);
10132
+ const loaded = useMemo(() => loadedForm(content), [content]);
9755
10133
  const redirectsInvalid = useMemo(() => redirectProblems(savedRedirects).some(Boolean), [savedRedirects]);
9756
10134
  const pages = useSitePages(content);
9757
10135
  const taken = useMemo(() => pages.map((page) => page.path), [pages]);
9758
10136
  const changes = useMemo(() => {
9759
10137
  const result = [];
9760
- if (validation.settings && serializeContent(validation.settings) !== serializeContent(content.settings)) result.push(siteSettingsFileChange(validation.settings));
10138
+ if (validation.settings && serializeContent(validation.settings) !== loaded.settings) result.push(siteSettingsFileChange(validation.settings));
9761
10139
  if (serializeContent(menus) !== serializeContent(content.menus) || serializeContent(languageMenus) !== serializeContent(content.languageMenus ?? {})) result.push(menusFileChange({
9762
10140
  menus,
9763
10141
  languageMenus
9764
10142
  }));
9765
- if (serializeContent(savedRedirects) !== serializeContent(content.redirects ?? []) || redirectMessage.trim() !== (content.redirectMessage ?? "").trim()) result.push(redirectsFileChange({
10143
+ if (serializeContent(savedRedirects) !== loaded.redirects || redirectMessage.trim() !== (content.redirectMessage ?? "").trim()) result.push(redirectsFileChange({
9766
10144
  redirects: savedRedirects,
9767
10145
  message: redirectMessage
9768
10146
  }));
@@ -9771,6 +10149,7 @@ function SettingsScreen({ tab, section }) {
9771
10149
  return result;
9772
10150
  }, [
9773
10151
  validation,
10152
+ loaded,
9774
10153
  menus,
9775
10154
  languageMenus,
9776
10155
  savedRedirects,
@@ -11792,7 +12171,7 @@ function DemoBanner() {
11792
12171
  */
11793
12172
  function DraftBanner() {
11794
12173
  const t = useStrings();
11795
- const { drafts, state } = useAdmin();
12174
+ const { drafts, state, limits } = useAdmin();
11796
12175
  const [busy, setBusy] = useState(false);
11797
12176
  const [confirming, setConfirming] = useState(false);
11798
12177
  const [chosen, setChosen] = useState();
@@ -11850,14 +12229,29 @@ function DraftBanner() {
11850
12229
  error: result.error
11851
12230
  });
11852
12231
  });
11853
- const waiting = names.filter((name) => name !== current?.name);
12232
+ const reviewed = new Map(drafts.reviews.map((review) => [review.name, review]));
12233
+ const others = names.filter((name) => name !== current?.name);
12234
+ const forReview = limits ? [] : others.filter((name) => reviewed.has(name));
12235
+ const waiting = forReview.length > 0 ? forReview : others;
11854
12236
  const selected = chosen && waiting.includes(chosen) ? chosen : waiting[0];
12237
+ const currentReview = current ? reviewed.get(current.name) : void 0;
12238
+ const selectedReview = selected ? reviewed.get(selected) : void 0;
11855
12239
  return /* @__PURE__ */ jsxs("section", {
11856
12240
  className: "gfa-draft-banner",
11857
12241
  "aria-label": t("draft.label"),
11858
12242
  children: [
11859
12243
  current ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
11860
12244
  /* @__PURE__ */ jsx("span", { children: t(kept ? "draft.bannerKept" : "draft.banner", { date: draftStarted(current.name) }) }),
12245
+ currentReview && /* @__PURE__ */ jsxs("span", { children: [
12246
+ t("draft.inReview"),
12247
+ " ",
12248
+ /* @__PURE__ */ jsx("a", {
12249
+ href: currentReview.url,
12250
+ target: "_blank",
12251
+ rel: "noreferrer",
12252
+ children: t("draft.seeReview")
12253
+ })
12254
+ ] }),
11861
12255
  current.paths.length > 0 && /* @__PURE__ */ jsx("span", {
11862
12256
  className: "gfa-draft-count",
11863
12257
  children: current.paths.length === 1 ? t("draft.oneChange") : t("draft.changes", { count: String(current.paths.length) })
@@ -11886,20 +12280,26 @@ function DraftBanner() {
11886
12280
  children: t("draft.leave")
11887
12281
  })
11888
12282
  ] }) : waiting.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [
11889
- /* @__PURE__ */ jsx("span", { children: waiting.length === 1 ? t("draft.waiting", { date: draftStarted(waiting[0] ?? "") }) : t("draft.waitingMany", { count: String(waiting.length) }) }),
12283
+ /* @__PURE__ */ jsx("span", { children: forReview.length > 0 ? waiting.length === 1 ? t("draft.reviewWaiting", { title: selectedReview?.title ?? "" }) : t("draft.reviewWaitingMany", { count: String(waiting.length) }) : waiting.length === 1 ? t("draft.waiting", { date: draftStarted(waiting[0] ?? "") }) : t("draft.waitingMany", { count: String(waiting.length) }) }),
11890
12284
  waiting.length > 1 && /* @__PURE__ */ jsx("select", {
11891
12285
  "aria-label": t("draft.choose"),
11892
12286
  value: selected,
11893
12287
  onChange: (event) => setChosen(event.target.value),
11894
12288
  children: waiting.map((name) => /* @__PURE__ */ jsx("option", {
11895
12289
  value: name,
11896
- children: t("draft.started", { date: draftStarted(name) })
12290
+ children: reviewed.get(name)?.title ?? t("draft.started", { date: draftStarted(name) })
11897
12291
  }, name))
11898
12292
  }),
11899
12293
  /* @__PURE__ */ jsx(Button, {
11900
12294
  disabled: busy,
11901
12295
  onClick: () => void switchTo(selected),
11902
12296
  children: t("draft.open")
12297
+ }),
12298
+ forReview.length > 0 && selectedReview && /* @__PURE__ */ jsx("a", {
12299
+ href: selectedReview.url,
12300
+ target: "_blank",
12301
+ rel: "noreferrer",
12302
+ children: t("draft.seeReview")
11903
12303
  })
11904
12304
  ] }),
11905
12305
  busy && notice === void 0 && current && /* @__PURE__ */ jsx("span", {