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:
parent
370c6b465b
commit
94a181ce2a
169 changed files with 1078 additions and 747 deletions
4
.github/scripts/dev.js
vendored
4
.github/scripts/dev.js
vendored
|
@ -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: {}
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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';
|
||||
|
|
41
apps/admin-x-framework/.eslintrc.cjs
Normal file
41
apps/admin-x-framework/.eslintrc.cjs
Normal 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
2
apps/admin-x-framework/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
es
|
||||
types
|
80
apps/admin-x-framework/package.json
Normal file
80
apps/admin-x-framework/package.json
Normal 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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
|
||||
// Types
|
||||
|
32
apps/admin-x-framework/src/api/currentUser.ts
Normal file
32
apps/admin-x-framework/src/api/currentUser.ts
Normal 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
|
||||
};
|
||||
};
|
|
@ -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 } |
|
|
@ -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
|
|
@ -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) {
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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}/`,
|
|
@ -1,5 +1,5 @@
|
|||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
import {Meta, createMutation} from '../utils/api/hooks';
|
||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
|
||||
// Types
|
||||
|
2
apps/admin-x-framework/src/errors.ts
Normal file
2
apps/admin-x-framework/src/errors.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './utils/errors';
|
||||
|
2
apps/admin-x-framework/src/helpers.ts
Normal file
2
apps/admin-x-framework/src/helpers.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './utils/helpers';
|
||||
|
4
apps/admin-x-framework/src/hooks.ts
Normal file
4
apps/admin-x-framework/src/hooks.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {default as useFilterableApi} from './hooks/useFilterableApi';
|
||||
export {default as useHandleError} from './hooks/useHandleError';
|
||||
export {usePermission} from './hooks/usePermissions';
|
||||
|
|
@ -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, '\\\'') + '\'';
|
|
@ -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);
|
|
@ -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;
|
6
apps/admin-x-framework/src/index.ts
Normal file
6
apps/admin-x-framework/src/index.ts
Normal 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';
|
||||
|
60
apps/admin-x-framework/src/providers/FrameworkProvider.tsx
Normal file
60
apps/admin-x-framework/src/providers/FrameworkProvider.tsx
Normal 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);
|
|
@ -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]);
|
||||
}
|
3
apps/admin-x-framework/src/routing.ts
Normal file
3
apps/admin-x-framework/src/routing.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
|
||||
export type {ExternalLink, InternalLink, RoutingModalProps} from './providers/RoutingProvider';
|
||||
|
125
apps/admin-x-framework/src/utils/api/fetchApi.ts
Normal file
125
apps/admin-x-framework/src/utils/api/fetchApi.ts
Normal 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();
|
||||
};
|
|
@ -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) {
|
32
apps/admin-x-framework/src/utils/helpers.ts
Normal file
32
apps/admin-x-framework/src/utils/helpers.ts
Normal 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}`);
|
||||
}
|
25
apps/admin-x-framework/src/utils/queryClient.ts
Normal file
25
apps/admin-x-framework/src/utils/queryClient.ts
Normal 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;
|
7
apps/admin-x-framework/test/.eslintrc.cjs
Normal file
7
apps/admin-x-framework/test/.eslintrc.cjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
]
|
||||
};
|
8
apps/admin-x-framework/test/hello.test.ts
Normal file
8
apps/admin-x-framework/test/hello.test.ts
Normal 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('../'));
|
||||
});
|
||||
});
|
11
apps/admin-x-framework/tsconfig.declaration.json
Normal file
11
apps/admin-x-framework/tsconfig.declaration.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationDir": "./types"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.stories.tsx"]
|
||||
}
|
23
apps/admin-x-framework/tsconfig.json
Normal file
23
apps/admin-x-framework/tsconfig.json
Normal 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" }]
|
||||
}
|
10
apps/admin-x-framework/tsconfig.node.json
Normal file
10
apps/admin-x-framework/tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts", "package.json"]
|
||||
}
|
63
apps/admin-x-framework/vite.config.ts
Normal file
63
apps/admin-x-framework/vite.config.ts
Normal 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
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import {createMutation} from '../utils/api/hooks';
|
||||
|
||||
export const useTestSlack = createMutation<unknown, null>({
|
||||
method: 'POST',
|
||||
path: () => '/slack/test/'
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}) => {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue