From a93c665d20e35513a7f357c37d60c7439559e91c Mon Sep 17 00:00:00 2001 From: Jono M Date: Mon, 20 Nov 2023 13:30:15 +0000 Subject: [PATCH] Created a skeleton AdminX demo app (#19005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://github.com/TryGhost/Product/issues/4152 --- ### 🤖[[deprecated]](https://githubnext.com/copilot-for-prs-sunset) Generated by Copilot at a28462f 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. --- .github/scripts/dev.js | 4 +- apps/admin-x-demo/.eslintignore | 1 + apps/admin-x-demo/.eslintrc.cjs | 56 ++++ apps/admin-x-demo/.gitignore | 1 + apps/admin-x-demo/package.json | 54 ++++ apps/admin-x-demo/postcss.config.cjs | 1 + apps/admin-x-demo/src/App.tsx | 27 ++ apps/admin-x-demo/src/MainContent.tsx | 12 + .../admin-x-demo/src/components/DemoModal.tsx | 20 ++ apps/admin-x-demo/src/components/modals.tsx | 9 + apps/admin-x-demo/src/index.tsx | 6 + apps/admin-x-demo/src/styles/index.css | 1 + apps/admin-x-demo/tailwind.config.cjs | 6 + apps/admin-x-demo/tsconfig.json | 23 ++ apps/admin-x-demo/vite.config.mjs | 10 + .../{postcss.config.js => postcss.config.cjs} | 2 +- .../src/global/modal/Modal.tsx | 2 +- apps/admin-x-framework/package.json | 19 +- apps/admin-x-framework/src/index.ts | 2 +- .../src/providers/FrameworkProvider.tsx | 3 + .../src/providers/RoutingProvider.tsx | 6 +- apps/admin-x-framework/src/routing.ts | 2 +- .../src/vite.ts} | 35 +-- apps/admin-x-framework/vite.config.ts | 4 - apps/admin-x-settings/postcss.config.cjs | 9 +- apps/admin-x-settings/src/App.tsx | 16 +- .../src/components/Sidebar.tsx | 2 +- .../settings/advanced/labs/AlphaFeatures.tsx | 4 + .../src/hooks/useScrollSection.tsx | 2 +- apps/admin-x-settings/src/main.tsx | 19 +- apps/admin-x-settings/tsconfig.json | 3 +- apps/admin-x-settings/tsconfig.node.json | 10 - apps/admin-x-settings/vite.config.mjs | 26 ++ .../components/admin-x/admin-x-component.js | 280 ++++++++++++++++++ ghost/admin/app/components/admin-x/demo.hbs | 1 + ghost/admin/app/components/admin-x/demo.js | 8 + .../admin/app/components/admin-x/settings.js | 265 +---------------- .../admin/app/components/gh-nav-menu/main.hbs | 5 + ghost/admin/app/controllers/demo-x.js | 3 + ghost/admin/app/router.js | 4 + ghost/admin/app/routes/application.js | 7 +- ghost/admin/app/routes/demo-x.js | 3 + ghost/admin/app/services/feature.js | 1 + ghost/admin/app/styles/layouts/main.css | 14 +- ghost/admin/app/templates/demo-x.hbs | 1 + ghost/admin/lib/asset-delivery/index.js | 34 ++- ghost/core/core/shared/labs.js | 4 +- 47 files changed, 667 insertions(+), 360 deletions(-) create mode 100644 apps/admin-x-demo/.eslintignore create mode 100644 apps/admin-x-demo/.eslintrc.cjs create mode 100644 apps/admin-x-demo/.gitignore create mode 100644 apps/admin-x-demo/package.json create mode 100644 apps/admin-x-demo/postcss.config.cjs create mode 100644 apps/admin-x-demo/src/App.tsx create mode 100644 apps/admin-x-demo/src/MainContent.tsx create mode 100644 apps/admin-x-demo/src/components/DemoModal.tsx create mode 100644 apps/admin-x-demo/src/components/modals.tsx create mode 100644 apps/admin-x-demo/src/index.tsx create mode 100644 apps/admin-x-demo/src/styles/index.css create mode 100644 apps/admin-x-demo/tailwind.config.cjs create mode 100644 apps/admin-x-demo/tsconfig.json create mode 100644 apps/admin-x-demo/vite.config.mjs rename apps/admin-x-design-system/{postcss.config.js => postcss.config.cjs} (87%) rename apps/{admin-x-settings/vite.config.ts => admin-x-framework/src/vite.ts} (73%) delete mode 100644 apps/admin-x-settings/tsconfig.node.json create mode 100644 apps/admin-x-settings/vite.config.mjs create mode 100644 ghost/admin/app/components/admin-x/admin-x-component.js create mode 100644 ghost/admin/app/components/admin-x/demo.hbs create mode 100644 ghost/admin/app/components/admin-x/demo.js create mode 100644 ghost/admin/app/controllers/demo-x.js create mode 100644 ghost/admin/app/routes/demo-x.js create mode 100644 ghost/admin/app/templates/demo-x.hbs diff --git a/.github/scripts/dev.js b/.github/scripts/dev.js index c7062494f6..fb7ded3ef7 100644 --- a/.github/scripts/dev.js +++ b/.github/scripts/dev.js @@ -51,6 +51,8 @@ const COMMAND_TYPESCRIPT = { env: {} }; +const adminXApps = '@tryghost/admin-x-demo,@tryghost/admin-x-settings'; + const COMMANDS_ADMINX = [{ name: 'adminXDeps', 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: {} }, { 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'), prefixColor: '#C35831', env: {} diff --git a/apps/admin-x-demo/.eslintignore b/apps/admin-x-demo/.eslintignore new file mode 100644 index 0000000000..9944eccea2 --- /dev/null +++ b/apps/admin-x-demo/.eslintignore @@ -0,0 +1 @@ +tailwind.config.cjs diff --git a/apps/admin-x-demo/.eslintrc.cjs b/apps/admin-x-demo/.eslintrc.cjs new file mode 100644 index 0000000000..919b0f2cdf --- /dev/null +++ b/apps/admin-x-demo/.eslintrc.cjs @@ -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'}] + } +}; diff --git a/apps/admin-x-demo/.gitignore b/apps/admin-x-demo/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/apps/admin-x-demo/.gitignore @@ -0,0 +1 @@ +dist diff --git a/apps/admin-x-demo/package.json b/apps/admin-x-demo/package.json new file mode 100644 index 0000000000..6e01b5b856 --- /dev/null +++ b/apps/admin-x-demo/package.json @@ -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"} + ] + } + } + } +} diff --git a/apps/admin-x-demo/postcss.config.cjs b/apps/admin-x-demo/postcss.config.cjs new file mode 100644 index 0000000000..8799f4acf8 --- /dev/null +++ b/apps/admin-x-demo/postcss.config.cjs @@ -0,0 +1 @@ +module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs'); diff --git a/apps/admin-x-demo/src/App.tsx b/apps/admin-x-demo/src/App.tsx new file mode 100644 index 0000000000..d5f34e0eec --- /dev/null +++ b/apps/admin-x-demo/src/App.tsx @@ -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 = ({framework, designSystem}) => { + return ( + + + + + + ); +}; + +export default App; diff --git a/apps/admin-x-demo/src/MainContent.tsx b/apps/admin-x-demo/src/MainContent.tsx new file mode 100644 index 0000000000..f6f68421bc --- /dev/null +++ b/apps/admin-x-demo/src/MainContent.tsx @@ -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
+
; +}; + +export default MainContent; diff --git a/apps/admin-x-demo/src/components/DemoModal.tsx b/apps/admin-x-demo/src/components/DemoModal.tsx new file mode 100644 index 0000000000..a08a46d43b --- /dev/null +++ b/apps/admin-x-demo/src/components/DemoModal.tsx @@ -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 ( + { + updateRoute(''); + }} + title='Demo modal' + > + Demo modal + + ); +}); + +export default DemoModal; diff --git a/apps/admin-x-demo/src/components/modals.tsx b/apps/admin-x-demo/src/components/modals.tsx new file mode 100644 index 0000000000..353df21d33 --- /dev/null +++ b/apps/admin-x-demo/src/components/modals.tsx @@ -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}; + +export default modals; + +export type ModalName = keyof typeof modals; diff --git a/apps/admin-x-demo/src/index.tsx b/apps/admin-x-demo/src/index.tsx new file mode 100644 index 0000000000..b9bcbb9c5f --- /dev/null +++ b/apps/admin-x-demo/src/index.tsx @@ -0,0 +1,6 @@ +import './styles/index.css'; +import App from './App.tsx'; + +export { + App as AdminXApp +}; diff --git a/apps/admin-x-demo/src/styles/index.css b/apps/admin-x-demo/src/styles/index.css new file mode 100644 index 0000000000..d1f1f198ed --- /dev/null +++ b/apps/admin-x-demo/src/styles/index.css @@ -0,0 +1 @@ +@import '@tryghost/admin-x-design-system/styles.css'; diff --git a/apps/admin-x-demo/tailwind.config.cjs b/apps/admin-x-demo/tailwind.config.cjs new file mode 100644 index 0000000000..23c889a089 --- /dev/null +++ b/apps/admin-x-demo/tailwind.config.cjs @@ -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}'] +}; diff --git a/apps/admin-x-demo/tsconfig.json b/apps/admin-x-demo/tsconfig.json new file mode 100644 index 0000000000..1ebb01c0f4 --- /dev/null +++ b/apps/admin-x-demo/tsconfig.json @@ -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"] +} diff --git a/apps/admin-x-demo/vite.config.mjs b/apps/admin-x-demo/vite.config.mjs new file mode 100644 index 0000000000..ae5b996d87 --- /dev/null +++ b/apps/admin-x-demo/vite.config.mjs @@ -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') + }); +}); diff --git a/apps/admin-x-design-system/postcss.config.js b/apps/admin-x-design-system/postcss.config.cjs similarity index 87% rename from apps/admin-x-design-system/postcss.config.js rename to apps/admin-x-design-system/postcss.config.cjs index 3f15318da0..ab7c4939b1 100644 --- a/apps/admin-x-design-system/postcss.config.js +++ b/apps/admin-x-design-system/postcss.config.cjs @@ -1,4 +1,4 @@ -export default { +module.exports = { plugins: { 'postcss-import': {}, 'tailwindcss/nesting': {}, diff --git a/apps/admin-x-design-system/src/global/modal/Modal.tsx b/apps/admin-x-design-system/src/global/modal/Modal.tsx index 952521fe96..0c8ba75126 100644 --- a/apps/admin-x-design-system/src/global/modal/Modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/Modal.tsx @@ -192,7 +192,7 @@ const Modal: React.FC = ({ ); let backdropClasses = clsx( - 'fixed inset-0 z-40 h-[100vh] w-[100vw]' + 'fixed inset-0 z-[1000] h-[100vh] w-[100vw]' ); let paddingClasses = ''; diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index c37078f8ff..4a7fd75707 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -29,6 +29,10 @@ "./api/*": { "import": "./es/api/*.js", "types": "./types/api/*.d.ts" + }, + "./vite": { + "import": "./es/vite.js", + "types": "./types/vite.d.ts" } }, "sideEffects": false, @@ -49,7 +53,6 @@ "devDependencies": { "@testing-library/react": "14.1.0", "@types/mocha": "10.0.1", - "@vitejs/plugin-react": "4.2.0", "c8": "8.0.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", @@ -58,13 +61,21 @@ "react-dom": "18.2.0", "sinon": "17.0.0", "ts-node": "10.9.1", - "typescript": "5.2.2", - "vite": "4.5.0" + "typescript": "5.2.2" }, "dependencies": { "@sentry/react": "7.80.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": { "react": "^18.2.0", diff --git a/apps/admin-x-framework/src/index.ts b/apps/admin-x-framework/src/index.ts index f6935b425f..ba48c23dee 100644 --- a/apps/admin-x-framework/src/index.ts +++ b/apps/admin-x-framework/src/index.ts @@ -1,5 +1,5 @@ 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 type {InfiniteData} from '@tanstack/react-query'; diff --git a/apps/admin-x-framework/src/providers/FrameworkProvider.tsx b/apps/admin-x-framework/src/providers/FrameworkProvider.tsx index 11c57b5b64..b94292ab37 100644 --- a/apps/admin-x-framework/src/providers/FrameworkProvider.tsx +++ b/apps/admin-x-framework/src/providers/FrameworkProvider.tsx @@ -24,6 +24,9 @@ export interface FrameworkProviderProps { children: ReactNode; } +// children, basePath and modals should be provided by each app, while others are passed in from Ember +export type TopLevelFrameworkProps = Omit; + export type FrameworkContextType = Omit; const FrameworkContext = createContext({ diff --git a/apps/admin-x-framework/src/providers/RoutingProvider.tsx b/apps/admin-x-framework/src/providers/RoutingProvider.tsx index 84af39f483..ff652dcdfe 100644 --- a/apps/admin-x-framework/src/providers/RoutingProvider.tsx +++ b/apps/admin-x-framework/src/providers/RoutingProvider.tsx @@ -115,13 +115,13 @@ const RoutingProvider: React.FC = ({basePath, externalNavi if (newPath === route) { // No change } else if (newPath) { - window.location.hash = `/settings/${newPath}`; + window.location.hash = `/${basePath}/${newPath}`; } else { - window.location.hash = `/settings`; + window.location.hash = `/${basePath}`; } eventTarget.dispatchEvent(new CustomEvent('routeChange', {detail: {newPath, oldPath: route}})); - }, [eventTarget, externalNavigate, route]); + }, [basePath, eventTarget, externalNavigate, route]); useEffect(() => { // Preload all the modals after initial render to avoid a delay when opening them diff --git a/apps/admin-x-framework/src/routing.ts b/apps/admin-x-framework/src/routing.ts index 7dc623213f..fdcf78e26e 100644 --- a/apps/admin-x-framework/src/routing.ts +++ b/apps/admin-x-framework/src/routing.ts @@ -1,3 +1,3 @@ export {useRouteChangeCallback, useRouting} from './providers/RoutingProvider'; -export type {ExternalLink, InternalLink, RoutingModalProps} from './providers/RoutingProvider'; +export type {ExternalLink, InternalLink, ModalComponent, RoutingModalProps} from './providers/RoutingProvider'; diff --git a/apps/admin-x-settings/vite.config.ts b/apps/admin-x-framework/src/vite.ts similarity index 73% rename from apps/admin-x-settings/vite.config.ts rename to apps/admin-x-framework/src/vite.ts index f2ab4dc1da..b5af10ccf3 100644 --- a/apps/admin-x-settings/vite.config.ts +++ b/apps/admin-x-framework/src/vite.ts @@ -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 {PluginOption, UserConfig, mergeConfig} from 'vite'; +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; import svgr from 'vite-plugin-svgr'; -import {PluginOption} from 'vite'; 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 }): PluginOption => { return { @@ -32,8 +28,10 @@ const externalPlugin = ({externals}: { externals: Record }): Plu }; // https://vitejs.dev/config/ -export default (function viteConfig() { - return defineConfig({ +export default function adminXViteConfig({packageName, entry, overrides}: {packageName: string; entry: string; overrides?: UserConfig}) { + const outputFileName = packageName[0] === '@' ? packageName.slice(packageName.indexOf('/') + 1) : packageName; + + const defaultConfig = defineConfig({ logLevel: process.env.CI ? 'info' : 'warn', plugins: [ svgr(), @@ -48,8 +46,7 @@ export default (function viteConfig() { ], define: { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), - 'process.env.VITEST_SEGFAULT_RETRY': 3, - 'process.env.DEBUG': false // Shim env var utilized by the @tryghost/nql package + 'process.env.VITEST_SEGFAULT_RETRY': 3 }, preview: { port: 4174 @@ -60,8 +57,8 @@ export default (function viteConfig() { sourcemap: true, lib: { formats: ['es'], - entry: resolve(__dirname, 'src/index.tsx'), - name: pkg.name, + entry, + name: packageName, fileName(format) { if (format === 'umd') { return `${outputFileName}.umd.js`; @@ -83,16 +80,8 @@ export default (function viteConfig() { minThreads: 1, 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 || {}); +}; diff --git a/apps/admin-x-framework/vite.config.ts b/apps/admin-x-framework/vite.config.ts index 349a3df7d6..884c831255 100644 --- a/apps/admin-x-framework/vite.config.ts +++ b/apps/admin-x-framework/vite.config.ts @@ -10,10 +10,6 @@ export default (function viteConfig() { plugins: [ react() ], - define: { - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), - 'process.env.VITEST_SEGFAULT_RETRY': 3 - }, preview: { port: 4174 }, diff --git a/apps/admin-x-settings/postcss.config.cjs b/apps/admin-x-settings/postcss.config.cjs index ab7c4939b1..8799f4acf8 100644 --- a/apps/admin-x-settings/postcss.config.cjs +++ b/apps/admin-x-settings/postcss.config.cjs @@ -1,8 +1 @@ -module.exports = { - plugins: { - 'postcss-import': {}, - 'tailwindcss/nesting': {}, - tailwindcss: {}, - autoprefixer: {} - } -}; +module.exports = require('@tryghost/admin-x-design-system/postcss.config.cjs'); diff --git a/apps/admin-x-settings/src/App.tsx b/apps/admin-x-settings/src/App.tsx index 19049d79ba..1a7db264c9 100644 --- a/apps/admin-x-settings/src/App.tsx +++ b/apps/admin-x-settings/src/App.tsx @@ -1,23 +1,23 @@ import MainContent from './MainContent'; 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 {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system'; +import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; import {ZapierTemplate} from './components/settings/advanced/integrations/ZapierModal'; -interface AppProps extends Omit { +interface AppProps { + framework: TopLevelFrameworkProps; + designSystem: DesignSystemAppProps; officialThemes: OfficialTheme[]; zapierTemplates: ZapierTemplate[]; - darkMode: boolean; - fetchKoenigLexical: FetchKoenigLexical; upgradeStatus?: UpgradeStatusType; } -function App({officialThemes, zapierTemplates, upgradeStatus, darkMode, fetchKoenigLexical, ...props}: AppProps) { +function App({framework, designSystem, officialThemes, zapierTemplates, upgradeStatus}: AppProps) { return ( - + - + diff --git a/apps/admin-x-settings/src/components/Sidebar.tsx b/apps/admin-x-settings/src/components/Sidebar.tsx index 6c23e4337e..5be099d112 100644 --- a/apps/admin-x-settings/src/components/Sidebar.tsx +++ b/apps/admin-x-settings/src/components/Sidebar.tsx @@ -77,7 +77,7 @@ const Sidebar: React.FC = () => { setFilter(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}); } }; diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx index 882770b8bb..00493b9648 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx @@ -55,6 +55,10 @@ const features = [{ title: 'Filter by email disabled', description: 'Allows filtering members by email disabled', flag: 'filterEmailDisabled' +},{ + title: 'AdminX Demo', + description: 'Adds a navigation link to the AdminX demo app', + flag: 'adminXDemo' }]; const AlphaFeatures: React.FC = () => { diff --git a/apps/admin-x-settings/src/hooks/useScrollSection.tsx b/apps/admin-x-settings/src/hooks/useScrollSection.tsx index 91fa5759db..2cd677c153 100644 --- a/apps/admin-x-settings/src/hooks/useScrollSection.tsx +++ b/apps/admin-x-settings/src/hooks/useScrollSection.tsx @@ -21,7 +21,7 @@ export const useScrollSectionContext = () => useContext(ScrollSectionContext); const scrollMargin = 193; 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; root.scrollTo({ diff --git a/apps/admin-x-settings/src/main.tsx b/apps/admin-x-settings/src/main.tsx index a15058704e..44aef09f02 100644 --- a/apps/admin-x-settings/src/main.tsx +++ b/apps/admin-x-settings/src/main.tsx @@ -8,12 +8,16 @@ import {DefaultHeaderTypes} from './unsplash/UnsplashTypes.ts'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( {}} - fetchKoenigLexical={() => { - return Promise.resolve(); + designSystem={{darkMode: false, fetchKoenigLexical: async () => {}}} + framework={{ + externalNavigate: () => {}, + ghostVersion: '5.x', + sentryDSN: null, + unsplashConfig: {} as DefaultHeaderTypes, + onDelete: () => {}, + onInvalidate: () => {}, + onUpdate: () => {} }} - ghostVersion='5.x' officialThemes={[{ name: 'Source', category: 'News', @@ -53,12 +57,7 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ref: 'TryGhost/Edition', image: 'assets/img/themes/Edition.png' }]} - sentryDSN={null} - unsplashConfig={{} as DefaultHeaderTypes} zapierTemplates={[]} - onDelete={() => {}} - onInvalidate={() => {}} - onUpdate={() => {}} /> ); diff --git a/apps/admin-x-settings/tsconfig.json b/apps/admin-x-settings/tsconfig.json index c81ef9f382..1ebb01c0f4 100644 --- a/apps/admin-x-settings/tsconfig.json +++ b/apps/admin-x-settings/tsconfig.json @@ -19,6 +19,5 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["src"] } diff --git a/apps/admin-x-settings/tsconfig.node.json b/apps/admin-x-settings/tsconfig.node.json deleted file mode 100644 index 364bc0ea55..0000000000 --- a/apps/admin-x-settings/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts", "package.json"] -} diff --git a/apps/admin-x-settings/vite.config.mjs b/apps/admin-x-settings/vite.config.mjs new file mode 100644 index 0000000000..27795efe4e --- /dev/null +++ b/apps/admin-x-settings/vite.config.mjs @@ -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') + } + } + } + }); +}); diff --git a/ghost/admin/app/components/admin-x/admin-x-component.js b/ghost/admin/app/components/admin-x/admin-x-component.js new file mode 100644 index 0000000000..c4ebce77fa --- /dev/null +++ b/ghost/admin/app/components/admin-x/admin-x-component.js @@ -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 ( +
+
+

Loading interrupted

+

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.

+ ← Back to the dashboard +
+
+ ); + } + + 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 = ( +
+ +
+ ); + return ( +
+ + + + + +
+ ); + }; +} diff --git a/ghost/admin/app/components/admin-x/demo.hbs b/ghost/admin/app/components/admin-x/demo.hbs new file mode 100644 index 0000000000..ed901509b3 --- /dev/null +++ b/ghost/admin/app/components/admin-x/demo.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/ghost/admin/app/components/admin-x/demo.js b/ghost/admin/app/components/admin-x/demo.js new file mode 100644 index 0000000000..17a6c42b41 --- /dev/null +++ b/ghost/admin/app/components/admin-x/demo.js @@ -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'; +} diff --git a/ghost/admin/app/components/admin-x/settings.js b/ghost/admin/app/components/admin-x/settings.js index 00148d6639..5e46924207 100644 --- a/ghost/admin/app/components/admin-x/settings.js +++ b/ghost/admin/app/components/admin-x/settings.js @@ -1,14 +1,5 @@ -import * as Sentry from '@sentry/ember'; -import Component from '@glimmer/component'; -import React, {Suspense} from 'react'; -import config from 'ghost-admin/config/environment'; -import fetchKoenigLexical from 'ghost-admin/utils/fetch-koenig-lexical'; -import ghostPaths from 'ghost-admin/utils/ghost-paths'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {run} from '@ember/runloop'; +import AdminXComponent from './admin-x-component'; import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; // TODO: Long term move asset management directly in AdminX const officialThemes = [{ @@ -196,254 +187,14 @@ const zapierTemplates = [{ url: 'https://zapier.com/webintent/create-zap?template=359342' }]; -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 ( -
-
-

Loading interrupted

-

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.

- ← Back to the dashboard -
-
- ); - } - - 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; +export default class AdminXSettings extends AdminXComponent { @service upgradeStatus; - @inject config; + static packageName = '@tryghost/admin-x-settings'; - @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); - }; - - editorResource = fetchSettings(); - - AdminXApp = (props) => { - const {AdminXApp: _AdminXApp} = this.editorResource.read(); - return <_AdminXApp {...props} />; - }; - - ReactComponent = () => { - const fallback = ( -
- -
- ); - return ( -
- - - - - -
- ); - }; + additionalProps = () => ({ + officialThemes, + zapierTemplates, + upgradeStatus: this.upgradeStatus + }); } diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index 08d8a63b9c..525fd3d377 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -119,6 +119,11 @@ {{/if}} {{/if}} + {{#if (feature "adminXDemo")}} +
  • + {{svg-jar "star"}}AdminX Demo +
  • + {{/if}} {{#if this.session.user.isOwnerOnly}} diff --git a/ghost/admin/app/controllers/demo-x.js b/ghost/admin/app/controllers/demo-x.js new file mode 100644 index 0000000000..74bb41563e --- /dev/null +++ b/ghost/admin/app/controllers/demo-x.js @@ -0,0 +1,3 @@ +import Controller from '@ember/controller'; + +export default class DemoXController extends Controller {} diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 2cd1878eea..aff3bcd32a 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -48,6 +48,10 @@ Router.map(function () { this.route('collection.new', {path: '/collections/new'}); 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: '/*sub'}); }); diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js index aef9c9ceab..7403f0facd 100644 --- a/ghost/admin/app/routes/application.js +++ b/ghost/admin/app/routes/application.js @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/ember'; +import AdminXSettings from '../components/admin-x/settings'; import AuthConfiguration from 'ember-simple-auth/configuration'; import React from 'react'; 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 windowProxy from 'ghost-admin/utils/window-proxy'; 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 { isAjaxError, @@ -239,7 +240,9 @@ export default Route.extend(ShortcutsRoute, { } // Preload settings to avoid a delay when opening - setTimeout(importSettings, 1000); + setTimeout(() => { + importComponent(AdminXSettings.packageName); + }, 1000); } }); diff --git a/ghost/admin/app/routes/demo-x.js b/ghost/admin/app/routes/demo-x.js new file mode 100644 index 0000000000..ce53c9e0b2 --- /dev/null +++ b/ghost/admin/app/routes/demo-x.js @@ -0,0 +1,3 @@ +import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; + +export default class DemoXRoute extends AuthenticatedRoute {} diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index 3ae605bc96..1c4f42baa4 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -79,6 +79,7 @@ export default class FeatureService extends Service { @feature('lexicalIndicators') lexicalIndicators; @feature('editorEmojiPicker') editorEmojiPicker; @feature('filterEmailDisabled') filterEmailDisabled; + @feature('adminXDemo') adminXDemo; _user = null; diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css index 0625a1c00a..244061d21f 100644 --- a/ghost/admin/app/styles/layouts/main.css +++ b/ghost/admin/app/styles/layouts/main.css @@ -2211,9 +2211,9 @@ section.gh-ds h2 { line-height: 40px; } -.admin-x-settings-container-error { - height: 100vh; - width: 100vw; +.admin-x-container-error { + height: 100%; + width: 100%; display: flex; align-items: center; justify-content: center; @@ -2221,7 +2221,7 @@ section.gh-ds h2 { background: var(--whitegrey-l2); } -.admin-x-settings-error { +.admin-x-error { display: flex; flex-direction: column; align-items: flex-start; @@ -2234,13 +2234,13 @@ section.gh-ds h2 { color: var(--darkgrey); } -.admin-x-settings-container-error p, -.admin-x-settings-container-error h1 { +.admin-x-container-error p, +.admin-x-container-error h1 { margin: 0; padding: 0; } -.admin-x-settings-container-error a { +.admin-x-container-error a { display: inline-block; padding: 6px 10px; border: 1px solid var(--green); diff --git a/ghost/admin/app/templates/demo-x.hbs b/ghost/admin/app/templates/demo-x.hbs new file mode 100644 index 0000000000..3f704286c3 --- /dev/null +++ b/ghost/admin/app/templates/demo-x.hbs @@ -0,0 +1 @@ + diff --git a/ghost/admin/lib/asset-delivery/index.js b/ghost/admin/lib/asset-delivery/index.js index aca83ba7e4..51231ac398 100644 --- a/ghost/admin/lib/asset-delivery/index.js +++ b/ghost/admin/lib/asset-delivery/index.js @@ -4,8 +4,9 @@ const crypto = require('crypto'); const fs = require('fs'); 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) { const fileContents = fs.readFileSync(filePath, 'utf8'); @@ -31,9 +32,12 @@ module.exports = { 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 - const defaultAdminXSettingFilename = 'admin-x-settings.js'; - this.packageConfig['adminXSettingsFilename'] = defaultAdminXSettingFilename; - this.packageConfig['adminXSettingsHash'] = (this.env === 'production') ? generateHash(path.join(adminXSettingsPath, defaultAdminXSettingFilename)) : 'development'; + for (const app of adminXApps) { + const defaultFilename = `${app}.js`; + 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') { 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}); }); - // copy the @tryghost/admin-x-settings assets - const assetsAdminXPath = `${assetsOut}/assets/admin-x-settings`; - - if (fs.existsSync(adminXSettingsPath)) { - if (this.env === 'production') { - fs.copySync(adminXSettingsPath, assetsAdminXPath, {overwrite: true, dereference: true}); - } else { - fs.ensureSymlinkSync(adminXSettingsPath, assetsAdminXPath); + // copy assets for each admin-x app + for (const app of adminXApps) { + const adminXPath = `../../apps/${app}/dist`; + const assetsAdminXPath = `${assetsOut}/assets/${app}`; + if (fs.existsSync(adminXPath)) { + if (this.env === 'production') { + fs.copySync(adminXPath, assetsAdminXPath, {overwrite: true, dereference: true}); + } 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 diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 20934d7ff2..4eeaa5f417 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -46,7 +46,9 @@ const ALPHA_FEATURES = [ 'importMemberTier', 'lexicalIndicators', 'editorEmojiPicker', - 'adminXOffers' + 'adminXOffers', + 'filterEmailDisabled', + 'adminXDemo' ]; module.exports.GA_KEYS = [...GA_FEATURES];