0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Separated common AdminX logic into a reusable package (#18919)

refs https://github.com/TryGhost/Product/issues/4123

---

<!-- Leave the line below if you'd like GitHub Copilot to generate a
summary from your commit -->
<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at a420f0b</samp>

This pull request moves most of the API-related types and functions from
the `admin-x-settings` package to the `admin-x-framework` package, which
is a new library of common utilities and hooks for the admin-x apps. It
also adds some configuration files, such as `.eslintrc.cjs` and
`.gitignore`, to the `admin-x-framework` package. Additionally, it
exports the `FetchKoenigLexical` type from the `admin-x-design-system`
package, which is used by the `HtmlEditor` component.
This commit is contained in:
Jono M 2023-11-14 13:50:08 +00:00 committed by GitHub
parent 370c6b465b
commit 94a181ce2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
169 changed files with 1078 additions and 747 deletions

View file

@ -51,8 +51,8 @@ const COMMAND_TYPESCRIPT = {
};
const COMMANDS_ADMINX = [{
name: 'adminXDS',
command: 'nx watch --projects=apps/admin-x-design-system -- nx run \\$NX_PROJECT_NAME:build --skip-nx-cache',
name: 'adminXDeps',
command: 'nx watch --projects=apps/admin-x-design-system,apps/admin-x-framework -- nx run \\$NX_PROJECT_NAME:build --skip-nx-cache',
cwd: path.resolve(__dirname, '../..'),
prefixColor: '#C35831',
env: {}

View file

@ -1,8 +1,9 @@
import type {ReactCodeMirrorRef} from '@uiw/react-codemirror';
import React, {Suspense, forwardRef} from 'react';
import type {CodeEditorProps} from './CodeEditorView';
import type {FetchKoenigLexical} from './HtmlEditor';
export type {CodeEditorProps};
export type {CodeEditorProps, FetchKoenigLexical};
// Imported asynchronously to avoid including CodeMirror in the main bundle
const CodeEditorView = React.lazy(() => import('./CodeEditorView'));

View file

@ -17,7 +17,7 @@ export type {CheckboxProps} from './global/form/Checkbox';
export {default as CheckboxGroup} from './global/form/CheckboxGroup';
export type {CheckboxGroupProps} from './global/form/CheckboxGroup';
export {default as CodeEditor} from './global/form/CodeEditor';
export type {CodeEditorProps} from './global/form/CodeEditor';
export type {CodeEditorProps, FetchKoenigLexical} from './global/form/CodeEditor';
export {default as ColorIndicator} from './global/form/ColorIndicator';
export type {ColorIndicatorProps} from './global/form/ColorIndicator';
export {default as ColorPicker} from './global/form/ColorPicker';

View file

@ -0,0 +1,41 @@
module.exports = {
extends: [
'plugin:ghost/ts',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
plugins: [
'ghost',
'react-refresh',
'tailwindcss'
],
settings: {
react: {
version: 'detect'
}
},
rules: {
// suppress errors for missing 'import React' in JSX files, as we don't need it
'react/react-in-jsx-scope': 'off',
// ignore prop-types for now
'react/prop-types': 'off',
'react/jsx-sort-props': ['error', {
reservedFirst: true,
callbacksLast: true,
shorthandLast: true,
locale: 'en'
}],
'react/button-has-type': 'error',
'react/no-array-index-key': 'error',
'react/jsx-key': 'off',
'tailwindcss/classnames-order': ['error', {config: 'tailwind.config.cjs'}],
'tailwindcss/enforces-negative-arbitrary-values': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/enforces-shorthand': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/migration-from-tailwind-2': ['warn', {config: 'tailwind.config.cjs'}],
'tailwindcss/no-arbitrary-value': 'off',
'tailwindcss/no-custom-classname': 'off',
'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}]
}
};

2
apps/admin-x-framework/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
es
types

View file

@ -0,0 +1,80 @@
{
"name": "@tryghost/admin-x-framework",
"type": "module",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Ghost/tree/main/apps/admin-x-framework",
"author": "Ghost Foundation",
"private": true,
"exports": {
".": {
"import": "./es/index.js",
"types": "./types/index.d.ts"
},
"./errors": {
"import": "./es/errors.js",
"types": "./types/errors.d.ts"
},
"./helpers": {
"import": "./es/helpers.js",
"types": "./types/helpers.d.ts"
},
"./hooks": {
"import": "./es/hooks.js",
"types": "./types/hooks.d.ts"
},
"./routing": {
"import": "./es/routing.js",
"types": "./types/routing.d.ts"
},
"./api/*": {
"import": "./es/api/*.js",
"types": "./types/api/*.d.ts"
}
},
"sideEffects": false,
"scripts": {
"build": "vite build && tsc -p tsconfig.declaration.json",
"prepare": "yarn build",
"test": "yarn test:types",
"test:types": "tsc --noEmit",
"lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache",
"lint": "yarn lint:code && (yarn lint:test || echo \"TODO ADD TESTS TO LINT\")",
"lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache"
},
"files": [
"es",
"types"
],
"devDependencies": {
"@vitejs/plugin-react": "4.1.1",
"c8": "8.0.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-refresh": "0.4.3",
"mocha": "10.2.0",
"sinon": "17.0.0",
"ts-node": "10.9.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.2.2",
"vite": "4.5.0"
},
"dependencies": {
"@sentry/react": "7.78.0",
"@tanstack/react-query": "4.36.1",
"@tryghost/admin-x-design-system": "0.0.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"nx": {
"targets": {
"build": {
"dependsOn": [
"build",
{"projects": ["@tryghost/admin-x-design-system"], "target": "build"}
]
}
}
}
}

View file

@ -1,7 +1,7 @@
import {ExternalLink, InternalLink} from '../components/providers/RoutingProvider';
import {InfiniteData} from '@tanstack/react-query';
import {JSONObject} from './config';
import {ExternalLink, InternalLink} from '../providers/RoutingProvider';
import {Meta, createInfiniteQuery} from '../utils/api/hooks';
import {JSONObject} from './config';
// Types

View file

@ -1,5 +1,5 @@
import {IntegrationsResponseType, integrationsDataType} from './integrations';
import {createMutation} from '../utils/api/hooks';
import {IntegrationsResponseType, integrationsDataType} from './integrations';
// Types

View file

@ -0,0 +1,32 @@
import {useQuery} from '@tanstack/react-query';
import {useEffect, useMemo} from 'react';
import useHandleError from '../hooks/useHandleError';
import {apiUrl, useFetchApi} from '../utils/api/fetchApi';
import {User} from './users';
export const usersDataType = 'UsersResponseType';
// Special case where we can't use createQuery because this is used by usePermissions, which is then used by createQuery
export const useCurrentUser = () => {
const url = apiUrl('/users/me/', {include: 'roles'});
const fetchApi = useFetchApi();
const handleError = useHandleError();
const result = useQuery({
queryKey: [usersDataType, url],
queryFn: () => fetchApi(url)
});
const data = useMemo<User | null>(() => result.data?.users?.[0], [result.data]);
useEffect(() => {
if (result.error) {
handleError(result.error);
}
}, [handleError, result.error]);
return {
...result,
data
};
};

View file

@ -1,5 +1,5 @@
import {Setting} from './settings';
import {createMutation, createQuery} from '../utils/api/hooks';
import {Setting} from './settings';
type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } |

View file

@ -1,5 +1,5 @@
import {APIKey} from './apiKeys';
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
import {APIKey} from './apiKeys';
import {Webhook} from './webhooks';
// Types

View file

@ -1,5 +1,5 @@
import {Config} from './config';
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
import {Config} from './config';
// Types
@ -49,6 +49,11 @@ export const useDeleteStripeSettings = createMutation<unknown, null>({
invalidateQueries: {dataType}
});
export const useTestSlack = createMutation<unknown, null>({
method: 'POST',
path: () => '/slack/test/'
});
// Helpers
export function humanizeSettingKey(key: string) {

View file

@ -1,4 +1,3 @@
import {OfficialTheme} from '../components/providers/ServiceProvider';
import {createMutation, createQuery} from '../utils/api/hooks';
import {customThemeSettingsDataType} from './customThemeSettings';
@ -133,15 +132,15 @@ export function isActiveTheme(theme: Theme): boolean {
return theme.active;
}
export function isDefaultTheme(theme: Theme | OfficialTheme): boolean {
export function isDefaultTheme(theme: {name: string}): boolean {
return theme.name.toLowerCase() === 'source';
}
export function isLegacyTheme(theme: Theme | OfficialTheme): boolean {
export function isLegacyTheme(theme: {name: string}): boolean {
return theme.name.toLowerCase() === 'casper';
}
export function isDefaultOrLegacyTheme(theme: Theme | OfficialTheme): boolean {
export function isDefaultOrLegacyTheme(theme: {name: string}): boolean {
return isDefaultTheme(theme) || isLegacyTheme(theme);
}

View file

@ -1,7 +1,8 @@
import {InfiniteData} from '@tanstack/react-query';
import {Meta, createInfiniteQuery, createMutation, createQuery} from '../utils/api/hooks';
import {UserRole} from './roles';
import {Meta, createInfiniteQuery, createMutation} from '../utils/api/hooks';
import {deleteFromQueryCache, updateQueryCache} from '../utils/api/updateQueries';
import {usersDataType} from './currentUser';
import {UserRole} from './roles';
// Types
@ -62,7 +63,7 @@ export interface DeleteUserResponse {
// Requests
const dataType = 'UsersResponseType';
const dataType = usersDataType;
export const useBrowseUsers = createInfiniteQuery<UsersResponseType & {isEnd: boolean}>({
dataType,
@ -85,13 +86,6 @@ export const useBrowseUsers = createInfiniteQuery<UsersResponseType & {isEnd: bo
}
});
export const useCurrentUser = createQuery<User>({
dataType,
path: '/users/me/',
defaultSearchParams: {include: 'roles'},
returnData: originalData => (originalData as UsersResponseType).users?.[0]
});
export const useEditUser = createMutation<UsersResponseType, User>({
method: 'PUT',
path: user => `/users/${user.id}/`,

View file

@ -1,5 +1,5 @@
import {IntegrationsResponseType, integrationsDataType} from './integrations';
import {Meta, createMutation} from '../utils/api/hooks';
import {IntegrationsResponseType, integrationsDataType} from './integrations';
// Types

View file

@ -0,0 +1,2 @@
export * from './utils/errors';

View file

@ -0,0 +1,2 @@
export * from './utils/helpers';

View file

@ -0,0 +1,4 @@
export {default as useFilterableApi} from './hooks/useFilterableApi';
export {default as useHandleError} from './hooks/useHandleError';
export {usePermission} from './hooks/usePermissions';

View file

@ -1,5 +1,6 @@
import {Meta, apiUrl, useFetchApi} from '../utils/api/hooks';
import {useRef} from 'react';
import {apiUrl, useFetchApi} from '../utils/api/fetchApi';
import {Meta} from '../utils/api/hooks';
const escapeNqlString = (value: string) => {
return '\'' + value.replace(/'/g, '\\\'') + '\'';

View file

@ -1,9 +1,9 @@
import * as Sentry from '@sentry/react';
import toast from 'react-hot-toast';
import {APIError, JSONError, ValidationError} from '../errors';
import {showToast} from '@tryghost/admin-x-design-system';
import {useCallback} from 'react';
import {useSentryDSN} from '../../components/providers/ServiceProvider';
import toast from 'react-hot-toast';
import {useFramework} from '../providers/FrameworkProvider';
import {APIError, JSONError, ValidationError} from '../utils/errors';
/**
* Generic error handling for API calls. This is enabled by default for queries (can be disabled by
@ -11,7 +11,7 @@ import {useSentryDSN} from '../../components/providers/ServiceProvider';
* errors in order to handle anything unexpected.
*/
const useHandleError = () => {
const sentryDSN = useSentryDSN();
const {sentryDSN} = useFramework();
/**
* @param error Thrown error.
@ -20,7 +20,7 @@ const useHandleError = () => {
* so this toast is intended as a worst-case fallback message when we don't know what else to do.
*
*/
type HandleErrorReturnType = void | any;
type HandleErrorReturnType = void | any; // eslint-disable-line @typescript-eslint/no-explicit-any
const handleError = useCallback((error: unknown, {withToast = true}: {withToast?: boolean} = {}) : HandleErrorReturnType => {
// eslint-disable-next-line no-console
console.error(error);

View file

@ -1,8 +1,8 @@
import {useCurrentUser} from '../api/currentUser';
import {UserRoleType} from '../api/roles';
import {useGlobalData} from '../components/providers/GlobalDataProvider';
export const usePermission = (userRoles:string[]) => {
const {currentUser} = useGlobalData();
const {data: currentUser} = useCurrentUser();
const currentUserRoles = currentUser?.roles.map(role => role.name);
if (!currentUserRoles) {
return false;

View file

@ -0,0 +1,6 @@
export {default as FrameworkProvider, useFramework} from './providers/FrameworkProvider';
export type {FrameworkContextType, FrameworkProviderProps} from './providers/FrameworkProvider';
export {useQueryClient} from '@tanstack/react-query';
export type {InfiniteData} from '@tanstack/react-query';

View file

@ -0,0 +1,60 @@
import {ErrorBoundary as SentryErrorBoundary} from '@sentry/react';
import {QueryClientProvider} from '@tanstack/react-query';
import {ReactNode, createContext, useContext} from 'react';
import queryClient from '../utils/queryClient';
import RoutingProvider, {RoutingProviderProps} from './RoutingProvider';
export interface FrameworkProviderProps {
basePath: string;
ghostVersion: string;
externalNavigate: RoutingProviderProps['externalNavigate'];
modals: RoutingProviderProps['modals'];
unsplashConfig: {
Authorization: string;
'Accept-Version': string;
'Content-Type': string;
'App-Pragma': string;
'X-Unsplash-Cache': boolean;
};
sentryDSN: string | null;
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
children: ReactNode;
}
export type FrameworkContextType = Omit<FrameworkProviderProps, 'basePath' | 'externalNavigate' | 'modals' | 'children'>;
const FrameworkContext = createContext<FrameworkContextType>({
ghostVersion: '',
unsplashConfig: {
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
},
sentryDSN: null,
onUpdate: () => {},
onInvalidate: () => {},
onDelete: () => {}
});
function FrameworkProvider({externalNavigate, basePath, modals, children, ...props}: FrameworkProviderProps) {
return (
<SentryErrorBoundary>
<QueryClientProvider client={queryClient}>
<FrameworkContext.Provider value={props}>
<RoutingProvider basePath={basePath} externalNavigate={externalNavigate} modals={modals}>
{children}
</RoutingProvider>
</FrameworkContext.Provider>
</QueryClientProvider>
</SentryErrorBoundary>
);
}
export default FrameworkProvider;
export const useFramework = () => useContext(FrameworkContext);

View file

@ -1,9 +1,7 @@
import NiceModal from '@ebay/nice-modal-react';
import React, {createContext, useCallback, useEffect, useState} from 'react';
import {useScrollSectionContext} from '../../hooks/useScrollSection';
import type {ModalComponent, ModalName} from './routing/modals';
import NiceModal, {NiceModalHocProps} from '@ebay/nice-modal-react';
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
export type RouteParams = {[key: string]: string}
export type RouteParams = Record<string, string>
export type ExternalLink = {
isExternal: true;
@ -16,63 +14,36 @@ export type InternalLink = {
route: string;
}
export type RoutingContextData = {
route: string;
updateRoute: (to: string | InternalLink | ExternalLink) => void;
loadingModal: boolean;
};
export const RouteContext = createContext<RoutingContextData>({
route: '',
updateRoute: () => {},
loadingModal: false
});
export type RoutingModalProps = {
pathName: string;
params?: Record<string, string>,
searchParams?: URLSearchParams
}
const modalPaths: {[key: string]: ModalName} = {
'design/change-theme': 'DesignAndThemeModal',
'design/edit': 'DesignAndThemeModal',
// this is a special route, because it can install a theme directly from the Ghost Marketplace
'design/change-theme/install': 'DesignAndThemeModal',
'navigation/edit': 'NavigationModal',
'staff/invite': 'InviteUserModal',
'staff/:slug': 'UserDetailModal',
'portal/edit': 'PortalModal',
'tiers/add': 'TierDetailModal',
'tiers/:id': 'TierDetailModal',
'stripe-connect': 'StripeConnectModal',
'newsletters/new': 'AddNewsletterModal',
'newsletters/:id': 'NewsletterDetailModal',
'history/view': 'HistoryModal',
'history/view/:user': 'HistoryModal',
'integrations/zapier': 'ZapierModal',
'integrations/slack': 'SlackModal',
'integrations/amp': 'AmpModal',
'integrations/unsplash': 'UnsplashModal',
'integrations/firstpromoter': 'FirstpromoterModal',
'integrations/pintura': 'PinturaModal',
'integrations/new': 'AddIntegrationModal',
'integrations/:id': 'CustomIntegrationModal',
'recommendations/add': 'AddRecommendationModal',
'recommendations/edit': 'EditRecommendationModal',
'announcement-bar/edit': 'AnnouncementBarModal',
'embed-signup-form/show': 'EmbedSignupFormModal',
'offers/edit': 'OffersModal',
'offers/new': 'AddOfferModal',
'offers/:id': 'EditOfferModal',
about: 'AboutModal'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ModalsModule = {default: {[key: string]: ModalComponent<any>}}
export type ModalComponent<Props = object> = React.FC<NiceModalHocProps & RoutingModalProps & Props>;
export type RoutingContextData = {
route: string;
updateRoute: (to: string | InternalLink | ExternalLink) => void;
loadingModal: boolean;
eventTarget: EventTarget;
};
function getHashPath(urlPath: string | undefined) {
export const RouteContext = createContext<RoutingContextData>({
route: '',
updateRoute: () => {},
loadingModal: false,
eventTarget: new EventTarget()
});
function getHashPath(basePath: string, urlPath: string | undefined) {
if (!urlPath) {
return null;
}
const regex = /\/settings\/(.*)/;
const regex = new RegExp(`/${basePath}/(.*)`);
const match = urlPath?.match(regex);
if (match) {
@ -82,19 +53,19 @@ function getHashPath(urlPath: string | undefined) {
return null;
}
const handleNavigation = (currentRoute: string | undefined) => {
const handleNavigation = (basePath: string, currentRoute: string | undefined, loadModals?: () => Promise<ModalsModule>, modalPaths?: Record<string, string>) => {
// Get the hash from the URL
let hash = window.location.hash;
hash = hash.substring(1);
// Create a URL to easily extract the path without query parameters
const domain = `${window.location.protocol}//${window.location.hostname}`;
let url = new URL(hash, domain);
const url = new URL(hash, domain);
const pathName = getHashPath(url.pathname);
const pathName = getHashPath(basePath, url.pathname);
const searchParams = url.searchParams;
if (pathName) {
if (pathName && modalPaths && loadModals) {
const [, currentModalName] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(currentRoute || '', modalPath)) || [];
const [path, modalName] = Object.entries(modalPaths).find(([modalPath]) => matchRoute(pathName, modalPath)) || [];
@ -102,7 +73,7 @@ const handleNavigation = (currentRoute: string | undefined) => {
pathName,
changingModal: modalName && modalName !== currentModalName,
modal: (path && modalName) ? // we should consider adding '&& modalName !== currentModalName' here, but this breaks tests
import('./routing/modals').then(({default: modals}) => {
loadModals().then(({default: modals}) => {
NiceModal.show(modals[modalName] as ModalComponent, {pathName, params: matchRoute(pathName, path), searchParams});
}) :
undefined
@ -119,22 +90,17 @@ const matchRoute = (pathname: string, routeDefinition: string) => {
}
};
type RouteProviderProps = {
export interface RoutingProviderProps {
basePath: string;
externalNavigate: (link: ExternalLink) => void;
modals?: {paths: Record<string, string>, load: () => Promise<ModalsModule>}
children: React.ReactNode;
};
}
const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, children}) => {
const RoutingProvider: React.FC<RoutingProviderProps> = ({basePath, externalNavigate, modals, children}) => {
const [route, setRoute] = useState<string | undefined>(undefined);
const [loadingModal, setLoadingModal] = useState(false);
const {updateNavigatedSection, scrollToSection} = useScrollSectionContext();
useEffect(() => {
// Preload all the modals after initial render to avoid a delay when opening them
setTimeout(() => {
import('./routing/modals');
}, 1000);
}, []);
const [eventTarget] = useState(new EventTarget());
const updateRoute = useCallback((to: string | InternalLink | ExternalLink) => {
const options = typeof to === 'string' ? {route: to} : to;
@ -147,18 +113,27 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
const newPath = options.route;
if (newPath === route) {
scrollToSection(newPath.split('/')[0]);
// No change
} else if (newPath) {
window.location.hash = `/settings/${newPath}`;
} else {
window.location.hash = `/settings`;
}
}, [externalNavigate, route, scrollToSection]);
eventTarget.dispatchEvent(new CustomEvent('routeChange', {detail: {newPath, oldPath: route}}));
}, [eventTarget, externalNavigate, route]);
useEffect(() => {
// Preload all the modals after initial render to avoid a delay when opening them
setTimeout(() => {
modals?.load();
}, 1000);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
const handleHashChange = () => {
setRoute((currentRoute) => {
const {pathName, modal, changingModal} = handleNavigation(currentRoute);
const {pathName, modal, changingModal} = handleNavigation(basePath, currentRoute, modals?.load, modals?.paths);
if (modal && changingModal) {
setLoadingModal(true);
@ -178,12 +153,6 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (route !== undefined) {
updateNavigatedSection(route.split('/')[0]);
}
}, [route, updateNavigatedSection]);
if (route === undefined) {
return null;
}
@ -193,7 +162,8 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
value={{
route,
updateRoute,
loadingModal
loadingModal,
eventTarget
}}
>
{children}
@ -202,3 +172,25 @@ const RoutingProvider: React.FC<RouteProviderProps> = ({externalNavigate, childr
};
export default RoutingProvider;
export function useRouting() {
return useContext(RouteContext);
}
export function useRouteChangeCallback(callback: (newPath: string, oldPath: string) => void, deps: React.DependencyList) {
const {eventTarget} = useRouting();
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableCallback = useCallback(callback, deps);
useEffect(() => {
const listener: EventListener = (e) => {
const event = e as CustomEvent<{newPath: string, oldPath: string}>;
stableCallback(event.detail.newPath, event.detail.oldPath);
};
eventTarget.addEventListener('routeChange', listener);
return () => eventTarget.removeEventListener('routeChange', listener);
}, [eventTarget, stableCallback]);
}

View file

@ -0,0 +1,3 @@
export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
export type {ExternalLink, InternalLink, RoutingModalProps} from './providers/RoutingProvider';

View file

@ -0,0 +1,125 @@
import * as Sentry from '@sentry/react';
import {useFramework} from '../../providers/FrameworkProvider';
import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from '../errors';
import {getGhostPaths} from '../helpers';
import handleResponse from './handleResponse';
export interface RequestOptions {
method?: string;
body?: string | FormData;
headers?: {
'Content-Type'?: string;
};
credentials?: 'include' | 'omit' | 'same-origin';
timeout?: number;
retry?: boolean;
}
export const useFetchApi = () => {
const {ghostVersion, sentryDSN} = useFramework();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return async <ResponseData = any>(endpoint: string | URL, options: RequestOptions = {}): Promise<ResponseData> => {
// By default, we set the Content-Type header to application/json
const defaultHeaders: Record<string, string> = {
'app-pragma': 'no-cache',
'x-ghost-version': ghostVersion
};
if (typeof options.body === 'string') {
defaultHeaders['content-type'] = 'application/json';
}
const headers = options?.headers || {};
const controller = new AbortController();
const {timeout} = options;
if (timeout) {
setTimeout(() => controller.abort(), timeout);
}
// attempt retries for 15 seconds in two situations:
// 1. Server Unreachable error from the browser (code 0 or TypeError), typically from short internet blips
// 2. Maintenance error from Ghost, upgrade in progress so API is temporarily unavailable
let attempts = 0;
const shouldRetry = options.retry === true || options.retry === undefined;
let retryingMs = 0;
const startTime = Date.now();
const maxRetryingMs = 15_000;
const retryPeriods = [500, 1000];
const retryableErrors = [ServerUnreachableError, MaintenanceError, TypeError];
const getErrorData = (error?: APIError, response?: Response) => {
const data: Record<string, unknown> = {
errorName: error?.name,
attempts,
totalSeconds: retryingMs / 1000,
endpoint: endpoint.toString()
};
if (endpoint.toString().includes('/ghost/api/')) {
data.server = response?.headers.get('server');
}
return data;
};
while (attempts === 0 || shouldRetry) {
try {
const response = await fetch(endpoint, {
headers: {
...defaultHeaders,
...headers
},
method: 'GET',
mode: 'cors',
credentials: 'include',
signal: controller.signal,
...options
});
if (attempts !== 0 && sentryDSN) {
Sentry.captureMessage('Request took multiple attempts', {extra: getErrorData()});
}
return handleResponse(response) as ResponseData;
} catch (error) {
retryingMs = Date.now() - startTime;
if (shouldRetry && (import.meta.env.MODE !== 'development' && retryableErrors.some(errorClass => error instanceof errorClass) && retryingMs <= maxRetryingMs)) {
await new Promise((resolve) => {
setTimeout(resolve, retryPeriods[attempts] || retryPeriods[retryPeriods.length - 1]);
});
attempts += 1;
continue;
}
if (attempts !== 0 && sentryDSN) {
Sentry.captureMessage('Request failed after multiple attempts', {extra: getErrorData()});
}
if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
throw new TimeoutError();
}
let newError = error;
if (!(error instanceof APIError)) {
newError = new ServerUnreachableError({cause: error});
}
throw newError;
};
}
// Used for type checking
// this can't happen, but TS isn't smart enough to undeerstand that the loop will never exit without an error or return
// because of shouldRetry + attemps usage combination
return undefined as never;
};
};
const {apiRoot} = getGhostPaths();
export const apiUrl = (path: string, searchParams: Record<string, string> = {}) => {
const url = new URL(`${apiRoot}${path}`, window.location.origin);
url.search = new URLSearchParams(searchParams).toString();
return url.toString();
};

View file

@ -1,13 +1,10 @@
import * as Sentry from '@sentry/react';
import handleResponse from './handleResponse';
import useHandleError from './handleError';
import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from '../errors';
import {UseInfiniteQueryOptions, UseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {getGhostPaths} from '../helpers';
import {useCallback, useEffect, useMemo, useState} from 'react';
import {UseInfiniteQueryOptions, UseQueryOptions, UseQueryResult, useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {usePagination} from '@tryghost/admin-x-design-system';
import {useCallback, useEffect, useMemo, useState} from 'react';
import useHandleError from '../../hooks/useHandleError';
import {usePermission} from '../../hooks/usePermissions';
import {useSentryDSN, useServices} from '../../components/providers/ServiceProvider';
import {useFramework} from '../../providers/FrameworkProvider';
import {RequestOptions, apiUrl, useFetchApi} from './fetchApi';
export interface Meta {
pagination: {
@ -20,127 +17,6 @@ export interface Meta {
}
}
interface RequestOptions {
method?: string;
body?: string | FormData;
headers?: {
'Content-Type'?: string;
};
credentials?: 'include' | 'omit' | 'same-origin';
timeout?: number;
retry?: boolean;
}
export const useFetchApi = () => {
const {ghostVersion} = useServices();
const sentryDSN = useSentryDSN();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return async <ResponseData = any>(endpoint: string | URL, options: RequestOptions = {}): Promise<ResponseData> => {
// By default, we set the Content-Type header to application/json
const defaultHeaders: Record<string, string> = {
'app-pragma': 'no-cache',
'x-ghost-version': ghostVersion
};
if (typeof options.body === 'string') {
defaultHeaders['content-type'] = 'application/json';
}
const headers = options?.headers || {};
const controller = new AbortController();
const {timeout} = options;
if (timeout) {
setTimeout(() => controller.abort(), timeout);
}
// attempt retries for 15 seconds in two situations:
// 1. Server Unreachable error from the browser (code 0 or TypeError), typically from short internet blips
// 2. Maintenance error from Ghost, upgrade in progress so API is temporarily unavailable
let attempts = 0;
let shouldRetry = options.retry === true || options.retry === undefined;
let retryingMs = 0;
const startTime = Date.now();
const maxRetryingMs = 15_000;
const retryPeriods = [500, 1000];
const retryableErrors = [ServerUnreachableError, MaintenanceError, TypeError];
const getErrorData = (error?: APIError, response?: Response) => {
const data: Record<string, unknown> = {
errorName: error?.name,
attempts,
totalSeconds: retryingMs / 1000,
endpoint: endpoint.toString()
};
if (endpoint.toString().includes('/ghost/api/')) {
data.server = response?.headers.get('server');
}
return data;
};
while (attempts === 0 || shouldRetry) {
try {
const response = await fetch(endpoint, {
headers: {
...defaultHeaders,
...headers
},
method: 'GET',
mode: 'cors',
credentials: 'include',
signal: controller.signal,
...options
});
if (attempts !== 0 && sentryDSN) {
Sentry.captureMessage('Request took multiple attempts', {extra: getErrorData()});
}
return handleResponse(response) as ResponseData;
} catch (error) {
retryingMs = Date.now() - startTime;
if (shouldRetry && (import.meta.env.MODE !== 'development' && retryableErrors.some(errorClass => error instanceof errorClass) && retryingMs <= maxRetryingMs)) {
await new Promise((resolve) => {
setTimeout(resolve, retryPeriods[attempts] || retryPeriods[retryPeriods.length - 1]);
});
attempts += 1;
continue;
}
if (attempts !== 0 && sentryDSN) {
Sentry.captureMessage('Request failed after multiple attempts', {extra: getErrorData()});
}
if (error && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
throw new TimeoutError();
}
let newError = error;
if (!(error instanceof APIError)) {
newError = new ServerUnreachableError({cause: error});
}
throw newError;
};
}
// Used for type checking
// this can't happen, but TS isn't smart enough to undeerstand that the loop will never exit without an error or return
// because of shouldRetry + attemps usage combination
return undefined as never;
};
};
const {apiRoot} = getGhostPaths();
export const apiUrl = (path: string, searchParams: Record<string, string> = {}) => {
const url = new URL(`${apiRoot}${path}`, window.location.origin);
url.search = new URLSearchParams(searchParams).toString();
return url.toString();
};
const parameterizedPath = (path: string, params: string | string[]) => {
const paramList = Array.isArray(params) ? params : [params];
return paramList.reduce(function (updatedPath, param) {
@ -163,7 +39,7 @@ type QueryHookOptions<ResponseData> = UseQueryOptions<ResponseData> & {
defaultErrorHandler?: boolean;
};
export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) => ({searchParams, ...query}: QueryHookOptions<ResponseData> = {}) => {
export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) => ({searchParams, ...query}: QueryHookOptions<ResponseData> = {}): Omit<UseQueryResult<ResponseData>, 'data'> & {data: ResponseData | undefined} => {
const url = apiUrl(options.path, searchParams || options.defaultSearchParams);
const fetchApi = useFetchApi();
const handleError = useHandleError();
@ -311,7 +187,7 @@ const mutate = <ResponseData, Payload>({fetchApi, path, payload, searchParams, o
export const createMutation = <ResponseData, Payload>(options: MutationOptions<ResponseData, Payload>) => () => {
const fetchApi = useFetchApi();
const queryClient = useQueryClient();
const {onUpdate, onInvalidate, onDelete} = useServices();
const {onUpdate, onInvalidate, onDelete} = useFramework();
const afterMutate = useCallback((newData: ResponseData, payload: Payload) => {
if (options.invalidateQueries) {

View file

@ -0,0 +1,32 @@
export interface IGhostPaths {
subdir: string;
adminRoot: string;
assetRoot: string;
apiRoot: string;
}
export function getGhostPaths(): IGhostPaths {
const path = window.location.pathname;
const subdir = path.substr(0, path.search('/ghost/'));
const adminRoot = `${subdir}/ghost/`;
const assetRoot = `${subdir}/ghost/assets/`;
const apiRoot = `${subdir}/ghost/api/admin`;
return {subdir, adminRoot, assetRoot, apiRoot};
}
export function downloadFile(url: string) {
let iframe = document.getElementById('iframeDownload');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'iframeDownload';
iframe.style.display = 'none';
document.body.append(iframe);
}
iframe.setAttribute('src', url);
}
export function downloadFromEndpoint(path: string) {
downloadFile(`${getGhostPaths().apiRoot}${path}`);
}

View file

@ -0,0 +1,25 @@
import {QueryClient} from '@tanstack/react-query';
declare global {
interface Window {
adminXQueryClient?: QueryClient;
}
}
const queryClient = window.adminXQueryClient || new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 5 * (60 * 1000), // 5 mins
cacheTime: 10 * (60 * 1000), // 10 mins
// We have custom retry logic for specific errors in fetchApi()
retry: false
}
}
});
if (!window.adminXQueryClient) {
window.adminXQueryClient = queryClient;
}
export default queryClient;

View file

@ -0,0 +1,7 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
]
};

View file

@ -0,0 +1,8 @@
import assert from 'assert/strict';
describe('Hello world', function () {
it('Runs a test', function () {
// TODO: Write me!
assert.ok(require('../'));
});
});

View file

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "./types"
},
"include": ["src"],
"exclude": ["src/**/*.stories.tsx"]
}

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json"]
}

View file

@ -0,0 +1,63 @@
import react from '@vitejs/plugin-react';
import glob from 'glob';
import { resolve } from 'path';
import { defineConfig } from 'vitest/config';
// https://vitejs.dev/config/
export default (function viteConfig() {
return defineConfig({
plugins: [
react()
],
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.VITEST_SEGFAULT_RETRY': 3
},
preview: {
port: 4174
},
build: {
minify: false,
sourcemap: true,
outDir: 'es',
lib: {
formats: ['es'],
entry: glob.sync(resolve(__dirname, 'src/**/*.{ts,tsx}')).reduce((entries, path) => {
if (path.endsWith('.d.ts')) {
return entries;
}
const outPath = path.replace(resolve(__dirname, 'src') + '/', '').replace(/\.(ts|tsx)$/, '');
entries[outPath] = path;
return entries;
}, {} as Record<string, string>)
},
commonjsOptions: {
include: [/packages/, /node_modules/]
},
rollupOptions: {
external: (source) => {
if (source.startsWith('.')) {
return false;
}
if (source.includes('node_modules')) {
return true;
}
return !source.includes(__dirname);
}
}
},
test: {
globals: true, // required for @testing-library/jest-dom extensions
environment: 'jsdom',
include: ['./test/unit/**/*'],
testTimeout: process.env.TIMEOUT ? parseInt(process.env.TIMEOUT) : 10000,
...(process.env.CI && { // https://github.com/vitest-dev/vitest/issues/1674
minThreads: 1,
maxThreads: 2
})
}
});
});

View file

@ -38,7 +38,6 @@
},
"dependencies": {
"@codemirror/lang-html": "^6.4.5",
"@tanstack/react-query": "4.36.1",
"@tryghost/color-utils": "0.2.0",
"@tryghost/limit-service": "^1.2.10",
"@tryghost/nql": "0.11.0",
@ -50,6 +49,7 @@
"devDependencies": {
"@playwright/test": "1.38.1",
"@tryghost/admin-x-design-system": "0.0.0",
"@tryghost/admin-x-framework": "0.0.0",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
"@types/validator": "13.11.6",
@ -68,13 +68,13 @@
"build": {
"dependsOn": [
"build",
{"projects": ["@tryghost/admin-x-design-system"], "target": "build"}
{"projects": ["@tryghost/admin-x-design-system", "@tryghost/admin-x-framework"], "target": "build"}
]
},
"test:acceptance": {
"dependsOn": [
"test:acceptance",
{"projects": ["@tryghost/admin-x-design-system"], "target": "build"}
{"projects": ["@tryghost/admin-x-design-system", "@tryghost/admin-x-framework"], "target": "build"}
]
}
}

View file

@ -1,62 +1,28 @@
import GlobalDataProvider from './components/providers/GlobalDataProvider';
import MainContent from './MainContent';
import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider';
import {DefaultHeaderTypes} from './unsplash/UnsplashTypes';
import {DesignSystemApp} from '@tryghost/admin-x-design-system';
import {FetchKoenigLexical, OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {ScrollSectionProvider} from './hooks/useScrollSection';
import {ErrorBoundary as SentryErrorBoundary} from '@sentry/react';
import {UpgradeStatusType} from './utils/globalTypes';
import SettingsAppProvider, {OfficialTheme, UpgradeStatusType} from './components/providers/SettingsAppProvider';
import SettingsRouter, {loadModals, modalPaths} from './components/providers/SettingsRouter';
import {DesignSystemApp, FetchKoenigLexical} from '@tryghost/admin-x-design-system';
import {FrameworkProvider, FrameworkProviderProps} from '@tryghost/admin-x-framework';
import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal';
interface AppProps {
ghostVersion: string;
interface AppProps extends Omit<FrameworkProviderProps, 'basePath' | 'modals' | 'children'> {
officialThemes: OfficialTheme[];
zapierTemplates: ZapierTemplate[];
externalNavigate: (link: ExternalLink) => void;
darkMode?: boolean;
unsplashConfig: DefaultHeaderTypes
sentryDSN: string | null;
darkMode: boolean;
fetchKoenigLexical: FetchKoenigLexical;
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
upgradeStatus?: UpgradeStatusType;
}
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 5 * (60 * 1000), // 5 mins
cacheTime: 10 * (60 * 1000), // 10 mins
// We have custom retry logic for specific errors in fetchApi()
retry: false
}
}
});
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false, unsplashConfig, fetchKoenigLexical, sentryDSN, onUpdate, onInvalidate, onDelete, upgradeStatus}: AppProps) {
function App({officialThemes, zapierTemplates, upgradeStatus, darkMode, fetchKoenigLexical, ...props}: AppProps) {
return (
<SentryErrorBoundary>
<QueryClientProvider client={queryClient}>
<ServicesProvider fetchKoenigLexical={fetchKoenigLexical} ghostVersion={ghostVersion} officialThemes={officialThemes} sentryDSN={sentryDSN} unsplashConfig={unsplashConfig} upgradeStatus={upgradeStatus} zapierTemplates={zapierTemplates} onDelete={onDelete} onInvalidate={onInvalidate} onUpdate={onUpdate}>
<GlobalDataProvider>
<ScrollSectionProvider>
<RoutingProvider externalNavigate={externalNavigate}>
<DesignSystemApp className='admin-x-settings' darkMode={darkMode} fetchKoenigLexical={fetchKoenigLexical} id="admin-x-settings" style={{
// height: '100vh',
// width: '100%'
}}>
<MainContent />
</DesignSystemApp>
</RoutingProvider>
</ScrollSectionProvider>
</GlobalDataProvider>
</ServicesProvider>
</QueryClientProvider>
</SentryErrorBoundary>
<FrameworkProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}} {...props}>
<SettingsAppProvider officialThemes={officialThemes} upgradeStatus={upgradeStatus} zapierTemplates={zapierTemplates}>
<DesignSystemApp className='admin-x-settings' darkMode={darkMode} fetchKoenigLexical={fetchKoenigLexical} id='admin-x-settings'>
<SettingsRouter />
<MainContent />
</DesignSystemApp>
</SettingsAppProvider>
</FrameworkProvider>
);
}

View file

@ -2,12 +2,12 @@ import ExitSettingsButton from './components/ExitSettingsButton';
import Settings from './components/Settings';
import Sidebar from './components/Sidebar';
import Users from './components/settings/general/Users';
import useRouting from './hooks/useRouting';
import {Heading, topLevelBackdropClasses} from '@tryghost/admin-x-design-system';
import {ReactNode, useEffect} from 'react';
import {canAccessSettings, isEditorUser} from './api/users';
import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users';
import {toast} from 'react-hot-toast';
import {useGlobalData} from './components/providers/GlobalDataProvider';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const Page: React.FC<{children: ReactNode}> = ({children}) => {
return <>

View file

@ -1,6 +0,0 @@
import {createMutation} from '../utils/api/hooks';
export const useTestSlack = createMutation<unknown, null>({
method: 'POST',
path: () => '/slack/test/'
});

View file

@ -1,5 +1,5 @@
import {SettingSection, SettingSectionProps} from '@tryghost/admin-x-design-system';
import {useSearch} from './providers/ServiceProvider';
import {useSearch} from './providers/SettingsAppProvider';
const SearchableSection: React.FC<Omit<SettingSectionProps, 'isVisible'> & {keywords: string[]}> = ({keywords, ...props}) => {
const {checkVisible} = useSearch();

View file

@ -2,17 +2,17 @@ import GhostLogo from '../assets/images/orb-pink.png';
import React, {useEffect, useRef} from 'react';
import clsx from 'clsx';
import useFeatureFlag from '../hooks/useFeatureFlag';
import useRouting from '../hooks/useRouting';
import {Button, Icon, SettingNavItem, SettingNavItemProps, SettingNavSection, TextField, useFocusContext} from '@tryghost/admin-x-design-system';
import {searchKeywords as advancedSearchKeywords} from './settings/advanced/AdvancedSettings';
import {searchKeywords as emailSearchKeywords} from './settings/email/EmailSettings';
import {searchKeywords as generalSearchKeywords} from './settings/general/GeneralSettings';
import {getSettingValues} from '../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {searchKeywords as membershipSearchKeywords} from './settings/membership/MembershipSettings';
import {searchKeywords as siteSearchKeywords} from './settings/site/SiteSettings';
import {useGlobalData} from './providers/GlobalDataProvider';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useScrollSectionContext, useScrollSectionNav} from '../hooks/useScrollSection';
import {useSearch} from './providers/ServiceProvider';
import {useSearch} from './providers/SettingsAppProvider';
const NavItem: React.FC<Omit<SettingNavItemProps, 'isVisible' | 'isCurrent'> & {keywords: string[]}> = ({keywords, navid, ...props}) => {
const {ref, props: scrollProps} = useScrollSectionNav(navid);

View file

@ -1,8 +1,8 @@
import React, {useEffect, useState} from 'react';
import useRouting from '../hooks/useRouting';
import {SettingGroup as Base, SettingGroupProps} from '@tryghost/admin-x-design-system';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useScrollSection} from '../hooks/useScrollSection';
import {useSearch} from './providers/ServiceProvider';
import {useSearch} from './providers/SettingsAppProvider';
const TopLevelGroup: React.FC<Omit<SettingGroupProps, 'isVisible' | 'highlight'> & {keywords: string[]}> = ({keywords, navid, ...props}) => {
const {checkVisible} = useSearch();

View file

@ -1,9 +1,10 @@
import SpinningOrb from '../../assets/videos/logo-loader.mp4';
import {Config, useBrowseConfig} from '../../api/config';
import {Config, useBrowseConfig} from '@tryghost/admin-x-framework/api/config';
import {ReactNode, createContext, useContext} from 'react';
import {Setting, useBrowseSettings} from '../../api/settings';
import {SiteData, useBrowseSite} from '../../api/site';
import {User, useCurrentUser} from '../../api/users';
import {Setting, useBrowseSettings} from '@tryghost/admin-x-framework/api/settings';
import {SiteData, useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {User} from '@tryghost/admin-x-framework/api/users';
import {useCurrentUser} from '@tryghost/admin-x-framework/api/currentUser';
interface GlobalData {
settings: Setting[]

View file

@ -1,108 +0,0 @@
import React, {createContext, useContext} from 'react';
import useSearchService, {SearchService} from '../../utils/search';
import {DefaultHeaderTypes} from '../../unsplash/UnsplashTypes';
import {UpgradeStatusType} from '../../utils/globalTypes';
import {ZapierTemplate} from '../settings/advanced/integrations/ZapierModal';
export type ThemeVariant = {
category: string;
previewUrl: string;
image: string;
};
export type OfficialTheme = {
name: string;
category: string;
previewUrl: string;
ref: string;
image: string;
url?: string;
variants?: ThemeVariant[]
};
export type FetchKoenigLexical = () => Promise<any>
interface ServicesContextProps {
ghostVersion: string
officialThemes: OfficialTheme[];
zapierTemplates: ZapierTemplate[];
search: SearchService;
unsplashConfig: DefaultHeaderTypes;
sentryDSN: string | null;
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
fetchKoenigLexical: FetchKoenigLexical;
upgradeStatus?: UpgradeStatusType;
}
interface ServicesProviderProps {
children: React.ReactNode;
ghostVersion: string;
zapierTemplates: ZapierTemplate[];
officialThemes: OfficialTheme[];
unsplashConfig: DefaultHeaderTypes;
sentryDSN: string | null;
onUpdate: (dataType: string, response: unknown) => void;
onInvalidate: (dataType: string) => void;
onDelete: (dataType: string, id: string) => void;
fetchKoenigLexical: FetchKoenigLexical;
upgradeStatus?: UpgradeStatusType;
}
const ServicesContext = createContext<ServicesContextProps>({
ghostVersion: '',
officialThemes: [],
zapierTemplates: [],
search: {filter: '', setFilter: () => {}, checkVisible: () => true, highlightKeywords: () => ''},
unsplashConfig: {
Authorization: '',
'Accept-Version': '',
'Content-Type': '',
'App-Pragma': '',
'X-Unsplash-Cache': true
},
sentryDSN: null,
onUpdate: () => {},
onInvalidate: () => {},
onDelete: () => {},
fetchKoenigLexical: async () => {},
upgradeStatus: {
isRequired: false,
message: ''
}
});
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, zapierTemplates, officialThemes, unsplashConfig, sentryDSN, onUpdate, onInvalidate, onDelete, fetchKoenigLexical, upgradeStatus}) => {
const search = useSearchService();
return (
<ServicesContext.Provider value={{
ghostVersion,
officialThemes,
zapierTemplates,
search,
unsplashConfig,
sentryDSN,
onUpdate,
onInvalidate,
onDelete,
fetchKoenigLexical,
upgradeStatus
}}>
{children}
</ServicesContext.Provider>
);
};
export {ServicesContext, ServicesProvider};
export const useServices = () => useContext(ServicesContext);
export const useOfficialThemes = () => useServices().officialThemes;
export const useSearch = () => useServices().search;
export const useSentryDSN = () => useServices().sentryDSN;
export const useUpgradeStatus = () => useServices().upgradeStatus;

View file

@ -0,0 +1,68 @@
import GlobalDataProvider from './GlobalDataProvider';
import useSearchService, {SearchService} from '../../utils/search';
import {ReactNode, createContext, useContext} from 'react';
import {ScrollSectionProvider} from '../../hooks/useScrollSection';
import {ZapierTemplate} from '../settings/advanced/integrations/ZapierModal';
export type ThemeVariant = {
category: string;
previewUrl: string;
image: string;
};
export type OfficialTheme = {
name: string;
category: string;
previewUrl: string;
ref: string;
image: string;
url?: string;
variants?: ThemeVariant[]
};
export interface UpgradeStatusType {
isRequired: boolean;
message: string;
}
interface SettingsAppContextType {
officialThemes: OfficialTheme[];
zapierTemplates: ZapierTemplate[];
search: SearchService;
upgradeStatus?: UpgradeStatusType;
}
const SettingsAppContext = createContext<SettingsAppContextType>({
officialThemes: [],
zapierTemplates: [],
search: {filter: '', setFilter: () => {}, checkVisible: () => true, highlightKeywords: () => ''}
});
type SettingsAppProviderProps = Omit<SettingsAppContextType, 'search'> & {children: ReactNode};
const SettingsAppProvider: React.FC<SettingsAppProviderProps> = ({children, ...props}) => {
const search = useSearchService();
return (
<SettingsAppContext.Provider value={{
...props,
search
}}>
<GlobalDataProvider>
<ScrollSectionProvider>
{children}
</ScrollSectionProvider>
</GlobalDataProvider>
</SettingsAppContext.Provider>
);
};
export default SettingsAppProvider;
export const useSettingsApp = () => useContext(SettingsAppContext);
export const useOfficialThemes = () => useSettingsApp().officialThemes;
export const useSearch = () => useSettingsApp().search;
export const useUpgradeStatus = () => useSettingsApp().upgradeStatus;

View file

@ -0,0 +1,61 @@
import React, {useEffect} from 'react';
import {useRouteChangeCallback, useRouting} from '@tryghost/admin-x-framework/routing';
import {useScrollSectionContext} from '../../hooks/useScrollSection';
import type {ModalName} from './routing/modals';
export const modalPaths: {[key: string]: ModalName} = {
'design/change-theme': 'DesignAndThemeModal',
'design/edit': 'DesignAndThemeModal',
// this is a special route, because it can install a theme directly from the Ghost Marketplace
'design/change-theme/install': 'DesignAndThemeModal',
'navigation/edit': 'NavigationModal',
'staff/invite': 'InviteUserModal',
'staff/:slug': 'UserDetailModal',
'portal/edit': 'PortalModal',
'tiers/add': 'TierDetailModal',
'tiers/:id': 'TierDetailModal',
'stripe-connect': 'StripeConnectModal',
'newsletters/new': 'AddNewsletterModal',
'newsletters/:id': 'NewsletterDetailModal',
'history/view': 'HistoryModal',
'history/view/:user': 'HistoryModal',
'integrations/zapier': 'ZapierModal',
'integrations/slack': 'SlackModal',
'integrations/amp': 'AmpModal',
'integrations/unsplash': 'UnsplashModal',
'integrations/firstpromoter': 'FirstpromoterModal',
'integrations/pintura': 'PinturaModal',
'integrations/new': 'AddIntegrationModal',
'integrations/:id': 'CustomIntegrationModal',
'recommendations/add': 'AddRecommendationModal',
'recommendations/edit': 'EditRecommendationModal',
'announcement-bar/edit': 'AnnouncementBarModal',
'embed-signup-form/show': 'EmbedSignupFormModal',
'offers/edit': 'OffersModal',
'offers/new': 'AddOfferModal',
'offers/:id': 'EditOfferModal',
about: 'AboutModal'
};
export const loadModals = () => import('./routing/modals');
const SettingsRouter: React.FC = () => {
const {updateNavigatedSection, scrollToSection} = useScrollSectionContext();
const {route} = useRouting();
useRouteChangeCallback((newPath, oldPath) => {
if (newPath === oldPath) {
scrollToSection(newPath.split('/')[0]);
}
}, [scrollToSection]);
useEffect(() => {
if (route !== undefined) {
updateNavigatedSection(route.split('/')[0]);
}
}, [route, updateNavigatedSection]);
return null;
};
export default SettingsRouter;

View file

@ -1,5 +1,5 @@
import type {NiceModalHocProps} from '@ebay/nice-modal-react';
import type {RoutingModalProps} from '../RoutingProvider';
import type {RoutingModalProps} from '@tryghost/admin-x-framework/routing';
import AboutModal from '../../settings/general/About';
import AddIntegrationModal from '../../settings/advanced/integrations/AddIntegrationModal';

View file

@ -5,7 +5,7 @@ import TopLevelGroup from '../../TopLevelGroup';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {Button, CodeEditor, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {ReactCodeMirrorRef} from '@uiw/react-codemirror';
import {getSettingValues} from '../../../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View file

@ -1,7 +1,7 @@
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useRouting from '../../../hooks/useRouting';
import {Button, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const History: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {updateRoute} = useRouting();

View file

@ -1,12 +1,11 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import useFilterableApi from '../../../hooks/useFilterableApi';
import useRouting from '../../../hooks/useRouting';
import {Action, getActionTitle, getContextResource, getLinkTarget, isBulkAction, useBrowseActions} from '../../../api/actions';
import {Action, getActionTitle, getContextResource, getLinkTarget, isBulkAction, useBrowseActions} from '@tryghost/admin-x-framework/api/actions';
import {Avatar, Button, Icon, InfiniteScrollListener, List, ListItem, LoadSelectOptions, Modal, NoValueLabel, Popover, Select, SelectOption, Toggle, ToggleGroup, debounce} from '@tryghost/admin-x-design-system';
import {RoutingModalProps} from '../../providers/RoutingProvider';
import {User} from '../../../api/users';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {User} from '@tryghost/admin-x-framework/api/users';
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
import {useCallback, useState} from 'react';
import {useFilterableApi} from '@tryghost/admin-x-framework/hooks';
const HistoryIcon: React.FC<{action: Action}> = ({action}) => {
let name = 'pen';

View file

@ -1,19 +1,19 @@
import NiceModal from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useHandleError from '../../../utils/api/handleError';
import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useRouting from '../../../hooks/useRouting';
import {ReactComponent as AmpIcon} from '../../../assets/icons/amp.svg';
import {Button, ConfirmationModal, Icon, List, ListItem, NoValueLabel, TabView, showToast, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {ReactComponent as FirstPromoterIcon} from '../../../assets/icons/firstpromoter.svg';
import {Integration, useBrowseIntegrations, useDeleteIntegration} from '../../../api/integrations';
import {Integration, useBrowseIntegrations, useDeleteIntegration} from '@tryghost/admin-x-framework/api/integrations';
import {ReactComponent as PinturaIcon} from '../../../assets/icons/pintura.svg';
import {ReactComponent as SlackIcon} from '../../../assets/icons/slack.svg';
import {ReactComponent as UnsplashIcon} from '../../../assets/icons/unsplash.svg';
import {ReactComponent as ZapierIcon} from '../../../assets/icons/zapier.svg';
import {getSettingValues} from '../../../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
interface IntegrationItemProps {
icon?: React.ReactNode,

View file

@ -1,11 +1,10 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Form, LimitModal, Modal, TextField} from '@tryghost/admin-x-design-system';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {RoutingModalProps} from '../../../providers/RoutingProvider';
import {useCreateIntegration} from '../../../../api/integrations';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {useCreateIntegration} from '@tryghost/admin-x-framework/api/integrations';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const AddIntegrationModal: React.FC<RoutingModalProps> = () => {
const modal = useModal();

View file

@ -1,12 +1,12 @@
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Form, Modal, TextField, Toggle} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/amp.svg';
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const AmpModal = NiceModal.create(() => {
const {updateRoute} = useRouting();

View file

@ -3,15 +3,14 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import WebhooksTable from './WebhooksTable';
import useForm from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {APIKey, useRefreshAPIKey} from '../../../../api/apiKeys';
import {APIKey, useRefreshAPIKey} from '@tryghost/admin-x-framework/api/apiKeys';
import {ConfirmationModal, Form, ImageUpload, Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
import {Integration, useBrowseIntegrations, useEditIntegration} from '../../../../api/integrations';
import {RoutingModalProps} from '../../../providers/RoutingProvider';
import {getGhostPaths} from '../../../../utils/helpers';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {Integration, useBrowseIntegrations, useEditIntegration} from '@tryghost/admin-x-framework/api/integrations';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {toast} from 'react-hot-toast';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({integration}) => {
const modal = useModal();

View file

@ -1,12 +1,12 @@
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Form, Modal, TextField, Toggle} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/firstpromoter.svg';
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const FirstpromoterModal = NiceModal.create(() => {
const {updateRoute} = useRouting();

View file

@ -1,14 +1,14 @@
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import pinturaScreenshot from '../../../../assets/images/pintura-screenshot.png';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Button, Form, Modal, Toggle, showToast} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/pintura.svg';
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useEffect, useRef, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useUploadFile} from '../../../../api/files';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useUploadFile} from '@tryghost/admin-x-framework/api/files';
const PinturaModal = NiceModal.create(() => {
const {updateRoute} = useRouting();

View file

@ -1,13 +1,12 @@
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import toast from 'react-hot-toast';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {Button, Form, Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/slack.svg';
import {getSettingValues} from '../../../../api/settings';
import {useTestSlack} from '../../../../api/slack';
import {getSettingValues, useTestSlack} from '@tryghost/admin-x-framework/api/settings';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const SlackModal = NiceModal.create(() => {
const {updateRoute} = useRouting();

View file

@ -1,11 +1,11 @@
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Form, Modal, Toggle} from '@tryghost/admin-x-design-system';
import {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const UnsplashModal = NiceModal.create(() => {
const {updateRoute} = useRouting();

View file

@ -2,11 +2,11 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import toast from 'react-hot-toast';
import useForm from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
import validator from 'validator';
import webhookEventOptions from './webhookEventOptions';
import {Form, Modal, Select, TextField, showToast} from '@tryghost/admin-x-design-system';
import {Webhook, useCreateWebhook, useEditWebhook} from '../../../../api/webhooks';
import {Webhook, useCreateWebhook, useEditWebhook} from '@tryghost/admin-x-framework/api/webhooks';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
interface WebhookModalProps {
webhook?: Webhook;

View file

@ -1,10 +1,10 @@
import NiceModal from '@ebay/nice-modal-react';
import WebhookModal from './WebhookModal';
import useHandleError from '../../../../utils/api/handleError';
import {Button, ConfirmationModal, Table, TableCell, TableHead, TableRow, showToast} from '@tryghost/admin-x-design-system';
import {Integration} from '../../../../api/integrations';
import {Integration} from '@tryghost/admin-x-framework/api/integrations';
import {getWebhookEventLabel} from './webhookEventOptions';
import {useDeleteWebhook} from '../../../../api/webhooks';
import {useDeleteWebhook} from '@tryghost/admin-x-framework/api/webhooks';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => {
const {mutateAsync: deleteWebhook} = useDeleteWebhook();

View file

@ -1,17 +1,18 @@
import APIKeys from './APIKeys';
import IntegrationHeader from './IntegrationHeader';
import NiceModal from '@ebay/nice-modal-react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Button, ConfirmationModal, Icon, List, ListItem, Modal} from '@tryghost/admin-x-design-system';
import {ReactComponent as Logo} from '../../../../assets/images/zapier-logo.svg';
import {ReactComponent as ZapierIcon} from '../../../../assets/icons/zapier.svg';
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
import {useBrowseIntegrations} from '../../../../api/integrations';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {resolveAsset} from '../../../../utils/helpers';
import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useRefreshAPIKey} from '../../../../api/apiKeys';
import {useServices} from '../../../providers/ServiceProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRefreshAPIKey} from '@tryghost/admin-x-framework/api/apiKeys';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useSettingsApp} from '../../../providers/SettingsAppProvider';
export interface ZapierTemplate {
ghostImage: string;
@ -23,7 +24,7 @@ export interface ZapierTemplate {
const ZapierModal = NiceModal.create(() => {
const modal = NiceModal.useModal();
const {updateRoute} = useRouting();
const {zapierTemplates} = useServices();
const {zapierTemplates} = useSettingsApp();
const {data: {integrations} = {integrations: []}} = useBrowseIntegrations();
const {config} = useGlobalData();
const {adminRoot} = getGhostPaths();

View file

@ -1,11 +1,11 @@
import FeatureToggle from './FeatureToggle';
import LabItem from './LabItem';
import React, {useState} from 'react';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Button, FileUpload, List, showToast} from '@tryghost/admin-x-design-system';
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
import {downloadRedirects, useUploadRedirects} from '@tryghost/admin-x-framework/api/redirects';
import {downloadRoutes, useUploadRoutes} from '@tryghost/admin-x-framework/api/routes';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const BetaFeatures: React.FC = () => {
const {updateRoute} = useRouting();

View file

@ -1,9 +1,9 @@
import React from 'react';
import useHandleError from '../../../../utils/api/handleError';
import {ConfigResponseType, configDataType} from '../../../../api/config';
import {ConfigResponseType, configDataType} from '@tryghost/admin-x-framework/api/config';
import {Toggle} from '@tryghost/admin-x-design-system';
import {getSettingValue, useEditSettings} from '../../../../api/settings';
import {getSettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useQueryClient} from '@tanstack/react-query';
const FeatureToggle: React.FC<{ flag: string; label?: string; }> = ({label, flag}) => {

View file

@ -1,9 +1,9 @@
import LabItem from './LabItem';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import useHandleError from '../../../../utils/api/handleError';
import {Button, ConfirmationModal, FileUpload, List, showToast} from '@tryghost/admin-x-design-system';
import {downloadAllContent, useDeleteAllContent, useImportContent} from '../../../../api/db';
import {downloadAllContent, useDeleteAllContent, useImportContent} from '@tryghost/admin-x-framework/api/db';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useQueryClient} from '@tanstack/react-query';
const ImportModalContent = () => {

View file

@ -5,7 +5,7 @@ import useSettingGroup from '../../../hooks/useSettingGroup';
import {MultiSelect, MultiSelectOption, Select, SettingGroupContent, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {MultiValue} from 'react-select';
import {getOptionLabel} from '../../../utils/helpers';
import {getSettingValues} from '../../../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
type RefipientValueArgs = {
defaultEmailRecipients: string;

View file

@ -4,7 +4,7 @@ import MailGun from './Mailgun';
import Newsletters from './Newsletters';
import React from 'react';
import SearchableSection from '../../SearchableSection';
import {getSettingValues} from '../../../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
export const searchKeywords = {

View file

@ -1,10 +1,10 @@
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useHandleError from '../../../utils/api/handleError';
import useRouting from '../../../hooks/useRouting';
import {Banner, Icon, SettingGroupContent, Toggle, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
import {Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const EnableNewsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {settings} = useGlobalData();

View file

@ -1,9 +1,9 @@
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useHandleError from '../../../utils/api/handleError';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {IconLabel, Link, Select, SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getSettingValues, useEditSettings} from '../../../api/settings';
import {getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const MAILGUN_REGIONS = [
{label: '🇺🇸 US', value: 'https://api.mailgun.net/v3'},

View file

@ -2,14 +2,14 @@ import NewslettersList from './newsletters/NewslettersList';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {ReactNode, useEffect, useState} from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useHandleError from '../../../utils/api/handleError';
import useQueryParams from '../../../hooks/useQueryParams';
import useRouting from '../../../hooks/useRouting';
import {APIError} from '../../../utils/errors';
import {APIError} from '@tryghost/admin-x-framework/errors';
import {Button, ConfirmationModal, TabView, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {InfiniteData, useQueryClient} from '@tanstack/react-query';
import {Newsletter, NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '../../../api/newsletters';
import {InfiniteData, useQueryClient} from '@tryghost/admin-x-framework';
import {Newsletter, NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '@tryghost/admin-x-framework/api/newsletters';
import {arrayMove} from '@dnd-kit/sortable';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
const NavigateToNewsletter = ({id, children}: {id: string; children: ReactNode}) => {
const modal = useModal();

View file

@ -1,14 +1,13 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect} from 'react';
import useForm from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import {Form, LimitModal, Modal, TextArea, TextField, Toggle, showToast} from '@tryghost/admin-x-design-system';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {RoutingModalProps} from '../../../providers/RoutingProvider';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {toast} from 'react-hot-toast';
import {useAddNewsletter} from '../../../../api/newsletters';
import {useBrowseMembers} from '../../../../api/members';
import {useAddNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const AddNewsletterModal: React.FC<RoutingModalProps> = () => {
const modal = useModal();

View file

@ -3,19 +3,18 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
import useHandleError from '../../../../utils/api/handleError';
import useRouting from '../../../../hooks/useRouting';
import useSettingGroup from '../../../../hooks/useSettingGroup';
import validator from 'validator';
import {Button, ButtonGroup, ColorPickerField, ConfirmationModal, Form, Heading, Hint, HtmlField, Icon, ImageUpload, LimitModal, PreviewModalContent, Select, SelectOption, Separator, Tab, TabView, TextArea, TextField, Toggle, ToggleGroup, showToast} from '@tryghost/admin-x-design-system';
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '../../../../api/newsletters';
import {RoutingModalProps} from '../../../providers/RoutingProvider';
import {fullEmailAddress} from '../../../../api/site';
import {getImageUrl, useUploadImage} from '../../../../api/images';
import {getSettingValues} from '../../../../api/settings';
import {Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {textColorForBackgroundColor} from '@tryghost/color-utils';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const Sidebar: React.FC<{
newsletter: Newsletter;

View file

@ -1,9 +1,9 @@
import NewsletterPreviewContent from './NewsletterPreviewContent';
import React from 'react';
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
import {Newsletter} from '../../../../api/newsletters';
import {fullEmailAddress} from '../../../../api/site';
import {getSettingValues} from '../../../../api/settings';
import {Newsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {fullEmailAddress} from '@tryghost/admin-x-framework/api/site';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {textColorForBackgroundColor} from '@tryghost/color-utils';
import {useGlobalData} from '../../../providers/GlobalDataProvider';

View file

@ -1,8 +1,8 @@
import React from 'react';
import useRouting from '../../../../hooks/useRouting';
import {Button, DragIndicator, NoValueLabel, SortableItemContainerProps, SortableList, Table, TableCell, TableRow} from '@tryghost/admin-x-design-system';
import {Newsletter} from '../../../../api/newsletters';
import {Newsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {numberWithCommas} from '../../../../utils/helpers';
import {useRouting} from '@tryghost/admin-x-framework/routing';
interface NewslettersListProps {
newsletters: Newsletter[];

View file

@ -1,11 +1,11 @@
import useFilterableApi from '../../../hooks/useFilterableApi';
import {GroupBase, MultiValue} from 'react-select';
import {Label} from '../../../api/labels';
import {Label} from '@tryghost/admin-x-framework/api/labels';
import {LoadMultiSelectOptions, MultiSelectOption, debounce} from '@tryghost/admin-x-design-system';
import {Offer} from '../../../api/offers';
import {Tier} from '../../../api/tiers';
import {Offer} from '@tryghost/admin-x-framework/api/offers';
import {Tier} from '@tryghost/admin-x-framework/api/tiers';
import {isObjectId} from '../../../utils/helpers';
import {useEffect, useState} from 'react';
import {useFilterableApi} from '@tryghost/admin-x-framework/hooks';
const SIMPLE_SEGMENT_OPTIONS: MultiSelectOption[] = [{
label: 'Free members',

View file

@ -1,11 +1,10 @@
import NiceModal from '@ebay/nice-modal-react';
import useRouting from '../../../hooks/useRouting';
import {GhostLogo, Icon, Modal, Separator} from '@tryghost/admin-x-design-system';
import {RoutingModalProps} from '../../providers/RoutingProvider';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {linkToGitHubReleases} from '../../../utils/linkToGithubReleases';
import {showDatabaseWarning} from '../../../utils/showDatabaseWarning';
import {useGlobalData} from '../../providers/GlobalDataProvider';
import {useUpgradeStatus} from '../../providers/ServiceProvider';
import {useUpgradeStatus} from '../../providers/SettingsAppProvider';
const AboutModal = NiceModal.create<RoutingModalProps>(({}) => {
const {updateRoute} = useRouting();

View file

@ -1,11 +1,11 @@
import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useHandleError from '../../../utils/api/handleError';
import usePinturaEditor from '../../../hooks/usePinturaEditor';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {FacebookLogo, ImageUpload, SettingGroupContent, TextField, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getImageUrl, useUploadImage} from '../../../api/images';
import {getSettingValues} from '../../../api/settings';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

View file

@ -1,13 +1,13 @@
import NiceModal from '@ebay/nice-modal-react';
import useHandleError from '../../../utils/api/handleError';
import useRouting from '../../../hooks/useRouting';
import validator from 'validator';
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
import {Modal, Radio, TextField, showToast} from '@tryghost/admin-x-design-system';
import {useAddInvite, useBrowseInvites} from '../../../api/invites';
import {useBrowseRoles} from '../../../api/roles';
import {useBrowseUsers} from '../../../api/users';
import {useAddInvite, useBrowseInvites} from '@tryghost/admin-x-framework/api/invites';
import {useBrowseRoles} from '@tryghost/admin-x-framework/api/roles';
import {useBrowseUsers} from '@tryghost/admin-x-framework/api/users';
import {useEffect, useRef, useState} from 'react';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';

View file

@ -2,7 +2,7 @@ import React from 'react';
import TopLevelGroup from '../../TopLevelGroup';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {Icon, Link, SettingGroupContent, TextField, Toggle, withErrorBoundary} from '@tryghost/admin-x-design-system';
import {getSettingValues} from '../../../api/settings';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
const LockSite: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {

Some files were not shown because too many files have changed in this diff Show more