Created a skeleton AdminX demo app (#19005)
refs https://github.com/TryGhost/Product/issues/4152 --- <!-- Leave the line below if you'd like GitHub Copilot to generate a summary from your commit --> <!-- copilot:summary --> ### <samp>🤖[[deprecated]](https://githubnext.com/copilot-for-prs-sunset) Generated by Copilot at a28462f</samp> This pull request adds a new admin-x app called `admin-x-demo`, which demonstrates how to use the shared packages `admin-x-framework` and `admin-x-design-system` to create a simple app that renders a button and a modal. It also improves the development workflow, the vite integration, the dependency management, and the type checking for the admin-x apps and packages. It modifies some files in the `admin-x-framework` and `admin-x-design-system` packages to make the modals prop optional, to introduce a new type for the props from the Ember app, to fix the z-index of the modal backdrop, and to use consistent file extensions and module syntax.
This commit is contained in:
parent
320eaac4c4
commit
a93c665d20
4
.github/scripts/dev.js
vendored
4
.github/scripts/dev.js
vendored
@ -51,6 +51,8 @@ const COMMAND_TYPESCRIPT = {
|
|||||||
env: {}
|
env: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings';
|
||||||
|
|
||||||
const COMMANDS_ADMINX = [{
|
const COMMANDS_ADMINX = [{
|
||||||
name: 'adminXDeps',
|
name: 'adminXDeps',
|
||||||
command: 'nx watch --projects=apps/admin-x-design-system,apps/admin-x-framework -- nx run \\$NX_PROJECT_NAME:build --skip-nx-cache',
|
command: 'nx watch --projects=apps/admin-x-design-system,apps/admin-x-framework -- nx run \\$NX_PROJECT_NAME:build --skip-nx-cache',
|
||||||
@ -59,7 +61,7 @@ const COMMANDS_ADMINX = [{
|
|||||||
env: {}
|
env: {}
|
||||||
}, {
|
}, {
|
||||||
name: 'adminX',
|
name: 'adminX',
|
||||||
command: 'nx run @tryghost/admin-x-settings:build && nx run @tryghost/admin-x-settings:dev',
|
command: `nx run-many --projects=${adminXApps} --targets=build && nx run-many --projects=${adminXApps} --parallel=${adminXApps.length} --targets=dev`,
|
||||||
cwd: path.resolve(__dirname, '../../apps/admin-x-settings'),
|
cwd: path.resolve(__dirname, '../../apps/admin-x-settings'),
|
||||||
prefixColor: '#C35831',
|
prefixColor: '#C35831',
|
||||||
env: {}
|
env: {}
|
||||||
|
1
apps/admin-x-demo/.eslintignore
Normal file
1
apps/admin-x-demo/.eslintignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
tailwind.config.cjs
|
56
apps/admin-x-demo/.eslintrc.cjs
Normal file
56
apps/admin-x-demo/.eslintrc.cjs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/ts',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended'
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'ghost',
|
||||||
|
'react-refresh',
|
||||||
|
'tailwindcss'
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// sort multiple import lines into alphabetical groups
|
||||||
|
'ghost/sort-imports-es6-autofix/sort-imports-es6': ['error', {
|
||||||
|
memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple']
|
||||||
|
}],
|
||||||
|
|
||||||
|
// TODO: re-enable this (maybe fixed fast refresh?)
|
||||||
|
'react-refresh/only-export-components': 'off',
|
||||||
|
|
||||||
|
// 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',
|
||||||
|
|
||||||
|
// TODO: re-enable these if deemed useful
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
|
||||||
|
// custom react rules
|
||||||
|
'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'}]
|
||||||
|
}
|
||||||
|
};
|
1
apps/admin-x-demo/.gitignore
vendored
Normal file
1
apps/admin-x-demo/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
54
apps/admin-x-demo/package.json
Normal file
54
apps/admin-x-demo/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@tryghost/admin-x-demo",
|
||||||
|
"version": "0.0.20",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/TryGhost/Ghost/tree/main/apps/admin-x-demo"
|
||||||
|
},
|
||||||
|
"author": "Ghost Foundation",
|
||||||
|
"files": [
|
||||||
|
"LICENSE",
|
||||||
|
"README.md",
|
||||||
|
"dist/"
|
||||||
|
],
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/admin-x-demo.umd.cjs",
|
||||||
|
"module": "./dist/admin-x-demo.js",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"dev:start": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "yarn run lint:js",
|
||||||
|
"lint:js": "eslint --ext .js,.ts,.cjs,.tsx --cache src",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
|
},
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": [
|
||||||
|
"build",
|
||||||
|
{"projects": ["@tryghost/admin-x-design-system", "@tryghost/admin-x-framework"], "target": "build"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"test:acceptance": {
|
||||||
|
"dependsOn": [
|
||||||
|
"test:acceptance",
|
||||||
|
{"projects": ["@tryghost/admin-x-design-system", "@tryghost/admin-x-framework"], "target": "build"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
apps/admin-x-demo/postcss.config.cjs
Normal file
1
apps/admin-x-demo/postcss.config.cjs
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs');
|
27
apps/admin-x-demo/src/App.tsx
Normal file
27
apps/admin-x-demo/src/App.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import MainContent from './MainContent';
|
||||||
|
import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system';
|
||||||
|
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
framework: TopLevelFrameworkProps;
|
||||||
|
designSystem: DesignSystemAppProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modals = {
|
||||||
|
paths: {
|
||||||
|
'demo-modal': 'DemoModal'
|
||||||
|
},
|
||||||
|
load: async () => import('./components/modals')
|
||||||
|
};
|
||||||
|
|
||||||
|
const App: React.FC<AppProps> = ({framework, designSystem}) => {
|
||||||
|
return (
|
||||||
|
<FrameworkProvider basePath='demo-x' modals={modals} {...framework}>
|
||||||
|
<DesignSystemApp className='admin-x-demo' {...designSystem}>
|
||||||
|
<MainContent />
|
||||||
|
</DesignSystemApp>
|
||||||
|
</FrameworkProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
12
apps/admin-x-demo/src/MainContent.tsx
Normal file
12
apps/admin-x-demo/src/MainContent.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import {Button} from '@tryghost/admin-x-design-system';
|
||||||
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
|
const MainContent = () => {
|
||||||
|
const {updateRoute} = useRouting();
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<Button label='Open modal' onClick={() => updateRoute('demo-modal')} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainContent;
|
20
apps/admin-x-demo/src/components/DemoModal.tsx
Normal file
20
apps/admin-x-demo/src/components/DemoModal.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
|
import {Modal} from '@tryghost/admin-x-design-system';
|
||||||
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
|
const DemoModal = NiceModal.create(() => {
|
||||||
|
const {updateRoute} = useRouting();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
afterClose={() => {
|
||||||
|
updateRoute('');
|
||||||
|
}}
|
||||||
|
title='Demo modal'
|
||||||
|
>
|
||||||
|
Demo modal
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DemoModal;
|
9
apps/admin-x-demo/src/components/modals.tsx
Normal file
9
apps/admin-x-demo/src/components/modals.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import DemoModal from './DemoModal';
|
||||||
|
import {ModalComponent} from '@tryghost/admin-x-framework/routing';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const modals = {DemoModal} satisfies {[key: string]: ModalComponent<any>};
|
||||||
|
|
||||||
|
export default modals;
|
||||||
|
|
||||||
|
export type ModalName = keyof typeof modals;
|
6
apps/admin-x-demo/src/index.tsx
Normal file
6
apps/admin-x-demo/src/index.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import './styles/index.css';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
export {
|
||||||
|
App as AdminXApp
|
||||||
|
};
|
1
apps/admin-x-demo/src/styles/index.css
Normal file
1
apps/admin-x-demo/src/styles/index.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import '@tryghost/admin-x-design-system/styles.css';
|
6
apps/admin-x-demo/tailwind.config.cjs
Normal file
6
apps/admin-x-demo/tailwind.config.cjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const adminXPreset = require('@tryghost/admin-x-design-system/tailwind.cjs');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
presets: [adminXPreset('.admin-x-demo')],
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', '../../node_modules/@tryghost/admin-x-design-system/es/**/*.{js,ts,jsx,tsx}']
|
||||||
|
};
|
23
apps/admin-x-demo/tsconfig.json
Normal file
23
apps/admin-x-demo/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
10
apps/admin-x-demo/vite.config.mjs
Normal file
10
apps/admin-x-demo/vite.config.mjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
|
||||||
|
import pkg from './package.json';
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
|
export default (function viteConfig() {
|
||||||
|
return adminXViteConfig({
|
||||||
|
packageName: pkg.name,
|
||||||
|
entry: resolve(__dirname, 'src/index.tsx')
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
export default {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-import': {},
|
'postcss-import': {},
|
||||||
'tailwindcss/nesting': {},
|
'tailwindcss/nesting': {},
|
@ -192,7 +192,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
let backdropClasses = clsx(
|
let backdropClasses = clsx(
|
||||||
'fixed inset-0 z-40 h-[100vh] w-[100vw]'
|
'fixed inset-0 z-[1000] h-[100vh] w-[100vw]'
|
||||||
);
|
);
|
||||||
|
|
||||||
let paddingClasses = '';
|
let paddingClasses = '';
|
||||||
|
@ -29,6 +29,10 @@
|
|||||||
"./api/*": {
|
"./api/*": {
|
||||||
"import": "./es/api/*.js",
|
"import": "./es/api/*.js",
|
||||||
"types": "./types/api/*.d.ts"
|
"types": "./types/api/*.d.ts"
|
||||||
|
},
|
||||||
|
"./vite": {
|
||||||
|
"import": "./es/vite.js",
|
||||||
|
"types": "./types/vite.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
@ -49,7 +53,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react": "14.1.0",
|
"@testing-library/react": "14.1.0",
|
||||||
"@types/mocha": "10.0.1",
|
"@types/mocha": "10.0.1",
|
||||||
"@vitejs/plugin-react": "4.2.0",
|
|
||||||
"c8": "8.0.1",
|
"c8": "8.0.1",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-react-refresh": "0.4.3",
|
"eslint-plugin-react-refresh": "0.4.3",
|
||||||
@ -58,13 +61,21 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"sinon": "17.0.0",
|
"sinon": "17.0.0",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2"
|
||||||
"vite": "4.5.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/react": "7.80.1",
|
"@sentry/react": "7.80.1",
|
||||||
"@tanstack/react-query": "4.36.1",
|
"@tanstack/react-query": "4.36.1",
|
||||||
"@tryghost/admin-x-design-system": "0.0.0"
|
"@tryghost/admin-x-design-system": "0.0.0",
|
||||||
|
"@types/react": "18.2.37",
|
||||||
|
"@types/react-dom": "18.2.15",
|
||||||
|
"@vitejs/plugin-react": "4.2.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"vite": "4.5.0",
|
||||||
|
"vite-plugin-css-injected-by-js": "^3.3.0",
|
||||||
|
"vite-plugin-svgr": "3.3.0",
|
||||||
|
"vitest": "0.34.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export {default as FrameworkProvider, useFramework} from './providers/FrameworkProvider';
|
export {default as FrameworkProvider, useFramework} from './providers/FrameworkProvider';
|
||||||
export type {FrameworkContextType, FrameworkProviderProps} from './providers/FrameworkProvider';
|
export type {FrameworkContextType, FrameworkProviderProps, TopLevelFrameworkProps} from './providers/FrameworkProvider';
|
||||||
|
|
||||||
export {useQueryClient} from '@tanstack/react-query';
|
export {useQueryClient} from '@tanstack/react-query';
|
||||||
export type {InfiniteData} from '@tanstack/react-query';
|
export type {InfiniteData} from '@tanstack/react-query';
|
||||||
|
@ -24,6 +24,9 @@ export interface FrameworkProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// children, basePath and modals should be provided by each app, while others are passed in from Ember
|
||||||
|
export type TopLevelFrameworkProps = Omit<FrameworkProviderProps, 'children' | 'basePath' | 'modals'>;
|
||||||
|
|
||||||
export type FrameworkContextType = Omit<FrameworkProviderProps, 'basePath' | 'externalNavigate' | 'modals' | 'children'>;
|
export type FrameworkContextType = Omit<FrameworkProviderProps, 'basePath' | 'externalNavigate' | 'modals' | 'children'>;
|
||||||
|
|
||||||
const FrameworkContext = createContext<FrameworkContextType>({
|
const FrameworkContext = createContext<FrameworkContextType>({
|
||||||
|
@ -115,13 +115,13 @@ const RoutingProvider: React.FC<RoutingProviderProps> = ({basePath, externalNavi
|
|||||||
if (newPath === route) {
|
if (newPath === route) {
|
||||||
// No change
|
// No change
|
||||||
} else if (newPath) {
|
} else if (newPath) {
|
||||||
window.location.hash = `/settings/${newPath}`;
|
window.location.hash = `/${basePath}/${newPath}`;
|
||||||
} else {
|
} else {
|
||||||
window.location.hash = `/settings`;
|
window.location.hash = `/${basePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
eventTarget.dispatchEvent(new CustomEvent('routeChange', {detail: {newPath, oldPath: route}}));
|
eventTarget.dispatchEvent(new CustomEvent('routeChange', {detail: {newPath, oldPath: route}}));
|
||||||
}, [eventTarget, externalNavigate, route]);
|
}, [basePath, eventTarget, externalNavigate, route]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Preload all the modals after initial render to avoid a delay when opening them
|
// Preload all the modals after initial render to avoid a delay when opening them
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
|
export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider';
|
||||||
export type {ExternalLink, InternalLink, RoutingModalProps} from './providers/RoutingProvider';
|
export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/RoutingProvider';
|
||||||
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
|
|
||||||
import pkg from './package.json';
|
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import {PluginOption, UserConfig, mergeConfig} from 'vite';
|
||||||
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
|
||||||
import svgr from 'vite-plugin-svgr';
|
import svgr from 'vite-plugin-svgr';
|
||||||
import {PluginOption} from 'vite';
|
|
||||||
import {defineConfig} from 'vitest/config';
|
import {defineConfig} from 'vitest/config';
|
||||||
import {resolve} from 'path';
|
|
||||||
|
|
||||||
const outputFileName = pkg.name[0] === '@' ? pkg.name.slice(pkg.name.indexOf('/') + 1) : pkg.name;
|
|
||||||
|
|
||||||
const externalPlugin = ({externals}: { externals: Record<string, string> }): PluginOption => {
|
const externalPlugin = ({externals}: { externals: Record<string, string> }): PluginOption => {
|
||||||
return {
|
return {
|
||||||
@ -32,8 +28,10 @@ const externalPlugin = ({externals}: { externals: Record<string, string> }): Plu
|
|||||||
};
|
};
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default (function viteConfig() {
|
export default function adminXViteConfig({packageName, entry, overrides}: {packageName: string; entry: string; overrides?: UserConfig}) {
|
||||||
return defineConfig({
|
const outputFileName = packageName[0] === '@' ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
|
||||||
|
|
||||||
|
const defaultConfig = defineConfig({
|
||||||
logLevel: process.env.CI ? 'info' : 'warn',
|
logLevel: process.env.CI ? 'info' : 'warn',
|
||||||
plugins: [
|
plugins: [
|
||||||
svgr(),
|
svgr(),
|
||||||
@ -48,8 +46,7 @@ export default (function viteConfig() {
|
|||||||
],
|
],
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||||
'process.env.VITEST_SEGFAULT_RETRY': 3,
|
'process.env.VITEST_SEGFAULT_RETRY': 3
|
||||||
'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package
|
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
port: 4174
|
port: 4174
|
||||||
@ -60,8 +57,8 @@ export default (function viteConfig() {
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
lib: {
|
lib: {
|
||||||
formats: ['es'],
|
formats: ['es'],
|
||||||
entry: resolve(__dirname, 'src/index.tsx'),
|
entry,
|
||||||
name: pkg.name,
|
name: packageName,
|
||||||
fileName(format) {
|
fileName(format) {
|
||||||
if (format === 'umd') {
|
if (format === 'umd') {
|
||||||
return `${outputFileName}.umd.js`;
|
return `${outputFileName}.umd.js`;
|
||||||
@ -83,16 +80,8 @@ export default (function viteConfig() {
|
|||||||
minThreads: 1,
|
minThreads: 1,
|
||||||
maxThreads: 2
|
maxThreads: 2
|
||||||
})
|
})
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
// Shim node modules utilized by the @tryghost/nql package
|
|
||||||
alias: {
|
|
||||||
fs: 'node-shim.cjs',
|
|
||||||
path: 'node-shim.cjs',
|
|
||||||
util: 'node-shim.cjs',
|
|
||||||
// @TODO: Remove this when @tryghost/nql is updated
|
|
||||||
mingo: resolve(__dirname, '../../node_modules/mingo/dist/mingo.js')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
return mergeConfig(defaultConfig, overrides || {});
|
||||||
|
};
|
@ -10,10 +10,6 @@ export default (function viteConfig() {
|
|||||||
plugins: [
|
plugins: [
|
||||||
react()
|
react()
|
||||||
],
|
],
|
||||||
define: {
|
|
||||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
|
||||||
'process.env.VITEST_SEGFAULT_RETRY': 3
|
|
||||||
},
|
|
||||||
preview: {
|
preview: {
|
||||||
port: 4174
|
port: 4174
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1 @@
|
|||||||
module.exports = {
|
module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs');
|
||||||
plugins: {
|
|
||||||
'postcss-import': {},
|
|
||||||
'tailwindcss/nesting': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import MainContent from './MainContent';
|
import MainContent from './MainContent';
|
||||||
import SettingsAppProvider, {OfficialTheme, UpgradeStatusType} from './components/providers/SettingsAppProvider';
|
import SettingsAppProvider, {OfficialTheme, UpgradeStatusType} from './components/providers/SettingsAppProvider';
|
||||||
import SettingsRouter, {loadModals, modalPaths} from './components/providers/SettingsRouter';
|
import SettingsRouter, {loadModals, modalPaths} from './components/providers/SettingsRouter';
|
||||||
import {DesignSystemApp, FetchKoenigLexical} from '@tryghost/admin-x-design-system';
|
import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system';
|
||||||
import {FrameworkProvider, FrameworkProviderProps} from '@tryghost/admin-x-framework';
|
import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework';
|
||||||
import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal';
|
import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal';
|
||||||
|
|
||||||
interface AppProps extends Omit<FrameworkProviderProps, 'basePath' | 'modals' | 'children'> {
|
interface AppProps {
|
||||||
|
framework: TopLevelFrameworkProps;
|
||||||
|
designSystem: DesignSystemAppProps;
|
||||||
officialThemes: OfficialTheme[];
|
officialThemes: OfficialTheme[];
|
||||||
zapierTemplates: ZapierTemplate[];
|
zapierTemplates: ZapierTemplate[];
|
||||||
darkMode: boolean;
|
|
||||||
fetchKoenigLexical: FetchKoenigLexical;
|
|
||||||
upgradeStatus?: UpgradeStatusType;
|
upgradeStatus?: UpgradeStatusType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App({officialThemes, zapierTemplates, upgradeStatus, darkMode, fetchKoenigLexical, ...props}: AppProps) {
|
function App({framework, designSystem, officialThemes, zapierTemplates, upgradeStatus}: AppProps) {
|
||||||
return (
|
return (
|
||||||
<FrameworkProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}} {...props}>
|
<FrameworkProvider basePath='settings' modals={{paths: modalPaths, load: loadModals}} {...framework}>
|
||||||
<SettingsAppProvider officialThemes={officialThemes} upgradeStatus={upgradeStatus} zapierTemplates={zapierTemplates}>
|
<SettingsAppProvider officialThemes={officialThemes} upgradeStatus={upgradeStatus} zapierTemplates={zapierTemplates}>
|
||||||
<DesignSystemApp className='admin-x-settings' darkMode={darkMode} fetchKoenigLexical={fetchKoenigLexical} id='admin-x-settings'>
|
<DesignSystemApp className='admin-x-settings' {...designSystem}>
|
||||||
<SettingsRouter />
|
<SettingsRouter />
|
||||||
<MainContent />
|
<MainContent />
|
||||||
</DesignSystemApp>
|
</DesignSystemApp>
|
||||||
|
@ -77,7 +77,7 @@ const Sidebar: React.FC = () => {
|
|||||||
setFilter(e.target.value);
|
setFilter(e.target.value);
|
||||||
|
|
||||||
if (e.target.value) {
|
if (e.target.value) {
|
||||||
document.getElementById('admin-x-settings')?.scrollTo({top: 0, left: 0});
|
document.querySelector('.admin-x-settings')?.scrollTo({top: 0, left: 0});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,6 +55,10 @@ const features = [{
|
|||||||
title: 'Filter by email disabled',
|
title: 'Filter by email disabled',
|
||||||
description: 'Allows filtering members by email disabled',
|
description: 'Allows filtering members by email disabled',
|
||||||
flag: 'filterEmailDisabled'
|
flag: 'filterEmailDisabled'
|
||||||
|
},{
|
||||||
|
title: 'AdminX Demo',
|
||||||
|
description: 'Adds a navigation link to the AdminX demo app',
|
||||||
|
flag: 'adminXDemo'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const AlphaFeatures: React.FC = () => {
|
const AlphaFeatures: React.FC = () => {
|
||||||
|
@ -21,7 +21,7 @@ export const useScrollSectionContext = () => useContext(ScrollSectionContext);
|
|||||||
const scrollMargin = 193;
|
const scrollMargin = 193;
|
||||||
|
|
||||||
const scrollToSection = (element: HTMLDivElement, doneInitialScroll: boolean) => {
|
const scrollToSection = (element: HTMLDivElement, doneInitialScroll: boolean) => {
|
||||||
const root = document.getElementById('admin-x-settings')!;
|
const root = document.querySelector('.admin-x-settings')!;
|
||||||
const top = element.getBoundingClientRect().top + root.scrollTop;
|
const top = element.getBoundingClientRect().top + root.scrollTop;
|
||||||
|
|
||||||
root.scrollTo({
|
root.scrollTo({
|
||||||
|
@ -8,12 +8,16 @@ import {DefaultHeaderTypes} from './unsplash/UnsplashTypes.ts';
|
|||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App
|
<App
|
||||||
darkMode={false}
|
designSystem={{darkMode: false, fetchKoenigLexical: async () => {}}}
|
||||||
externalNavigate={() => {}}
|
framework={{
|
||||||
fetchKoenigLexical={() => {
|
externalNavigate: () => {},
|
||||||
return Promise.resolve();
|
ghostVersion: '5.x',
|
||||||
|
sentryDSN: null,
|
||||||
|
unsplashConfig: {} as DefaultHeaderTypes,
|
||||||
|
onDelete: () => {},
|
||||||
|
onInvalidate: () => {},
|
||||||
|
onUpdate: () => {}
|
||||||
}}
|
}}
|
||||||
ghostVersion='5.x'
|
|
||||||
officialThemes={[{
|
officialThemes={[{
|
||||||
name: 'Source',
|
name: 'Source',
|
||||||
category: 'News',
|
category: 'News',
|
||||||
@ -53,12 +57,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|||||||
ref: 'TryGhost/Edition',
|
ref: 'TryGhost/Edition',
|
||||||
image: 'assets/img/themes/Edition.png'
|
image: 'assets/img/themes/Edition.png'
|
||||||
}]}
|
}]}
|
||||||
sentryDSN={null}
|
|
||||||
unsplashConfig={{} as DefaultHeaderTypes}
|
|
||||||
zapierTemplates={[]}
|
zapierTemplates={[]}
|
||||||
onDelete={() => {}}
|
|
||||||
onInvalidate={() => {}}
|
|
||||||
onUpdate={() => {}}
|
|
||||||
/>
|
/>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,5 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"]
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts", "package.json"]
|
|
||||||
}
|
|
26
apps/admin-x-settings/vite.config.mjs
Normal file
26
apps/admin-x-settings/vite.config.mjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import adminXViteConfig from '@tryghost/admin-x-framework/vite';
|
||||||
|
import pkg from './package.json';
|
||||||
|
import {resolve} from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default (function viteConfig() {
|
||||||
|
return adminXViteConfig({
|
||||||
|
packageName: pkg.name,
|
||||||
|
entry: resolve(__dirname, 'src/index.tsx'),
|
||||||
|
overrides: {
|
||||||
|
define: {
|
||||||
|
'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
// Shim node modules utilized by the @tryghost/nql package
|
||||||
|
alias: {
|
||||||
|
fs: 'node-shim.cjs',
|
||||||
|
path: 'node-shim.cjs',
|
||||||
|
util: 'node-shim.cjs',
|
||||||
|
// @TODO: Remove this when @tryghost/nql is updated
|
||||||
|
mingo: resolve(__dirname, '../../node_modules/mingo/dist/mingo.js')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
280
ghost/admin/app/components/admin-x/admin-x-component.js
Normal file
280
ghost/admin/app/components/admin-x/admin-x-component.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import * as Sentry from '@sentry/ember';
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import React, {Suspense} from 'react';
|
||||||
|
import config from 'ghost-admin/config/environment';
|
||||||
|
import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical';
|
||||||
|
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {camelize} from '@ember/string';
|
||||||
|
import {inject} from 'ghost-admin/decorators/inject';
|
||||||
|
import {run} from '@ember/runloop';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export const defaultUnsplashHeaders = {
|
||||||
|
Authorization: `Client-ID 8672af113b0a8573edae3aa3713886265d9bb741d707f6c01a486cde8c278980`,
|
||||||
|
'Accept-Version': 'v1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'App-Pragma': 'no-cache',
|
||||||
|
'X-Unsplash-Cache': true
|
||||||
|
};
|
||||||
|
|
||||||
|
class ErrorHandler extends React.Component {
|
||||||
|
state = {
|
||||||
|
hasError: false
|
||||||
|
};
|
||||||
|
|
||||||
|
static getDerivedStateFromError() {
|
||||||
|
return {hasError: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="admin-x-container-error">
|
||||||
|
<div className="admin-x-error">
|
||||||
|
<h1>Loading interrupted</h1>
|
||||||
|
<p>They say life is a series of trials and tribulations. This moment right here? It's a tribulation. Our app was supposed to load, and yet here we are. Loadless. Click back to the dashboard to try again.</p>
|
||||||
|
<a href={ghostPaths().adminRoot}>← Back to the dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const importComponent = async (packageName) => {
|
||||||
|
if (window[packageName]) {
|
||||||
|
return window[packageName];
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = packageName.replace('@tryghost/', '');
|
||||||
|
const configKey = camelize(relativePath);
|
||||||
|
|
||||||
|
if (!config[`${configKey}Filename`] || !config[`${configKey}Hash`]) {
|
||||||
|
throw new Error(`Missing config for ${packageName}. Add it in asset delivery.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = (config.cdnUrl ? `${config.cdnUrl}assets/` : ghostPaths().assetRootWithHost);
|
||||||
|
const url = new URL(`${baseUrl}${relativePath}/${config[`${configKey}Filename`]}?v=${config[`${configKey}Hash`]}`);
|
||||||
|
|
||||||
|
if (url.protocol === 'http:') {
|
||||||
|
window[packageName] = await import(`http://${url.host}${url.pathname}${url.search}`);
|
||||||
|
} else {
|
||||||
|
window[packageName] = await import(`https://${url.host}${url.pathname}${url.search}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window[packageName];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchComponent = function (packageName) {
|
||||||
|
if (!packageName) {
|
||||||
|
throw new Error('Unknown package name. Make sure you set a static packageName property on your AdminX component class');
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = 'pending';
|
||||||
|
let response;
|
||||||
|
|
||||||
|
const suspender = importComponent(packageName).then(
|
||||||
|
(res) => {
|
||||||
|
status = 'success';
|
||||||
|
response = res;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
status = 'error';
|
||||||
|
response = err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const read = () => {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
throw suspender;
|
||||||
|
case 'error':
|
||||||
|
throw response;
|
||||||
|
default:
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {read};
|
||||||
|
};
|
||||||
|
|
||||||
|
const emberDataTypeMapping = {
|
||||||
|
IntegrationsResponseType: {type: 'integration'},
|
||||||
|
InvitesResponseType: {type: 'invite'},
|
||||||
|
OffersResponseType: {type: 'offer'},
|
||||||
|
NewslettersResponseType: {type: 'newsletter'},
|
||||||
|
RecommendationResponseType: {type: 'recommendation'},
|
||||||
|
SettingsResponseType: {type: 'setting', singleton: true},
|
||||||
|
ThemesResponseType: {type: 'theme'},
|
||||||
|
TiersResponseType: {type: 'tier'},
|
||||||
|
UsersResponseType: {type: 'user'},
|
||||||
|
CustomThemeSettingsResponseType: {type: 'custom-theme-setting'}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Abstract class which AdminX components should inherit from
|
||||||
|
export default class AdminXComponent extends Component {
|
||||||
|
@service ajax;
|
||||||
|
@service feature;
|
||||||
|
@service ghostPaths;
|
||||||
|
@service session;
|
||||||
|
@service store;
|
||||||
|
@service settings;
|
||||||
|
@service router;
|
||||||
|
@service membersUtils;
|
||||||
|
@service themeManagement;
|
||||||
|
|
||||||
|
@inject config;
|
||||||
|
|
||||||
|
@tracked display = 'none';
|
||||||
|
|
||||||
|
@action
|
||||||
|
onError(error) {
|
||||||
|
// ensure we're still showing errors in development
|
||||||
|
console.error(error); // eslint-disable-line
|
||||||
|
|
||||||
|
if (this.config.sentry_dsn) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't rethrow, app should attempt to gracefully recover
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate = (dataType, response) => {
|
||||||
|
if (!emberDataTypeMapping[dataType]) {
|
||||||
|
throw new Error(`A mutation updating ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {type, singleton} = emberDataTypeMapping[dataType];
|
||||||
|
|
||||||
|
if (singleton) {
|
||||||
|
// Special singleton objects like settings don't work with pushPayload, we need to add the ID explicitly
|
||||||
|
this.store.push(this.store.serializerFor(type).normalizeSingleResponse(
|
||||||
|
this.store,
|
||||||
|
this.store.modelFor(type),
|
||||||
|
response,
|
||||||
|
null,
|
||||||
|
'queryRecord'
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
this.store.pushPayload(type, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataType === 'SettingsResponseType') {
|
||||||
|
// Blog title is based on settings, but the one stored in config is used instead in various places
|
||||||
|
this.config.blogTitle = response.settings.find(setting => setting.key === 'title').value;
|
||||||
|
|
||||||
|
this.settings.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataType === 'TiersResponseType') {
|
||||||
|
// membersUtils has local state which needs to be updated
|
||||||
|
this.membersUtils.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataType === 'ThemesResponseType') {
|
||||||
|
const activated = response.themes.find(theme => theme.active);
|
||||||
|
|
||||||
|
if (activated) {
|
||||||
|
this.themeManagement.activeTheme = this.store.peekAll('theme').filterBy('name', activated.name).firstObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onInvalidate = (dataType) => {
|
||||||
|
if (!emberDataTypeMapping[dataType]) {
|
||||||
|
throw new Error(`A mutation invalidating ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {type, singleton} = emberDataTypeMapping[dataType];
|
||||||
|
|
||||||
|
if (singleton) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(`An AdminX mutation invalidated ${dataType}, but this is is marked as a singleton and cannot be reloaded in Ember. You probably wanted to use updateQueries instead of invalidateQueries`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run(() => this.store.unloadAll(type));
|
||||||
|
|
||||||
|
if (dataType === 'TiersResponseType') {
|
||||||
|
// membersUtils has local state which needs to be updated
|
||||||
|
this.membersUtils.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onDelete = (dataType, id) => {
|
||||||
|
if (!emberDataTypeMapping[dataType]) {
|
||||||
|
throw new Error(`A mutation deleting ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {type} = emberDataTypeMapping[dataType];
|
||||||
|
|
||||||
|
const record = this.store.peekRecord(type, id);
|
||||||
|
|
||||||
|
if (record) {
|
||||||
|
record.unloadRecord();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
externalNavigate = ({route, models = []}) => {
|
||||||
|
this.router.transitionTo(route, ...models);
|
||||||
|
};
|
||||||
|
|
||||||
|
resource = fetchComponent(this.constructor.packageName);
|
||||||
|
|
||||||
|
AdminXApp = (props) => {
|
||||||
|
const {AdminXApp: _AdminXApp} = this.resource.read();
|
||||||
|
return <_AdminXApp {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Can be overridden by subclasses to add additional props to the React app
|
||||||
|
additionalProps = () => ({});
|
||||||
|
|
||||||
|
ReactComponent = () => {
|
||||||
|
const fallback = (
|
||||||
|
<div className="admin-x-settings-container--loading" style={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingBottom: '8vh'
|
||||||
|
}}>
|
||||||
|
<video width="100" height="100" loop autoPlay muted playsInline preload="metadata" style={{
|
||||||
|
width: '100px',
|
||||||
|
height: '100px'
|
||||||
|
}}>
|
||||||
|
<source src="assets/videos/logo-loader.mp4" type="video/mp4" />
|
||||||
|
<div className="gh-loading-spinner"></div>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className={['admin-x-settings-container-', (this.feature.nightShift && 'dark'), this.args.className].filter(Boolean).join(' ')}>
|
||||||
|
<ErrorHandler>
|
||||||
|
<Suspense fallback={fallback}>
|
||||||
|
<this.AdminXApp
|
||||||
|
framework={{
|
||||||
|
ghostVersion: config.APP.version,
|
||||||
|
externalNavigate: this.externalNavigate,
|
||||||
|
unsplashConfig: defaultUnsplashHeaders,
|
||||||
|
sentryDSN: this.config.sentry_dsn ?? null,
|
||||||
|
onUpdate: this.onUpdate,
|
||||||
|
onInvalidate: this.onInvalidate,
|
||||||
|
onDelete: this.onDelete
|
||||||
|
}}
|
||||||
|
designSystem={{
|
||||||
|
fetchKoenigLexical: fetchKoenigLexical,
|
||||||
|
darkMode: this.feature.nightShift
|
||||||
|
}}
|
||||||
|
{...this.additionalProps()}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorHandler>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
1
ghost/admin/app/components/admin-x/demo.hbs
Normal file
1
ghost/admin/app/components/admin-x/demo.hbs
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div {{react-render this.ReactComponent}}></div>
|
8
ghost/admin/app/components/admin-x/demo.js
Normal file
8
ghost/admin/app/components/admin-x/demo.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import AdminXComponent from './admin-x-component';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class AdminXDemo extends AdminXComponent {
|
||||||
|
@service upgradeStatus;
|
||||||
|
|
||||||
|
static packageName = '@tryghost/admin-x-demo';
|
||||||
|
}
|
@ -1,14 +1,5 @@
|
|||||||
import * as Sentry from '@sentry/ember';
|
import AdminXComponent from './admin-x-component';
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import React, {Suspense} from 'react';
|
|
||||||
import config from 'ghost-admin/config/environment';
|
|
||||||
import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical';
|
|
||||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {inject} from 'ghost-admin/decorators/inject';
|
|
||||||
import {run} from '@ember/runloop';
|
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {tracked} from '@glimmer/tracking';
|
|
||||||
|
|
||||||
// TODO: Long term move asset management directly in AdminX
|
// TODO: Long term move asset management directly in AdminX
|
||||||
const officialThemes = [{
|
const officialThemes = [{
|
||||||
@ -196,254 +187,14 @@ const zapierTemplates = [{
|
|||||||
url: 'https://zapier.com/webintent/create-zap?template=359342'
|
url: 'https://zapier.com/webintent/create-zap?template=359342'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
export const defaultUnsplashHeaders = {
|
export default class AdminXSettings extends AdminXComponent {
|
||||||
Authorization: `Client-ID 8672af113b0a8573edae3aa3713886265d9bb741d707f6c01a486cde8c278980`,
|
|
||||||
'Accept-Version': 'v1',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'App-Pragma': 'no-cache',
|
|
||||||
'X-Unsplash-Cache': true
|
|
||||||
};
|
|
||||||
|
|
||||||
class ErrorHandler extends React.Component {
|
|
||||||
state = {
|
|
||||||
hasError: false
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromError() {
|
|
||||||
return {hasError: true};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return (
|
|
||||||
<div className="admin-x-settings-container-error">
|
|
||||||
<div className="admin-x-settings-error">
|
|
||||||
<h1>Loading interrupted</h1>
|
|
||||||
<p>They say life is a series of trials and tribulations. This moment right here? It's a tribulation. Our app was supposed to load, and yet here we are. Loadless. Click back to the dashboard to try again.</p>
|
|
||||||
<a href={ghostPaths().adminRoot}>← Back to the dashboard</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const importSettings = async () => {
|
|
||||||
if (window['@tryghost/admin-x-settings']) {
|
|
||||||
return window['@tryghost/admin-x-settings'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrl = (config.cdnUrl ? `${config.cdnUrl}assets/` : ghostPaths().assetRootWithHost);
|
|
||||||
const url = new URL(`${baseUrl}admin-x-settings/${config.adminXSettingsFilename}?v=${config.adminXSettingsHash}`);
|
|
||||||
|
|
||||||
if (url.protocol === 'http:') {
|
|
||||||
window['@tryghost/admin-x-settings'] = await import(`http://${url.host}${url.pathname}${url.search}`);
|
|
||||||
} else {
|
|
||||||
window['@tryghost/admin-x-settings'] = await import(`https://${url.host}${url.pathname}${url.search}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return window['@tryghost/admin-x-settings'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchSettings = function () {
|
|
||||||
let status = 'pending';
|
|
||||||
let response;
|
|
||||||
|
|
||||||
const suspender = importSettings().then(
|
|
||||||
(res) => {
|
|
||||||
status = 'success';
|
|
||||||
response = res;
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
status = 'error';
|
|
||||||
response = err;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const read = () => {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
throw suspender;
|
|
||||||
case 'error':
|
|
||||||
throw response;
|
|
||||||
default:
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {read};
|
|
||||||
};
|
|
||||||
|
|
||||||
const emberDataTypeMapping = {
|
|
||||||
IntegrationsResponseType: {type: 'integration'},
|
|
||||||
InvitesResponseType: {type: 'invite'},
|
|
||||||
OffersResponseType: {type: 'offer'},
|
|
||||||
NewslettersResponseType: {type: 'newsletter'},
|
|
||||||
RecommendationResponseType: {type: 'recommendation'},
|
|
||||||
SettingsResponseType: {type: 'setting', singleton: true},
|
|
||||||
ThemesResponseType: {type: 'theme'},
|
|
||||||
TiersResponseType: {type: 'tier'},
|
|
||||||
UsersResponseType: {type: 'user'},
|
|
||||||
CustomThemeSettingsResponseType: {type: 'custom-theme-setting'}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class AdminXSettings extends Component {
|
|
||||||
@service ajax;
|
|
||||||
@service feature;
|
|
||||||
@service ghostPaths;
|
|
||||||
@service session;
|
|
||||||
@service store;
|
|
||||||
@service settings;
|
|
||||||
@service router;
|
|
||||||
@service membersUtils;
|
|
||||||
@service themeManagement;
|
|
||||||
@service upgradeStatus;
|
@service upgradeStatus;
|
||||||
|
|
||||||
@inject config;
|
static packageName = '@tryghost/admin-x-settings';
|
||||||
|
|
||||||
@tracked display = 'none';
|
additionalProps = () => ({
|
||||||
|
officialThemes,
|
||||||
@action
|
zapierTemplates,
|
||||||
onError(error) {
|
upgradeStatus: this.upgradeStatus
|
||||||
// ensure we're still showing errors in development
|
});
|
||||||
console.error(error); // eslint-disable-line
|
|
||||||
|
|
||||||
if (this.config.sentry_dsn) {
|
|
||||||
Sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't rethrow, app should attempt to gracefully recover
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate = (dataType, response) => {
|
|
||||||
if (!emberDataTypeMapping[dataType]) {
|
|
||||||
throw new Error(`A mutation updating ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {type, singleton} = emberDataTypeMapping[dataType];
|
|
||||||
|
|
||||||
if (singleton) {
|
|
||||||
// Special singleton objects like settings don't work with pushPayload, we need to add the ID explicitly
|
|
||||||
this.store.push(this.store.serializerFor(type).normalizeSingleResponse(
|
|
||||||
this.store,
|
|
||||||
this.store.modelFor(type),
|
|
||||||
response,
|
|
||||||
null,
|
|
||||||
'queryRecord'
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
this.store.pushPayload(type, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataType === 'SettingsResponseType') {
|
|
||||||
// Blog title is based on settings, but the one stored in config is used instead in various places
|
|
||||||
this.config.blogTitle = response.settings.find(setting => setting.key === 'title').value;
|
|
||||||
|
|
||||||
this.settings.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataType === 'TiersResponseType') {
|
|
||||||
// membersUtils has local state which needs to be updated
|
|
||||||
this.membersUtils.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataType === 'ThemesResponseType') {
|
|
||||||
const activated = response.themes.find(theme => theme.active);
|
|
||||||
|
|
||||||
if (activated) {
|
|
||||||
this.themeManagement.activeTheme = this.store.peekAll('theme').filterBy('name', activated.name).firstObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onInvalidate = (dataType) => {
|
|
||||||
if (!emberDataTypeMapping[dataType]) {
|
|
||||||
throw new Error(`A mutation invalidating ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {type, singleton} = emberDataTypeMapping[dataType];
|
|
||||||
|
|
||||||
if (singleton) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(`An AdminX mutation invalidated ${dataType}, but this is is marked as a singleton and cannot be reloaded in Ember. You probably wanted to use updateQueries instead of invalidateQueries`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
run(() => this.store.unloadAll(type));
|
|
||||||
|
|
||||||
if (dataType === 'TiersResponseType') {
|
|
||||||
// membersUtils has local state which needs to be updated
|
|
||||||
this.membersUtils.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onDelete = (dataType, id) => {
|
|
||||||
if (!emberDataTypeMapping[dataType]) {
|
|
||||||
throw new Error(`A mutation deleting ${dataType} succeeded in AdminX but there is no mapping to an Ember type. Add one to emberDataTypeMapping`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {type} = emberDataTypeMapping[dataType];
|
|
||||||
|
|
||||||
const record = this.store.peekRecord(type, id);
|
|
||||||
|
|
||||||
if (record) {
|
|
||||||
record.unloadRecord();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
externalNavigate = ({route, models = []}) => {
|
|
||||||
this.router.transitionTo(route, ...models);
|
|
||||||
};
|
|
||||||
|
|
||||||
editorResource = fetchSettings();
|
|
||||||
|
|
||||||
AdminXApp = (props) => {
|
|
||||||
const {AdminXApp: _AdminXApp} = this.editorResource.read();
|
|
||||||
return <_AdminXApp {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactComponent = () => {
|
|
||||||
const fallback = (
|
|
||||||
<div className="admin-x-settings-container--loading" style={{
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingBottom: '8vh'
|
|
||||||
}}>
|
|
||||||
<video width="100" height="100" loop autoPlay muted playsInline preload="metadata" style={{
|
|
||||||
width: '100px',
|
|
||||||
height: '100px'
|
|
||||||
}}>
|
|
||||||
<source src="assets/videos/logo-loader.mp4" type="video/mp4" />
|
|
||||||
<div className="gh-loading-spinner"></div>
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className={['admin-x-settings-container-', (this.feature.nightShift && 'dark'), this.args.className].filter(Boolean).join(' ')}>
|
|
||||||
<ErrorHandler>
|
|
||||||
<Suspense fallback={fallback}>
|
|
||||||
<this.AdminXApp
|
|
||||||
ghostVersion={config.APP.version}
|
|
||||||
officialThemes={officialThemes}
|
|
||||||
zapierTemplates={zapierTemplates}
|
|
||||||
externalNavigate={this.externalNavigate}
|
|
||||||
darkMode={this.feature.nightShift}
|
|
||||||
unsplashConfig={defaultUnsplashHeaders}
|
|
||||||
sentryDSN={this.config.sentry_dsn ?? null}
|
|
||||||
fetchKoenigLexical={fetchKoenigLexical}
|
|
||||||
onUpdate={this.onUpdate}
|
|
||||||
onInvalidate={this.onInvalidate}
|
|
||||||
onDelete={this.onDelete}
|
|
||||||
upgradeStatus={this.upgradeStatus}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</ErrorHandler>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,11 @@
|
|||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (feature "adminXDemo")}}
|
||||||
|
<li>
|
||||||
|
<LinkTo @route="demo-x" @current-when="demo-x">{{svg-jar "star"}}AdminX Demo</LinkTo>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{#if this.session.user.isOwnerOnly}}
|
{{#if this.session.user.isOwnerOnly}}
|
||||||
|
3
ghost/admin/app/controllers/demo-x.js
Normal file
3
ghost/admin/app/controllers/demo-x.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
|
export default class DemoXController extends Controller {}
|
@ -48,6 +48,10 @@ Router.map(function () {
|
|||||||
this.route('collection.new', {path: '/collections/new'});
|
this.route('collection.new', {path: '/collections/new'});
|
||||||
this.route('collection', {path: '/collections/:collection_slug'});
|
this.route('collection', {path: '/collections/:collection_slug'});
|
||||||
|
|
||||||
|
this.route('demo-x', function () {
|
||||||
|
this.route('demo-x', {path: '/*sub'});
|
||||||
|
});
|
||||||
|
|
||||||
this.route('settings-x', {path: '/settings'}, function () {
|
this.route('settings-x', {path: '/settings'}, function () {
|
||||||
this.route('settings-x', {path: '/*sub'});
|
this.route('settings-x', {path: '/*sub'});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as Sentry from '@sentry/ember';
|
import * as Sentry from '@sentry/ember';
|
||||||
|
import AdminXSettings from '../components/admin-x/settings';
|
||||||
import AuthConfiguration from 'ember-simple-auth/configuration';
|
import AuthConfiguration from 'ember-simple-auth/configuration';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
@ -7,7 +8,7 @@ import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route';
|
|||||||
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
||||||
import windowProxy from 'ghost-admin/utils/window-proxy';
|
import windowProxy from 'ghost-admin/utils/window-proxy';
|
||||||
import {Debug} from '@sentry/integrations';
|
import {Debug} from '@sentry/integrations';
|
||||||
import {importSettings} from '../components/admin-x/settings';
|
import {importComponent} from '../components/admin-x/admin-x-component';
|
||||||
import {inject} from 'ghost-admin/decorators/inject';
|
import {inject} from 'ghost-admin/decorators/inject';
|
||||||
import {
|
import {
|
||||||
isAjaxError,
|
isAjaxError,
|
||||||
@ -239,7 +240,9 @@ export default Route.extend(ShortcutsRoute, {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preload settings to avoid a delay when opening
|
// Preload settings to avoid a delay when opening
|
||||||
setTimeout(importSettings, 1000);
|
setTimeout(() => {
|
||||||
|
importComponent(AdminXSettings.packageName);
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
3
ghost/admin/app/routes/demo-x.js
Normal file
3
ghost/admin/app/routes/demo-x.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
|
||||||
|
export default class DemoXRoute extends AuthenticatedRoute {}
|
@ -79,6 +79,7 @@ export default class FeatureService extends Service {
|
|||||||
@feature('lexicalIndicators') lexicalIndicators;
|
@feature('lexicalIndicators') lexicalIndicators;
|
||||||
@feature('editorEmojiPicker') editorEmojiPicker;
|
@feature('editorEmojiPicker') editorEmojiPicker;
|
||||||
@feature('filterEmailDisabled') filterEmailDisabled;
|
@feature('filterEmailDisabled') filterEmailDisabled;
|
||||||
|
@feature('adminXDemo') adminXDemo;
|
||||||
|
|
||||||
_user = null;
|
_user = null;
|
||||||
|
|
||||||
|
@ -2211,9 +2211,9 @@ section.gh-ds h2 {
|
|||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-x-settings-container-error {
|
.admin-x-container-error {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -2221,7 +2221,7 @@ section.gh-ds h2 {
|
|||||||
background: var(--whitegrey-l2);
|
background: var(--whitegrey-l2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-x-settings-error {
|
.admin-x-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -2234,13 +2234,13 @@ section.gh-ds h2 {
|
|||||||
color: var(--darkgrey);
|
color: var(--darkgrey);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-x-settings-container-error p,
|
.admin-x-container-error p,
|
||||||
.admin-x-settings-container-error h1 {
|
.admin-x-container-error h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-x-settings-container-error a {
|
.admin-x-container-error a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border: 1px solid var(--green);
|
border: 1px solid var(--green);
|
||||||
|
1
ghost/admin/app/templates/demo-x.hbs
Normal file
1
ghost/admin/app/templates/demo-x.hbs
Normal file
@ -0,0 +1 @@
|
|||||||
|
<AdminX::Demo />
|
@ -4,8 +4,9 @@
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const camelCase = require('lodash/camelCase');
|
||||||
|
|
||||||
const adminXSettingsPath = '../../apps/admin-x-settings/dist';
|
const adminXApps = ['admin-x-demo', 'admin-x-settings'];
|
||||||
|
|
||||||
function generateHash(filePath) {
|
function generateHash(filePath) {
|
||||||
const fileContents = fs.readFileSync(filePath, 'utf8');
|
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||||
@ -31,9 +32,12 @@ module.exports = {
|
|||||||
this.packageConfig['editorHash'] = process.env.EDITOR_URL ? 'development' : generateHash(koenigLexicalPath);
|
this.packageConfig['editorHash'] = process.env.EDITOR_URL ? 'development' : generateHash(koenigLexicalPath);
|
||||||
|
|
||||||
// TODO: ideally take this from the package, but that's broken thanks to .cjs file ext
|
// TODO: ideally take this from the package, but that's broken thanks to .cjs file ext
|
||||||
const defaultAdminXSettingFilename = 'admin-x-settings.js';
|
for (const app of adminXApps) {
|
||||||
this.packageConfig['adminXSettingsFilename'] = defaultAdminXSettingFilename;
|
const defaultFilename = `${app}.js`;
|
||||||
this.packageConfig['adminXSettingsHash'] = (this.env === 'production') ? generateHash(path.join(adminXSettingsPath, defaultAdminXSettingFilename)) : 'development';
|
const configName = camelCase(app);
|
||||||
|
this.packageConfig[`${configName}Filename`] = defaultFilename;
|
||||||
|
this.packageConfig[`${configName}Hash`] = (this.env === 'production') ? generateHash(path.join(`../../apps/${app}/dist`, defaultFilename)) : 'development';
|
||||||
|
}
|
||||||
|
|
||||||
if (this.env === 'production') {
|
if (this.env === 'production') {
|
||||||
console.log('Admin-X Settings:', this.packageConfig['adminXSettingsFilename'], this.packageConfig['adminXSettingsHash']);
|
console.log('Admin-X Settings:', this.packageConfig['adminXSettingsFilename'], this.packageConfig['adminXSettingsHash']);
|
||||||
@ -84,17 +88,19 @@ module.exports = {
|
|||||||
fs.copySync(`${results.directory}/assets/${relativePath}`, `${assetsOut}/assets/${relativePath}`, {overwrite: true, dereference: true});
|
fs.copySync(`${results.directory}/assets/${relativePath}`, `${assetsOut}/assets/${relativePath}`, {overwrite: true, dereference: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
// copy the @tryghost/admin-x-settings assets
|
// copy assets for each admin-x app
|
||||||
const assetsAdminXPath = `${assetsOut}/assets/admin-x-settings`;
|
for (const app of adminXApps) {
|
||||||
|
const adminXPath = `../../apps/${app}/dist`;
|
||||||
if (fs.existsSync(adminXSettingsPath)) {
|
const assetsAdminXPath = `${assetsOut}/assets/${app}`;
|
||||||
if (this.env === 'production') {
|
if (fs.existsSync(adminXPath)) {
|
||||||
fs.copySync(adminXSettingsPath, assetsAdminXPath, {overwrite: true, dereference: true});
|
if (this.env === 'production') {
|
||||||
} else {
|
fs.copySync(adminXPath, assetsAdminXPath, {overwrite: true, dereference: true});
|
||||||
fs.ensureSymlinkSync(adminXSettingsPath, assetsAdminXPath);
|
} else {
|
||||||
|
fs.ensureSymlinkSync(adminXPath, assetsAdminXPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`${app} folder not found`);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('Admin-X-Settings folder not found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we are passed a URL for Koenig-Lexical dev server, we don't need to copy the assets
|
// if we are passed a URL for Koenig-Lexical dev server, we don't need to copy the assets
|
||||||
|
@ -46,7 +46,9 @@ const ALPHA_FEATURES = [
|
|||||||
'importMemberTier',
|
'importMemberTier',
|
||||||
'lexicalIndicators',
|
'lexicalIndicators',
|
||||||
'editorEmojiPicker',
|
'editorEmojiPicker',
|
||||||
'adminXOffers'
|
'adminXOffers',
|
||||||
|
'filterEmailDisabled',
|
||||||
|
'adminXDemo'
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||||
|
Loading…
Reference in New Issue
Block a user