@goodfellow-cms/admin 0.3.0 → 0.4.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.d.ts +23 -1
- package/dist/index.js +429 -100
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export declare const defaultStrings: {
|
|
|
58
58
|
readonly "signIn.install": "Install Goodfellow on {host}";
|
|
59
59
|
readonly "signIn.error.expired": "You were signed out because your sign-in expired. Sign in again to keep editing.";
|
|
60
60
|
readonly "signIn.error.other": "Couldn't reach {host}. Check your connection and try again.";
|
|
61
|
+
readonly "signIn.app.intro": "Sign in with your account on this site to edit it.";
|
|
62
|
+
readonly "signIn.app.button": "Sign in";
|
|
63
|
+
readonly "signIn.app.expired": "You've been signed out. Sign in again to keep editing.";
|
|
64
|
+
readonly "signIn.app.notAllowed": "You're signed in, but your account isn't allowed to edit this site. Ask one of its owners to give you access.";
|
|
65
|
+
readonly "signIn.app.error": "Couldn't reach the site. Check your connection and try again.";
|
|
66
|
+
readonly "owners.only": "Only the site's owners can change this.";
|
|
67
|
+
readonly "owners.onlyHint": "Ask one of them if something here needs changing.";
|
|
61
68
|
readonly "account.signedInAs": "Signed in as {name}";
|
|
62
69
|
readonly "account.signOut": "Sign out";
|
|
63
70
|
readonly "deploy.building": "Updating the live site…";
|
|
@@ -305,6 +312,8 @@ export declare const defaultStrings: {
|
|
|
305
312
|
readonly "demo.startOver": "Start over";
|
|
306
313
|
readonly "demo.startOverTitle": "Start over?";
|
|
307
314
|
readonly "demo.startOverBody": "Everything you've changed in this demo, including uploads, will be undone.";
|
|
315
|
+
readonly "blocks.contentOnly": "This site's blocks are added by its developer.";
|
|
316
|
+
readonly "blocks.contentOnlyHint": "Its code is kept apart from its content, so adding or removing a block is done where the code is, and shows up here once the site has been rebuilt.";
|
|
308
317
|
readonly "blocks.demo": "In this demo you can see which blocks there are, but not add or remove them: a block is added to a real site by updating its code.";
|
|
309
318
|
readonly "markdown.formatted": "Formatted";
|
|
310
319
|
readonly "markdown.source": "Markdown";
|
|
@@ -415,6 +424,8 @@ export declare const defaultStrings: {
|
|
|
415
424
|
readonly "ai.provider.manual": "Copy and paste into a chat app (free)";
|
|
416
425
|
readonly "ai.provider.custom": "Another service (OpenAI-compatible)";
|
|
417
426
|
readonly "ai.noKey": "(free, no key needed)";
|
|
427
|
+
readonly "ai.siteKey": "(provided by this site)";
|
|
428
|
+
readonly "ai.siteKeyHint": "This site provides {service}, so you don't need a key of your own.";
|
|
418
429
|
readonly "ai.baseUrl": "Address of the service's API";
|
|
419
430
|
readonly "ai.baseUrlHint": "Such as https://example.org/v1.";
|
|
420
431
|
readonly "ai.key": "API key";
|
|
@@ -526,6 +537,11 @@ export declare const defaultStrings: {
|
|
|
526
537
|
readonly "settings.tab.editors": "Editors";
|
|
527
538
|
readonly "settings.tab.updates": "Updates";
|
|
528
539
|
readonly "updates.intro": "Goodfellow, the software the site runs on, gets fixes and new releases. Fixes install on their own each night, and only once the site has been checked to still build with them.";
|
|
540
|
+
readonly "updates.contentOnly": "This site's code is kept apart from its content, so Goodfellow's updates are installed where the code is, by the site's own setup or its developer.";
|
|
541
|
+
readonly "updates.code": "This site's code is kept apart from its content, at {place}, where Goodfellow's updates are installed.";
|
|
542
|
+
readonly "updates.code.link": "See it on {host}";
|
|
543
|
+
readonly "updates.noAccess": "Your sign-in can't see the site's code, at {place}, where its updates are installed. Ask the site's developer to give you access to it. On GitHub, an access token needs to reach it too, or with Sign in with GitHub, Goodfellow's app needs to be installed there.";
|
|
544
|
+
readonly "updates.back.skip": "Don't install Goodfellow {version} again";
|
|
529
545
|
readonly "updates.local": "Updates are shown in the published site's admin panel, where you sign in with GitHub or GitLab.";
|
|
530
546
|
readonly "updates.demo": "Updates aren't installed in a demo.";
|
|
531
547
|
readonly "updates.loading": "Checking for updates…";
|
|
@@ -762,6 +778,12 @@ interface AdminProps {
|
|
|
762
778
|
store?: ContentStore;
|
|
763
779
|
/** The git host the site is stored on. Defaults to the config's `backend`. */
|
|
764
780
|
host?: GitHost;
|
|
781
|
+
/**
|
|
782
|
+
* The address of a full-stack site's API for the admin panel, such as
|
|
783
|
+
* `/api/goodfellow`. Editors then sign in with the site's own sign-in, and its
|
|
784
|
+
* server reads and publishes for them, so they never sign in to the git host.
|
|
785
|
+
*/
|
|
786
|
+
server?: string;
|
|
765
787
|
preview: PreviewOptions;
|
|
766
788
|
/** The live site's address, for "View" links. Defaults to `/`. */
|
|
767
789
|
siteUrl?: string;
|
|
@@ -774,7 +796,7 @@ interface AdminProps {
|
|
|
774
796
|
registry?: string;
|
|
775
797
|
}
|
|
776
798
|
/** The admin panel. Render it on its own page, such as `/admin`. */
|
|
777
|
-
export declare function Admin({ config, store, host, preview, siteUrl, strings, registry }: AdminProps): import("react").JSX.Element;
|
|
799
|
+
export declare function Admin({ config, store, host, server, preview, siteUrl, strings, registry }: AdminProps): import("react").JSX.Element;
|
|
778
800
|
/** Renders the admin panel into an element. */
|
|
779
801
|
export declare function mountAdmin(element: Element, props: AdminProps): void;
|
|
780
802
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CODE_THEMES, COLLECTIONS_DIR, COLOR_SCHEME_ATTRIBUTE, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DomainError, EditorsError, FIELD_TYPES, FOOTER_FILE, GOODFELLOW_REGISTRY, HEADER_FILE, HeadCodeError, INSTALLED_RECORD_FILE, MEDIA_DIR, NPM_REGISTRY, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RegistryError, SignInError, THEME_COLORS, TITLE_FIELD, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, WEEKDAYS, addressPatternFor, addressPatternProblem, allPages, availableBlocks, availableUpdates, blockUses, canFormatMarkdown, checkEntrySlug, checkPageAddress, checkRecords, cleanEventValue, codeFileChanges, collectionFileChange, collectionFileChange as collectionFileChange$1, collectionFileSchema, compareReleases, credentialStorage, customCssFileChange, datePart, deleteCollectionChanges, demoContentStore, demoStore, domainZone, entryAddress, entryFileChange, entryFileChange as entryFileChange$1, entryTitle, escapeStyleText, eventValueProblem, findEntry, findSitePage, formTarget, formatDate, formatOccurrence, goodfellowRegistryUrl, googleFontsUrl, isAddressSegment, isAllDay, isDemoStore, isEmptyValue, isEventLike, isReservedPagePath, layoutFileChange, loadSiteContent, markdownToHtml, mediaFileChange, menusFileChange, menusFileSchema, moveEntryChanges, movePageChanges, newCollectionSettings, newPageData, normalizeDomain, pageFileChange, pageTitle, pageView, parseDemoContent, parseHeadCode, parseInstalledRecord, parseUpdateSettings, planBlockChanges, recordsInPlace, redirectFrom, redirectProblems, redirectsFileChange, registrarName, releaseVersions, serializeContent, shownOccurrence, sitePages, siteSettingsFileChange, siteSettingsSchema, siteVersion, slugify, storedCss, storedData, storedEntryFields, storedRedirects, themeToCss, todayIn, trimChars, uniqueName, updateSettingsFileChange, wallClock, weekdayName, weekdayOf, withoutCredits, writeChanges } from "@goodfellow-cms/core";
|
|
1
|
+
import { APP_API_HEADER, CODE_THEMES, COLLECTIONS_DIR, COLOR_SCHEME_ATTRIBUTE, ConflictError, ContentError, DEFAULT_CODE_THEMES, DEFAULT_DARK_THEME_COLORS, DEFAULT_THEME_COLORS, DEMO_CONTENT_PATH, DomainError, EditorsError, FIELD_TYPES, FOOTER_FILE, GOODFELLOW_REGISTRY, HEADER_FILE, HeadCodeError, INSTALLED_RECORD_FILE, MEDIA_DIR, NPM_REGISTRY, REDIRECT_LINK_PLACEHOLDER, REDIRECT_MESSAGE, RegistryError, SignInError, THEME_COLORS, TITLE_FIELD, UPDATES_FILE, UpdatesError, VERSION_PACKAGE, WEEKDAYS, addressPatternFor, addressPatternProblem, allPages, availableBlocks, availableUpdates, blockUses, canFormatMarkdown, checkEntrySlug, checkPageAddress, checkRecords, cleanEventValue, codeFileChanges, collectionFileChange, collectionFileChange as collectionFileChange$1, collectionFileSchema, compareReleases, credentialStorage, customCssFileChange, datePart, decodeBase64, deleteCollectionChanges, demoContentStore, demoStore, domainZone, encodeBase64Bytes, entryAddress, entryFileChange, entryFileChange as entryFileChange$1, entryTitle, escapeStyleText, eventValueProblem, findEntry, findSitePage, formTarget, formatDate, formatOccurrence, goodfellowRegistryUrl, googleFontsUrl, isAddressSegment, isAllDay, isDemoStore, isEmptyValue, isEventLike, isReservedPagePath, layoutFileChange, loadSiteContent, markdownToHtml, mediaFileChange, menusFileChange, menusFileSchema, moveEntryChanges, movePageChanges, newCollectionSettings, newPageData, normalizeDomain, pageFileChange, pageTitle, pageView, parseDemoContent, parseHeadCode, parseInstalledRecord, parseUpdateSettings, planBlockChanges, recordsInPlace, redirectFrom, redirectProblems, redirectsFileChange, registrarName, releaseVersions, serializeContent, shownOccurrence, sitePages, siteSettingsFileChange, siteSettingsSchema, siteVersion, slugify, storedCss, storedData, storedEntryFields, storedRedirects, themeToCss, todayIn, trimChars, uniqueName, updateSettingsFileChange, wallClock, weekdayName, weekdayOf, withoutCredits, writeChanges } from "@goodfellow-cms/core";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
4
|
import { PageBody, SiteProvider, applyEntry, createPuckConfig, createPuckConfigs, cx, mediaFieldKind, preparePage, siteMetadata } from "@goodfellow-cms/react";
|
|
@@ -385,7 +385,7 @@ function AdminProvider({ config, store, account, preview, siteUrl, registry, onS
|
|
|
385
385
|
const templateConfig = useMemo(() => createPuckConfig(config, "template"), [config]);
|
|
386
386
|
const mediaPreviews = useMemo(() => createMediaPreviews(store, siteUrl), [store, siteUrl]);
|
|
387
387
|
const registries = useMemo(() => ({
|
|
388
|
-
[GOODFELLOW_REGISTRY]: registry ?? goodfellowRegistryUrl("0.
|
|
388
|
+
[GOODFELLOW_REGISTRY]: registry ?? goodfellowRegistryUrl("0.4.0"),
|
|
389
389
|
...config.registries
|
|
390
390
|
}), [registry, config.registries]);
|
|
391
391
|
const favicon = state.status === "ready" ? state.content.settings.favicon : void 0;
|
|
@@ -398,6 +398,7 @@ function AdminProvider({ config, store, account, preview, siteUrl, registry, onS
|
|
|
398
398
|
const changedPaths = useMemo(() => isGitBackend(store) ? (from, to) => store.changedPaths(from, to) : void 0, [store]);
|
|
399
399
|
const editors = isGitBackend(store) ? store.editors : void 0;
|
|
400
400
|
const updates = isGitBackend(store) ? store.updates : void 0;
|
|
401
|
+
const contentOnly = isGitBackend(store) && store.contentOnly === true;
|
|
401
402
|
const backendOwner = isGitBackend(store) ? store.ownerAccess : void 0;
|
|
402
403
|
const [ownerActive, setOwnerActive] = useState(backendOwner?.active ?? false);
|
|
403
404
|
const ownerAccess = useMemo(() => backendOwner && {
|
|
@@ -507,7 +508,8 @@ function AdminProvider({ config, store, account, preview, siteUrl, registry, onS
|
|
|
507
508
|
changedPaths,
|
|
508
509
|
editors,
|
|
509
510
|
ownerAccess,
|
|
510
|
-
updates
|
|
511
|
+
updates,
|
|
512
|
+
contentOnly
|
|
511
513
|
}), [
|
|
512
514
|
config,
|
|
513
515
|
pageConfig,
|
|
@@ -530,13 +532,18 @@ function AdminProvider({ config, store, account, preview, siteUrl, registry, onS
|
|
|
530
532
|
changedPaths,
|
|
531
533
|
editors,
|
|
532
534
|
ownerAccess,
|
|
533
|
-
updates
|
|
535
|
+
updates,
|
|
536
|
+
contentOnly
|
|
534
537
|
]);
|
|
535
538
|
return /* @__PURE__ */ jsx(AdminContext.Provider, {
|
|
536
539
|
value,
|
|
537
540
|
children
|
|
538
541
|
});
|
|
539
542
|
}
|
|
543
|
+
/** Whether the signed-in person may change Site Settings, blocks, and collections' settings and templates. */
|
|
544
|
+
function useCanChangeSettings() {
|
|
545
|
+
return useAdmin().account?.role !== "editor";
|
|
546
|
+
}
|
|
540
547
|
function useAdmin() {
|
|
541
548
|
const value = useContext(AdminContext);
|
|
542
549
|
if (!value) throw new Error("useAdmin() must be used inside <Admin>.");
|
|
@@ -642,75 +649,133 @@ function useStockPhotoChoice() {
|
|
|
642
649
|
}, [])];
|
|
643
650
|
}
|
|
644
651
|
//#endregion
|
|
645
|
-
//#region src/
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
function useRoute() {
|
|
658
|
-
return parseHash(useSyncExternalStore(subscribe$1, () => window.location.hash, () => ""));
|
|
659
|
-
}
|
|
660
|
-
const guards = /* @__PURE__ */ new Set();
|
|
661
|
-
/** Whether any screen has unpublished changes. */
|
|
662
|
-
function hasUnsavedChanges() {
|
|
663
|
-
return [...guards].some((isDirty) => isDirty());
|
|
664
|
-
}
|
|
665
|
-
/**
|
|
666
|
-
* Registers a screen's unpublished changes, so leaving it (inside the admin
|
|
667
|
-
* panel or by closing the tab) asks for confirmation first.
|
|
668
|
-
*/
|
|
669
|
-
function useUnsavedChanges(isDirty) {
|
|
670
|
-
useEffect(() => {
|
|
671
|
-
if (!isDirty) return;
|
|
672
|
-
const guard = () => true;
|
|
673
|
-
guards.add(guard);
|
|
674
|
-
const onBeforeUnload = (event) => event.preventDefault();
|
|
675
|
-
window.addEventListener("beforeunload", onBeforeUnload);
|
|
676
|
-
return () => {
|
|
677
|
-
guards.delete(guard);
|
|
678
|
-
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
679
|
-
};
|
|
680
|
-
}, [isDirty]);
|
|
681
|
-
}
|
|
682
|
-
/** Goes to another screen, asking first if there are unpublished changes. Returns whether it navigated. */
|
|
683
|
-
function navigate(to, confirmLeave) {
|
|
684
|
-
if (hasUnsavedChanges() && !confirmLeave()) return false;
|
|
685
|
-
guards.clear();
|
|
686
|
-
window.location.hash = to;
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
/** Sections of Site Settings' General tab that links can scroll to, as `#/settings/general?section=forms`. */
|
|
690
|
-
const SETTINGS_SECTIONS = {
|
|
691
|
-
contact: "contact",
|
|
692
|
-
forms: "forms"
|
|
652
|
+
//#region src/app-server.ts
|
|
653
|
+
/** Thrown when the site's server refuses something, such as an editor changing settings only owners may change. */
|
|
654
|
+
var AppServerError = class extends Error {
|
|
655
|
+
status;
|
|
656
|
+
code;
|
|
657
|
+
name = "AppServerError";
|
|
658
|
+
constructor(status, code) {
|
|
659
|
+
super(code === "owners-only" ? "Only the site's owners can change this." : code === "git" || code === "git-sign-in" ? "The site's server couldn't reach where the site is stored." : `The site's server answered ${status} (${code}).`);
|
|
660
|
+
this.status = status;
|
|
661
|
+
this.code = code;
|
|
662
|
+
}
|
|
693
663
|
};
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
664
|
+
/** A full-stack site's API for the admin panel, at `api`, such as `/api/goodfellow`. */
|
|
665
|
+
function appServer(api) {
|
|
666
|
+
const base = api.replace(/\/+$/, "");
|
|
667
|
+
async function request(endpoint, { query, body, missingIsFine = false } = {}) {
|
|
668
|
+
const search = query ? `?${new URLSearchParams(query)}` : "";
|
|
669
|
+
const response = await fetch(`${base}/${endpoint}/${search}`, {
|
|
670
|
+
method: body === void 0 ? "GET" : "POST",
|
|
671
|
+
headers: {
|
|
672
|
+
[APP_API_HEADER]: "1",
|
|
673
|
+
...body !== void 0 && { "content-type": "application/json" }
|
|
674
|
+
},
|
|
675
|
+
...body !== void 0 && { body: JSON.stringify(body) },
|
|
676
|
+
credentials: "same-origin",
|
|
677
|
+
cache: "no-store"
|
|
678
|
+
});
|
|
679
|
+
if (response.ok || response.status === 404 && missingIsFine) return response;
|
|
680
|
+
if (response.status === 409) throw new ConflictError();
|
|
681
|
+
const answer = await response.json().catch(() => ({}));
|
|
682
|
+
if (response.status === 401) throw new SignInError("invalid", "Signing in to the site has ended.");
|
|
683
|
+
throw new AppServerError(response.status, answer.error ?? "error");
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
/** Who's signed in, and what they may do, or where to sign in. */
|
|
687
|
+
async session() {
|
|
688
|
+
return await (await request("session")).json();
|
|
689
|
+
},
|
|
690
|
+
/** The site's files through the server, as an editor signed in to it reads and publishes them. */
|
|
691
|
+
backend(session, onSignOut) {
|
|
692
|
+
let pinned;
|
|
693
|
+
const revision = async () => {
|
|
694
|
+
pinned = (await (await request("revision")).json()).revision;
|
|
695
|
+
return pinned;
|
|
696
|
+
};
|
|
697
|
+
const at = async () => pinned ?? await revision();
|
|
698
|
+
return {
|
|
699
|
+
user: session.user,
|
|
700
|
+
...session.contentOnly && { contentOnly: true },
|
|
701
|
+
revision,
|
|
702
|
+
async read(path) {
|
|
703
|
+
const response = await request("file", {
|
|
704
|
+
query: {
|
|
705
|
+
path,
|
|
706
|
+
revision: await at()
|
|
707
|
+
},
|
|
708
|
+
missingIsFine: true
|
|
709
|
+
});
|
|
710
|
+
return response.status === 404 ? void 0 : (await response.json()).content;
|
|
711
|
+
},
|
|
712
|
+
async readBytes(path) {
|
|
713
|
+
const response = await request("file", {
|
|
714
|
+
query: {
|
|
715
|
+
path,
|
|
716
|
+
revision: await at(),
|
|
717
|
+
as: "bytes"
|
|
718
|
+
},
|
|
719
|
+
missingIsFine: true
|
|
720
|
+
});
|
|
721
|
+
return response.status === 404 ? void 0 : decodeBase64((await response.json()).base64);
|
|
722
|
+
},
|
|
723
|
+
async list(dir) {
|
|
724
|
+
return (await (await request("files", { query: {
|
|
725
|
+
dir,
|
|
726
|
+
revision: await at()
|
|
727
|
+
} })).json()).files;
|
|
728
|
+
},
|
|
729
|
+
async readFiles(paths) {
|
|
730
|
+
const answer = await (await request("files", { body: {
|
|
731
|
+
paths,
|
|
732
|
+
revision: await at()
|
|
733
|
+
} })).json();
|
|
734
|
+
return new Map(Object.entries(answer.files));
|
|
735
|
+
},
|
|
736
|
+
async write(changes, { message, expectedRevision }) {
|
|
737
|
+
const saved = await (await request("write", { body: {
|
|
738
|
+
changes: changes.map((change) => "bytes" in change ? {
|
|
739
|
+
path: change.path,
|
|
740
|
+
base64: encodeBase64Bytes(change.bytes)
|
|
741
|
+
} : change),
|
|
742
|
+
message,
|
|
743
|
+
expectedRevision
|
|
744
|
+
} })).json();
|
|
745
|
+
pinned = saved.revision;
|
|
746
|
+
return saved;
|
|
747
|
+
},
|
|
748
|
+
async changedPaths(from, to) {
|
|
749
|
+
return (await (await request("changed", { query: {
|
|
750
|
+
from,
|
|
751
|
+
to
|
|
752
|
+
} })).json()).paths;
|
|
753
|
+
},
|
|
754
|
+
async deployStatus(revision) {
|
|
755
|
+
return await (await request("deploy", { query: { revision } })).json();
|
|
756
|
+
},
|
|
757
|
+
async history(path, { page = 1, perPage = 20 } = {}) {
|
|
758
|
+
return (await (await request("history", { query: {
|
|
759
|
+
path,
|
|
760
|
+
revision: await at(),
|
|
761
|
+
page: String(page),
|
|
762
|
+
perPage: String(perPage)
|
|
763
|
+
} })).json()).versions;
|
|
764
|
+
},
|
|
765
|
+
async readAt(path, revision) {
|
|
766
|
+
const response = await request("version", {
|
|
767
|
+
query: {
|
|
768
|
+
path,
|
|
769
|
+
revision
|
|
770
|
+
},
|
|
771
|
+
missingIsFine: true
|
|
772
|
+
});
|
|
773
|
+
return response.status === 404 ? void 0 : (await response.json()).content;
|
|
774
|
+
},
|
|
775
|
+
signOut: onSignOut
|
|
776
|
+
};
|
|
706
777
|
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
function collectionHref(id, tab = "entries") {
|
|
710
|
-
return tab === "entries" ? `#/collections/${id}` : `#/collections/${id}/${tab}`;
|
|
711
|
-
}
|
|
712
|
-
function entryEditorHref(collection, slug) {
|
|
713
|
-
return `#/collections/${collection}/edit?slug=${encodeURIComponent(slug)}`;
|
|
778
|
+
};
|
|
714
779
|
}
|
|
715
780
|
//#endregion
|
|
716
781
|
//#region src/strings.tsx
|
|
@@ -759,6 +824,13 @@ const defaultStrings = {
|
|
|
759
824
|
"signIn.install": "Install Goodfellow on {host}",
|
|
760
825
|
"signIn.error.expired": "You were signed out because your sign-in expired. Sign in again to keep editing.",
|
|
761
826
|
"signIn.error.other": "Couldn't reach {host}. Check your connection and try again.",
|
|
827
|
+
"signIn.app.intro": "Sign in with your account on this site to edit it.",
|
|
828
|
+
"signIn.app.button": "Sign in",
|
|
829
|
+
"signIn.app.expired": "You've been signed out. Sign in again to keep editing.",
|
|
830
|
+
"signIn.app.notAllowed": "You're signed in, but your account isn't allowed to edit this site. Ask one of its owners to give you access.",
|
|
831
|
+
"signIn.app.error": "Couldn't reach the site. Check your connection and try again.",
|
|
832
|
+
"owners.only": "Only the site's owners can change this.",
|
|
833
|
+
"owners.onlyHint": "Ask one of them if something here needs changing.",
|
|
762
834
|
"account.signedInAs": "Signed in as {name}",
|
|
763
835
|
"account.signOut": "Sign out",
|
|
764
836
|
"deploy.building": "Updating the live site…",
|
|
@@ -1006,6 +1078,8 @@ const defaultStrings = {
|
|
|
1006
1078
|
"demo.startOver": "Start over",
|
|
1007
1079
|
"demo.startOverTitle": "Start over?",
|
|
1008
1080
|
"demo.startOverBody": "Everything you've changed in this demo, including uploads, will be undone.",
|
|
1081
|
+
"blocks.contentOnly": "This site's blocks are added by its developer.",
|
|
1082
|
+
"blocks.contentOnlyHint": "Its code is kept apart from its content, so adding or removing a block is done where the code is, and shows up here once the site has been rebuilt.",
|
|
1009
1083
|
"blocks.demo": "In this demo you can see which blocks there are, but not add or remove them: a block is added to a real site by updating its code.",
|
|
1010
1084
|
"markdown.formatted": "Formatted",
|
|
1011
1085
|
"markdown.source": "Markdown",
|
|
@@ -1116,6 +1190,8 @@ const defaultStrings = {
|
|
|
1116
1190
|
"ai.provider.manual": "Copy and paste into a chat app (free)",
|
|
1117
1191
|
"ai.provider.custom": "Another service (OpenAI-compatible)",
|
|
1118
1192
|
"ai.noKey": "(free, no key needed)",
|
|
1193
|
+
"ai.siteKey": "(provided by this site)",
|
|
1194
|
+
"ai.siteKeyHint": "This site provides {service}, so you don't need a key of your own.",
|
|
1119
1195
|
"ai.baseUrl": "Address of the service's API",
|
|
1120
1196
|
"ai.baseUrlHint": "Such as https://example.org/v1.",
|
|
1121
1197
|
"ai.key": "API key",
|
|
@@ -1227,6 +1303,11 @@ const defaultStrings = {
|
|
|
1227
1303
|
"settings.tab.editors": "Editors",
|
|
1228
1304
|
"settings.tab.updates": "Updates",
|
|
1229
1305
|
"updates.intro": "Goodfellow, the software the site runs on, gets fixes and new releases. Fixes install on their own each night, and only once the site has been checked to still build with them.",
|
|
1306
|
+
"updates.contentOnly": "This site's code is kept apart from its content, so Goodfellow's updates are installed where the code is, by the site's own setup or its developer.",
|
|
1307
|
+
"updates.code": "This site's code is kept apart from its content, at {place}, where Goodfellow's updates are installed.",
|
|
1308
|
+
"updates.code.link": "See it on {host}",
|
|
1309
|
+
"updates.noAccess": "Your sign-in can't see the site's code, at {place}, where its updates are installed. Ask the site's developer to give you access to it. On GitHub, an access token needs to reach it too, or with Sign in with GitHub, Goodfellow's app needs to be installed there.",
|
|
1310
|
+
"updates.back.skip": "Don't install Goodfellow {version} again",
|
|
1230
1311
|
"updates.local": "Updates are shown in the published site's admin panel, where you sign in with GitHub or GitLab.",
|
|
1231
1312
|
"updates.demo": "Updates aren't installed in a demo.",
|
|
1232
1313
|
"updates.loading": "Checking for updates…",
|
|
@@ -1585,6 +1666,146 @@ function PublishedNotice({ onHide }) {
|
|
|
1585
1666
|
});
|
|
1586
1667
|
}
|
|
1587
1668
|
//#endregion
|
|
1669
|
+
//#region src/app-sign-in.tsx
|
|
1670
|
+
/**
|
|
1671
|
+
* Shows the site's own sign-in until someone is signed in to it and allowed to
|
|
1672
|
+
* edit, on a full-stack site whose server publishes for them (`api`), then
|
|
1673
|
+
* renders its children with a backend that reads and publishes through it.
|
|
1674
|
+
*/
|
|
1675
|
+
function AppSignInGate({ api, children }) {
|
|
1676
|
+
const t = useStrings();
|
|
1677
|
+
const server = useMemo(() => appServer(api), [api]);
|
|
1678
|
+
const [state, setState] = useState({ status: "checking" });
|
|
1679
|
+
const check = useCallback(async (expired = false) => {
|
|
1680
|
+
try {
|
|
1681
|
+
const session = await server.session();
|
|
1682
|
+
if (session.signedIn) {
|
|
1683
|
+
const signOut = () => {
|
|
1684
|
+
if (session.signOut) location.assign(session.signOut);
|
|
1685
|
+
};
|
|
1686
|
+
setState({
|
|
1687
|
+
status: "signed-in",
|
|
1688
|
+
session,
|
|
1689
|
+
backend: server.backend(session, signOut)
|
|
1690
|
+
});
|
|
1691
|
+
} else setState({
|
|
1692
|
+
status: "signed-out",
|
|
1693
|
+
session,
|
|
1694
|
+
expired
|
|
1695
|
+
});
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
setState({
|
|
1698
|
+
status: "signed-out",
|
|
1699
|
+
error
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}, [server]);
|
|
1703
|
+
useEffect(() => {
|
|
1704
|
+
check();
|
|
1705
|
+
}, [check]);
|
|
1706
|
+
if (state.status === "checking") return /* @__PURE__ */ jsx("p", {
|
|
1707
|
+
className: "gfa-screen",
|
|
1708
|
+
children: t("signIn.checking")
|
|
1709
|
+
});
|
|
1710
|
+
if (state.status === "signed-out") {
|
|
1711
|
+
const { session, error, expired } = state;
|
|
1712
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1713
|
+
className: "gfa-sign-in",
|
|
1714
|
+
children: [/* @__PURE__ */ jsx("h1", { children: t("signIn.title") }), error !== void 0 ? /* @__PURE__ */ jsx(ErrorMessage, {
|
|
1715
|
+
message: t("signIn.app.error"),
|
|
1716
|
+
error,
|
|
1717
|
+
action: /* @__PURE__ */ jsx(Button, {
|
|
1718
|
+
onClick: () => void check(),
|
|
1719
|
+
children: t("action.tryAgain")
|
|
1720
|
+
})
|
|
1721
|
+
}) : session?.notAllowed ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: t("signIn.app.notAllowed") }), session.signOut && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("a", {
|
|
1722
|
+
className: "gfa-button gfa-button-secondary",
|
|
1723
|
+
href: session.signOut,
|
|
1724
|
+
children: t("account.signOut")
|
|
1725
|
+
}) })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: t(expired ? "signIn.app.expired" : "signIn.app.intro") }), session && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("a", {
|
|
1726
|
+
className: "gfa-button gfa-button-primary",
|
|
1727
|
+
href: session.signIn,
|
|
1728
|
+
children: t("signIn.app.button")
|
|
1729
|
+
}) })] })]
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
return children(state.backend, state.session, (error) => {
|
|
1733
|
+
if (error === void 0) state.backend.signOut();
|
|
1734
|
+
else check(true);
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
//#endregion
|
|
1738
|
+
//#region src/router.tsx
|
|
1739
|
+
function parseHash(hash) {
|
|
1740
|
+
const [path = "", query = ""] = hash.replace(/^#\/?/, "").split("?");
|
|
1741
|
+
return {
|
|
1742
|
+
segments: path.split("/").filter(Boolean),
|
|
1743
|
+
params: new URLSearchParams(query)
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
function subscribe$1(onChange) {
|
|
1747
|
+
window.addEventListener("hashchange", onChange);
|
|
1748
|
+
return () => window.removeEventListener("hashchange", onChange);
|
|
1749
|
+
}
|
|
1750
|
+
function useRoute() {
|
|
1751
|
+
return parseHash(useSyncExternalStore(subscribe$1, () => window.location.hash, () => ""));
|
|
1752
|
+
}
|
|
1753
|
+
const guards = /* @__PURE__ */ new Set();
|
|
1754
|
+
/** Whether any screen has unpublished changes. */
|
|
1755
|
+
function hasUnsavedChanges() {
|
|
1756
|
+
return [...guards].some((isDirty) => isDirty());
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Registers a screen's unpublished changes, so leaving it (inside the admin
|
|
1760
|
+
* panel or by closing the tab) asks for confirmation first.
|
|
1761
|
+
*/
|
|
1762
|
+
function useUnsavedChanges(isDirty) {
|
|
1763
|
+
useEffect(() => {
|
|
1764
|
+
if (!isDirty) return;
|
|
1765
|
+
const guard = () => true;
|
|
1766
|
+
guards.add(guard);
|
|
1767
|
+
const onBeforeUnload = (event) => event.preventDefault();
|
|
1768
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
1769
|
+
return () => {
|
|
1770
|
+
guards.delete(guard);
|
|
1771
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
1772
|
+
};
|
|
1773
|
+
}, [isDirty]);
|
|
1774
|
+
}
|
|
1775
|
+
/** Goes to another screen, asking first if there are unpublished changes. Returns whether it navigated. */
|
|
1776
|
+
function navigate(to, confirmLeave) {
|
|
1777
|
+
if (hasUnsavedChanges() && !confirmLeave()) return false;
|
|
1778
|
+
guards.clear();
|
|
1779
|
+
window.location.hash = to;
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
/** Sections of Site Settings' General tab that links can scroll to, as `#/settings/general?section=forms`. */
|
|
1783
|
+
const SETTINGS_SECTIONS = {
|
|
1784
|
+
contact: "contact",
|
|
1785
|
+
forms: "forms"
|
|
1786
|
+
};
|
|
1787
|
+
function pageEditorHref(path) {
|
|
1788
|
+
return `#/pages/edit?path=${encodeURIComponent(path)}`;
|
|
1789
|
+
}
|
|
1790
|
+
/** An in-app link that asks before leaving a screen with unpublished changes. */
|
|
1791
|
+
function Link({ href, confirmLeave, ...props }) {
|
|
1792
|
+
return /* @__PURE__ */ jsx("a", {
|
|
1793
|
+
...props,
|
|
1794
|
+
href,
|
|
1795
|
+
onClick: (event) => {
|
|
1796
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
1797
|
+
event.preventDefault();
|
|
1798
|
+
navigate(href, confirmLeave);
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
function collectionHref(id, tab = "entries") {
|
|
1803
|
+
return tab === "entries" ? `#/collections/${id}` : `#/collections/${id}/${tab}`;
|
|
1804
|
+
}
|
|
1805
|
+
function entryEditorHref(collection, slug) {
|
|
1806
|
+
return `#/collections/${collection}/edit?slug=${encodeURIComponent(slug)}`;
|
|
1807
|
+
}
|
|
1808
|
+
//#endregion
|
|
1588
1809
|
//#region src/blocks-screen.tsx
|
|
1589
1810
|
const fetchJson = async (url) => {
|
|
1590
1811
|
const response = await fetch(url);
|
|
@@ -2796,19 +3017,25 @@ function problemKey(error) {
|
|
|
2796
3017
|
if (error instanceof SyntaxError) return "ai.error.answer";
|
|
2797
3018
|
return "ai.error.service";
|
|
2798
3019
|
}
|
|
2799
|
-
function ServiceSettings({ providers, settings, onChange }) {
|
|
3020
|
+
function ServiceSettings({ providers, settings, onChange, siteAi }) {
|
|
2800
3021
|
const t = useStrings();
|
|
2801
3022
|
const info = PROVIDERS[settings.provider];
|
|
3023
|
+
const sitePays = siteAi?.services.includes(settings.provider) ?? false;
|
|
2802
3024
|
const service = serviceName(settings.provider, t);
|
|
2803
3025
|
const [models, setModels] = useState(info.models ?? []);
|
|
2804
3026
|
const listId = useRef(`gfa-ai-models-${Math.random().toString(36).slice(2)}`).current;
|
|
2805
3027
|
const { provider, apiKey, baseUrl } = settings;
|
|
3028
|
+
const server = sitePays ? siteAi?.server : void 0;
|
|
2806
3029
|
useEffect(() => {
|
|
2807
3030
|
let cancelled = false;
|
|
2808
3031
|
const controller = new AbortController();
|
|
2809
3032
|
setModels(PROVIDERS[provider].models ?? []);
|
|
2810
3033
|
const timer = setTimeout(() => {
|
|
2811
|
-
listModels({
|
|
3034
|
+
listModels(server ? {
|
|
3035
|
+
provider,
|
|
3036
|
+
server,
|
|
3037
|
+
model: ""
|
|
3038
|
+
} : {
|
|
2812
3039
|
provider,
|
|
2813
3040
|
apiKey,
|
|
2814
3041
|
baseUrl,
|
|
@@ -2825,7 +3052,8 @@ function ServiceSettings({ providers, settings, onChange }) {
|
|
|
2825
3052
|
}, [
|
|
2826
3053
|
provider,
|
|
2827
3054
|
apiKey,
|
|
2828
|
-
baseUrl
|
|
3055
|
+
baseUrl,
|
|
3056
|
+
server
|
|
2829
3057
|
]);
|
|
2830
3058
|
return /* @__PURE__ */ jsxs("div", {
|
|
2831
3059
|
className: "gfa-form gfa-ai-settings",
|
|
@@ -2839,18 +3067,22 @@ function ServiceSettings({ providers, settings, onChange }) {
|
|
|
2839
3067
|
onChange: (event) => onChange({ provider: event.target.value }),
|
|
2840
3068
|
children: providers.map((id) => /* @__PURE__ */ jsxs("option", {
|
|
2841
3069
|
value: id,
|
|
2842
|
-
children: [serviceName(id, t), !PROVIDERS[id].needsKey && id !== "manual" && id !== "custom" ? ` ${t("ai.noKey")}` : ""]
|
|
3070
|
+
children: [serviceName(id, t), siteAi?.services.includes(id) ? ` ${t("ai.siteKey")}` : !PROVIDERS[id].needsKey && id !== "manual" && id !== "custom" ? ` ${t("ai.noKey")}` : ""]
|
|
2843
3071
|
}, id))
|
|
2844
3072
|
})
|
|
2845
3073
|
}),
|
|
2846
|
-
|
|
3074
|
+
sitePays && /* @__PURE__ */ jsx("p", {
|
|
3075
|
+
className: "gfa-hint",
|
|
3076
|
+
children: t("ai.siteKeyHint", { service })
|
|
3077
|
+
}),
|
|
3078
|
+
settings.provider === "custom" && !sitePays && /* @__PURE__ */ jsx(TextField, {
|
|
2847
3079
|
label: t("ai.baseUrl"),
|
|
2848
3080
|
hint: t("ai.baseUrlHint"),
|
|
2849
3081
|
type: "url",
|
|
2850
3082
|
value: settings.baseUrl ?? "",
|
|
2851
3083
|
onChange: (baseUrl) => onChange({ baseUrl })
|
|
2852
3084
|
}),
|
|
2853
|
-
settings.provider !== "manual" && (info.needsKey || settings.provider === "custom") && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3085
|
+
settings.provider !== "manual" && !sitePays && (info.needsKey || settings.provider === "custom") && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2854
3086
|
/* @__PURE__ */ jsx(TextField, {
|
|
2855
3087
|
label: t("ai.key"),
|
|
2856
3088
|
hint: t("ai.keyHint", { service }),
|
|
@@ -2919,12 +3151,23 @@ function serviceName(id, t) {
|
|
|
2919
3151
|
/** The AI panel: describe what to write, and it's added to the page, replaces the selected block, or fills in the fields. */
|
|
2920
3152
|
function AiPanel({ context }) {
|
|
2921
3153
|
const t = useStrings();
|
|
2922
|
-
const { config, publish, mediaPreviews } = useAdmin();
|
|
3154
|
+
const { config, publish, mediaPreviews, account } = useAdmin();
|
|
2923
3155
|
const { content, media } = useSiteContent();
|
|
2924
3156
|
const getPuck = useGetPuck();
|
|
2925
3157
|
const selected = usePuck$1((state) => state.selectedItem);
|
|
2926
|
-
const
|
|
2927
|
-
const
|
|
3158
|
+
const siteAi = account?.ai;
|
|
3159
|
+
const providers = useMemo(() => {
|
|
3160
|
+
const allowed = allowedProviders(config.ai ? config.ai.providers : void 0);
|
|
3161
|
+
const paid = allowed.filter((id) => siteAi?.services.includes(id));
|
|
3162
|
+
return [...paid, ...allowed.filter((id) => !paid.includes(id))];
|
|
3163
|
+
}, [config.ai, siteAi]);
|
|
3164
|
+
const [chosen, updateSettings] = useAiSettings(providers);
|
|
3165
|
+
const settings = useMemo(() => siteAi?.services.includes(chosen.provider) ? {
|
|
3166
|
+
...chosen,
|
|
3167
|
+
apiKey: void 0,
|
|
3168
|
+
baseUrl: void 0,
|
|
3169
|
+
server: siteAi.server
|
|
3170
|
+
} : chosen, [chosen, siteAi]);
|
|
2928
3171
|
const [photosChosen, choosePhotos] = useStockPhotoChoice();
|
|
2929
3172
|
const photosOffered = !(config.ai && config.ai.stockPhotos === false);
|
|
2930
3173
|
const photosAllowed = photosOffered && photosChosen;
|
|
@@ -2937,7 +3180,7 @@ function AiPanel({ context }) {
|
|
|
2937
3180
|
const service = serviceName(settings.provider, t);
|
|
2938
3181
|
const blockName = (block) => block ? getPuck().config.components[block.type]?.label ?? block.type : "";
|
|
2939
3182
|
const itemName = inSentence(context.collection?.settings.entryName ?? "");
|
|
2940
|
-
const ready = settings.provider === "manual" || Boolean(settings.model) && (!PROVIDERS[settings.provider].needsKey || Boolean(settings.apiKey));
|
|
3183
|
+
const ready = settings.provider === "manual" || Boolean(settings.model) && (!PROVIDERS[settings.provider].needsKey || Boolean(settings.apiKey) || Boolean(settings.server));
|
|
2941
3184
|
const [settingsOpen, setSettingsOpen] = useState(!ready);
|
|
2942
3185
|
useEffect(() => () => controller.current?.abort(), []);
|
|
2943
3186
|
const taskFor = (text) => {
|
|
@@ -3235,8 +3478,9 @@ function AiPanel({ context }) {
|
|
|
3235
3478
|
onToggle: (event) => setSettingsOpen(event.currentTarget.open),
|
|
3236
3479
|
children: [/* @__PURE__ */ jsx("summary", { children: t("ai.settings", { service }) }), /* @__PURE__ */ jsx(ServiceSettings, {
|
|
3237
3480
|
providers,
|
|
3238
|
-
settings,
|
|
3239
|
-
onChange: updateSettings
|
|
3481
|
+
settings: chosen,
|
|
3482
|
+
onChange: updateSettings,
|
|
3483
|
+
...siteAi && { siteAi }
|
|
3240
3484
|
})]
|
|
3241
3485
|
})
|
|
3242
3486
|
]
|
|
@@ -4073,10 +4317,11 @@ function inSentence(word) {
|
|
|
4073
4317
|
/** The tabs shared by a collection's screens, above the screen itself. */
|
|
4074
4318
|
function CollectionLayout({ collection, tab, children }) {
|
|
4075
4319
|
const t = useStrings();
|
|
4320
|
+
const owner = useCanChangeSettings();
|
|
4076
4321
|
const tabs = [
|
|
4077
4322
|
["entries", "collection.tab.entries"],
|
|
4078
|
-
...collection.settings.path ? [["template", "collection.tab.template"]] : [],
|
|
4079
|
-
["settings", "collection.tab.settings"]
|
|
4323
|
+
...owner && collection.settings.path ? [["template", "collection.tab.template"]] : [],
|
|
4324
|
+
...owner ? [["settings", "collection.tab.settings"]] : []
|
|
4080
4325
|
];
|
|
4081
4326
|
return /* @__PURE__ */ jsxs("div", {
|
|
4082
4327
|
className: "gfa-layout-screen",
|
|
@@ -5247,13 +5492,14 @@ function NewCollectionDialog({ onClose }) {
|
|
|
5247
5492
|
function CollectionsScreen() {
|
|
5248
5493
|
const t = useStrings();
|
|
5249
5494
|
const { content } = useSiteContent();
|
|
5495
|
+
const owner = useCanChangeSettings();
|
|
5250
5496
|
const [creating, setCreating] = useState(false);
|
|
5251
5497
|
return /* @__PURE__ */ jsxs("div", {
|
|
5252
5498
|
className: "gfa-screen",
|
|
5253
5499
|
children: [
|
|
5254
5500
|
/* @__PURE__ */ jsxs("div", {
|
|
5255
5501
|
className: "gfa-screen-header",
|
|
5256
|
-
children: [/* @__PURE__ */ jsx("h1", { children: t("collections.title") }), /* @__PURE__ */ jsx(Button, {
|
|
5502
|
+
children: [/* @__PURE__ */ jsx("h1", { children: t("collections.title") }), owner && /* @__PURE__ */ jsx(Button, {
|
|
5257
5503
|
variant: "primary",
|
|
5258
5504
|
onClick: () => setCreating(true),
|
|
5259
5505
|
children: t("collections.new")
|
|
@@ -8957,9 +9203,10 @@ function MovePageDialog({ page, onClose }) {
|
|
|
8957
9203
|
const t = useStrings();
|
|
8958
9204
|
const { publish } = useAdmin();
|
|
8959
9205
|
const { content } = useSiteContent();
|
|
9206
|
+
const owner = useCanChangeSettings();
|
|
8960
9207
|
const [address, setAddress] = useState(page.path);
|
|
8961
9208
|
const [updateLinks, setUpdateLinks] = useState(true);
|
|
8962
|
-
const canRedirect = page.path !== "/";
|
|
9209
|
+
const canRedirect = page.path !== "/" && owner;
|
|
8963
9210
|
const [redirect, setRedirect] = useState(canRedirect);
|
|
8964
9211
|
const [submitted, setSubmitted] = useState(false);
|
|
8965
9212
|
const [busy, setBusy] = useState(false);
|
|
@@ -9302,7 +9549,7 @@ async function previousRelease(versions, current) {
|
|
|
9302
9549
|
/** The site's Goodfellow release, the fixes and releases it can update to, and its nightly update job. */
|
|
9303
9550
|
function UpdatesScreen() {
|
|
9304
9551
|
const t = useStrings();
|
|
9305
|
-
const { updates, demo } = useAdmin();
|
|
9552
|
+
const { updates, demo, contentOnly } = useAdmin();
|
|
9306
9553
|
return /* @__PURE__ */ jsxs("div", {
|
|
9307
9554
|
className: "gfa-screen",
|
|
9308
9555
|
children: [
|
|
@@ -9313,7 +9560,7 @@ function UpdatesScreen() {
|
|
|
9313
9560
|
/* @__PURE__ */ jsx(SettingsTabs, { tab: "updates" }),
|
|
9314
9561
|
/* @__PURE__ */ jsxs("div", {
|
|
9315
9562
|
className: "gfa-form",
|
|
9316
|
-
children: [/* @__PURE__ */ jsx("p", { children: t("updates.intro") }), demo ? /* @__PURE__ */ jsx("p", { children: t("updates.demo") }) : updates ? /* @__PURE__ */ jsx(UpdatesPanel, {}) : /* @__PURE__ */ jsx("p", { children: t("updates.local") })]
|
|
9563
|
+
children: [/* @__PURE__ */ jsx("p", { children: t("updates.intro") }), demo ? /* @__PURE__ */ jsx("p", { children: t("updates.demo") }) : contentOnly && !updates ? /* @__PURE__ */ jsx("p", { children: t("updates.contentOnly") }) : updates ? /* @__PURE__ */ jsx(UpdatesPanel, {}) : /* @__PURE__ */ jsx("p", { children: t("updates.local") })]
|
|
9317
9564
|
})
|
|
9318
9565
|
]
|
|
9319
9566
|
});
|
|
@@ -9331,8 +9578,9 @@ function UpdatesPanel() {
|
|
|
9331
9578
|
const load = useCallback(async () => {
|
|
9332
9579
|
if (!updates) return;
|
|
9333
9580
|
try {
|
|
9581
|
+
const code = updates.code;
|
|
9334
9582
|
const [packageJson, settingsText, metadata, setup, last, people] = await Promise.all([
|
|
9335
|
-
versions?.readAt("package.json", revision),
|
|
9583
|
+
code ? code.read("package.json").catch(() => void 0) : versions?.readAt("package.json", revision),
|
|
9336
9584
|
readFile(UPDATES_FILE),
|
|
9337
9585
|
window.fetch(`${NPM_REGISTRY}/${VERSION_PACKAGE}`, { headers: { accept: "application/vnd.npm.install-v1+json" } }).then((response) => response.ok ? response.json() : void 0).catch(() => void 0),
|
|
9338
9586
|
updates.setup(),
|
|
@@ -9341,7 +9589,8 @@ function UpdatesPanel() {
|
|
|
9341
9589
|
]);
|
|
9342
9590
|
const current = packageJson ? siteVersion(packageJson) : void 0;
|
|
9343
9591
|
const settings = parseUpdateSettings(settingsText);
|
|
9344
|
-
const
|
|
9592
|
+
const history = code ?? versions;
|
|
9593
|
+
const previous = current && history ? await previousRelease(history, current).catch(() => void 0) : void 0;
|
|
9345
9594
|
setLoaded({
|
|
9346
9595
|
current,
|
|
9347
9596
|
previous,
|
|
@@ -9387,6 +9636,20 @@ function UpdatesPanel() {
|
|
|
9387
9636
|
setBusy(false);
|
|
9388
9637
|
};
|
|
9389
9638
|
const start = (target) => act(() => updates?.start(target) ?? Promise.resolve(), () => setStarted(true));
|
|
9639
|
+
/**
|
|
9640
|
+
* Goes back to an older release. Where the code is kept apart from the
|
|
9641
|
+
* content, the update can't change the content's settings, so the release
|
|
9642
|
+
* the site leaves is skipped here, as the update does elsewhere.
|
|
9643
|
+
*/
|
|
9644
|
+
const goBack = (target, settings, current) => act(async () => {
|
|
9645
|
+
await updates?.start(target);
|
|
9646
|
+
if (!updates?.code || settings.skip.includes(current)) return;
|
|
9647
|
+
const result = await publish([updateSettingsFileChange({
|
|
9648
|
+
...settings,
|
|
9649
|
+
skip: [...settings.skip, current]
|
|
9650
|
+
})], t("updates.back.skip", { version: current }));
|
|
9651
|
+
if (!result.ok) throw new SaveError(result.error);
|
|
9652
|
+
}, () => setStarted(true));
|
|
9390
9653
|
const saveSettings = (settings, message) => act(async () => {
|
|
9391
9654
|
const result = await publish([updateSettingsFileChange(settings)], message);
|
|
9392
9655
|
if (!result.ok) throw new SaveError(result.error);
|
|
@@ -9403,7 +9666,20 @@ function UpdatesPanel() {
|
|
|
9403
9666
|
const { current, previous, settings, available, setup, last, owner } = loaded;
|
|
9404
9667
|
const needsToken = problem instanceof UpdatesError && problem.problem === "not-allowed" && ownerAccess;
|
|
9405
9668
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9406
|
-
/* @__PURE__ */
|
|
9669
|
+
updates.code && /* @__PURE__ */ jsxs("p", { children: [
|
|
9670
|
+
t("updates.code", { place: updates.code.repo }),
|
|
9671
|
+
" ",
|
|
9672
|
+
/* @__PURE__ */ jsx("a", {
|
|
9673
|
+
href: updates.code.webUrl,
|
|
9674
|
+
target: "_blank",
|
|
9675
|
+
rel: "noreferrer",
|
|
9676
|
+
children: t("updates.code.link", { host })
|
|
9677
|
+
})
|
|
9678
|
+
] }),
|
|
9679
|
+
setup === "no-access" ? /* @__PURE__ */ jsx("p", {
|
|
9680
|
+
className: "gfa-notice",
|
|
9681
|
+
children: t("updates.noAccess", { place: updates.code?.repo ?? "" })
|
|
9682
|
+
}) : /* @__PURE__ */ jsx("p", { children: current ? t("updates.current", { version: current }) : t("updates.unknownVersion") }),
|
|
9407
9683
|
/* @__PURE__ */ jsx(OwnerTokenActive, { onForget: () => void load() }),
|
|
9408
9684
|
setup === "missing" && /* @__PURE__ */ jsx("p", {
|
|
9409
9685
|
className: "gfa-notice",
|
|
@@ -9417,7 +9693,7 @@ function UpdatesPanel() {
|
|
|
9417
9693
|
children: t("updates.enable")
|
|
9418
9694
|
})]
|
|
9419
9695
|
}),
|
|
9420
|
-
current && setup !== "missing" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9696
|
+
current && setup !== "missing" && setup !== "no-access" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9421
9697
|
/* @__PURE__ */ jsx("h2", {
|
|
9422
9698
|
className: "gfa-section-title",
|
|
9423
9699
|
children: t("updates.fixes.title")
|
|
@@ -9480,7 +9756,7 @@ function UpdatesPanel() {
|
|
|
9480
9756
|
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, {
|
|
9481
9757
|
disabled: busy || running,
|
|
9482
9758
|
onClick: () => {
|
|
9483
|
-
if (window.confirm(t("updates.back.confirm", { version: previous })))
|
|
9759
|
+
if (window.confirm(t("updates.back.confirm", { version: previous }))) goBack(previous, settings, current ?? "");
|
|
9484
9760
|
},
|
|
9485
9761
|
children: t("updates.back.button", { version: previous })
|
|
9486
9762
|
}) })
|
|
@@ -9586,9 +9862,32 @@ function LoadError({ error }) {
|
|
|
9586
9862
|
})]
|
|
9587
9863
|
});
|
|
9588
9864
|
}
|
|
9865
|
+
/** Shown in place of the Blocks screen where the site's code is kept in a repository of its own. */
|
|
9866
|
+
function BlocksInCode() {
|
|
9867
|
+
const t = useStrings();
|
|
9868
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9869
|
+
className: "gfa-screen",
|
|
9870
|
+
children: [/* @__PURE__ */ jsx("h1", { children: t("blocks.contentOnly") }), /* @__PURE__ */ jsx("p", {
|
|
9871
|
+
className: "gfa-hint",
|
|
9872
|
+
children: t("blocks.contentOnlyHint")
|
|
9873
|
+
})]
|
|
9874
|
+
});
|
|
9875
|
+
}
|
|
9876
|
+
/** Shown in place of screens only owners use, where the site's own sign-in says someone is an editor. */
|
|
9877
|
+
function OwnersOnly() {
|
|
9878
|
+
const t = useStrings();
|
|
9879
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9880
|
+
className: "gfa-screen",
|
|
9881
|
+
children: [/* @__PURE__ */ jsx("h1", { children: t("owners.only") }), /* @__PURE__ */ jsx("p", {
|
|
9882
|
+
className: "gfa-hint",
|
|
9883
|
+
children: t("owners.onlyHint")
|
|
9884
|
+
})]
|
|
9885
|
+
});
|
|
9886
|
+
}
|
|
9589
9887
|
function CollectionRoute({ id, sub, slug }) {
|
|
9590
9888
|
const t = useStrings();
|
|
9591
9889
|
const { content } = useSiteContent();
|
|
9890
|
+
const owner = useCanChangeSettings();
|
|
9592
9891
|
if (sub === "edit") return /* @__PURE__ */ jsx(EntryEditorScreen, {
|
|
9593
9892
|
collectionId: id,
|
|
9594
9893
|
slug
|
|
@@ -9601,6 +9900,7 @@ function CollectionRoute({ id, sub, slug }) {
|
|
|
9601
9900
|
children: t("collection.back")
|
|
9602
9901
|
})]
|
|
9603
9902
|
});
|
|
9903
|
+
if ((sub === "template" || sub === "settings") && !owner) return /* @__PURE__ */ jsx(OwnersOnly, {});
|
|
9604
9904
|
if (sub === "template") return /* @__PURE__ */ jsx(TemplateScreen, { collection });
|
|
9605
9905
|
if (sub === "settings") return /* @__PURE__ */ jsx(CollectionSettingsScreen, { collection }, collection.id);
|
|
9606
9906
|
return /* @__PURE__ */ jsx(EntriesScreen, { collection });
|
|
@@ -9608,8 +9908,11 @@ function CollectionRoute({ id, sub, slug }) {
|
|
|
9608
9908
|
function Screen() {
|
|
9609
9909
|
const { segments, params } = useRoute();
|
|
9610
9910
|
const [section, sub, tab] = segments;
|
|
9911
|
+
const owner = useCanChangeSettings();
|
|
9912
|
+
const { contentOnly } = useAdmin();
|
|
9913
|
+
if ((section === "blocks" || section === "settings") && !owner) return /* @__PURE__ */ jsx(OwnersOnly, {});
|
|
9611
9914
|
if (section === "media") return /* @__PURE__ */ jsx(MediaScreen, {});
|
|
9612
|
-
if (section === "blocks") return /* @__PURE__ */ jsx(BlocksScreen, {});
|
|
9915
|
+
if (section === "blocks") return contentOnly ? /* @__PURE__ */ jsx(BlocksInCode, {}) : /* @__PURE__ */ jsx(BlocksScreen, {});
|
|
9613
9916
|
if (section === "collections") return sub ? /* @__PURE__ */ jsx(CollectionRoute, {
|
|
9614
9917
|
id: sub,
|
|
9615
9918
|
sub: tab,
|
|
@@ -9700,7 +10003,7 @@ function AccountMenu() {
|
|
|
9700
10003
|
title: t("account.signedInAs", { name: user.login }),
|
|
9701
10004
|
children: user.name || user.login
|
|
9702
10005
|
}),
|
|
9703
|
-
/* @__PURE__ */ jsx(Button, {
|
|
10006
|
+
account.signOut && /* @__PURE__ */ jsx(Button, {
|
|
9704
10007
|
variant: "ghost",
|
|
9705
10008
|
onClick: account.signOut,
|
|
9706
10009
|
children: t("account.signOut")
|
|
@@ -9740,7 +10043,8 @@ function versionsSection(file) {
|
|
|
9740
10043
|
}
|
|
9741
10044
|
function Shell() {
|
|
9742
10045
|
const t = useStrings();
|
|
9743
|
-
const { state, siteUrl } = useAdmin();
|
|
10046
|
+
const { state, siteUrl, contentOnly } = useAdmin();
|
|
10047
|
+
const owner = useCanChangeSettings();
|
|
9744
10048
|
const { segments, params } = useRoute();
|
|
9745
10049
|
const [section] = segments;
|
|
9746
10050
|
const current = section === "versions" ? versionsSection(params.get("file") ?? "") : section === "layout" || section === "settings" || section === "collections" || section === "media" || section === "blocks" ? section : "pages";
|
|
@@ -9778,12 +10082,12 @@ function Shell() {
|
|
|
9778
10082
|
"aria-current": current === "layout" ? "page" : void 0,
|
|
9779
10083
|
children: t("nav.layout")
|
|
9780
10084
|
}),
|
|
9781
|
-
/* @__PURE__ */ jsx(AppLink, {
|
|
10085
|
+
owner && !contentOnly && /* @__PURE__ */ jsx(AppLink, {
|
|
9782
10086
|
href: "#/blocks",
|
|
9783
10087
|
"aria-current": current === "blocks" ? "page" : void 0,
|
|
9784
10088
|
children: t("nav.blocks")
|
|
9785
10089
|
}),
|
|
9786
|
-
/* @__PURE__ */ jsx(AppLink, {
|
|
10090
|
+
owner && /* @__PURE__ */ jsx(AppLink, {
|
|
9787
10091
|
href: "#/settings/general",
|
|
9788
10092
|
"aria-current": current === "settings" ? "page" : void 0,
|
|
9789
10093
|
children: t("nav.settings")
|
|
@@ -9825,7 +10129,7 @@ function MissingBackend() {
|
|
|
9825
10129
|
});
|
|
9826
10130
|
}
|
|
9827
10131
|
/** The admin panel. Render it on its own page, such as `/admin`. */
|
|
9828
|
-
function Admin({ config, store, host = config.backend, preview, siteUrl = "/", strings, registry }) {
|
|
10132
|
+
function Admin({ config, store, host = config.backend, server, preview, siteUrl = "/", strings, registry }) {
|
|
9829
10133
|
useApplyThemeChoice();
|
|
9830
10134
|
const demo = useMemo(() => config.demo ? createDemoStore(siteUrl, store) : void 0, [
|
|
9831
10135
|
config.demo,
|
|
@@ -9856,6 +10160,31 @@ function Admin({ config, store, host = config.backend, preview, siteUrl = "/", s
|
|
|
9856
10160
|
siteUrl,
|
|
9857
10161
|
registry,
|
|
9858
10162
|
children: /* @__PURE__ */ jsx(Shell, {})
|
|
10163
|
+
}) : server ? /* @__PURE__ */ jsx(AppSignInGate, {
|
|
10164
|
+
api: server,
|
|
10165
|
+
children: (backend, session, signOut) => /* @__PURE__ */ jsx(AdminProvider, {
|
|
10166
|
+
config,
|
|
10167
|
+
store: backend,
|
|
10168
|
+
account: {
|
|
10169
|
+
user: backend.user,
|
|
10170
|
+
hostName: host?.name ?? "",
|
|
10171
|
+
redirectAddressesListed: false,
|
|
10172
|
+
role: session.role,
|
|
10173
|
+
...session.ai.length > 0 && { ai: {
|
|
10174
|
+
server: new URL(`${server.replace(/\/+$/, "")}/ai`, location.href).href,
|
|
10175
|
+
services: session.ai
|
|
10176
|
+
} },
|
|
10177
|
+
...session.signOut && { signOut: () => {
|
|
10178
|
+
forgetAiKeys();
|
|
10179
|
+
signOut();
|
|
10180
|
+
} }
|
|
10181
|
+
},
|
|
10182
|
+
preview,
|
|
10183
|
+
siteUrl,
|
|
10184
|
+
registry,
|
|
10185
|
+
onSignInError: (error) => signOut(error),
|
|
10186
|
+
children: /* @__PURE__ */ jsx(Shell, {})
|
|
10187
|
+
})
|
|
9859
10188
|
}) : host ? /* @__PURE__ */ jsx(SignInGate, {
|
|
9860
10189
|
host,
|
|
9861
10190
|
children: (backend, signOut) => /* @__PURE__ */ jsx(AdminProvider, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodfellow-cms/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Goodfellow's admin panel: a visual editor for a site's pages, layout and settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodfellow",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"codemirror": "6.0.2",
|
|
50
50
|
"turndown": "7.2.4",
|
|
51
51
|
"turndown-plugin-gfm": "1.0.2",
|
|
52
|
-
"@goodfellow-cms/
|
|
53
|
-
"@goodfellow-cms/
|
|
54
|
-
"@goodfellow-cms/registry": "0.
|
|
55
|
-
"@goodfellow-cms/
|
|
52
|
+
"@goodfellow-cms/ai": "0.4.0",
|
|
53
|
+
"@goodfellow-cms/react": "0.4.0",
|
|
54
|
+
"@goodfellow-cms/registry": "0.4.0",
|
|
55
|
+
"@goodfellow-cms/core": "0.4.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@puckeditor/core": "0.23.0",
|