2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Refactored loading of Koenig-Lexical package (#18392)

refs https://ghost.slack.com/archives/C02G9E68C/p1695901801049219?thread_ts=1695035790.122589&cid=C02G9E68C

- DRY up all the fetching of Koenig-Lexical so we only do it from one place
- this will help when we switch to loading Koenig-Lexical from local assets

Co-authored-by: Jono Mingard <reason.koan@gmail.com>
This commit is contained in:
Daniel Lockyer 2023-09-28 17:43:12 +02:00 committed by GitHub
parent e4f518868e
commit 6e46e8b5ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 62 additions and 103 deletions

View file

@ -5,8 +5,8 @@ import NiceModal from '@ebay/nice-modal-react';
import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider';
import clsx from 'clsx';
import {DefaultHeaderTypes} from './utils/unsplash/UnsplashTypes';
import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
import {OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {ErrorBoundary as SentryErrorBoundary} from '@sentry/react';
import {Toaster} from 'react-hot-toast';
@ -20,6 +20,7 @@ interface AppProps {
darkMode?: boolean;
unsplashConfig: DefaultHeaderTypes
sentryDSN: string | null;
fetchKoenigLexical: FetchKoenigLexical;
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
@ -37,7 +38,7 @@ const queryClient = new QueryClient({
}
});
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false, unsplashConfig, sentryDSN, onUpdate, onInvalidate, onDelete}: AppProps) {
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false, unsplashConfig, fetchKoenigLexical, sentryDSN, onUpdate, onInvalidate, onDelete}: AppProps) {
const appClassName = clsx(
'admin-x-settings h-[100vh] w-full overflow-y-auto overflow-x-hidden',
darkMode && 'dark'
@ -46,7 +47,7 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, d
return (
<SentryErrorBoundary>
<QueryClientProvider client={queryClient}>
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes} sentryDSN={sentryDSN} unsplashConfig={unsplashConfig} zapierTemplates={zapierTemplates} onDelete={onDelete} onInvalidate={onInvalidate} onUpdate={onUpdate}>
<ServicesProvider fetchKoenigLexical={fetchKoenigLexical} ghostVersion={ghostVersion} officialThemes={officialThemes} sentryDSN={sentryDSN} unsplashConfig={unsplashConfig} zapierTemplates={zapierTemplates} onDelete={onDelete} onInvalidate={onInvalidate} onUpdate={onUpdate}>
<GlobalDataProvider>
<RoutingProvider externalNavigate={externalNavigate}>
<GlobalDirtyStateProvider>

View file

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/react';
import ErrorBoundary from '../ErrorBoundary';
import React, {Suspense, useCallback, useMemo} from 'react';
import {FetchKoenigLexical, useServices} from '../../../components/providers/ServiceProvider';
import {useFocusContext} from '../../providers/DesignSystemProvider';
export interface HtmlEditorProps {
@ -18,22 +19,12 @@ declare global {
}
}
const fetchKoenig = function ({editorUrl, editorVersion}: { editorUrl: string; editorVersion: string; }) {
const loadKoenig = function (fetchKoenigLexical: FetchKoenigLexical) {
let status = 'pending';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let response: any;
const fetchPackage = async () => {
if (window['@tryghost/koenig-lexical']) {
return window['@tryghost/koenig-lexical'];
}
await import(editorUrl.replace('{version}', editorVersion));
return window['@tryghost/koenig-lexical'];
};
const suspender = fetchPackage().then(
const suspender = fetchKoenigLexical().then(
(res) => {
status = 'success';
response = res;
@ -58,7 +49,7 @@ const fetchKoenig = function ({editorUrl, editorVersion}: { editorUrl: string; e
return {read};
};
type EditorResource = ReturnType<typeof fetchKoenig>;
type EditorResource = ReturnType<typeof loadKoenig>;
const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
editor,
@ -149,17 +140,14 @@ const KoenigWrapper: React.FC<HtmlEditorProps & { editor: EditorResource }> = ({
};
const HtmlEditor: React.FC<HtmlEditorProps & {
config: { editor: { url: string; version: string; } };
className?: string;
}> = ({
config,
className,
...props
}) => {
const editorResource = useMemo(() => fetchKoenig({
editorUrl: config.editor.url,
editorVersion: config.editor.version
}), [config.editor.url, config.editor.version]);
const {fetchKoenigLexical} = useServices();
const editorResource = useMemo(() => loadKoenig(fetchKoenigLexical), [fetchKoenigLexical]);
const {setFocusState} = useFocusContext();
// this is not ideal, we need to add a focus plugin inside the Koenig editor package to handle this properly
const handleFocus = () => {

View file

@ -5,15 +5,7 @@ import HtmlField from './HtmlField';
const meta = {
title: 'Global / Form / Htmlfield',
component: HtmlField,
tags: ['autodocs'],
args: {
config: {
editor: {
url: 'https://cdn.jsdelivr.net/ghost/koenig-lexical@~{version}/dist/koenig-lexical.umd.js',
version: '0.3'
}
}
}
tags: ['autodocs']
} satisfies Meta<typeof HtmlField>;
export default meta;

View file

@ -4,13 +4,7 @@ import HtmlEditor, {HtmlEditorProps} from './HtmlEditor';
import React from 'react';
import clsx from 'clsx';
export type EditorConfig = { editor: { url: string; version: string; } }
export type HtmlFieldProps = HtmlEditorProps & {
/**
* Should be passed the Ghost instance config to get the editor JS URL
*/
config: EditorConfig;
title?: string;
hideTitle?: boolean;
error?: boolean;

View file

@ -34,4 +34,4 @@ const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({children}) =
);
};
export default DesignSystemProvider;
export default DesignSystemProvider;

View file

@ -12,6 +12,8 @@ export type OfficialTheme = {
url?: string;
};
export type FetchKoenigLexical = () => Promise<any>
interface ServicesContextProps {
ghostVersion: string
officialThemes: OfficialTheme[];
@ -22,6 +24,7 @@ interface ServicesContextProps {
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
fetchKoenigLexical: FetchKoenigLexical;
}
interface ServicesProviderProps {
@ -34,6 +37,7 @@ interface ServicesProviderProps {
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
fetchKoenigLexical: FetchKoenigLexical;
}
const ServicesContext = createContext<ServicesContextProps>({
@ -51,10 +55,11 @@ const ServicesContext = createContext<ServicesContextProps>({
sentryDSN: null,
onUpdate: () => {},
onInvalidate: () => {},
onDelete: () => {}
onDelete: () => {},
fetchKoenigLexical: async () => {}
});
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, zapierTemplates, officialThemes, unsplashConfig, sentryDSN, onUpdate, onInvalidate, onDelete}) => {
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, zapierTemplates, officialThemes, unsplashConfig, sentryDSN, onUpdate, onInvalidate, onDelete, fetchKoenigLexical}) => {
const search = useSearchService();
return (
@ -67,7 +72,8 @@ const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersi
sentryDSN,
onUpdate,
onInvalidate,
onDelete
onDelete,
fetchKoenigLexical
}}>
{children}
</ServicesContext.Provider>

View file

@ -46,7 +46,7 @@ const Sidebar: React.FC<{
}> = ({newsletter, onlyOne, updateNewsletter, validate, errors, clearError}) => {
const {mutateAsync: editNewsletter} = useEditNewsletter();
const limiter = useLimiter();
const {settings, siteData, config} = useGlobalData();
const {settings, siteData} = useGlobalData();
const [membersSupportAddress, icon] = getSettingValues<string>(settings, ['members_support_address', 'icon']);
const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings');
@ -406,7 +406,6 @@ const Sidebar: React.FC<{
/>
</ToggleGroup>
<HtmlField
config={config}
hint='Any extra information or legal text'
nodes='MINIMAL_NODES'
placeholder=' '

View file

@ -124,7 +124,6 @@ const SignupOptions: React.FC<{
)}
<HtmlField
config={config}
error={Boolean(errors.portal_signup_terms_html)}
hint={errors.portal_signup_terms_html || <>Recommended: <strong>115</strong> characters. You&apos;ve used <strong className="text-green">{signupTermsLength}</strong></>}
nodes='MINIMAL_NODES'

View file

@ -38,8 +38,6 @@ const Sidebar: React.FC<SidebarProps> = ({
paidMembersEnabled,
onBlur
}) => {
const {config} = useGlobalData();
const visibilityCheckboxes = [
{
label: 'Logged out visitors',
@ -70,7 +68,6 @@ const Sidebar: React.FC<SidebarProps> = ({
return (
<Form>
<HtmlField
config={config}
nodes='MINIMAL_NODES'
placeholder='Highlight breaking news, offers or updates'
title='Announcement'
@ -201,10 +198,10 @@ const AnnouncementBarModal: React.FC = () => {
break;
}
const preview = <AnnouncementBarPreview
announcementBackgroundColor={announcementBackgroundColor}
announcementContent={announcementContent}
url={selectedTabURL}
const preview = <AnnouncementBarPreview
announcementBackgroundColor={announcementBackgroundColor}
announcementContent={announcementContent}
url={selectedTabURL}
visibility={visibilitySettings}
/>;

View file

@ -8,6 +8,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App
externalNavigate={() => {}}
fetchKoenigLexical={() => {
return Promise.resolve();
}}
ghostVersion='5.x'
officialThemes={[{
name: 'Casper',

View file

@ -2,6 +2,7 @@ import * as Sentry from '@sentry/ember';
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
import config from 'ghost-admin/config/environment';
import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
@ -413,6 +414,7 @@ export default class AdminXSettings extends Component {
darkMode={this.feature.nightShift}
unsplashConfig={defaultUnsplashHeaders}
sentry={this.config.sentry_dsn ? Sentry : undefined}
fetchKoenigLexical={fetchKoenigLexical}
onUpdate={this.onUpdate}
onInvalidate={this.onInvalidate}
onDelete={this.onDelete}

View file

@ -1,6 +1,7 @@
import * as Sentry from '@sentry/ember';
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
import {inject as service} from '@ember/service';
@ -25,34 +26,11 @@ class ErrorHandler extends React.Component {
}
}
const fetchKoenig = function () {
const loadKoenig = function () {
let status = 'pending';
let response;
const fetchPackage = async () => {
if (window['@tryghost/koenig-lexical']) {
return window['@tryghost/koenig-lexical'];
}
// the manual specification of the protocol in the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.GhostAdmin || window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const urlTemplate = GhostAdmin.__container__.lookup('config:main').editor?.url;
const urlVersion = GhostAdmin.__container__.lookup('config:main').editor?.version;
const url = new URL(urlTemplate.replace('{version}', urlVersion));
if (url.protocol === 'http:') {
await import(`http://${url.host}${url.pathname}`);
} else {
await import(`https://${url.host}${url.pathname}`);
}
return window['@tryghost/koenig-lexical'];
};
const suspender = fetchPackage().then(
const suspender = fetchKoenigLexical().then(
(res) => {
status = 'success';
response = res;
@ -77,7 +55,7 @@ const fetchKoenig = function () {
return {read};
};
const editorResource = fetchKoenig();
const editorResource = loadKoenig();
const KoenigComposer = (props) => {
const {KoenigComposer: _KoenigComposer, MINIMAL_NODES: _MINIMAL_NODES} = editorResource.read();

View file

@ -2,6 +2,7 @@ import * as Sentry from '@sentry/ember';
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
import fetch from 'fetch';
import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {action} from '@ember/object';
import {inject} from 'ghost-admin/decorators/inject';
@ -60,34 +61,11 @@ class ErrorHandler extends React.Component {
}
}
const fetchKoenig = function () {
const loadKoenig = function () {
let status = 'pending';
let response;
const fetchPackage = async () => {
if (window['@tryghost/koenig-lexical']) {
return window['@tryghost/koenig-lexical'];
}
// the manual specification of the protocol in the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.GhostAdmin || window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const urlTemplate = GhostAdmin.__container__.lookup('config:main').editor?.url;
const urlVersion = GhostAdmin.__container__.lookup('config:main').editor?.version;
const url = new URL(urlTemplate.replace('{version}', urlVersion));
if (url.protocol === 'http:') {
await import(`http://${url.host}${url.pathname}`);
} else {
await import(`https://${url.host}${url.pathname}`);
}
return window['@tryghost/koenig-lexical'];
};
const suspender = fetchPackage().then(
const suspender = fetchKoenigLexical().then(
(res) => {
status = 'success';
response = res;
@ -112,7 +90,7 @@ const fetchKoenig = function () {
return {read};
};
const editorResource = fetchKoenig();
const editorResource = loadKoenig();
const KoenigComposer = (props) => {
const {KoenigComposer: _KoenigComposer} = editorResource.read();

View file

@ -0,0 +1,22 @@
export default async function fetchKoenigLexical() {
if (window['@tryghost/koenig-lexical']) {
return window['@tryghost/koenig-lexical'];
}
// the manual specification of the protocol in the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.GhostAdmin || window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const urlTemplate = GhostAdmin.__container__.lookup('config:main').editor?.url;
const urlVersion = GhostAdmin.__container__.lookup('config:main').editor?.version;
const url = new URL(urlTemplate.replace('{version}', urlVersion));
if (url.protocol === 'http:') {
await import(`http://${url.host}${url.pathname}`);
} else {
await import(`https://${url.host}${url.pathname}`);
}
return window['@tryghost/koenig-lexical'];
}