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:
parent
e4f518868e
commit
6e46e8b5ba
14 changed files with 62 additions and 103 deletions
|
@ -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>
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -34,4 +34,4 @@ const DesignSystemProvider: React.FC<DesignSystemProviderProps> = ({children}) =
|
|||
);
|
||||
};
|
||||
|
||||
export default DesignSystemProvider;
|
||||
export default DesignSystemProvider;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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=' '
|
||||
|
|
|
@ -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've used <strong className="text-green">{signupTermsLength}</strong></>}
|
||||
nodes='MINIMAL_NODES'
|
||||
|
|
|
@ -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}
|
||||
/>;
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
22
ghost/admin/app/utils/fetch-koenig-lexical.js
Normal file
22
ghost/admin/app/utils/fetch-koenig-lexical.js
Normal 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'];
|
||||
}
|
Loading…
Reference in a new issue