@goodfellow-cms/admin 0.9.0 → 0.10.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,8 +1,8 @@
1
- import { A as duplicatePageData, B as pageFileChange, C as checkPageAddress, D as customCssFileChange, E as collectionSettingsChanges, F as menusFileChange, G as storedCss, H as redirectsFileChange, I as moveEntryChanges, J as syncedBlockFileChange, K as storedData, L as movePageChanges, M as itemLinkRemovalChanges, N as layoutFileChange, O as deleteCollectionChanges, P as mediaFileChange, R as newCollectionSettings, S as checkEntrySlug, T as collectionFileChange$1, U as siteSettingsFileChange, V as pageTitle$1, W as slugify, X as updateSettingsFileChange, Y as uniqueName, _ as mediaUrl, b as uploadName, f as fileName$1, j as entryFileChange$1, k as deleteSyncedBlockChanges, r as createMediaPreviews, t as installCustomFonts, u as allMedia, w as codeFileChanges, x as addressPatternFor, y as siteImages, z as newPageData } from "./custom-font-faces-DJ2AF2oN.js";
2
- import { $ as hasUnsavedChanges, A as useOutsideHost, B as useFrameServices, E as MediaScreen, F as mayChange, I as useMayChange, J as shortcutFor, K as isTyping, L as FrameServicesProvider, M as usePluginFrame, N as usePluginHost, O as AppLink, P as PublishFailure, Q as entryEditorHref, R as reloadFrames, S as REGISTRY_VERSION, T as MediaField, U as findPlaces, V as useFrameUpdates, W as goToPlaces, X as SETTINGS_SECTIONS, Y as usesCommandKey, Z as collectionHref, _ as useThemeChoice$1, _t as useDebounced, a as applyEditorChange, at as useUnsavedChanges, b as ScheduleStatus, ct as draftStarted, d as useCanChangeSettings, dt as ErrorMessage, et as navigate, f as useSiteContent, ft as Field, g as useDarkMode, gt as docsLink, h as useApplyThemeChoice, ht as TextField, it as useRoute, j as PluginScreenFrame, k as prepareUpload, l as AdminProvider, lt as Button, mt as PublishedNotice, n as formValues, nt as sectionId, o as findBlock, ot as EditorTextContext, p as setDarkMode, pt as InfoBubble, q as onShortcut, r as PreviewFrame, rt as syncedBlockEditorHref, t as SCHEDULE_PROP, tt as pageEditorHref, u as useAdmin, ut as Dialog, v as ComingUp, vt as describeContrast, x as scheduleSettingsChanges, y as ScheduleFields, z as useFrame } from "./entry-values-FcVgwQkJ.js";
3
- import { i as useStrings, n as defaultStrings, r as useStringTable, t as StringsProvider } from "./strings-D94noZrv.js";
4
- import { a as languageStatus, c as sourcesToTranslate, d as PUCK_ENGLISH, i as labelSources, l as withEdits, n as entryOf, o as sourceKey, r as filterSources, s as sourceState, t as adminTextSources, u as withTranslations } from "./admin-text-sources-BTZJPlVK.js";
5
- import { ADMIN_TEXT_INDEX, APP_API_HEADER, BROKER_AI_ADDRESS, BROKER_CONNECT_MESSAGE, BROKER_READY_MESSAGE, CODE_PURPOSES, CODE_THEMES, COLLECTIONS_DIR, CONFIG_FILE, CONSENT_DAYS, CONSENT_KINDS, CONTENT_DIR, CommentsError, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DISQUS_CREATE_URL, DomainError, EditorsError, FIELD_TYPES, FONT_CATEGORIES, FOOTER_FILE, GARRUL_URL, GISCUS_APP_URL, GOODFELLOW_VERSION, GROUP_FIELD_TYPES, HEADER_FILE, HIDE_REASONS, HeadCodeError, INSTALLED_PLUGINS_FILE, INSTALLED_RECORD_FILE, MAX_CONSENT_DAYS, MAX_CUSTOM_FONTS, MAX_FONT_BYTES, MAX_FONT_FILES, MAX_GROUP_FIELDS, MEDIA_DIR, NIGHTLY_MODES, NPM_REGISTRY, PUBLISHED_ADMIN_TEXT_DIR, PackageError, PluginInstallError, PluginTaskError, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RELEASE_AGE_DAYS, RegistryError, STATS_SERVICES, SYNCED_BLOCK, SYNCED_BLOCKS_DIR, SYNCED_BLOCK_PROP, SetupError, SignInError, THEME_COLORS, THEME_FILE, THEME_RELEASE_AGE_DAYS, TITLE_FIELD, ThemeError, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, addressPatternProblem, adminOrigin, adminTextFile, adminTextFileChange, adminTextOverlay, adminTextProblem, adminTextTable, allPages, availableBlocks, availableUpdates, blockUses, brokerAddressProblem, brokerOrigin, brokerReleaseUrl, checkAdminAddress, checkEditedData, checkPuckData, checkRecords, chooseLanguagePackRelease, chooseRelease, chooseThemeRelease, collectionFileSchema, collectionsLinkingTo, compareReleases, configAdmin, configBroker, consentDefaults, consentInLanguage, consentKinds, consentSchema, contentInLanguage, contrastProblems, credentialStorage, customFont, customFontFileChanges, decodeBase64, demoContentStore, demoStore, describeSiteChanges, describedConfig, disqusModerationUrl, disqusShortname, domainZone, encodeBase64Bytes, endsHeldTemplate, entryAddress, entryLanguage, entryPathIn, entrySourceHash, entryTitle, entryTranslations, eventValueProblem, expandSyncedBlocks, fetchBrokerRelease, fetchLanguagePack, fetchPluginPackage, fetchTheme, findEntry, findSyncedBlock, fontFileName, formTarget, garrulAddress, garrulModerationUrl, getPageHead, googleFontsUrl, guessFontFamily, guessFontFile, hasLanguages, inRange, isAddressSegment, isDisqus, isEmptyValue, isGarrul, isLanguageCode, isLanguageHome, isReservedPagePath, isWoff2, itemsLinkingTo, languageLayoutFile, languageName, languagePackReleases, languagePrefix, loadSiteContent, mainLanguage, mayInstallEarly, mediaDetailsOf, menusFileSchema, normalizeDomain, pageLanguage, pageLinks, pageSourceHash, pageTitle, pageTranslations, parseAdminTextFile, parseAdminTextIndex, parseDemoContent, parseHeadCode, parseInstalledPlugins, parseInstalledRecord, parseThemeRecord, parseUpdateSettings, pathLanguage, permissionChanges, permissionsFileChange, planBlockChanges, planBroker, planLanguagePack, planPluginInstall, planPluginRemove, planTheme, pluginReleases, pluginsOutOfRange, readAdminText, readProvenanceSource, readStatsCode, recordsInPlace, redirectFrom, redirectProblems, registrarName, releaseVersions, remoteBackend, reviewPluginPackages, scheduleOf, scheduleProblem, searchLanguagePacks, searchPlugins, searchThemes, serializeContent, siteChangesSince, siteHistory, siteLanguages, siteRestoreChanges, siteSettingsSchema, siteVersion, statsDashboard, statsElements, storedRedirects, syncedBlockUses, themeFileUrl, themeLabel, themeLoads, themeReleases, themeToCss, themeUnknownBlocks, todayIn, translationState, trimChars, withAdminConfig, withBrokerConfig, withSchedule, writesEarlyLockfile, writesLockfile } from "@goodfellow-cms/core";
1
+ import { A as duplicatePageData, B as pageFileChange, C as checkPageAddress, D as customCssFileChange, E as collectionSettingsChanges, F as menusFileChange, G as storedCss, H as redirectsFileChange, I as moveEntryChanges, J as syncedBlockFileChange, K as storedData, L as movePageChanges, M as itemLinkRemovalChanges, N as layoutFileChange, O as deleteCollectionChanges, P as mediaFileChange, R as newCollectionSettings, S as checkEntrySlug, T as collectionFileChange$1, U as siteSettingsFileChange, V as pageTitle$1, W as slugify, X as updateSettingsFileChange, Y as uniqueName, _ as mediaUrl, b as uploadName, f as fileName$1, j as entryFileChange$1, k as deleteSyncedBlockChanges, r as createMediaPreviews, t as installCustomFonts, u as allMedia, w as codeFileChanges, x as addressPatternFor, y as siteImages, z as newPageData } from "./custom-font-faces-DCan2jgW.js";
2
+ import { $ as hasUnsavedChanges, A as useOutsideHost, B as useFrameServices, E as MediaScreen, F as mayChange, I as useMayChange, J as shortcutFor, K as isTyping, L as FrameServicesProvider, M as usePluginFrame, N as usePluginHost, O as AppLink, P as PublishFailure, Q as entryEditorHref, R as reloadFrames, S as REGISTRY_VERSION, T as MediaField, U as findPlaces, V as useFrameUpdates, W as goToPlaces, X as SETTINGS_SECTIONS, Y as usesCommandKey, Z as collectionHref, _ as useThemeChoice$1, _t as useDebounced, a as applyEditorChange, at as useUnsavedChanges, b as ScheduleStatus, ct as draftStarted, d as useCanChangeSettings, dt as ErrorMessage, et as navigate, f as useSiteContent, ft as Field, g as useDarkMode, gt as docsLink, h as useApplyThemeChoice, ht as TextField, it as useRoute, j as PluginScreenFrame, k as prepareUpload, l as AdminProvider, lt as Button, mt as PublishedNotice, n as formValues, nt as sectionId, o as findBlock, ot as EditorTextContext, p as setDarkMode, pt as InfoBubble, q as onShortcut, r as PreviewFrame, rt as syncedBlockEditorHref, t as SCHEDULE_PROP, tt as pageEditorHref, u as useAdmin, ut as Dialog, v as ComingUp, vt as describeContrast, x as scheduleSettingsChanges, y as ScheduleFields, z as useFrame } from "./entry-values-CAPB0igb.js";
3
+ import { i as useStrings, n as defaultStrings, r as useStringTable, t as StringsProvider } from "./strings-CgC6pd6k.js";
4
+ import { a as languageStatus, c as sourcesToTranslate, d as PUCK_ENGLISH, i as labelSources, l as withEdits, n as entryOf, o as sourceKey, r as filterSources, s as sourceState, t as adminTextSources, u as withTranslations } from "./admin-text-sources-B1CoPEu6.js";
5
+ import { ADMIN_TEXT_INDEX, APP_API_HEADER, BROKER_AI_ADDRESS, BROKER_CONNECT_MESSAGE, BROKER_READY_MESSAGE, CODE_PURPOSES, CODE_THEMES, COLLECTIONS_DIR, CONFIG_FILE, CONSENT_DAYS, CONSENT_KINDS, CONTENT_DIR, CommentsError, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DISQUS_CREATE_URL, DomainError, EditorsError, FIELD_TYPES, FONT_CATEGORIES, FOOTER_FILE, GARRUL_URL, GISCUS_APP_URL, GOODFELLOW_REGISTRY, GOODFELLOW_VERSION, GROUP_FIELD_TYPES, HEADER_FILE, HIDE_REASONS, HeadCodeError, INSTALLED_PLUGINS_FILE, INSTALLED_RECORD_FILE, MAX_CONSENT_DAYS, MAX_CUSTOM_FONTS, MAX_FONT_BYTES, MAX_FONT_FILES, MAX_GROUP_FIELDS, MAX_UPLOAD_BYTES, MEDIA_DIR, NIGHTLY_MODES, NPM_REGISTRY, PUBLISHED_ADMIN_TEXT_DIR, PackageError, PluginInstallError, PluginTaskError, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RELEASE_AGE_DAYS, RegistryError, STATS_SERVICES, SYNCED_BLOCK, SYNCED_BLOCKS_DIR, SYNCED_BLOCK_PROP, SetupError, SignInError, SiteImportError, THEME_COLORS, THEME_FILE, THEME_RELEASE_AGE_DAYS, TITLE_FIELD, TRANSFER_FOLDERS, TRANSFER_RECORDS, ThemeError, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, addressPatternProblem, adminOrigin, adminTextFile, adminTextFileChange, adminTextOverlay, adminTextProblem, adminTextTable, allPages, availableBlocks, availableUpdates, blockUses, brokerAddressProblem, brokerOrigin, brokerReleaseUrl, checkAdminAddress, checkEditedData, checkPuckData, checkRecords, chooseLanguagePackRelease, chooseRelease, chooseThemeRelease, collectionFileSchema, collectionsLinkingTo, compareReleases, configAdmin, configBackendHost, configBackendOption, configBroker, consentDefaults, consentInLanguage, consentKinds, consentSchema, contentInLanguage, contrastProblems, credentialStorage, customFont, customFontFileChanges, decodeBase64, demoContentStore, demoStore, describeSiteChanges, describedConfig, disqusModerationUrl, disqusShortname, domainZone, encodeBase64Bytes, endsHeldTemplate, entryAddress, entryLanguage, entryPathIn, entrySourceHash, entryTitle, entryTranslations, eventValueProblem, expandSyncedBlocks, fetchBrokerRelease, fetchLanguagePack, fetchPluginPackage, fetchTheme, findEntry, findSyncedBlock, fontFileName, formTarget, garrulAddress, garrulModerationUrl, getPageHead, googleFontsUrl, guessFontFamily, guessFontFile, hasLanguages, inRange, isAddressSegment, isDisqus, isEmptyValue, isGarrul, isLanguageCode, isLanguageHome, isReservedPagePath, isWoff2, itemsLinkingTo, languageLayoutFile, languageName, languagePackReleases, languagePrefix, loadSiteContent, mainLanguage, mayInstallEarly, mediaDetailsOf, mediaKind, menusFileSchema, normalizeDomain, pageLanguage, pageLinks, pageSourceHash, pageTitle, pageTranslations, parseAdminTextFile, parseAdminTextIndex, parseDemoContent, parseHeadCode, parseInstalledPlugins, parseInstalledRecord, parseThemeRecord, parseUpdateSettings, pathLanguage, permissionChanges, permissionsFileChange, planBlockChanges, planBroker, planLanguagePack, planPluginInstall, planPluginRemove, planSiteImport, planTheme, pluginReleases, pluginsOutOfRange, readAdminText, readProvenanceSource, readStatsCode, recordsInPlace, redirectFrom, redirectProblems, registrarName, releaseVersions, remoteBackend, reviewPluginPackages, scheduleOf, scheduleProblem, searchLanguagePacks, searchPlugins, searchThemes, serializeContent, signInClientIdProblem, signInRedirectAddresses, signInServiceProblem, siteChangesSince, siteHistory, siteLanguages, siteRestoreChanges, siteSettingsSchema, siteVersion, statsDashboard, statsElements, storedRedirects, syncedBlockUses, themeFileUrl, themeLabel, themeLoads, themeReleases, themeToCss, themeUnknownBlocks, todayIn, transferManifestFile, transferName, transferPaths, translationState, trimChars, withAdminConfig, withBackendOption, withBrokerConfig, withSchedule, writesEarlyLockfile, writesLockfile } from "@goodfellow-cms/core";
6
6
  import { Braces, ChartColumn, CodeXml, FileInput, Globe, History, Languages, Menu, MessageSquare, MessageSquareText, Moon, Palette, PanelLeftClose, PanelLeftOpen, Plug, Puzzle, RefreshCw, Settings, ShieldCheck, Signpost, Sun, SwatchBook, Users, X } from "lucide-react";
7
7
  import { Fragment, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
8
  import { createRoot } from "react-dom/client";
@@ -24,7 +24,7 @@ const ENGLISH = {
24
24
  labels: {}
25
25
  };
26
26
  /** Each language Goodfellow ships but English, loaded only when someone chooses it, so the admin panel stays small for everyone else. */
27
- const LOADERS = { es: () => import("./es-ChxY0KVH.js").then((module) => module.spanish) };
27
+ const LOADERS = { es: () => import("./es-Z-QY8sdc.js").then((module) => module.spanish) };
28
28
  const builtIn = (code) => ADMIN_LANGUAGES.some((language) => language.code === code);
29
29
  const listeners = /* @__PURE__ */ new Set();
30
30
  let siteText = {
@@ -3805,7 +3805,7 @@ function problemKey(error) {
3805
3805
  if (error instanceof SyntaxError) return "ai.error.answer";
3806
3806
  return "ai.error.service";
3807
3807
  }
3808
- function ServiceSettings({ providers, settings, onChange, siteAi }) {
3808
+ function ServiceSettings$1({ providers, settings, onChange, siteAi }) {
3809
3809
  const t = useStrings();
3810
3810
  const info = PROVIDERS[settings.provider];
3811
3811
  const sitePays = siteAi?.services.includes(settings.provider) ?? false;
@@ -4339,7 +4339,7 @@ function AiPanel({ context, editor }) {
4339
4339
  className: "gfa-ai-service",
4340
4340
  open: settingsOpen,
4341
4341
  onToggle: (event) => setSettingsOpen(event.currentTarget.open),
4342
- children: [/* @__PURE__ */ jsx("summary", { children: t("ai.settings", { service }) }), /* @__PURE__ */ jsx(ServiceSettings, {
4342
+ children: [/* @__PURE__ */ jsx("summary", { children: t("ai.settings", { service }) }), /* @__PURE__ */ jsx(ServiceSettings$1, {
4343
4343
  providers,
4344
4344
  settings: chosen,
4345
4345
  onChange: updateSettings,
@@ -4794,7 +4794,7 @@ function useDefaults(code) {
4794
4794
  useEffect(() => {
4795
4795
  if (code !== "es" || spanish) return;
4796
4796
  let live = true;
4797
- import("./es-ChxY0KVH.js").then((module) => live && setSpanish(module.spanish));
4797
+ import("./es-Z-QY8sdc.js").then((module) => live && setSpanish(module.spanish));
4798
4798
  return () => {
4799
4799
  live = false;
4800
4800
  };
@@ -5081,7 +5081,7 @@ function AdminLanguagesScreen() {
5081
5081
  }),
5082
5082
  !ai.ready && /* @__PURE__ */ jsx("p", { children: t("adminText.noService") }),
5083
5083
  ai.settings.provider === "manual" && /* @__PURE__ */ jsx("p", { children: t("adminText.manual") }),
5084
- /* @__PURE__ */ jsx(ServiceSettings, {
5084
+ /* @__PURE__ */ jsx(ServiceSettings$1, {
5085
5085
  providers: ai.providers,
5086
5086
  settings: ai.chosen,
5087
5087
  onChange: ai.updateSettings,
@@ -5420,13 +5420,13 @@ function AppSignInGate({ api, children }) {
5420
5420
  }
5421
5421
  //#endregion
5422
5422
  //#region src/blocks-screen.tsx
5423
- const fetchJson = async (url) => {
5423
+ const fetchJson$1 = async (url) => {
5424
5424
  const response = await fetch(url);
5425
5425
  if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
5426
5426
  return response.json();
5427
5427
  };
5428
5428
  /** What went wrong with a block registry, in plain words. */
5429
- function problemText$1(t, problem, name) {
5429
+ function problemText$2(t, problem, name) {
5430
5430
  const key = `blocks.error.${problem.code}`;
5431
5431
  if (problem.code === "packages") return t(key, { packages: problem.packages.join(", ") });
5432
5432
  if (problem.code === "file-exists" || problem.code === "unsupported-file") return t(key, { path: problem.path });
@@ -5546,7 +5546,7 @@ function BlocksScreen() {
5546
5546
  }, [readFile, revision]);
5547
5547
  const loadAvailable = useCallback(() => {
5548
5548
  setLoadError(void 0);
5549
- availableBlocks(registries, fetchJson).then(setAvailable, setLoadError);
5549
+ availableBlocks(registries, fetchJson$1).then(setAvailable, setLoadError);
5550
5550
  }, [registries]);
5551
5551
  useEffect(loadAvailable, [loadAvailable]);
5552
5552
  const isPendingAdd = (block) => pending.some((change) => change.kind === "add" && change.block.ref === block.ref);
@@ -5583,7 +5583,7 @@ function BlocksScreen() {
5583
5583
  const plan = await planBlockChanges({
5584
5584
  changes: pending.map((item) => item.kind === "add" ? { add: item.block.ref } : { remove: item.name }),
5585
5585
  registries,
5586
- fetchJson,
5586
+ fetchJson: fetchJson$1,
5587
5587
  readFile,
5588
5588
  record,
5589
5589
  blocks: Object.keys(blocks),
@@ -5616,7 +5616,7 @@ function BlocksScreen() {
5616
5616
  } catch (error) {
5617
5617
  setNotice({
5618
5618
  type: "failed",
5619
- message: error instanceof RegistryError ? problemText$1(t, error.problem, failedTitle(error)) : t("publish.error"),
5619
+ message: error instanceof RegistryError ? problemText$2(t, error.problem, failedTitle(error)) : t("publish.error"),
5620
5620
  error
5621
5621
  });
5622
5622
  }
@@ -5735,7 +5735,7 @@ function BlocksScreen() {
5735
5735
  error: recordError
5736
5736
  }),
5737
5737
  loadError !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
5738
- message: loadError instanceof RegistryError ? problemText$1(t, loadError.problem, "") : t("blocks.error.unreachable"),
5738
+ message: loadError instanceof RegistryError ? problemText$2(t, loadError.problem, "") : t("blocks.error.unreachable"),
5739
5739
  error: loadError,
5740
5740
  action: /* @__PURE__ */ jsx(Button, {
5741
5741
  onClick: loadAvailable,
@@ -9014,12 +9014,19 @@ function ConnectedDomain({ domain, onRemoved, admin = false }) {
9014
9014
  className: "gfa-form",
9015
9015
  children: [
9016
9016
  /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("strong", { children: t(admin ? "domain.admin.connected" : "domain.connected", { domain }) }) }),
9017
- account?.redirectAddressesListed && /* @__PURE__ */ jsx("p", {
9017
+ account?.redirectAddressesListed && /* @__PURE__ */ jsxs("p", {
9018
9018
  className: "gfa-notice",
9019
- children: t("domain.signIn", {
9020
- host,
9021
- address: `${address}/admin/`
9022
- })
9019
+ children: [
9020
+ t("domain.signIn", {
9021
+ host,
9022
+ address: `${address}/admin/`
9023
+ }),
9024
+ " ",
9025
+ /* @__PURE__ */ jsx(AppLink, {
9026
+ href: "#/settings/security",
9027
+ children: t("domain.signInSettings")
9028
+ })
9029
+ ]
9023
9030
  }),
9024
9031
  done && /* @__PURE__ */ jsx("p", {
9025
9032
  className: "gfa-notice gfa-notice-success",
@@ -9558,7 +9565,7 @@ const ROLE_LABELS = {
9558
9565
  viewer: "editors.role.viewer"
9559
9566
  };
9560
9567
  /** Plain words for why a change to the editors didn't work. */
9561
- function problemText(error) {
9568
+ function problemText$1(error) {
9562
9569
  return error instanceof EditorsError ? `editors.error.${error.problem}` : "editors.error.other";
9563
9570
  }
9564
9571
  /** Who edits the site: inviting people, changing what they can do, and removing them. */
@@ -9648,7 +9655,7 @@ function EditorsPanel() {
9648
9655
  })]
9649
9656
  }),
9650
9657
  problem !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
9651
- message: t(problemText(problem), { host }),
9658
+ message: t(problemText$1(problem), { host }),
9652
9659
  error: problem
9653
9660
  }),
9654
9661
  /* @__PURE__ */ jsxs("table", {
@@ -9841,6 +9848,271 @@ function EntryEditorScreen({ collectionId, slug }) {
9841
9848
  });
9842
9849
  }
9843
9850
  //#endregion
9851
+ //#region src/site-download.ts
9852
+ /**
9853
+ * A copy of the site to keep: its content, its media library and the records
9854
+ * of the blocks and plugins it installed, as one zip file, read at the
9855
+ * revision the admin panel loaded. fflate is loaded only when a copy is made.
9856
+ * No React or DOM, so it's tested in Node.
9857
+ */
9858
+ /** The folders a copy holds whole. */
9859
+ const DOWNLOAD_FOLDERS = TRANSFER_FOLDERS;
9860
+ /** Single files a copy holds where the site has them: which blocks and plugins it installed, to add them again. */
9861
+ const DOWNLOAD_RECORDS = TRANSFER_RECORDS;
9862
+ /** How many files are read at once. */
9863
+ const READ_AT_ONCE = 6;
9864
+ /** Files that are already compressed are stored as they are, which is as small and much quicker. */
9865
+ const COMPRESSED = /\.(jpe?g|png|webp|gif|avif|mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|pdf|woff2?|zip|gz)$/i;
9866
+ /** Every file a copy holds, sorted, from the folders and records the site has. */
9867
+ async function downloadPaths(list) {
9868
+ const folders = await Promise.all(DOWNLOAD_FOLDERS.map((dir) => list(dir)));
9869
+ const records = await Promise.all(DOWNLOAD_RECORDS.map(async (path) => {
9870
+ return (await list(path.slice(0, path.lastIndexOf("/")))).includes(path) ? [path] : [];
9871
+ }));
9872
+ return [.../* @__PURE__ */ new Set([...folders.flat(), ...records.flat()])].sort();
9873
+ }
9874
+ /**
9875
+ * The copy's file name: the site's title in plain letters, then the day,
9876
+ * `st-anne-parish-2026-11-01.zip`, or `site-…` for a title with none.
9877
+ */
9878
+ const downloadName = transferName;
9879
+ /**
9880
+ * Zips the files, read with `read`, in the order given, a few at a time, and
9881
+ * returns the zip in pieces, for a `Blob`. A file that's gone since it was
9882
+ * listed is left out. Throws what reading throws.
9883
+ */
9884
+ async function siteArchive(paths, read, onProgress) {
9885
+ const { Zip, ZipDeflate, ZipPassThrough } = await import("fflate");
9886
+ const chunks = [];
9887
+ let finish = () => {};
9888
+ let fail = () => {};
9889
+ const finished = new Promise((resolve, reject) => {
9890
+ finish = resolve;
9891
+ fail = reject;
9892
+ });
9893
+ const zip = new Zip((error, chunk, final) => {
9894
+ if (error) return fail(error);
9895
+ chunks.push(chunk);
9896
+ if (final) finish();
9897
+ });
9898
+ let done = 0;
9899
+ onProgress?.({
9900
+ done,
9901
+ total: paths.length
9902
+ });
9903
+ try {
9904
+ for (let start = 0; start < paths.length; start += READ_AT_ONCE) {
9905
+ const batch = paths.slice(start, start + READ_AT_ONCE);
9906
+ const files = await Promise.all(batch.map((path) => read(path)));
9907
+ batch.forEach((path, index) => {
9908
+ const bytes = files[index];
9909
+ if (bytes) {
9910
+ const entry = COMPRESSED.test(path) ? new ZipPassThrough(path) : new ZipDeflate(path, { level: 6 });
9911
+ zip.add(entry);
9912
+ entry.push(bytes, true);
9913
+ }
9914
+ done += 1;
9915
+ });
9916
+ onProgress?.({
9917
+ done,
9918
+ total: paths.length
9919
+ });
9920
+ }
9921
+ } catch (error) {
9922
+ zip.terminate();
9923
+ throw error;
9924
+ }
9925
+ zip.end();
9926
+ await finished;
9927
+ return chunks;
9928
+ }
9929
+ /**
9930
+ * A copy of the site as one zip, with the manifest that lets another
9931
+ * Goodfellow site import it (`TRANSFER_MANIFEST`), in pieces for a `Blob`.
9932
+ */
9933
+ async function siteCopyArchive(list, readBytes, content, onProgress) {
9934
+ const paths = await downloadPaths(list);
9935
+ const manifest = transferManifestFile(content);
9936
+ const bytes = new TextEncoder().encode(manifest.content);
9937
+ return siteArchive([...paths, manifest.path], async (path) => path === manifest.path ? bytes : readBytes(path), onProgress);
9938
+ }
9939
+ //#endregion
9940
+ //#region src/markdown-export.tsx
9941
+ /** What a Markdown export couldn't bring across as it was, in words. */
9942
+ function noteText$3(t, note, blockName) {
9943
+ const listed = (items) => {
9944
+ const shown = items.slice(0, 3).join(", ");
9945
+ return items.length > 3 ? t("import.note.more", {
9946
+ items: shown,
9947
+ count: String(items.length - 3)
9948
+ }) : shown;
9949
+ };
9950
+ switch (note.kind) {
9951
+ case "left-out-block": return t("markdownExport.note.leftOutBlock", {
9952
+ name: blockName(note.name),
9953
+ items: listed(note.items)
9954
+ });
9955
+ case "scheduled": return t(note.included ? "markdownExport.note.scheduledIncluded" : "markdownExport.note.scheduledLeftOut", { items: listed(note.items) });
9956
+ case "no-pages": return t("markdownExport.note.noPages", { collections: listed(note.collections) });
9957
+ case "templates": return t("markdownExport.note.templates", { collections: listed(note.collections) });
9958
+ case "redirects": return t("markdownExport.note.redirects", { count: String(note.count) });
9959
+ case "layout": return t("markdownExport.note.layout");
9960
+ }
9961
+ }
9962
+ /**
9963
+ * Writes the site as a folder of Markdown files (`markdownExport()`), zipped
9964
+ * in the browser with the media library, for other static sites, notes apps
9965
+ * and AI tools, on Site Settings' Import tab beside the WordPress export.
9966
+ */
9967
+ function MarkdownExportSection() {
9968
+ const t = useStrings();
9969
+ const { blocks, readBytes } = useAdmin();
9970
+ const { content, media } = useSiteContent();
9971
+ const [scheduled, setScheduled] = useState(false);
9972
+ const [links, setLinks] = useState("site");
9973
+ const [result, setResult] = useState();
9974
+ const [progress, setProgress] = useState();
9975
+ const [problem, setProblem] = useState();
9976
+ const busy = progress !== void 0;
9977
+ const blockName = (name) => blocks[name]?.label ?? name;
9978
+ const stored = Object.keys(content.mediaStorage?.files ?? {}).length > 0;
9979
+ const exportSite = async () => {
9980
+ setProblem(void 0);
9981
+ setResult(void 0);
9982
+ setProgress({
9983
+ done: 0,
9984
+ total: 0
9985
+ });
9986
+ try {
9987
+ const { markdownExport, markdownExportName } = await import("@goodfellow-cms/core/wordpress");
9988
+ const written = markdownExport(content, {
9989
+ mediaFiles: media,
9990
+ scheduled,
9991
+ links
9992
+ });
9993
+ const folder = markdownExportName(content);
9994
+ const texts = new Map(written.files.map((file) => [`${folder}/${file.path}`, file.content]));
9995
+ const copies = new Map(written.media.map((file) => [`${folder}/${file.path}`, file.from]));
9996
+ const encoder = new TextEncoder();
9997
+ const chunks = await siteArchive([...texts.keys(), ...copies.keys()], async (path) => {
9998
+ const text = texts.get(path);
9999
+ if (text !== void 0) return encoder.encode(text);
10000
+ const from = copies.get(path);
10001
+ return from ? readBytes(from) : void 0;
10002
+ }, setProgress);
10003
+ const url = URL.createObjectURL(new Blob(chunks, { type: "application/zip" }));
10004
+ const link = document.createElement("a");
10005
+ link.href = url;
10006
+ link.download = `${folder}.zip`;
10007
+ document.body.append(link);
10008
+ link.click();
10009
+ link.remove();
10010
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
10011
+ setResult(written);
10012
+ } catch (error) {
10013
+ setProblem(error ?? /* @__PURE__ */ new Error("Export failed"));
10014
+ } finally {
10015
+ setProgress(void 0);
10016
+ }
10017
+ };
10018
+ return /* @__PURE__ */ jsxs("section", {
10019
+ "aria-labelledby": "gfa-markdown-export-title",
10020
+ children: [
10021
+ /* @__PURE__ */ jsx("h2", {
10022
+ id: "gfa-markdown-export-title",
10023
+ className: "gfa-section-title",
10024
+ children: t("markdownExport.title")
10025
+ }),
10026
+ /* @__PURE__ */ jsx("p", { children: t("markdownExport.intro") }),
10027
+ /* @__PURE__ */ jsx("p", {
10028
+ className: "gfa-hint",
10029
+ children: t("markdownExport.limits")
10030
+ }),
10031
+ stored && /* @__PURE__ */ jsx("p", {
10032
+ className: "gfa-hint",
10033
+ children: t("markdownExport.stored")
10034
+ }),
10035
+ /* @__PURE__ */ jsxs("fieldset", {
10036
+ className: "gfa-fieldset",
10037
+ children: [
10038
+ /* @__PURE__ */ jsx("legend", { children: t("markdownExport.links") }),
10039
+ /* @__PURE__ */ jsxs("label", {
10040
+ className: "gfa-checkbox",
10041
+ children: [/* @__PURE__ */ jsx("input", {
10042
+ type: "radio",
10043
+ name: "gfa-markdown-links",
10044
+ checked: links === "site",
10045
+ disabled: busy,
10046
+ onChange: () => setLinks("site")
10047
+ }), t("markdownExport.links.site")]
10048
+ }),
10049
+ /* @__PURE__ */ jsxs("label", {
10050
+ className: "gfa-checkbox",
10051
+ children: [/* @__PURE__ */ jsx("input", {
10052
+ type: "radio",
10053
+ name: "gfa-markdown-links",
10054
+ checked: links === "files",
10055
+ disabled: busy,
10056
+ onChange: () => setLinks("files")
10057
+ }), t("markdownExport.links.files")]
10058
+ })
10059
+ ]
10060
+ }),
10061
+ /* @__PURE__ */ jsxs("label", {
10062
+ className: "gfa-checkbox",
10063
+ children: [/* @__PURE__ */ jsx("input", {
10064
+ type: "checkbox",
10065
+ checked: scheduled,
10066
+ disabled: busy,
10067
+ onChange: (event) => setScheduled(event.target.checked)
10068
+ }), t("markdownExport.scheduled")]
10069
+ }),
10070
+ problem !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
10071
+ message: t("markdownExport.error"),
10072
+ error: problem
10073
+ }),
10074
+ /* @__PURE__ */ jsx("p", {
10075
+ className: "gfa-import-actions",
10076
+ children: /* @__PURE__ */ jsx(Button, {
10077
+ onClick: () => void exportSite(),
10078
+ disabled: busy,
10079
+ children: t("markdownExport.download")
10080
+ })
10081
+ }),
10082
+ progress && progress.total > 0 && /* @__PURE__ */ jsx("p", {
10083
+ role: "status",
10084
+ children: t("markdownExport.working", {
10085
+ done: String(progress.done),
10086
+ total: String(progress.total)
10087
+ })
10088
+ }),
10089
+ result && /* @__PURE__ */ jsxs(Fragment$1, { children: [
10090
+ /* @__PURE__ */ jsx("p", {
10091
+ role: "status",
10092
+ className: "gfa-notice gfa-notice-success",
10093
+ children: t("markdownExport.done")
10094
+ }),
10095
+ /* @__PURE__ */ jsxs("ul", {
10096
+ className: "gfa-import-summary",
10097
+ children: [
10098
+ /* @__PURE__ */ jsx("li", { children: t("export.summary.pages", { count: String(result.summary.pages) }) }),
10099
+ /* @__PURE__ */ jsx("li", { children: t("markdownExport.summary.items", { count: String(result.summary.items) }) }),
10100
+ /* @__PURE__ */ jsx("li", { children: t("import.summary.menus", { count: String(result.summary.menus) }) }),
10101
+ /* @__PURE__ */ jsx("li", { children: t("markdownExport.summary.media", { count: String(result.summary.media) }) })
10102
+ ]
10103
+ }),
10104
+ result.notes.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h3", {
10105
+ className: "gfa-section-title",
10106
+ children: t("import.check")
10107
+ }), /* @__PURE__ */ jsx("ul", {
10108
+ className: "gfa-import-notes",
10109
+ children: result.notes.map((note) => /* @__PURE__ */ jsx("li", { children: noteText$3(t, note, blockName) }, JSON.stringify(note)))
10110
+ })] })
10111
+ ] })
10112
+ ]
10113
+ });
10114
+ }
10115
+ //#endregion
9844
10116
  //#region src/markdown-import.tsx
9845
10117
  const NOTES = {
9846
10118
  drafts: "markdownImport.note.drafts",
@@ -9852,7 +10124,7 @@ const NOTES = {
9852
10124
  "template-code": "markdownImport.note.templateCode",
9853
10125
  html: "markdownImport.note.html"
9854
10126
  };
9855
- function listed(t, items) {
10127
+ function listed$1(t, items) {
9856
10128
  const shown = items.slice(0, 3).join(", ");
9857
10129
  return items.length > 3 ? t("import.note.more", {
9858
10130
  items: shown,
@@ -9860,12 +10132,12 @@ function listed(t, items) {
9860
10132
  }) : shown;
9861
10133
  }
9862
10134
  /** What the import couldn't bring across as it was, in words. */
9863
- function noteText$1(t, note) {
10135
+ function noteText$2(t, note) {
9864
10136
  switch (note.kind) {
9865
- case "fields": return t("markdownImport.note.fields", { names: listed(t, note.names) });
9866
- case "converted": return t("markdownImport.note.converted", { items: listed(t, note.files) });
10137
+ case "fields": return t("markdownImport.note.fields", { names: listed$1(t, note.names) });
10138
+ case "converted": return t("markdownImport.note.converted", { items: listed$1(t, note.files) });
9867
10139
  default: return t(NOTES[note.kind], {
9868
- items: listed(t, note.files),
10140
+ items: listed$1(t, note.files),
9869
10141
  count: String(note.files.length)
9870
10142
  });
9871
10143
  }
@@ -10206,45 +10478,578 @@ function Notes$1({ plan }) {
10206
10478
  children: t("import.check")
10207
10479
  }), /* @__PURE__ */ jsx("ul", {
10208
10480
  className: "gfa-import-notes",
10209
- children: plan.notes.map((note) => /* @__PURE__ */ jsx("li", { children: noteText$1(t, note) }, JSON.stringify(note)))
10481
+ children: plan.notes.map((note) => /* @__PURE__ */ jsx("li", { children: noteText$2(t, note) }, JSON.stringify(note)))
10210
10482
  })] });
10211
10483
  }
10212
10484
  //#endregion
10213
- //#region src/import-screen.tsx
10214
- /** How many media files are downloaded at once. */
10215
- const DOWNLOADS = 4;
10216
- const CONVERTED = {
10217
- block: "import.note.block",
10218
- dropped: "import.note.dropped",
10219
- shortcode: "import.note.shortcode",
10220
- embed: "import.note.embed",
10221
- form: "import.note.form",
10222
- script: "import.note.script",
10223
- table: "import.note.table",
10224
- posts: "import.note.posts",
10225
- video: "import.note.video"
10226
- };
10227
- const LEFT_OUT = {
10228
- drafts: "import.note.leftOut.drafts",
10229
- private: "import.note.leftOut.private",
10230
- trash: "import.note.leftOut.trash",
10231
- comments: "import.note.leftOut.comments"
10485
+ //#region src/site-transfer.tsx
10486
+ const fetchJson = async (url) => {
10487
+ const response = await fetch(url);
10488
+ if (!response.ok) throw new Error(`${url} answered ${response.status}.`);
10489
+ return response.json();
10232
10490
  };
10233
- /** What the import couldn't bring across as it was, in words. */
10234
- function noteText(t, note) {
10235
- switch (note.kind) {
10236
- case "left-out": return t(LEFT_OUT[note.what] ?? "import.note.leftOut.other", {
10237
- count: String(note.count),
10238
- name: note.what
10491
+ /** Saves zip pieces as a download. */
10492
+ function saveZip(chunks, name) {
10493
+ const url = URL.createObjectURL(new Blob(chunks, { type: "application/zip" }));
10494
+ const link = document.createElement("a");
10495
+ link.href = url;
10496
+ link.download = name;
10497
+ document.body.append(link);
10498
+ link.click();
10499
+ link.remove();
10500
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
10501
+ }
10502
+ /** Downloads the site's content and media as one zip, with its manifest, for another Goodfellow site to import. */
10503
+ function SiteExportSection() {
10504
+ const t = useStrings();
10505
+ const { listFiles, readBytes } = useAdmin();
10506
+ const { content } = useSiteContent();
10507
+ const [progress, setProgress] = useState();
10508
+ const [error, setError] = useState();
10509
+ const stored = Object.keys(content.mediaStorage?.files ?? {}).length > 0;
10510
+ const download = async () => {
10511
+ setError(void 0);
10512
+ setProgress({
10513
+ done: 0,
10514
+ total: 0
10239
10515
  });
10240
- case "converted": {
10241
- const shown = note.items.slice(0, 3).join(", ");
10242
- const items = note.items.length > 3 ? t("import.note.more", {
10243
- items: shown,
10244
- count: String(note.items.length - 3)
10245
- }) : shown;
10246
- return t(CONVERTED[note.note] ?? "import.note.block", {
10247
- name: note.name,
10516
+ try {
10517
+ saveZip(await siteCopyArchive(listFiles, readBytes, content, setProgress), downloadName(content.settings.title, /* @__PURE__ */ new Date()));
10518
+ } catch (caught) {
10519
+ setError(caught);
10520
+ } finally {
10521
+ setProgress(void 0);
10522
+ }
10523
+ };
10524
+ return /* @__PURE__ */ jsxs("section", {
10525
+ "aria-labelledby": "gfa-site-export-title",
10526
+ children: [
10527
+ /* @__PURE__ */ jsx("h2", {
10528
+ id: "gfa-site-export-title",
10529
+ className: "gfa-section-title",
10530
+ children: t("siteTransfer.export.title")
10531
+ }),
10532
+ /* @__PURE__ */ jsx("p", { children: t("siteTransfer.export.intro") }),
10533
+ stored && /* @__PURE__ */ jsx("p", {
10534
+ className: "gfa-hint",
10535
+ children: t("siteTransfer.export.stored")
10536
+ }),
10537
+ /* @__PURE__ */ jsx("p", {
10538
+ className: "gfa-import-actions",
10539
+ children: /* @__PURE__ */ jsx(Button, {
10540
+ onClick: () => void download(),
10541
+ disabled: progress !== void 0,
10542
+ children: t("siteTransfer.export.button")
10543
+ })
10544
+ }),
10545
+ progress && progress.total > 0 && /* @__PURE__ */ jsx("p", {
10546
+ role: "status",
10547
+ children: t("siteTransfer.export.working", {
10548
+ done: String(progress.done),
10549
+ total: String(progress.total)
10550
+ })
10551
+ }),
10552
+ error !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
10553
+ message: t("siteTransfer.export.error"),
10554
+ error
10555
+ })
10556
+ ]
10557
+ });
10558
+ }
10559
+ const NO_CHOICES = {
10560
+ replace: false,
10561
+ menus: false,
10562
+ settings: false,
10563
+ layout: false,
10564
+ redirects: false,
10565
+ withoutMissingBlocks: false,
10566
+ take: /* @__PURE__ */ new Set(),
10567
+ install: /* @__PURE__ */ new Set()
10568
+ };
10569
+ const decoder = new TextDecoder();
10570
+ /** The export's files, as `planSiteImport()` reads them. */
10571
+ function transferFiles(files) {
10572
+ return {
10573
+ paths: [...files.keys()],
10574
+ read: (path) => {
10575
+ const bytes = files.get(path);
10576
+ return bytes && decoder.decode(bytes);
10577
+ }
10578
+ };
10579
+ }
10580
+ function listed(t, items) {
10581
+ const shown = items.slice(0, 3).join(", ");
10582
+ return items.length > 3 ? t("import.note.more", {
10583
+ items: shown,
10584
+ count: String(items.length - 3)
10585
+ }) : shown;
10586
+ }
10587
+ function noteText$1(t, note, blockName) {
10588
+ switch (note.kind) {
10589
+ case "missing-block": return t("siteTransfer.note.missingBlock", {
10590
+ name: blockName(note.name),
10591
+ items: listed(t, note.items)
10592
+ });
10593
+ case "left-out": return t("siteTransfer.note.leftOut", { items: listed(t, note.items) });
10594
+ case "without-blocks": return t("siteTransfer.note.withoutBlocks", { items: listed(t, note.items) });
10595
+ case "taken": return t("siteTransfer.note.taken", { items: listed(t, note.items) });
10596
+ case "collection-differs": return t("siteTransfer.note.collectionDiffers", { collections: listed(t, note.collections) });
10597
+ case "uncopied": return t("siteTransfer.note.uncopied", { items: listed(t, note.files) });
10598
+ case "stored-media": return t(note.merged ? "siteTransfer.note.storedMerged" : "siteTransfer.note.stored", { count: String(note.count) });
10599
+ case "languages": return t("siteTransfer.note.languages", { codes: note.codes.join(", ") });
10600
+ case "plugins": return t("siteTransfer.note.plugins", { names: listed(t, note.names) });
10601
+ case "code": return t("siteTransfer.note.code");
10602
+ case "kept": return t("siteTransfer.note.kept", { things: note.things.map((thing) => t(`siteTransfer.kept.${thing}`)).join(", ") });
10603
+ }
10604
+ }
10605
+ function conflictText(t, conflict) {
10606
+ const values = {
10607
+ name: conflict.name,
10608
+ detail: conflict.detail ?? ""
10609
+ };
10610
+ switch (conflict.kind) {
10611
+ case "page": return t("siteTransfer.conflict.page", values);
10612
+ case "item": return t("siteTransfer.conflict.item", values);
10613
+ case "collection": return t("siteTransfer.conflict.collection", values);
10614
+ case "synced-block": return t("siteTransfer.conflict.syncedBlock", values);
10615
+ case "media": return t("siteTransfer.conflict.media", values);
10616
+ }
10617
+ }
10618
+ function problemText(t, error) {
10619
+ if (error instanceof SiteImportError) {
10620
+ if (error.problem === "not-a-site") return t("siteTransfer.error.notASite");
10621
+ if (error.problem === "newer") return t("siteTransfer.error.newer");
10622
+ return t("siteTransfer.error.invalid");
10623
+ }
10624
+ return t("siteTransfer.error.other");
10625
+ }
10626
+ /**
10627
+ * Brings another Goodfellow site's export into this one with
10628
+ * `planSiteImport()`: added to its content, with a choice for each thing both
10629
+ * have differently, or in place of it, then published in one go, with blocks
10630
+ * from Goodfellow's registry it lacks added in the same publish where the
10631
+ * site's code can be changed from here.
10632
+ */
10633
+ function SiteImportSection() {
10634
+ const t = useStrings();
10635
+ const { blocks, publish, mediaPreviews, listFiles, readBytes, readFile, registries } = useAdmin();
10636
+ const { contentOnly, demo, account, site } = useAdmin();
10637
+ const { content } = useSiteContent();
10638
+ const [loaded, setLoaded] = useState();
10639
+ const [choices, setChoices] = useState(NO_CHOICES);
10640
+ const [plan, setPlan] = useState();
10641
+ const [reading, setReading] = useState(false);
10642
+ const [publishing, setPublishing] = useState(false);
10643
+ const [done, setDone] = useState();
10644
+ const [problem, setProblem] = useState();
10645
+ const [failure, setFailure] = useState(null);
10646
+ const mayInstall = !contentOnly && !demo && !account?.codeReview;
10647
+ const blockName = (name) => blocks[name]?.label ?? name;
10648
+ useEffect(() => {
10649
+ if (!loaded) return;
10650
+ let current = true;
10651
+ planSiteImport(transferFiles(loaded.files), {
10652
+ content,
10653
+ files: loaded.siteFiles,
10654
+ read: readFile
10655
+ }, {
10656
+ ...choices,
10657
+ blocks: Object.keys(blocks),
10658
+ installing: [...choices.install],
10659
+ plugins: loaded.plugins,
10660
+ sameMedia: loaded.same,
10661
+ uncopied: loaded.uncopied
10662
+ }).then((next) => current && setPlan(next), (error) => {
10663
+ if (!current) return;
10664
+ setProblem({
10665
+ message: problemText(t, error),
10666
+ error
10667
+ });
10668
+ setLoaded(void 0);
10669
+ setPlan(void 0);
10670
+ });
10671
+ return () => {
10672
+ current = false;
10673
+ };
10674
+ }, [
10675
+ loaded,
10676
+ choices,
10677
+ content,
10678
+ blocks,
10679
+ readFile,
10680
+ t
10681
+ ]);
10682
+ const choose = async (file) => {
10683
+ setProblem(void 0);
10684
+ setFailure(null);
10685
+ setDone(void 0);
10686
+ setPlan(void 0);
10687
+ setReading(true);
10688
+ try {
10689
+ const { unzipSync } = await import("fflate");
10690
+ const archive = unzipSync(new Uint8Array(await file.arrayBuffer()));
10691
+ const paths = transferPaths(Object.keys(archive));
10692
+ if (paths.size === 0) throw new SiteImportError("not-a-site");
10693
+ const files = /* @__PURE__ */ new Map();
10694
+ for (const [path, at] of paths) {
10695
+ const bytes = archive[at];
10696
+ if (bytes) files.set(path, bytes);
10697
+ }
10698
+ const siteFiles = [...await listFiles(CONTENT_DIR), ...await listFiles(MEDIA_DIR)];
10699
+ const same = /* @__PURE__ */ new Set();
10700
+ const uncopied = /* @__PURE__ */ new Set();
10701
+ const mine = new Set(siteFiles);
10702
+ for (const [path, bytes] of files) {
10703
+ if (!path.startsWith(`${MEDIA_DIR}/`)) continue;
10704
+ if (bytes.length > MAX_UPLOAD_BYTES || !mediaKind(fileName$1(path))) uncopied.add(path);
10705
+ else if (mine.has(path)) {
10706
+ const own = await readBytes(path);
10707
+ if (own && own.length === bytes.length && own.every((byte, index) => byte === bytes[index])) same.add(path);
10708
+ }
10709
+ }
10710
+ let plugins = (site.description.plugins ?? []).map((plugin) => plugin.name);
10711
+ try {
10712
+ const text = await readFile(INSTALLED_PLUGINS_FILE);
10713
+ if (text) plugins = [...plugins, ...Object.keys(JSON.parse(text).plugins ?? {})];
10714
+ } catch {}
10715
+ setChoices(NO_CHOICES);
10716
+ setLoaded({
10717
+ name: file.name,
10718
+ files,
10719
+ siteFiles,
10720
+ same,
10721
+ uncopied,
10722
+ plugins
10723
+ });
10724
+ } catch (error) {
10725
+ setProblem({
10726
+ message: problemText(t, error),
10727
+ error
10728
+ });
10729
+ } finally {
10730
+ setReading(false);
10731
+ }
10732
+ };
10733
+ const importNow = async () => {
10734
+ if (!loaded || !plan) return;
10735
+ setPublishing(true);
10736
+ setFailure(null);
10737
+ setProblem(void 0);
10738
+ try {
10739
+ let blockChanges = [];
10740
+ const install = plan.installable.filter((block) => choices.install.has(block.name));
10741
+ if (install.length > 0) try {
10742
+ const record = parseInstalledRecord(await readFile(INSTALLED_RECORD_FILE));
10743
+ blockChanges = (await planBlockChanges({
10744
+ changes: install.map((block) => ({ add: block.source })),
10745
+ registries: { [GOODFELLOW_REGISTRY]: registries[GOODFELLOW_REGISTRY] },
10746
+ fetchJson,
10747
+ readFile,
10748
+ record,
10749
+ blocks: Object.keys(blocks),
10750
+ content
10751
+ })).changes;
10752
+ } catch (error) {
10753
+ setProblem({
10754
+ message: t("siteTransfer.error.blocks"),
10755
+ error
10756
+ });
10757
+ setPublishing(false);
10758
+ return;
10759
+ }
10760
+ const media = plan.media.flatMap((path) => {
10761
+ const bytes = loaded.files.get(path);
10762
+ return bytes ? [{
10763
+ path,
10764
+ bytes
10765
+ }] : [];
10766
+ });
10767
+ for (const file of media) if ("bytes" in file) mediaPreviews.remember(mediaUrl(file.path), file.bytes);
10768
+ const title = plan.summary.title || loaded.name;
10769
+ const result = await publish([
10770
+ ...plan.changes,
10771
+ ...media,
10772
+ ...blockChanges
10773
+ ], t("siteTransfer.import.message", { title }));
10774
+ if (result.ok) {
10775
+ setDone(title);
10776
+ setLoaded(void 0);
10777
+ setPlan(void 0);
10778
+ } else setFailure(result);
10779
+ } finally {
10780
+ setPublishing(false);
10781
+ }
10782
+ };
10783
+ const toggle = (key, label) => /* @__PURE__ */ jsxs("label", {
10784
+ className: "gfa-checkbox",
10785
+ children: [/* @__PURE__ */ jsx("input", {
10786
+ type: "checkbox",
10787
+ checked: choices[key],
10788
+ disabled: publishing,
10789
+ onChange: (event) => setChoices({
10790
+ ...choices,
10791
+ [key]: event.target.checked
10792
+ })
10793
+ }), t(label)]
10794
+ });
10795
+ const setTake = (keys) => setChoices({
10796
+ ...choices,
10797
+ take: new Set(keys)
10798
+ });
10799
+ const leftOut = plan?.notes.some((note) => note.kind === "left-out" || note.kind === "without-blocks");
10800
+ const nothing = plan !== void 0 && plan.changes.length === 0 && plan.media.length === 0 && choices.install.size === 0;
10801
+ return /* @__PURE__ */ jsxs("section", {
10802
+ "aria-labelledby": "gfa-site-import-title",
10803
+ children: [
10804
+ /* @__PURE__ */ jsx("h2", {
10805
+ id: "gfa-site-import-title",
10806
+ className: "gfa-section-title",
10807
+ children: t("siteTransfer.import.title")
10808
+ }),
10809
+ done && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
10810
+ role: "status",
10811
+ className: "gfa-notice gfa-notice-success",
10812
+ children: t("siteTransfer.import.done", { title: done })
10813
+ }), /* @__PURE__ */ jsx("p", {
10814
+ className: "gfa-import-actions",
10815
+ children: /* @__PURE__ */ jsx(AppLink, {
10816
+ href: "#/pages",
10817
+ className: "gfa-button gfa-button-primary",
10818
+ children: t("import.viewPages")
10819
+ })
10820
+ })] }),
10821
+ !loaded ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
10822
+ /* @__PURE__ */ jsx("p", { children: t("siteTransfer.import.intro") }),
10823
+ problem && /* @__PURE__ */ jsx(ErrorMessage, {
10824
+ message: problem.message,
10825
+ error: problem.error
10826
+ }),
10827
+ /* @__PURE__ */ jsxs("label", {
10828
+ className: "gfa-field",
10829
+ children: [
10830
+ /* @__PURE__ */ jsx("span", {
10831
+ className: "gfa-label",
10832
+ children: t("siteTransfer.import.file")
10833
+ }),
10834
+ /* @__PURE__ */ jsx("input", {
10835
+ type: "file",
10836
+ className: "gfa-input",
10837
+ accept: ".zip,application/zip",
10838
+ disabled: reading,
10839
+ onChange: (event) => {
10840
+ const chosen = event.target.files?.[0];
10841
+ event.target.value = "";
10842
+ if (chosen) choose(chosen);
10843
+ }
10844
+ }),
10845
+ /* @__PURE__ */ jsx("span", {
10846
+ className: "gfa-hint",
10847
+ children: t("siteTransfer.import.fileHint")
10848
+ })
10849
+ ]
10850
+ }),
10851
+ reading && /* @__PURE__ */ jsx("p", {
10852
+ role: "status",
10853
+ children: t("siteTransfer.import.reading")
10854
+ })
10855
+ ] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
10856
+ /* @__PURE__ */ jsx("p", { children: t("siteTransfer.import.ready", { title: plan?.summary.title || loaded.name }) }),
10857
+ /* @__PURE__ */ jsxs("fieldset", {
10858
+ className: "gfa-fieldset",
10859
+ children: [
10860
+ /* @__PURE__ */ jsx("legend", { children: t("siteTransfer.import.mode") }),
10861
+ /* @__PURE__ */ jsxs("label", {
10862
+ className: "gfa-checkbox",
10863
+ children: [/* @__PURE__ */ jsx("input", {
10864
+ type: "radio",
10865
+ name: "gfa-site-import-mode",
10866
+ checked: !choices.replace,
10867
+ disabled: publishing,
10868
+ onChange: () => setChoices({
10869
+ ...choices,
10870
+ replace: false
10871
+ })
10872
+ }), t("siteTransfer.import.mode.add")]
10873
+ }),
10874
+ /* @__PURE__ */ jsxs("label", {
10875
+ className: "gfa-checkbox",
10876
+ children: [/* @__PURE__ */ jsx("input", {
10877
+ type: "radio",
10878
+ name: "gfa-site-import-mode",
10879
+ checked: choices.replace,
10880
+ disabled: publishing,
10881
+ onChange: () => setChoices({
10882
+ ...choices,
10883
+ replace: true
10884
+ })
10885
+ }), t("siteTransfer.import.mode.replace")]
10886
+ })
10887
+ ]
10888
+ }),
10889
+ choices.replace ? /* @__PURE__ */ jsx("p", {
10890
+ className: "gfa-notice gfa-notice-warning",
10891
+ children: t("siteTransfer.import.replaceWarning")
10892
+ }) : /* @__PURE__ */ jsxs("fieldset", {
10893
+ className: "gfa-fieldset",
10894
+ children: [
10895
+ /* @__PURE__ */ jsx("legend", { children: t("siteTransfer.import.also") }),
10896
+ toggle("menus", "siteTransfer.import.menus"),
10897
+ toggle("settings", "siteTransfer.import.settings"),
10898
+ toggle("layout", "siteTransfer.import.layout"),
10899
+ toggle("redirects", "siteTransfer.import.redirects")
10900
+ ]
10901
+ }),
10902
+ plan && mayInstall && plan.installable.length > 0 && /* @__PURE__ */ jsxs("fieldset", {
10903
+ className: "gfa-fieldset",
10904
+ children: [
10905
+ /* @__PURE__ */ jsx("legend", { children: t("siteTransfer.import.blocks") }),
10906
+ plan.installable.map((block) => /* @__PURE__ */ jsxs("label", {
10907
+ className: "gfa-checkbox",
10908
+ children: [/* @__PURE__ */ jsx("input", {
10909
+ type: "checkbox",
10910
+ checked: choices.install.has(block.name),
10911
+ disabled: publishing,
10912
+ onChange: (event) => {
10913
+ const install = new Set(choices.install);
10914
+ if (event.target.checked) install.add(block.name);
10915
+ else install.delete(block.name);
10916
+ setChoices({
10917
+ ...choices,
10918
+ install
10919
+ });
10920
+ }
10921
+ }), t("siteTransfer.import.install", { title: block.title })]
10922
+ }, block.name)),
10923
+ /* @__PURE__ */ jsx("p", {
10924
+ className: "gfa-hint",
10925
+ children: t("siteTransfer.import.installHint")
10926
+ })
10927
+ ]
10928
+ }),
10929
+ leftOut && toggle("withoutMissingBlocks", "siteTransfer.import.withoutBlocks"),
10930
+ plan && plan.conflicts.length > 0 && /* @__PURE__ */ jsxs("fieldset", {
10931
+ className: "gfa-fieldset",
10932
+ children: [
10933
+ /* @__PURE__ */ jsx("legend", { children: t("siteTransfer.import.conflicts") }),
10934
+ /* @__PURE__ */ jsx("p", {
10935
+ className: "gfa-hint",
10936
+ children: t("siteTransfer.import.conflictsHint")
10937
+ }),
10938
+ /* @__PURE__ */ jsxs("p", {
10939
+ className: "gfa-import-actions",
10940
+ children: [/* @__PURE__ */ jsx(Button, {
10941
+ onClick: () => setTake(plan.conflicts.map((conflict) => conflict.key)),
10942
+ disabled: publishing,
10943
+ children: t("siteTransfer.import.useAllTheirs")
10944
+ }), /* @__PURE__ */ jsx(Button, {
10945
+ onClick: () => setTake([]),
10946
+ disabled: publishing,
10947
+ children: t("siteTransfer.import.useAllMine")
10948
+ })]
10949
+ }),
10950
+ plan.conflicts.map((conflict) => /* @__PURE__ */ jsxs("label", {
10951
+ className: "gfa-checkbox",
10952
+ children: [/* @__PURE__ */ jsx("input", {
10953
+ type: "checkbox",
10954
+ checked: choices.take.has(conflict.key),
10955
+ disabled: publishing,
10956
+ onChange: (event) => {
10957
+ const take = new Set(choices.take);
10958
+ if (event.target.checked) take.add(conflict.key);
10959
+ else take.delete(conflict.key);
10960
+ setTake(take);
10961
+ }
10962
+ }), t("siteTransfer.import.useTheirs", { what: conflictText(t, conflict) })]
10963
+ }, conflict.key))
10964
+ ]
10965
+ }),
10966
+ plan && /* @__PURE__ */ jsxs(Fragment$1, { children: [nothing ? /* @__PURE__ */ jsx("p", {
10967
+ className: "gfa-notice",
10968
+ children: t("siteTransfer.summary.nothing")
10969
+ }) : /* @__PURE__ */ jsxs("ul", {
10970
+ className: "gfa-import-summary",
10971
+ children: [
10972
+ /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.pages", { count: String(plan.summary.pages) }) }),
10973
+ /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.items", { count: String(plan.summary.items) }) }),
10974
+ plan.summary.collections > 0 && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.collections", { count: String(plan.summary.collections) }) }),
10975
+ plan.summary.syncedBlocks > 0 && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.syncedBlocks", { count: String(plan.summary.syncedBlocks) }) }),
10976
+ /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.media", { count: String(plan.summary.media) }) }),
10977
+ plan.summary.menus && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.menus") }),
10978
+ plan.summary.settings && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.settings") }),
10979
+ plan.summary.layout && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.layout") }),
10980
+ plan.summary.redirects && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.redirects") }),
10981
+ plan.summary.removed > 0 && /* @__PURE__ */ jsx("li", { children: t("siteTransfer.summary.removed", { count: String(plan.summary.removed) }) })
10982
+ ]
10983
+ }), plan.notes.length > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("h3", {
10984
+ className: "gfa-section-title",
10985
+ children: t("import.check")
10986
+ }), /* @__PURE__ */ jsx("ul", {
10987
+ className: "gfa-import-notes",
10988
+ children: plan.notes.map((note) => /* @__PURE__ */ jsx("li", { children: noteText$1(t, note, blockName) }, JSON.stringify(note)))
10989
+ })] })] }),
10990
+ problem && /* @__PURE__ */ jsx(ErrorMessage, {
10991
+ message: problem.message,
10992
+ error: problem.error
10993
+ }),
10994
+ /* @__PURE__ */ jsx(PublishFailure, { failure }),
10995
+ publishing ? /* @__PURE__ */ jsx("p", {
10996
+ role: "status",
10997
+ children: t("import.publishing")
10998
+ }) : /* @__PURE__ */ jsxs("p", {
10999
+ className: "gfa-import-actions",
11000
+ children: [/* @__PURE__ */ jsx(Button, {
11001
+ variant: "primary",
11002
+ onClick: () => void importNow(),
11003
+ disabled: !plan || nothing,
11004
+ children: t("import.start")
11005
+ }), /* @__PURE__ */ jsx(Button, {
11006
+ onClick: () => {
11007
+ setLoaded(void 0);
11008
+ setPlan(void 0);
11009
+ },
11010
+ children: t("siteTransfer.import.another")
11011
+ })]
11012
+ })
11013
+ ] })
11014
+ ]
11015
+ });
11016
+ }
11017
+ //#endregion
11018
+ //#region src/import-screen.tsx
11019
+ /** How many media files are downloaded at once. */
11020
+ const DOWNLOADS = 4;
11021
+ const CONVERTED = {
11022
+ block: "import.note.block",
11023
+ dropped: "import.note.dropped",
11024
+ shortcode: "import.note.shortcode",
11025
+ embed: "import.note.embed",
11026
+ form: "import.note.form",
11027
+ script: "import.note.script",
11028
+ table: "import.note.table",
11029
+ posts: "import.note.posts",
11030
+ video: "import.note.video"
11031
+ };
11032
+ const LEFT_OUT = {
11033
+ drafts: "import.note.leftOut.drafts",
11034
+ private: "import.note.leftOut.private",
11035
+ trash: "import.note.leftOut.trash",
11036
+ comments: "import.note.leftOut.comments"
11037
+ };
11038
+ /** What the import couldn't bring across as it was, in words. */
11039
+ function noteText(t, note) {
11040
+ switch (note.kind) {
11041
+ case "left-out": return t(LEFT_OUT[note.what] ?? "import.note.leftOut.other", {
11042
+ count: String(note.count),
11043
+ name: note.what
11044
+ });
11045
+ case "converted": {
11046
+ const shown = note.items.slice(0, 3).join(", ");
11047
+ const items = note.items.length > 3 ? t("import.note.more", {
11048
+ items: shown,
11049
+ count: String(note.items.length - 3)
11050
+ }) : shown;
11051
+ return t(CONVERTED[note.note] ?? "import.note.block", {
11052
+ name: note.name,
10248
11053
  items
10249
11054
  });
10250
11055
  }
@@ -10600,7 +11405,10 @@ function ImportScreen() {
10600
11405
  children: t("import.reading")
10601
11406
  }),
10602
11407
  /* @__PURE__ */ jsx(MarkdownImportSection, {}),
10603
- /* @__PURE__ */ jsx(ExportSection, {})
11408
+ /* @__PURE__ */ jsx(SiteImportSection, {}),
11409
+ /* @__PURE__ */ jsx(ExportSection, {}),
11410
+ /* @__PURE__ */ jsx(MarkdownExportSection, {}),
11411
+ /* @__PURE__ */ jsx(SiteExportSection, {})
10604
11412
  ] }) : stage.kind === "done" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
10605
11413
  /* @__PURE__ */ jsx("p", {
10606
11414
  role: "status",
@@ -11817,73 +12625,437 @@ function PluginDetails({ name, installed, record, canInstall, canInstallEarly =
11817
12625
  ]
11818
12626
  });
11819
12627
  }
11820
- /** Packages every site has, which reviews leave out: Goodfellow's own, React and Puck. */
11821
- const KNOWN_PACKAGES = [
11822
- "react",
11823
- "react-dom",
11824
- "@puckeditor/core",
11825
- "@goodfellow-cms/core",
11826
- "@goodfellow-cms/react",
11827
- "@goodfellow-cms/blocks",
11828
- "@goodfellow-cms/admin"
11829
- ];
11830
- const NOTE_KINDS = [
11831
- "network",
11832
- "programs",
11833
- "eval",
11834
- "native",
11835
- "install-script"
11836
- ];
11837
- /** One sentence on what the packages read could do, for the confirmation. */
11838
- function noteSummary(t, reviews) {
11839
- const kinds = NOTE_KINDS.filter((kind) => reviews.some((review) => (review.counts[kind] ?? 0) > 0));
11840
- const unread = reviews.filter((review) => review.unread).length;
11841
- return [kinds.length > 0 ? t("plugins.code.summary", { list: kinds.map((kind) => t(`plugins.note.${kind}`)).join("; ") }) : t("plugins.code.nothing"), ...unread > 0 ? [t("plugins.code.unreadSummary", { count: String(unread) })] : []].join(" ");
11842
- }
11843
- /** What each package's code could do, as the review found it. */
11844
- function CodeReview({ reviews }) {
12628
+ /** Packages every site has, which reviews leave out: Goodfellow's own, React and Puck. */
12629
+ const KNOWN_PACKAGES = [
12630
+ "react",
12631
+ "react-dom",
12632
+ "@puckeditor/core",
12633
+ "@goodfellow-cms/core",
12634
+ "@goodfellow-cms/react",
12635
+ "@goodfellow-cms/blocks",
12636
+ "@goodfellow-cms/admin"
12637
+ ];
12638
+ const NOTE_KINDS = [
12639
+ "network",
12640
+ "programs",
12641
+ "eval",
12642
+ "native",
12643
+ "install-script"
12644
+ ];
12645
+ /** One sentence on what the packages read could do, for the confirmation. */
12646
+ function noteSummary(t, reviews) {
12647
+ const kinds = NOTE_KINDS.filter((kind) => reviews.some((review) => (review.counts[kind] ?? 0) > 0));
12648
+ const unread = reviews.filter((review) => review.unread).length;
12649
+ return [kinds.length > 0 ? t("plugins.code.summary", { list: kinds.map((kind) => t(`plugins.note.${kind}`)).join("; ") }) : t("plugins.code.nothing"), ...unread > 0 ? [t("plugins.code.unreadSummary", { count: String(unread) })] : []].join(" ");
12650
+ }
12651
+ /** What each package's code could do, as the review found it. */
12652
+ function CodeReview({ reviews }) {
12653
+ const t = useStrings();
12654
+ return /* @__PURE__ */ jsx("ul", {
12655
+ className: "gfa-plugin-review",
12656
+ children: reviews.map((review) => {
12657
+ const kinds = NOTE_KINDS.filter((kind) => (review.counts[kind] ?? 0) > 0);
12658
+ return /* @__PURE__ */ jsxs("li", { children: [
12659
+ /* @__PURE__ */ jsxs("p", { children: [
12660
+ /* @__PURE__ */ jsxs("code", { children: [
12661
+ review.name,
12662
+ " ",
12663
+ review.version
12664
+ ] }),
12665
+ " ",
12666
+ review.via.length > 0 && /* @__PURE__ */ jsx("span", {
12667
+ className: "gfa-hint",
12668
+ children: t("plugins.code.via", { via: review.via.join(" → ") })
12669
+ })
12670
+ ] }),
12671
+ kinds.length === 0 && !review.unread && /* @__PURE__ */ jsx("p", {
12672
+ className: "gfa-hint",
12673
+ children: t("plugins.code.none")
12674
+ }),
12675
+ kinds.map((kind) => /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsxs("summary", { children: [
12676
+ t(`plugins.note.${kind}`),
12677
+ " (",
12678
+ review.counts[kind],
12679
+ ")"
12680
+ ] }), /* @__PURE__ */ jsx("ul", { children: review.notes.filter((note) => note.kind === kind).map((note) => /* @__PURE__ */ jsxs("li", { children: [
12681
+ /* @__PURE__ */ jsxs("span", {
12682
+ className: "gfa-hint",
12683
+ children: [note.file, note.line ? `:${note.line}` : ""]
12684
+ }),
12685
+ " ",
12686
+ /* @__PURE__ */ jsx("code", { children: note.excerpt })
12687
+ ] }, `${note.file}:${note.line ?? 0}:${note.excerpt}`)) })] }, kind)),
12688
+ review.unread && /* @__PURE__ */ jsx("p", {
12689
+ className: "gfa-hint",
12690
+ children: t(`plugins.unread.${review.unread}`)
12691
+ })
12692
+ ] }, `${review.name}@${review.version}`);
12693
+ })
12694
+ });
12695
+ }
12696
+ const DEPLOY_TO_CLOUDFLARE = `https://deploy.workers.cloudflare.com/?url=https://github.com/goodfellow-cms/goodfellow-sign-in`;
12697
+ const HOST_NAMES = {
12698
+ github: "GitHub",
12699
+ gitlab: "GitLab",
12700
+ bitbucket: "Bitbucket"
12701
+ };
12702
+ /** The permissions a Bitbucket consumer needs, as Bitbucket's form names them. */
12703
+ const BITBUCKET_PERMISSIONS = "Account: Read, Repositories: Write, Pull requests: Write, Pipelines: Write";
12704
+ function SignInSection({ config: given, contentOnly, hostName, save }) {
12705
+ const t = useStrings();
12706
+ const config = given ?? "";
12707
+ const fromAccount = Object.entries(HOST_NAMES).find(([, name]) => name === hostName)?.[0];
12708
+ const host = (given ? configBackendHost(given) : void 0) ?? (contentOnly ? fromAccount : void 0);
12709
+ if (!host) return null;
12710
+ const key = host === "github" ? "signIn" : "clientId";
12711
+ const current = configBackendOption(config, key);
12712
+ const writable = !contentOnly && withBackendOption(config, key, "placeholder-value") !== void 0;
12713
+ const publish = async (value, message) => {
12714
+ const next = withBackendOption(config, key, value);
12715
+ if (next === void 0) throw new Error("goodfellow.config.tsx's backend line can't be changed here.");
12716
+ await save(next, message);
12717
+ };
12718
+ return /* @__PURE__ */ jsxs("section", {
12719
+ className: "gfa-form",
12720
+ "aria-labelledby": "gfa-sign-in",
12721
+ children: [/* @__PURE__ */ jsx("h2", {
12722
+ className: "gfa-section-title",
12723
+ id: "gfa-sign-in",
12724
+ children: t(host === "github" ? "signInSetup.service.title" : "signInSetup.app.title", { host: HOST_NAMES[host] })
12725
+ }), host === "github" ? /* @__PURE__ */ jsx(ServiceSettings, {
12726
+ current,
12727
+ writable,
12728
+ contentOnly,
12729
+ publish
12730
+ }) : /* @__PURE__ */ jsx(AppSettings, {
12731
+ host,
12732
+ config,
12733
+ current,
12734
+ writable,
12735
+ contentOnly,
12736
+ publish
12737
+ })]
12738
+ });
12739
+ }
12740
+ function Current({ text, remove }) {
12741
+ const t = useStrings();
12742
+ const [busy, setBusy] = useState(false);
12743
+ const [error, setError] = useState();
12744
+ return /* @__PURE__ */ jsxs("div", {
12745
+ className: "gfa-form",
12746
+ children: [
12747
+ /* @__PURE__ */ jsx("p", {
12748
+ className: "gfa-notice gfa-notice-success",
12749
+ role: "status",
12750
+ children: text
12751
+ }),
12752
+ error !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
12753
+ message: t("signInSetup.error"),
12754
+ error
12755
+ }),
12756
+ remove && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
12757
+ variant: "ghost",
12758
+ disabled: busy,
12759
+ onClick: async () => {
12760
+ setBusy(true);
12761
+ setError(void 0);
12762
+ try {
12763
+ await remove();
12764
+ } catch (caught) {
12765
+ setError(caught);
12766
+ } finally {
12767
+ setBusy(false);
12768
+ }
12769
+ },
12770
+ children: t("signInSetup.remove")
12771
+ }) })
12772
+ ]
12773
+ });
12774
+ }
12775
+ /** What a developer runs where the config is in the code's repository. */
12776
+ function Command({ command }) {
12777
+ const t = useStrings();
12778
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: t("signInSetup.contentOnly") }), /* @__PURE__ */ jsx("pre", { children: command })] });
12779
+ }
12780
+ function ServiceSettings({ current, writable, contentOnly, publish }) {
12781
+ const t = useStrings();
12782
+ const [input, setInput] = useState("");
12783
+ const [problem, setProblem] = useState();
12784
+ const [busy, setBusy] = useState(false);
12785
+ const [error, setError] = useState();
12786
+ if (current) return /* @__PURE__ */ jsx(Current, {
12787
+ text: t("signInSetup.service.using", { address: current }),
12788
+ remove: writable ? () => publish(void 0, t("signInSetup.service.removeMessage", { address: current })) : void 0
12789
+ });
12790
+ const submit = async () => {
12791
+ const url = input.trim().replace(/\/+$/, "");
12792
+ if (signInServiceProblem(url)) {
12793
+ setProblem("invalid");
12794
+ return;
12795
+ }
12796
+ setBusy(true);
12797
+ setError(void 0);
12798
+ try {
12799
+ const origin = new URL(url).origin;
12800
+ const health = await fetch(`${origin}/health`).then((response) => response.ok ? response.json() : void 0, () => void 0);
12801
+ if (!health) {
12802
+ setProblem("unreachable");
12803
+ return;
12804
+ }
12805
+ if (health.ok !== true) {
12806
+ setProblem("unset");
12807
+ return;
12808
+ }
12809
+ await publish(origin, t("signInSetup.service.message", { address: origin }));
12810
+ } catch (caught) {
12811
+ setError(caught);
12812
+ } finally {
12813
+ setBusy(false);
12814
+ }
12815
+ };
12816
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
12817
+ /* @__PURE__ */ jsx("p", { children: t("signInSetup.service.intro") }),
12818
+ /* @__PURE__ */ jsxs("ol", {
12819
+ className: "gfa-steps",
12820
+ children: [
12821
+ /* @__PURE__ */ jsxs("li", { children: [
12822
+ t("signInSetup.service.step.deploy"),
12823
+ " ",
12824
+ /* @__PURE__ */ jsx("a", {
12825
+ href: DEPLOY_TO_CLOUDFLARE,
12826
+ target: "_blank",
12827
+ rel: "noopener noreferrer",
12828
+ children: t("signInSetup.service.deploy")
12829
+ })
12830
+ ] }),
12831
+ /* @__PURE__ */ jsx("li", { children: t("signInSetup.service.step.setup") }),
12832
+ /* @__PURE__ */ jsx("li", { children: t("signInSetup.service.step.use") })
12833
+ ]
12834
+ }),
12835
+ /* @__PURE__ */ jsx("p", {
12836
+ className: "gfa-hint",
12837
+ children: /* @__PURE__ */ jsx("a", {
12838
+ href: docsLink("sign-in-service"),
12839
+ target: "_blank",
12840
+ rel: "noopener noreferrer",
12841
+ children: t("signInSetup.service.more")
12842
+ })
12843
+ }),
12844
+ contentOnly ? /* @__PURE__ */ jsx(Command, { command: "npx goodfellow sign-in service https://sign-in.your-name.workers.dev" }) : !writable ? /* @__PURE__ */ jsx("p", { children: t("signInSetup.handWritten", { key: "signIn" }) }) : /* @__PURE__ */ jsxs("form", {
12845
+ className: "gfa-form",
12846
+ onSubmit: (event) => {
12847
+ event.preventDefault();
12848
+ submit();
12849
+ },
12850
+ children: [
12851
+ /* @__PURE__ */ jsx(TextField, {
12852
+ label: t("signInSetup.service.field"),
12853
+ hint: t("signInSetup.service.fieldHint"),
12854
+ placeholder: "https://sign-in.your-name.workers.dev",
12855
+ value: input,
12856
+ error: problem ? t(`signInSetup.service.problem.${problem}`) : void 0,
12857
+ onChange: (value) => {
12858
+ setInput(value);
12859
+ setProblem(void 0);
12860
+ }
12861
+ }),
12862
+ error !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
12863
+ message: t("signInSetup.error"),
12864
+ error
12865
+ }),
12866
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
12867
+ variant: "primary",
12868
+ type: "submit",
12869
+ disabled: busy || !input.trim(),
12870
+ children: t("signInSetup.service.use")
12871
+ }) })
12872
+ ]
12873
+ })
12874
+ ] });
12875
+ }
12876
+ function CopyValue({ label, value }) {
12877
+ const t = useStrings();
12878
+ const [copied, setCopied] = useState(false);
12879
+ return /* @__PURE__ */ jsxs("div", {
12880
+ className: "gfa-copy-value",
12881
+ children: [
12882
+ /* @__PURE__ */ jsx("span", {
12883
+ className: "gfa-copy-label",
12884
+ children: label
12885
+ }),
12886
+ /* @__PURE__ */ jsx("code", { children: value }),
12887
+ /* @__PURE__ */ jsx(Button, {
12888
+ variant: "ghost",
12889
+ onClick: () => {
12890
+ navigator.clipboard?.writeText(value).then(() => setCopied(true), () => {});
12891
+ },
12892
+ children: copied ? t("signInSetup.copied") : t("signInSetup.copy")
12893
+ })
12894
+ ]
12895
+ });
12896
+ }
12897
+ function AppSettings({ host, config, current, writable, contentOnly, publish }) {
11845
12898
  const t = useStrings();
11846
- return /* @__PURE__ */ jsx("ul", {
11847
- className: "gfa-plugin-review",
11848
- children: reviews.map((review) => {
11849
- const kinds = NOTE_KINDS.filter((kind) => (review.counts[kind] ?? 0) > 0);
11850
- return /* @__PURE__ */ jsxs("li", { children: [
11851
- /* @__PURE__ */ jsxs("p", { children: [
11852
- /* @__PURE__ */ jsxs("code", { children: [
11853
- review.name,
12899
+ const { content } = useSiteContent();
12900
+ const [input, setInput] = useState("");
12901
+ const [problem, setProblem] = useState(false);
12902
+ const [busy, setBusy] = useState(false);
12903
+ const [error, setError] = useState();
12904
+ const name = HOST_NAMES[host];
12905
+ const admin = configAdmin(config);
12906
+ const addresses = signInRedirectAddresses(content.settings.url, {
12907
+ admin: admin?.url,
12908
+ broker: configBroker(config)?.url
12909
+ });
12910
+ if (current) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Current, {
12911
+ text: t("signInSetup.app.on", { host: name }),
12912
+ remove: writable ? () => publish(void 0, t("signInSetup.app.removeMessage", { host: name })) : void 0
12913
+ }), addresses.length > 0 && /* @__PURE__ */ jsx("p", {
12914
+ className: "gfa-hint",
12915
+ children: t(host === "gitlab" ? "signInSetup.app.gitlab.keep" : "signInSetup.app.bitbucket.keep", { addresses: addresses.join(", ") })
12916
+ })] });
12917
+ const submit = async () => {
12918
+ const id = input.trim();
12919
+ if (signInClientIdProblem(id)) {
12920
+ setProblem(true);
12921
+ return;
12922
+ }
12923
+ setBusy(true);
12924
+ setError(void 0);
12925
+ try {
12926
+ await publish(id, t("signInSetup.app.message", { host: name }));
12927
+ } catch (caught) {
12928
+ setError(caught);
12929
+ } finally {
12930
+ setBusy(false);
12931
+ }
12932
+ };
12933
+ const title = `Goodfellow ${content.settings.title ?? ""}`.trim().slice(0, 100);
12934
+ let steps;
12935
+ if (host === "gitlab") {
12936
+ const web = (configBackendOption(config, "url") ?? "https://gitlab.com").replace(/\/$/, "");
12937
+ const project = configBackendOption(config, "project") ?? "";
12938
+ const group = project.includes("/") ? project.slice(0, project.lastIndexOf("/")) : void 0;
12939
+ steps = /* @__PURE__ */ jsxs("ol", {
12940
+ className: "gfa-steps",
12941
+ children: [
12942
+ /* @__PURE__ */ jsxs("li", { children: [
12943
+ t("signInSetup.app.gitlab.open"),
12944
+ " ",
12945
+ group && /* @__PURE__ */ jsxs(Fragment$1, { children: [
12946
+ /* @__PURE__ */ jsx("a", {
12947
+ href: `${web}/groups/${group}/-/settings/applications`,
12948
+ target: "_blank",
12949
+ rel: "noopener noreferrer",
12950
+ children: t("signInSetup.app.gitlab.group", { group })
12951
+ }),
11854
12952
  " ",
11855
- review.version
12953
+ t("signInSetup.or"),
12954
+ " "
11856
12955
  ] }),
12956
+ /* @__PURE__ */ jsx("a", {
12957
+ href: `${web}/-/user_settings/applications`,
12958
+ target: "_blank",
12959
+ rel: "noopener noreferrer",
12960
+ children: t("signInSetup.app.gitlab.own")
12961
+ })
12962
+ ] }),
12963
+ /* @__PURE__ */ jsxs("li", { children: [
12964
+ t("signInSetup.app.gitlab.fill"),
12965
+ /* @__PURE__ */ jsx(CopyValue, {
12966
+ label: t("signInSetup.app.name"),
12967
+ value: title
12968
+ }),
12969
+ /* @__PURE__ */ jsx(CopyValue, {
12970
+ label: t("signInSetup.app.gitlab.redirect"),
12971
+ value: addresses.join("\n")
12972
+ }),
12973
+ /* @__PURE__ */ jsx("p", {
12974
+ className: "gfa-hint",
12975
+ children: t("signInSetup.app.gitlab.settings")
12976
+ })
12977
+ ] }),
12978
+ /* @__PURE__ */ jsx("li", { children: t("signInSetup.app.gitlab.copyId") })
12979
+ ]
12980
+ });
12981
+ } else {
12982
+ const workspace = (configBackendOption(config, "repo") ?? "").split("/")[0] ?? "";
12983
+ const callback = admin?.checked ? addresses.find((address) => address.startsWith(new URL(admin.url).origin)) : addresses[0];
12984
+ steps = /* @__PURE__ */ jsxs("ol", {
12985
+ className: "gfa-steps",
12986
+ children: [
12987
+ /* @__PURE__ */ jsxs("li", { children: [
12988
+ t("signInSetup.app.bitbucket.open"),
11857
12989
  " ",
11858
- review.via.length > 0 && /* @__PURE__ */ jsx("span", {
12990
+ /* @__PURE__ */ jsx("a", {
12991
+ href: `https://bitbucket.org/${workspace}/workspace/settings/api`,
12992
+ target: "_blank",
12993
+ rel: "noopener noreferrer",
12994
+ children: t("signInSetup.app.bitbucket.consumers", { workspace })
12995
+ })
12996
+ ] }),
12997
+ /* @__PURE__ */ jsxs("li", { children: [
12998
+ t("signInSetup.app.bitbucket.fill"),
12999
+ /* @__PURE__ */ jsx(CopyValue, {
13000
+ label: t("signInSetup.app.name"),
13001
+ value: title
13002
+ }),
13003
+ callback && /* @__PURE__ */ jsx(CopyValue, {
13004
+ label: t("signInSetup.app.bitbucket.callback"),
13005
+ value: callback
13006
+ }),
13007
+ /* @__PURE__ */ jsx(CopyValue, {
13008
+ label: t("signInSetup.app.bitbucket.permissions"),
13009
+ value: BITBUCKET_PERMISSIONS
13010
+ }),
13011
+ /* @__PURE__ */ jsx("p", {
11859
13012
  className: "gfa-hint",
11860
- children: t("plugins.code.via", { via: review.via.join(" → ") })
13013
+ children: t("signInSetup.app.bitbucket.settings")
11861
13014
  })
11862
13015
  ] }),
11863
- kinds.length === 0 && !review.unread && /* @__PURE__ */ jsx("p", {
11864
- className: "gfa-hint",
11865
- children: t("plugins.code.none")
13016
+ /* @__PURE__ */ jsx("li", { children: t("signInSetup.app.bitbucket.copyId") })
13017
+ ]
13018
+ });
13019
+ }
13020
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
13021
+ /* @__PURE__ */ jsx("p", { children: t("signInSetup.app.intro", { host: name }) }),
13022
+ addresses.length === 0 ? /* @__PURE__ */ jsx("p", {
13023
+ className: "gfa-notice",
13024
+ children: t("signInSetup.app.noAddress")
13025
+ }) : steps,
13026
+ contentOnly ? /* @__PURE__ */ jsx(Command, { command: "npx goodfellow sign-in client-id <the id>" }) : !writable ? /* @__PURE__ */ jsx("p", { children: t("signInSetup.handWritten", { key: "clientId" }) }) : /* @__PURE__ */ jsxs("form", {
13027
+ className: "gfa-form",
13028
+ onSubmit: (event) => {
13029
+ event.preventDefault();
13030
+ submit();
13031
+ },
13032
+ children: [
13033
+ /* @__PURE__ */ jsx(TextField, {
13034
+ label: t(host === "gitlab" ? "signInSetup.app.gitlab.field" : "signInSetup.app.bitbucket.field"),
13035
+ value: input,
13036
+ error: problem ? t("signInSetup.app.problem", { host: name }) : void 0,
13037
+ onChange: (value) => {
13038
+ setInput(value);
13039
+ setProblem(false);
13040
+ }
11866
13041
  }),
11867
- kinds.map((kind) => /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsxs("summary", { children: [
11868
- t(`plugins.note.${kind}`),
11869
- " (",
11870
- review.counts[kind],
11871
- ")"
11872
- ] }), /* @__PURE__ */ jsx("ul", { children: review.notes.filter((note) => note.kind === kind).map((note) => /* @__PURE__ */ jsxs("li", { children: [
11873
- /* @__PURE__ */ jsxs("span", {
11874
- className: "gfa-hint",
11875
- children: [note.file, note.line ? `:${note.line}` : ""]
11876
- }),
11877
- " ",
11878
- /* @__PURE__ */ jsx("code", { children: note.excerpt })
11879
- ] }, `${note.file}:${note.line ?? 0}:${note.excerpt}`)) })] }, kind)),
11880
- review.unread && /* @__PURE__ */ jsx("p", {
13042
+ error !== void 0 && /* @__PURE__ */ jsx(ErrorMessage, {
13043
+ message: t("signInSetup.error"),
13044
+ error
13045
+ }),
13046
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
13047
+ variant: "primary",
13048
+ type: "submit",
13049
+ disabled: busy || !input.trim(),
13050
+ children: t("signInSetup.app.turnOn", { host: name })
13051
+ }) }),
13052
+ /* @__PURE__ */ jsx("p", {
11881
13053
  className: "gfa-hint",
11882
- children: t(`plugins.unread.${review.unread}`)
13054
+ children: t("signInSetup.app.after", { host: name })
11883
13055
  })
11884
- ] }, `${review.name}@${review.version}`);
13056
+ ]
11885
13057
  })
11886
- });
13058
+ ] });
11887
13059
  }
11888
13060
  //#endregion
11889
13061
  //#region src/security-screen.tsx
@@ -11891,7 +13063,8 @@ function CodeReview({ reviews }) {
11891
13063
  * Site Settings' Security tab: the site's token broker, a sign-in page on an
11892
13064
  * address of its own that keeps editors' sign-in and AI keys and shows the
11893
13065
  * admin panel in a frame. Owners publish its address in the config's `broker`
11894
- * line (`withBrokerConfig()`), or remove it.
13066
+ * line (`withBrokerConfig()`), or remove it. Below it, how editors sign in:
13067
+ * the sign-in service or OAuth application (`SignInSection`).
11895
13068
  */
11896
13069
  /**
11897
13070
  * Whether editors sign in where the site's own pages could reach their
@@ -11931,6 +13104,14 @@ function SecurityScreen() {
11931
13104
  if (!result.ok) throw result.error;
11932
13105
  setConfig(next);
11933
13106
  }, [config, publish]);
13107
+ const saveConfig = useCallback(async (next, message) => {
13108
+ const result = await publish([{
13109
+ path: CONFIG_FILE$1,
13110
+ content: next
13111
+ }], message, { checks: false });
13112
+ if (!result.ok) throw result.error;
13113
+ setConfig(next);
13114
+ }, [publish]);
11934
13115
  let body;
11935
13116
  if (demo) body = /* @__PURE__ */ jsx("p", { children: t("security.demo") });
11936
13117
  else if (!versions) body = /* @__PURE__ */ jsx("p", { children: t("security.local") });
@@ -11981,27 +13162,36 @@ function SecurityScreen() {
11981
13162
  }
11982
13163
  return /* @__PURE__ */ jsxs("div", {
11983
13164
  className: "gfa-screen",
11984
- children: [/* @__PURE__ */ jsx("div", {
11985
- className: "gfa-screen-header",
11986
- children: /* @__PURE__ */ jsx("h1", { children: t("settings.tab.security") })
11987
- }), /* @__PURE__ */ jsxs("section", {
11988
- className: "gfa-form",
11989
- "aria-labelledby": "gfa-broker",
11990
- children: [
11991
- /* @__PURE__ */ jsx("h2", {
11992
- className: "gfa-section-title",
11993
- id: "gfa-broker",
11994
- children: t("security.broker.title")
11995
- }),
11996
- /* @__PURE__ */ jsx("p", { children: t("security.broker.intro") }),
11997
- exposed === "site" && /* @__PURE__ */ jsx("p", {
11998
- className: "gfa-notice",
11999
- role: "note",
12000
- children: t("security.exposed")
12001
- }),
12002
- body
12003
- ]
12004
- })]
13165
+ children: [
13166
+ /* @__PURE__ */ jsx("div", {
13167
+ className: "gfa-screen-header",
13168
+ children: /* @__PURE__ */ jsx("h1", { children: t("settings.tab.security") })
13169
+ }),
13170
+ /* @__PURE__ */ jsxs("section", {
13171
+ className: "gfa-form",
13172
+ "aria-labelledby": "gfa-broker",
13173
+ children: [
13174
+ /* @__PURE__ */ jsx("h2", {
13175
+ className: "gfa-section-title",
13176
+ id: "gfa-broker",
13177
+ children: t("security.broker.title")
13178
+ }),
13179
+ /* @__PURE__ */ jsx("p", { children: t("security.broker.intro") }),
13180
+ exposed === "site" && /* @__PURE__ */ jsx("p", {
13181
+ className: "gfa-notice",
13182
+ role: "note",
13183
+ children: t("security.exposed")
13184
+ }),
13185
+ body
13186
+ ]
13187
+ }),
13188
+ !demo && versions && config !== void 0 && account?.role === void 0 && /* @__PURE__ */ jsx(SignInSection, {
13189
+ config,
13190
+ contentOnly,
13191
+ hostName: account?.hostName ?? "",
13192
+ save: saveConfig
13193
+ })
13194
+ ]
12005
13195
  });
12006
13196
  }
12007
13197
  function BrokerForm({ initial, check, host, listed, save }) {
@@ -14955,91 +16145,6 @@ function SettingsScreen({ tab, section }) {
14955
16145
  });
14956
16146
  }
14957
16147
  //#endregion
14958
- //#region src/site-download.ts
14959
- /**
14960
- * A copy of the site to keep: its content, its media library and the records
14961
- * of the blocks and plugins it installed, as one zip file, read at the
14962
- * revision the admin panel loaded. fflate is loaded only when a copy is made.
14963
- * No React or DOM, so it's tested in Node.
14964
- */
14965
- /** The folders a copy holds whole. */
14966
- const DOWNLOAD_FOLDERS = [CONTENT_DIR, MEDIA_DIR];
14967
- /** Single files a copy holds where the site has them: which blocks and plugins it installed, to add them again. */
14968
- const DOWNLOAD_RECORDS = [INSTALLED_RECORD_FILE, INSTALLED_PLUGINS_FILE];
14969
- /** How many files are read at once. */
14970
- const READ_AT_ONCE = 6;
14971
- /** Files that are already compressed are stored as they are, which is as small and much quicker. */
14972
- const COMPRESSED = /\.(jpe?g|png|webp|gif|avif|mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|pdf|woff2?|zip|gz)$/i;
14973
- /** Every file a copy holds, sorted, from the folders and records the site has. */
14974
- async function downloadPaths(list) {
14975
- const folders = await Promise.all(DOWNLOAD_FOLDERS.map((dir) => list(dir)));
14976
- const records = await Promise.all(DOWNLOAD_RECORDS.map(async (path) => {
14977
- return (await list(path.slice(0, path.lastIndexOf("/")))).includes(path) ? [path] : [];
14978
- }));
14979
- return [.../* @__PURE__ */ new Set([...folders.flat(), ...records.flat()])].sort();
14980
- }
14981
- /**
14982
- * The copy's file name: the site's title in plain letters, then the day,
14983
- * `st-anne-parish-2026-11-01.zip`, or `site-…` for a title with none.
14984
- */
14985
- function downloadName(title, date) {
14986
- return `${(title ?? "").normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/, "") || "site"}-${[
14987
- date.getFullYear(),
14988
- date.getMonth() + 1,
14989
- date.getDate()
14990
- ].map((part) => String(part).padStart(2, "0")).join("-")}.zip`;
14991
- }
14992
- /**
14993
- * Zips the files, read with `read`, in the order given, a few at a time, and
14994
- * returns the zip in pieces, for a `Blob`. A file that's gone since it was
14995
- * listed is left out. Throws what reading throws.
14996
- */
14997
- async function siteArchive(paths, read, onProgress) {
14998
- const { Zip, ZipDeflate, ZipPassThrough } = await import("fflate");
14999
- const chunks = [];
15000
- let finish = () => {};
15001
- let fail = () => {};
15002
- const finished = new Promise((resolve, reject) => {
15003
- finish = resolve;
15004
- fail = reject;
15005
- });
15006
- const zip = new Zip((error, chunk, final) => {
15007
- if (error) return fail(error);
15008
- chunks.push(chunk);
15009
- if (final) finish();
15010
- });
15011
- let done = 0;
15012
- onProgress?.({
15013
- done,
15014
- total: paths.length
15015
- });
15016
- try {
15017
- for (let start = 0; start < paths.length; start += READ_AT_ONCE) {
15018
- const batch = paths.slice(start, start + READ_AT_ONCE);
15019
- const files = await Promise.all(batch.map((path) => read(path)));
15020
- batch.forEach((path, index) => {
15021
- const bytes = files[index];
15022
- if (bytes) {
15023
- const entry = COMPRESSED.test(path) ? new ZipPassThrough(path) : new ZipDeflate(path, { level: 6 });
15024
- zip.add(entry);
15025
- entry.push(bytes, true);
15026
- }
15027
- done += 1;
15028
- });
15029
- onProgress?.({
15030
- done,
15031
- total: paths.length
15032
- });
15033
- }
15034
- } catch (error) {
15035
- zip.terminate();
15036
- throw error;
15037
- }
15038
- zip.end();
15039
- await finished;
15040
- return chunks;
15041
- }
15042
- //#endregion
15043
16148
  //#region src/site-history-screen.tsx
15044
16149
  const PER_PAGE = 20;
15045
16150
  /**
@@ -15084,7 +16189,7 @@ function DownloadCopy() {
15084
16189
  total: 0
15085
16190
  });
15086
16191
  try {
15087
- const chunks = await siteArchive(await downloadPaths(listFiles), readBytes, setProgress);
16192
+ const chunks = await siteCopyArchive(listFiles, readBytes, content, setProgress);
15088
16193
  const url = URL.createObjectURL(new Blob(chunks, { type: "application/zip" }));
15089
16194
  const link = document.createElement("a");
15090
16195
  link.href = url;