diff --git a/.github/scripts/dev.js b/.github/scripts/dev.js index e022c50b55c..5ff1f1bcbd5 100644 --- a/.github/scripts/dev.js +++ b/.github/scripts/dev.js @@ -50,20 +50,26 @@ const COMMAND_TYPESCRIPT = { env: {} }; -const COMMAND_ADMINX = { +const COMMANDS_ADMINX = [{ + name: 'adminXDS', + command: 'nx watch --projects=apps/admin-x-design-system -- nx run \\$NX_PROJECT_NAME:build --skip-nx-cache', + cwd: path.resolve(__dirname, '../..'), + prefixColor: '#C35831', + env: {} +}, { name: 'adminX', - command: 'yarn dev', + command: 'yarn nx build && yarn dev', cwd: path.resolve(__dirname, '../../apps/admin-x-settings'), prefixColor: '#C35831', env: {} -}; +}]; if (DASH_DASH_ARGS.includes('ghost')) { commands = [COMMAND_GHOST, COMMAND_TYPESCRIPT]; } else if (DASH_DASH_ARGS.includes('admin')) { - commands = [COMMAND_ADMIN, COMMAND_ADMINX]; + commands = [COMMAND_ADMIN, ...COMMANDS_ADMINX]; } else { - commands = [COMMAND_GHOST, COMMAND_TYPESCRIPT, COMMAND_ADMIN, COMMAND_ADMINX]; + commands = [COMMAND_GHOST, COMMAND_TYPESCRIPT, COMMAND_ADMIN, ...COMMANDS_ADMINX]; } if (DASH_DASH_ARGS.includes('portal') || DASH_DASH_ARGS.includes('all')) { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f42641eaafa..eb0620a7200 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,7 +167,7 @@ jobs: key: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -192,7 +192,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 100 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -228,7 +228,7 @@ jobs: || needs.job_get_metadata.outputs.changed_core == 'true' steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.12.1" @@ -252,7 +252,7 @@ jobs: COVERAGE: true steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18.12.1" @@ -294,7 +294,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -371,7 +371,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 100 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -418,7 +418,7 @@ jobs: name: Database tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -539,7 +539,7 @@ jobs: name: Regression tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }}) steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -590,7 +590,7 @@ jobs: CI: true steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -643,7 +643,7 @@ jobs: CI: true steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -696,7 +696,7 @@ jobs: CI: true steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -750,7 +750,7 @@ jobs: with: fetch-depth: 0 submodules: true - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: @@ -779,7 +779,7 @@ jobs: echo "V4_DIR=$DIR" >> $GITHUB_ENV ghost install v4 --local -d $DIR - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 with: diff --git a/apps/admin-x-design-system/.eslintrc.cjs b/apps/admin-x-design-system/.eslintrc.cjs new file mode 100644 index 00000000000..27d400ede58 --- /dev/null +++ b/apps/admin-x-design-system/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + extends: [ + 'plugin:ghost/ts', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended' + ], + plugins: [ + 'ghost', + 'react-refresh', + 'tailwindcss' + ], + settings: { + react: { + version: 'detect' + } + }, + rules: { + // suppress errors for missing 'import React' in JSX files, as we don't need it + 'react/react-in-jsx-scope': 'off', + // ignore prop-types for now + 'react/prop-types': 'off', + + 'react/jsx-sort-props': ['error', { + reservedFirst: true, + callbacksLast: true, + shorthandLast: true, + locale: 'en' + }], + 'react/button-has-type': 'error', + 'react/no-array-index-key': 'error', + 'react/jsx-key': 'off', + + 'tailwindcss/classnames-order': ['error', {config: 'tailwind.config.cjs'}], + 'tailwindcss/enforces-negative-arbitrary-values': ['warn', {config: 'tailwind.config.cjs'}], + 'tailwindcss/enforces-shorthand': ['warn', {config: 'tailwind.config.cjs'}], + 'tailwindcss/migration-from-tailwind-2': ['warn', {config: 'tailwind.config.cjs'}], + 'tailwindcss/no-arbitrary-value': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-contradicting-classname': ['error', {config: 'tailwind.config.cjs'}] + } +}; diff --git a/apps/admin-x-design-system/.gitignore b/apps/admin-x-design-system/.gitignore new file mode 100644 index 00000000000..62ac7b71aa7 --- /dev/null +++ b/apps/admin-x-design-system/.gitignore @@ -0,0 +1,2 @@ +es +types diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/fonts/Inter.ttf b/apps/admin-x-design-system/.storybook/Inter.ttf similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/fonts/Inter.ttf rename to apps/admin-x-design-system/.storybook/Inter.ttf diff --git a/apps/admin-x-settings/.storybook/adminx-theme.tsx b/apps/admin-x-design-system/.storybook/adminx-theme.tsx similarity index 100% rename from apps/admin-x-settings/.storybook/adminx-theme.tsx rename to apps/admin-x-design-system/.storybook/adminx-theme.tsx diff --git a/apps/admin-x-settings/.storybook/main.tsx b/apps/admin-x-design-system/.storybook/main.tsx similarity index 59% rename from apps/admin-x-settings/.storybook/main.tsx rename to apps/admin-x-design-system/.storybook/main.tsx index c80dfe7b948..53b7664ef08 100644 --- a/apps/admin-x-settings/.storybook/main.tsx +++ b/apps/admin-x-design-system/.storybook/main.tsx @@ -1,5 +1,5 @@ -import {resolve} from "path"; import type { StorybookConfig } from "@storybook/react-vite"; + const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], addons: [ @@ -17,14 +17,11 @@ const config: StorybookConfig = { docs: { autodocs: "tag", }, - // staticDirs: ['../public/fonts'], - async viteFinal(config, options) { - config.resolve.alias = { - crypto: require.resolve('rollup-plugin-node-builtins'), - // @TODO: Remove this when @tryghost/nql is updated - mingo: resolve(__dirname, '../../../node_modules/mingo/dist/mingo.js') + async viteFinal(config, options) { + config.resolve!.alias = { + crypto: require.resolve('rollup-plugin-node-builtins') } return config; - }, + } }; export default config; diff --git a/apps/admin-x-settings/.storybook/manager.tsx b/apps/admin-x-design-system/.storybook/manager.tsx similarity index 100% rename from apps/admin-x-settings/.storybook/manager.tsx rename to apps/admin-x-design-system/.storybook/manager.tsx diff --git a/apps/admin-x-settings/.storybook/preview.tsx b/apps/admin-x-design-system/.storybook/preview.tsx similarity index 76% rename from apps/admin-x-settings/.storybook/preview.tsx rename to apps/admin-x-design-system/.storybook/preview.tsx index 268a8befcda..6f1dfede995 100644 --- a/apps/admin-x-settings/.storybook/preview.tsx +++ b/apps/admin-x-design-system/.storybook/preview.tsx @@ -1,13 +1,11 @@ import React from 'react'; -import '../src/styles/demo.css'; +import '../styles.css'; +import './storybook.css'; + import type { Preview } from "@storybook/react"; -import '../src/admin-x-ds/providers/DesignSystemProvider'; -import DesignSystemProvider from '../src/admin-x-ds/providers/DesignSystemProvider'; +import DesignSystemProvider from '../src/providers/DesignSystemProvider'; import adminxTheme from './adminx-theme'; -import { themes } from '@storybook/theming'; - -import '../src/admin-x-ds/assets/styles/storybook.css'; const preview: Preview = { parameters: { @@ -33,12 +31,12 @@ const preview: Preview = { let {scheme} = context.globals; return ( -
{/* 👇 Decorators in Storybook also accept a function. Replace with Story() to enable it */} - + {}}>
); diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/styles/storybook.css b/apps/admin-x-design-system/.storybook/storybook.css similarity index 83% rename from apps/admin-x-settings/src/admin-x-ds/assets/styles/storybook.css rename to apps/admin-x-design-system/.storybook/storybook.css index aecd9585204..062e7a1056b 100644 --- a/apps/admin-x-settings/src/admin-x-ds/assets/styles/storybook.css +++ b/apps/admin-x-design-system/.storybook/storybook.css @@ -1,3 +1,32 @@ +/* + * We load Inter in Ember admin, so loading it explicitly here makes the final rendering + * in Storybook match the final rendering when embedded in Ember + */ +@font-face { + font-family: "Inter"; + src: url("./Inter.ttf") format("truetype-variations"); + font-weight: 100 900; +} + +:root { + font-size: 62.5%; + line-height: 1.5; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +html, body, #root { + width: 100%; + height: 100%; + margin: 0; + letter-spacing: unset; +} + .sbdocs-wrapper { padding: 3vmin !important; } @@ -211,4 +240,4 @@ display: grid; grid-template-columns: auto 240px; gap: 32px; -} \ No newline at end of file +} diff --git a/apps/admin-x-design-system/README.md b/apps/admin-x-design-system/README.md new file mode 100644 index 00000000000..a76239c6f2d --- /dev/null +++ b/apps/admin-x-design-system/README.md @@ -0,0 +1,23 @@ +# Admin X Design + +Components, design guidelines and documentation for building apps in Ghost Admin + + +## Usage + + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json new file mode 100644 index 00000000000..263956fc957 --- /dev/null +++ b/apps/admin-x-design-system/package.json @@ -0,0 +1,75 @@ +{ + "name": "@tryghost/admin-x-design-system", + "type": "module", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/admin-x-design-system", + "author": "Ghost Foundation", + "private": true, + "main": "es/index.js", + "types": "types/index.d.ts", + "sideEffects": false, + "scripts": { + "build": "vite build && tsc -p tsconfig.declaration.json", + "prepare": "yarn build", + "test": "yarn test:types", + "test:types": "tsc --noEmit", + "lint:code": "eslint --ext .js,.ts,.cjs,.tsx src/ --cache", + "lint": "yarn lint:code && yarn lint:test", + "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx test/ --cache", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "files": [ + "es", + "types", + "tailwind.cjs", + "tailwind.config.cjs" + ], + "devDependencies": { + "@codemirror/lang-html": "^6.4.5", + "@storybook/addon-essentials": "7.4.0", + "@storybook/addon-interactions": "7.4.0", + "@storybook/addon-links": "7.4.0", + "@storybook/addon-styling": "1.3.7", + "@storybook/blocks": "7.4.0", + "@storybook/react": "7.4.0", + "@storybook/react-vite": "7.4.0", + "@storybook/testing-library": "0.2.2", + "@vitejs/plugin-react": "4.1.1", + "c8": "8.0.1", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-react-refresh": "0.4.3", + "eslint-plugin-tailwindcss": "3.13.0", + "mocha": "10.2.0", + "rollup-plugin-node-builtins": "2.1.2", + "sinon": "17.0.0", + "ts-node": "10.9.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "storybook": "7.4.0", + "typescript": "5.2.2", + "vite": "4.5.0", + "vite-plugin-svgr": "3.3.0" + }, + "dependencies": { + "@dnd-kit/core": "6.0.8", + "@dnd-kit/sortable": "7.0.2", + "@ebay/nice-modal-react": "1.2.13", + "@sentry/react": "7.78.0", + "@tailwindcss/forms": "0.5.6", + "@tailwindcss/line-clamp": "0.4.4", + "@uiw/react-codemirror": "^4.21.9", + "autoprefixer": "10.4.16", + "clsx": "2.0.0", + "postcss": "8.4.31", + "postcss-import": "15.1.0", + "react-colorful": "^5.1.2", + "react-hot-toast": "2.4.1", + "react-select": "5.8.0", + "tailwindcss": "3.3.5" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} diff --git a/apps/admin-x-design-system/postcss.config.js b/apps/admin-x-design-system/postcss.config.js new file mode 100644 index 00000000000..3f15318da0b --- /dev/null +++ b/apps/admin-x-design-system/postcss.config.js @@ -0,0 +1,8 @@ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/apps/admin-x-settings/src/styles/preflight.css b/apps/admin-x-design-system/preflight.css similarity index 99% rename from apps/admin-x-settings/src/styles/preflight.css rename to apps/admin-x-design-system/preflight.css index ff179a06507..894cd6629a7 100644 --- a/apps/admin-x-settings/src/styles/preflight.css +++ b/apps/admin-x-design-system/preflight.css @@ -1,5 +1,4 @@ -.admin-x-settings { - +.admin-x-base { /* 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) @@ -329,7 +328,7 @@ input::placeholder, textarea::placeholder { opacity: 1; /* 1 */ - color: theme('colors.grey.500'); /* 2 */ + @apply text-grey-500; /* 2 */ } button:focus-visible, @@ -364,5 +363,4 @@ max-width: 100%; height: auto; } - -} \ No newline at end of file +} diff --git a/apps/admin-x-settings/src/admin-x-ds/Boilerplate.stories.tsx b/apps/admin-x-design-system/src/Boilerplate.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/Boilerplate.stories.tsx rename to apps/admin-x-design-system/src/Boilerplate.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/Boilerplate.tsx b/apps/admin-x-design-system/src/Boilerplate.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/Boilerplate.tsx rename to apps/admin-x-design-system/src/Boilerplate.tsx diff --git a/apps/admin-x-design-system/src/DesignSystemApp.tsx b/apps/admin-x-design-system/src/DesignSystemApp.tsx new file mode 100644 index 00000000000..6606867af16 --- /dev/null +++ b/apps/admin-x-design-system/src/DesignSystemApp.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx'; +import React from 'react'; +import {FetchKoenigLexical} from './global/form/HtmlEditor'; +import DesignSystemProvider from './providers/DesignSystemProvider'; + +export interface DesignSystemAppProps extends React.HTMLProps { + darkMode: boolean; + fetchKoenigLexical: FetchKoenigLexical; +} + +const DesignSystemApp: React.FC = ({darkMode, fetchKoenigLexical, className, children, ...props}) => { + const appClassName = clsx( + 'admin-x-base', + darkMode && 'dark', + className + ); + + return ( +
+ + {children} + +
+ ); +}; + +export default DesignSystemApp; diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/add.svg b/apps/admin-x-design-system/src/assets/icons/add.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/add.svg rename to apps/admin-x-design-system/src/assets/icons/add.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/align-center.svg b/apps/admin-x-design-system/src/assets/icons/align-center.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/align-center.svg rename to apps/admin-x-design-system/src/assets/icons/align-center.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/align-left.svg b/apps/admin-x-design-system/src/assets/icons/align-left.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/align-left.svg rename to apps/admin-x-design-system/src/assets/icons/align-left.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/angle-brackets.svg b/apps/admin-x-design-system/src/assets/icons/angle-brackets.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/angle-brackets.svg rename to apps/admin-x-design-system/src/assets/icons/angle-brackets.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-bottom-left.svg b/apps/admin-x-design-system/src/assets/icons/arrow-bottom-left.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-bottom-left.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-bottom-left.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-bottom-right.svg b/apps/admin-x-design-system/src/assets/icons/arrow-bottom-right.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-bottom-right.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-bottom-right.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-down.svg b/apps/admin-x-design-system/src/assets/icons/arrow-down.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-down.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-down.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-left.svg b/apps/admin-x-design-system/src/assets/icons/arrow-left.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-left.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-left.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-right.svg b/apps/admin-x-design-system/src/assets/icons/arrow-right.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-right.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-right.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-top-left.svg b/apps/admin-x-design-system/src/assets/icons/arrow-top-left.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-top-left.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-top-left.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-top-right.svg b/apps/admin-x-design-system/src/assets/icons/arrow-top-right.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-top-right.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-top-right.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-up.svg b/apps/admin-x-design-system/src/assets/icons/arrow-up.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/arrow-up.svg rename to apps/admin-x-design-system/src/assets/icons/arrow-up.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/at-sign.svg b/apps/admin-x-design-system/src/assets/icons/at-sign.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/at-sign.svg rename to apps/admin-x-design-system/src/assets/icons/at-sign.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/baseline-chart.svg b/apps/admin-x-design-system/src/assets/icons/baseline-chart.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/baseline-chart.svg rename to apps/admin-x-design-system/src/assets/icons/baseline-chart.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/bills.svg b/apps/admin-x-design-system/src/assets/icons/bills.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/bills.svg rename to apps/admin-x-design-system/src/assets/icons/bills.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/book-open.svg b/apps/admin-x-design-system/src/assets/icons/book-open.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/book-open.svg rename to apps/admin-x-design-system/src/assets/icons/book-open.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/brackets.svg b/apps/admin-x-design-system/src/assets/icons/brackets.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/brackets.svg rename to apps/admin-x-design-system/src/assets/icons/brackets.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/check-circle.svg b/apps/admin-x-design-system/src/assets/icons/check-circle.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/check-circle.svg rename to apps/admin-x-design-system/src/assets/icons/check-circle.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/check.svg b/apps/admin-x-design-system/src/assets/icons/check.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/check.svg rename to apps/admin-x-design-system/src/assets/icons/check.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-down.svg b/apps/admin-x-design-system/src/assets/icons/chevron-down.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-down.svg rename to apps/admin-x-design-system/src/assets/icons/chevron-down.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-left.svg b/apps/admin-x-design-system/src/assets/icons/chevron-left.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-left.svg rename to apps/admin-x-design-system/src/assets/icons/chevron-left.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-right.svg b/apps/admin-x-design-system/src/assets/icons/chevron-right.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-right.svg rename to apps/admin-x-design-system/src/assets/icons/chevron-right.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-up.svg b/apps/admin-x-design-system/src/assets/icons/chevron-up.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/chevron-up.svg rename to apps/admin-x-design-system/src/assets/icons/chevron-up.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/close.svg b/apps/admin-x-design-system/src/assets/icons/close.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/close.svg rename to apps/admin-x-design-system/src/assets/icons/close.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/comment.svg b/apps/admin-x-design-system/src/assets/icons/comment.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/comment.svg rename to apps/admin-x-design-system/src/assets/icons/comment.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/crown.svg b/apps/admin-x-design-system/src/assets/icons/crown.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/crown.svg rename to apps/admin-x-design-system/src/assets/icons/crown.svg diff --git a/apps/admin-x-design-system/src/assets/icons/discount.svg b/apps/admin-x-design-system/src/assets/icons/discount.svg new file mode 100644 index 00000000000..5d5ef793b09 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/discount.svg @@ -0,0 +1 @@ + diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/duplicate.svg b/apps/admin-x-design-system/src/assets/icons/duplicate.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/duplicate.svg rename to apps/admin-x-design-system/src/assets/icons/duplicate.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/ellipsis.svg b/apps/admin-x-design-system/src/assets/icons/ellipsis.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/ellipsis.svg rename to apps/admin-x-design-system/src/assets/icons/ellipsis.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/email-check.svg b/apps/admin-x-design-system/src/assets/icons/email-check.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/email-check.svg rename to apps/admin-x-design-system/src/assets/icons/email-check.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/email.svg b/apps/admin-x-design-system/src/assets/icons/email.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/email.svg rename to apps/admin-x-design-system/src/assets/icons/email.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/emailfield.svg b/apps/admin-x-design-system/src/assets/icons/emailfield.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/emailfield.svg rename to apps/admin-x-design-system/src/assets/icons/emailfield.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/eyedropper.svg b/apps/admin-x-design-system/src/assets/icons/eyedropper.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/eyedropper.svg rename to apps/admin-x-design-system/src/assets/icons/eyedropper.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/facebook.svg b/apps/admin-x-design-system/src/assets/icons/facebook.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/facebook.svg rename to apps/admin-x-design-system/src/assets/icons/facebook.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/finger-up.svg b/apps/admin-x-design-system/src/assets/icons/finger-up.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/finger-up.svg rename to apps/admin-x-design-system/src/assets/icons/finger-up.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/hamburger.svg b/apps/admin-x-design-system/src/assets/icons/hamburger.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/hamburger.svg rename to apps/admin-x-design-system/src/assets/icons/hamburger.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/heart.svg b/apps/admin-x-design-system/src/assets/icons/heart.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/heart.svg rename to apps/admin-x-design-system/src/assets/icons/heart.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/integration.svg b/apps/admin-x-design-system/src/assets/icons/integration.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/integration.svg rename to apps/admin-x-design-system/src/assets/icons/integration.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/key.svg b/apps/admin-x-design-system/src/assets/icons/key.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/key.svg rename to apps/admin-x-design-system/src/assets/icons/key.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/labs-flask.svg b/apps/admin-x-design-system/src/assets/icons/labs-flask.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/labs-flask.svg rename to apps/admin-x-design-system/src/assets/icons/labs-flask.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/language.svg b/apps/admin-x-design-system/src/assets/icons/language.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/language.svg rename to apps/admin-x-design-system/src/assets/icons/language.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/laptop.svg b/apps/admin-x-design-system/src/assets/icons/laptop.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/laptop.svg rename to apps/admin-x-design-system/src/assets/icons/laptop.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/layer.svg b/apps/admin-x-design-system/src/assets/icons/layer.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/layer.svg rename to apps/admin-x-design-system/src/assets/icons/layer.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/like.svg b/apps/admin-x-design-system/src/assets/icons/like.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/like.svg rename to apps/admin-x-design-system/src/assets/icons/like.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/link-broken.svg b/apps/admin-x-design-system/src/assets/icons/link-broken.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/link-broken.svg rename to apps/admin-x-design-system/src/assets/icons/link-broken.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/lock-locked.svg b/apps/admin-x-design-system/src/assets/icons/lock-locked.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/lock-locked.svg rename to apps/admin-x-design-system/src/assets/icons/lock-locked.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/lock-unlocked.svg b/apps/admin-x-design-system/src/assets/icons/lock-unlocked.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/lock-unlocked.svg rename to apps/admin-x-design-system/src/assets/icons/lock-unlocked.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/magnifying-glass.svg b/apps/admin-x-design-system/src/assets/icons/magnifying-glass.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/magnifying-glass.svg rename to apps/admin-x-design-system/src/assets/icons/magnifying-glass.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/mail-block.svg b/apps/admin-x-design-system/src/assets/icons/mail-block.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/mail-block.svg rename to apps/admin-x-design-system/src/assets/icons/mail-block.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/megaphone.svg b/apps/admin-x-design-system/src/assets/icons/megaphone.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/megaphone.svg rename to apps/admin-x-design-system/src/assets/icons/megaphone.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/mobile.svg b/apps/admin-x-design-system/src/assets/icons/mobile.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/mobile.svg rename to apps/admin-x-design-system/src/assets/icons/mobile.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/modules-3.svg b/apps/admin-x-design-system/src/assets/icons/modules-3.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/modules-3.svg rename to apps/admin-x-design-system/src/assets/icons/modules-3.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/money-bags.svg b/apps/admin-x-design-system/src/assets/icons/money-bags.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/money-bags.svg rename to apps/admin-x-design-system/src/assets/icons/money-bags.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/navigation.svg b/apps/admin-x-design-system/src/assets/icons/navigation.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/navigation.svg rename to apps/admin-x-design-system/src/assets/icons/navigation.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/palette.svg b/apps/admin-x-design-system/src/assets/icons/palette.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/palette.svg rename to apps/admin-x-design-system/src/assets/icons/palette.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/pen.svg b/apps/admin-x-design-system/src/assets/icons/pen.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/pen.svg rename to apps/admin-x-design-system/src/assets/icons/pen.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/picture.svg b/apps/admin-x-design-system/src/assets/icons/picture.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/picture.svg rename to apps/admin-x-design-system/src/assets/icons/picture.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/piggybank.svg b/apps/admin-x-design-system/src/assets/icons/piggybank.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/piggybank.svg rename to apps/admin-x-design-system/src/assets/icons/piggybank.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/portal.svg b/apps/admin-x-design-system/src/assets/icons/portal.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/portal.svg rename to apps/admin-x-design-system/src/assets/icons/portal.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/question-circle.svg b/apps/admin-x-design-system/src/assets/icons/question-circle.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/question-circle.svg rename to apps/admin-x-design-system/src/assets/icons/question-circle.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/recepients.svg b/apps/admin-x-design-system/src/assets/icons/recepients.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/recepients.svg rename to apps/admin-x-design-system/src/assets/icons/recepients.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/single-user-block.svg b/apps/admin-x-design-system/src/assets/icons/single-user-block.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/single-user-block.svg rename to apps/admin-x-design-system/src/assets/icons/single-user-block.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/single-user-fill.svg b/apps/admin-x-design-system/src/assets/icons/single-user-fill.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/single-user-fill.svg rename to apps/admin-x-design-system/src/assets/icons/single-user-fill.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/textfield.svg b/apps/admin-x-design-system/src/assets/icons/textfield.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/textfield.svg rename to apps/admin-x-design-system/src/assets/icons/textfield.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/thumbs-down.svg b/apps/admin-x-design-system/src/assets/icons/thumbs-down.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/thumbs-down.svg rename to apps/admin-x-design-system/src/assets/icons/thumbs-down.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/thumbs-up.svg b/apps/admin-x-design-system/src/assets/icons/thumbs-up.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/thumbs-up.svg rename to apps/admin-x-design-system/src/assets/icons/thumbs-up.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/time-back.svg b/apps/admin-x-design-system/src/assets/icons/time-back.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/time-back.svg rename to apps/admin-x-design-system/src/assets/icons/time-back.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/trash.svg b/apps/admin-x-design-system/src/assets/icons/trash.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/trash.svg rename to apps/admin-x-design-system/src/assets/icons/trash.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/twitter-x.svg b/apps/admin-x-design-system/src/assets/icons/twitter-x.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/twitter-x.svg rename to apps/admin-x-design-system/src/assets/icons/twitter-x.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/unsplash-logo.svg b/apps/admin-x-design-system/src/assets/icons/unsplash-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/unsplash-logo.svg rename to apps/admin-x-design-system/src/assets/icons/unsplash-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/upload.svg b/apps/admin-x-design-system/src/assets/icons/upload.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/upload.svg rename to apps/admin-x-design-system/src/assets/icons/upload.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/user-add.svg b/apps/admin-x-design-system/src/assets/icons/user-add.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/user-add.svg rename to apps/admin-x-design-system/src/assets/icons/user-add.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/user-page.svg b/apps/admin-x-design-system/src/assets/icons/user-page.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/user-page.svg rename to apps/admin-x-design-system/src/assets/icons/user-page.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/warning.svg b/apps/admin-x-design-system/src/assets/icons/warning.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/warning.svg rename to apps/admin-x-design-system/src/assets/icons/warning.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/icons/world-clock.svg b/apps/admin-x-design-system/src/assets/icons/world-clock.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/icons/world-clock.svg rename to apps/admin-x-design-system/src/assets/icons/world-clock.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/facebook-logo.svg b/apps/admin-x-design-system/src/assets/images/facebook-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/facebook-logo.svg rename to apps/admin-x-design-system/src/assets/images/facebook-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/ghost-logo.svg b/apps/admin-x-design-system/src/assets/images/ghost-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/ghost-logo.svg rename to apps/admin-x-design-system/src/assets/images/ghost-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/ghost-orb.svg b/apps/admin-x-design-system/src/assets/images/ghost-orb.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/ghost-orb.svg rename to apps/admin-x-design-system/src/assets/images/ghost-orb.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/google-logo.svg b/apps/admin-x-design-system/src/assets/images/google-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/google-logo.svg rename to apps/admin-x-design-system/src/assets/images/google-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/twitter-logo.svg b/apps/admin-x-design-system/src/assets/images/twitter-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/twitter-logo.svg rename to apps/admin-x-design-system/src/assets/images/twitter-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/assets/images/x-logo.svg b/apps/admin-x-design-system/src/assets/images/x-logo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/assets/images/x-logo.svg rename to apps/admin-x-design-system/src/assets/images/x-logo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/Colors.mdx b/apps/admin-x-design-system/src/docs/Colors.mdx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/Colors.mdx rename to apps/admin-x-design-system/src/docs/Colors.mdx diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/ErrorHandling.mdx b/apps/admin-x-design-system/src/docs/ErrorHandling.mdx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/ErrorHandling.mdx rename to apps/admin-x-design-system/src/docs/ErrorHandling.mdx diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/Icons.mdx b/apps/admin-x-design-system/src/docs/Icons.mdx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/Icons.mdx rename to apps/admin-x-design-system/src/docs/Icons.mdx diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/Welcome.mdx b/apps/admin-x-design-system/src/docs/Welcome.mdx similarity index 85% rename from apps/admin-x-settings/src/admin-x-ds/docs/Welcome.mdx rename to apps/admin-x-design-system/src/docs/Welcome.mdx index 6d5c1180e49..68670193752 100644 --- a/apps/admin-x-settings/src/admin-x-ds/docs/Welcome.mdx +++ b/apps/admin-x-design-system/src/docs/Welcome.mdx @@ -14,8 +14,6 @@ import AppsIcon from './assets/apps.svg';

Here you can find our design guidelines, component documentation, and resources for building apps in Ghost Admin.

-
This is a work in progress. As of today, the Storybook system is part of AdminX Settings app but it's about to be a separated into its own package.
- ### What's inside The AdminX Design System is a collection of React UI building blocks for designers and developers to create apps for Ghost Admin. Its main purpose is to provide a library of available components and maintain consistency across the whole product. @@ -46,4 +44,4 @@ The system uses Storybook — if you're new to it, we recommend reading about w

- \ No newline at end of file + diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/adminx-screenshot.png b/apps/admin-x-design-system/src/docs/assets/adminx-screenshot.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/adminx-screenshot.png rename to apps/admin-x-design-system/src/docs/assets/adminx-screenshot.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/apps.svg b/apps/admin-x-design-system/src/docs/assets/apps.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/apps.svg rename to apps/admin-x-design-system/src/docs/assets/apps.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/blocks.svg b/apps/admin-x-design-system/src/docs/assets/blocks.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/blocks.svg rename to apps/admin-x-design-system/src/docs/assets/blocks.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/circle-menu.svg b/apps/admin-x-design-system/src/docs/assets/circle-menu.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/circle-menu.svg rename to apps/admin-x-design-system/src/docs/assets/circle-menu.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/code-brackets.svg b/apps/admin-x-design-system/src/docs/assets/code-brackets.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/code-brackets.svg rename to apps/admin-x-design-system/src/docs/assets/code-brackets.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/colors.svg b/apps/admin-x-design-system/src/docs/assets/colors.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/colors.svg rename to apps/admin-x-design-system/src/docs/assets/colors.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/comments.svg b/apps/admin-x-design-system/src/docs/assets/comments.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/comments.svg rename to apps/admin-x-design-system/src/docs/assets/comments.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/direction.svg b/apps/admin-x-design-system/src/docs/assets/direction.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/direction.svg rename to apps/admin-x-design-system/src/docs/assets/direction.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/ds-structure.png b/apps/admin-x-design-system/src/docs/assets/ds-structure.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/ds-structure.png rename to apps/admin-x-design-system/src/docs/assets/ds-structure.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/flow.svg b/apps/admin-x-design-system/src/docs/assets/flow.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/flow.svg rename to apps/admin-x-design-system/src/docs/assets/flow.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/global-error-example.png b/apps/admin-x-design-system/src/docs/assets/global-error-example.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/global-error-example.png rename to apps/admin-x-design-system/src/docs/assets/global-error-example.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/local-error-example.png b/apps/admin-x-design-system/src/docs/assets/local-error-example.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/local-error-example.png rename to apps/admin-x-design-system/src/docs/assets/local-error-example.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/page-error-example.png b/apps/admin-x-design-system/src/docs/assets/page-error-example.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/page-error-example.png rename to apps/admin-x-design-system/src/docs/assets/page-error-example.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/plugin.svg b/apps/admin-x-design-system/src/docs/assets/plugin.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/plugin.svg rename to apps/admin-x-design-system/src/docs/assets/plugin.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/repo.svg b/apps/admin-x-design-system/src/docs/assets/repo.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/repo.svg rename to apps/admin-x-design-system/src/docs/assets/repo.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/stackalt.svg b/apps/admin-x-design-system/src/docs/assets/stackalt.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/stackalt.svg rename to apps/admin-x-design-system/src/docs/assets/stackalt.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/streamline-settings.png b/apps/admin-x-design-system/src/docs/assets/streamline-settings.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/streamline-settings.png rename to apps/admin-x-design-system/src/docs/assets/streamline-settings.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/style-guide-layout.png b/apps/admin-x-design-system/src/docs/assets/style-guide-layout.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/style-guide-layout.png rename to apps/admin-x-design-system/src/docs/assets/style-guide-layout.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/style-guide.png b/apps/admin-x-design-system/src/docs/assets/style-guide.png similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/style-guide.png rename to apps/admin-x-design-system/src/docs/assets/style-guide.png diff --git a/apps/admin-x-settings/src/admin-x-ds/docs/assets/tower.svg b/apps/admin-x-design-system/src/docs/assets/tower.svg similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/docs/assets/tower.svg rename to apps/admin-x-design-system/src/docs/assets/tower.svg diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Avatar.stories.tsx b/apps/admin-x-design-system/src/global/Avatar.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Avatar.stories.tsx rename to apps/admin-x-design-system/src/global/Avatar.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Avatar.tsx b/apps/admin-x-design-system/src/global/Avatar.tsx similarity index 97% rename from apps/admin-x-settings/src/admin-x-ds/global/Avatar.tsx rename to apps/admin-x-design-system/src/global/Avatar.tsx index 6d0a30befa9..f95801b0fa5 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Avatar.tsx +++ b/apps/admin-x-design-system/src/global/Avatar.tsx @@ -3,7 +3,7 @@ import {ReactComponent as UserIcon} from '../assets/icons/single-user-fill.svg'; type AvatarSize = 'sm' | 'md' | 'lg' | 'xl'; -interface AvatarProps { +export interface AvatarProps { image?: string; label?: string; @@ -57,4 +57,4 @@ const Avatar: React.FC = ({image, label, labelColor, bgColor, size, } }; -export default Avatar; \ No newline at end of file +export default Avatar; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Banner.stories.tsx b/apps/admin-x-design-system/src/global/Banner.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Banner.stories.tsx rename to apps/admin-x-design-system/src/global/Banner.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Banner.tsx b/apps/admin-x-design-system/src/global/Banner.tsx similarity index 95% rename from apps/admin-x-settings/src/admin-x-ds/global/Banner.tsx rename to apps/admin-x-design-system/src/global/Banner.tsx index ad74060d140..40b9608367b 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Banner.tsx +++ b/apps/admin-x-design-system/src/global/Banner.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; -interface BannerProps { +export interface BannerProps { color?: 'grey' | 'blue' | 'green' | 'yellow' | 'red'; children?: React.ReactNode; className?: string; @@ -33,4 +33,4 @@ const Banner: React.FC = ({color = 'grey', children, className}) => ); }; -export default Banner; \ No newline at end of file +export default Banner; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Breadcrumbs.stories.tsx b/apps/admin-x-design-system/src/global/Breadcrumbs.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Breadcrumbs.stories.tsx rename to apps/admin-x-design-system/src/global/Breadcrumbs.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Breadcrumbs.tsx b/apps/admin-x-design-system/src/global/Breadcrumbs.tsx similarity index 96% rename from apps/admin-x-settings/src/admin-x-ds/global/Breadcrumbs.tsx rename to apps/admin-x-design-system/src/global/Breadcrumbs.tsx index 63b9443181c..aa93dfafaeb 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Breadcrumbs.tsx +++ b/apps/admin-x-design-system/src/global/Breadcrumbs.tsx @@ -1,13 +1,13 @@ -import Button from './Button'; -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; +import Button from './Button'; export type BreadcrumbItem = { label: React.ReactNode; onClick?: () => void; } -interface BreadcrumbsProps { +export interface BreadcrumbsProps { items: BreadcrumbItem[]; backIcon?: boolean; onBack?: () => void; @@ -71,4 +71,4 @@ const Breadcrumbs: React.FC = ({ ); }; -export default Breadcrumbs; \ No newline at end of file +export default Breadcrumbs; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Button.stories.tsx b/apps/admin-x-design-system/src/global/Button.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Button.stories.tsx rename to apps/admin-x-design-system/src/global/Button.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Button.tsx b/apps/admin-x-design-system/src/global/Button.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Button.tsx rename to apps/admin-x-design-system/src/global/Button.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ButtonGroup.stories.tsx b/apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/ButtonGroup.stories.tsx rename to apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ButtonGroup.tsx b/apps/admin-x-design-system/src/global/ButtonGroup.tsx similarity index 90% rename from apps/admin-x-settings/src/admin-x-ds/global/ButtonGroup.tsx rename to apps/admin-x-design-system/src/global/ButtonGroup.tsx index 1c8e54cd5d8..bd86202daf3 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ButtonGroup.tsx +++ b/apps/admin-x-design-system/src/global/ButtonGroup.tsx @@ -1,9 +1,9 @@ -import Button from './Button'; import React from 'react'; +import Button from './Button'; import {ButtonProps} from './Button'; -interface ButtonGroupProps { +export interface ButtonGroupProps { buttons: Array; link?: boolean; linkWithPadding?: boolean; @@ -20,4 +20,4 @@ const ButtonGroup: React.FC = ({buttons, link, linkWithPadding ); }; -export default ButtonGroup; \ No newline at end of file +export default ButtonGroup; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.stories.tsx b/apps/admin-x-design-system/src/global/ErrorBoundary.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.stories.tsx rename to apps/admin-x-design-system/src/global/ErrorBoundary.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx b/apps/admin-x-design-system/src/global/ErrorBoundary.tsx similarity index 86% rename from apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx rename to apps/admin-x-design-system/src/global/ErrorBoundary.tsx index 4fe0db91ad1..f328a7f0c26 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ErrorBoundary.tsx +++ b/apps/admin-x-design-system/src/global/ErrorBoundary.tsx @@ -1,12 +1,17 @@ import * as Sentry from '@sentry/react'; -import Banner from './Banner'; import React, {ComponentType, ErrorInfo, ReactNode} from 'react'; +import Banner from './Banner'; + +export interface ErrorBoundaryProps { + children: ReactNode; + name: ReactNode; +} /** * Catches errors in child components and displays a banner. Useful to prevent errors in one * section from crashing the entire page */ -class ErrorBoundary extends React.Component<{children: ReactNode, name: ReactNode}> { +class ErrorBoundary extends React.Component { state = {hasError: false}; constructor(props: {children: ReactNode, name: ReactNode}) { @@ -19,7 +24,7 @@ class ErrorBoundary extends React.Component<{children: ReactNode, name: ReactNod componentDidCatch(error: unknown, info: ErrorInfo) { Sentry.withScope((scope) => { - scope.setTag('adminX settings component-', info.componentStack); + scope.setTag('adminx_settings_component', info.componentStack); Sentry.captureException(error); }); // eslint-disable-next-line no-console diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Heading.stories.tsx b/apps/admin-x-design-system/src/global/Heading.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Heading.stories.tsx rename to apps/admin-x-design-system/src/global/Heading.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Heading.tsx b/apps/admin-x-design-system/src/global/Heading.tsx similarity index 91% rename from apps/admin-x-settings/src/admin-x-ds/global/Heading.tsx rename to apps/admin-x-design-system/src/global/Heading.tsx index ff378d37423..427213cea06 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Heading.tsx +++ b/apps/admin-x-design-system/src/global/Heading.tsx @@ -1,6 +1,6 @@ +import clsx from 'clsx'; import React from 'react'; import Separator from './Separator'; -import clsx from 'clsx'; export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; @@ -44,7 +44,9 @@ export const Heading6StylesGrey = clsx( 'text-grey-900 dark:text-grey-500' ); -const Heading: React.FC = ({ +export type HeadingProps = Heading1to5Props | Heading6Props | HeadingLabelProps + +const Heading: React.FC = ({ level = 1, children, styles = '', @@ -88,8 +90,8 @@ const Heading: React.FC = const Element = React.createElement(newElement, {className: className, key: 'heading-elem', ...props}, children); if (separator) { - let gap = (!level || level === 1) ? 2 : 1; - let bottomMargin = (level === 6) ? 2 : 3; + const gap = (!level || level === 1) ? 2 : 1; + const bottomMargin = (level === 6) ? 2 : 3; return (
{Element} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Hint.stories.tsx b/apps/admin-x-design-system/src/global/Hint.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Hint.stories.tsx rename to apps/admin-x-design-system/src/global/Hint.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Hint.tsx b/apps/admin-x-design-system/src/global/Hint.tsx similarity index 96% rename from apps/admin-x-settings/src/admin-x-ds/global/Hint.tsx rename to apps/admin-x-design-system/src/global/Hint.tsx index a8c170b9210..77dc0fbb893 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Hint.tsx +++ b/apps/admin-x-design-system/src/global/Hint.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; -interface HintProps { +export interface HintProps { children?: React.ReactNode; color?: 'red' | 'green' | 'default' | ''; className?: string; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Icon.stories.tsx b/apps/admin-x-design-system/src/global/Icon.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Icon.stories.tsx rename to apps/admin-x-design-system/src/global/Icon.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx b/apps/admin-x-design-system/src/global/Icon.tsx similarity index 91% rename from apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx rename to apps/admin-x-design-system/src/global/Icon.tsx index 4a082527f9c..bb3e71df4b0 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Icon.tsx +++ b/apps/admin-x-design-system/src/global/Icon.tsx @@ -1,11 +1,11 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; const icons: Record>}> = import.meta.glob('../assets/icons/*.svg', {eager: true}); -export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; +export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'custom' | number; -interface IconProps { +export interface IconProps { name: string; /** @@ -34,6 +34,8 @@ const Icon: React.FC = ({name, size = 'md', colorClass = '', classNam if (!styles) { switch (size) { + case 'custom': + break; case 'xs': styles = 'w-3 h-3'; break; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/IconLabel.stories.tsx b/apps/admin-x-design-system/src/global/IconLabel.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/IconLabel.stories.tsx rename to apps/admin-x-design-system/src/global/IconLabel.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/IconLabel.tsx b/apps/admin-x-design-system/src/global/IconLabel.tsx similarity index 87% rename from apps/admin-x-settings/src/admin-x-ds/global/IconLabel.tsx rename to apps/admin-x-design-system/src/global/IconLabel.tsx index d9ec84bfd79..ca66ff46c88 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/IconLabel.tsx +++ b/apps/admin-x-design-system/src/global/IconLabel.tsx @@ -1,7 +1,7 @@ -import Icon from './Icon'; import React from 'react'; +import Icon from './Icon'; -interface IconLabelProps { +export interface IconLabelProps { icon: string; iconColorClass?: string; children?: React.ReactNode; @@ -16,4 +16,4 @@ const IconLabel: React.FC = ({icon, iconColorClass, children}) = ); }; -export default IconLabel; \ No newline at end of file +export default IconLabel; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/InfiniteScrollListener.stories.tsx b/apps/admin-x-design-system/src/global/InfiniteScrollListener.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/InfiniteScrollListener.stories.tsx rename to apps/admin-x-design-system/src/global/InfiniteScrollListener.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/InfiniteScrollListener.tsx b/apps/admin-x-design-system/src/global/InfiniteScrollListener.tsx similarity index 82% rename from apps/admin-x-settings/src/admin-x-ds/global/InfiniteScrollListener.tsx rename to apps/admin-x-design-system/src/global/InfiniteScrollListener.tsx index 632bc75ac9d..08dd4d8d60c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/InfiniteScrollListener.tsx +++ b/apps/admin-x-design-system/src/global/InfiniteScrollListener.tsx @@ -1,15 +1,17 @@ import React, {useEffect, useRef} from 'react'; +export interface InfiniteScrollListenerProps { + /** How many pixels before the end of the container the callback should trigger */ + offset: number; + onTrigger: () => void; +} + /** * Triggers a callback when the user scrolls close to the end of an element * (exactly how close is configurable with `offset`). The parent element must have * position: relative/absolute/etc. */ -const InfiniteScrollListener: React.FC<{ - /** How many pixels before the end of the container the callback should trigger */ - offset: number - onTrigger: () => void -}> = ({offset, onTrigger}) => { +const InfiniteScrollListener: React.FC = ({offset, onTrigger}) => { const ref = useRef(null); useEffect(() => { diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Link.stories.tsx b/apps/admin-x-design-system/src/global/Link.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Link.stories.tsx rename to apps/admin-x-design-system/src/global/Link.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Link.tsx b/apps/admin-x-design-system/src/global/Link.tsx similarity index 89% rename from apps/admin-x-settings/src/admin-x-ds/global/Link.tsx rename to apps/admin-x-design-system/src/global/Link.tsx index dc389789e5c..476fa0244ab 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Link.tsx +++ b/apps/admin-x-design-system/src/global/Link.tsx @@ -1,6 +1,6 @@ import React from 'react'; -interface LinkProps extends React.ComponentPropsWithoutRef<'a'> { +export interface LinkProps extends React.ComponentPropsWithoutRef<'a'> { href: string; /** diff --git a/apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx b/apps/admin-x-design-system/src/global/List.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/List.stories.tsx rename to apps/admin-x-design-system/src/global/List.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/List.tsx b/apps/admin-x-design-system/src/global/List.tsx similarity index 97% rename from apps/admin-x-settings/src/admin-x-ds/global/List.tsx rename to apps/admin-x-design-system/src/global/List.tsx index a2e43abcd50..bbbb41dffab 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/List.tsx +++ b/apps/admin-x-design-system/src/global/List.tsx @@ -1,11 +1,11 @@ +import clsx from 'clsx'; +import React from 'react'; import Heading from './Heading'; import Hint from './Hint'; import ListHeading, {ListHeadingSize} from './ListHeading'; -import React from 'react'; import Separator from './Separator'; -import clsx from 'clsx'; -interface ListProps { +export interface ListProps { /** * If the list is the primary content on a page (e.g. Members list) then you can set a pagetitle to be consistent */ @@ -62,4 +62,4 @@ const List: React.FC = ({ ); }; -export default List; \ No newline at end of file +export default List; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ListHeading.tsx b/apps/admin-x-design-system/src/global/ListHeading.tsx similarity index 94% rename from apps/admin-x-settings/src/admin-x-ds/global/ListHeading.tsx rename to apps/admin-x-design-system/src/global/ListHeading.tsx index bf82d757dbd..50e60095908 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ListHeading.tsx +++ b/apps/admin-x-design-system/src/global/ListHeading.tsx @@ -1,10 +1,10 @@ -import Heading from './Heading'; import React from 'react'; +import Heading from './Heading'; import Separator from './Separator'; export type ListHeadingSize = 'sm' | 'lg'; -interface ListHeadingProps { +export interface ListHeadingProps { title?: React.ReactNode; titleSize?: ListHeadingSize, actions?: React.ReactNode; @@ -44,4 +44,4 @@ const ListHeading: React.FC = ({ return <>; }; -export default ListHeading; \ No newline at end of file +export default ListHeading; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx b/apps/admin-x-design-system/src/global/ListItem.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/ListItem.stories.tsx rename to apps/admin-x-design-system/src/global/ListItem.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ListItem.tsx b/apps/admin-x-design-system/src/global/ListItem.tsx similarity index 98% rename from apps/admin-x-settings/src/admin-x-ds/global/ListItem.tsx rename to apps/admin-x-design-system/src/global/ListItem.tsx index 8c1b0fffdae..ca2b616ad6f 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/ListItem.tsx +++ b/apps/admin-x-design-system/src/global/ListItem.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; -interface ListItemProps { +export interface ListItemProps { id?: string; title?: React.ReactNode; detail?: React.ReactNode; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/LoadingIndicator.stories.tsx b/apps/admin-x-design-system/src/global/LoadingIndicator.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/LoadingIndicator.stories.tsx rename to apps/admin-x-design-system/src/global/LoadingIndicator.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/LoadingIndicator.tsx b/apps/admin-x-design-system/src/global/LoadingIndicator.tsx similarity index 97% rename from apps/admin-x-settings/src/admin-x-ds/global/LoadingIndicator.tsx rename to apps/admin-x-design-system/src/global/LoadingIndicator.tsx index 1b77113acc5..4f5f40fb752 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/LoadingIndicator.tsx +++ b/apps/admin-x-design-system/src/global/LoadingIndicator.tsx @@ -3,7 +3,7 @@ import React from 'react'; export type LoadingIndicatorSize = 'sm' | 'md' | 'lg'; export type LoadingIndicatorColor = 'light' | 'dark'; -type LoadingIndicatorProps = { +export interface LoadingIndicatorProps { size?: LoadingIndicatorSize; color?: LoadingIndicatorColor; delay?: number; @@ -60,4 +60,4 @@ export const LoadingIndicator: React.FC = ({size, color,
); } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx b/apps/admin-x-design-system/src/global/Menu.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Menu.stories.tsx rename to apps/admin-x-design-system/src/global/Menu.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Menu.tsx b/apps/admin-x-design-system/src/global/Menu.tsx similarity index 97% rename from apps/admin-x-settings/src/admin-x-ds/global/Menu.tsx rename to apps/admin-x-design-system/src/global/Menu.tsx index 042f51d479a..480604eacc8 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Menu.tsx +++ b/apps/admin-x-design-system/src/global/Menu.tsx @@ -1,6 +1,6 @@ +import React from 'react'; import Button, {ButtonProps, ButtonSize} from './Button'; import Popover, {PopoverPosition} from './Popover'; -import React from 'react'; export type MenuItem = { id: string, @@ -8,7 +8,7 @@ export type MenuItem = { onClick?: () => void } -interface MenuProps { +export interface MenuProps { trigger?: React.ReactNode; triggerButtonProps?: ButtonProps; triggerSize?: ButtonSize; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/NoValueLabel.stories.tsx b/apps/admin-x-design-system/src/global/NoValueLabel.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/NoValueLabel.stories.tsx rename to apps/admin-x-design-system/src/global/NoValueLabel.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/NoValueLabel.tsx b/apps/admin-x-design-system/src/global/NoValueLabel.tsx similarity index 88% rename from apps/admin-x-settings/src/admin-x-ds/global/NoValueLabel.tsx rename to apps/admin-x-design-system/src/global/NoValueLabel.tsx index 7b02360fe0b..1a980acc14a 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/NoValueLabel.tsx +++ b/apps/admin-x-design-system/src/global/NoValueLabel.tsx @@ -1,7 +1,7 @@ -import Icon from './Icon'; import React from 'react'; +import Icon from './Icon'; -interface NoValueLabelProps { +export interface NoValueLabelProps { icon?: string; children: React.ReactNode; } @@ -18,4 +18,4 @@ const NoValueLabel: React.FC = ({icon, children}) => { ); }; -export default NoValueLabel; \ No newline at end of file +export default NoValueLabel; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Pagination.stories.tsx b/apps/admin-x-design-system/src/global/Pagination.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Pagination.stories.tsx rename to apps/admin-x-design-system/src/global/Pagination.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Pagination.tsx b/apps/admin-x-design-system/src/global/Pagination.tsx similarity index 94% rename from apps/admin-x-settings/src/admin-x-ds/global/Pagination.tsx rename to apps/admin-x-design-system/src/global/Pagination.tsx index 6f2f6283f53..d994234d88f 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Pagination.tsx +++ b/apps/admin-x-design-system/src/global/Pagination.tsx @@ -1,8 +1,8 @@ -import Icon from './Icon'; import React from 'react'; -import {PaginationData} from '../../hooks/usePagination'; +import {PaginationData} from '../hooks/usePagination'; +import Icon from './Icon'; -type PaginationProps = PaginationData +export type PaginationProps = PaginationData const Pagination: React.FC = ({page, limit, total, prevPage, nextPage}) => { // Detect loading state diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Popover.stories.tsx b/apps/admin-x-design-system/src/global/Popover.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Popover.stories.tsx rename to apps/admin-x-design-system/src/global/Popover.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Popover.tsx b/apps/admin-x-design-system/src/global/Popover.tsx similarity index 85% rename from apps/admin-x-settings/src/admin-x-ds/global/Popover.tsx rename to apps/admin-x-design-system/src/global/Popover.tsx index f84cb6dca13..01b25a2d3cd 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Popover.tsx +++ b/apps/admin-x-design-system/src/global/Popover.tsx @@ -1,10 +1,10 @@ -import React, {useRef, useState} from 'react'; import clsx from 'clsx'; +import React, {useRef, useState} from 'react'; import {createPortal} from 'react-dom'; export type PopoverPosition = 'left' | 'right'; -interface PopoverProps { +export interface PopoverProps { trigger: React.ReactNode; children: React.ReactNode; position?: PopoverPosition; @@ -32,14 +32,14 @@ const Popover: React.FC = ({ const handleTriggerClick = () => { if (!open && triggerRef.current) { const parentRect = getOffsetPosition(triggerRef.current); - let {x, y, width, height} = triggerRef.current.getBoundingClientRect(); - x -= parentRect.x; - y -= parentRect.y; + const {x, y, width, height} = triggerRef.current.getBoundingClientRect(); + const relativeX = x - parentRect.x; + const relativeY = y - parentRect.y; - const finalX = (position === 'left') ? x : window.innerWidth - (x + width); + const finalX = (position === 'left') ? relativeX : window.innerWidth - (relativeX + width); setOpen(true); setPositionX(finalX); - setPositionY(y + height); + setPositionY(relativeY + height); } else { setOpen(false); } @@ -89,7 +89,7 @@ const Popover: React.FC = ({
{children}
-
, triggerRef.current?.closest('.admin-x-settings') || document.body)} + , triggerRef.current?.closest('.admin-x-base') || document.body)} ); }; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Separator.stories.tsx b/apps/admin-x-design-system/src/global/Separator.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Separator.stories.tsx rename to apps/admin-x-design-system/src/global/Separator.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Separator.tsx b/apps/admin-x-design-system/src/global/Separator.tsx similarity index 80% rename from apps/admin-x-settings/src/admin-x-ds/global/Separator.tsx rename to apps/admin-x-design-system/src/global/Separator.tsx index 5df81e57e1e..2f1b9c18473 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Separator.tsx +++ b/apps/admin-x-design-system/src/global/Separator.tsx @@ -1,6 +1,6 @@ import React from 'react'; -interface SeparatorProps { +export interface SeparatorProps { className?: string; } @@ -11,4 +11,4 @@ const Separator: React.FC = ({className}) => { return
; }; -export default Separator; \ No newline at end of file +export default Separator; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx b/apps/admin-x-design-system/src/global/SortableList.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/SortableList.stories.tsx rename to apps/admin-x-design-system/src/global/SortableList.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/SortableList.tsx b/apps/admin-x-design-system/src/global/SortableList.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/SortableList.tsx rename to apps/admin-x-design-system/src/global/SortableList.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx b/apps/admin-x-design-system/src/global/StickyFooter.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.stories.tsx rename to apps/admin-x-design-system/src/global/StickyFooter.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.tsx b/apps/admin-x-design-system/src/global/StickyFooter.tsx similarity index 96% rename from apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.tsx rename to apps/admin-x-design-system/src/global/StickyFooter.tsx index ff951baf1ed..9271689d401 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/StickyFooter.tsx +++ b/apps/admin-x-design-system/src/global/StickyFooter.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; -interface StickyFooterProps { +export interface StickyFooterProps { shiftY?: string; footerBgColorClass?: string; contentBgColorClass?: string; @@ -68,4 +68,4 @@ const StickyFooter: React.FC = ({ ); }; -export default StickyFooter; \ No newline at end of file +export default StickyFooter; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TabView.stories.tsx b/apps/admin-x-design-system/src/global/TabView.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/TabView.stories.tsx rename to apps/admin-x-design-system/src/global/TabView.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TabView.tsx b/apps/admin-x-design-system/src/global/TabView.tsx similarity index 98% rename from apps/admin-x-settings/src/admin-x-ds/global/TabView.tsx rename to apps/admin-x-design-system/src/global/TabView.tsx index aa959ce572c..ca8fd8361e5 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/TabView.tsx +++ b/apps/admin-x-design-system/src/global/TabView.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; export type Tab = { id: ID; @@ -12,7 +12,7 @@ export type Tab = { contents?: React.ReactNode; } -interface TabViewProps { +export interface TabViewProps { tabs: readonly Tab[]; onTabChange: (id: ID) => void; selectedTab?: ID; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Table.stories.tsx b/apps/admin-x-design-system/src/global/Table.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/Table.stories.tsx rename to apps/admin-x-design-system/src/global/Table.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Table.tsx b/apps/admin-x-design-system/src/global/Table.tsx similarity index 98% rename from apps/admin-x-settings/src/admin-x-ds/global/Table.tsx rename to apps/admin-x-design-system/src/global/Table.tsx index 68db36fa239..e7d21055b21 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Table.tsx +++ b/apps/admin-x-design-system/src/global/Table.tsx @@ -1,19 +1,19 @@ +import clsx from 'clsx'; +import React from 'react'; +import {PaginationData} from '../hooks/usePagination'; import Heading from './Heading'; import Hint from './Hint'; +import {LoadingIndicator} from './LoadingIndicator'; import Pagination from './Pagination'; -import React from 'react'; import Separator from './Separator'; import TableRow from './TableRow'; -import clsx from 'clsx'; -import {LoadingIndicator} from './LoadingIndicator'; -import {PaginationData} from '../../hooks/usePagination'; export interface ShowMoreData { hasMore: boolean; loadMore: () => void; } -interface TableProps { +export interface TableProps { /** * If the table is the primary content on a page (e.g. Members table) then you can set a pagetitle to be consistent */ diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TableCell.tsx b/apps/admin-x-design-system/src/global/TableCell.tsx similarity index 87% rename from apps/admin-x-settings/src/admin-x-ds/global/TableCell.tsx rename to apps/admin-x-design-system/src/global/TableCell.tsx index 0e284261ba4..fe6e3bdcd2c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/TableCell.tsx +++ b/apps/admin-x-design-system/src/global/TableCell.tsx @@ -1,7 +1,7 @@ -import React, {HTMLProps} from 'react'; import clsx from 'clsx'; +import React, {HTMLProps} from 'react'; -interface TableCellProps extends HTMLProps { +export interface TableCellProps extends HTMLProps { padding?: boolean; } diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TableHead.tsx b/apps/admin-x-design-system/src/global/TableHead.tsx similarity index 71% rename from apps/admin-x-settings/src/admin-x-ds/global/TableHead.tsx rename to apps/admin-x-design-system/src/global/TableHead.tsx index 78a6c0e0924..248cb7b625d 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/TableHead.tsx +++ b/apps/admin-x-design-system/src/global/TableHead.tsx @@ -1,8 +1,10 @@ -import Heading from './Heading'; -import React, {HTMLProps} from 'react'; import clsx from 'clsx'; +import React, {HTMLProps} from 'react'; +import Heading from './Heading'; + +export type TableHeadProps = HTMLProps -const TableHead: React.FC> = ({className, children, colSpan, ...props}) => { +const TableHead: React.FC = ({className, children, colSpan, ...props}) => { const tableCellClasses = clsx( '!py-2 !pl-0 !pr-6 text-left align-top', props.onClick && 'hover:cursor-pointer', @@ -16,4 +18,4 @@ const TableHead: React.FC> = ({className, childr ); }; -export default TableHead; \ No newline at end of file +export default TableHead; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TableRow.stories.tsx b/apps/admin-x-design-system/src/global/TableRow.stories.tsx similarity index 100% rename from apps/admin-x-settings/src/admin-x-ds/global/TableRow.stories.tsx rename to apps/admin-x-design-system/src/global/TableRow.stories.tsx diff --git a/apps/admin-x-settings/src/admin-x-ds/global/TableRow.tsx b/apps/admin-x-design-system/src/global/TableRow.tsx similarity index 98% rename from apps/admin-x-settings/src/admin-x-ds/global/TableRow.tsx rename to apps/admin-x-design-system/src/global/TableRow.tsx index bf7567ea647..d7831f34550 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/TableRow.tsx +++ b/apps/admin-x-design-system/src/global/TableRow.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import clsx from 'clsx'; +import React from 'react'; -interface TableRowProps { +export interface TableRowProps { id?: string; action?: React.ReactNode; hideActions?: boolean; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx b/apps/admin-x-design-system/src/global/Toast.stories.tsx similarity index 83% rename from apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx rename to apps/admin-x-design-system/src/global/Toast.stories.tsx index f806ef499cd..6609b6c0b9f 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/Toast.stories.tsx +++ b/apps/admin-x-design-system/src/global/Toast.stories.tsx @@ -1,8 +1,19 @@ -import {ReactNode} from 'react'; import type {Meta, StoryObj} from '@storybook/react'; +import {ReactNode} from 'react'; -import ToastContainer from './ToastContainer'; import {Toaster} from 'react-hot-toast'; +import Button from './Button'; +import {ShowToastProps, showToast} from './Toast'; + +const ToastContainer: React.FC = ({...props}) => { + return ( + <> + - ); -}; diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Header.stories.ts b/apps/admin-x-settings/src/admin-x-ds/experimental/Header.stories.ts deleted file mode 100644 index 26f70b8aea0..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Header.stories.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {Header} from './Header'; -import type {Meta, StoryObj} from '@storybook/react'; - -const meta = { - title: 'Experimental / Header', - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs - tags: ['autodocs'], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout - layout: 'fullscreen' - } -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: 'Jane Doe' - } - } -}; - -export const LoggedOut: Story = {}; diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Header.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Header.tsx deleted file mode 100644 index b55ae7c02d6..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Header.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import './header.css'; -import {ExampleButton} from './ExampleButton'; - -type User = { - name: string; -}; - -interface HeaderProps { - user?: User; - onLogin: () => void; - onLogout: () => void; - onCreateAccount: () => void; -} - -export const Header = ({user, onLogin, onLogout, onCreateAccount}: HeaderProps) => ( -
-
-
- - - - - - - -

Acme

-
-
- {user ? ( - <> - - Welcome, {user.name}! - - - - ) : ( - <> - - - - )} -
-
-
-); diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Page.stories.ts b/apps/admin-x-settings/src/admin-x-ds/experimental/Page.stories.ts deleted file mode 100644 index 0da02551892..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Page.stories.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {userEvent, within} from '@storybook/testing-library'; -import type {Meta, StoryObj} from '@storybook/react'; - -import {Page} from './Page'; - -const meta = { - title: 'Experimental / Page', - component: Page, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout - layout: 'fullscreen' - } -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedOut: Story = {}; - -// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing -export const LoggedIn: Story = { - play: async ({canvasElement}) => { - const canvas = within(canvasElement); - const loginButton = await canvas.getByRole('button', { - name: /Log in/i - }); - await userEvent.click(loginButton); - } -}; diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Page.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Page.tsx deleted file mode 100644 index b31ddf2ec29..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import './page.css'; -import {Header} from './Header'; - -type User = { - name: string; -}; - -export const Page: React.FC = () => { - const [user, setUser] = React.useState(); - - return ( -
-
setUser({name: 'Jane Doe'})} - onLogin={() => setUser({name: 'Jane Doe'})} - onLogout={() => setUser(undefined)} - /> - -
-

Pages in Storybook

-

- We recommend building UIs with a{' '} - - component-driven - {' '} - process starting with atomic components and ending with pages. -

-

- Render pages with mock data. This makes it easy to build and review page states without - needing to navigate to them in your app. Here are some handy patterns for managing page - data in Storybook: -

-
    -
  • - Use a higher-level connected component. Storybook helps you compose such data from the - "args" of child component stories -
  • -
  • - Assemble data in the page component from your services. You can mock these services out - using Storybook. -
  • -
-

- Get a guided tutorial on component-driven development at{' '} - - Storybook tutorials - - . Read more in the{' '} - - docs - - . -

-
- Tip Adjust the width of the canvas with the{' '} - - - - - - Viewports addon in the toolbar -
-
-
- ); -}; diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Task.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Task.stories.tsx deleted file mode 100644 index 3ad8e8ca455..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Task.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Task from './Task'; - -const story = { - component: Task, - title: 'Experimental / Task', - tags: ['autodocs'] -}; - -export default story; - -export const Default = { - args: { - task: { - id: '1', - title: 'Test task', - state: 'TASK_INBOX' - } - } -}; - -export const Pinned = { - args: { - task: { - ...Default.args.task, - state: 'TASK_PINNED' - } - } -}; - -export const Archived = { - args: { - task: { - ...Default.args.task, - state: 'TASK_ARCHIVED' - } - } -}; \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Task.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Task.tsx deleted file mode 100644 index 9960ad4aa1d..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Task.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; - -interface Props { - task: { - id: string, - title: string, - state: string, - } - onArchiveTask: (id: string) => void, - onPinTask: (id: string) => void, -} - -const Task: React.FC = ({task: {id, title, state}, onArchiveTask, onPinTask}) => { - return ( -
- - - - - {state !== 'TASK_ARCHIVED' && ( - - )} -
- ); -}; - -export default Task; \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx deleted file mode 100644 index e07cb3ed48b..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.stories.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import TaskList from './Tasklist'; -import {ReactNode} from 'react'; - -import * as TaskStories from './Task.stories'; - -const story = { - component: TaskList, - title: 'Experimental / Task List', - decorators: [(_story: () => ReactNode) =>
{_story()}
], - tags: ['autodocs'] -}; - -export default story; - -export const Default = { - args: { - tasks: [ - {...TaskStories.Default.args.task, id: '1', title: 'Task 1'}, - {...TaskStories.Default.args.task, id: '2', title: 'Task 2'}, - {...TaskStories.Default.args.task, id: '3', title: 'Task 3'}, - {...TaskStories.Default.args.task, id: '4', title: 'Task 4'} - ] - } -}; - -export const WithPinnedTasks = { - args: { - tasks: [ - ...Default.args.tasks.slice(0, 3), - {id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED'} - ] - } -}; - -export const Loading = { - args: { - tasks: [], - loading: true - } -}; - -export const Empty = { - args: { - ...Loading.args, - loading: false - } -}; diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.tsx b/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.tsx deleted file mode 100644 index 3bbae3876a7..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/Tasklist.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import Task from './Task'; - -interface Props { - loading: boolean, - tasks: Array<{ - id: string, - title: string, - state: string, - }>, - onArchiveTask: (id: string) => void, - onPinTask: (id: string) => void, -} - -const TaskList: React.FC = ({loading, tasks, onPinTask, onArchiveTask}) => { - const events = { - onPinTask, - onArchiveTask - }; - - if (loading) { - return
Loading
; - } - - if (tasks.length === 0) { - return
empty
; - } - - return ( -
- {tasks.map(task => ( - - ))} -
- ); -}; - -export default TaskList; \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/code-brackets.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/code-brackets.svg deleted file mode 100644 index 73de9477600..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/code-brackets.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/code-brackets \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/colors.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/colors.svg deleted file mode 100644 index 17d58d516e1..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/colors.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/colors \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/comments.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/comments.svg deleted file mode 100644 index 6493a139f52..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/comments.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/comments \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/direction.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/direction.svg deleted file mode 100644 index 65676ac2722..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/direction.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/direction \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/flow.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/flow.svg deleted file mode 100644 index 8ac27db403c..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/flow.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/flow \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/plugin.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/plugin.svg deleted file mode 100644 index 29e5c690c0a..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/plugin.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/plugin \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/repo.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/repo.svg deleted file mode 100644 index f386ee902c1..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/repo.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/repo \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/stackalt.svg b/apps/admin-x-settings/src/admin-x-ds/experimental/assets/stackalt.svg deleted file mode 100644 index 9b7ad274350..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/assets/stackalt.svg +++ /dev/null @@ -1 +0,0 @@ -illustration/stackalt \ No newline at end of file diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/button.css b/apps/admin-x-settings/src/admin-x-ds/experimental/button.css deleted file mode 100644 index dc91dc76370..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-weight: 700; - border: 0; - border-radius: 3em; - cursor: pointer; - display: inline-block; - line-height: 1; -} -.storybook-button--primary { - color: white; - background-color: #1ea7fd; -} -.storybook-button--secondary { - color: #333; - background-color: transparent; - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; -} -.storybook-button--small { - font-size: 12px; - padding: 10px 16px; -} -.storybook-button--medium { - font-size: 14px; - padding: 11px 20px; -} -.storybook-button--large { - font-size: 16px; - padding: 12px 24px; -} diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/header.css b/apps/admin-x-settings/src/admin-x-ds/experimental/header.css deleted file mode 100644 index 9fb414e5040..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/header.css +++ /dev/null @@ -1,14 +0,0 @@ -.storybook-wrapper { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.storybook-welcome { - color: #333; - font-size: 14px; - margin-right: 10px; -} diff --git a/apps/admin-x-settings/src/admin-x-ds/experimental/page.css b/apps/admin-x-settings/src/admin-x-ds/experimental/page.css deleted file mode 100644 index fb64fe46294..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/experimental/page.css +++ /dev/null @@ -1,69 +0,0 @@ -section { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 24px; - padding: 48px 20px; - margin: 0 auto; - max-width: 600px; - color: #333; -} - -section h2 { - font-weight: 700; - font-size: 32px; - line-height: 1; - margin: 0 0 4px; - display: inline-block; - vertical-align: top; -} - -section p { - margin: 1em 0; -} - -section a { - text-decoration: none; - color: #1ea7fd; -} - -section ul { - padding-left: 30px; - margin: 1em 0; -} - -section li { - margin-bottom: 8px; -} - -section .tip { - display: inline-block; - border-radius: 1em; - font-size: 11px; - line-height: 12px; - font-weight: 700; - background: #e7fdd8; - color: #66bf3c; - padding: 4px 12px; - margin-right: 10px; - vertical-align: top; -} - -section .tip-wrapper { - font-size: 13px; - line-height: 20px; - margin-top: 40px; - margin-bottom: 40px; -} - -section .tip-wrapper svg { - display: inline-block; - height: 12px; - width: 12px; - margin-right: 4px; - vertical-align: top; - margin-top: 3px; -} - -section .tip-wrapper svg path { - fill: #1ea7fd; -} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/ToastContainer.tsx b/apps/admin-x-settings/src/admin-x-ds/global/ToastContainer.tsx deleted file mode 100644 index 36166a8a8c2..00000000000 --- a/apps/admin-x-settings/src/admin-x-ds/global/ToastContainer.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Button from './Button'; -import React from 'react'; -import {ShowToastProps, showToast} from './Toast'; - -const ToastContainer: React.FC = ({...props}) => { - return ( - <> - + ); +}; + +const Sidebar: React.FC = () => { + const typeOptions = [ + {title: 'Discount', description: 'Offer a special reduced price'}, + {title: 'Free trial', description: 'Give free access for a limited time'} + ]; + + const tierCadenceOptions = [ + {value: '1', label: 'Bronze — Monthly'}, + {value: '2', label: 'Bronze — Yearly'}, + {value: '3', label: 'Silver — Monthly'}, + {value: '4', label: 'Silver — Yearly'}, + {value: '5', label: 'Gold — Monthly'}, + {value: '6', label: 'Gold — Yearly'} + ]; + + const amountOptions = [ + {value: '1', label: '%'}, + {value: '2', label: 'USD'} + ]; + + const durationOptions = [ + {value: '1', label: 'First-payment'}, + {value: '2', label: 'Multiple-months'}, + {value: '3', label: 'Forever'} + ]; + + return ( +
+
+ +
+

Offer details

+
+
+ + +
+ {}} + /> +
+
+ - -

- Recommended: {{this.maxTermsLength}} characters. - You've used {{gh-count-down-html-characters this.settings.portalSignupTermsHtml this.maxTermsLength}} -

- - - -
-

Require agreement

-
- -
-
-
- - {{/liquid-if}} - {{/let}} - - {{#let (eq this.openSection "look-and-feel") as |isOpen|}} - - {{#liquid-if isOpen}} - - {{/liquid-if}} - {{/let}} - - {{#let (eq this.openSection "account-page") as |isOpen|}} - - {{#liquid-if isOpen}} - - {{/liquid-if}} - {{/let}} - - - - {{#if (and (not this.membersUtils.isStripeEnabled) this.session.user.isAdmin)}} -
-

Collect payments on signup?

-

Generate revenue to support your work, by offering premium membership tiers.

-

Ghost takes 0% payment fees, everything you earn is yours to keep.

- -
- {{/if}} - -
-
- -
- - {{svg-jar "arrow-down-small"}} -
- -
- - - -
-
- - {{#if this.showLinksPage}} -
-
- -
-
- {{/if}} - -
-
- -
- -
- {{/if}} - - diff --git a/ghost/admin/app/components/modal-portal-settings.js b/ghost/admin/app/components/modal-portal-settings.js deleted file mode 100644 index f28efaabf5d..00000000000 --- a/ghost/admin/app/components/modal-portal-settings.js +++ /dev/null @@ -1,436 +0,0 @@ -import ConfirmEmailModal from './modals/settings/confirm-email'; -import ModalComponent from 'ghost-admin/components/modal-base'; -import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; -import {action, computed} from '@ember/object'; -import {htmlSafe} from '@ember/template'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task, timeout} from 'ember-concurrency'; -const ICON_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg']; - -export default ModalComponent.extend({ - modals: service(), - membersUtils: service(), - settings: service(), - store: service(), - session: service(), - feature: service(), - ghostPaths: service(), - ajax: service(), - - page: 'signup', - iconExtensions: null, - isShowModalLink: true, - customIcon: null, - showLinksPage: false, - showLeaveSettingsModal: false, - isPreloading: true, - changedTiers: null, - openSection: null, - portalPreviewGuid: 'modal-portal-settings', - closeOnEnter: false, - maxTermsLength: 115, - - confirm() {}, - - config: inject(), - - backgroundStyle: computed('settings.accentColor', function () { - let color = this.settings.accentColor || '#ffffff'; - return htmlSafe(`background-color: ${color}`); - }), - - showModalLinkOrAttribute: computed('isShowModalLink', function () { - if (this.isShowModalLink) { - return `#/portal`; - } - return `data-portal`; - }), - - portalPreviewUrl: computed('page', 'model.tiers.[]', 'changedTiers.[]', 'membersUtils.{isFreeChecked,isMonthlyChecked,isYearlyChecked}', 'settings.{portalName,portalButton,portalButtonIcon,portalButtonSignupText,portalSignupTermsHtml,portalSignupCheckboxRequired,portalButtonStyle,accentColor,portalPlans.[]}', function () { - const options = this.getProperties(['page']); - options.portalTiers = this.model.tiers?.filter((tier) => { - return tier.get('visibility') === 'public' - && tier.get('active') === true - && tier.get('type') === 'paid'; - }).map((tier) => { - return tier.id; - }); - const freeTier = this.model.tiers?.find((tier) => { - return tier.type === 'free'; - }); - options.isFreeChecked = freeTier?.visibility === 'public'; - return this.membersUtils.getPortalPreviewUrl(options); - }), - - showIconSetting: computed('selectedButtonStyle', function () { - const selectedButtonStyle = this.get('selectedButtonStyle.name') || ''; - return selectedButtonStyle.includes('icon'); - }), - - showButtonTextSetting: computed('selectedButtonStyle', function () { - const selectedButtonStyle = this.get('selectedButtonStyle.name') || ''; - return selectedButtonStyle.includes('text'); - }), - - selectedButtonStyle: computed('settings.portalButtonStyle', function () { - return this.buttonStyleOptions.find((buttonStyle) => { - return (buttonStyle.name === this.settings.portalButtonStyle); - }); - }), - - isFreeChecked: computed('settings.{portalPlans.[],membersSignupAccess}', function () { - const allowedPlans = this.settings.portalPlans || []; - return (this.settings.membersSignupAccess === 'all' && allowedPlans.includes('free')); - }), - isMonthlyChecked: computed('settings.portalPlans.[]', 'membersUtils.paidMembersEnabled', function () { - const allowedPlans = this.settings.portalPlans || []; - return (this.membersUtils.paidMembersEnabled && allowedPlans.includes('monthly')); - }), - isYearlyChecked: computed('settings.portalPlans.[]', 'membersUtils.paidMembersEnabled', function () { - const allowedPlans = this.settings.portalPlans || []; - return (this.membersUtils.paidMembersEnabled && allowedPlans.includes('yearly')); - }), - tiers: computed('model.tiers.[]', 'changedTiers.[]', 'isPreloading', function () { - const paidTiers = this.model.tiers?.filter(tier => tier.type === 'paid' && tier.active === true); - if (this.isPreloading || !paidTiers?.length) { - return []; - } - - const tiers = paidTiers.map((tier) => { - return { - id: tier.id, - name: tier.name, - checked: tier.visibility === 'public' - }; - }); - return tiers; - }), - - showPortalPrices: computed('tiers', function () { - const visibleTiers = this.model.tiers?.filter((tier) => { - return tier.visibility === 'public' && tier.type === 'paid'; - }); - - return !!visibleTiers?.length; - }), - - setTermsHtml: action((event) => { - this.settings.portalSignupTermsHtml = event.target.value; - }), - - init() { - this._super(...arguments); - this.buttonStyleOptions = [ - {name: 'icon-and-text', label: 'Icon and text'}, - {name: 'icon-only', label: 'Icon only'}, - {name: 'text-only', label: 'Text only'} - ]; - this.availablePages = [{ - name: 'signup', - label: 'Signup' - }, { - name: 'accountHome', - label: 'Account' - }, { - name: 'links', - label: 'Links' - }]; - this.iconExtensions = ICON_EXTENSIONS; - this.changedTiers = []; - this.set('supportAddress', this.parseEmailAddress(this.settings.membersSupportAddress)); - this.set('openSection', 'signup-options'); - }, - - didInsertElement() { - this._super(...arguments); - this.settings.errors.clear(); - }, - - actions: { - toggleFreePlan(isChecked) { - this.updateAllowedPlan('free', isChecked); - }, - togglePlan(plan, event) { - this.updateAllowedPlan(plan, event.target.checked); - }, - toggleTier(tierId, event) { - this.updateAllowedTier(tierId, event.target.checked); - }, - togglePortalButton(showButton) { - this.settings.portalButton = showButton; - }, - - togglePortalName(showSignupName) { - this.settings.portalName = showSignupName; - }, - toggleSection(section) { - if (this.get('openSection') === section) { - this.set('openSection', null); - } else { - this.set('openSection', section); - } - }, - - confirm() { - return this.saveTask.perform(); - }, - - isPlanSelected(plan) { - const allowedPlans = this.settings.portalPlans; - return allowedPlans.includes(plan); - }, - - switchPreviewPage(page) { - if (page.name === 'links') { - this.set('showLinksPage', true); - this.set('page', ''); - } else { - this.set('showLinksPage', false); - this.set('page', page.name); - } - }, - - switchToSignupPage() { - if (this.showLinksPage) { - this.set('showLinksPage', false); - this.set('page', 'signup'); - } - }, - - setButtonStyle(buttonStyle) { - this.settings.portalButtonStyle = buttonStyle.name; - }, - - setSignupButtonText(event) { - this.settings.portalButtonSignupText = event.target.value; - }, - /** - * Fired after an image upload completes - * @param {string} property - Property name to be set on `this.settings` - * @param {UploadResult[]} results - Array of UploadResult objects - * @return {string} The URL that was set on `this.settings.property` - */ - imageUploaded(property, results) { - if (results[0]) { - this.set('customIcon', results[0].url); - this.settings.portalButtonIcon = results[0].url; - } - }, - /** - * Opens a file selection dialog - Triggered by "Upload Image" buttons, - * searches for the hidden file input within the .gh-setting element - * containing the clicked button then simulates a click - * @param {MouseEvent} event - MouseEvent fired by the button click - */ - triggerFileDialog(event) { - event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click(); - }, - - deleteCustomIcon() { - this.set('customIcon', null); - this.settings.portalButtonIcon = this.membersUtils.defaultIconKeys[0]; - }, - - selectDefaultIcon(icon) { - this.settings.portalButtonIcon = icon; - }, - - closeLeaveSettingsModal() { - this.set('showLeaveSettingsModal', false); - }, - - openStripeConnect() { - this.isWaitingForStripeConnection = true; - this.model.openStripeConnect(); - }, - - leaveSettings() { - this.closeModal(); - }, - - validateFreeSignupRedirect() { - return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect'); - }, - - validatePaidSignupRedirect() { - return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect'); - }, - - setSupportAddress(supportAddress) { - this.set('supportAddress', supportAddress); - - if (this.config.emailDomain && supportAddress === `noreply@${this.config.emailDomain}`) { - this.settings.membersSupportAddress = 'noreply'; - } else { - this.settings.membersSupportAddress = supportAddress; - } - }, - - toggleSignupCheckboxRequired(checked) { - this.settings.portalSignupCheckboxRequired = checked; - }, - - validateTermsHtml() { - let content = this.settings.portalSignupTermsHtml ?? ''; - - // Strip HTML-tags and characters from content so we have a reliable character count - content = content.replace(/<[^>]*>?/gm, ''); - content = content.replace(/ /g, ' '); - content = content.replace(/&/g, '&'); - content = content.replace(/"/g, '"'); - content = content.replace(/</g, '<'); - content = content.replace(/>/g, '>'); - - this.settings.errors.remove('portalSignupTermsHtml'); - this.settings.hasValidated.removeObject('portalSignupTermsHtml'); - - if (content.length > this.maxTermsLength) { - this.settings.errors.add('portalSignupTermsHtml', 'Signup notice is too long'); - this.settings.hasValidated.pushObject('portalSignupTermsHtml'); - } - } - }, - - parseEmailAddress(address) { - const emailAddress = address || 'noreply'; - // Adds default domain as site domain - if (emailAddress.indexOf('@') < 0 && this.config.emailDomain) { - return `${emailAddress}@${this.config.emailDomain}`; - } - return emailAddress; - }, - - updateAllowedPlan(plan, isChecked) { - const portalPlans = this.settings.portalPlans || []; - const allowedPlans = [...portalPlans]; - const freeTier = this.model.tiers.find(p => p.type === 'free'); - - if (!isChecked) { - this.settings.portalPlans = allowedPlans.filter(p => p !== plan); - if (plan === 'free') { - freeTier.set('visibility', 'none'); - } - } else { - allowedPlans.push(plan); - this.settings.portalPlans = allowedPlans; - if (plan === 'free') { - freeTier.set('visibility', 'public'); - } - } - }, - - updateAllowedTier(tierId, isChecked) { - const tier = this.model.tiers.find(p => p.id === tierId); - if (!isChecked) { - tier.set('visibility', 'none'); - } else { - tier.set('visibility', 'public'); - } - let portalTiers = this.model.tiers.filter((p) => { - return p.visibility === 'public'; - }).map(p => p.id); - this.set('changedTiers', portalTiers); - }, - - _validateSignupRedirect(url, type) { - let errMessage = `Please enter a valid URL`; - this.settings.errors.remove(type); - this.settings.hasValidated.removeObject(type); - - if (url === null) { - this.settings.errors.add(type, errMessage); - this.settings.hasValidated.pushObject(type); - return false; - } - - if (url === undefined) { - // Not initialised - return; - } - - if (url.href.startsWith(this.siteUrl)) { - const path = url.href.replace(this.siteUrl, ''); - this.settings[type] = path; - } else { - this.settings[type] = url.href; - } - }, - - finishPreloading: action(async function () { - if (this.model.preloadTask?.isRunning) { - await this.model.preloadTask; - } - - const portalButtonIcon = this.settings.portalButtonIcon || ''; - if (portalButtonIcon && !this.membersUtils.defaultIconKeys.includes(portalButtonIcon)) { - this.set('customIcon', this.settings.portalButtonIcon); - } - - this.siteUrl = this.config.blogUrl; - this.set('isPreloading', false); - }), - - refreshAfterStripeConnected: action(async function () { - if (this.isWaitingForStripeConnection) { - await this.finishPreloading(); - this.notifyPropertyChange('page'); // force preview url to recompute - this.set('portalPreviewGuid', Date.now().valueOf()); // force preview re-render - this.isWaitingForStripeConnection = false; - } - }), - - copyLinkOrAttribute: task(function* () { - copyTextToClipboard(this.showModalLinkOrAttribute); - yield timeout(this.isTesting ? 50 : 3000); - }), - - saveTask: task(function* () { - this.send('validateFreeSignupRedirect'); - this.send('validatePaidSignupRedirect'); - this.send('validateTermsHtml'); - - this.settings.errors.remove('members_support_address'); - this.settings.hasValidated.removeObject('members_support_address'); - - if (this.settings.errors.length !== 0) { - return; - } - - // Save tier visibility if changed - yield Promise.all( - this.model.tiers.filter((tier) => { - const changedAttrs = tier.changedAttributes(); - return !!changedAttrs.visibility; - }).map((tier) => { - return tier.save(); - }) - ); - - const newEmail = this.settings.membersSupportAddress; - - try { - const result = yield this.settings.save(); - if (result._meta?.sent_email_verification) { - yield this.modals.open(ConfirmEmailModal, { - newEmail, - currentEmail: this.settings.membersSupportAddress - }); - } - - this.closeModal(); - } catch (error) { - // Do we have an error that we can show inline? - if (error.payload && error.payload.errors) { - for (const payloadError of error.payload.errors) { - if (payloadError.type === 'ValidationError' && payloadError.property && (payloadError.context || payloadError.message)) { - // Context has a better error message for validation errors - this.settings.errors.add(payloadError.property, payloadError.context || payloadError.message); - this.settings.hasValidated.pushObject(payloadError.property); - } - } - } - throw error; - } - }).drop() -}); diff --git a/ghost/admin/app/components/modal-reset-all-passwords.hbs b/ghost/admin/app/components/modal-reset-all-passwords.hbs deleted file mode 100644 index ccf060f8a4a..00000000000 --- a/ghost/admin/app/components/modal-reset-all-passwords.hbs +++ /dev/null @@ -1,25 +0,0 @@ - -{{svg-jar "close"}} - - - - diff --git a/ghost/admin/app/components/modal-reset-all-passwords.js b/ghost/admin/app/components/modal-reset-all-passwords.js deleted file mode 100644 index 1c4d72f0a12..00000000000 --- a/ghost/admin/app/components/modal-reset-all-passwords.js +++ /dev/null @@ -1,46 +0,0 @@ -import ModalComponent from 'ghost-admin/components/modal-base'; -import {fetch} from 'fetch'; -import {not} from '@ember/object/computed'; -import {inject as service} from '@ember/service'; -import {set} from '@ember/object'; -import {task} from 'ember-concurrency'; - -export default ModalComponent.extend({ - notifications: service(), - - isChecked: false, - isConfirmDisabled: not('isChecked'), - - actions: { - toggleCheckbox() { - set(this, 'isChecked', !this.isChecked); - }, - confirm() { - this.deletePost.perform(); - } - }, - - async _resetPasswords() { - const res = await fetch('/ghost/api/admin/authentication/global_password_reset/', { - method: 'POST' - }); - if (res.status < 200 || res.status >= 300) { - throw new Error('api failed ' + res.status + ' ' + res.statusText); - } - }, - - _failure(error) { - this.notifications.showAPIError(error, {key: 'user.resetAllPasswords.failed'}); - }, - - resetPasswords: task(function* () { - try { - yield this._resetPasswords(); - window.location = window.location.href.split('#')[0]; - } catch (e) { - this._failure(e); - } finally { - this.send('closeModal'); - } - }).drop() -}); diff --git a/ghost/admin/app/components/modal-stripe-connect.hbs b/ghost/admin/app/components/modal-stripe-connect.hbs deleted file mode 100644 index 4f95e6e56b4..00000000000 --- a/ghost/admin/app/components/modal-stripe-connect.hbs +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/ghost/admin/app/components/modal-stripe-connect.js b/ghost/admin/app/components/modal-stripe-connect.js deleted file mode 100644 index db678db0877..00000000000 --- a/ghost/admin/app/components/modal-stripe-connect.js +++ /dev/null @@ -1,62 +0,0 @@ -import ModalBase from 'ghost-admin/components/modal-base'; -import classic from 'ember-classic-decorator'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -// TODO: update modals to work fully with Glimmer components -@classic -export default class ModalStripeConnect extends ModalBase { - @service settings; - @service membersUtils; - - @action - setStripeConnectIntegrationTokenSetting(stripeConnectIntegrationToken) { - this.settings.stripeConnectIntegrationToken = stripeConnectIntegrationToken; - } - - @action - reset() { - // stripeConnectIntegrationToken is not a persisted value so we don't want - // to keep it around across transitions - this.settings.stripeConnectIntegrationToken = undefined; - } - - @action - close(event) { - event?.preventDefault?.(); - this.closeModal(); - } - - @action - confirmAction() { - this.confirm(); - this.close(); - } - - @action - updateSuccessModifier() { - // Note, we should also check isStripeEnabled because stripeDirect option might be enabled - if (this.membersUtils.get('isStripeEnabled') && this.settings.stripeConnectAccountId) { - if (this.modifier?.indexOf('stripe-connected') === -1) { - this.updateModifier(`${this.modifier} stripe-connected`); - } - } else { - if (this.modifier?.indexOf('stripe-connected') !== -1) { - this.updateModifier(this.modifier.replace(/\s?stripe-connected/, '')); - } - } - } - - actions = { - confirm() { - if (this.settings.stripeConnectAccountId) { - return this.confirmAction(); - } - // noop - enter key shouldn't do anything - }, - // needed because ModalBase uses .send() for keyboard events - closeModal() { - this.close(); - } - }; -} diff --git a/ghost/admin/app/components/modal-tier.hbs b/ghost/admin/app/components/modal-tier.hbs deleted file mode 100644 index 5b329a03976..00000000000 --- a/ghost/admin/app/components/modal-tier.hbs +++ /dev/null @@ -1,325 +0,0 @@ - - -
- - -
- -
-
- - diff --git a/ghost/admin/app/components/modal-tier.js b/ghost/admin/app/components/modal-tier.js deleted file mode 100644 index f8523630156..00000000000 --- a/ghost/admin/app/components/modal-tier.js +++ /dev/null @@ -1,336 +0,0 @@ -import ModalBase from 'ghost-admin/components/modal-base'; -import TierBenefitItem from '../models/tier-benefit-item'; -import classic from 'ember-classic-decorator'; -import {action} from '@ember/object'; -import {currencies, getCurrencyOptions, getSymbol, minimumAmountForCurrency} from 'ghost-admin/utils/currency'; -import {A as emberA} from '@ember/array'; -import {htmlSafe} from '@ember/template'; -import {inject} from 'ghost-admin/decorators/inject'; -import {run} from '@ember/runloop'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -const CURRENCIES = currencies.map((currency) => { - return { - value: currency.isoCode.toLowerCase(), - label: `${currency.isoCode} - ${currency.name}`, - isoCode: currency.isoCode - }; -}); - -// Stripe has an upper amount limit of 999,999.99 -// See https://stripe.com/docs/api/payment_intents/object#payment_intent_object-amount -const MAX_AMOUNT = 999_999.99; - -// TODO: update modals to work fully with Glimmer components -@classic -export default class ModalTierPrice extends ModalBase { - @service feature; - @service settings; - @service membersUtils; - - @inject config; - - @tracked model; - @tracked tier; - @tracked periodVal; - @tracked stripeMonthlyAmount = 5; - @tracked stripeYearlyAmount = 50; - @tracked currency = 'usd'; - @tracked stripePlanError = ''; - @tracked benefits = emberA([]); - @tracked newBenefit = null; - @tracked welcomePageURL; - @tracked previewCadence = 'yearly'; - @tracked discountValue = 0; - @tracked hasSaved = false; - @tracked freeTrialEnabled = false; - @tracked savedBenefits; - - accentColorStyle = ''; - - confirm() {} - - get isFreeTier() { - return this.tier.type === 'free'; - } - - get hasTrialDaysError() { - const trialDays = this.tier.get('trialDays'); - return this.freeTrialEnabled && (!trialDays || trialDays < 1); - } - - get allCurrencies() { - return getCurrencyOptions(); - } - - get selectedCurrency() { - return CURRENCIES.findBy('value', this.currency); - } - - get isFreeTrialEnabled() { - return this.freeTrialEnabled && this.tier.get('trialDays') > 0; - } - - init() { - super.init(...arguments); - this.tier = this.model.tier; - this.savedBenefits = this.model.tier?.get('benefits'); - const monthlyPrice = this.tier.get('monthlyPrice'); - const yearlyPrice = this.tier.get('yearlyPrice'); - if (monthlyPrice) { - this.stripeMonthlyAmount = (monthlyPrice / 100); - } - if (yearlyPrice) { - this.stripeYearlyAmount = (yearlyPrice / 100); - } - this.currency = this.tier.get('currency') || 'usd'; - this.benefits = this.tier.get('benefits') || emberA([]); - this.newBenefit = TierBenefitItem.create({ - isNew: true, - name: '' - }); - this.calculateDiscount(); - if (this.tier.get('trialDays')) { - this.freeTrialEnabled = true; - } - this.accentColorStyle = htmlSafe(`color: ${this.settings.accentColor}`); - } - - @action - validateWelcomePageURL() { - const siteUrl = this.siteUrl; - - if (this.welcomePageURL === undefined) { - // Not initialised - return; - } - - if (this.welcomePageURL.href.startsWith(siteUrl)) { - const path = this.welcomePageURL.href.replace(siteUrl, ''); - this.model.tier.welcomePageURL = path; - } else { - this.model.tier.welcomePageURL = this.welcomePageURL.href; - } - } - - get siteUrl() { - return this.config.blogUrl; - } - - // eslint-disable-next-line no-dupe-class-members - get welcomePageURL() { - return this.model.tier.welcomePageURL; - } - - get title() { - if (this.isExistingTier) { - if (this.isFreeTier) { - return `Edit free membership`; - } - return `Edit tier`; - } - return 'New tier'; - } - - get isExistingTier() { - return !this.model.tier.isNew; - } - - @action - close(event) { - if (!this.hasSaved) { - this.reset(); - } - event?.preventDefault?.(); - this.closeModal(); - } - @action - setCurrency(event) { - const newCurrency = event.value; - this.currency = newCurrency; - } - @action - setWelcomePageURL(url) { - this.welcomePageURL = url; - } - - reset() { - this.newBenefit = TierBenefitItem.create({isNew: true, name: ''}); - const finalBenefits = this.savedBenefits || emberA([]); - this.tier.set('benefits', finalBenefits); - this.tier.rollbackAttributes(); - } - - @task({drop: true}) - *saveTier() { - this.validatePrices(); - - if (this.stripePlanError || this.hasTrialDaysError) { - return; - } - - if (!this.newBenefit.get('isBlank')) { - yield this.send('addBenefit', this.newBenefit); - } - - if (!this.isFreeTier) { - const monthlyAmount = Math.round(this.stripeMonthlyAmount * 100); - const yearlyAmount = Math.round(this.stripeYearlyAmount * 100); - this.tier.set('monthlyPrice', monthlyAmount); - this.tier.set('yearlyPrice', yearlyAmount); - this.tier.set('currency', this.currency); - } - - if (!this.freeTrialEnabled) { - this.tier.set('trialDays', 0); - } - - this.tier.set('benefits', this.benefits.filter(benefit => !benefit.get('isBlank'))); - - try { - yield this.tier.save(); - this.hasSaved = true; - yield this.confirm(); - this.send('closeModal'); - - // Reload in the background (no await here) - this.membersUtils.reload(); - } catch (error) { - if (error === undefined) { - // Validation error - return; - } - - throw error; - } - } - - validatePrices() { - this.stripePlanError = undefined; - - try { - const yearlyAmount = this.stripeYearlyAmount; - const monthlyAmount = this.stripeMonthlyAmount; - const symbol = getSymbol(this.currency); - const minimumAmount = minimumAmountForCurrency(this.currency); - - if (!yearlyAmount || (yearlyAmount < minimumAmount) || !monthlyAmount || (monthlyAmount < minimumAmount)) { - throw new TypeError(`Subscription amount cannot be less than ${symbol}${minimumAmount}`); - } - - if (yearlyAmount > MAX_AMOUNT || monthlyAmount > MAX_AMOUNT) { - throw new TypeError(`Subscription amount cannot be more than ${symbol}${MAX_AMOUNT}`); - } - } catch (err) { - this.stripePlanError = err.message; - } - } - - addNewBenefitItem(item) { - item.set('isNew', false); - this.benefits.pushObject(item); - - this.newBenefit = TierBenefitItem.create({isNew: true, name: ''}); - } - - calculateDiscount() { - const discount = this.stripeMonthlyAmount ? 100 - Math.floor((this.stripeYearlyAmount / 12 * 100) / this.stripeMonthlyAmount) : 0; - this.discountValue = discount > 0 ? discount : 0; - } - - @action - changeCadence(cadence) { - this.previewCadence = cadence; - } - - @action - setTrialDays(event) { - const value = parseInt(event.target.value); - this.tier.set('trialDays', value); - } - - @action - setFreeTrialEnabled(event) { - this.freeTrialEnabled = event.target.checked; - if (event.target.checked && !this.tier.get('trialDays')) { - this.tier.set('trialDays', 7); - } - } - - @action - validateStripePlans() { - this.calculateDiscount(); - this.stripePlanError = undefined; - - try { - const yearlyAmount = this.stripeYearlyAmount; - const monthlyAmount = this.stripeMonthlyAmount; - const symbol = getSymbol(this.currency); - const minimumAmount = minimumAmountForCurrency(this.currency); - - if (!yearlyAmount || (yearlyAmount < minimumAmount) || !monthlyAmount || (monthlyAmount < minimumAmount)) { - throw new TypeError(`Subscription amount cannot be less than ${symbol}${minimumAmount}`); - } - - if (yearlyAmount > MAX_AMOUNT || monthlyAmount > MAX_AMOUNT) { - throw new TypeError(`Subscription amount cannot be more than ${symbol}${MAX_AMOUNT}`); - } - } catch (err) { - this.stripePlanError = err.message; - } - } - - actions = { - addBenefit(item) { - return item.validate().then(() => { - this.addNewBenefitItem(item); - }); - }, - focusItem() { - // Focus on next benefit on enter - }, - deleteBenefit(item) { - if (!item) { - return; - } - this.benefits.removeObject(item); - }, - reorderItems() { - this.tier.set('benefits', this.benefits); - }, - updateLabel(label, benefitItem) { - if (!benefitItem) { - return; - } - - if (benefitItem.get('name') !== label) { - benefitItem.set('name', label); - } - }, - // noop - we don't want the enter key doing anything - confirm() {}, - setAmount(amount) { - this.price.amount = !isNaN(amount) ? parseInt(amount) : 0; - }, - - setCurrency(event) { - const newCurrency = event.value; - this.currency = newCurrency; - }, - - // needed because ModalBase uses .send() for keyboard events - closeModal() { - this.close(); - } - }; - - keyPress(event) { - // enter key - if (event.keyCode === 13) { - event.preventDefault(); - run.scheduleOnce('actions', this, this.send, 'addBenefit', this.newBenefit); - } - } -} diff --git a/ghost/admin/app/components/modals/design/confirm-delete-theme.hbs b/ghost/admin/app/components/modals/design/confirm-delete-theme.hbs deleted file mode 100644 index 28d10550485..00000000000 --- a/ghost/admin/app/components/modals/design/confirm-delete-theme.hbs +++ /dev/null @@ -1,20 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/modals/design/confirm-delete-theme.js b/ghost/admin/app/components/modals/design/confirm-delete-theme.js deleted file mode 100644 index f0d4854e2b1..00000000000 --- a/ghost/admin/app/components/modals/design/confirm-delete-theme.js +++ /dev/null @@ -1,31 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class ConfirmDeleteThemeModal extends Component { - @service ghostPaths; - @service notifications; - @service utils; - - @action - downloadTheme(event) { - event.preventDefault(); - this.utils.downloadFile(`${this.ghostPaths.apiRoot}/themes/${this.args.data.theme.name}/download/`); - } - - @task - *deleteThemeTask() { - try { - yield this.args.data.theme.destroyRecord(); - // we need to unload from the store here so that uploading another - // theme with the same "id" doesn't attempt to update the deleted record - this.args.data.theme.unloadRecord(); - this.args.close(); - return true; - } catch (error) { - // TODO: show error in modal rather than generic message - this.notifications.showAPIError(error); - } - } -} diff --git a/ghost/admin/app/components/modals/design/install-theme.hbs b/ghost/admin/app/components/modals/design/install-theme.hbs deleted file mode 100644 index 30cfe550cab..00000000000 --- a/ghost/admin/app/components/modals/design/install-theme.hbs +++ /dev/null @@ -1,107 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/modals/design/install-theme.js b/ghost/admin/app/components/modals/design/install-theme.js deleted file mode 100644 index dc23b174e8a..00000000000 --- a/ghost/admin/app/components/modals/design/install-theme.js +++ /dev/null @@ -1,141 +0,0 @@ -import Component from '@glimmer/component'; -import {isThemeValidationError} from 'ghost-admin/services/ajax'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class InstallThemeModal extends Component { - @service ajax; - @service ghostPaths; - @service store; - @service themeManagement; - - @tracked installedTheme = null; - @tracked installError = ''; - @tracked validationWarnings = []; - @tracked validationErrors = []; - @tracked fatalValidationErrors = []; - - themes = this.store.peekAll('theme'); - - constructor() { - super(...arguments); - this.refreshThemesTask.perform(); - } - - get themeName() { - return this.args.data.theme?.name || this.args.data.ref.split('/')[1]; - } - - get themeRef() { - return this.args.data.theme?.ref || this.args.data.ref; - } - - get isDefaultOrLegacyTheme() { - return this.themeName.toLowerCase() === 'casper' || this.themeName.toLowerCase() === 'source'; - } - - get isConfirming() { - return !this.installSuccess && !this.installError && !this.installFailure; - } - - get installSuccess() { - return !!this.installedTheme; - } - - get installFailure() { - return !this.installSuccess && (this.validationErrors.length || this.fatalValidationErrors.length); - } - - get willOverwriteExisting() { - return !this.isDefaultOrLegacyTheme && this.themes.findBy('name', this.themeName.toLowerCase()); - } - - get hasWarningsOrErrors() { - return this.validationWarnings.length > 0 || this.validationErrors.length > 0; - } - - get shouldShowInstall() { - return !this.installSuccess && !this.installFailure; - } - - @task - *refreshThemesTask() { - yield this.store.findAll('theme', {reload: true}); - } - - @task - *installThemeTask() { - try { - if (this.isDefaultOrLegacyTheme) { - // default theme can't be installed, only activated - const themeName = this.themeName.toLowerCase(); - const defaultTheme = this.store.peekRecord('theme', themeName); - yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true}); - this.installedTheme = defaultTheme; - - // let modal opener do any other background stuff - this.args.data.onSuccess?.(); - - return true; - } - - const url = this.ghostPaths.url.api('themes/install') + `?source=github&ref=${this.themeRef}`; - const result = yield this.ajax.post(url); - - this.installError = ''; - - if (result.themes) { - // show theme in list immediately - this.store.pushPayload(result); - - this.installedTheme = this.store.peekRecord('theme', result.themes[0].name); - - this.validationWarnings = this.installedTheme.warnings || []; - this.validationErrors = this.installedTheme.gscanErrors || []; - this.fatalValidationErrors = []; - - // activate but prevent additional error modal from showing - yield this.themeManagement.activateTask.perform(this.installedTheme, {skipErrors: true}); - - // let modal opener do any other background stuff - this.args.data.onSuccess?.(); - - return true; - } - } catch (error) { - if (isThemeValidationError(error)) { - this.resetErrors(); - - let errors = error.payload.errors[0].details.errors; - let fatalErrors = []; - let normalErrors = []; - - // to have a proper grouping of fatal errors and none fatal, we need to check - // our errors for the fatal property - if (errors && errors.length > 0) { - for (let i = 0; i < errors.length; i += 1) { - if (errors[i].fatal) { - fatalErrors.push(errors[i]); - } else { - normalErrors.push(errors[i]); - } - } - } - - this.fatalValidationErrors = fatalErrors; - this.validationErrors = normalErrors; - this.validationWarnings = error.payload.errors[0].details.warnings || []; - return false; - } - - if (error.payload?.errors) { - this.installError = error.payload.errors[0].message; - return false; - } - - this.installError = error.message; - throw error; - } - } -} diff --git a/ghost/admin/app/components/modals/design/upload-theme.hbs b/ghost/admin/app/components/modals/design/upload-theme.hbs deleted file mode 100644 index ade36203c26..00000000000 --- a/ghost/admin/app/components/modals/design/upload-theme.hbs +++ /dev/null @@ -1,153 +0,0 @@ - diff --git a/ghost/admin/app/components/modals/design/upload-theme.js b/ghost/admin/app/components/modals/design/upload-theme.js deleted file mode 100644 index 5a2ec2df325..00000000000 --- a/ghost/admin/app/components/modals/design/upload-theme.js +++ /dev/null @@ -1,174 +0,0 @@ -import Component from '@glimmer/component'; -import { - UnsupportedMediaTypeError, - isThemeValidationError -} from 'ghost-admin/services/ajax'; -import {action} from '@ember/object'; -import {run} from '@ember/runloop'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class UploadThemeModal extends Component { - @service eventBus; - @service ghostPaths; - @service store; - @service themeManagement; - - static modalOptions = { - beforeClose: () => { - if (this.themeManagement.isUploading) { - return false; - } - } - }; - - @tracked displayOverwriteWarning = false; - @tracked file; - @tracked theme; - @tracked validationErrors; - @tracked validationWarnings; - @tracked fatalValidationErrors; - - accept = ['application/zip', 'application/x-zip-compressed']; - extensions = ['zip']; - - get themes() { - return this.store.peekAll('theme'); - } - - get currentThemeNames() { - return this.themes.map(theme => theme.name); - } - - get themeName() { - let themePackage = this.theme.package; - let name = this.theme.name; - - return themePackage ? `${themePackage.name} - ${themePackage.version}` : name; - } - - get fileThemeName() { - return this.file?.name.replace(/\.zip$/, ''); - } - - get canActivateTheme() { - return this.theme && !this.theme.active; - } - - get uploadUrl() { - return `${this.ghostPaths.apiRoot}/themes/upload/`; - } - - get hasWarningsOrErrors() { - return this.validationWarnings?.length || this.validationErrors?.length; - } - - get closeDisabled() { - return this.themeManagement.isUploading; - } - - constructor() { - super(...arguments); - this.refreshThemesTask.perform(); - } - - @task - *refreshThemesTask() { - yield this.store.findAll('theme'); - } - - @action - validateTheme(file) { - const themeName = file.name.replace(/\.zip$/, '').replace(/[^\w@.]/gi, '-').toLowerCase(); - - this.file = file; - - const [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); - const extensions = this.extensions; - - if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) { - return new UnsupportedMediaTypeError(); - } - - if (file.name.match(/^casper\.zip$/i) || file.name.match(/^source\.zip$/i)) { - return {payload: {errors: [{message: 'Sorry, the default theme cannot be overwritten.
Please rename your zip file to continue.'}]}}; - } - - if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) { - this.displayOverwriteWarning = true; - return false; - } - - return true; - } - - @action - confirmOverwrite() { - this._allowOverwrite = true; - this.displayOverwriteWarning = false; - - // we need to schedule afterRender so that the upload component is - // displayed again in order to subscribe/respond to the event bus - run.schedule('afterRender', this, function () { - this.eventBus.publish('themeUploader:upload', this.file); - }); - } - - @action - uploadSuccess(response) { - this.store.pushPayload(response); - - const theme = this.store.peekRecord('theme', response.themes[0].name); - - this.theme = theme; - - if (theme.warnings?.length > 0) { - this.validationWarnings = theme.warnings; - } - - // Ghost differentiates between errors and fatal errors - // You can't activate a theme with fatal errors, but with errors. - if (theme.gscanErrors?.length > 0) { - this.validationErrors = theme.gscanErrors; - } - } - - @action - uploadFailed(errorResponse) { - if (isThemeValidationError(errorResponse)) { - const errors = errorResponse.payload.errors[0].details.errors; - const fatalErrors = []; - const normalErrors = []; - - // to have a proper grouping of fatal errors and none fatal, we need to check - // our errors for the fatal property - errors.forEach?.((error) => { - if (error.fatal) { - fatalErrors.push(error); - } else { - normalErrors.push(error); - } - }); - - this.fatalValidationErrors = fatalErrors; - this.validationErrors = normalErrors; - this.validationWarnings = errorResponse.payload.errors[0].details.warnings || []; - } - } - - @action - activate() { - this.themeManagement.activateTask.perform(this.theme); - this.args.data.onActivationSuccess?.(); - this.args.close(); - } - - @action - reset() { - this.theme = null; - this.validationWarnings = []; - this.validationErrors = []; - this.fatalValidationErrors = []; - } -} diff --git a/ghost/admin/app/components/modals/design/view-theme.hbs b/ghost/admin/app/components/modals/design/view-theme.hbs deleted file mode 100644 index 910b2ef1a3a..00000000000 --- a/ghost/admin/app/components/modals/design/view-theme.hbs +++ /dev/null @@ -1,24 +0,0 @@ -
- -

- Themes - {{svg-jar "arrow-right"}} - {{@data.theme.name}} -

- -
-
- - -
- - -
-
- -
- - - - - diff --git a/ghost/admin/app/components/settings/signup-form/preview.js b/ghost/admin/app/components/settings/signup-form/preview.js deleted file mode 100644 index 8fd7d0caf61..00000000000 --- a/ghost/admin/app/components/settings/signup-form/preview.js +++ /dev/null @@ -1,134 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {throttle} from '@ember/runloop'; -import {tracked} from '@glimmer/tracking'; - -export default class Preview extends Component { - // We have two iframes - // When the HTML changes, the invisible iframe will get changed, once that one is fully loaded, it will become visible and the other iframe will be hidden - - @tracked - iframes = [ - {element: null, html: '', loading: false, style: ''}, - {element: null, html: '', loading: false, style: ''} - ]; - - @tracked - visibleIframeIndex = 0; - - lastChange = new Date(); - - get visibleHtml() { - return this.iframes[this.visibleIframeIndex].html; - } - - get firstIframeStyle() { - return this.iframes[0].style; - } - - get secondIframeStyle() { - return this.iframes[1].style; - } - - @action - onLoad(index, event) { - const iframe = this.iframes[index]; - if (!iframe) { - return; - } - - if (iframe.timer) { - clearTimeout(iframe.timer); - iframe.timer = null; - } - - if (!iframe.element) { - iframe.element = event.currentTarget; - - if (index === this.visibleIframeIndex) { - setTimeout(() => { - this.updateContent(index); - }); - } - } else { - if (!iframe.loading) { - return; - } - // We need to wait until the iframe is fully rendered. The onLoad is kinda okay in Chrome, but on Safari it is fired too early - // So we need to poll if needed - // Check if this iframe element has an iframe inside of the body - // If so, we need to wait for that iframe to load as well - const iframeElement = iframe.element.contentWindow.document.querySelector('iframe'); - - // Check that iframe contains a non empty body - const hasChildren = iframeElement && iframeElement.contentWindow.document.body && iframeElement.contentWindow.document.body.children.length > 0; - - if (hasChildren) { - // Finished loading this iframe - this.visibleIframeIndex = index; - - // Force tracked update - this.iframes = [...this.iframes]; - iframe.loading = false; - } else { - // Wait 50ms - iframe.timer = setTimeout(() => { - this.onLoad(index, event); - }, 50); - } - } - } - - @action onChangeHtml() { - // Check if no loading iframes - if (!this.iframes[0].loading && !this.iframes[1].loading && this.lastChange < new Date() - 500) { - // We make it feel more responsive by updating the frame immediately, but only if the last change was more than 500ms ago - // otherwise we get a lot of flickering - - this.lastChange = new Date(); - this.throttledUpdate(); - return; - } - - // Only update the iframe after 400ms, with 400ms in between - this.lastChange = new Date(); - throttle(this, this.throttledUpdate, 400, false); - } - - throttledUpdate() { - const currentVisibleHtml = this.iframes[this.visibleIframeIndex].html; - if (currentVisibleHtml === this.args.html) { - return; - } - - // Update the invisible iframe content - const index = this.visibleIframeIndex === 0 ? 1 : 0; - this.updateContent(index); - } - - @action - updateContent(index) { - const iframe = this.iframes[index]; - if (!iframe || !iframe.element) { - return; - } - - if (iframe.timer) { - clearTimeout(iframe.timer); - iframe.timer = null; - } - - iframe.loading = true; - const html = `${this.args.html}`; - iframe.html = this.args.html; - iframe.style = this.args.style; - - // Set the iframe's new HTML - iframe.element.contentWindow.document.open(); - iframe.element.contentWindow.document.write(html); - iframe.element.contentWindow.document.close(); - - // Force tracked update - this.iframes = [...this.iframes]; - } -} diff --git a/ghost/admin/app/components/settings/signup-form/style-select.hbs b/ghost/admin/app/components/settings/signup-form/style-select.hbs deleted file mode 100644 index 69c57b53878..00000000000 --- a/ghost/admin/app/components/settings/signup-form/style-select.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{#each this.options as |option|}} - - {{/each}} -
diff --git a/ghost/admin/app/components/settings/signup-form/style-select.js b/ghost/admin/app/components/settings/signup-form/style-select.js deleted file mode 100644 index 052973743b7..00000000000 --- a/ghost/admin/app/components/settings/signup-form/style-select.js +++ /dev/null @@ -1,28 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; - -export default class StyleSelect extends Component { - get options() { - return [{ - name: 'Branded', - value: 'all-in-one' - }, { - name: 'Minimal', - value: 'minimal' - }]; - } - - get selectedOption() { - return this.options.find(o => o.value === this.args.value); - } - - @action - setRecipients(option) { - this.args.onChange(option.value); - } - - @action - changeOption(option) { - this.option = option; - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/delete-user.hbs b/ghost/admin/app/components/settings/staff/modals/delete-user.hbs deleted file mode 100644 index 758650c24ae..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/delete-user.hbs +++ /dev/null @@ -1,34 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/delete-user.js b/ghost/admin/app/components/settings/staff/modals/delete-user.js deleted file mode 100644 index 83eadf104d1..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/delete-user.js +++ /dev/null @@ -1,31 +0,0 @@ -import Component from '@glimmer/component'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class DeleteUserModal extends Component { - @service notifications; - @service router; - @service store; - - get ownerUser() { - return this.store.peekAll('user').findBy('isOwnerOnly', true); - } - - @task({drop: true}) - *deleteUserTask() { - try { - const {user} = this.args.data; - - yield user.destroyRecord(); - - this.notifications.closeAlerts('user.delete'); - this.store.unloadAll('post'); - this.router.transitionTo('settings.staff'); - } catch (error) { - this.notifications.showAlert('The user could not be deleted. Please try again.', {type: 'error', key: 'user.delete.failed'}); - throw error; - } finally { - this.args.close(); - } - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.hbs b/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.hbs deleted file mode 100644 index 4f811b38521..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.hbs +++ /dev/null @@ -1,22 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.js b/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.js deleted file mode 100644 index 82700ff2be3..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/regenerate-staff-token.js +++ /dev/null @@ -1,23 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class RegenerateStaffTokenModal extends Component { - @service ajax; - @service ghostPaths; - @service notifications; - - @action - async regenerateStaffToken() { - const url = this.ghostPaths.url.api('users', 'me', 'token'); - - try { - const {apiKey} = await this.ajax.put(url, {data: {}}); - - this.args.close(`${apiKey.id}:${apiKey.secret}`); - } catch (error) { - this.notifications.showAPIError(error, {key: 'token.regenerate'}); - this.args.close(); - } - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/select-role.hbs b/ghost/admin/app/components/settings/staff/modals/select-role.hbs deleted file mode 100644 index faca0d3f23a..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/select-role.hbs +++ /dev/null @@ -1,18 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/select-role.js b/ghost/admin/app/components/settings/staff/modals/select-role.js deleted file mode 100644 index c38f44b3f2d..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/select-role.js +++ /dev/null @@ -1,11 +0,0 @@ -import Component from '@glimmer/component'; -import {tracked} from '@glimmer/tracking'; - -export default class SelectRoleModal extends Component { - @tracked role; - - constructor() { - super(...arguments); - this.role = this.args.data.currentRole; - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/suspend-user.hbs b/ghost/admin/app/components/settings/staff/modals/suspend-user.hbs deleted file mode 100644 index 2208023bbf3..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/suspend-user.hbs +++ /dev/null @@ -1,21 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/suspend-user.js b/ghost/admin/app/components/settings/staff/modals/suspend-user.js deleted file mode 100644 index 65ea2c6ce52..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/suspend-user.js +++ /dev/null @@ -1,24 +0,0 @@ -import Component from '@glimmer/component'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class SuspendUserModal extends Component { - @service notifications; - - @task({drop: true}) - *suspendUserTask() { - try { - const {user, saveTask} = this.args.data; - - user.status = 'inactive'; - yield saveTask.perform(); - - this.notifications.closeAlerts('user.suspend'); - } catch (error) { - this.notifications.showAlert('The user could not be suspended. Please try again.', {type: 'error', key: 'user.suspend.failed'}); - throw error; - } finally { - this.args.close(); - } - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/transfer-ownership.hbs b/ghost/admin/app/components/settings/staff/modals/transfer-ownership.hbs deleted file mode 100644 index 32bb26259db..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/transfer-ownership.hbs +++ /dev/null @@ -1,22 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/transfer-ownership.js b/ghost/admin/app/components/settings/staff/modals/transfer-ownership.js deleted file mode 100644 index 6caf44ea718..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/transfer-ownership.js +++ /dev/null @@ -1,48 +0,0 @@ -import Component from '@glimmer/component'; -import {isArray} from '@ember/array'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class TransferOwnershipModal extends Component { - @service ajax; - @service dropdown; - @service ghostPaths; - @service notifications; - @service store; - - @task({drop: true}) - *transferOwnershipTask() { - try { - this.dropdown.closeDropdowns(); - - const {user} = this.args.data; - const url = this.ghostPaths.url.api('users', 'owner'); - - const response = yield this.ajax.put(url, { - data: { - owner: [{ - id: user.id - }] - } - }); - - // manually update the roles for the users that just changed roles - // because store.pushPayload is not working with embedded relations - if (isArray(response?.users)) { - response.users.forEach((userJSON) => { - const updatedUser = this.store.peekRecord('user', userJSON.id); - const role = this.store.peekRecord('role', userJSON.roles[0].id); - - updatedUser.role = role; - }); - } - - this.notifications.showAlert(`Ownership successfully transferred to ${user.get('name')}`, {type: 'success', key: 'owner.transfer.success'}); - } catch (error) { - this.notifications.showAPIError(error, {key: 'owner.transfer'}); - throw error; - } finally { - this.args.close(); - } - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/unsuspend-user.hbs b/ghost/admin/app/components/settings/staff/modals/unsuspend-user.hbs deleted file mode 100644 index 0cedd25509e..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/unsuspend-user.hbs +++ /dev/null @@ -1,46 +0,0 @@ -{{#if this.hostLimitError}} - -{{else}} - -{{/if}} \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/unsuspend-user.js b/ghost/admin/app/components/settings/staff/modals/unsuspend-user.js deleted file mode 100644 index b654e4ce2a5..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/unsuspend-user.js +++ /dev/null @@ -1,57 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class UnsuspendUserModal extends Component { - @service limit; - @service notifications; - @service router; - - @tracked hostLimitError = null; - - constructor() { - super(...arguments); - this.checkHostLimitsTask.perform(); - } - - @action - upgrade() { - this.router.transitionTo('pro'); - this.args.close(); - } - - @task - *checkHostLimitsTask() { - if (this.args.data.user.role.name !== 'Contributor' && this.limit.limiter.isLimited('staff')) { - try { - yield this.limit.limiter.errorIfWouldGoOverLimit('staff'); - } catch (error) { - if (error.errorType === 'HostLimitError') { - this.hostLimitError = error.message; - } else { - this.notifications.showAPIError(error, {key: 'staff.limit'}); - this.args.close(); - } - } - } - } - - @task({drop: true}) - *unsuspendUserTask() { - try { - const {user, saveTask} = this.args.data; - - user.status = 'active'; - yield saveTask.perform(); - - this.notifications.closeAlerts('user.unsuspend'); - } catch (error) { - this.notifications.showAlert('The user could not be unsuspended. Please try again.', {type: 'error', key: 'user.unsuspend.failed'}); - throw error; - } finally { - this.args.close(); - } - } -} diff --git a/ghost/admin/app/components/settings/staff/modals/upload-image.hbs b/ghost/admin/app/components/settings/staff/modals/upload-image.hbs deleted file mode 100644 index 3f01713a0a8..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/upload-image.hbs +++ /dev/null @@ -1,42 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/settings/staff/modals/upload-image.js b/ghost/admin/app/components/settings/staff/modals/upload-image.js deleted file mode 100644 index 345064967df..00000000000 --- a/ghost/admin/app/components/settings/staff/modals/upload-image.js +++ /dev/null @@ -1,52 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class UploadImageModal extends Component { - @service notifications; - - @tracked errorMessage; - @tracked isUploading = false; - @tracked url = ''; - - constructor() { - super(...arguments); - this.url = this._getModelProperty(); - } - - @action - fileUploaded(url) { - this.url = url; - } - - @action - removeImage() { - this.url = ''; - } - - @task({drop: true}) - *uploadImageTask() { - this._setModelProperty(this.url); - - try { - yield this.args.data.model.save(); - } catch (e) { - this.notifications.showAPIError(e, {key: 'image.upload'}); - } finally { - this.args.close(); - } - } - - _getModelProperty() { - const {model, modelProperty} = this.args.data; - return model[modelProperty]; - } - - _setModelProperty(url) { - const {model, modelProperty} = this.args.data; - model[modelProperty] = url; - return url; - } -} diff --git a/ghost/admin/app/components/settings/tips-and-donations.hbs b/ghost/admin/app/components/settings/tips-and-donations.hbs deleted file mode 100644 index 192cce81bc2..00000000000 --- a/ghost/admin/app/components/settings/tips-and-donations.hbs +++ /dev/null @@ -1,112 +0,0 @@ -
-
-
-

Tips or donations

- {{#if (feature "tipsAndDonations")}} -

Give your audience a one-time way to support your work, no membership required. - {{#if this.membersUtils.isStripeEnabled}} - {{else}} - - {{/if}} -

- {{else}} -

Give your audience a one-time way to support your work, no membership required

- {{/if}} -
- {{#if (feature "tipsAndDonations")}} - {{#if this.membersUtils.isStripeEnabled}} - - {{else}} - - {{/if}} - {{else}} - {{#if this.membersUtils.isStripeEnabled}} - - {{else}} - - {{/if}} - {{/if}} -
-
- {{#liquid-if (and this.tipsAndDonationsOpen this.membersUtils.isStripeEnabled) }} -
- -
-
- -
- -
- -
-
-
- - - - {{svg-jar "arrow-down-small"}} - - -
-
-
- - - - - - -
- {{#if this.tipsAndDonationsError}} -

{{this.tipsAndDonationsError}}

- {{/if}} -
-
- {{/liquid-if}} -
-
diff --git a/ghost/admin/app/components/settings/tips-and-donations.js b/ghost/admin/app/components/settings/tips-and-donations.js deleted file mode 100644 index 30c35648c88..00000000000 --- a/ghost/admin/app/components/settings/tips-and-donations.js +++ /dev/null @@ -1,119 +0,0 @@ -import Component from '@glimmer/component'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; -import envConfig from 'ghost-admin/config/environment'; -import {action} from '@ember/object'; -import {currencies, getSymbol, minimumAmountForCurrency} from 'ghost-admin/utils/currency'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task, timeout} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -const CURRENCIES = currencies.map((currency) => { - return { - value: currency.isoCode, - label: `${currency.isoCode}` - }; -}); - -// Stripe doesn't allow amounts over 10,000 as a preset amount -const MAX_AMOUNT = 10_000; - -export default class TipsAndDonations extends Component { - @service settings; - @service session; - @service membersUtils; - @service modals; - - @inject config; - @tracked tipsAndDonationsError = ''; - - get allCurrencies() { - return CURRENCIES; - } - - get selectedAmount() { - return this.settings.donationsSuggestedAmount && this.settings.donationsSuggestedAmount / 100; - } - - get selectedCurrency() { - return CURRENCIES.findBy('value', this.settings.donationsCurrency); - } - - get siteUrl() { - return this.config.blogUrl; - } - - get isConnectDisallowed() { - const siteUrl = this.config.blogUrl; - return envConfig.environment !== 'development' && !/^https:/.test(siteUrl); - } - - @action - setDonationsCurrency(event) { - this.settings.donationsCurrency = event.value; - } - - @action - setDonationsSuggestedAmount(event) { - const amount = Math.abs(event.target.value); - const amountInCents = Math.round(amount * 100); - const currency = this.settings.donationsCurrency; - const symbol = getSymbol(currency); - const minAmount = minimumAmountForCurrency(currency); - - if (amountInCents !== 0 && amountInCents < (minAmount * 100)) { - this.tipsAndDonationsError = `Non-zero amount must be at least ${symbol}${minAmount}.`; - return; - } - - if (amountInCents !== 0 && amountInCents > (MAX_AMOUNT * 100)) { - this.tipsAndDonationsError = `Suggested amount cannot be more than ${symbol}${MAX_AMOUNT}.`; - return; - } - - this.tipsAndDonationsError = ''; - this.settings.donationsSuggestedAmount = amountInCents; - } - - @action - openStripeConnect() { - this.args.openStripeConnect(); - } - - @action - async closeStripeConnect() { - this.showStripeConnect = false; - } - - @action - async showPreview(event) { - event.preventDefault(); - - const preview = () => window.open(`${this.siteUrl}/#/portal/support`, '_blank'); - const changedAttributes = this.settings.changedAttributes(); - - if (changedAttributes && Object.keys(changedAttributes).length > 0) { - const shouldClose = await this.modals.open(ConfirmUnsavedChangesModal); - - if (shouldClose) { - this.settings.rollbackAttributes(); - preview(); - } - } else { - preview(); - } - } - - @task - *copyTipsAndDonationsLink() { - const link = document.getElementById('gh-tips-and-donations-link')?.value; - - if (link) { - copyTextToClipboard(link); - yield timeout(this.isTesting ? 50 : 3000); - } - - return true; - } -} diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 7d89311fd12..f8f4188e9cb 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -131,6 +131,7 @@ export default class LexicalEditorController extends Controller { /* private properties ----------------------------------------------------*/ _leaveConfirmed = false; + _saveOnLeavePerformed = false; _previousTagNames = null; // set by setPost and _postSaved, used in hasDirtyAttributes /* computed properties ---------------------------------------------------*/ @@ -346,7 +347,10 @@ export default class LexicalEditorController extends Controller { @action setFeatureImageCaption(html) { this.post.set('featureImageCaption', html); + } + @action + handleFeatureImageCaptionBlur() { if (this.post.isDraft) { this.autosaveTask.perform(); } @@ -431,9 +435,12 @@ export default class LexicalEditorController extends Controller { // _xSave tasks that will also cancel the autosave task @task({group: 'saveTasks'}) *saveTask(options = {}) { + if (this.post.isDestroyed || this.post.isDestroying) { + return; + } + let prevStatus = this.get('post.status'); let isNew = this.get('post.isNew'); - let status; const adapterOptions = {}; this.cancelAutosave(); @@ -442,28 +449,39 @@ export default class LexicalEditorController extends Controller { return; } - if (options.backgroundSave) { - // do not allow a post's status to be set to published by a background save - status = 'draft'; + // leaving the editor should never result in a status change, we only save on editor + // leave to trigger a revision save + if (options.leavingEditor) { + // ensure we always have a status present otherwise we'll error when saving + if (!this.post.status) { + this.post.status = 'draft'; + } } else { - if (this.get('post.pastScheduledTime')) { - status = (!this.willSchedule && !this.willPublish) ? 'draft' : 'published'; + let status; + + if (options.backgroundSave) { + // do not allow a post's status to be set to published by a background save + status = 'draft'; } else { - if (this.willPublish && !this.get('post.isScheduled')) { - status = 'published'; - } else if (this.willSchedule && !this.get('post.isPublished')) { - status = 'scheduled'; - } else if (this.get('post.isSent')) { - status = 'sent'; + if (this.get('post.pastScheduledTime')) { + status = (!this.willSchedule && !this.willPublish) ? 'draft' : 'published'; } else { - status = 'draft'; + if (this.willPublish && !this.get('post.isScheduled')) { + status = 'published'; + } else if (this.willSchedule && !this.get('post.isPublished')) { + status = 'scheduled'; + } else if (this.get('post.isSent')) { + status = 'sent'; + } else { + status = 'draft'; + } } } - } - // set manually here instead of in beforeSaveTask because the - // new publishing flow sets the post status manually on publish - this.set('post.status', status); + // set manually here instead of in beforeSaveTask because the + // new publishing flow sets the post status manually on publish + this.set('post.status', status); + } const explicitSave = !options.backgroundSave; const leavingEditor = options.leavingEditor; @@ -475,8 +493,6 @@ export default class LexicalEditorController extends Controller { try { let post = yield this._savePostTask.perform({...options, adapterOptions}); - post.set('statusScratch', null); - // Clear any error notification (if any) this.notifications.clearAll(); @@ -498,8 +514,7 @@ export default class LexicalEditorController extends Controller { yield this.modals.open(ReAuthenticateModal); if (this.session.isAuthenticated) { - this.saveTask.perform(options); - return; + return this.saveTask.perform(options); } } @@ -680,7 +695,6 @@ export default class LexicalEditorController extends Controller { scope.setTag('post_type', post.isPage ? 'page' : 'post'); scope.setTag('save_revision', options.adapterOptions?.saveRevision); scope.setTag('email_segment', options.adapterOptions?.emailSegment); - scope.setTag('save_revision', options.adapterOptions?.saveRevision); scope.setTag('convert_to_lexical', options.adapterOptions?.convertToLexical); }); } @@ -693,7 +707,6 @@ export default class LexicalEditorController extends Controller { scope.setTag('post_type', post.isPage ? 'page' : 'post'); scope.setTag('save_revision', options.adapterOptions?.saveRevision); scope.setTag('email_segment', options.adapterOptions?.emailSegment); - scope.setTag('save_revision', options.adapterOptions?.saveRevision); scope.setTag('convert_to_lexical', options.adapterOptions?.convertToLexical); }); } @@ -938,21 +951,6 @@ export default class LexicalEditorController extends Controller { return; } - // wait for any save to finish before continuing to avoid any issues - // with attempting a new save whilst another has requests in-flight - if (this.saveTask.isRunning) { - transition.abort(); - await this.saveTask.last; - return transition.retry(); - } - // extra handling for PSM-triggered save tasks that aren't captured above - // NOTE: we don't wait on `_savePostTask` as it's only used as a child task - if (this.savePostTask.isRunning) { - transition.abort(); - await this.savePostTask.last; - return transition.retry(); - } - // user can enter the slug name and then leave the post page, // in such case we should wait until the slug would be saved on backend if (this.updateSlugTask.isRunning) { @@ -961,6 +959,10 @@ export default class LexicalEditorController extends Controller { return transition.retry(); } + const fromNewToEdit = this.router.currentRouteName === 'lexical-editor.new' + && transition.targetName === 'lexical-editor.edit' + && transition.intent.contexts?.[0]?.id === post.id; + // clean up blank cards when leaving the editor if we have a draft post // - blank cards could be left around due to autosave triggering whilst // a blank card is present then the user attempting to leave @@ -970,31 +972,30 @@ export default class LexicalEditorController extends Controller { // this._koenig.cleanup(); // } + // if we need to save when leaving the editor, abort the transition, save, + // then retry. If a previous transition already performed a save, skip to + // avoid potential infinite transition+save loops + let hasDirtyAttributes = this.hasDirtyAttributes; let state = post.getProperties('isDeleted', 'isSaving', 'hasDirtyAttributes', 'isNew'); // Check if anything has changed since the last revision let postRevisions = post.get('postRevisions').toArray(); let latestRevision = postRevisions[postRevisions.length - 1]; - let hasChangedSinceLastRevision = !post.isNew && post.lexical.replaceAll(this.config.blogUrl, '') !== latestRevision.lexical.replaceAll(this.config.blogUrl, ''); - - let fromNewToEdit = this.router.currentRouteName === 'lexical-editor.new' - && transition.targetName === 'lexical-editor.edit' - && transition.intent.contexts - && transition.intent.contexts[0] - && transition.intent.contexts[0].id === post.id; + let hasChangedSinceLastRevision = !latestRevision || (!post.isNew && post.lexical.replaceAll(this.config.blogUrl, '') !== latestRevision.lexical.replaceAll(this.config.blogUrl, '')); let deletedWithoutChanges = state.isDeleted - && (state.isSaving || !state.hasDirtyAttributes); + && (state.isSaving || !state.hasDirtyAttributes); // If leaving the editor and the post has changed since we last saved a revision, always save a new revision - if (hasChangedSinceLastRevision) { + if (!this._saveOnLeavePerformed && hasChangedSinceLastRevision && hasDirtyAttributes) { transition.abort(); if (this._autosaveRunning) { this.cancelAutosave(); this.autosaveTask.cancelAll(); } await this.autosaveTask.perform({leavingEditor: true, backgroundSave: false}); + this._saveOnLeavePerformed = true; return transition.retry(); } @@ -1015,7 +1016,10 @@ export default class LexicalEditorController extends Controller { this.autosaveTask.cancelAll(); // If leaving the editor, always save a revision - await this.autosaveTask.perform({leavingEditor: true}); + if (!this._saveOnLeavePerformed) { + await this.autosaveTask.perform({leavingEditor: true}); + this._saveOnLeavePerformed = true; + } return transition.retry(); } @@ -1065,6 +1069,7 @@ export default class LexicalEditorController extends Controller { this._previousTagNames = []; this._leaveConfirmed = false; + this._saveOnLeavePerformed = false; this.set('post', null); this.set('hasDirtyAttributes', false); diff --git a/ghost/admin/app/controllers/settings.js b/ghost/admin/app/controllers/settings.js deleted file mode 100644 index dd9f9e990f0..00000000000 --- a/ghost/admin/app/controllers/settings.js +++ /dev/null @@ -1,14 +0,0 @@ -import AboutModal from '../components/modals/settings/about'; -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class SettingsController extends Controller { - @service modals; - @service upgradeStatus; - - @action - openAbout() { - this.advancedModal = this.modals.open(AboutModal); - } -} diff --git a/ghost/admin/app/controllers/settings/analytics.js b/ghost/admin/app/controllers/settings/analytics.js deleted file mode 100644 index 2f51689d40e..00000000000 --- a/ghost/admin/app/controllers/settings/analytics.js +++ /dev/null @@ -1,13 +0,0 @@ -import Controller from '@ember/controller'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class AnalyticsController extends Controller { - @service settings; - - @task({drop: true}) - *saveSettings() { - const response = yield this.settings.save(); - return response; - } -} diff --git a/ghost/admin/app/controllers/settings/announcement-bar/index.js b/ghost/admin/app/controllers/settings/announcement-bar/index.js deleted file mode 100644 index 2d775b16bc0..00000000000 --- a/ghost/admin/app/controllers/settings/announcement-bar/index.js +++ /dev/null @@ -1,62 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class SettingsAnnouncementBarIndexController extends Controller { - @service customThemeSettings; - @service notifications; - @service settings; - @service themeManagement; - - @inject config; - - @tracked previewSize = 'desktop'; - - get isDesktopPreview() { - return this.previewSize === 'desktop'; - } - - get isMobilePreview() { - return this.previewSize === 'mobile'; - } - - @action - setPreviewSize(size) { - this.previewSize = size; - } - - @action - saveFromKeyboard() { - document.activeElement.blur?.(); - return this.saveTask.perform(); - } - - @task - *saveTask() { - try { - if (this.settings.errors.length !== 0) { - return; - } - - yield Promise.all([ - this.settings.save() - ]); - - // ensure task button switches to success state - return true; - } catch (error) { - if (error) { - this.notifications.showAPIError(error); - throw error; - } - } - } - - reset() { - this.previewSize = 'desktop'; - this.themeManagement.setPreviewType('homepage'); - } -} diff --git a/ghost/admin/app/controllers/settings/code-injection.js b/ghost/admin/app/controllers/settings/code-injection.js deleted file mode 100644 index 623766a4634..00000000000 --- a/ghost/admin/app/controllers/settings/code-injection.js +++ /dev/null @@ -1,26 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class CodeInjectionController extends Controller { - @service notifications; - @service settings; - - @action - save() { - this.saveTask.perform(); - } - - @task - *saveTask() { - let notifications = this.notifications; - - try { - return yield this.settings.save(); - } catch (error) { - notifications.showAPIError(error, {key: 'code-injection.save'}); - throw error; - } - } -} diff --git a/ghost/admin/app/controllers/settings/design.js b/ghost/admin/app/controllers/settings/design.js deleted file mode 100644 index 15ec4c3b600..00000000000 --- a/ghost/admin/app/controllers/settings/design.js +++ /dev/null @@ -1,4 +0,0 @@ -import Controller from '@ember/controller'; - -export default class SettingsDesignController extends Controller { -} diff --git a/ghost/admin/app/controllers/settings/design/change-theme.js b/ghost/admin/app/controllers/settings/design/change-theme.js deleted file mode 100644 index c425c02bc2a..00000000000 --- a/ghost/admin/app/controllers/settings/design/change-theme.js +++ /dev/null @@ -1,207 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class ChangeThemeController extends Controller { - @service router; - @service store; - @service themeManagement; - - @tracked showAdvanced = false; - - themes = this.store.peekAll('theme'); - - officialThemes = [{ - name: 'Source', - category: 'News', - previewUrl: 'https://source.ghost.io/', - ref: 'default', - image: 'assets/img/themes/Source.png' - }, { - name: 'Casper', - category: 'Blog', - previewUrl: 'https://demo.ghost.io/', - ref: 'TryGhost/Casper', - image: 'assets/img/themes/Casper.png' - }, { - name: 'Edition', - category: 'Newsletter', - url: 'https://github.com/TryGhost/Edition', - previewUrl: 'https://edition.ghost.io/', - ref: 'TryGhost/Edition', - image: 'assets/img/themes/Edition.png' - }, { - name: 'Solo', - category: 'Blog', - url: 'https://github.com/TryGhost/Solo', - previewUrl: 'https://solo.ghost.io', - ref: 'TryGhost/Solo', - image: 'assets/img/themes/Solo.png' - }, { - name: 'Taste', - category: 'Blog', - url: 'https://github.com/TryGhost/Taste', - previewUrl: 'https://taste.ghost.io', - ref: 'TryGhost/Taste', - image: 'assets/img/themes/Taste.png' - }, { - name: 'Episode', - category: 'Podcast', - url: 'https://github.com/TryGhost/Episode', - previewUrl: 'https://episode.ghost.io', - ref: 'TryGhost/Episode', - image: 'assets/img/themes/Episode.png' - }, { - name: 'Digest', - category: 'Newsletter', - url: 'https://github.com/TryGhost/Digest', - previewUrl: 'https://digest.ghost.io/', - ref: 'TryGhost/Digest', - image: 'assets/img/themes/Digest.png' - }, { - name: 'Bulletin', - category: 'Newsletter', - url: 'https://github.com/TryGhost/Bulletin', - previewUrl: 'https://bulletin.ghost.io/', - ref: 'TryGhost/Bulletin', - image: 'assets/img/themes/Bulletin.png' - }, { - name: 'Alto', - category: 'Blog', - url: 'https://github.com/TryGhost/Alto', - previewUrl: 'https://alto.ghost.io', - ref: 'TryGhost/Alto', - image: 'assets/img/themes/Alto.png' - }, { - name: 'Dope', - category: 'Magazine', - url: 'https://github.com/TryGhost/Dope', - previewUrl: 'https://dope.ghost.io', - ref: 'TryGhost/Dope', - image: 'assets/img/themes/Dope.png' - }, { - name: 'Wave', - category: 'Podcast', - url: 'https://github.com/TryGhost/Wave', - previewUrl: 'https://wave.ghost.io', - ref: 'TryGhost/Wave', - image: 'assets/img/themes/Wave.png' - }, { - name: 'Edge', - category: 'Photography', - url: 'https://github.com/TryGhost/Edge', - previewUrl: 'https://edge.ghost.io', - ref: 'TryGhost/Edge', - image: 'assets/img/themes/Edge.png' - }, { - name: 'Dawn', - category: 'Newsletter', - url: 'https://github.com/TryGhost/Dawn', - previewUrl: 'https://dawn.ghost.io/', - ref: 'TryGhost/Dawn', - image: 'assets/img/themes/Dawn.png' - }, { - name: 'Ease', - category: 'Documentation', - url: 'https://github.com/TryGhost/Ease', - previewUrl: 'https://ease.ghost.io', - ref: 'TryGhost/Ease', - image: 'assets/img/themes/Ease.png' - }, { - name: 'Headline', - category: 'News', - url: 'https://github.com/TryGhost/Headline', - previewUrl: 'https://headline.ghost.io', - ref: 'TryGhost/Headline', - image: 'assets/img/themes/Headline.png' - }, { - name: 'Ruby', - category: 'Magazine', - url: 'https://github.com/TryGhost/Ruby', - previewUrl: 'https://ruby.ghost.io', - ref: 'TryGhost/Ruby', - image: 'assets/img/themes/Ruby.png' - }, { - name: 'London', - category: 'Photography', - url: 'https://github.com/TryGhost/London', - previewUrl: 'https://london.ghost.io', - ref: 'TryGhost/London', - image: 'assets/img/themes/London.png' - }, { - name: 'Journal', - category: 'Newsletter', - url: 'https://github.com/TryGhost/Journal', - previewUrl: 'https://journal.ghost.io/', - ref: 'TryGhost/Journal', - image: 'assets/img/themes/Journal.png' - }]; - - get themesList() { - const activeTheme = this.themes.findBy('active', true); - - // decorate themes based on current usage - let themesList = this.officialThemes.map((theme) => { - const decoratedTheme = Object.assign({}, theme); - - if (theme.ref === 'default') { - decoratedTheme.isDefault = true; - } - - if (typeof activeTheme !== 'undefined' - && theme.name.toLowerCase() === activeTheme.name) { - decoratedTheme.isActive = true; - } - - return decoratedTheme; - }); - - // move default themes to the beginning of the list - themesList.sort((a, b) => { - if (b.isDefault) { - return 1; - } - - return 0; - }); - - // ensure active theme is always first - const activeThemeInList = themesList.find(theme => theme.isActive); - const activeThemeIndex = themesList.indexOf(activeThemeInList); - if (activeThemeIndex > 0) { - themesList.splice(activeThemeIndex, 1); - themesList.unshift(activeThemeInList); - } - - return themesList; - } - - @action - startThemeUpload(event) { - event?.preventDefault(); - - this.themeManagement.upload({ - onActivationSuccess: () => { - this.router.transitionTo('settings.design'); - } - }); - } - - @action - toggleAdvanced(event) { - this.showAdvanced = !this.showAdvanced; - - if (this.showAdvanced) { - const mainContainer = event?.target.closest('.gh-main'); - - if (mainContainer) { - mainContainer.scrollTop = 0; - } - } - } - - reset() { - this.showAdvanced = false; - } -} diff --git a/ghost/admin/app/controllers/settings/design/change-theme/install.js b/ghost/admin/app/controllers/settings/design/change-theme/install.js deleted file mode 100644 index 33468d4d0f0..00000000000 --- a/ghost/admin/app/controllers/settings/design/change-theme/install.js +++ /dev/null @@ -1,9 +0,0 @@ -import Controller from '@ember/controller'; -import {tracked} from '@glimmer/tracking'; - -export default class InstallThemeController extends Controller { - queryParams = ['source', 'ref']; - - @tracked source = ''; - @tracked ref = ''; -} diff --git a/ghost/admin/app/controllers/settings/design/index.js b/ghost/admin/app/controllers/settings/design/index.js deleted file mode 100644 index ef67b0399e8..00000000000 --- a/ghost/admin/app/controllers/settings/design/index.js +++ /dev/null @@ -1,63 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class SettingsDesignIndexController extends Controller { - @service customThemeSettings; - @service notifications; - @service settings; - @service themeManagement; - - @inject config; - - @tracked previewSize = 'desktop'; - - get isDesktopPreview() { - return this.previewSize === 'desktop'; - } - - get isMobilePreview() { - return this.previewSize === 'mobile'; - } - - @action - setPreviewSize(size) { - this.previewSize = size; - } - - @action - saveFromKeyboard() { - document.activeElement.blur?.(); - return this.saveTask.perform(); - } - - @task - *saveTask() { - try { - if (this.settings.errors.length !== 0) { - return; - } - - yield Promise.all([ - this.settings.save(), - this.customThemeSettings.save() - ]); - - // ensure task button switches to success state - return true; - } catch (error) { - if (error) { - this.notifications.showAPIError(error); - throw error; - } - } - } - - reset() { - this.previewSize = 'desktop'; - this.themeManagement.setPreviewType('homepage'); - } -} diff --git a/ghost/admin/app/controllers/settings/general.js b/ghost/admin/app/controllers/settings/general.js deleted file mode 100644 index 00e602537c6..00000000000 --- a/ghost/admin/app/controllers/settings/general.js +++ /dev/null @@ -1,174 +0,0 @@ -import classic from 'ember-classic-decorator'; -import {action, computed} from '@ember/object'; -import {inject as service} from '@ember/service'; -/* eslint-disable ghost/ember/alias-model-in-controller */ -import Controller from '@ember/controller'; -import generatePassword from 'ghost-admin/utils/password-generator'; -import { - IMAGE_EXTENSIONS, - IMAGE_MIME_TYPES -} from 'ghost-admin/components/gh-image-uploader'; -import {TrackedObject} from 'tracked-built-ins'; -import {inject} from 'ghost-admin/decorators/inject'; -import {run} from '@ember/runloop'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -function randomPassword() { - let word = generatePassword(6); - let randomN = Math.floor(Math.random() * 1000); - - return word + randomN; -} - -@classic -export default class GeneralController extends Controller { - @service ghostPaths; - @service notifications; - @service session; - @service settings; - @service frontend; - @service ui; - - @inject config; - - @tracked scratchValues = new TrackedObject(); - - imageExtensions = IMAGE_EXTENSIONS; - imageMimeTypes = IMAGE_MIME_TYPES; - - get availableTimezones() { - return this.config.availableTimezones; - } - - @computed('config.blogUrl', 'settings.publicHash') - get privateRSSUrl() { - let blogUrl = this.config.blogUrl; - let publicHash = this.settings.publicHash; - - return `${blogUrl}/${publicHash}/rss`; - } - - @action - save() { - this.saveTask.perform(); - } - - @action - setTimezone(timezone) { - this.settings.timezone = timezone.name; - } - - @action - removeImage(image) { - // setting `null` here will error as the server treats it as "null" - this.settings[image] = ''; - } - - /** - * Opens a file selection dialog - Triggered by "Upload Image" buttons, - * searches for the hidden file input within the .gh-setting element - * containing the clicked button then simulates a click - * @param {MouseEvent} event - MouseEvent fired by the button click - */ - @action - triggerFileDialog(event) { - event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click(); - } - - /** - * Fired after an image upload completes - * @param {string} property - Property name to be set on `this.settings` - * @param {UploadResult[]} results - Array of UploadResult objects - * @return {string} The URL that was set on `this.settings.property` - */ - @action - imageUploaded(property, results) { - if (results[0]) { - return this.settings[property] = results[0].url; - } - } - - @action - toggleIsPrivate(isPrivate) { - let settings = this.settings; - - settings.isPrivate = isPrivate; - settings.errors.remove('password'); - - let changedAttrs = settings.changedAttributes(); - - // set a new random password when isPrivate is enabled - if (isPrivate && changedAttrs.isPrivate) { - settings.password = randomPassword(); - - // reset the password when isPrivate is disabled - } else if (changedAttrs.password) { - settings.password = changedAttrs.password[0]; - } - } - - @action - setScratchValue(property, value) { - this.scratchValues[property] = value; - } - - clearScratchValues() { - this.scratchValues = new TrackedObject(); - } - - _deleteTheme() { - let theme = this.store.peekRecord('theme', this.themeToDelete.name); - - if (!theme) { - return; - } - - return theme.destroyRecord().catch((error) => { - this.notifications.showAPIError(error); - }); - } - - @task - *saveTask() { - let notifications = this.notifications; - - try { - let changedAttrs = this.settings.changedAttributes(); - let settings = yield this.settings.save(); - - this.clearScratchValues(); - - this.config.blogTitle = settings.title; - - if (changedAttrs.password) { - this.frontend.loginIfNeeded(); - } - - // this forces the document title to recompute after a blog title change - this.ui.updateDocumentTitle(); - - return settings; - } catch (error) { - if (error) { - notifications.showAPIError(error, {key: 'settings.save'}); - } - throw error; - } - } - - @action - saveViaKeyboard(event) { - event.preventDefault(); - - // trigger any set-on-blur actions - const focusedElement = document.activeElement; - focusedElement?.blur(); - - // schedule save for when set-on-blur actions have finished - run.schedule('actions', this, function () { - focusedElement?.focus(); - this.saveTask.perform(); - }); - } -} diff --git a/ghost/admin/app/controllers/settings/history.js b/ghost/admin/app/controllers/settings/history.js deleted file mode 100644 index 867a679a57d..00000000000 --- a/ghost/admin/app/controllers/settings/history.js +++ /dev/null @@ -1,44 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class HistoryController extends Controller { - @service router; - @service settings; - @service store; - - queryParams = ['excludedEvents', 'excludedResources', 'user']; - - @tracked excludedEvents = null; - @tracked excludedResources = null; - @tracked user = null; - - get fullExcludedEvents() { - return (this.excludedEvents || '').split(','); - } - - get fullExcludedResources() { - return (this.excludedResources || '').split(','); - } - - get userRecord() { - if (!this.user) { - return null; - } - - // TODO: {reload: true} here shouldn't be needed but without it - // the template renders nothing if the record is already in the store - return this.store.findRecord('user', this.user, {reload: true}); - } - - @action - changeExcludedItems({excludedEvents, excludedResources} = {}) { - this.router.transitionTo({queryParams: {excludedEvents, excludedResources}}); - } - - @action - changeUser(user) { - this.router.transitionTo({queryParams: {user: user?.id}}); - } -} diff --git a/ghost/admin/app/controllers/settings/integration.js b/ghost/admin/app/controllers/settings/integration.js deleted file mode 100644 index 2a6f8f86fdb..00000000000 --- a/ghost/admin/app/controllers/settings/integration.js +++ /dev/null @@ -1,165 +0,0 @@ -import Controller from '@ember/controller'; -import DeleteIntegrationModal from '../../components/settings/integrations/delete-integration-modal'; -import DeleteWebhookModal from '../../components/settings/integrations/delete-webhook-modal'; -import RegenerateKeyModal from '../../components/settings/integrations/regenerate-key-modal'; -import config from 'ghost-admin/config/environment'; -import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; -import { - IMAGE_EXTENSIONS, - IMAGE_MIME_TYPES -} from 'ghost-admin/components/gh-image-uploader'; -import {action} from '@ember/object'; -import {htmlSafe} from '@ember/template'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task, timeout} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class IntegrationController extends Controller { - @service ghostPaths; - @service modals; - - @inject config; - - imageExtensions = IMAGE_EXTENSIONS; - imageMimeTypes = IMAGE_MIME_TYPES; - - @tracked regeneratedApiKey = null; - - constructor() { - super(...arguments); - if (this.isTesting === undefined) { - this.isTesting = config.environment === 'test'; - } - } - - get integration() { - return this.model; - } - - get apiUrl() { - let origin = window.location.origin; - let subdir = this.ghostPaths.subdir; - let url = this.ghostPaths.url.join(origin, subdir); - - return url.replace(/\/$/, ''); - } - - get allWebhooks() { - return this.store.peekAll('webhook'); - } - - get filteredWebhooks() { - return this.allWebhooks.filter((webhook) => { - let matchesIntegration = webhook.belongsTo('integration').id() === this.integration.id; - - return matchesIntegration - && !webhook.isNew - && !webhook.isDeleted; - }); - } - - get iconImageStyle() { - let url = this.integration.iconImage; - if (url) { - let styles = [ - `background-image: url(${url})`, - 'background-size: 50%', - 'background-position: 50%', - 'background-repeat: no-repeat' - ]; - return htmlSafe(styles.join('; ')); - } - - return htmlSafe(''); - } - - @action - triggerIconFileDialog(event) { - event.preventDefault(); - let input = document.querySelector('input[type="file"][name="iconImage"]'); - input.click(); - } - - @action - updateProperty(property, event) { - this.integration.set(property, event.target.value); - } - - @action - validateProperty(property) { - this.integration.validate({property}); - } - - @action - setIconImage([image]) { - this.integration.set('iconImage', image.url); - } - - @action - save() { - return this.saveTask.perform(); - } - - @action - confirmIntegrationDeletion(event) { - event?.preventDefault(); - return this.modals.open(DeleteIntegrationModal, { - integration: this.integration - }); - } - - @action - async confirmRegenerateKey(apiKey, event) { - event?.preventDefault(); - this.regeneratedApiKey = null; - this.regeneratedApiKey = await this.modals.open(RegenerateKeyModal, { - apiKey, - integration: this.integration - }); - } - - @action - confirmWebhookDeletion(webhook, event) { - event?.preventDefault(); - return this.modals.open(DeleteWebhookModal, {webhook}); - } - - @action - deleteWebhook(event) { - event?.preventDefault(); - return this.webhookToDelete.destroyRecord(); - } - - @task - *saveTask() { - try { - return yield this.integration.save(); - } catch (e) { - if (e === undefined) { - // validation error - return false; - } - - throw e; - } - } - - @task - *copyContentKey() { - copyTextToClipboard(this.integration.contentKey.secret); - yield timeout(this.isTesting ? 50 : 3000); - } - - @task - *copyAdminKey() { - copyTextToClipboard(this.integration.adminKey.secret); - yield timeout(this.isTesting ? 50 : 3000); - } - - @task - *copyApiUrl() { - copyTextToClipboard(this.apiUrl); - yield timeout(this.isTesting ? 50 : 3000); - } -} diff --git a/ghost/admin/app/controllers/settings/integration/webhooks/edit.js b/ghost/admin/app/controllers/settings/integration/webhooks/edit.js deleted file mode 100644 index a1c986ec4fc..00000000000 --- a/ghost/admin/app/controllers/settings/integration/webhooks/edit.js +++ /dev/null @@ -1,4 +0,0 @@ -import Controller from '@ember/controller'; - -export default class EditController extends Controller { -} diff --git a/ghost/admin/app/controllers/settings/integration/webhooks/new.js b/ghost/admin/app/controllers/settings/integration/webhooks/new.js deleted file mode 100644 index 2c1d6ff7580..00000000000 --- a/ghost/admin/app/controllers/settings/integration/webhooks/new.js +++ /dev/null @@ -1,4 +0,0 @@ -import Controller from '@ember/controller'; - -export default class NewWebhookController extends Controller { -} diff --git a/ghost/admin/app/controllers/settings/integrations.js b/ghost/admin/app/controllers/settings/integrations.js deleted file mode 100644 index 9a9b6c4a8d3..00000000000 --- a/ghost/admin/app/controllers/settings/integrations.js +++ /dev/null @@ -1,53 +0,0 @@ -import Controller from '@ember/controller'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class IntegrationsController extends Controller { - @service settings; - @service store; - - @inject config; - - _allIntegrations = this.store.peekAll('integration'); - - get zapierDisabled() { - return this.config.hostSettings?.limits?.customIntegrations?.disabled; - } - - // filter over the live query so that the list is automatically updated - // as integrations are added/removed - get integrations() { - return this._allIntegrations.reject((integration) => { - return integration.isNew || integration.type !== 'custom'; - }); - } - - // use ember-concurrency so that we can use the derived state to show - // a spinner only in the integrations list and avoid delaying the whole - // screen display - @task - *fetchIntegrations() { - return yield this.store.findAll('integration'); - } - - // used by individual integration routes' `model` hooks - integrationModelHook(prop, value, route, transition) { - let preloadedIntegration = this.store.peekAll('integration').findBy(prop, value); - - if (preloadedIntegration) { - return preloadedIntegration; - } - - return this.fetchIntegrations.perform().then((integrations) => { - let integration = integrations.findBy(prop, value); - - if (!integration) { - let path = transition.intent.url.replace(/^\//, ''); - return route.replaceWith('error404', {path, status: 404}); - } - - return integration; - }); - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/amp.js b/ghost/admin/app/controllers/settings/integrations/amp.js deleted file mode 100644 index 7edbbcb65c5..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/amp.js +++ /dev/null @@ -1,30 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class AmpController extends Controller { - @service notifications; - @service settings; - - @action - update(value) { - this.settings.amp = value; - } - - @action - save() { - this.saveTask.perform(); - } - - @task({drop: true}) - *saveTask() { - try { - yield this.settings.validate(); - return yield this.settings.save(); - } catch (error) { - this.notifications.showAPIError(error); - throw error; - } - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/firstpromoter.js b/ghost/admin/app/controllers/settings/integrations/firstpromoter.js deleted file mode 100644 index 6b1432d650e..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/firstpromoter.js +++ /dev/null @@ -1,30 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class FirstpromoterController extends Controller { - @service notifications; - @service settings; - - @action - update(value) { - this.settings.firstpromoter = value; - } - - @action - save() { - this.saveTask.perform(); - } - - @task({drop: true}) - *saveTask() { - try { - yield this.settings.validate(); - return yield this.settings.save(); - } catch (error) { - this.notifications.showAPIError(error); - throw error; - } - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/pintura.js b/ghost/admin/app/controllers/settings/integrations/pintura.js deleted file mode 100644 index 2c7623e3cf7..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/pintura.js +++ /dev/null @@ -1,97 +0,0 @@ -import Controller from '@ember/controller'; -import config from 'ghost-admin/config/environment'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -const JS_EXTENSION = ['js']; -const JS_MIME_TYPE = ['application/javascript']; -const CSS_EXTENSION = ['css']; -const CSS_MIME_TYPE = ['text/css']; -export default class PinturaController extends Controller { - @service notifications; - @service settings; - @service utils; - - @inject config; - - @tracked jsSuccess; - @tracked jsFailure; - @tracked cssSuccess; - @tracked cssFailure; - - get showUploadSettings() { - return this.settings.pintura && !this.config.pintura; - } - - constructor() { - super(...arguments); - this.jsExtension = JS_EXTENSION; - this.jsMimeType = JS_MIME_TYPE; - this.jsAccept = [...this.jsMimeType, ...Array.from(this.jsExtension, extension => '.' + extension)]; - this.cssExtension = CSS_EXTENSION; - this.cssMimeType = CSS_MIME_TYPE; - this.cssAccept = [...this.cssMimeType, ...Array.from(this.cssExtension, extension => '.' + extension)]; - } - - /** - * Opens a file selection dialog - Triggered by "Upload x" buttons, - * searches for the hidden file input within the .gh-setting element - * containing the clicked button then simulates a click - * @param {MouseEvent} event - MouseEvent fired by the button click - */ - @action - triggerFileDialog(event) { - // simulate click to open file dialog - event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click(); - } - - @action - async fileUploadCompleted(fileType, [uploadedFile]) { - let successKey = `${fileType}Success`; - let failureKey = `${fileType}Failure`; - - if (!uploadedFile || !uploadedFile.url && !uploadedFile.fileName) { - this[successKey] = false; - this[failureKey] = true; - return; // upload failed - } - this[successKey] = true; - this[failureKey] = false; - - window.setTimeout(() => { - this[successKey] = null; - this[failureKey] = null; - }, config.environment === 'test' ? 100 : 5000); - - // Save the uploaded file url to the settings - if (fileType === 'js') { - this.settings.pinturaJsUrl = uploadedFile.url; - } else if (fileType === 'css') { - this.settings.pinturaCssUrl = uploadedFile.url; - } - } - - @action - update(event) { - this.settings.pintura = event.target.checked; - } - - @action - save() { - this.saveTask.perform(); - } - - @task({drop: true}) - *saveTask() { - try { - yield this.settings.validate(); - return yield this.settings.save(); - } catch (error) { - this.notifications.showAPIError(error); - throw error; - } - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/slack.js b/ghost/admin/app/controllers/settings/integrations/slack.js deleted file mode 100644 index 2393599e6ae..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/slack.js +++ /dev/null @@ -1,56 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {isInvalidError} from 'ember-ajax/errors'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class SlackController extends Controller { - @service ghostPaths; - @service ajax; - @service notifications; - @service settings; - - get testNotificationDisabled() { - const slackUrl = this.settings.slackUrl; - return !slackUrl; - } - - @action - save() { - this.saveTask.perform(); - } - - @task({drop: true}) - *saveTask() { - try { - yield this.settings.validate(); - return yield this.settings.save(); - } catch (error) { - if (error) { - this.notifications.showAPIError(error); - throw error; - } - } - } - - @task({drop: true}) - *sendTestNotification() { - let notifications = this.notifications; - let slackApi = this.ghostPaths.url.api('slack', 'test'); - - try { - yield this.saveTask.perform(); - yield this.ajax.post(slackApi); - notifications.showNotification('Test notification sent', {type: 'info', key: 'slack-test.send.success', description: 'Check your Slack channel for the test message'}); - return true; - } catch (error) { - if (error) { - notifications.showAPIError(error, {key: 'slack-test:send'}); - - if (!isInvalidError(error)) { - throw error; - } - } - } - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/unsplash.js b/ghost/admin/app/controllers/settings/integrations/unsplash.js deleted file mode 100644 index da7b4868fe3..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/unsplash.js +++ /dev/null @@ -1,30 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class UnsplashController extends Controller { - @service notifications; - @service settings; - - @action - update(value) { - this.settings.unsplash = value; - } - - @action - save() { - this.saveTask.perform(); - } - - @task({drop: true}) - *saveTask() { - try { - yield this.settings.validate(); - return yield this.settings.save(); - } catch (error) { - this.notifications.showAPIError(error); - throw error; - } - } -} diff --git a/ghost/admin/app/controllers/settings/integrations/zapier.js b/ghost/admin/app/controllers/settings/integrations/zapier.js deleted file mode 100644 index a49984e0321..00000000000 --- a/ghost/admin/app/controllers/settings/integrations/zapier.js +++ /dev/null @@ -1,59 +0,0 @@ -import Controller from '@ember/controller'; -import RegenerateKeyModal from '../../../components/settings/integrations/regenerate-key-modal'; -import config from 'ghost-admin/config/environment'; -import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task, timeout} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class ZapierController extends Controller { - @service ghostPaths; - @service modals; - - @tracked regeneratedApiKey = null; - - constructor() { - super(...arguments); - if (this.isTesting === undefined) { - this.isTesting = config.environment === 'test'; - } - } - - get integration() { - return this.model; - } - - get apiUrl() { - let origin = window.location.origin; - let subdir = this.ghostPaths.subdir; - let url = this.ghostPaths.url.join(origin, subdir); - - return url.replace(/\/$/, ''); - } - - @action - async confirmRegenerateKey(apiKey, event) { - event?.preventDefault(); - this.regeneratedApiKey = null; - this.regeneratedApiKey = await this.modals.open(RegenerateKeyModal, { - apiKey, - integration: this.integration, - internalIntegration: 'zapier' - }); - } - - @task - *copyAdminKeyTask(event) { - event?.preventDefault(); - copyTextToClipboard(this.integration.adminKey.secret); - yield timeout(this.isTesting ? 50 : 3000); - } - - @task - *copyApiUrlTask(event) { - event?.preventDefault(); - copyTextToClipboard(this.apiUrl); - yield timeout(this.isTesting ? 50 : 3000); - } -} diff --git a/ghost/admin/app/controllers/settings/labs.js b/ghost/admin/app/controllers/settings/labs.js deleted file mode 100644 index 94be24d1154..00000000000 --- a/ghost/admin/app/controllers/settings/labs.js +++ /dev/null @@ -1,244 +0,0 @@ -import classic from 'ember-classic-decorator'; -import {inject as service} from '@ember/service'; -/* eslint-disable ghost/ember/alias-model-in-controller */ -import Controller from '@ember/controller'; -import DeleteAllModal from '../../components/settings/labs/delete-all-content-modal'; -import ImportContentModal from '../../components/modal-import-content'; -import RSVP from 'rsvp'; -import config from 'ghost-admin/config/environment'; -import { - UnsupportedMediaTypeError, - isRequestEntityTooLargeError, - isUnsupportedMediaTypeError -} from 'ghost-admin/services/ajax'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {isBlank} from '@ember/utils'; -import {isArray as isEmberArray} from '@ember/array'; -import {run} from '@ember/runloop'; -import {task, timeout} from 'ember-concurrency'; - -const {Promise} = RSVP; - -const IMPORT_MIME_TYPES = [ - 'application/json', - 'application/zip', - 'application/x-zip-compressed' -]; - -const JSON_EXTENSION = ['json']; -const JSON_MIME_TYPE = ['application/json']; - -const YAML_EXTENSION = ['yaml']; -const YAML_MIME_TYPE = [ - 'text/vnd.yaml', - 'application/vnd.yaml', - 'text/x-yaml', - 'application/x-yaml' -]; - -@classic -export default class LabsController extends Controller { - @service ajax; - @service feature; - @service ghostPaths; - @service modals; - @service notifications; - @service session; - @service settings; - @service utils; - - @inject config; - - importErrors = null; - importSuccessful = false; - showEarlyAccessModal = false; - submitting = false; - uploadButtonText = 'Import'; - importMimeType = null; - redirectsFileExtensions = null; - redirectsFileMimeTypes = null; - yamlExtension = null; - yamlMimeType = null; - yamlAccept = null; - - init() { - super.init(...arguments); - this.importMimeType = IMPORT_MIME_TYPES; - this.redirectsFileExtensions = [...JSON_EXTENSION, ...YAML_EXTENSION]; - // .yaml is added below for file dialogs to show .yaml by default. - this.redirectsFileMimeTypes = [...JSON_MIME_TYPE, ...YAML_MIME_TYPE, '.yaml']; - this.yamlExtension = YAML_EXTENSION; - this.yamlMimeType = YAML_MIME_TYPE; - // (macOS) Safari only allows files with the `yml` extension to be selected with the specified MIME types - // so explicitly allow the `yaml` extension. - this.yamlAccept = [...this.yamlMimeType, ...Array.from(this.yamlExtension, extension => '.' + extension)]; - } - - @action - onUpload(file) { - let formData = new FormData(); - let notifications = this.notifications; - let currentUserId = this.get('session.user.id'); - let dbUrl = this.get('ghostPaths.url').api('db'); - - this.set('uploadButtonText', 'Importing'); - this.set('importErrors', null); - this.set('importSuccessful', false); - - return this._validate(file).then(() => { - formData.append('importfile', file); - - return this.ajax.post(dbUrl, { - data: formData, - dataType: 'json', - cache: false, - contentType: false, - processData: false - }); - }).then((response) => { - let store = this.store; - - this.set('importSuccessful', true); - - if (response.problems) { - this.set('importErrors', response.problems); - } - - // Clear the store, so that all the new data gets fetched correctly. - store.unloadAll(); - - // NOTE: workaround for behaviour change in Ember 2.13 - // store.unloadAll has some async tendencies so we need to schedule - // the reload of the current user once the unload has finished - // https://github.com/emberjs/data/issues/4963 - run.schedule('destroy', this, () => { - // Reload currentUser and set session - this.session.populateUser({id: currentUserId}); - - // TODO: keep as notification, add link to view content - notifications.showNotification('Import successful', {key: 'import.upload.success'}); - - // reload settings - return this.settings.reload().then((settings) => { - this.feature.fetch(); - this.config.blogTitle = settings.title; - }); - }); - }).catch((response) => { - if (isUnsupportedMediaTypeError(response) || isRequestEntityTooLargeError(response)) { - this.set('importErrors', [response]); - } else if (response && response.payload.errors && isEmberArray(response.payload.errors)) { - this.set('importErrors', response.payload.errors); - } else { - this.set('importErrors', [{message: 'Import failed due to an unknown error. Check the Web Inspector console and network tabs for errors.'}]); - } - - throw response; - }).finally(() => { - this.set('uploadButtonText', 'Import'); - }); - } - - @action - importContent() { - return this.modals.open(ImportContentModal); - } - - @action - downloadFile(endpoint) { - this.utils.downloadFile(this.ghostPaths.url.api(endpoint)); - } - - @action - confirmDeleteAll() { - return this.modals.open(DeleteAllModal); - } - - @action - toggleEarlyAccessModal() { - this.toggleProperty('showEarlyAccessModal'); - } - - /** - * Opens a file selection dialog - Triggered by "Upload x" buttons, - * searches for the hidden file input within the .gh-setting element - * containing the clicked button then simulates a click - * @param {MouseEvent} event - MouseEvent fired by the button click - */ - @action - triggerFileDialog(event) { - // simulate click to open file dialog - event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click(); - } - - // TODO: convert to ember-concurrency task - _validate(file) { - // Windows doesn't have mime-types for json files by default, so we - // need to have some additional checking - if (file.type === '') { - // First check file extension so we can early return - let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name); - - if (!extension || extension.toLowerCase() !== 'json') { - return RSVP.reject(new UnsupportedMediaTypeError()); - } - - return new Promise((resolve, reject) => { - // Extension is correct, so check the contents of the file - let reader = new FileReader(); - - reader.onload = function () { - let {result} = reader; - - try { - JSON.parse(result); - - return resolve(); - } catch (e) { - return reject(new UnsupportedMediaTypeError()); - } - }; - - reader.readAsText(file); - }); - } - - let accept = this.importMimeType; - - if (!isBlank(accept) && file && accept.indexOf(file.type) === -1) { - return RSVP.reject(new UnsupportedMediaTypeError()); - } - - return RSVP.resolve(); - } - - @(task(function* (success) { - this.set('redirectSuccess', success); - this.set('redirectFailure', !success); - - yield timeout(config.environment === 'test' ? 100 : 5000); - - this.set('redirectSuccess', null); - this.set('redirectFailure', null); - return true; - }).drop()) - redirectUploadResult; - - @(task(function* (success) { - this.set('routesSuccess', success); - this.set('routesFailure', !success); - - yield timeout(config.environment === 'test' ? 100 : 5000); - - this.set('routesSuccess', null); - this.set('routesFailure', null); - return true; - }).drop()) - routesUploadResult; - - reset() { - this.set('importErrors', null); - this.set('importSuccessful', false); - } -} diff --git a/ghost/admin/app/controllers/settings/labs/import.js b/ghost/admin/app/controllers/settings/labs/import.js deleted file mode 100644 index 51519f65114..00000000000 --- a/ghost/admin/app/controllers/settings/labs/import.js +++ /dev/null @@ -1,12 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class ImportController extends Controller { - @service router; - - @action - close() { - this.router.transitionTo('settings.labs'); - } -} diff --git a/ghost/admin/app/controllers/settings/membership.js b/ghost/admin/app/controllers/settings/membership.js deleted file mode 100644 index 2074fb16184..00000000000 --- a/ghost/admin/app/controllers/settings/membership.js +++ /dev/null @@ -1,411 +0,0 @@ -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import Controller from '@ember/controller'; -import envConfig from 'ghost-admin/config/environment'; -import {action} from '@ember/object'; -import {currencies, getCurrencyOptions, getSymbol} from 'ghost-admin/utils/currency'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -const CURRENCIES = currencies.map((currency) => { - return { - value: currency.isoCode.toLowerCase(), - label: `${currency.isoCode} - ${currency.name}`, - isoCode: currency.isoCode - }; -}); - -export default class MembersAccessController extends Controller { - @service feature; - @service membersUtils; - @service modals; - @service settings; - @service store; - @service session; - - @inject config; - - @tracked showPortalSettings = false; - @tracked showStripeConnect = false; - @tracked showTierModal = false; - - @tracked tier = null; - @tracked tiers = null; - @tracked tierModel = null; - @tracked paidSignupRedirect; - @tracked freeSignupRedirect; - @tracked welcomePageURL; - @tracked stripeMonthlyAmount = 5; - @tracked stripeYearlyAmount = 50; - @tracked currency = 'usd'; - @tracked stripePlanError = ''; - - @tracked portalPreviewUrl = ''; - - portalPreviewGuid = Date.now().valueOf(); - - queryParams = ['showPortalSettings', 'verifyEmail']; - @tracked verifyEmail = null; - - get freeTier() { - return this.tiers?.find(tier => tier.type === 'free'); - } - - get paidTiers() { - return this.tiers?.filter(tier => tier.type === 'paid'); - } - - get allCurrencies() { - return getCurrencyOptions(); - } - - get siteUrl() { - return this.config.blogUrl; - } - - get selectedCurrency() { - return CURRENCIES.findBy('value', this.currency); - } - - get isConnectDisallowed() { - const siteUrl = this.config.blogUrl; - return envConfig.environment !== 'development' && !/^https:/.test(siteUrl); - } - - get hasChangedPrices() { - if (this.tier) { - const monthlyPrice = this.tier.get('monthlyPrice'); - const yearlyPrice = this.tier.get('yearlyPrice'); - - if (monthlyPrice?.amount && parseFloat(this.stripeMonthlyAmount) !== (monthlyPrice.amount / 100)) { - return true; - } - if (yearlyPrice?.amount && parseFloat(this.stripeYearlyAmount) !== (yearlyPrice.amount / 100)) { - return true; - } - } - - return false; - } - - @action - setup() { - this.fetchTiers.perform(); - this.updatePortalPreview(); - } - - get isDirty() { - return this.settings.hasDirtyAttributes || this.hasChangedPrices; - } - - @action - async membersSubscriptionAccessChanged() { - const oldValue = this.settings.changedAttributes().membersSignupAccess?.[0]; - - if (oldValue === 'none') { - // when saved value is 'none' the server won't inject the portal script - // to work around that and show the expected portal preview we save and - // force a refresh - await this.switchFromNoneTask.perform(); - } else { - this.updatePortalPreview(); - } - } - - @action - setStripePlansCurrency(event) { - const newCurrency = event.value; - this.currency = newCurrency; - } - - @action - setPaidSignupRedirect(url) { - this.paidSignupRedirect = url; - } - - @action - setFreeSignupRedirect(url) { - this.freeSignupRedirect = url; - } - - @action - setWelcomePageURL(url) { - this.welcomePageURL = url; - } - - @action - validatePaidSignupRedirect() { - return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect'); - } - - @action - validateFreeSignupRedirect() { - return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect'); - } - - @action - validateWelcomePageURL() { - const siteUrl = this.siteUrl; - - if (this.welcomePageURL === undefined) { - // Not initialised - return; - } - - if (this.welcomePageURL.href.startsWith(siteUrl)) { - const path = this.welcomePageURL.href.replace(siteUrl, ''); - this.freeTier.welcomePageURL = path; - } else { - this.freeTier.welcomePageURL = this.welcomePageURL.href; - } - } - - @action - validateStripePlans({updatePortalPreview = true} = {}) { - this.stripePlanError = undefined; - - try { - const yearlyAmount = this.stripeYearlyAmount; - const monthlyAmount = this.stripeMonthlyAmount; - const symbol = getSymbol(this.currency); - if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) { - throw new TypeError(`Subscription amount must be at least ${symbol}1.00`); - } - - if (updatePortalPreview) { - this.updatePortalPreview(); - } - } catch (err) { - this.stripePlanError = err.message; - } - } - - @action - openStripeConnect() { - this.stripeEnabledOnOpen = this.membersUtils.isStripeEnabled; - this.showStripeConnect = true; - } - - @action - async closeStripeConnect() { - if (this.stripeEnabledOnOpen !== this.membersUtils.isStripeEnabled) { - await this.saveSettingsTask.perform({forceRefresh: true}); - } - this.showStripeConnect = false; - } - - @action - async openEditTier(tier) { - this.tierModel = tier; - this.showTierModal = true; - } - - @action - async openNewTier() { - this.tierModel = this.store.createRecord('tier'); - this.showTierModal = true; - } - - @action - closeTierModal() { - this.showTierModal = false; - } - - @action - openPortalSettings() { - this.saveSettingsTask.perform(); - this.showPortalSettings = true; - } - - @action - async closePortalSettings() { - const changedAttributes = this.settings.changedAttributes(); - - if (changedAttributes && Object.keys(changedAttributes).length > 0) { - const shouldClose = await this.modals.open(ConfirmUnsavedChangesModal); - - if (shouldClose) { - this.settings.rollbackAttributes(); - this.showPortalSettings = false; - this.updatePortalPreview(); - } - } else { - this.showPortalSettings = false; - this.updatePortalPreview(); - } - } - - @action - updatePortalPreview({forceRefresh} = {forceRefresh: false}) { - // TODO: can these be worked out from settings in membersUtils? - const monthlyPrice = Math.round(this.stripeMonthlyAmount * 100); - const yearlyPrice = Math.round(this.stripeYearlyAmount * 100); - let portalPlans = this.settings.portalPlans || []; - - let isMonthlyChecked = portalPlans.includes('monthly'); - let isYearlyChecked = portalPlans.includes('yearly'); - - const tiers = this.store.peekAll('tier'); - const portalTiers = tiers?.filter((tier) => { - return tier.get('visibility') === 'public' - && tier.get('active') === true - && tier.get('type') === 'paid'; - }).map((tier) => { - return tier.id; - }); - - const newUrl = new URL(this.membersUtils.getPortalPreviewUrl({ - button: false, - monthlyPrice, - yearlyPrice, - portalTiers, - currency: this.currency, - isMonthlyChecked, - isYearlyChecked, - portalPlans: null - })); - - if (forceRefresh) { - this.portalPreviewGuid = Date.now().valueOf(); - } - newUrl.searchParams.set('v', this.portalPreviewGuid); - - this.portalPreviewUrl = newUrl; - } - - @action - portalPreviewInserted(iframe) { - this.portalPreviewIframe = iframe; - - if (!this.portalMessageListener) { - this.portalMessageListener = (event) => { - // don't resize membership portal preview when events fire in customize portal modal - if (this.showPortalSettings) { - return; - } - - const resizeEvents = ['portal-ready', 'portal-preview-updated']; - if (resizeEvents.includes(event.data.type) && event.data.payload?.height && this.portalPreviewIframe?.parentNode) { - this.portalPreviewIframe.parentNode.style.height = `${event.data.payload.height}px`; - } - }; - - window.addEventListener('message', this.portalMessageListener, true); - } - } - - @action - portalPreviewDestroyed() { - this.portalPreviewIframe = null; - - if (this.portalMessageListener) { - window.removeEventListener('message', this.portalMessageListener); - } - } - - @action - confirmTierSave() { - this.updatePortalPreview({forceRefresh: true}); - return this.fetchTiers.perform(); - } - - @task - *switchFromNoneTask() { - return yield this.saveSettingsTask.perform({forceRefresh: true}); - } - - setupPortalTier(tier) { - if (tier) { - const monthlyPrice = tier.get('monthlyPrice'); - const yearlyPrice = tier.get('yearlyPrice'); - this.currency = tier.get('currency'); - if (monthlyPrice) { - this.stripeMonthlyAmount = (monthlyPrice / 100); - } - if (yearlyPrice) { - this.stripeYearlyAmount = (yearlyPrice / 100); - } - this.updatePortalPreview(); - } - } - - @task({drop: true}) - *fetchTiers() { - this.tiers = yield this.store.query('tier', { - include: 'monthly_price,yearly_price,benefits' - }); - this.tier = this.paidTiers.firstObject; - this.setupPortalTier(this.tier); - } - - @task({drop: true}) - *saveSettingsTask(options) { - if (this.settings.errors.length !== 0) { - return; - } - // When no filer is selected in `Specific tier(s)` option - if (!this.settings.defaultContentVisibility) { - return; - } - const result = yield this.settings.save(); - yield this.freeTier.save(); - this.updatePortalPreview(options); - return result; - } - - async saveTier() { - const paidMembersEnabled = this.settings.paidMembersEnabled; - if (this.tier && paidMembersEnabled) { - const monthlyAmount = Math.round(this.stripeMonthlyAmount * 100); - const yearlyAmount = Math.round(this.stripeYearlyAmount * 100); - - this.tier.set('monthlyPrice', monthlyAmount); - this.tier.set('yearlyPrice', yearlyAmount); - - const savedTier = await this.tier.save(); - return savedTier; - } - } - - @action - reset() { - this.settings.rollbackAttributes(); - this.resetPrices(); - this.showLeavePortalModal = false; - this.showPortalSettings = false; - } - - resetPrices() { - const monthlyPrice = this.tier.get('monthlyPrice'); - const yearlyPrice = this.tier.get('yearlyPrice'); - - this.stripeMonthlyAmount = monthlyPrice ? (monthlyPrice.amount / 100) : 5; - this.stripeYearlyAmount = yearlyPrice ? (yearlyPrice.amount / 100) : 50; - } - - _validateSignupRedirect(url, type) { - const siteUrl = this.config.blogUrl; - let errMessage = `Please enter a valid URL`; - this.settings.errors.remove(type); - this.settings.hasValidated.removeObject(type); - - if (url === null) { - this.settings.errors.add(type, errMessage); - this.settings.hasValidated.pushObject(type); - return false; - } - - if (url === undefined) { - // Not initialised - return; - } - - if (url.href.startsWith(siteUrl)) { - const path = url.href.replace(siteUrl, ''); - this.settings[type] = path; - } else { - this.settings[type] = url.href; - } - } -} diff --git a/ghost/admin/app/controllers/settings/navigation.js b/ghost/admin/app/controllers/settings/navigation.js deleted file mode 100644 index 48b50a93204..00000000000 --- a/ghost/admin/app/controllers/settings/navigation.js +++ /dev/null @@ -1,146 +0,0 @@ -import Controller from '@ember/controller'; -import NavigationItem from 'ghost-admin/models/navigation-item'; -import RSVP from 'rsvp'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class NavigationController extends Controller { - @service ghostPaths; - @service notifications; - @service session; - @service settings; - - @inject config; - - @tracked dirtyAttributes = false; - @tracked newNavItem = NavigationItem.create({isNew: true}); - @tracked newSecondaryNavItem = NavigationItem.create({isNew: true, isSecondary: true}); - - get blogUrl() { - let url = this.config.blogUrl; - - return url.slice(-1) !== '/' ? `${url}/` : url; - } - - @action - save() { - this.saveTask.perform(); - } - - @action - addNavItem(item) { - // If the url sent through is blank (user never edited the url) - if (item.get('url') === '') { - item.set('url', '/'); - } - - return item.validate().then(() => { - this.addNewNavItem(item); - }); - } - - @action - deleteNavItem(item) { - if (!item) { - return; - } - - let navItems = item.isSecondary ? this.settings.secondaryNavigation : this.settings.navigation; - - navItems.removeObject(item); - this.dirtyAttributes = true; - } - - @action - updateLabel(label, navItem) { - if (!navItem) { - return; - } - - if (navItem.get('label') !== label) { - navItem.set('label', label); - this.dirtyAttributes = true; - } - } - - @action - updateUrl(url, navItem) { - if (!navItem) { - return; - } - - if (navItem.get('url') !== url) { - navItem.set('url', url); - this.dirtyAttributes = true; - } - - return url; - } - - @action - reset() { - this.newNavItem = NavigationItem.create({isNew: true}); - this.newSecondaryNavItem = NavigationItem.create({isNew: true, isSecondary: true}); - } - - addNewNavItem(item) { - let navItems = item.isSecondary ? this.settings.secondaryNavigation : this.settings.navigation; - - item.set('isNew', false); - navItems.pushObject(item); - this.dirtyAttributes = true; - - if (item.isSecondary) { - this.newSecondaryNavItem = NavigationItem.create({isNew: true, isSecondary: true}); - } else { - this.newNavItem = NavigationItem.create({isNew: true}); - } - } - - @task - *saveTask() { - let navItems = this.settings.navigation; - let secondaryNavItems = this.settings.secondaryNavigation; - - let notifications = this.notifications; - let validationPromises = []; - - if (!this.newNavItem.get('isBlank')) { - validationPromises.pushObject(this.send('addNavItem', this.newNavItem)); - } - - if (!this.newSecondaryNavItem.get('isBlank')) { - validationPromises.pushObject(this.send('addNavItem', this.newSecondaryNavItem)); - } - - navItems.forEach((item) => { - validationPromises.pushObject(item.validate()); - }); - - secondaryNavItems.forEach((item) => { - validationPromises.pushObject(item.validate()); - }); - - try { - yield RSVP.all(validationPromises); - - // If some attributes have been changed, rebuild - // the model arrays or changes will not be detected - if (this.dirtyAttributes) { - this.settings.navigation = [...this.settings.navigation]; - this.settings.secondaryNavigation = [...this.settings.secondaryNavigation]; - } - - this.dirtyAttributes = false; - return yield this.settings.save(); - } catch (error) { - if (error) { - notifications.showAPIError(error); - throw error; - } - } - } -} diff --git a/ghost/admin/app/controllers/settings/newsletters.js b/ghost/admin/app/controllers/settings/newsletters.js deleted file mode 100644 index 979694b0d3c..00000000000 --- a/ghost/admin/app/controllers/settings/newsletters.js +++ /dev/null @@ -1,18 +0,0 @@ -import Controller from '@ember/controller'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class NewslettersController extends Controller { - @service settings; - - queryParams = ['verifyEmail']; - - @tracked verifyEmail = null; - - @task({drop: true}) - *saveSettings() { - const response = yield this.settings.save(); - return response; - } -} diff --git a/ghost/admin/app/controllers/settings/staff/index.js b/ghost/admin/app/controllers/settings/staff/index.js deleted file mode 100644 index 848af932ce4..00000000000 --- a/ghost/admin/app/controllers/settings/staff/index.js +++ /dev/null @@ -1,83 +0,0 @@ -import Controller from '@ember/controller'; -import RSVP from 'rsvp'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class IndexController extends Controller { - @service session; - @service store; - - @tracked showInviteUserModal = false; - @tracked showResetAllPasswordsModal = false; - - inviteOrder = ['email']; - userOrder = ['name', 'email']; - - allInvites = this.store.peekAll('invite'); - allUsers = this.store.peekAll('user'); - - get currentUser() { - return this.model; - } - - get invites() { - return this.allInvites - .filter(i => !i.isNew) - .sortBy(...this.inviteOrder); - } - - get activeUsers() { - return this.allUsers - .filter(u => u.status !== 'inactive') - .sortBy(...this.userOrder); - } - - get suspendedUsers() { - return this.allUsers - .filter(u => u.status === 'inactive') - .sortBy(...this.userOrder); - } - - @action - toggleInviteUserModal() { - this.showInviteUserModal = !this.showInviteUserModal; - } - - @action - toggleResetAllPasswordsModal() { - this.showResetAllPasswordsModal = !this.showResetAllPasswordsModal; - } - - @task - *backgroundUpdateTask() { - let users = this.fetchUsersTask.perform(); - let invites = this.fetchInvitesTask.perform(); - - try { - yield RSVP.all([users, invites]); - } catch (error) { - this.send('error', error); - } - } - - @task - *fetchUsersTask() { - yield this.store.query('user', {limit: 'all'}); - } - - @task - *fetchInvitesTask() { - if (this.currentUser.isAuthorOrContributor) { - return; - } - - // ensure roles are loaded before invites. Invites do not have embedded - // role records which means Ember Data will throw errors when trying to - // read the invite.role data when the role has not yet been loaded - yield this.store.query('role', {limit: 'all'}); - - return yield this.store.query('invite', {limit: 'all'}); - } -} diff --git a/ghost/admin/app/controllers/settings/staff/user-loading.js b/ghost/admin/app/controllers/settings/staff/user-loading.js deleted file mode 100644 index 79046d93edb..00000000000 --- a/ghost/admin/app/controllers/settings/staff/user-loading.js +++ /dev/null @@ -1,6 +0,0 @@ -import Controller from '@ember/controller'; -import {inject as service} from '@ember/service'; - -export default class StaffUserLoadingController extends Controller { - @service session; -} \ No newline at end of file diff --git a/ghost/admin/app/controllers/settings/staff/user.js b/ghost/admin/app/controllers/settings/staff/user.js deleted file mode 100644 index 0cd80509b59..00000000000 --- a/ghost/admin/app/controllers/settings/staff/user.js +++ /dev/null @@ -1,378 +0,0 @@ -import Controller from '@ember/controller'; -import DeleteUserModal from '../../../components/settings/staff/modals/delete-user'; -import RegenerateStaffTokenModal from '../../../components/settings/staff/modals/regenerate-staff-token'; -import SelectRoleModal from '../../../components/settings/staff/modals/select-role'; -import SuspendUserModal from '../../../components/settings/staff/modals/suspend-user'; -import TransferOwnershipModal from '../../../components/settings/staff/modals/transfer-ownership'; -import UnsuspendUserModal from '../../../components/settings/staff/modals/unsuspend-user'; -import UploadImageModal from '../../../components/settings/staff/modals/upload-image'; -import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard'; -import isNumber from 'ghost-admin/utils/isNumber'; -import windowProxy from 'ghost-admin/utils/window-proxy'; -import {TrackedObject} from 'tracked-built-ins'; -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 {task, taskGroup, timeout} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class UserController extends Controller { - @service ajax; - @service ghostPaths; - @service membersUtils; - @service modals; - @service notifications; - @service session; - @service slugGenerator; - @service utils; - - @inject config; - - @tracked dirtyAttributes = false; - @tracked personalToken = null; - @tracked personalTokenRegenerated = false; - @tracked scratchValues = new TrackedObject(); - @tracked slugValue = null; // not set directly on model to avoid URL changing before save - - get user() { - return this.model; - } - - get currentUser() { - return this.session.user; - } - - get isOwnProfile() { - return this.currentUser.id === this.user.id; - } - - get isAdminUserOnOwnProfile() { - return this.currentUser.isAdminOnly && this.isOwnProfile; - } - - get isAdminUserOnOwnerProfile() { - return this.currentUser.isAdminOnly && this.user.isOwnerOnly; - } - - get canChangeEmail() { - return !this.isAdminUserOnOwnerProfile; - } - - get canChangePassword() { - return !this.isAdminUserOnOwnerProfile; - } - - get canMakeOwner() { - return this.currentUser.isOwnerOnly - && !this.isOwnProfile - && this.user.isAdminOnly - && !this.user.isSuspended; - } - - get canToggleMemberAlerts() { - return this.currentUser.isOwnerOnly || this.isAdminUserOnOwnProfile; - } - - get rolesDropdownIsVisible() { - return this.currentUser.isAdmin && !this.isOwnProfile && !this.user.isOwnerOnly; - } - - get userActionsAreVisible() { - return this.deleteUserActionIsVisible || this.canMakeOwner; - } - - get deleteUserActionIsVisible() { - // users can't delete themselves - if (this.isOwnProfile) { - return false; - } - - if ( - // owners/admins can delete any non-owner user - (this.currentUser.isAdmin && !this.user.isOwnerOnly) || - // editors can delete any author or contributor - (this.currentUser.isEditor && this.user.isAuthorOrContributor) - ) { - return true; - } - - return false; - } - - @action - setModelProperty(property, event) { - const value = event.target.value; - this.user[property] = value; - } - - @action - validateModelProperty(property) { - this.user.validate({property}); - } - - @action - clearModelErrors(property) { - this.user.hasValidated.removeObject(property); - this.user.errors.remove(property); - } - - @action - setSlugValue(event) { - this.slugValue = event.target.value; - } - - @action - async deleteUser() { - await this.modals.open(DeleteUserModal, { - user: this.model - }); - } - - @action - async suspendUser() { - await this.modals.open(SuspendUserModal, { - user: this.model, - saveTask: this.saveTask - }); - } - - @action - async unsuspendUser() { - await this.modals.open(UnsuspendUserModal, { - user: this.model, - saveTask: this.saveTask - }); - } - - @action - async transferOwnership() { - await this.modals.open(TransferOwnershipModal, { - user: this.model - }); - } - - @action - async regenerateStaffToken() { - const apiToken = await this.modals.open(RegenerateStaffTokenModal); - - if (apiToken) { - this.personalToken = apiToken; - this.personalTokenRegenerated = true; - } - } - - @action - async selectRole() { - const newRole = await this.modals.open(SelectRoleModal, { - currentRole: this.model.role - }); - - if (newRole) { - this.user.role = newRole; - this.dirtyAttributes = true; - } - } - - @action - async changeCoverImage() { - await this.modals.open(UploadImageModal, { - model: this.model, - modelProperty: 'coverImage' - }); - } - - @action - async changeProfileImage() { - await this.modals.open(UploadImageModal, { - model: this.model, - modelProperty: 'profileImage' - }); - } - - @action - setScratchValue(property, value) { - this.scratchValues[property] = value; - } - - @action - reset() { - this.user.rollbackAttributes(); - this.user.password = ''; - this.user.newPassword = ''; - this.user.ne2Password = ''; - this.slugValue = this.user.slug; - this.dirtyAttributes = false; - this.clearScratchValues(); - } - - @action - toggleCommentNotifications(event) { - this.user.commentNotifications = event.target.checked; - } - - @action - toggleMentionNotifications(event) { - this.user.mentionNotifications = event.target.checked; - } - - @action - toggleMilestoneNotifications(event) { - this.user.milestoneNotifications = event.target.checked; - } - - @action - toggleDonationNotifications(event) { - this.user.donationNotifications = event.target.checked; - } - - @action - toggleMemberEmailAlerts(type, event) { - if (type === 'free-signup') { - this.user.freeMemberSignupNotification = event.target.checked; - } else if (type === 'paid-started') { - this.user.paidSubscriptionStartedNotification = event.target.checked; - } else if (type === 'paid-canceled') { - this.user.paidSubscriptionCanceledNotification = event.target.checked; - } - } - - @taskGroup({enqueue: true}) saveHandlers; - - @task({group: 'saveHandlers'}) - *updateSlugTask(event) { - let newSlug = event.target.value; - let slug = this.user.slug; - - newSlug = newSlug || slug; - newSlug = newSlug.trim(); - - // Ignore unchanged slugs or candidate slugs that are empty - if (!newSlug || slug === newSlug) { - this.slugValue = slug; - - return true; - } - - let serverSlug = yield this.slugGenerator.generateSlug('user', newSlug); - - // If after getting the sanitized and unique slug back from the API - // we end up with a slug that matches the existing slug, abort the change - if (serverSlug === slug) { - return true; - } - - // Because the server transforms the candidate slug by stripping - // certain characters and appending a number onto the end of slugs - // to enforce uniqueness, there are cases where we can get back a - // candidate slug that is a duplicate of the original except for - // the trailing incrementor (e.g., this-is-a-slug and this-is-a-slug-2) - - // get the last token out of the slug candidate and see if it's a number - let slugTokens = serverSlug.split('-'); - let check = Number(slugTokens.pop()); - - // if the candidate slug is the same as the existing slug except - // for the incrementor then the existing slug should be used - if (isNumber(check) && check > 0) { - if (slug === slugTokens.join('-') && serverSlug !== newSlug) { - this.slugValue = slug; - - return true; - } - } - - this.slugValue = serverSlug; - this.dirtyAttributes = true; - - return true; - } - - @task({group: 'saveHandlers'}) - *saveTask() { - let user = this.user; - let slugValue = this.slugValue; - let slugChanged; - - if (user.slug !== slugValue) { - slugChanged = true; - user.slug = slugValue; - } - - try { - user = yield user.save(); - - this.clearScratchValues(); - - // If the user's slug has changed, change the URL and replace - // the history so refresh and back button still work - if (slugChanged) { - let currentPath = window.location.hash; - - let newPath = currentPath.split('/'); - newPath[newPath.length - 1] = user.get('slug'); - newPath = newPath.join('/'); - - windowProxy.replaceState({path: newPath}, '', newPath); - } - - this.dirtyAttributes = false; - this.notifications.closeAlerts('user.update'); - - return user; - } catch (error) { - // validation engine returns undefined so we have to check - // before treating the failure as an API error - if (error) { - this.notifications.showAPIError(error, {key: 'user.update'}); - } - } - } - - @task - *saveNewPasswordTask() { - try { - const user = yield this.user.saveNewPasswordTask.perform(); - document.querySelector('#password-reset')?.reset(); - return user; - } catch (error) { - if (error) { - this.notifications.showAPIError(error, {key: 'user.update'}); - } - } - } - - @action - submitPasswordForm(event) { - event.preventDefault(); - this._blurAndTrigger(() => this.saveNewPasswordTask.perform()); - } - - @action - saveViaKeyboard(event) { - event.preventDefault(); - this._blurAndTrigger(() => this.saveTask.perform()); - } - - @task - *copyContentKeyTask() { - copyTextToClipboard(this.personalToken); - yield timeout(this.isTesting ? 50 : 3000); - } - - clearScratchValues() { - this.scratchValues = new TrackedObject(); - } - - _blurAndTrigger(fn) { - // trigger any set-on-blur actions - const focusedElement = document.activeElement; - focusedElement?.blur(); - - // schedule save for when set-on-blur actions have finished - run.schedule('actions', this, function () { - focusedElement?.focus(); - fn(); - }); - } -} diff --git a/ghost/admin/app/controllers/settings/tier.js b/ghost/admin/app/controllers/settings/tier.js deleted file mode 100644 index 9785de54730..00000000000 --- a/ghost/admin/app/controllers/settings/tier.js +++ /dev/null @@ -1,208 +0,0 @@ -import Controller from '@ember/controller'; -import EmberObject, {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; -import {tracked} from '@glimmer/tracking'; - -export default class TierController extends Controller { - @service membersUtils; - @service settings; - - @inject config; - - @tracked showLeaveSettingsModal = false; - @tracked showPriceModal = false; - @tracked priceModel = null; - @tracked showUnsavedChangesModal = false; - @tracked paidSignupRedirect; - - get siteUrl() { - return this.config.blogUrl; - } - - get tier() { - return this.model; - } - - get stripePrices() { - const stripePrices = this.model.stripePrices || []; - return stripePrices.map((d) => { - return { - ...d, - amount: d.amount / 100 - }; - }).sort((a, b) => { - return a.amount - b.amount; - }).sort((a, b) => { - return a.currency.localeCompare(b.currency, undefined, {ignorePunctuation: true}); - }).sort((a, b) => { - return (a.active === b.active) ? 0 : (a.active ? -1 : 1); - }); - } - - get noOfPrices() { - return (this.tier.stripePrices || []).length; - } - - @action - toggleUnsavedChangesModal(transition) { - let leaveTransition = this.leaveScreenTransition; - - if (!transition && this.showUnsavedChangesModal) { - this.leaveScreenTransition = null; - this.showUnsavedChangesModal = false; - return; - } - - if (!leaveTransition || transition.targetName === leaveTransition.targetName) { - this.leaveScreenTransition = transition; - - // if a save is running, wait for it to finish then transition - if (this.saveTask.isRunning) { - return this.saveTask.last.then(() => { - transition.retry(); - }); - } - - // we genuinely have unsaved data, show the modal - this.showUnsavedChangesModal = true; - } - } - - @action - leaveScreen() { - this.tier.rollbackAttributes(); - return this.leaveScreenTransition.retry(); - } - - @action - async openEditPrice(price) { - this.priceModel = price; - this.showPriceModal = true; - } - - @action - async openNewPrice() { - this.priceModel = null; - this.showPriceModal = true; - } - - @action - async archivePrice(price) { - price.active = false; - price.amount = price.amount * 100; - this.send('savePrice', price); - } - - @action - async activatePrice(price) { - price.active = true; - price.amount = price.amount * 100; - this.send('savePrice', price); - } - - @action - openStripeConnect() { - alert('Update to use stripe-connect modal (see memberships screen)'); - } - - @action - async confirmLeave() { - this.settings.rollbackAttributes(); - this.showLeaveSettingsModal = false; - this.leaveSettingsTransition.retry(); - } - - @action - cancelLeave() { - this.showLeaveSettingsModal = false; - this.leaveSettingsTransition = null; - } - - @action - save() { - return this.saveTask.perform(); - } - - @action - savePrice(price) { - const stripePrices = this.tier.stripePrices.map((d) => { - if (d.id === price.id) { - return EmberObject.create({ - ...price, - active: !!price.active - }); - } - return { - ...d, - active: !!d.active - }; - }); - if (!price.id) { - stripePrices.push(EmberObject.create({ - ...price, - active: !!price.active - })); - } - this.tier.set('stripePrices', stripePrices); - this.saveTask.perform(); - } - - @action - closePriceModal() { - this.showPriceModal = false; - } - - @action - setPaidSignupRedirect(url) { - this.paidSignupRedirect = url; - } - - @action - validatePaidSignupRedirect() { - return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect'); - } - - @task({restartable: true}) - *saveTask() { - this.send('validatePaidSignupRedirect'); - this.tier.validate(); - if (this.tier.get('errors').length !== 0) { - return; - } - if (this.settings.errors.length !== 0) { - return; - } - yield this.settings.save(); - const response = yield this.tier.save(); - if (this.showPriceModal) { - this.closePriceModal(); - } - return response; - } - - _validateSignupRedirect(url, type) { - let errMessage = `Please enter a valid URL`; - this.settings.errors.remove(type); - this.settings.hasValidated.removeObject(type); - - if (url === null) { - this.settings.errors.add(type, errMessage); - this.settings.hasValidated.pushObject(type); - return false; - } - - if (url === undefined) { - // Not initialised - return; - } - - if (url.href.startsWith(this.siteUrl)) { - const path = url.href.replace(this.siteUrl, ''); - this.settings[type] = path; - } else { - this.settings[type] = url.href; - } - } -} diff --git a/ghost/admin/app/controllers/settings/tiers.js b/ghost/admin/app/controllers/settings/tiers.js deleted file mode 100644 index acd28e55c35..00000000000 --- a/ghost/admin/app/controllers/settings/tiers.js +++ /dev/null @@ -1,38 +0,0 @@ -import Controller from '@ember/controller'; -import {action} from '@ember/object'; -import {htmlSafe} from '@ember/template'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class TiersController extends Controller { - @service settings; - - @inject config; - - @tracked iconStyle = ''; - @tracked showFreeMembershipModal = false; - - constructor() { - super(...arguments); - this.iconStyle = this.setIconStyle(); - } - - get tiers() { - return this.model.sortBy('name'); - } - - setIconStyle() { - let icon = this.config.icon; - if (icon) { - return htmlSafe(`background-image: url(${icon})`); - } - icon = 'https://static.ghost.org/v4.0.0/images/ghost-orb-2.png'; - return htmlSafe(`background-image: url(${icon})`); - } - - @action - closeFreeMembershipModal() { - this.showFreeMembershipModal = false; - } -} diff --git a/ghost/admin/app/helpers/parse-member-event.js b/ghost/admin/app/helpers/parse-member-event.js index ddf8369d0be..f0581eb3b6c 100644 --- a/ghost/admin/app/helpers/parse-member-event.js +++ b/ghost/admin/app/helpers/parse-member-event.js @@ -220,7 +220,7 @@ export default class ParseMemberEventHelper extends Helper { * object: 'My blog post' * When both words need to get appended, we'll add 'on' * -> do this by returning 'on' in getJoin() - * This string is not added when action and object are in a separete table column, or when the getObject/getURL is empty + * This string is not added when action and object are in a separate table column, or when the getObject/getURL is empty */ getJoin() { return '–'; diff --git a/ghost/admin/app/helpers/set-query-params.js b/ghost/admin/app/helpers/set-query-params.js index cee3f6ebd45..d0380aa0889 100644 --- a/ghost/admin/app/helpers/set-query-params.js +++ b/ghost/admin/app/helpers/set-query-params.js @@ -9,7 +9,7 @@ import {helper} from '@ember/component/helper'; * * This example will return https://myurl.com?utm_source=admin * - * You can set every query/search parameter you want. It will override existing paramters if they are already set. + * You can set every query/search parameter you want. It will override existing parameters if they are already set. */ export function setQueryParams([url], parameters) { if (url) { diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 9f6ec1bccb1..3cbc4b01a73 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -136,10 +136,6 @@ export default Model.extend(Comparable, ValidationEngine, { lexicalScratch: null, titleScratch: null, - // HACK: used for validation so that date/time can be validated based on - // eventual status rather than current status - statusScratch: null, - // For use by date/time pickers - will be validated then converted to UTC // on save. Updated by an observer whenever publishedAtUTC changes. // Everything that revolves around publishedAtUTC only cares about the saved diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js index c64ee527288..628fe2af4ca 100644 --- a/ghost/admin/app/routes/application.js +++ b/ghost/admin/app/routes/application.js @@ -92,7 +92,6 @@ export default Route.extend(ShortcutsRoute, { // Need a tiny delay here to allow the router to update to the current route later(() => { Sentry.setTag('route', this.router.currentRouteName); - Sentry.setTag('path', this.router.currentURL); }, 2); }, @@ -181,10 +180,32 @@ export default Route.extend(ShortcutsRoute, { dsn: this.config.sentry_dsn, environment: this.config.sentry_env, release: `ghost@${this.config.version}`, - beforeSend(event) { + beforeSend(event, hint) { + const exception = hint.originalException; event.tags = event.tags || {}; event.tags.shown_to_user = event.tags.shown_to_user || false; event.tags.grammarly = !!document.querySelector('[data-gr-ext-installed]'); + + // Do not report "handled" errors to Sentry + if (event.tags.shown_to_user === true) { + return null; + } + + // ajax errors — improve logging and add context for debugging + if (isAjaxError(exception)) { + const error = exception.payload.errors[0]; + event.exception.values[0].type = `${error.type}: ${error.context}`; + event.exception.values[0].value = error.message; + event.exception.values[0].context = error.context; + event.tags.isAjaxError = true; + } else { + event.tags.isAjaxError = false; + delete event.contexts.ajax; + delete event.tags.ajaxStatus; + delete event.tags.ajaxMethod; + delete event.tags.ajaxUrl; + } + return event; }, // TransitionAborted errors surface from normal application behaviour diff --git a/ghost/admin/app/routes/settings.js b/ghost/admin/app/routes/settings.js deleted file mode 100644 index 75508ef2266..00000000000 --- a/ghost/admin/app/routes/settings.js +++ /dev/null @@ -1,16 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import {inject as service} from '@ember/service'; - -export default class SettingsRoute extends AuthenticatedRoute { - @service session; - - beforeModel() { - super.beforeModel(...arguments); - - const user = this.session.user; - - if (!user.isAdmin) { - return this.transitionTo('settings.staff.user', user); - } - } -} diff --git a/ghost/admin/app/routes/settings/analytics.js b/ghost/admin/app/routes/settings/analytics.js deleted file mode 100644 index 6f1a3d55d86..00000000000 --- a/ghost/admin/app/routes/settings/analytics.js +++ /dev/null @@ -1,75 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import RSVP from 'rsvp'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; - -export default class AnalyticsSettingsRoute extends AdminRoute { - @service modals; - @service settings; - - @inject config; - - model() { - return RSVP.hash({ - settings: this.settings.reload() - }); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - reloadSettings() { - return this.settings.reload(); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Analytics' - }; - } -} diff --git a/ghost/admin/app/routes/settings/announcement-bar.js b/ghost/admin/app/routes/settings/announcement-bar.js deleted file mode 100644 index c8d97230b23..00000000000 --- a/ghost/admin/app/routes/settings/announcement-bar.js +++ /dev/null @@ -1,93 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/authenticated'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class AnnouncementBarRoute extends AdminRoute { - @service customThemeSettings; - @service feature; - @service modals; - @service settings; - @service themeManagement; - @service ui; - @service session; - @service store; - - model() { - // background refresh of preview - // not doing it on the 'index' route so that we don't reload going to/from the index, - // any actions performed on child routes that need a refresh should trigger it explicitly - this.themeManagement.updatePreviewHtmlTask.perform(); - - // wait for settings to be loaded - we need the data to be present before display - return Promise.all([ - this.settings.reload(), - this.customThemeSettings.load(), - this.store.findAll('theme') - ]); - } - - beforeModel() { - super.beforeModel(...arguments); - - const user = this.session.user; - - if (!user.isAdmin) { - return this.transitionTo('settings.staff.user', user); - } - } - - activate() { - this.ui.contextualNavMenu = 'announcement-bar'; - } - - deactivate() { - this.ui.contextualNavMenu = null; - this.confirmModal = null; - this.hasConfirmed = false; - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Announcement bar', - mainClasses: ['gh-main-fullwidth'] - }; - } - - @action - willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - // always abort when not confirmed because Ember's router doesn't automatically wait on promises - transition.abort(); - - this.confirmUnsavedChanges().then((shouldLeave) => { - if (shouldLeave === true) { - this.hasConfirmed = true; - return transition.retry(); - } - }); - } - - confirmUnsavedChanges() { - if (!this.settings.hasDirtyAttributes) { - return Promise.resolve(true); - } - - if (!this.confirmModal) { - this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal) - .then((discardChanges) => { - if (discardChanges === true) { - this.settings.rollbackAttributes(); - } - return discardChanges; - }).finally(() => { - this.confirmModal = null; - }); - } - - return this.confirmModal; - } -} diff --git a/ghost/admin/app/routes/settings/code-injection.js b/ghost/admin/app/routes/settings/code-injection.js deleted file mode 100644 index 82717c13630..00000000000 --- a/ghost/admin/app/routes/settings/code-injection.js +++ /dev/null @@ -1,71 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class CodeInjectionRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - return this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - const settings = this.settings; - - if (settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Code injection' - }; - } -} diff --git a/ghost/admin/app/routes/settings/design.js b/ghost/admin/app/routes/settings/design.js deleted file mode 100644 index 1392502a492..00000000000 --- a/ghost/admin/app/routes/settings/design.js +++ /dev/null @@ -1,52 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/authenticated'; -import {inject as service} from '@ember/service'; - -export default class SettingsDesignRoute extends AdminRoute { - @service customThemeSettings; - @service feature; - @service modals; - @service settings; - @service themeManagement; - @service ui; - @service session; - @service store; - - model() { - // background refresh of preview - // not doing it on the 'index' route so that we don't reload going to/from the index, - // any actions performed on child routes that need a refresh should trigger it explicitly - this.themeManagement.updatePreviewHtmlTask.perform(); - - // wait for settings to be loaded - we need the data to be present before display - return Promise.all([ - this.settings.reload(), - this.customThemeSettings.load(), - this.store.findAll('theme') - ]); - } - - beforeModel() { - super.beforeModel(...arguments); - - const user = this.session.user; - - if (!user.isAdmin) { - return this.transitionTo('settings.staff.user', user); - } - } - - activate() { - this.ui.contextualNavMenu = 'design'; - } - - deactivate() { - this.ui.contextualNavMenu = null; - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Design', - mainClasses: ['gh-main-fullwidth'] - }; - } -} diff --git a/ghost/admin/app/routes/settings/design/change-theme.js b/ghost/admin/app/routes/settings/design/change-theme.js deleted file mode 100644 index f218ac04518..00000000000 --- a/ghost/admin/app/routes/settings/design/change-theme.js +++ /dev/null @@ -1,16 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class ChangeThemeRoute extends AdminRoute { - @service store; - - model() { - return this.store.findAll('theme'); - } - - @action - willTransition() { - this.controllerFor('settings.design.change-theme').reset(); - } -} diff --git a/ghost/admin/app/routes/settings/design/change-theme/install.js b/ghost/admin/app/routes/settings/design/change-theme/install.js deleted file mode 100644 index 3d896b387f6..00000000000 --- a/ghost/admin/app/routes/settings/design/change-theme/install.js +++ /dev/null @@ -1,55 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import InstallThemeModal from '../../../../components/modals/design/install-theme'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class InstallThemeRoute extends AdminRoute { - @service modals; - @service router; - - redirect(model, transition) { - const {source, ref} = transition.to.queryParams || {}; - - if (!source || !ref) { - this.transitionTo('settings.design.change-theme'); - } - } - - // use `didTransition` rather than `activate` so that controller setup has completed - @action - didTransition() { - const installController = this.controllerFor('settings.design.change-theme.install'); - const themesController = this.controllerFor('settings.design.change-theme'); - - const theme = themesController.officialThemes.findBy('ref', installController.ref); - - this.installModal = this.modals.open(InstallThemeModal, { - theme, - ref: installController.ref, - onSuccess: () => { - this.showingSuccessModal = true; - this.router.transitionTo('settings.design'); - } - }, { - beforeClose: this.beforeModalClose - }); - } - - deactivate() { - // leave install modal visible if it's in the success state because - // we're switching over to the design customisation screen in the bg - // and don't want to auto-close when this modal closes - if (this.installModal && !this.showingSuccessModal) { - this.installModal.close(); - } - } - - @action - beforeModalClose() { - if (!this.showingSuccessModal) { - this.transitionTo('settings.design.change-theme'); - } - this.showingSuccessModal = false; - this.installModal = null; - } -} diff --git a/ghost/admin/app/routes/settings/design/change-theme/view.js b/ghost/admin/app/routes/settings/design/change-theme/view.js deleted file mode 100644 index 70f8c0ce6cb..00000000000 --- a/ghost/admin/app/routes/settings/design/change-theme/view.js +++ /dev/null @@ -1,49 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ViewThemeModal from 'ghost-admin/components/modals/design/view-theme'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class ViewThemeRoute extends AdminRoute { - @service modals; - - themeModal = null; - - model(params, transition) { - const changeThemeController = this.controllerFor('settings.design.change-theme'); - const knownThemes = changeThemeController.officialThemes; - - const foundTheme = knownThemes.find(theme => theme.name === params.theme_name); - - if (foundTheme) { - return foundTheme; - } - - const path = transition.intent.url.replace(/^\//, ''); - return this.replaceWith('error404', {path, status: 404}); - } - - setupController(controller, model) { - this.themeModal?.close(); - - this.themeModal = this.modals.open(ViewThemeModal, { - theme: model - }, { - beforeClose: this.beforeModalClose - }); - } - - deactivate() { - this.isLeaving = true; - this.themeModal?.close(); - - this.isLeaving = false; - this.themeModal = null; - } - - @action - beforeModalClose() { - if (this.themeModal && !this.isLeaving) { - this.router.transitionTo('settings.design.change-theme'); - } - } -} diff --git a/ghost/admin/app/routes/settings/design/index.js b/ghost/admin/app/routes/settings/design/index.js deleted file mode 100644 index f378f4f92fd..00000000000 --- a/ghost/admin/app/routes/settings/design/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class SettingsDesignIndexRoute extends AuthenticatedRoute { - @service customThemeSettings; - @service modals; - @service settings; - @service store; - - confirmModal = null; - hasConfirmed = false; - themes = this.store.peekAll('theme'); - - afterModel() { - super.afterModel(...arguments); - let activeTheme = this.themes.findBy('active', true); - if (typeof activeTheme === 'undefined') { - return this.transitionTo('settings.design.no-theme'); - } - } - - @action - willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - // always abort when not confirmed because Ember's router doesn't automatically wait on promises - transition.abort(); - - this.confirmUnsavedChanges().then((shouldLeave) => { - if (shouldLeave === true) { - this.hasConfirmed = true; - return transition.retry(); - } - }); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - - this.controllerFor('settings.design.index').reset(); - } - - confirmUnsavedChanges() { - if (!this.settings.hasDirtyAttributes && !this.customThemeSettings.isDirty) { - return Promise.resolve(true); - } - - if (!this.confirmModal) { - this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal) - .then((discardChanges) => { - if (discardChanges === true) { - this.settings.rollbackAttributes(); - this.customThemeSettings.rollback(); - } - return discardChanges; - }).finally(() => { - this.confirmModal = null; - }); - } - - return this.confirmModal; - } -} diff --git a/ghost/admin/app/routes/settings/design/no-theme.js b/ghost/admin/app/routes/settings/design/no-theme.js deleted file mode 100644 index b0913630418..00000000000 --- a/ghost/admin/app/routes/settings/design/no-theme.js +++ /dev/null @@ -1,16 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import {inject as service} from '@ember/service'; - -export default class SettingsDesignNoThemeRoute extends AuthenticatedRoute { - @service store; - - themes = this.store.peekAll('theme'); - - afterModel() { - super.afterModel(...arguments); - let activeTheme = this.themes.findBy('active', true); - if (typeof activeTheme !== 'undefined') { - return this.transitionTo('settings.design.index'); - } - } -} diff --git a/ghost/admin/app/routes/settings/general.js b/ghost/admin/app/routes/settings/general.js deleted file mode 100644 index f23da5cc6d5..00000000000 --- a/ghost/admin/app/routes/settings/general.js +++ /dev/null @@ -1,75 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import RSVP from 'rsvp'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; - -export default class GeneralSettingsRoute extends AdminRoute { - @service modals; - @service settings; - - @inject config; - - model() { - return RSVP.hash({ - settings: this.settings.reload() - }); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - reloadSettings() { - return this.settings.reload(); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - General' - }; - } -} diff --git a/ghost/admin/app/routes/settings/history.js b/ghost/admin/app/routes/settings/history.js deleted file mode 100644 index dc363ad7b77..00000000000 --- a/ghost/admin/app/routes/settings/history.js +++ /dev/null @@ -1,9 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; - -export default class HistoryRoute extends AdminRoute { - buildRouteInfoMetadata() { - return { - titleToken: 'History log' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integration.js b/ghost/admin/app/routes/settings/integration.js deleted file mode 100644 index 9223b720987..00000000000 --- a/ghost/admin/app/routes/settings/integration.js +++ /dev/null @@ -1,78 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class IntegrationRoute extends AdminRoute { - @service modals; - @service router; - - model(params, transition) { - // use the integrations controller to fetch all integrations and pick - // out the one we want. Allows navigation back to integrations screen - // without a loading state - return this - .controllerFor('settings.integrations') - .integrationModelHook('id', params.integration_id, this, transition); - } - - resetController(controller) { - controller.regeneratedApiKey = null; - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.controller.model.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.controller.model?.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Integrations' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integration/webhooks/edit.js b/ghost/admin/app/routes/settings/integration/webhooks/edit.js deleted file mode 100644 index 4d6e9cd48b1..00000000000 --- a/ghost/admin/app/routes/settings/integration/webhooks/edit.js +++ /dev/null @@ -1,49 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import WebhookFormModal from '../../../../components/settings/integrations/webhook-form-modal'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class EditWebhookRoute extends AdminRoute { - @service modals; - @service router; - - webhook = null; - modal = null; - - get integration() { - return this.modelFor('settings.integration'); - } - - model(params) { - return this.integration.webhooks.findBy('id', params.webhook_id); - } - - setupController(controller, model) { - this.webhook = model; - - this.modal = this.modals.open(WebhookFormModal, { - webhook: this.webhook - }, { - beforeClose: this.beforeModalClose - }); - } - - deactivate() { - this.webhook?.errors.clear(); - this.webhook?.rollbackAttributes(); - - // ensure we don't try to redirect on modal close if we're already transitioning away - this.isLeaving = true; - this.modal?.close(); - - this.modal = null; - this.isLeaving = false; - } - - @action - beforeModalClose() { - if (this.modal && !this.isLeaving) { - this.router.transitionTo('settings.integration', this.integration); - } - } -} diff --git a/ghost/admin/app/routes/settings/integration/webhooks/new.js b/ghost/admin/app/routes/settings/integration/webhooks/new.js deleted file mode 100644 index f972b4f1d44..00000000000 --- a/ghost/admin/app/routes/settings/integration/webhooks/new.js +++ /dev/null @@ -1,45 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import WebhookFormModal from '../../../../components/settings/integrations/webhook-form-modal'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class NewWebhookRoute extends AdminRoute { - @service modals; - @service router; - - webhook = null; - modal = null; - - get integration() { - return this.modelFor('settings.integration'); - } - - model() { - this.webhook = this.store.createRecord('webhook', {integration: this.integration}); - - this.modal = this.modals.open(WebhookFormModal, { - webhook: this.webhook - }, { - beforeClose: this.beforeModalClose - }); - } - - deactivate() { - this.webhook?.errors.clear(); - this.webhook?.rollbackAttributes(); - - // ensure we don't try to redirect on modal close if we're already transitioning away - this.isLeaving = true; - this.modal?.close(); - - this.modal = null; - this.isLeaving = false; - } - - @action - beforeModalClose() { - if (this.modal && !this.isLeaving) { - this.router.transitionTo('settings.integration', this.integration); - } - } -} diff --git a/ghost/admin/app/routes/settings/integrations.js b/ghost/admin/app/routes/settings/integrations.js deleted file mode 100644 index 68ba78187c6..00000000000 --- a/ghost/admin/app/routes/settings/integrations.js +++ /dev/null @@ -1,18 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import {inject as service} from '@ember/service'; - -export default class IntegrationsRoute extends AdminRoute { - @service settings; - - setupController(controller) { - // kick off the background fetch of integrations so that we can - // show the screen immediately - controller.fetchIntegrations.perform(); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Integrations' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/amp.js b/ghost/admin/app/routes/settings/integrations/amp.js deleted file mode 100644 index 7d17e828714..00000000000 --- a/ghost/admin/app/routes/settings/integrations/amp.js +++ /dev/null @@ -1,69 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class AMPRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'AMP' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/firstpromoter.js b/ghost/admin/app/routes/settings/integrations/firstpromoter.js deleted file mode 100644 index 573c0fb7683..00000000000 --- a/ghost/admin/app/routes/settings/integrations/firstpromoter.js +++ /dev/null @@ -1,69 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class FirstPromotionIntegrationRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - return this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'FirstPromoter' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/new.js b/ghost/admin/app/routes/settings/integrations/new.js deleted file mode 100644 index d6d65bf49fb..00000000000 --- a/ghost/admin/app/routes/settings/integrations/new.js +++ /dev/null @@ -1,48 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import CustomIntegrationLimitsModal from '../../../components/modals/limits/custom-integration'; -import NewCustomIntegrationModal from '../../../components/modals/new-custom-integration'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class NewIntegrationRoute extends AdminRoute { - @service limit; - @service modals; - @service router; - - modal = null; - - async model() { - if (this.limit.limiter?.isLimited('customIntegrations')) { - try { - await this.limit.limiter.errorIfWouldGoOverLimit('customIntegrations'); - } catch (error) { - this.modal = this.modals.open(CustomIntegrationLimitsModal, { - message: error.message - }, { - beforeClose: this.beforeModalClose - }); - return; - } - } - - this.modal = this.modals.open(NewCustomIntegrationModal, {}, { - beforeClose: this.beforeModalClose - }); - } - - deactivate() { - // ensure we don't try to redirect on modal close if we're already transitioning away - this.isLeaving = true; - this.modal?.close(); - - this.modal = null; - this.isLeaving = false; - } - - @action - beforeModalClose() { - if (this.modal && !this.isLeaving) { - this.router.transitionTo('settings.integrations'); - } - } -} diff --git a/ghost/admin/app/routes/settings/integrations/pintura.js b/ghost/admin/app/routes/settings/integrations/pintura.js deleted file mode 100644 index a00242dfaca..00000000000 --- a/ghost/admin/app/routes/settings/integrations/pintura.js +++ /dev/null @@ -1,69 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class PinturaIntegrationRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - return this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Pintura' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/slack.js b/ghost/admin/app/routes/settings/integrations/slack.js deleted file mode 100644 index 22424c01bb3..00000000000 --- a/ghost/admin/app/routes/settings/integrations/slack.js +++ /dev/null @@ -1,69 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class SlackIntegrationRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Slack' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/unsplash.js b/ghost/admin/app/routes/settings/integrations/unsplash.js deleted file mode 100644 index 96bba29ff09..00000000000 --- a/ghost/admin/app/routes/settings/integrations/unsplash.js +++ /dev/null @@ -1,69 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class UnsplashIntegrationRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - this.settings.reload(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.settings.hasDirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Unsplash' - }; - } -} diff --git a/ghost/admin/app/routes/settings/integrations/zapier.js b/ghost/admin/app/routes/settings/integrations/zapier.js deleted file mode 100644 index f8311f506d9..00000000000 --- a/ghost/admin/app/routes/settings/integrations/zapier.js +++ /dev/null @@ -1,36 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; - -export default class ZapierRoute extends AdminRoute { - @service router; - - @inject config; - - beforeModel() { - super.beforeModel(...arguments); - - if (this.config.hostSettings?.limits?.customIntegrations?.disabled) { - return this.transitionTo('settings.integrations'); - } - } - - model(params, transition) { - // use the integrations controller to fetch all integrations and pick - // out the one we want. Allows navigation back to integrations screen - // without a loading state - return this - .controllerFor('settings.integrations') - .integrationModelHook('slug', 'zapier', this, transition); - } - - resetController(controller) { - controller.regeneratedApiKey = null; - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Zapier' - }; - } -} diff --git a/ghost/admin/app/routes/settings/labs.js b/ghost/admin/app/routes/settings/labs.js deleted file mode 100644 index febc7234af1..00000000000 --- a/ghost/admin/app/routes/settings/labs.js +++ /dev/null @@ -1,23 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import {inject as service} from '@ember/service'; - -export default class LabsRoute extends AdminRoute { - @service settings; - @service notifications; - - model() { - return this.settings.reload(); - } - - resetController(controller, isExiting) { - if (isExiting) { - controller.reset(); - } - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Labs' - }; - } -} diff --git a/ghost/admin/app/routes/settings/labs/import.js b/ghost/admin/app/routes/settings/labs/import.js deleted file mode 100644 index f33f63d0c99..00000000000 --- a/ghost/admin/app/routes/settings/labs/import.js +++ /dev/null @@ -1,25 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ImportContentModal from '../../../components/modal-import-content'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class LabsImportRoute extends AdminRoute { - @service modals; - - importModal = null; - - setupController() { - this.importModal?.close(); - - this.importModal = this.modals.open(ImportContentModal, {}, { - className: 'fullscreen-modal fullscreen-modal-action fullscreen-modal-import-content', - beforeClose: this.beforeModalClose - }); - } - - @action - async beforeModalClose() { - this.router.transitionTo('settings.labs'); - return true; - } -} diff --git a/ghost/admin/app/routes/settings/members-email.js b/ghost/admin/app/routes/settings/members-email.js deleted file mode 100644 index 26d0ef64cb6..00000000000 --- a/ghost/admin/app/routes/settings/members-email.js +++ /dev/null @@ -1,8 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; - -export default class MembersEmailRoute extends AdminRoute { - beforeModel() { - // Moved to newsletters - return this.replaceWith('settings.newsletters'); - } -} diff --git a/ghost/admin/app/routes/settings/membership.js b/ghost/admin/app/routes/settings/membership.js deleted file mode 100644 index 42c6d57a633..00000000000 --- a/ghost/admin/app/routes/settings/membership.js +++ /dev/null @@ -1,98 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import VerifyEmail from '../../components/modals/settings/verify-email'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class MembershipSettingsRoute extends AdminRoute { - @service notifications; - @service settings; - @service modals; - - queryParams = { - verifyEmail: { - replace: true - } - }; - - beforeModel(transition) { - super.beforeModel(...arguments); - - // @todo: remove in the future, but keep it for now because we might still have some old verification urls in emails - if (transition.to.queryParams?.supportAddressUpdate === 'success') { - this.notifications.showAlert( - `Support email address has been updated`, - {type: 'success', key: 'members.settings.support-address.updated'} - ); - } - } - - model() { - this.settings.reload(); - } - - afterModel(model, transition) { - if (transition.to.queryParams.verifyEmail) { - this.modals.open(VerifyEmail, { - token: transition.to.queryParams.verifyEmail - }); - - // clear query param so it doesn't linger and cause problems re-entering route - transition.abort(); - return this.transitionTo('settings.membership', {queryParams: {verifyEmail: null}}); - } - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.controller.reset(); - - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.controller.isDirty) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Membership' - }; - } - - resetController(controller, isExiting) { - if (isExiting) { - controller.reset(); - } - } -} diff --git a/ghost/admin/app/routes/settings/navigation.js b/ghost/admin/app/routes/settings/navigation.js deleted file mode 100644 index 44d28ab2332..00000000000 --- a/ghost/admin/app/routes/settings/navigation.js +++ /dev/null @@ -1,74 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class NavigationRoute extends AdminRoute { - @service modals; - @service settings; - - model() { - this.settings.reload(); - } - - setupController() { - this.controller.reset(); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - this.controller.reset(); - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.settings.rollbackAttributes(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.controller.dirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - save() { - this.controller.send('save'); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Navigation' - }; - } -} diff --git a/ghost/admin/app/routes/settings/newsletters.js b/ghost/admin/app/routes/settings/newsletters.js deleted file mode 100644 index 2c014101815..00000000000 --- a/ghost/admin/app/routes/settings/newsletters.js +++ /dev/null @@ -1,85 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; -import VerifyNewsletterEmail from '../../components/modals/newsletters/verify-newsletter-email'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class MembersEmailLabsRoute extends AdminRoute { - @service feature; - @service modals; - @service notifications; - @service settings; - - queryParams = { - verifyEmail: { - replace: true - } - }; - - confirmModal = null; - hasConfirmed = false; - - model() { - return this.settings.reload(); - } - - afterModel(model, transition) { - if (transition.to.queryParams.verifyEmail) { - this.modals.open(VerifyNewsletterEmail, { - token: transition.to.queryParams.verifyEmail - }); - - // clear query param so it doesn't linger and cause problems re-entering route - transition.abort(); - return this.transitionTo('settings.newsletters', {queryParams: {verifyEmail: null}}); - } - } - - @action - willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - // always abort when not confirmed because Ember's router doesn't automatically wait on promises - transition.abort(); - - this.confirmUnsavedChanges().then((shouldLeave) => { - if (shouldLeave) { - this.hasConfirmed = true; - return transition.retry(); - } - }); - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - } - - confirmUnsavedChanges() { - if (!this.settings.hasDirtyAttributes) { - return Promise.resolve(true); - } - - if (!this.confirmModal) { - this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal) - .then((discardChanges) => { - if (discardChanges === true) { - this.settings.rollbackAttributes(); - } - return discardChanges; - }).finally(() => { - this.confirmModal = null; - }); - } - - return this.confirmModal; - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Settings - Email newsletter' - }; - } -} diff --git a/ghost/admin/app/routes/settings/newsletters/edit-newsletter.js b/ghost/admin/app/routes/settings/newsletters/edit-newsletter.js deleted file mode 100644 index 0d936613154..00000000000 --- a/ghost/admin/app/routes/settings/newsletters/edit-newsletter.js +++ /dev/null @@ -1,97 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import EditNewsletterModal from '../../../components/modals/newsletters/edit'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class EditNewsletterRoute extends AdminRoute { - @service modals; - @service router; - @service store; - - newsletterModal = null; - - model(params) { - return this.store.find('newsletter', params.newsletter_id); - } - - setupController(controller, model) { - this.newsletterModal?.close(); - - this.newsletterModal = this.modals.open(EditNewsletterModal, { - newsletter: model, - afterSave: this.afterSave - }, { - beforeClose: this.beforeModalClose - }); - } - - @action - afterSave() { - this.router.transitionTo('settings.newsletters'); - } - - deactivate() { - this.isLeaving = true; - this.newsletterModal?.close(); - - this.isLeaving = false; - this.newsletterModal = null; - - this.confirmModal = null; - this.hasConfirmed = false; - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - const newsletter = this.newsletterModal?._data.newsletter; - - if (newsletter?.hasDirtyAttributes) { - this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal) - .then((discardChanges) => { - if (discardChanges === true) { - newsletter.rollbackAttributes(); - } - return discardChanges; - }).finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - @action - async beforeModalClose() { - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave && !this.isLeaving) { - this.router.transitionTo('settings.newsletters'); - return true; - } - - return false; - } -} diff --git a/ghost/admin/app/routes/settings/newsletters/new-newsletter.js b/ghost/admin/app/routes/settings/newsletters/new-newsletter.js deleted file mode 100644 index c7e72e402be..00000000000 --- a/ghost/admin/app/routes/settings/newsletters/new-newsletter.js +++ /dev/null @@ -1,67 +0,0 @@ -import AdminRoute from 'ghost-admin/routes/admin'; -import MultipleNewslettersLimitModal from '../../../components/modals/limits/multiple-newsletters'; -import NewNewsletterModal from '../../../components/modals/newsletters/new'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class NewNewsletterRoute extends AdminRoute { - @service modals; - @service router; - @service settings; - @service store; - @service limit; - - newsletterModal = null; - - /** - * Before we allow the creation of a new newsletter, we should check the limits and return to the newsletters page if required. - */ - async beforeModel() { - try { - await this.limit.limiter.errorIfWouldGoOverLimit('newsletters'); - } catch (error) { - if (error.errorType === 'HostLimitError') { - // Not allowed: we reached the limit here - this.modals.open(MultipleNewslettersLimitModal, { - message: error.message - }); - return this.replaceWith('settings.newsletters'); - } - - throw error; - } - } - - model() { - return this.store.createRecord('newsletter'); - } - - setupController(controller, model) { - this.newsletterModal?.close(); - - this.newsletterModal = this.modals.open(NewNewsletterModal, { - newsletter: model, - afterSave: this.afterSave - }, { - beforeClose: this.beforeModalClose - }); - } - - @action - afterSave() { - this.router.transitionTo('settings.newsletters'); - } - - deactivate() { - this.isLeaving = true; - this.newsletterModal?.close(); - this.isLeaving = false; - } - - @action - async beforeModalClose() { - if (!this.isLeaving) { - this.router.transitionTo('settings.newsletters'); - } - } -} diff --git a/ghost/admin/app/routes/settings/staff/index.js b/ghost/admin/app/routes/settings/staff/index.js deleted file mode 100644 index 03445cdd8dc..00000000000 --- a/ghost/admin/app/routes/settings/staff/index.js +++ /dev/null @@ -1,32 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import {inject as service} from '@ember/service'; - -export default class IndexRoute extends AuthenticatedRoute { - @service infinity; - @service session; - - beforeModel() { - super.beforeModel(...arguments); - - const user = this.session.user; - - if (user.isAuthorOrContributor) { - return this.transitionTo('settings.staff.user', user); - } - } - - model() { - return this.session.user; - } - - setupController(controller) { - super.setupController(...arguments); - controller.backgroundUpdateTask.perform(); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Staff' - }; - } -} diff --git a/ghost/admin/app/routes/settings/staff/user.js b/ghost/admin/app/routes/settings/staff/user.js deleted file mode 100644 index 3f2a0ff6e9a..00000000000 --- a/ghost/admin/app/routes/settings/staff/user.js +++ /dev/null @@ -1,101 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class UserRoute extends AuthenticatedRoute { - @service modals; - - model(params) { - return this.store.queryRecord('user', {slug: params.user_slug, include: 'count.posts'}); - } - - afterModel(user) { - super.afterModel(...arguments); - - const currentUser = this.session.user; - - let isOwnProfile = user.id === currentUser.id; - let isAuthorOrContributor = currentUser.isAuthorOrContributor; - let isEditor = currentUser.isEditor; - - if (isAuthorOrContributor && !isOwnProfile) { - this.transitionTo('settings.staff.user', currentUser); - } else if (isEditor && !isOwnProfile && !user.isAuthorOrContributor) { - this.transitionTo('settings.staff'); - } - - if (isOwnProfile) { - this.store.queryRecord('api-key', {id: 'me'}).then((apiKey) => { - this.controller.personalToken = apiKey.id + ':' + apiKey.secret; - this.controller.personalTokenRegenerated = false; - }); - } - } - - serialize(model) { - return {user_slug: model.get('slug')}; - } - - setupController(controller, model) { - controller.model = model; - controller.reset(); - } - - @action - async willTransition(transition) { - if (this.hasConfirmed) { - return true; - } - - transition.abort(); - - // wait for any existing confirm modal to be closed before allowing transition - if (this.confirmModal) { - return; - } - - if (this.controller.saveTask?.isRunning) { - await this.controller.saveTask.last; - } - - const shouldLeave = await this.confirmUnsavedChanges(); - - if (shouldLeave) { - this.controller.reset(); - this.hasConfirmed = true; - return transition.retry(); - } - } - - async confirmUnsavedChanges() { - if (this.controller.model.hasDirtyAttributes || this.controller.dirtyAttributes) { - this.confirmModal = this.modals - .open(ConfirmUnsavedChangesModal) - .finally(() => { - this.confirmModal = null; - }); - - return this.confirmModal; - } - - return true; - } - - deactivate() { - this.confirmModal = null; - this.hasConfirmed = false; - this.controller.reset(); - } - - @action - didTransition() { - this.modelFor('settings.staff.user').get('errors').clear(); - } - - buildRouteInfoMetadata() { - return { - titleToken: 'Staff - User' - }; - } -} diff --git a/ghost/admin/app/routes/settings/theme-install.js b/ghost/admin/app/routes/settings/theme-install.js deleted file mode 100644 index ad53daa2eb1..00000000000 --- a/ghost/admin/app/routes/settings/theme-install.js +++ /dev/null @@ -1,7 +0,0 @@ -import Route from '@ember/routing/route'; - -export default class InstallThemeRoute extends Route { - redirect(model, transition) { - this.transitionTo('settings.design.change-theme.install', {queryParams: transition.to.queryParams}); - } -} diff --git a/ghost/admin/app/services/ajax.js b/ghost/admin/app/services/ajax.js index aec7a81c48d..402fe1f7970 100644 --- a/ghost/admin/app/services/ajax.js +++ b/ghost/admin/app/services/ajax.js @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/ember'; import AjaxService from 'ember-ajax/services/ajax'; import classic from 'ember-classic-decorator'; import config from 'ghost-admin/config/environment'; @@ -5,7 +6,6 @@ import moment from 'moment-timezone'; import semverCoerce from 'semver/functions/coerce'; import semverLt from 'semver/functions/lt'; import {AjaxError, isAjaxError, isForbiddenError} from 'ember-ajax/errors'; -import {captureMessage} from '@sentry/ember'; import {get} from '@ember/object'; import {inject} from 'ghost-admin/decorators/inject'; import {isArray as isEmberArray} from '@ember/array'; @@ -43,7 +43,7 @@ export function isVersionMismatchError(errorOrStatus, payload) { export class DataImportError extends AjaxError { constructor(payload) { - super(payload, 'he server encountered an error whilst importing data.'); + super(payload, 'The server encountered an error whilst importing data.'); } } @@ -271,7 +271,7 @@ class ajaxService extends AjaxService { success = true; if (attempts !== 0 && this.config.sentry_dsn) { - captureMessage('Request took multiple attempts', {extra: getErrorData()}); + Sentry.captureMessage('Request took multiple attempts', {extra: getErrorData()}); } return result; @@ -289,7 +289,7 @@ class ajaxService extends AjaxService { await timeout(retryPeriods[attempts] || retryPeriods[retryPeriods.length - 1]); attempts += 1; } else if (attempts > 0 && this.config.sentry_dsn) { - captureMessage('Request failed after multiple attempts', {extra: getErrorData()}); + Sentry.captureMessage('Request failed after multiple attempts', {extra: getErrorData()}); throw error; } else { throw error; @@ -299,6 +299,16 @@ class ajaxService extends AjaxService { } handleResponse(status, headers, payload, request) { + // set some context variables for Sentry in case there is an error + Sentry.setContext('ajax', { + url: request.url, + method: request.method, + status + }); + Sentry.setTag('ajax_status', status); + Sentry.setTag('ajax_url', request.url); + Sentry.setTag('ajax_method', request.method); + if (headers['content-version']) { const contentVersion = semverCoerce(headers['content-version']); const appVersion = semverCoerce(config.APP.version); diff --git a/ghost/admin/app/services/dashboard-stats.js b/ghost/admin/app/services/dashboard-stats.js index 7ab37b65f03..f1b2b384f9a 100644 --- a/ghost/admin/app/services/dashboard-stats.js +++ b/ghost/admin/app/services/dashboard-stats.js @@ -57,8 +57,8 @@ import {tracked} from '@glimmer/tracking'; /** * @typedef PaidMembersByCadence * @type {Object} - * @property {number} year Paid memebrs on annual plan - * @property {number} month Paid memebrs on monthly plan + * @property {number} year Paid members on annual plan + * @property {number} month Paid members on monthly plan */ /** diff --git a/ghost/admin/app/services/notifications.js b/ghost/admin/app/services/notifications.js index b0d9e31073f..2393826a54d 100644 --- a/ghost/admin/app/services/notifications.js +++ b/ghost/admin/app/services/notifications.js @@ -34,6 +34,8 @@ const GENERIC_ERROR_NAMES = [ 'SyntaxError', 'TypeError', 'URIError', + // ember-ajax errors - https://github.com/ember-cli/ember-ajax/blob/master/addon/errors.ts + 'AjaxError', 'ServerError' ]; diff --git a/ghost/admin/app/services/ui.js b/ghost/admin/app/services/ui.js index 0af3285704a..f291d0830eb 100644 --- a/ghost/admin/app/services/ui.js +++ b/ghost/admin/app/services/ui.js @@ -48,7 +48,6 @@ export default class UiService extends Service { @inject config; - @tracked contextualNavMenu = null; @tracked isFullScreen = false; @tracked mainClass = ''; @tracked showMobileMenu = false; diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index 18342e2cc80..1faed6eb5b0 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -42,6 +42,8 @@ @import "components/stacks.css"; @import "components/browser-preview.css"; @import "components/filter-builder.css"; +@import "components/theme-errors.css"; +@import "components/modal-about.css"; /* Layouts @@ -51,19 +53,12 @@ @import "layouts/auth.css"; @import "layouts/content.css"; @import "layouts/editor.css"; -@import "layouts/settings.css"; -@import "layouts/users.css"; -@import "layouts/user.css"; @import "layouts/whatsnew.css"; @import "layouts/tags.css"; @import "layouts/members.css"; @import "layouts/member-activity.css"; @import "layouts/error.css"; -@import "layouts/apps.css"; -@import "layouts/packages.css"; -@import "layouts/labs.css"; @import "layouts/preview-email.css"; -@import "layouts/portal-settings.css"; @import "layouts/billing.css"; @import "layouts/post-history.css"; @import "layouts/post-preview.css"; @@ -527,7 +522,6 @@ input:focus, } .gh-about-logo svg, -.apps-card-app-orb, .gh-nav-logo-default, .gh-unsplash-logo { filter: invert(100%) brightness(150%); @@ -553,10 +547,6 @@ input:focus, background: #fff; } -.user-cover-edit { - color: #fff; -} - .gh-unsplash-search:focus { background-color: color-mod(var(--lightgrey)); } @@ -583,33 +573,6 @@ input:focus, color: var(--darkgrey); } -.apps-grid, -.apps-grid-cell { - background: var(--dark-main-bg-color); -} - -.td-item-overlay:hover, -.td-item-overlay:focus { - background-color: var(--black-90); -} - -.td-item-empty { - background: var(--whitegrey-l1); -} - -.gh-themes-container { - background: var(--whitegrey-l2); -} - -.gh-themes-container .apps-grid { - background: none; -} - -.gh-theme-directory-footer { - color: var(--darkgrey); - background-color: var(--whitegrey); -} - .settings-code-editor .CodeMirror-gutters, .settings-code code, .form-group code { @@ -634,26 +597,6 @@ input:focus, border-color: var(--midgrey); } -.gh-branding-settings-right { - background: var(--dark-main-bg-color); -} - -.gh-branding-settings-header { - border-bottom: 1px solid var(--hairline-color-1); -} - -.gh-branding-settings-options { - border-right: 1px solid var(--hairline-color-1); -} - -.gh-branding-image-container.transparent-bg { - background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3ERectangle%3C/title%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23303e46' d='M0 0h24v24H0z'/%3E%3Cpath fill='%233e515b' d='M0 0h12v12H0zM12 12h12v12H12z'/%3E%3C/g%3E%3C/svg%3E"); -} - -.id-item { - background: var(--dark-main-bg-color); -} - .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) { background: rgba(255, 0, 0, .30); } @@ -678,14 +621,6 @@ input:focus, color: color-mod(#5A5CAD l(+20%)); } -.id-github img, -.id-typeform img, -.id-buffer img, -.id-unsplash, -.id-more img { - filter: invert(100%); -} - .dark-no-shadow { box-shadow: none; } @@ -718,29 +653,12 @@ input:focus, background: var(--lightgrey); } -.apps-grid-cell:hover { - background: var(--whitegrey); -} - -.apps-card-app { - border-bottom: var(--hairline-color-1) 1px solid; -} - -.apps-card-app-icon { - mix-blend-mode: normal; -} - -.apps-card-app-icon.id-typeform { - filter: invert(100%); -} - .gh-nav, .gh-contentfilter-menu-trigger, .gh-contentfilter-menu-trigger--active, .gh-contentfilter-menu-trigger:focus, .tags-container, -.content-list ol, -.gh-settings-main-grid { +.content-list ol { background: var(--dark-main-bg-color); } @@ -788,14 +706,6 @@ input:focus, background-color: transparent; } -.gh-setting-header { - border-color: var(--hairline-color-1); -} - -.gh-settings-main-grid .gh-setting-group span { - background: color-mod(var(--dark-main-bg-color) l(+2%)); -} - .gh-referral-toast-close:hover { color: #fff; } @@ -979,7 +889,6 @@ input:focus, background: none !important; } -.modal-fullsettings-preview-container, .fullscreen-modal-email-preview .gh-pe-mobile-container, .fullscreen-modal-email-preview .gh-pe-desktop-container{ background: var(--dark-main-bg-color); @@ -991,10 +900,6 @@ input:focus, background: transparent; } -.modal-fullsettings-main { - border-left: 1px solid var(--hairline-color-1); -} - .gh-pe-mobile-bezel { background: var(--lightgrey-l2); } @@ -1007,11 +912,6 @@ input:focus, border: 1px solid var(--hairline-color-1); } -.gh-members-connect-testmodelabel { - background: #983705; - color: #f8e5b9; -} - .members-list .gh-list-header { background: var(--dark-main-bg-color); } @@ -1136,10 +1036,6 @@ input:focus, background: rgb(55, 59, 67, 0.99); } -.user-image { - border-color: var(--dark-main-bg-color); -} - .gh-whats-new-badge-account { top: 0; right: -2px; @@ -1326,18 +1222,6 @@ kbd { color: #4d4f52; } -.gh-input-group-tier-trial-disabled .gh-input-append { - background: #0e0f11; - color: #4c4f52; - border-color: #17191c; - opacity: 1; -} - -.gh-input-group-tier-trial-disabled .gh-input-append:before { - background: #0e0f11; -} - - /* Settings Links */ .kg-settings-link-url::before { @@ -1476,9 +1360,3 @@ kbd { .gh-update-banner { background: var(--main-color-content-greybg); } - -/* Staff */ -.gh-roles-container .popover { - background: var(--dark-main-bg-color); - box-shadow: 0 0 1px rgba(0,0,0,.65), 0 8px 28px rgba(0,0,0,.72); -} diff --git a/ghost/admin/app/styles/app.css b/ghost/admin/app/styles/app.css index 1d8de82c86c..e4baaa03aee 100644 --- a/ghost/admin/app/styles/app.css +++ b/ghost/admin/app/styles/app.css @@ -44,6 +44,8 @@ @import "components/browser-preview.css"; @import "components/filter-builder.css"; @import "components/pintura.css"; +@import "components/theme-errors.css"; +@import "components/modal-about.css"; /* Layouts @@ -53,20 +55,13 @@ @import "layouts/auth.css"; @import "layouts/content.css"; @import "layouts/editor.css"; -@import "layouts/settings.css"; -@import "layouts/users.css"; -@import "layouts/user.css"; @import "layouts/whatsnew.css"; @import "layouts/tags.css"; @import "layouts/members.css"; @import "layouts/posts.css"; @import "layouts/member-activity.css"; @import "layouts/error.css"; -@import "layouts/apps.css"; -@import "layouts/packages.css"; -@import "layouts/labs.css"; @import "layouts/preview-email.css"; -@import "layouts/portal-settings.css"; @import "layouts/billing.css"; @import "layouts/post-history.css"; @import "layouts/post-preview.css"; diff --git a/ghost/admin/app/styles/components/modal-about.css b/ghost/admin/app/styles/components/modal-about.css new file mode 100644 index 00000000000..ae20aa34938 --- /dev/null +++ b/ghost/admin/app/styles/components/modal-about.css @@ -0,0 +1,152 @@ +.gh-about-logo svg { + position: relative; + width: 120px; + height: auto; +} + +.gh-about-logo { + border-bottom: 1px solid var(--lightgrey-l2); + padding-bottom: 10px; + margin-bottom: 16px; +} + +.gh-about-modal .gh-about-logo { + margin: 4px 0 20px; + border-bottom: none; + padding-bottom: 0; +} + +.gh-about-container { + display: grid; + grid-template-columns: 2fr 1fr; + grid-gap: 80px; +} + +.gh-whats-new-canvas .gh-about-container { + display: flex; + grid-template-columns: unset; + grid-gap: unset; + margin: 0 auto; + max-width: 920px; + margin-top: 60px; +} + +.gh-about-box { + position: sticky; + top: 96px; + right: 0; + display: flex; + flex-grow: 1; + flex-direction: column; + height: max-content; + border-radius: 3px; + min-width: 300px; +} + +.gh-about-box.grey { + border: none; + background: var(--main-color-content-greybg); +} + +.gh-env-details { + display: flex; + flex-grow: 1; + flex-direction: column; + padding: 24px 28px 28px; +} + +.gh-about-container h2 { + font-size: 1.65rem; + line-height: 1.4em; + font-weight: 600; + border-bottom: 1px solid var(--lightgrey-l2); + padding-bottom: 12px; + margin-bottom: 12px; +} + +.gh-env-list { + margin: 0; + padding: 0; + list-style: none; +} + +.gh-env-list li { + margin: 0 0 4px; + font-size: 1.4rem; + line-height: 1.5em; +} + +.gh-env-error { + margin: 1.2rem 0; + padding: 16px; + line-height: 1.4em; + border: none; + background: color-mod(var(--red) a(6%)); + border-radius: 3px; +} + +.gh-env-error a { + color: var(--red); +} + +.gh-env-help { + max-width: 200px; +} + +.gh-env-help .gh-btn { + margin: 4px 0; +} + +@media (max-width: 670px) { + .gh-env-details { + flex-direction: column; + } + .gh-env-help { + margin: 1em 0; + max-width: none; + } + .gh-env-help .gh-btn { + display: inline-block; + } +} + +.gh-about-content-actions { + display: none; +} + + + +/* Upgrade +/* ---------------------------------------------------------- */ + +.gh-upgrade-notification { + padding-top: 1em; +} + +.gh-upgrade-notification a { + text-decoration: underline; +} + +.gh-about-modal .gh-upgrade-notification { + background: color-mod(var(--green) a(8%)); + padding: 24px 28px 24px; + border-radius: 3px; + margin-bottom: 28px; +} + +/* Copyright Info +/* ---------------------------------------------------------- */ + +.gh-copyright-info { + color: var(--midgrey); + font-size: 1.3rem; + border-top: 1px solid var(--lightgrey-l2); + padding-top: 16px; + margin-top: 16px; + line-height: 1.45em; +} + +.gh-about-modal .gh-copyright-info { + margin: 4px 0 8px; + border-top: none; +} diff --git a/ghost/admin/app/styles/components/modals.css b/ghost/admin/app/styles/components/modals.css index f8fe2c12f9d..020294a2fb6 100644 --- a/ghost/admin/app/styles/components/modals.css +++ b/ghost/admin/app/styles/components/modals.css @@ -262,285 +262,6 @@ background: var(--whitegrey-l2); } - - -/* Full screen setting modal with preview. Used in e.g. Portal -/* settings, Email design settings etc. -/* ---------------------------------------------------------- */ -.modal-fullsettings { - height: 100%; - display: flex; - flex-direction: column; -} - -.modal-fullsettings-body { - display: flex; - padding: 0; - flex-grow: 1; - overflow: hidden; -} - -.modal-fullsettings-body .form-group.space-l { - margin-bottom: 1.9em; -} - -.modal-fullsettings-body .for-switch.small { - width: 36px !important; - height: 22px !important; -} - -.modal-fullsettings-body .gh-select svg { - top: 19px; - right: 9px; -} - -.modal-fullsettings-body .modal-footer { - margin-top: 28px; -} - -.modal-fullsettings-sidebar { - display: flex; - flex-direction: column; - padding: 0px 24px 20px; - width: 420px; - overflow-y: auto; -} - -.modal-fullsettings-sidebar.with-footer { - justify-content: space-between; -} - -.modal-fullsettings-topbar { - height: 66px; - padding: 0 24px; - border-bottom: 1px solid var(--whitegrey); -} - -.modal-fullsettings-heading { - display: flex; - align-items: center; - height: 66px; - font-size: 1.9rem; - font-weight: 600; - padding: 0 24px; - margin: 0 -24px 1px; -} - -.modal-fullsettings-form { - min-width: 292px; -} - -.modal-fullsettings-section { - margin: 24px -24px; - padding: 0 24px; -} - -.modal-fullsettings-section.divider-top { - border-top: 1px solid var(--whitegrey); - padding-top: 24px; -} - -.modal-fullsettings-sectionheading { - font-size: 1.2rem; - font-weight: 500; - color: var(--midlightgrey); - margin: 0 0 12px; - text-transform: uppercase; - letter-spacing: 0.2px; -} - -.modal-fullsettings-section .form-group { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0; - margin-bottom: 20px; -} - -.modal-fullsettings-section .form-group > p { - font-size: 1.25rem !important; - line-height: 1.4em; -} - -.modal-fullsettings-section .form-group.vertical { - display: block; -} - -.modal-fullsettings-section .form-group.vertical h4 { - margin-bottom: 8px; -} - -.modal-fullsettings-section .form-group.vertical p { - margin-top: 8px; -} - -.modal-fullsettings-section .gh-select select, -.modal-fullsettings-section textarea { - font-size: 1.4rem; -} - -.modal-fullsettings-radiogroup { - margin: 0; -} - -.modal-fullsettings-radiogroup .gh-radio { - margin-bottom: 14px; -} - -.modal-fullsettings-radiogroup .gh-radio:last-of-type { - margin-bottom: 12px; -} - -.modal-fullsettings-radiogroup + p { - margin-top: 4px !important; - margin-bottom: 28px; -} - -.modal-fullsettings-title { - font-size: 1.3rem; - font-weight: 600; - margin: 0 12px 0 0; -} - -.modal-fullsettings-title.disabled { - opacity: .5; -} - -.modal-fullsettings-uploader { - display: flex; - justify-content: space-between; - align-items: center; - margin: 18px 0 0; - border-radius: 3px; -} - -.gh-header-img-container { - display: flex; - justify-content: center; - align-items: center; - width: 64px; - height: 44px; -} - -.gh-header-img-container .gh-loading-spinner { - width: 20px; - height: 20px; -} - -.gh-header-img-uploadicon, -.gh-header-img-uploadicon:hover, -.gh-header-img-uploadicon:focus { - width: 64px; - height: 44px; - border: 1px dashed var(--lightgrey); - background: transparent; - box-shadow: none; -} - -.gh-header-img-uploadicon span { - display: flex; - justify-content: center; - align-items: center; -} - -.gh-header-img-uploadicon span svg { - width: 18px; - height: 18px; - fill: var(--black); -} - -.gh-header-img-uploadicon:hover span svg { - fill: var(--green-d1); -} - -.gh-header-img { - height: 44px; -} - -.gh-header-img-thumbnail { - display: inline-block; - width: 64px; - height: 44px; - border: 1px solid var(--whitegrey); - cursor: pointer; - background-position: center; - object-fit: cover; - border-radius: 3px; -} - -.gh-header-img-thumbnail svg path { - stroke: var(--midlightgrey-d1); -} - -.gh-header-img-deleteicon { - position: absolute; - right: 0; - width:64px; - height: 44px; - background: var(--black) !important; - opacity: 0; - color: var(--whitegrey-d1); -} - -.gh-header-img-deleteicon:hover { - opacity: 1; - color: var(--whitegrey-d1); -} - -.gh-header-img-deleteicon span { - display: flex; - justify-content: center; - align-items: center; -} - -.gh-header-img-deleteicon span svg { - width: 18px; - height: 18px; -} - -.modal-fullsettings-uploader h4 { - margin: 0 !important; - padding: 0; - font-size: 1.3rem; - line-height: 1.65em; -} - -.modal-fullsettings-uploader p { - margin: 0 !important; - padding: 0; - font-size: 1.2rem !important; -} - -.modal-fullsettings-main { - display: flex; - flex-direction: column; - flex-grow: 1; - padding: 0; - border-left: 1px solid var(--whitegrey); -} - -.modal-fullsettings-preview-container { - overflow: hidden; - background: var(--whitegrey-l1); - height: 100vh; - overflow-y: scroll; -} - -.modal-fullsettings-preview-hidescrollbar { - overflow: hidden; - height: 100vh; - background: var(--whitegrey-l1); - border: 1px solid var(--whitegrey); - border-radius: 5px; -} - -.modal-fullsettings-preview-hidescrollbar .modal-fullsettings-preview-container { - border: none; - border-radius: 0; - margin: 0 -50px; - padding: 0 50px; -} - /* Content Modifiers /* ---------------------------------------------------------- */ @@ -596,186 +317,6 @@ } } -/* Fullscreen Modal Labs -/* ---------------------------------------------------------- */ - -.modal-fullsettings-sidebar-labs { - display: flex; - flex-direction: column; - width: 400px; - overflow-y: auto; -} - -.modal-fullsettings-heading-labs { - display: flex; - align-items: center; - margin: 0; - padding: 32px; - font-size: 2rem; - font-weight: 600; -} - -.modal-fullsettings-body-labs { - display: flex; - flex-direction: column; - flex-grow: 1; - justify-content: space-between; - overflow: hidden; - overflow-y: auto; - overflow-x: hidden; -} - -.modal-fullsettings-section-labs { - display: flex; - flex-direction: column; - justify-content: space-between; - flex-grow: 1; - padding: 0; - margin-bottom: 24px; -} - -.modal-fullsettings-section-title { - margin: 0 16px 4px; - padding: 8px 16px; - color: var(--black); - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; - line-height: 1.3em; -} - -.modal-fullsettings-tab { - display: flex; - flex-grow: 1; - position: relative; - align-items: center; - box-sizing: border-box; - padding: 7px var(--main-layout-area-padding); - color: var(--darkgrey-l1); - font-weight: 400; - font-size: 1.45rem; - transition: none; - z-index: 0; -} - -.modal-fullsettings-tab:hover { - color: var(--black); -} - -.modal-fullsettings-tab.active { - color: var(--black); - font-weight: 400; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.modal-fullsettings-tab:not(.active):hover { - background: var(--mainmenu-color-hover-bg); -} - -.modal-fullsettings-form-labs .modal-fullsettings-tab svg { - margin-right: 17px; - width: 16px; - height: 16px; - line-height: 1; - transition: none; - z-index: 999; -} - -.modal-fullsettings-tab-expanded { - margin: 8px 0 24px; - padding: 24px var(--main-layout-area-padding) 16px; - background: var(--mainmenu-color-hover-bg); -} - -.modal-fullsettings-tab-expanded .gh-setting { - padding: 12px 0; -} - -.modal-fullsettings-tab-expanded .gh-setting.gh-setting-extra { - padding-bottom: 24px; -} - -.modal-fullsettings-tab-expanded .gh-setting-first { - padding-top: 0; -} - -.modal-fullsettings-form-labs .for-checkbox .input-toggle-component { - background: var(--white); -} - -.modal-fullsettings-form-labs .gh-nav-button-expand { - position: relative; - top: inherit; - left: inherit; - margin: 0 8px 0 auto; - padding-top: 3px; -} - -.modal-fullsettings-form-labs .gh-nav-button-expand svg { - margin-right: 0; -} - -.modal-fullsettings-main-topbar-labs { - display: flex; - align-items: center; - justify-content: flex-end; - position: relative; - width: 100%; - height: 90px; - padding: 2.4rem; -} - -.modal-fullsettings-bottom { - position: sticky; - -webkit-position: sticky; - bottom: -24px; - z-index: 9997; - height: 164px; - -webkit-backface-visibility: hidden; -} - -.modal-fullsettings-bottom::before, -.modal-fullsettings-bottom::after { - content: ""; - position: sticky; - -webkit-position: sticky; - display: block; - height: 24px; -} - -.modal-fullsettings-bottom::before { - z-index: 9998; - bottom: 0; - background: var(--white); -} - -.modal-fullsettings-bottom::after { - bottom: 116px; - box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 -8px 16px -3px rgba(0,0,0,.15); -} - -.modal-fullsettings-footer { - position: sticky; - -webkit-position: sticky; - bottom: 0; - z-index: 9999; - display: flex; - align-items: center; - height: 140px; - margin-bottom: -24px; - padding: var(--main-layout-area-padding); - background: var(--white); -} - -.modal-fullsettings-footer .form-group { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: 0; - margin-bottom: 0; -} - - /* Re-authentication modal (cookies expired with unsaved changes in editor) /* ---------------------------------------------------------- */ diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index 4df322891fa..c2f3cd4dfde 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -9,7 +9,7 @@ border: var(--input-border); } -.ember-power-select-trigger:not(.gh-setting-dropdown):not(.ember-power-select-multiple-trigger):not(.gh-preview-newsletter-trigger) svg { +.ember-power-select-trigger:not(.ember-power-select-multiple-trigger):not(.gh-preview-newsletter-trigger) svg { height: 4px; width: 6.11px; margin-left: 2px; @@ -17,7 +17,7 @@ vertical-align: middle; } -.ember-power-select-trigger:not(.gh-setting-dropdown):not(.ember-power-select-multiple-trigger) svg path { +.ember-power-select-trigger:not(.ember-power-select-multiple-trigger) svg path { stroke: var(--darkgrey); } diff --git a/ghost/admin/app/styles/components/settings-menu.css b/ghost/admin/app/styles/components/settings-menu.css index cbba8de544c..29f3440976c 100644 --- a/ghost/admin/app/styles/components/settings-menu.css +++ b/ghost/admin/app/styles/components/settings-menu.css @@ -535,13 +535,6 @@ li.nav-list-item .for-switch.x-small label { transform: translate3d(205px, 0px, 0px); } - -/* Badges -/* ---------------------------------------------------------- */ -.gh-setting-title .gh-badge { - font-size: 13px; -} - /* Email /* ---------------------------------------------------------- */ .settings-menu-email-button span { diff --git a/ghost/admin/app/styles/components/stacks.css b/ghost/admin/app/styles/components/stacks.css index 4f184d0e529..1c726fabb3f 100644 --- a/ghost/admin/app/styles/components/stacks.css +++ b/ghost/admin/app/styles/components/stacks.css @@ -1,4 +1,4 @@ -/* Lists that are open on the sides +/* Lists that are open on the sides /* ------------------------------------------------- */ .gh-stack { display: flex; @@ -19,10 +19,6 @@ justify-content: space-between; } -.gh-stack-item .gh-setting-content { - margin-right: 24px; -} - .gh-stack-item.row label { margin-bottom: 0; -} \ No newline at end of file +} diff --git a/ghost/admin/app/styles/components/theme-errors.css b/ghost/admin/app/styles/components/theme-errors.css new file mode 100644 index 00000000000..29d4b6c01e7 --- /dev/null +++ b/ghost/admin/app/styles/components/theme-errors.css @@ -0,0 +1,148 @@ +/*Errors */ +.theme-validation-container { + overflow-y: auto; + margin: -32px -32px 0; + padding: 32px 32px 0; + max-height: calc(100vh - 20vw); +} + +@media (max-height: 960px) { + .theme-validation-container { + max-height: calc(100vh - 180px); + } +} + +.theme-validation-container .gh-image-uploader { + justify-content: center; +} + +.theme-validation-container .gh-image-uploader .description { + color: var(--green-d1); + font-weight: 500; +} + +.theme-validation-container .gh-image-uploader .x-file-input.try-again, +.theme-validation-container .gh-image-uploader .x-file-input.try-again label { + display: inline; +} + +.theme-validation-item { + margin: 12px 0 0; + padding: 12px 16px 12px 28px; + border: 1px solid #e5eff5; + border-radius: 5px; + display: flex; + flex-direction: column; + border: 1px solid var(--lightgrey); +} + +.theme-validation-item h4 { + margin: 0; + font-size: 1.4rem; + font-weight: 400; + line-height: 1.5em; +} + +.theme-validation-rule-text { + flex-grow: 1; +} + +.theme-validation-item.theme-fatal-error { + border: 1px solid var(--red); +} + +.theme-validation-item.theme-fatal-error .theme-validation-rule-text::before, +.theme-validation-item.theme-error .theme-validation-rule-text::before, +.theme-validation-item.theme-warning .theme-validation-rule-text::before +{ + font-weight: 600; +} + +.theme-validation-item.theme-fatal-error .theme-validation-rule-text::before { + content: "Fatal error:"; + color: var(--red); +} + +.theme-validation-item.theme-error .theme-validation-rule-text::before { + content: "Error:"; +} + +.theme-validation-item.theme-warning .theme-validation-rule-text::before { + content: "Warning:"; +} + +.theme-fatal-error .theme-validation-type-label::before, +.theme-error .theme-validation-type-label::before, +.theme-warning .theme-validation-type-label::before { + content: ""; + display: block; + width: 8px; + height: 8px; + margin-top: 6px; + margin-left: -16px; + border-radius: 999px; +} + +.theme-fatal-error .theme-validation-type-label::before, +.theme-error .theme-validation-type-label::before { + background: color-mod(var(--red) alpha(0.85)); +} + +.theme-warning .theme-validation-type-label::before { + background: color-mod(var(--yellow)); +} + +.theme-validation-list ul { + list-style: disc; +} + +.theme-validation-list code, +.theme-validation-rule-text code { + font-size: 0.9em; +} + +.theme-validation-item h6 { + font-size: 1.3rem; + font-weight: 500; +} + +.theme-validation-toggle-details { + display: flex; + justify-content: space-between; + flex-grow: 1; + align-items: flex-start; + padding: 0; + color: var(--darkgrey); + text-decoration: none!important; + font-size: 1.3rem; +} + +.theme-validation-rule-icon { + flex-shrink: 0; + margin-left: 5px; + width: 13px; + height: 14px; + color: var(--midgrey); + transition: all 0.1s ease-out; +} + +.theme-validation-rule-icon svg path { + fill: var(--midgrey); +} + +.theme-validation-details { + margin-top: 12px; + padding-top: 12px; + font-size: 1.3rem; + border-top: 1px solid var(--lightgrey); +} + +p.theme-validation-details { + font-size: 1.3rem; +} + +.theme-validation-screenshot img { + margin-bottom: 2rem; + border: 1px solid var(--main-color-area-divider); + border-radius: 3px; +} diff --git a/ghost/admin/app/styles/components/uploader.css b/ghost/admin/app/styles/components/uploader.css index a52217e224c..e220e34fe0f 100644 --- a/ghost/admin/app/styles/components/uploader.css +++ b/ghost/admin/app/styles/components/uploader.css @@ -130,7 +130,7 @@ color: color-mod(var(--midgrey)); } -/* TODO: remove the gh-image-uploader classes once it's using gh-progrss-bar */ +/* TODO: remove the gh-image-uploader classes once it's using gh-progress-bar */ .gh-image-uploader .progress-container, .gh-progress-container { flex-grow: 1; diff --git a/ghost/admin/app/styles/layouts/apps.css b/ghost/admin/app/styles/layouts/apps.css deleted file mode 100644 index ec393c60d95..00000000000 --- a/ghost/admin/app/styles/layouts/apps.css +++ /dev/null @@ -1,645 +0,0 @@ -/* Apps -/* ---------------------------------------------------------- */ - -.apps-filter { - border-radius: 5px; -} -@media (max-width: 1460px) { - .apps-filter { - max-width: 700px; - } -} - -/* Main Layout -/* ---------------------------------------------------------- */ - -.integrations-directory { - display: grid; - justify-content: space-between; - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - grid-gap: 25px; - background: var(--whitegrey-l1); - padding: 24px; - border-radius: 3px; -} - -.id-item { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: 100px; - padding: 10px; - text-align: center; - text-decoration: none; - color: var(--darkgrey); - border-radius: 5px; - box-shadow: 0 3px 6px -2px rgba(0,0,0,.1); - background: var(--white); - transition: all .5s ease; -} - -.id-item:hover { - transform: translateY(-2.5%); - box-shadow: 0 0 1px rgba(0,0,0,.02), 0 8px 26px -4px rgba(0,0,0,.08); - transition: all .3s ease; -} - -.id-item-logo { - display: flex; - justify-content: center; - align-items: center; - height: 38px; - width: 38px; - margin-top: 4px; -} - -.id-more svg circle { - stroke: var(--midlightgrey); -} - -@media (max-width: 1320px) { - .integrations-directory { - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr; - } - .id-item:nth-child(7) { - display: none; - } -} -@media (max-width: 1160px) { - .integrations-directory { - grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; - } - .id-item:nth-child(6) { - display: none; - } -} -@media (max-width: 1020px) { - .integrations-directory { - grid-template-columns: 1fr 1fr 1fr 1fr 1fr; - } - .id-item:nth-child(5) { - display: none; - } -} -@media (max-width: 900px) { - .integrations-directory { - grid-template-columns: 1fr 1fr 1fr 1fr; - } - .id-item:nth-child(5), - .id-item:nth-child(6), - .id-item:nth-child(7) { - display: flex; - } -} - -@media (max-width: 500px) { - .integrations-directory { - grid-template-columns: 1fr 1fr 1fr; - } - .id-item:nth-child(6), - .id-item:nth-child(7) { - display: none; - } -} - -/* Built-in integrations */ -.apps-first-header, -.apps-first-header .apps-grid-title { - margin-top: 0px; -} - -.apps-grid { - display: flex; - flex-flow: row wrap; - align-items: flex-start; -} - -.apps-grid-note { - display: block; - margin-top: 5px; - color: var(--midgrey); - font-size: 1.2rem; - font-style: italic; -} - -/* Apps Card -/* ---------------------------------------------------------- */ - -.apps-grid-cell { - flex: 1 1 100%; - background: var(--white); - margin: 0; -} - -.apps-grid-cell { - transition: background 0.3s ease; -} - -.apps-grid-cell:hover { - background: var(--whitegrey-l2); - transition: none; -} - -.apps-card-app { - display: flex; - align-items: center; - justify-content: space-between; - overflow: hidden; - padding: 16px 4px; - height: 65px; - border-bottom: var(--whitegrey) 1px solid; - transition: background 0.3s ease; -} - -.new-integration-cell .apps-card-app { - padding: 10px 16px; - height: auto; -} - -@media (max-width: 500px) { - .apps-card-app { - min-height: 75px; - height: auto; - } - - .new-integration-cell .apps-card-app { - min-height: auto; - height: auto; - } -} - -.apps-grid-cell:first-of-type .apps-card-app { - border-top: none; -} - -.apps-card-left { - display: flex; - align-items: center; -} - -@media (max-width: 500px) { - .apps-card-left { - flex-basis: 70%; - } -} - -.apps-card-right { - display: flex; - align-items: center; -} - -.apps-card-right svg { - margin-left: 15px; - height: 14px; -} - -.apps-card-right svg path { - fill: var(--midgrey); -} - -.apps-configured { - display: flex; - align-items: center; - color: var(--midgrey); - font-weight: 400; -} - -.apps-configured svg { - margin-left: 15px; - height: 14px; -} - -.apps-configured svg path { - fill: var(--midgrey); -} - -.apps-configured a { - display: inline-block; - padding: 2px 6px; - border-radius: 3px; -} - -.apps-configured-action { - margin-left: 15px; - text-transform: uppercase; - font-size: 1.2rem; - font-weight: 500; -} - -.apps-card-app-icon { - flex: 0 0 47px; - margin: 0 12px 0 0; - width: 47px; - height: 47px; - background-position: center center; - background-size: cover; - background-repeat: no-repeat; - border-radius: 15%; - mix-blend-mode: multiply; -} - -.apps-card-meta { - display: flex; - flex-direction: column; - padding-right: 40px; -} -@media (max-width: 500px) { - .apps-card-meta { - flex-basis: 70%; - padding-right: 10px; - } -} - -.apps-card-app-disabled .apps-card-meta { - opacity: 0.4; -} - -.apps-card-app-disabled .apps-card-app-title-container { - display: flex; -} - -.apps-card-app-disabled .apps-card-app-icon { - filter:grayscale(1); - opacity:0.3; -} - -.apps-card-app-disabled .apps-card-meta svg { - margin-left: 4px; - width: 14px; - opacity: 0.9; -} - -.apps-card-app-disabled .apps-card-meta path { - fill: #292d33; -} - -.apps-card-app-disabled .apps-card-app-disabled-cta { - color: #30cf43; - margin-right: 28px; - font-weight: 600; -} - -.apps-card-app-title { - margin: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.3em; - font-weight: 600; -} -@media (max-width: 500px) { - .apps-card-app-title { - white-space: normal; - font-size: 1.5rem; - letter-spacing: 0; - } -} - - -/* Apps Card Meta -/* ---------------------------------------------------------- */ - -.apps-card-app-desc { - display: -webkit-box; - overflow: hidden; - margin: 4px 0 0; - padding: 0; - max-height: 4.2rem; - color: var(--midgrey); - text-overflow: ellipsis; - font-size: 1.3rem; - line-height: 1.3em; - font-weight: 400; - - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -@media (min-width: 600px) and (max-width: 1460px) { - .apps-card-app-desc { - padding-right: 24px; - } -} - - -/* Apps Card Footer -/* ---------------------------------------------------------- */ - -.apps-card-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 15px; - width: 100%; -} - -/* Details -/* ---------------------------------------------------------- */ - -.app-grid { - display: flex; - justify-content: flex-start; - align-items: flex-start; - align-content: flex-start; -} - -.app-icon { - position: relative; - flex: 1 0 60px; - width: 60px; - min-width: 60px; - height: 60px; - background-position: center center; - background-size: cover; - border-radius: 10%; - margin-right: 24px; -} - -.app-icon img { - display: block; -} - -.app-cell h3 { - margin: -2px 0 0; - color: var(--black); - font-size: 3.2rem; - font-weight: 700; -} - -.app-cell p { - margin: 0; - margin-bottom: 5px; - color: var(--midgrey-l2); - font-size: 1.4rem; - line-height: 1.4em; -} - -.app-subtitle { - max-width: 550px; - color: var(--midgrey); - font-size: 1.6rem; -} -.app-config-form .gh-btn-grey { - margin-top: 1.6em; - background-color: #e8e8e8; - box-shadow: none; - font-size: 1.1rem; -} - -.app-config-form > .gh-btn-grey:hover, -.app-config-form > .gh-btn-grey:focus { - border-color: rgb(223, 225, 227); -} - -.app-api-buttons { - display: flex; - align-items: center; - position: absolute; - top: -3px; - right: -3px; -} - -.app-api-personal-token-buttons { - display: flex; - align-items: center; - position: absolute; - right: 5px; -} - -.app-button-regenerate { - height: 26px; - display: flex; - align-items: center; - border: 1px solid var(--lightgrey); - border-radius: 3px; - padding: 5px 8px; - margin-right: 8px; - background: var(--white); -} - -.app-button-copy { - height: 26px; - display: flex; - align-items: center; - background: var(--black); - font-size: 1.2rem; - padding: 4px 12px; - color: var(--white); - font-weight: 500; - border-radius: 3px; -} - -/* Zapier templates */ -/* ---------------------------------------------------------- */ -.gh-zapier-data-container { - margin: 1.8em 0; -} - -.gh-zapier-data-container .gh-zapier-data { - display: flex; -} - -@media (max-width: 500px) { - .gh-zapier-data-container .gh-zapier-data { - flex-direction: column; - align-items: flex-start; - } -} - -.gh-zapier-data .data-label { - width: 128px; - height: 28px; - padding: 4px 4px 4px 0; - color: var(--midgrey-l2); - font-size: 1.4rem; - line-height: 1.45; - font-weight: 400; - white-space: nowrap; -} - -.gh-zapier-data .data { - width: 100%; - padding: 4px; - color: var(--darkgrey); - font-size: 1.4rem; - line-height: 1.45; - font-weight: 500; - border-radius: 3px; - overflow-x: hidden; -} - -.gh-zapier-data .data.highlight-hover:hover { - background: var(--whitegrey-l1); -} - -@media (max-width: 500px) { - .gh-zapier-data .data { - padding: 4px 0; - } - - .gh-zapier-data .data.highlight-hover:hover { - background: transparent; - } -} - -.gh-zapier-data .admin-key, -.gh-zapier-data .api-url { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - word-wrap: break-word; - word-break: break-word; -} - -.gh-settings-zapier-templates .apps-grid-cell:hover { - background: var(--white); -} - -.zapier-template-link:hover { - border-color: #f04600; -} - -.zapier-template-link span { - transition: all 0.2s ease; - transition-property: color; -} - -.zapier-template-link:hover span { - color: #f04600; -} - -.gh-settings-zapier-templates .apps-card-app-title { - margin-left: 8px; - white-space: unset; -} - -.gh-settings-zapier-templates .apps-card-app { - height: 68px; -} - -.gh-settings-zapier-templates .gh-card-right { - display: flex; - flex-direction: column; - justify-content: center; -} - -.zapier-footer, -.zapier-footer a { - display: flex; - justify-content: flex-end; - align-items: center; - color: var(--midgrey); - font-size: 1.3rem; -} - -.zapier-footer figure { - margin: 0 0 2px; - width: 47px; - height: 47px; - background-position: 50%; - background-size: cover; - background-repeat: no-repeat; - border-radius: 15%; -} - -/* Custom Integrations -/* ---------------------------------------------------------- */ -.new-webhook-cell td { - padding: 0; -} - -.new-webhook-cell:hover { - background: var(--whitegrey-l2); -} - -.app-custom-icon-container { - margin-right: 32px; -} - -.app-custom-icon { - display: flex; - position: relative; - align-items: center; - height: 117px; - width: 117px; - margin: 0; - border-radius: 3px; - border: 1px solid var(--whitegrey-d1); - background: var(--white); - padding: 24px; -} - -.app-custom-icon-uploadlabel { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - width: 100%; - height: 100%; - color: var(--white); - border-radius: 3px; - text-align: center; - background-color: var(--black); - font-size: 1.3rem; - font-weight: 600; -} - -.app-custom-api-table { - margin-bottom: 0; -} - -.app-custom-api-table .data-label { - width: 160px; -} - -.app-custom-api-table .data.highlight-hover:hover { - background: var(--white) !important; -} - -.apps-card-app-orb { - width: 32px; - height: 32px; -} - -.apps-card-app-orb.rot-1 { - transform: rotate(90deg); -} - -.apps-card-app-orb.rot-2 { - transform: rotate(180deg); -} - -.apps-card-app-orb.rot-3 { - transform: rotate(270deg); -} - -/* Pintura integration -/* ---------------------------------------------------------- */ -.gh-pintura-banner.gh-main-section-content { - display: grid; - grid-template-columns: 4fr 3fr; - padding: 0; - font-size: 1.5rem; -} - -.gh-pintura-banner-content { - padding: 24px; -} - -.gh-pintura-banner-content p { - margin-top: 0.8em; - margin-bottom: 0; -} - -.gh-pintura-banner-content .gh-btn { - margin-top: 1.2em; - background-color: var(--black) !important; -} - -.gh-pintura-banner-image { - border-radius: 0 3px 3px 0; -} diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index bb3cd517ad1..70f5b1d0154 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -593,6 +593,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone { width: 1em; height: 1em; margin-left: 24px; + margin-bottom: 2px; line-height: 1.2; } @@ -624,6 +625,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone { width: 100%; height: 24px; margin: 0 0 1.7em 0; + padding: 0; outline: none; border-width: 0; border-style: none; @@ -1057,14 +1059,6 @@ figure { padding-right: 8px; /* extra padding used for dynamic positioning with js */ } -.gh-announcement-editor { - width: 100%; -} - -.gh-announcement-editor div { - width: 100%; -} - /* Labs /* ---------------------------------------------------------- */ @@ -1110,3 +1104,11 @@ figure { .gh-editor-hidden-indicator svg { height: 2.4rem; } + +.gh-setting-error { + margin-top: 1em; + line-height: 1.3em; + color: var(--red); + font-weight: 300; + letter-spacing: 0.3px; +} diff --git a/ghost/admin/app/styles/layouts/labs.css b/ghost/admin/app/styles/layouts/labs.css deleted file mode 100644 index 051e711ac1d..00000000000 --- a/ghost/admin/app/styles/layouts/labs.css +++ /dev/null @@ -1,524 +0,0 @@ -.gh-labs-price-label input::-webkit-outer-spin-button, .gh-labs-price-label input::-webkit-inner-spin-button { - /* display: none; <- Crashes Chrome on hover */ - -webkit-appearance: none; - margin: 0; -} - -.gh-labs-price-label input[type=number] { - -moz-appearance: textfield; - /* Firefox */ -} - -.gh-labs-toggle-wrapper { - padding-top: 6px; - padding-bottom: 6px; - border-radius: 5px; -} - -.gh-btn-labs-toggle { - border: none !important; - display: flex; - align-items: center; - color: var(--blue) !important; - background: transparent !important; -} - -.gh-btn-labs-toggle, -.gh-btn-labs-toggle:hover { - box-shadow: none !important; -} - -.gh-btn-labs-toggle svg { - width: 10px; - height: 10px; - margin-right: 5px; -} - -.gh-btn-labs-toggle svg path { - stroke: var(--blue); -} - -.gh-labs-disabled .for-checkbox label, -.gh-labs-disabled .for-checkbox .input-toggle-component, -.gh-labs-disabled .for-switch label, -.gh-labs-disabled .for-switch .input-toggle-component -.gh-labs-disabled .for-radio label, -.gh-labs-disabled .for-radio .input-toggle-component { - cursor: default; -} - -/* Members settings */ -/* ------------------------------------------------ */ - -.gh-labs-members-radio { - cursor: pointer; - margin: 0 8px; -} - -.gh-labs-members-radio.active { - background: color-mod(var(--blue) alpha(6%)); - border-color: var(--blue); -} - -.gh-labs-disabled .gh-setting-content, .gh-labs-disabled .gh-setting-action { - opacity: 0.25; -} - -.gh-labs-members-emaildropdown { - min-width: 208px; - margin-left: 8px; -} - -.gh-labs-members-emaildropdown[disabled] { - background: var(--whitegrey-d2); - color: var(--darkgrey); -} - -.gh-labs-sso-settings svg { - position: relative; - bottom: 1px; - width: 18px; - margin-right: 8px; -} - -/* Import modal -/* ---------------------------------------------------------- */ - -.fullscreen-modal-import-content { - max-width: unset !important; -} - -.gh-content-import-wrapper { - width: 420px; -} - -.gh-content-import-wrapper .gh-btn.disabled, -.gh-content-import-wrapper .gh-btn.disabled:hover { - cursor: auto !important; - opacity: 0.6 !important; -} - -.gh-content-import-wrapper .gh-btn.disabled span, -.gh-content-import-wrapper .gh-btn.disabled span:hover { - cursor: auto !important; - pointer-events: none; -} - -.gh-content-import-wrapper .gh-token-input .ember-power-select-trigger[aria-disabled=true], -.gh-content-import-wrapper .gh-token-input .ember-power-select-trigger-multiple-input:disabled { - background: var(--whitegrey-l2); -} - -@media (max-width: 600px) { - .gh-content-import-wrapper, - .gh-content-import-wrapper.wide { - width: calc(100vw - 128px); - } -} - -.gh-content-import-uploader { - width: 100%; - min-height: 180px; -} - -.gh-content-import-uploader svg { - width: 3.2rem; - height: 3.2rem; - margin-bottom: 1rem; -} - -.gh-content-import-uploader svg path { - stroke: var(--midlightgrey); -} - -.gh-content-import-uploader:hover svg path { - stroke: var(--midgrey-l1); -} - -.gh-content-import-uploader .description { - color: var(--midgrey); - font-size: 1.4rem; - font-weight: 500; -} - -.gh-content-import-uploader:hover .description { - color: var(--midgrey-d2); -} - -.gh-content-import-file { - min-height: 180px; -} - -.gh-content-import-spinner { - position: relative; - display: flex; - min-height: 182px; - justify-content: center; - align-items: center; - margin-bottom: -20px; -} - -.gh-content-import-spinner .gh-loading-content { - padding-bottom: 0px; -} - -.gh-content-import-spinner .description { - padding-top: 46px; -} - -.gh-content-upload-errorcontainer { - border: 1px solid var(--whitegrey); - border-radius: 4px; - padding: 12px; - margin-bottom: 24px; - color: var(--middarkgrey); -} - -.gh-content-upload-errorcontainer.warning { - border-left: 4px solid var(--yellow); -} - - -.gh-content-upload-errorcontainer.warning p a { - color: color-mod(var(--yellow) l(-12%)); - text-decoration: underline; -} - -.gh-content-upload-errorcontainer.error { - border-left: 4px solid var(--red); -} - -.gh-content-upload-errorcontainer.error p a { - color: var(--red); - text-decoration: underline; -} - -.gh-content-import-errormessage { - font-size: 1.25rem; - font-weight: 600; - margin: 12px 0 0; -} - -p.gh-content-import-errorcontext { - font-size: 1.25rem; - line-height: 1.3em; - margin: 0; - font-weight: 400; -} - -.gh-content-import-mapping .error { - color: var(--red); -} - -.gh-content-import-mappingwrapper.error { - position: relative; -} - -.gh-content-import-mappingwrapper.error::before { - display: block; - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid red; - z-index: 9999; - pointer-events: none; -} - -.gh-content-import-scrollarea { - position: relative; - max-height: calc(100vh - 350px - 12vw); - overflow-y: scroll; - margin: 0 -32px; - padding: 0 32px; - background: - /* Shadow covers */ - linear-gradient(var(--white) 30%, rgba(255,255,255,0)), - linear-gradient(rgba(255,255,255,0), var(--white) 70%) 0 100%, - - /* Shadows */ - /* radial-gradient(farthest-side at 50% 0, rgba(0,0,0,.12), rgba(0,0,0,0)) -64px 0, - radial-gradient(farthest-side at 50% 100%, rgba(0,0,0,.12), rgba(0,0,0,0)) -64px 100%; */ - linear-gradient(rgba(0,0,0,0.08), rgba(0,0,0,0)), - linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.08)) 0 100%; - background-repeat: no-repeat; - background-color: var(--white); - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - - /* Opera doesn't support this in the shorthand */ - background-attachment: local, local, scroll, scroll; - margin-top: 4px; -} - -.gh-content-import-errorheading { - font-size: 1.4rem; - line-height: 1.55em; - margin-top: 2px; -} - -p.gh-content-import-errordetailtext { - font-size: 1.3rem; - line-height: 1.4em; - color: var(--midgrey); -} - -.gh-content-import-errordetailtext:first-of-type { - border-top: 1px solid var(--lightgrey); - padding-top: 8px; - margin-top: 8px; -} - -.gh-content-import-errordetailtext:not(:last-of-type) { - padding-bottom: 4px; - margin-bottom: 6px; -} - -.gh-content-import-table { - position: relative; - margin-bottom: 1px; -} - -.gh-content-import-table::before { - position: absolute; - display: block; - content: ""; - top: 0; - left: -33px; - bottom: 0; - height: 100%; - width: 32px; - background: var(--white); -} - -.gh-content-import-table::after { - position: absolute; - display: block; - content: ""; - top: 0; - right: -32px; - bottom: 0; - height: 100%; - width: 32px; - background: var(--white); -} - -.gh-content-import-table th { - padding: 3px 8px; - background: color-mod(var(--darkgrey) a(5%) s(+50%)); - border-left: 1px solid var(--content-import-table-border); - border-top: 1px solid var(--content-import-table-outline); - border-bottom: 1px solid var(--content-import-table-border); -} - -.gh-content-import-table tr th:first-of-type { - border-left: 1px solid var(--content-import-table-outline); - width: 180px; -} - -.gh-content-import-table tr th:last-of-type { - border-right: 1px solid var(--content-import-table-outline); -} - -.gh-content-import-table td.empty-cell { - background: color-mod(var(--darkgrey) a(3%) s(+50%)); -} - -.gh-content-import-table td { - padding: 7px 8px 6px; - border-left: 1px solid var(--content-import-table-border); - border-bottom: 1px solid var(--content-import-table-border); - vertical-align: top; -} - -.gh-content-import-table tr td:first-of-type { - border-left: 1px solid var(--content-import-table-outline); - width: 180px; -} - -.gh-content-import-table tr td:last-of-type { - padding: 0; - border-right: 1px solid var(--content-import-table-outline); -} - -.gh-content-import-table tr:last-of-type td { - border-bottom: 1px solid var(--content-import-table-outline); -} - -.gh-content-import-table td span, -.gh-content-import-table th span { - user-select: none !important; -} - -.gh-content-import-datanav { - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -p.gh-content-import-errordetail { - font-size: 1.2rem; - line-height: 1.4em; - margin: 10px 0 0 24px; -} - -p.gh-content-import-errordetail:first-of-type { - border-top: 1px solid var(--whitegrey); - padding-top: 8px; - margin-top: 8px; -} - -.gh-import-content-select { - height: auto; - border: none; - background: none; - border-radius: 0; -} - -.gh-import-content-select select { - height: 34px; - border: none; - font-size: 1.3rem; - line-height: 1em; - padding: 4px 4px 4px 8px; - background: none; - color: var(--middarkgrey); - font-weight: 600; - border-radius: 0; -} - -.gh-import-content-select select option { - font-weight: 400; - color: var(--darkgrey); -} - -.gh-import-content-select select:focus { - background: none; - color: var(--middarkgrey); -} - -.gh-import-content-select.unmapped select, -.gh-import-content-select.unmapped select:focus { - color: var(--midlightgrey); - font-weight: 400; -} - -.gh-import-content-select svg { - right: 9px; -} - -.gh-content-import-table th.table-cell-field, -.gh-content-import-table td.table-cell-field, -.gh-content-import-table th.table-cell-data, -.gh-content-import-table td.table-cell-data { - max-width: 180px; - overflow-wrap: break-word; -} - -.gh-content-import-resultcontainer { - margin-bottom: 28px; -} - -.gh-content-import-result-summary { - flex-basis: 50%; -} - -.gh-content-import-result-summary h2 { - font-size: 3.6rem; - font-weight: 600; - margin: 0; - padding: 0; -} - -.gh-content-import-result-summary p { - color: var(--darkgrey); - margin: 0; - padding: 0; - line-height: 1.6em; - margin-bottom: 12px; -} - -.gh-content-import-result-summary p strong { - font-size: 1.5rem; - letter-spacing: 0; -} - -.gh-content-import-errorlist { - width: 100%; - margin: 8px 0 28px; -} - -.gh-content-import-errorlist h4 { - font-size: 13px; - font-weight: 500; - border-bottom: 1px solid var(--whitegrey); - padding-bottom: 8px; - margin-top: 0px; - color: var(--midgrey); -} - -.gh-content-import-errorlist ul li { - font-size: 13px; - font-weight: 400; - color: var(--midlightgrey-d2); - padding: 0; - margin-bottom: 6px; -} - -.gh-content-import-resultcontainer hr { - margin: 24px -32px; - border-color: var(--whitegrey); -} - -.gh-content-import-nodata span { - display: flex; - min-height: 144px; - align-items: center; - justify-content: center; - color: var(--midgrey); -} - -.gh-content-import-icon-content path, -.gh-content-import-icon-content circle { - stroke-width: 0.85px; -} - -.gh-content-import-icon-confetti { - color: var(--pink); - margin-left: 12px; -} - -.gh-content-import-icon-confetti path, -.gh-content-import-icon-confetti circle, -.gh-content-import-icon-confetti ellipse { - stroke-width: 0.85px; -} - -.gh-import-content-icon { - color: var(--darkgrey); - width: 54px !important; - height: 54px !important; - margin-right: -8px; -} - -.gh-import-content-icon * { - stroke-width: 0.8px !important; -} - -/* Fixing Firefox's select padding */ -@-moz-document url-prefix() { - .gh-import-content-select select { - padding: 4px; - } -} - -.fullscreen-modal-import-content { - max-width: unset !important; -} - -.gh-import-content-spinner { - min-height: 120px; -} - -.gh-import-content-spinner .gh-loading-content { - padding-bottom: 0; -} diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css index 9c210c85226..0625a1c00a4 100644 --- a/ghost/admin/app/styles/layouts/main.css +++ b/ghost/admin/app/styles/layouts/main.css @@ -5,7 +5,6 @@ --mainmenu-color-active-bg: var(--whitegrey); --mainmenu-width: 320px; --mainmenu-padding: var(--main-layout-area-padding); - --contextualmenu-width: 360px; } /* Utils */ @@ -216,9 +215,7 @@ } .gh-nav-main-enter-active, -.gh-nav-main-leave-active, -.gh-nav-contextual-enter-active, -.gh-nav-contextual-leave-active { +.gh-nav-main-leave-active { position: absolute; top: 0; height: 100%; @@ -235,29 +232,6 @@ transform: translateX(-100%); } -.gh-nav-contextual-enter-active, -.gh-nav-contextual-leave-active { - width: calc(var(--contextualmenu-width) - 1px); -} - -/* For some reason the animate-in transition wasn't working with pure transforms */ -/* Using `left` correctly positioned the starting state so we could then use opposite transforms */ -.gh-nav-contextual-enter-active { - left: calc(var(--contextualmenu-width) - 1px); -} - -.gh-nav-contextual-enter-to { - transform: translateX(-100%); -} - -.gh-nav-contextual-leave { - transform: translateX(0); -} - -.gh-nav-contextual-leave-to { - transform: translateX(calc(var(--mainmenu-width) - 1px)); -} - .gh-account-menu-header { position: relative; display: flex; @@ -2044,14 +2018,6 @@ section.gh-ds h2 { line-height: 1.45em; } -.gh-done .gh-setting-desc { - padding-right: 2rem; -} - -.gh-done .gh-nav-design .gh-setting-action { - margin-bottom: 12px; -} - .gh-done-panel { margin: 8px 0 24px; padding: 52px var(--main-layout-area-padding) 16px; @@ -2280,4 +2246,4 @@ section.gh-ds h2 { border: 1px solid var(--green); border-radius: 4px; margin-top: 8px; -} \ No newline at end of file +} diff --git a/ghost/admin/app/styles/layouts/members.css b/ghost/admin/app/styles/layouts/members.css index 7baa45e351e..2c72e91165b 100644 --- a/ghost/admin/app/styles/layouts/members.css +++ b/ghost/admin/app/styles/layouts/members.css @@ -1862,24 +1862,10 @@ p.gh-members-import-errordetail:first-of-type { width: 100%; } -.gh-email-design-typography .gh-setting-dropdown { - margin: 0; - padding: 0 8px 0 8px; -} - -.gh-email-design-typography-wrapper.header .gh-setting-dropdown { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - .gh-email-design-typography-wrapper.header .ember-power-select-status-icon { right: 16px !important; } -.gh-email-design-typography .gh-setting-dropdown-list .ember-power-select-option { - padding: 2px 8px; -} - .gh-email-design-typography-wrapper.header .gh-btn-group { background: var(--whitegrey); border-top-left-radius: 0; diff --git a/ghost/admin/app/styles/layouts/offers.css b/ghost/admin/app/styles/layouts/offers.css index 336504c2051..4371aebd3da 100644 --- a/ghost/admin/app/styles/layouts/offers.css +++ b/ghost/admin/app/styles/layouts/offers.css @@ -296,6 +296,38 @@ box-shadow: var(--box-shadow-preview-box); } +.gh-setting-members-portal-mock { + display: flex; + position: relative; + align-items: center; + justify-content: center; + background: #fff; + box-shadow: var(--box-shadow-preview-box); + width: 420px; + height: 562px; + margin-bottom: 32px; + border-radius: 5px; + pointer-events: none; + transition: height 0.17s ease-out; +} + +.gh-setting-members-portal-mock.mock-enabled { + pointer-events: unset; +} + +.gh-setting-members-portalpreview { + justify-self: end; + font-size: 1.3rem; + font-weight: 500; + color: var(--midgrey); +} + +@media (max-width: 1140px) { + .gh-setting-members-portalpreview { + display: none; + } +} + .gh-offers-help { margin-top: 5vmin; margin-bottom: 0; diff --git a/ghost/admin/app/styles/layouts/packages.css b/ghost/admin/app/styles/layouts/packages.css deleted file mode 100644 index 08d895a483d..00000000000 --- a/ghost/admin/app/styles/layouts/packages.css +++ /dev/null @@ -1,374 +0,0 @@ -/* Packages - Themes / Integrations -/* ---------------------------------------------------------- */ - -.package-filter { - border-radius: 5px; -} -@media (max-width: 1460px) { - .package-filter { - max-width: 700px; - } -} - - -/* Main Layout -/* ---------------------------------------------------------- */ - -.package-grid { - display: flex; - flex-flow: row wrap; - align-items: space-between; - margin: -10px -10px 4vw -10px; - max-width: 1200px; -} - -/* 3 col themes */ -.package-grid-themes .package-grid-cell { - flex: 0 0 33.3333%; -} - -/* 2 col themes */ -@media (max-width: 1240px) { - .package-grid-themes .package-grid-cell { - flex: 0 0 100%; - } -} - -/* 1 col themes */ -@media (max-width: 800px) { - .package-grid-themes .package-grid-cell { - flex: 1 1 100%; - } -} - -/* 2 col apps */ -.package-grid-apps .package-grid-cell { - flex: 0 0 100%; -} - -/* 1 col apps */ -@media (max-width: 1200px) { - .package-grid-apps .package-grid-cell { - flex: 1 1 100%; - } -} - - -/* Package Card Theme -/* ---------------------------------------------------------- */ - -.package-card-theme { - overflow: hidden; - margin: 10px; - border: rgba(0,0,0,0.1) 1px solid; - border-radius: 5px; -} - -.package-index .package-card-theme, -.package-featured .package-card-theme { - flex: 1 1 240px; -} - -.package-card-theme-image { - position: relative; - display: block; -} - -.package-card-theme-image:hover img { - filter: grayscale(0.5) blur(1px); - - -webkit-filter: grayscale(0.5) blur(1px); -} - -.package-card-theme-image:hover .package-card-theme-overlay { - opacity: 1; - transition: all 0.2s ease; -} - -.package-card-theme-image img { - display: block; - max-width: 100%; - line-height: 0; -} - -.package-card-theme-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - justify-content: center; - align-items: center; - padding: 10%; - background: rgba(0,20,40,0.2); - text-align: center; - opacity: 0; - transition: opacity 0.4s ease; -} - -.package-card-theme-title { - color: #fff; - font-size: 2rem; - line-height: 1.15em; - font-weight: 300; -} - -.package-card-theme .package-card-footer { - margin: 0; - padding: 16px 20px; - border-top: rgba(0,0,0,0.1) 1px solid; -} - -/* Package Card App -/* ---------------------------------------------------------- */ - -.package-card-app { - overflow: hidden; - margin: 10px; - padding: 14px; - height: 75px; - /*max-width: 700px;*/ - border: rgba(0,0,0,0.1) 1px solid; - border-radius: 5px; - transition: background 0.3s ease; -} - -.package-card-app:hover { - background: rgba(0,20,60,0.03); - cursor: pointer; - transition: background 0.1s ease; -} - -.package-card-content { - position: relative; - display: flex; -} - -.package-card-content .gh-btn { - position: absolute; - right: 20px; -} - -.package-card-app-icon { - flex: 0 0 47px; - margin: 0 15px 0 0; - width: 47px; - height: 47px; - background-position: center center; - background-size: cover; - border-radius: 15%; -} - -.package-card-meta { - position: relative; - display: flex; - flex-direction: column; -} - -.package-card-app-title { - overflow: hidden; - margin: 0 0 4px 0; - padding: 0 70px 0 0; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 1.7rem; - font-weight: normal; -} - - -/* Package Card Meta -/* ---------------------------------------------------------- */ - -.package-card-stats { - position: absolute; - top: -5px; - right: 0; - display: flex; - align-items: center; -} - -.package-downloads { - display: flex; - align-items: center; - height: 26px; - border: transparent 1px solid; - color: var(--midgrey); - font-size: 13px; - line-height: 24px; -} - -.package-downloads:hover { - cursor: default; -} - -.package-downloads svg { - margin-right: 5px; - height: 15px; -} - -.package-download-count { - font-size: 13px; -} - -.package-card-app-desc { - display: -webkit-box; - overflow: hidden; - margin: 0; - padding: 0; - max-height: 4.2rem; - color: var(--midgrey); - text-overflow: ellipsis; - font-size: 1.4rem; - line-height: 1.3em; - font-weight: 300; - - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} -@media (min-width: 600px) and (max-width: 1460px) { - .package-card-app-desc { - padding-right: 80px; - } -} - - -/* Package Card Footer -/* ---------------------------------------------------------- */ - -.package-card-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 15px; - width: 100%; -} - -.package-developer { - display: flex; - align-items: center; - color: var(--midgrey); -} - -.package-developer:hover { - color: var(--blue); -} - -.package-developer img { - flex-shrink: 0; - margin-right: 6px; - width: 20px; - height: 20px; - border-radius: 100%; -} - -.package-developer-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 1.4rem; -} - -.package-controls { - flex-shrink: 0; - display: flex; - overflow: hidden; - border: rgba(0,0,0,0.1) 1px solid; - border-radius: 4px; -} - -.package-controls-button { - display: flex; - align-items: center; - padding: 7px 12px; - border-left: rgba(0,0,0,0.1) 1px solid; - background: #fff; - color: var(--midgrey); - font-size: 1.3rem; - line-height: 1; - transition: none; -} - -.package-controls-button:first-child { - border: none; -} - -.package-controls-button:hover { - color: var(--darkgrey); -} - -.package-controls-button svg { - margin-right: 5px; - width: 11px; - height: 11px; -} - -.package-disable { - border-right: var(--green) 3px solid; -} - -.package-enable { - border-right: var(--red) 3px solid; -} - - -/* Media Queries -/* ---------------------------------------------------------- */ - -@media (max-width: 800px) { - .package-grid-apps { - overflow: hidden; - margin: 0 0 4vw 0; - border: #dfe1e3 1px solid; - border-radius: 5px; - } - - .package-card-app { - margin: 0; - border: none; - border-top: #dfe1e3 1px solid; - border-radius: 0; - } - .package-grid-cell:first-of-type .package-card-app { - border-top: none; - } -} - -@media (max-width: 760px) { - .package-card-app { - padding: 15px; - } - .package-card-app .package-developer { - display: none; - } - .package-card-app .package-card-footer { - justify-content: flex-end; - } - .package-card-theme .package-card-footer { - margin: 0; - padding: 15px; - } -} - -@media (max-width: 600px) { - .package-grid { - margin: -10px -10px 4vw -10px; - border: none; - } - .package-grid-apps { - margin: -10px -20px 4vw -20px; - } -} - -@media (max-width: 540px) { - .package-card-footer { - justify-content: flex-end; - } - .package-card-app .package-card-footer { - flex-direction: column; - align-items: flex-start; - } - .package-card-footer .package-developer { - display: none; - } -} diff --git a/ghost/admin/app/styles/layouts/portal-settings.css b/ghost/admin/app/styles/layouts/portal-settings.css deleted file mode 100644 index cccabbc32d6..00000000000 --- a/ghost/admin/app/styles/layouts/portal-settings.css +++ /dev/null @@ -1,688 +0,0 @@ -.fullscreen-modal-portal-settings { - margin: 30px; - max-width: 100%; -} - -.fullscreen-modal-portal-settings .modal-content { - position: relative; - overflow: auto; - height: 100%; - padding: 0; -} - -.fullscreen-modal-portal-settings .modal-body { - margin: 0; -} - -.gh-ps-header { - position: sticky; - top: 0; - left: 0; - right: 0; - display: flex; - align-items: center; - justify-content: space-between; - margin: 0; - padding: 18px 32px; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - overflow: hidden; - background-position: center; - background-repeat: no-repeat; - background-size: cover; - background: var(--white); - z-index: 9999; -} - -.gh-ps-header h2 { - width: calc(50vw - 200px); - margin: 0; -} - -.gh-ps-header-border { - border-bottom: 1px solid var(--whitegrey); -} - -.gh-ps-close { - width: calc(50vw - 200px); -} - -.gh-ps-modal-body { - height: 100%; - display: flex; - flex-direction: column; -} - -.gh-show-modal-link-form .gh-input { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.gh-portal-settings-sidebar { - padding: 0; - width: 400px; - height: calc(100vh - 60px); - overflow-x: hidden; -} - -.gh-portal-settings-sidebar .modal-fullsettings-body-labs { - overflow: unset; -} - -.gh-portal-form-wrapper { - overflow: hidden; - width: 400px; -} - -.gh-portal-settings-form { - min-width: 292px; - margin: 0 -80px 0 0 !important; - padding: 4px 100px 0 20px; - max-height: calc(100vh - 60px - 66px); - overflow-y: scroll; - overflow-x: hidden; -} - -.gh-portal-settings .form-group.space-l { - margin-bottom: 1.6em; -} - -.gh-portal-setting-title { - font-size: 1.3rem; - font-weight: 600; - margin: 0; -} - -.gh-portal-settings .for-switch.small { - width: 36px !important; - height: 22px !important; -} - -.gh-portal-settings .gh-members-emailsettings-footer-input { - height: 78px; -} - -.gh-portal-settings .gh-members-emailsettings-footer-input p { - height: 60px; -} - -.gh-portal-setting-sectionheading { - font-size: 1.1rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.2px; - color: var(--midgrey-l1); - margin: 0; - padding: 0 0 8px; - border-bottom: 1px solid var(--whitegrey); -} - -.gh-portal-setting-sectionheading.gh-stripe-connect { - display: flex; - justify-content: space-between; -} - -.gh-portal-setting-sectionheading.gh-stripe-connect .gh-btn-link { - color: #6772E5; - text-transform: initial; - font-size: 1.25rem; - font-weight: 400; - cursor: pointer; -} - -.gh-portal-setting-section { - margin: 0 -24px 2.4em; - padding: 0 28px; -} - -.gh-portal-setting-section p { - line-height: 1.5em; -} - -.gh-portal-settings .input-color input { - position: relative; - height: 30px; - width: 102px; - padding: 3px 4px 3px 44px; - font-size: 1.3rem; -} - -.gh-portal-settings .input-color::after { - top: 5px; - left: 34px; -} - -.gh-portal-settings .color-picker-horizontal-divider { - position: absolute; - display: block; - content: ""; - width: 1px; - top: 0; - left: 29px; - bottom: 0; - background: var(--input-border-color); -} - -.gh-portal-settings .input-color input:focus + .color-picker-horizontal-divider { - top: 2px; - bottom: 2px; -} - -.gh-portal-settings .color-box-container { - height: 26px; - width: 26px; - position: absolute; - overflow: hidden; - top: 2px; - left: 2px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} - -.gh-portal-settings .color-box-container .color-picker { - position: absolute; - top: -10px; - left: -10px; - border: none; - outline: none; - padding: 0; - margin: 0; - width: 50px; - height: 50px; -} - -.gh-portal-settings .gh-select svg { - top: 19px; - right: 9px; -} - -.gh-portal-settings-topbarheight { - height: 66px; -} - -.gh-portal-settings-main { - display: flex; - flex-direction: column; - flex-grow: 1; - padding: 0; - border-left: 1px solid var(--whitegrey); - background: linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(249,249,250,1) 100%); -} - -.gh-portal-settings .modal-footer { - margin-top: 28px; -} - -.gh-portal-settings-maintabs { - list-style: none; - list-style-type: none; - display: flex; - align-items: center; - padding: 0; - margin: 0; - border: 1px solid color-mod(var(--midgrey) l(+35%) s(+10%)); - border-radius: 5px; - letter-spacing: 0.2px; - box-shadow: 0 2px 5px -3px rgba(0,0,0,.12); -} - -.gh-portal-settings-maintabs li { - padding: 0; - margin: 0; -} - -.gh-portal-settings-maintabs li:not(:last-of-type) { - border-right: 1px solid color-mod(var(--midgrey) l(+35%) s(+10%)); -} - -.gh-portal-settings-maintabs li a { - position: relative; - display: inline-block; - padding: 3px 10px 4px; - margin: 4px; - color: color-mod(var(--midgrey) l(-7%)); - background: var(--white); - min-width: 56px; - text-align: center; - border-radius: 2px; - outline: none; - box-sizing: content-box; - font-size: 1.3rem; -} - -.gh-portal-settings-maintabs li.active a { - color: var(--blue); - font-weight: 500; -} - -.gh-portal-settings-icons { - display: flex; - flex-grow: 1; - align-items: center; - justify-content: flex-start; - padding: 2px; - gap: 12px; -} - -.gh-portal-setting-first { - margin: 12px 0 0; -} - -.gh-portal-setting-last { - margin-bottom: 12px !important; -} - -.gh-portal-button-icon { - display: inline-block; - cursor: pointer; - width: 38px; - height: 40px; - padding: 10px; - background-repeat: no-repeat; - background-size: 22px 22px; - background-position: center; - border-radius: 2px; - margin: 3px; -} - -.gh-portal-button-icon:hover { - box-shadow: 0px 0px 0px 1px color-mod(var(--green) a(40%)); -} - -.gh-portal-button-icon:hover svg path { - stroke: var(--green); -} - -.gh-portal-button-icon.selected-icon { - box-shadow: 0px 0px 0px 2px var(--green); -} - -.gh-portal-button-icon svg path { - stroke: var(--midlightgrey-d1); -} - -.gh-portal-button-icon.selected-icon svg path { - stroke: var(--green); -} - -.gh-portal-button-icon .gh-loading-spinner { - width: 20px; - height: 20px; -} - -.gh-portal-button-icon .gh-loading-spinner:before { - margin-top: -2px; -} - -.gh-portal-button-uploadicon, -.gh-portal-button-uploadicon:hover, -.gh-portal-button-uploadicon:focus { - height: 44px; - width: 44px; - border: none; - box-shadow: none; - background: transparent; - border: 1px dashed var(--lightgrey); -} - -.gh-portal-button-uploadicon span { - display: flex; - align-items: center; - justify-content: center; -} - -.gh-portal-button-uploadicon span svg { - width: 18px; - height: 18px; - fill: var(--darkgrey); -} - -.gh-portal-button-uploadicon:hover span svg { - fill: var(--darkgrey); -} - -.gh-portal-button-deleteicon, -.gh-portal-button-deleteicon:hover, -.gh-portal-button-deleteicon:focus { - height: 44px; - width: 44px; - border: none; - color: var(--white); - box-shadow: none; -} - -.gh-portal-button-deleteicon span { - display: flex; - align-items: center; - justify-content: center; -} - -.gh-portal-button-deleteicon span svg { - width: 18px; - height: 18px; -} - -.gh-portal-setting-copy { - position: absolute; - display: flex; - align-items: center; - top: 2px; - right: 2px; - height: 32px; - padding: 0 8px 0 9px; - border-radius: 2px; - background: var(--whitegrey-l2); - border-color: transparent; - box-shadow: none; - font-size: 1.3rem; -} - -.gh-portal-setting-copy span { - margin-top: -2px; -} - -.gh-portal-siteiframe { - pointer-events: none; - transform: scale(0.95) !important; - transform-origin: 0 0; - width: calc((1 / 0.95) * 100%) !important; - height: calc((1 / 0.95) * 100%) !important; -} - -.gh-portal-siteiframe-enabled { - pointer-events: unset; -} - -.gh-portal-site-frame-cover { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: #edf0f2; - overflow: hidden; - border: none; -} - -.gh-portal-settings-title { - padding-bottom: 28px; - position: sticky; - background: var(--main-bg-color); - z-index: 9999; - width: 100%; - top: 0; -} - -.gh-portal-settings-previewheader { - display: flex; - align-items: center; - justify-content: flex-end; - position: relative; - height: 84px; - padding: 0 24px; - margin: 0; - width: 100%; -} - -.gh-portal-settings-previewheader .gh-btn-group .gh-btn { - min-width: 90px; -} - -.gh-portal-settings-actions { - display: flex; - align-items: center; - margin-left: 24px; -} - -.gh-portal-preview-wrapper { - overflow: hidden; - max-height: calc(100vh - 60px - 84px); - height: 100%; - background: linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(249,249,250,1) 100%); -} - -.gh-portal-preview-container { - position: relative; - flex-grow: 1; - overflow: hidden; - background: linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(249,249,250,1) 100%); - max-height: calc(100vh - 60px - 84px); -} - -.gh-portal-preview-wrapper .gh-portal-preview-container { - overflow-x: hidden; - overflow-y: scroll; - margin: 0 -70px; - padding: 0 70px; - border: none; - border-radius: 0; - height: 100%; -} - -.gh-portal-preview-container.hide { - display: none -} - -.gh-portal-links-container { - position: relative; - display: flex; - box-sizing: border-box; - flex-direction: column; - justify-content: flex-start; - overflow: hidden; - font-size: 1.5rem; - letter-spacing: 0; - text-align: left; - letter-spacing: 0; - text-rendering: optimizeLegibility; - background: var(--white); - width: 720px; - padding: 32px; - margin: 95px auto 32px; - border-radius: 5px; - box-shadow: 0 0 0 1px rgba(0,0,0,0.02), 0 2.8px 2.2px rgba(0, 0, 0, 0.02), 0 6.7px 5.3px rgba(0, 0, 0, 0.028), 0 12.5px 10px rgba(0, 0, 0, 0.035), 0 22.3px 17.9px rgba(0, 0, 0, 0.042), 0 41.8px 33.4px rgba(0, 0, 0, 0.05), 0 100px 80px rgba(0, 0, 0, 0.07); -} - -.gh-portal-links-main h2 { - font-weight: 600; - font-size: 1.9rem; -} - -.gh-portal-links-main p { - margin-bottom: 10px; -} - -.gh-portal-links-table { - width: 100%; - padding: 0; - margin: 20px 0 0; -} - -.gh-portal-links-table tr td { - white-space: nowrap; - padding: 10px 12px 0 0; -} - -.gh-portal-links-table tr.header h4 { - font-size: 1.2rem; - text-transform: uppercase; - color: var(--midlightgrey-d2); - font-weight: 500; - margin-bottom: 8px; -} - -.gh-portal-links-table tr.header .gh-portal-links-cell { - font-size: 1.3rem; - font-weight: 500; - cursor: pointer; - text-transform: none; - color: var(--green); -} - -.gh-portal-links-table tr td:last-of-type { - padding-right: 0; -} - -.gh-portal-links-table tr.header .toggle-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.gh-portal-links-table tr td.pagename { - font-size: 1.4rem; - width: 130px; -} - -.gh-portal-links-table tr td.pagename.strong { - font-weight: 600; -} - -.gh-portal-page-url-container { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - font-size: 1.4rem; - padding: 5px 4px 5px 8px; - height: 38px; - background: var(--whitegrey-l2); - border-radius: 4px; - border: 1px solid var(--whitegrey); - color: var(--darkgrey); - font-weight: 500; - width: 100%; -} - -.gh-portal-page-url-container .page-url-field { - font-size: 1.4rem; - border: none; - padding-left: 0; - padding-right: 40px; - color: var(--middarkgrey) !important; - background: none; - cursor: text; -} - -.gh-portal-page-url-container .page-url-slash { - color: var(--midlightgrey); - font-weight: 400; -} - -.gh-portal-page-url-container .page-url-label { - max-width: 470px; - overflow: hidden; - white-space: nowrap; -} - -.gh-portal-page-url-container .page-url-label .page-url-label-inner { - overflow-y: hidden; - overflow-x: scroll; - margin: 0 0 -80px 0!important; - padding: 0 0 80px 0; -} - -.gh-show-modal-link-form .page-url-label { - max-width: 230px; -} - -.gh-portal-page-url-container .page-url-disabled { - color: var(--midgrey); - font-weight: 400; -} - -.gh-portal-links-group-divider { - margin: 8px -32px; - border-top-color: var(--whitegrey); -} - -.gh-portal-links-group-divider.first { - margin-top: -4px; -} - -.gh-portal-custom-icon { - display: flex; - justify-content: center; - width: 50px; -} - -.gh-portal-button-custom.selected-icon:hover { - box-shadow: 0px 0px 0px 1px color-mod(var(--blue) a(40%)); -} - -.gh-portal-custom-icon:hover .gh-portal-button-custom.selected-icon { - display: none; -} - -.gh-portal-custom-icon:hover .gh-portal-button-deleteicon { - display: inline-block; -} - -.gh-portal-custom-icon .gh-portal-button-deleteicon { - display: none; - background: color-mod(var(--darkgrey) a(0.8)); -} - -.gh-portal-setting-no-stripe { - padding: 20px; - font-size: 1.3rem; - text-align: center; - background: var(--whitegrey-l2); - border: 1px solid var(--whitegrey); - border-radius: 4px; - color: var(--midgrey); -} - -.gh-portal-setting-section.redirects p { - margin-top: 4px; -} - -.gh-portal-emailupdate-button { - width: 100%; - margin-top: 8px; -} - -.gh-portal-emailupdate-button[disabled], -.gh-portal-emailupdate-button[disabled]:hover { - background: var(--whitegrey-d2) !important; - color: var(--darkgrey) !important; - cursor: auto; -} - -.gh-portal-settings-sidebar .modal-fullsettings-tab-expanded { - padding-top: 16px; -} - -.gh-portal-settings-previewheader .gh-preview-page-selector, -.gh-portal-settings-previewheader .gh-preview-page-selector select { - width: 160px; -} - -.gh-portal-settings-previewheader .gh-preview-page-selector svg { - top: 16px; -} - -.gh-portal-settings-stripefooter { - padding: 24px; - margin: 32px; - border: 1px solid var(--whitegrey); - border-radius: 4px; -} - -.gh-portal-settings-stripefooter h4 { - font-weight: 600; - font-size: 1.5rem; - margin-bottom: 12px; -} - -.gh-portal-settings-stripefooter p { - font-size: 1.35rem; - margin-bottom: 10px; - color: var(--darkgrey); -} - -.gh-portal-settings-stripefooter .stripe-connect { - margin-top: 20px; - width: 100%; -} diff --git a/ghost/admin/app/styles/layouts/post-history.css b/ghost/admin/app/styles/layouts/post-history.css index 807bd1065f0..b9a3c1f219d 100644 --- a/ghost/admin/app/styles/layouts/post-history.css +++ b/ghost/admin/app/styles/layouts/post-history.css @@ -29,6 +29,27 @@ background: var(--whitegrey-l2); } +.user-list-item-figure { + position: relative; + display: block; + width: 36px; + height: 36px; + margin-right: 12px; + margin-left: 3px; + background-position: center center; + background-size: cover; + border-radius: 100%; +} + +.user-list-item-figure img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; +} + .gh-post-history .user-list-item-figure { width: 2.8rem; height: 2.8rem; diff --git a/ghost/admin/app/styles/layouts/posts.css b/ghost/admin/app/styles/layouts/posts.css index ba6259d1ab9..1496804eeff 100644 --- a/ghost/admin/app/styles/layouts/posts.css +++ b/ghost/admin/app/styles/layouts/posts.css @@ -8,3 +8,342 @@ animation-duration: .001s; pointer-events: none; } + +/* Debug screen +/* ---------------------------------------------------------- */ +.gh-email-debug .gh-post-analytics-header .gh-canvas-header-content { + border-bottom: none; + padding-bottom: 0; +} + +.gh-email-debug-error { + display: flex; + margin-top: 20px; + padding: 12px 16px 12px 40px; +} + +.gh-email-debug-error svg { + height: 20px !important; + margin-top: -2px; +} + +.gh-email-debug-errortext { + flex-grow: 1; + margin-left: 4px; +} + +.gh-email-debug-error h4 { + font-size: 1.5rem; + font-weight: 600; + margin: 0; + padding: 0; +} + +.gh-email-debug-error button { + min-width: 80px; +} + +.gh-email-debug-settingstab-icon { + width: 20px; + height: 20px; + margin-bottom: 11px; + margin-top: 2px; +} + +.gh-email-debug-settingstab-icon svg path, +.gh-email-debug-settingstab-icon svg circle { + stroke-width: 2; +} + +.gh-email-debug .gh-list { + border-bottom: none; +} + +.gh-email-debug .gh-list thead, +.gh-email-debug .gh-list tbody { + width: 100%; +} + +.gh-email-debug .gh-list tr:first-of-type .gh-list-data { + border-top: none; +} + +.gh-email-debug .gh-list tr .gh-list-data:first-of-type { + padding-left: 0; +} + +.gh-email-debug-col-member { + padding-left: 0; + width: 25%; +} + +.gh-email-debug-member { + display: flex; + flex-wrap: nowrap; + align-items: center; +} + +.gh-email-debug-failure { + display: flex; +} + +.gh-email-debug-failure svg { + height: 16px; + width: 16px; + margin-right: 4px; +} + +.gh-email-debug-failure-details { + display: flex; + flex-direction: column; + gap: 2px; + width: 100%; +} + +.gh-email-debug-failure-codes { + display: flex; + gap: 20px; +} + +.gh-email-debug-failure-code { + color: var(--midgrey); +} + +.gh-email-debug-failure-code span { + color: var(--darkgrey); + font-weight: 500; +} + +.gh-email-debug .gh-list-data { + height: 98px; +} + +.gh-email-debug-permanent-failures .gh-list-data, +.gh-email-debug-temporary-failures .gh-list-data { + height: 80px; +} + +.gh-email-debug-batch-col-status span { + display: inline-block; + position: relative; + padding-left: 16px; + color: var(--midlightgrey); +} + +.gh-email-debug-batch-col-status span::before { + display: block; + position: absolute; + content: ""; + top: 6px; + left: 0; + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--midlightgrey); +} + +.gh-email-debug-batch-col-status .failed { + color: color-mod(var(--red) l(-2%)); +} + +.gh-email-debug-batch-col-status .failed::before { + background: var(--red); +} + +.gh-email-debug-batch-col-status .submitting { + color: var(--blue); +} + +.gh-email-debug-batch-col-status .submitting::before { + background: var(--blue); +} + +.gh-email-debug-batch-col-status .submitted { + color: var(--green); +} + +.gh-email-debug-batch-col-status .submitted::before { + background: var(--green); +} + +.gh-email-debug-batch-col-created, +.gh-email-debug-batch-col-details { + color: var(--midgrey); +} + +.gh-email-debug-batch-col-created span, +.gh-email-debug-batch-col-segment span { + white-space: nowrap; +} + +.gh-email-debug-batch-col-details .detailtext div { + word-break: break-all; +} + +.gh-email-debug-batch-col-details .detailtext .noselect { + user-select: none; +} + +.gh-email-debug-batch-col-details .detailtext div code { + white-space: unset; + word-break: normal; + user-select: text; +} + +.gh-email-debug-batch-col-details .error { + color: color-mod(var(--red) l(-2%)); + font-weight: unset; +} + +.gh-email-debug-batch-col-segment span { + display: inline-block; + border-radius: 2px; + background: color-mod(var(--black) a(5%)); + padding: 1px 6px; + color: var(--middarkgrey); +} + +.gh-email-debug-batch-col-details span { + color: var(--darkgrey); + font-weight: 500; +} + +.gh-email-debug-batch-col-details .download-icon { + width: 20px; + height: 20px; + margin-left: 20px; +} + +.gh-email-debug-batch-col-details .download-icon path { + stroke: var(--midgrey); +} + +.gh-email-debug-batch-col-details .detailtext { + flex-grow: 1; +} + +.gh-email-debug-settings { + font-size: 1.3rem; + margin: 12px 0 20px; +} + +.gh-email-debug-settings .gh-type-number { + font-variant-numeric: tabular-nums; +} + +.gh-email-debug-settings hr { + margin: 8px 0; + border-top-color: var(--whitegrey); +} + +.gh-email-debug-settings tr td { + font-weight: 500; + padding: 6px 0; +} + +.gh-email-debug-settings tr td:first-of-type { + width: 30%; + white-space: nowrap; + color: var(--midgrey); +} + +.gh-email-debug-settings-icon svg { + width: 14px; + height: 14px; +} + +.gh-email-debug-settings-icon .check { + width: 18px; + height: 18px; +} + +.gh-email-debug-settings-icon .check path { + stroke: var(--green); + stroke-width: 2.5; +} + +.gh-email-debug-settings-icon .x path { + stroke: var(--midgrey); +} + +.gh-email-debug-schedule-analytics { + display: flex; + align-items: center; + width: max-content; + margin: .8rem 0 0; + color: var(--green-d1); +} + +.gh-email-debug-schedule-analytics svg { + width: 1rem; + height: 1rem; + margin-right: 6px; +} + +.gh-email-debug-schedule-analytics svg g { + stroke: var(--green-d1); + stroke-width: 3px; +} + +.gh-email-debug-empty-list { + margin: 120px 40px; + text-align: center; + font-size: 1.3rem; + color: var(--midgrey); +} + +.gh-email-debug-readmore-error { + display: inline-flex; + width: 100%; +} + +.gh-email-debug-readmore-error label { + order: 3; + cursor: pointer; +} + +.gh-email-debug-readmore-error .toggle-checkbox { + display: none; +} + +.gh-email-debug-readmore-error span { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + overflow: hidden; + color: color-mod(var(--red) l(-2%)); + word-break: break-all; +} + +.gh-email-debug-readmore-error .toggle-checkbox:checked ~ span { + display: block; + -webkit-box-orient: unset; + -webkit-line-clamp: unset; + overflow: unset; + word-break: break-word; +} + +.gh-email-debug-readmore-error .toggle-checkbox:checked ~ label { + display: none !important; +} + +.gh-email-debug-readmore-error svg { + width: 12px; + height: 12px; + margin: 0; +} + +.gh-email-debug-readmore-error svg circle { + fill: var(--midgrey); +} + +.gh-email-debug-readmore-error label { + display: flex; + align-items: center; + padding: 0 4px; + border-radius: 2px; + height: 14px; + margin-top: 3px; + margin-left: 8px; + background: var(--whitegrey); +} diff --git a/ghost/admin/app/styles/layouts/settings.css b/ghost/admin/app/styles/layouts/settings.css deleted file mode 100644 index 23b1da32528..00000000000 --- a/ghost/admin/app/styles/layouts/settings.css +++ /dev/null @@ -1,3906 +0,0 @@ -/* Settings menu -/* ---------------------------------------------------------- */ -.gh-nav-settings-close { - margin: 26px 0; - padding: 0; -} - -.gh-nav-settings-close h4 { - display: flex; - width: 100%; - align-items: center; - justify-content: space-between; - padding: 2px 28px; - font-size: 1.9rem; -} - -.gh-nav-settings-close a { - display: flex; - padding: 5px 4px 2px; - width: 30px; - height: 30px; - border-radius: 999px; - margin: 0 -12px 0 0; - align-items: center; - justify-content: center; -} - -.gh-nav-settings-close a:hover { - background: var(--mainmenu-color-active-bg); -} - -.gh-nav-settings-close a svg { - width: 16px; - height: 16px; - margin-top: -3px; -} - -.gh-nav-settings-main { - margin: 7px 0; -} - -.gh-nav-settings-main .active { - background: none !important; - font-weight: 400; - color: color-mod(var(--middarkgrey) l(-10%)); -} - - -/* Settings -/* ---------------------------------------------------------- */ -.gh-settings-main-grid { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-auto-rows: minmax(72px, auto); - background: var(--white); - border-radius: .5rem;; - grid-gap: 24px; - margin: 24px 0 96px; -} - -.gh-settings-main-grid .gh-setting-group { - display: flex; - color: var(--darkgrey); - min-height: 72px; - text-align: left; -} - -.gh-settings-main-grid .gh-setting-group span { - display: flex; - align-items: center; - justify-content: center; - padding: 5px; - background: var(--black); - width: 48px; - height: 48px; - min-width: 48px;; - border-radius: 999px; - color: #fff; -} - -.gh-settings-main-grid .gh-setting-group span.yellow { - background: var(--yellow); -} - -.gh-settings-main-grid .gh-setting-group span.green { - background: var(--green); -} - -.gh-settings-main-grid .gh-setting-group span.blue { - background: var(--blue); -} - -.gh-settings-main-grid .gh-setting-group span.pink { - background: var(--pink); -} - -.gh-settings-main-grid .gh-setting-group:hover span { - opacity: 0.9; -} - -.gh-settings-main-grid .gh-setting-group svg { - width: 20px; - height: 20px; -} - -.gh-settings-main-grid .gh-setting-group.portal svg { - width: 24px; - height: 24px; -} - -.gh-settings-main-grid .gh-setting-group div { - margin-left: 14px; - flex-shrink: 1; -} - -.gh-settings-main-grid .gh-setting-group h4 { - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; - margin: 4px 0 2px; -} - -.gh-settings-main-grid .gh-setting-group p { - color: var(--midgrey); - margin: 4px 0 0; - padding: 0; - line-height: 1.4em; -} - -@media (max-width: 1100px) { - .gh-settings-main-grid { - grid-template-columns: 1fr 1fr; - } -} - -@media (max-width: 680px) { - .gh-settings-main-grid { - grid-template-columns: 1fr; - } -} - -/* Setting headers */ - -.gh-setting-header { - color: var(--black); - text-transform: uppercase; - font-weight: 500; - letter-spacing: 0.03em; - font-size: 1.1rem; - padding: 8px 0; - border-bottom: 1px solid var(--main-color-area-divider); - margin: 0 0 0 1px; -} - -.gh-first-header { - margin-top: 0; -} - -.gh-setting, -.gh-setting-first, -.gh-setting-last { - display: flex; - justify-content: space-between; - padding: 18px 0; - margin: 0; -} - -.gh-setting-first { - border: none; - padding-top: 0px; -} - -.gh-setting-first .description-container { - margin-bottom: 0; -} - -.gh-setting-last { - padding-bottom: 0px; -} - -.gh-setting-content { - width: 100%; - margin: 0 50px 0 0; -} - -.gh-members-setting-content { - width: 100%; - margin: 0; -} - -.gh-setting-content--no-action { - margin: 0; -} - -.gh-setting-title { - display: block; - margin-bottom: 12px; - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.15em; - font-weight: 600; - color: var(--black); -} - -.gh-setting-title.m { - font-size: 1.4rem; - font-weight: 500; -} - -.gh-setting-title.mb0 { - margin-bottom: 0; -} - -.gh-setting-desc { - line-height: 1.4em; - color: var(--middarkgrey); - letter-spacing: 0.3px; - font-size: 1.3rem; - font-weight: 400; - margin: 8px 0 0; -} - -.gh-setting-desc.mb0 { - margin-bottom: 0; -} - -.gh-setting-title + .gh-setting-desc { - margin-top: -6px; -} - -.gh-setting-error { - margin-top: 1em; - line-height: 1.3em; - color: var(--red); - font-weight: 300; - letter-spacing: 0.3px; -} - -:is(.gh-setting-title, .gh-setting-desc) + .gh-setting-error { - margin-top: -4px; - font-size: 1.3rem; -} - -.gh-setting-title + .gh-setting-error { - margin-top: 8px; -} - -.gh-setting-action { - flex-shrink: 0; - margin: 1px 0 0 0; - align-self: center; -} - -.gh-setting-action .for-checkbox label, -.gh-setting-action .for-radio label { - padding-bottom: 0; - margin-bottom: 0; -} - -.gh-setting-content-extended label { - display: block; - font-size: 1.3rem; - font-weight: 600; - color: var(--darkgrey); - margin-bottom: 4px; -} - -.gh-setting-content-extended textarea { - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.4em; - max-width: initial; -} - -.gh-setting-content-extended .gh-image-uploader { - margin: 0; - border: 1px solid var(--whitegrey-d2); -} - -.gh-setting-content-extended .gh-btn span { - height: 36px; - line-height: 36px; -} - -.gh-setting-liquid-section .liquid-container, -.gh-setting-liquid-section .liquid-child { - padding: 0 20px; - margin: 0 -20px; -} - -.gh-settings-portal-section { - box-shadow: - 0 0 1px rgba(0,0,0,.07), - 0 1.5px 1.2px -11px rgba(0, 0, 0, 0.028), - 0 5.1px 4px -11px rgba(0, 0, 0, 0.042), - 0 23px 18px -16px rgba(0, 0, 0, 0.07) - ; -} - -.gh-settings-portal-border { - position: absolute; - content: ""; - top: -5px; - right: -5px; - left: -5px; - bottom: -5px; - border: 1px solid var(--blue); - border-radius: 8px; -} - - -/* Images */ - -.gh-setting-action-smallimg { - position: relative; -} - -.gh-setting-action-smallimg img, -.gh-setting-action-smallimg input[type="image"] { - height: 50px; - width: auto; - max-width: 250px; -} - -.gh-setting-action-largeimg img, -.gh-setting-action-largeimg input[type="image"] { - min-height: 80px; - width: auto; - max-width: 250px; -} - -@media (max-width: 500px) { - .gh-setting-action-largeimg img, - .gh-setting-action-largeimg input[type="image"] { - max-width: 190px; - } -} - -.gh-setting-action-smallimg img:hover, -.gh-setting-action-largeimg img:hover, -.gh-setting-action-smallimg input[type="image"], -.gh-setting-action-largeimg input[type="image"] { - cursor: pointer; -} - -.gh-setting-action-smallimg-delete, -.gh-setting-action-largeimg-delete { - display: flex; - flex-direction: column; - align-items: center; - color: var(--midgrey); - margin-top: 8px; - color: var(--whitegrey); - text-decoration: none; - font-size: 13px; - line-height: 10px; -} - -.gh-setting-action-smallimg-delete:hover, -.gh-setting-action-largeimg-delete:hover { - color: var(--white); - text-decoration: underline; -} - -.gh-setting-action .gh-progress-container { - width: 113px; - height: 100%; -} - -.gh-setting-action .gh-progress-container-progress { - width: 100%; -} - -.gh-setting-action .gh-progress-bar { - height: 9px; -} - -/* Checkboxes */ - -.gh-setting-action .input-toggle-component { - float: none; - margin-right: 0; - width: 24px; - height: 24px; -} - -.gh-setting-action .input-toggle-component:before { - top: 6px; - left: 5px; - width: 12px; - height: 7px; -} - -.gh-setting-content-extended { - width: 100%; -} - -/* Theme Directory -/* ---------------------------------------------------------- */ - -.gh-td-marketplace { - display: inline-block; - outline: none; - color: var(--green-d1); - font-weight: 500; - text-decoration: none !important; - text-transform: none; -} - -.gh-td-marketplace span { - display: block; - overflow: hidden; - font-size: 1.35rem; - letter-spacing: 0.2px; -} - -.gh-td-marketplace span svg { - position: relative; - top: 1px; - width: .7em; - height: .7em; - margin-left: 4px; -} - -.gh-td-marketplace span svg path { - stroke: var(--green-d1); - stroke-width: 4px; -} - -.td-item { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - text-align: center; - text-decoration: none; - color: var(--darkgrey); - transition: all .8s ease; -} - -.td-item img { - box-shadow: 0 0 1px rgba(0,0,0,.02), 0 9px 25px -10px rgba(0,0,0,0.2); - transition: all .8s ease; - border-radius: 3px; -} - -.td-item svg circle { - stroke: var(--midlightgrey); -} - -.td-item:hover { - transform: translateY(-1%); - transition: all .3s ease; -} - -.td-item:hover img { - box-shadow: 0 0 1px rgba(0,0,0,.02), 0 19px 35px -14px rgba(0,0,0,.2); - transition: all .3s ease; -} - -.td-item-desc { - display: flex; - width: 100%; - margin-top: 16px; - text-transform: uppercase; - font-weight: 700; -} - -.td-item-category { - display: inline-flex; - align-items: center; - margin-left: 4px; - text-transform: none; - font-weight: 400; - font-size: 1em; - color: color-mod(var(--midgrey) l(-5%)); -} - -.td-item-screenshot { - line-height: 0; - border-radius: 3px; -} - -.td-item-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.td-item-overlay:hover, -.td-item-overlay:focus { - background-color: var(--white-90); - transition: all .3s ease; -} - -.td-item-action { - visibility: hidden; - opacity: 0; - transition: visibility 0s, opacity 0.3s ease;} - -.td-item-overlay:hover .td-item-action { - visibility: visible; - opacity: 1; -} - -.td-item-action.gh-btn { - width: 80px; -} - -@media (max-width: 1000px) { - .td-item:nth-child(4), - .td-item:nth-child(5), - .td-item:nth-child(6) { - display: flex; - } -} - -/* General -/* ---------------------------------------------------------- */ -.gh-general-settings { - display: grid; - grid-template-columns: 2fr 1fr; - grid-gap: 80px; -} - -@media (max-width: 1200px) { - .gh-general-settings { - display: flex; - flex-direction: column; - gap: 0; - } - - .gh-general-settings .gh-copyright-info { - max-width: 620px !important; - } -} - -.gh-general-settings .gh-expandable-header .gh-expandable-description { - max-width: 620px; -} - -.gh-seo-settings { - display: flex; -} - -@media (max-width: 1360px) { - .gh-seo-settings { - flex-direction: column; - } - - .gh-seo-settings .form-group { - max-width: 100%; - } - - .gh-seo-settings-left, - .gh-seo-container { - max-width: 591px; - } -} - -@media (min-width: 1360px) { - .gh-seo-settings-left { - margin-right: 2.4rem; - } - - .gh-seo-container { - max-width: 1091px; - } -} - -.gh-seo-container { - display: flex; - width: 100%; - margin-bottom: 2.4rem; - padding: 20px 30px 16px; - border: 1px solid var(--whitegrey-d1); - font-family: Arial, sans-serif; - background: #fff; - border-radius: 3px; -} - -.gh-seo-container svg { - width: 92px; - height: 30px; - margin-right: 32px; -} - -.gh-general-settings .gh-seo-settings { - flex-direction: column; -} - -.gh-general-settings .gh-seo-settings .form-group { - max-width: 100%; -} - -.gh-general-settings .gh-seo-settings-left, -.gh-general-settings .gh-seo-container { - max-width: 591px; -} - -.gh-twitter-settings { - display: flex; -} - -@media (max-width: 1360px) { - .gh-twitter-settings { - flex-direction: column; - } - - .gh-twitter-settings .form-group { - max-width: 100%; - } - - .gh-twitter-settings-left { - max-width: 591px; - } -} - -@media (min-width: 1360px) { - .gh-twitter-settings-left { - margin-right: 2.4rem; - } -} - -.gh-twitter-container { - width: 591px; - margin-bottom: 2.4rem; - border: 1px solid var(--whitegrey-d1); - background: #fff; - border-radius: 3px; -} - -@media (max-width: 1080px) { - .gh-twitter-container { - width: 100%; - max-width: 591px; - } -} - -.gh-general-settings .gh-twitter-settings { - flex-direction: column; -} - -.gh-general-settings .gh-twitter-settings .form-group { - max-width: 100%; -} - -.gh-general-settings .gh-twitter-settings-left, -.gh-general-settings .gh-twitter-container { - max-width: 591px; -} - -.gh-og-settings { - display: flex; -} - -@media (max-width: 1360px) { - .gh-og-settings { - flex-direction: column; - } - - .gh-og-settings .form-group { - max-width: 100%; - } - - .gh-og-settings-left { - max-width: 591px; - } -} - -@media (min-width: 1360px) { - .gh-og-settings-left { - margin-right: 2rem; - } -} - -.gh-og-container { - width: 476px; - margin-bottom: 2.4rem; - border: 1px solid var(--whitegrey-d1); - background: #fff; - border-radius: 3px; -} - -@media (max-width: 1080px) { - .gh-og-container { - width: 100%; - max-width: 476px; - } -} - -.gh-general-settings .gh-og-settings { - flex-direction: column; -} - -.gh-general-settings .gh-og-settings .form-group { - max-width: 100%; -} - -.gh-general-settings .gh-og-settings-left, -.gh-general-settings .gh-og-container { - max-width: 591px; -} - -.gh-general-settings .gh-about-box { - margin-top: 19px; - position: relative; - top: unset; - right: unset; -} - -.gh-general-settings .gh-copyright-info { - border-top: none; - max-width: 350px; - margin-top: 12px; - padding-top: 0; -} - -.gh-about-links { - margin: 0; - padding: 0; - list-style: none; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--lightgrey-l2); -} - -.gh-about-links li { - margin: 0 0 4px; - font-size: 1.4rem; - line-height: 1.8em; -} - -.gh-about-links li a span { - display: flex; - align-items: center; -} - -.gh-about-links li a span svg { - width: 16px; - height: 16px; - margin-right: 6px; -} - -.gh-about-links li a { - color: var(--middarkgrey); -} - -.gh-about-links li a:hover { - color: var(--black); -} - -.gh-about-links:last-of-type { - margin-bottom: -12px; -} - -.gh-about-links .calendar-icon { - width: 14px; - height: 14px; - margin-right: 8px; -} - -.gh-about-links li a:hover .hover-stroke path { - stroke: var(--black); -} - -.gh-about-links li a:hover .hover-fill path { - fill: var(--black); -} - -/* Navigation -/* ---------------------------------------------------------- */ - -.gh-blognav-container { - padding: 25px 0; - border-top: var(--lightgrey) 1px solid; -} - -.gh-blognav { - margin: 8px 0 0; -} - -.gh-blognav-item { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.gh-blognav-item--error { - margin-bottom: calc(1em + 10px); -} - -.gh-blognav-item .response { - position: absolute; - margin-bottom: 0; -} - -.gh-blognav-grab { - padding: 6px 16px 0 0; - width: 16px; - text-indent: -4px; - cursor: move; -} - -.gh-blognav-grab svg { - width: 16px; - height: 16px; - fill: color-mod(var(--midgrey) l(+15%)); -} - -.gh-blognav-line { - display: flex; - width: 100%; -} - -.gh-blognav-label { - flex-grow: 1; - margin-right: 10px; -} - -.gh-blognav-url { - flex-grow: 3; -} - -.gh-blognav-delete { - padding: 8px 0 8px 10px; - display: flex; - align-items: center; - color: var(--midgrey); - transition: fill 0.1s linear; -} - -.gh-blognav-delete:hover { - color: var(--red); -} - -.gh-blognav-delete svg { - height: 14px; - width: 14px; -} - -.gh-blognav-add { - margin-right: -1px; - margin-left: 9px; - width: 16px; - height: 16px; - background: var(--green); - color: var(--white); - border-radius: 2px; - transition: background 0.1s linear; - display: flex; - align-items: center; - justify-content: center; -} - -.gh-blognav-add svg { - height: 9px; - width: 9px; -} - -.gh-blognav-add:hover, -.gh-blognav-add:focus { - background: color-mod(var(--green) lightness(-10%)); -} - -.gh-blognav-item:not(.gh-blognav-item--sortable) { - padding-left: 16px; - margin-bottom: 0; -} - -/* Remove space between inputs on smaller screens */ -@media (max-width: 800px) { - .gh-blognav-label { - margin-right: -1px; - } - .gh-blognav-label input { - border-right-color: color-mod(var(--lightgrey) l(-5%) s(-10%)); - border-radius: 4px 0 0 4px; - } - .gh-blognav-url input { - border-left-color: color-mod(var(--lightgrey) l(-5%) s(-10%)); - border-radius: 0 4px 4px 0; - } - .gh-blognav-item input:focus { - position: relative; - z-index: 100; - } -} - - -/* Email newsletter -/* ---------------------------------------------------------- */ - -.gh-setting-email hr { - margin: 4.8em 0; -} - -.gh-email-design-typography-wrapper .gh-setting-dropdown .ember-power-select-status-icon { - right: 16px; -} - -.gh-mailgun-region { - width: 140px !important; - margin-right: 12px; -} - -.gh-mailgun-region .ember-power-select-trigger { - padding: 6px 12px; - white-space: nowrap; - background: var(--white); -} - -.gh-mailgun-region .ember-power-select-selected-item { - margin-left: 0; -} - -.gh-mailgun-region .ember-power-select-trigger svg { - position: absolute; - top: 50%; - right: 16px; - width: 10px; - height: 6px; -} - - -/* Email newsletter LABS -/* ---------------------------------------------------------- */ - -.gh-newsletters .gh-dropdown-archived { - color: var(--darkgrey); - font-size: 1.35rem; - font-weight: 400; -} - -.gh-newsletters .gh-expandable-title { - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; - color: var(--black); - margin: 0; - padding: 0; -} - -.gh-newsletters .gh-expandable-description { - margin: 0 0 1.8rem; - padding: 0; - color: var(--midgrey); - font-size: 1.3rem; - font-weight: 400; -} - -.gh-newsletters .gh-expandable-block { - padding: 24px; - - /* Correct last card margin by reducing bottom padding */ - padding-bottom: 12px; -} - -.gh-newsletter-tracking { - padding: 16px 20px; - background: var(--main-bg-color); - box-shadow: 0 1px 4px -1px rgb(0 0 0 / 10%); - border-radius: 3px; -} - -.gh-newsletter-tracking-row { - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid var(--whitegrey); - padding: 16px 0; -} - -.gh-newsletter-tracking-row:first-child { - padding-top: 0; -} - -.gh-newsletter-tracking-row:last-child { - padding-bottom: 0; - border-bottom: none; -} - -.gh-newsletter-tracking-title { - font-weight: 600; - font-size: 1.4rem; - margin-bottom: 0; -} - -.gh-newsletter-card-container { - margin-left: -24px; - padding-left: 24px; - position: relative; -} - -.gh-newsletters .gh-main-content-card:last-of-type { - /* Note that we need to keep the margin of the cards consistent, or they'll jump when we drag the cards to sort them */ - margin-bottom: 12px; -} - -.gh-newsletter-card { - position:relative; - display: flex; - align-items: center; - justify-content: space-between; -} - -.gh-newsletter-card-container[draggable="true"] { - /* required to avoid background being picked up when dragging (Chrome only) */ - z-index: 1; -} - -.gh-newsletter-card-container .grab-newsletter { - visibility: hidden; - position: absolute; - top: 0; - left: 8px; - width: 2rem; - height: 100%; - padding-right: 2px; - fill: var(--lightgrey-d2); - cursor: move; - opacity: 0; - transition: visibility 200ms step-end, opacity 200ms ease-in-out; -} - -.gh-newsletter-card { - transition: margin 125ms ease-in-out, padding 125ms ease-in-out; -} - -/* - When dragging, the :hover selector could be on the wrong element. So we explicitly ignore :hover when we are dragging and switch to the .is-dragging-object -*/ -body:not([data-user-is-dragging]) .gh-newsletter-card-draggable:hover .gh-newsletter-card, .gh-newsletter-card-draggable.is-dragging-object .gh-newsletter-card { - margin-left: 4px; - padding-left: 20px; -} - -body:not([data-user-is-dragging]) .gh-newsletter-card-draggable:hover .grab-newsletter, .gh-newsletter-card-draggable.is-dragging-object .grab-newsletter { - visibility: visible; - opacity: 1; - - /* - To make sure the grab handler also fades out correctly and only change visibility after the animation, - we need to update the animation curve for visibility to step-start at the start of the animation - */ - transition: visibility 200ms step-start, opacity 200ms ease-in-out; -} - -.gh-newsletter-card-container .grab-newsletter { - display: none; -} - -.gh-newsletter-card-draggable .grab-newsletter { - display: inline-block; -} - -.gh-newsletter-card-block.title-block { - flex-basis: 60%; -} - -.gh-newsletter-card-block.stats-block { - display: grid; - flex-basis: 30%; - grid-template-columns: 1fr 1fr; -} - -.gh-newsletter-card-block.stats-block.multiple { - margin-right: -4.4rem; -} - -.gh-newsletter-card-block.cta-block { - display: flex; -} - -.gh-newsletter-card-block:not(:first-of-type) { - padding-left: 16px; -} - -.gh-newsletter-card-block h4 { - font-size: 1.3rem; - font-weight: 500; -} - -.gh-newsletter-card-block h4 .counter { - font-weight: 400; - color: var(--midgrey); -} - -.gh-newsletter-card-name { - display: flex; - align-items: center; - margin: 0; - font-size: 1.8rem; - font-weight: 600; - line-height: 1.3em; -} - -.stats-block .gh-newsletter-card-name { - font-size: 1.7rem; -} - -.gh-newsletter-card-description { - font-size: 1.3rem; - line-height: 1.45em; - margin: 4px 20px 4px 0; - color: var(--midgrey); -} - -.gh-newsletter-card-button-container { - position: absolute; - right: 24px; - top: 24px; - margin-right: 0; -} - -.gh-newsletter-actions-menu { - margin-top: 4px; - background: none; -} - -.gh-add-newsletter { - display: flex; - align-items: center; - width: max-content; - margin: .8rem 0 0; - color: var(--green-d1); -} - -.gh-add-newsletter svg { - width: 1rem; - height: 1rem; - margin-right: 6px; -} - -.gh-newsletters-setting-sectionheading { - font-size: 1.1rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.2px; - color: var(--midgrey-l1); - margin: 0 0 16px; - padding: 0 0 8px; - border-bottom: 1px solid var(--whitegrey); -} - -.gh-newsletters-labs .form-group.vertical p { - margin: 4px 0 0; -} - -.gh-newsletters-labs .modal-fullsettings-uploader { - flex-grow: 1; - margin: 0; -} - -.gh-newsletters-labs .gh-header-img-uploadicon { - background: var(--whitegrey-l2); -} - -.gh-newsletters-labs .gh-email-design-typography-wrapper.header .gh-btn-group { - background: var(--whitegrey-d1); -} - -.gh-newsletters-labs .gh-stack-item .tooltip-top-left::before { - width: max-content; - max-width: 320px; - white-space: normal; - word-break: break-word; - text-align: left; -} - -.gh-newsletters-labs .gh-members-emailsettings-footer { - margin-top: auto; - margin-bottom: 0; -} - -.gh-newsletters-labs .gh-members-emailpreview-container { - margin: 16px auto 32px; -} - -.gh-newsletters-labs .gh-members-emailpreview-faux { - height: 88px; - background: var(--whitegrey-l2); -} - -.gh-newsletters-labs .gh-members-emailpreview-faux .dark, -.gh-newsletters-labs .gh-members-emailpreview-faux .strong { - color: var(--darkgrey); - font-size: 1.4rem; - font-weight: 600; -} - -.gh-newsletters-labs .gh-members-emailpreview-faux p { - color: var(--midgrey-l2); -} - -/* Customise email newsletter LABS -/* ---------------------------------------------------------- */ - -.modal-fullsettings-form-labs.email-design .modal-fullsettings-tab-expanded { - padding: 20px 32px; -} - -.modal-fullsettings-form-labs.email-design .gh-stack-item { - margin: 12px 0; -} - - -/* Code injection -/* ---------------------------------------------------------- */ - -.settings-code { - max-width: 100%; -} - -.settings-code label { - font-size: 1.5rem; - letter-spacing: 0; - margin-bottom: 2px; -} - -.settings-code p { - margin: 0 0 8px; - font-size: 1.3rem; -} - -.settings-code code { - background-color: rgb(242, 244, 247); - border: 1px solid var(--lightgrey); - vertical-align: middle; - font-size: 1.2rem; -} - -.settings-code-editor { - padding: 0; - min-width: 250px; - min-height: 300px; - max-width: 1224px; - width: calc(100vw - 416px) !important; - height: auto; - line-height: 22px; - border: 1px solid var(--lightgrey); -} - -.settings-code-editor:hover { - cursor: text; -} - -.settings-code-editor textarea { - width: 100%; - max-width: none; - min-height: 300px; - line-height: 22px; - border: none; -} - -.settings-code-editor .CodeMirror { - padding: 0; - border: none; - border-radius: inherit; - background: var(--white); - color: var(--darkgrey); -} - -.settings-code-editor .CodeMirror-gutters { - background-color: var(--whitegrey-l2); - border-right: 1px solid var(--lightgrey); -} - -.settings-code-editor .CodeMirror-cursor { - border: 1px solid var(--midgrey); -} - -.settings-code-editor .cm-s-xq-light span.cm-meta { - color: #000; -} - -@media (max-width: 800px) { - .settings-code-editor { - width: calc(100vw - 8vw - 40px) !important; - } -} - - - -/* Labs -/* ---------------------------------------------------------- */ - -#startupload { - line-height: inherit; -} - -@media (max-width: 500px) { - #importfile { - flex-direction: column; - } - #importfile input { - width: 150px; - } - - #startupload { - margin-left: 0; - margin-top: 5px; - } -} - -.gh-import-errors { - position: relative; - padding: 12px 10px 14px 10px; - border: 1px solid var(--lightgrey); - border-left-width: 5px; - border-left-color: var(--red); - color: var(--midgrey); - line-height: 1.4em; - letter-spacing: 0.2px; - background: #fff; - border-radius: 5px; - margin-top: 18px; - margin-bottom: 25px; -} - -.gh-import-errors-alert { - border-left-color: color-mod(var(--yellow) l(-8%) s(+10%)); -} - -.gh-import-errors-title { - margin-bottom: 1em; - font-size: 1.8rem; - line-height: 1.15em; - font-weight: 600; - color: var(--red); -} - -.gh-import-errors-alert .gh-import-errors-title { - color: color-mod(var(--yellow) l(-8%) s(+10%)); -} - -.gh-import-error { - margin-bottom: 1.75em; -} - -.gh-import-error:last-of-type { - margin-bottom: 0; -} - -.gh-import-error-message { - margin-bottom: 0.5em; - font-weight: 300; -} - -.gh-import-error-entry pre { - margin: 0; - font-size: 10px; -} - -.gh-setting-linkrow:hover { - background: var(--whitegrey-l2); -} - -/* Themes -/* ---------------------------------------------------------- */ - -@media (max-width: 500px) { - .gh-themes-container .apps-configured { - justify-content: flex-end; - } - .gh-themes-container .apps-card-meta { - flex-basis: auto; - } -} - -/*Errors */ -.theme-validation-container { - overflow-y: auto; - margin: -32px -32px 0; - padding: 32px 32px 0; - max-height: calc(100vh - 20vw); -} - -@media (max-height: 960px) { - .theme-validation-container { - max-height: calc(100vh - 180px); - } -} - -.theme-validation-container .gh-image-uploader { - justify-content: center; -} - -.theme-validation-container .gh-image-uploader .description { - color: var(--green-d1); - font-weight: 500; -} - -.theme-validation-container .gh-image-uploader .x-file-input.try-again, -.theme-validation-container .gh-image-uploader .x-file-input.try-again label { - display: inline; -} - -.theme-validation-item { - margin: 12px 0 0; - padding: 12px 16px 12px 28px; - border: 1px solid #e5eff5; - border-radius: 5px; - display: flex; - flex-direction: column; - border: 1px solid var(--lightgrey); -} - -.theme-validation-item h4 { - margin: 0; - font-size: 1.4rem; - font-weight: 400; - line-height: 1.5em; -} - -.theme-validation-rule-text { - flex-grow: 1; -} - -.theme-validation-item.theme-fatal-error { - border: 1px solid var(--red); -} - -.theme-validation-item.theme-fatal-error .theme-validation-rule-text::before, -.theme-validation-item.theme-error .theme-validation-rule-text::before, -.theme-validation-item.theme-warning .theme-validation-rule-text::before -{ - font-weight: 600; -} - -.theme-validation-item.theme-fatal-error .theme-validation-rule-text::before { - content: "Fatal error:"; - color: var(--red); -} - -.theme-validation-item.theme-error .theme-validation-rule-text::before { - content: "Error:"; -} - -.theme-validation-item.theme-warning .theme-validation-rule-text::before { - content: "Warning:"; -} - -.theme-fatal-error .theme-validation-type-label::before, -.theme-error .theme-validation-type-label::before, -.theme-warning .theme-validation-type-label::before { - content: ""; - display: block; - width: 8px; - height: 8px; - margin-top: 6px; - margin-left: -16px; - border-radius: 999px; -} - -.theme-fatal-error .theme-validation-type-label::before, -.theme-error .theme-validation-type-label::before { - background: color-mod(var(--red) alpha(0.85)); -} - -.theme-warning .theme-validation-type-label::before { - background: color-mod(var(--yellow)); -} - -.theme-validation-list ul { - list-style: disc; -} - -.theme-validation-list code, -.theme-validation-rule-text code { - font-size: 0.9em; -} - -.theme-validation-item h6 { - font-size: 1.3rem; - font-weight: 500; -} - -.theme-validation-toggle-details { - display: flex; - justify-content: space-between; - flex-grow: 1; - align-items: flex-start; - padding: 0; - color: var(--darkgrey); - text-decoration: none!important; - font-size: 1.3rem; -} - -.theme-validation-rule-icon { - flex-shrink: 0; - margin-left: 5px; - width: 13px; - height: 14px; - color: var(--midgrey); - transition: all 0.1s ease-out; -} - -.theme-validation-rule-icon svg path { - fill: var(--midgrey); -} - -.theme-validation-details { - margin-top: 12px; - padding-top: 12px; - font-size: 1.3rem; - border-top: 1px solid var(--lightgrey); -} - -p.theme-validation-details { - font-size: 1.3rem; -} - -.theme-validation-screenshot img { - margin-bottom: 2rem; - border: 1px solid var(--main-color-area-divider); - border-radius: 3px; -} - - -/* Publication identity -/* ---------------------------------------------------------- */ -.blog-logo, -.blog-icon { - max-height: 50px; - height: auto !important; -} - -/** CSS for accent color */ -.input-color-form-group { - display: flex; - align-items: flex-end; - flex-direction: column; - margin-bottom: 0; -} - -.input-color { - display: flex; - position: relative; -} - -.input-color:after { - content: "#"; - position: absolute; - top: 9px; - left: 43px; - color: var(--midlightgrey); - font-family: "Consolas", monaco, monospace; - font-size: 13px; -} - -.input-color:focus { - border: none; -} - -.input-color input { - padding-left: 52px; - width: 112px; - height: 38px; - padding-right: 8px; - font-family: "Consolas", monaco, monospace; - font-size: 13px; -} - -.input-color .color-box { - position: absolute; - top: 1px; - left: 1px; - width: 36px; - height: 36px; - display: inline-block; - background-color: var(--lightgrey); - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - border-right: 1px solid var(--input-border); - box-shadow: inset 0 0 0 1px var(--white); -} - -.input-color input:focus + .color-box { - top: 2px; - left: 2px; - width: 35px; - height: 34px; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; -} - -.gh-setting-unsplash-checkbox { - margin-bottom: 0; -} - -/* Branding -/* ---------------------------------------------------- */ - -.gh-branding-settings { - display: flex; - align-items: stretch; - height: 100%; -} - -.gh-branding-settings-header { - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid var(--whitegrey); - margin: -20px -24px; - padding: 16px 24px; -} - -.gh-branding-settings-header h4 { - margin: 0; - padding: 0; - font-size: 1.9rem; - font-weight: 600; -} - -.gh-branding-settings-actions { - display: flex; - align-items: center; - justify-content: flex-end; -} - -.gh-branding-settings-actions .close { - padding: 4px; - margin-right: 12px; -} - -.gh-branding-settings-options { - flex-basis: 25%; - flex-grow: 0; - flex-shrink: 0; - border-right: 1px solid var(--whitegrey); - min-width: 320px; - max-width: 400px; - margin: 20px 0 -20px; - padding: 24px 24px 24px 0; - overflow-y: auto; - height: calc(100vh - 136px); -} - -.gh-branding-image-container { - position: relative; - align-self: flex-start; - height: 50px; -} - -.gh-branding-image-container.largeimg { - width: 100%; - display: flex; - height: unset; - min-height: 80px; - align-items: center; -} - -.gh-branding-image-container.transparent-bg { - background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3ERectangle%3C/title%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23E6EEF2' d='M0 0h24v24H0z'/%3E%3Cpath fill='%23D8E2E8' d='M0 0h12v12H0zM12 12h12v12H12z'/%3E%3C/g%3E%3C/svg%3E"); -} - -.gh-branding-settings-options .gh-setting-action-largeimg-delete, -.gh-branding-settings-options .gh-setting-action-smallimg-delete { - position: absolute; - top: 5px; - right: 5px; - background: rgba(0, 0, 0, 0.9); - border: 1px solid rgba(255, 255, 255, 0.25); - padding: 5px; - margin: 0; - border-radius: 3px; - opacity: 0; -} - -.gh-branding-settings-options .gh-setting-action-largeimg-delete:hover, -.gh-branding-settings-options .gh-setting-action-smallimg-delete:hover { - background: var(--red); - border-color: transparent; -} - -.gh-branding-image-container:hover .gh-setting-action-largeimg-delete, -.gh-branding-image-container:hover .gh-setting-action-smallimg-delete { - opacity: 1; -} - -.gh-branding-settings-right { - flex-grow: 1; - flex-basis: 100%; - display: flex; - flex-direction: column; - align-items: stretch; - margin: 20px -24px -20px 0; - background: var(--whitegrey-l1); -} - -.gh-branding-settings-previewcontainer { - margin: 32px 68px 68px; -} - -.gh-branding-settings-previewcontainer .site-frame { - width: 133.33333%; - height: 133.33333%; - transform: scale(0.75); - transform-origin: 0 0; -} - -.gh-branding-settings .input-color input { - position: relative; - height: 30px; - width: 102px; - padding: 3px 4px 3px 44px; - font-size: 1.3rem; -} - -.gh-branding-settings .input-color::after { - top: 5px; - left: 34px; -} - -.gh-branding-settings .color-picker-horizontal-divider { - position: absolute; - display: block; - content: ""; - width: 1px; - top: 0; - left: 29px; - bottom: 0; - background: var(--input-border-color); -} - -.gh-branding-settings .input-color input:focus + .color-picker-horizontal-divider { - top: 2px; - bottom: 2px; -} - -.gh-branding-settings .color-box-container { - height: 26px; - width: 26px; - position: absolute; - overflow: hidden; - top: 2px; - left: 2px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} - -.gh-branding-settings .color-box-container .color-picker { - position: absolute; - top: -10px; - left: -10px; - border: none; - outline: none; - padding: 0; - margin: 0; - width: 50px; - height: 50px; -} - -.gh-branding-settings .gh-accent-color .gh-setting-action { - align-self: flex-start; - margin-top: 22px; -} - -.gh-branding-settings .gh-accent-color .response { - margin: -8px 0 0; - font-size: 1.3rem; -} - -/* Design (Labs) -/* ---------------------------------------------------- */ - -.gh-nav-contextual { - flex: 0 0 clamp(320px, 17.09vw + 86.5px, 360px); -} - -.gh-nav-header { - display: flex; - align-items: center; - padding: 32px 32px 48px; - color: var(--midgrey-l2); - font-size: 1.35rem; - font-weight: 400; - line-height: 36px; - letter-spacing: .2px; -} - -.gh-nav-header.no-accordion { - padding-bottom: 32px; -} - -.gh-nav-header.no-accordion + .gh-nav-body .gh-nav-top:first-of-type .gh-nav-design-settings { - margin-top: 4px; -} - -.gh-nav-menu-back-button { - display: flex; - align-items: center; - height: 36px; - color: var(--darkgrey); - background: none; -} - -.gh-nav-header svg { - display: block; - width: 9px; - height: 9px; - margin: 1px 6px 0; -} - -.gh-nav-header svg path { - stroke: var(--darkgrey-l1); -} - -.gh-nav-menu-back-button:hover { - color: var(--black); -} - -.gh-nav-design { - overflow-x: hidden; -} - -.gh-nav-design .gh-nav-menu-title { - display: flex; - align-items: center; - overflow: hidden; - margin: 0 16px 4px 16px; - padding: 8px 16px; - color: var(--black); - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; - line-height: 1.3em; - border-radius: var(--border-radius); -} - -.gh-nav-design .gh-nav-menu-title:hover { - background: none !important; -} - -.gh-nav-design-tab { - display: flex; - flex-grow: 1; - position: relative; - align-items: center; - box-sizing: border-box; - padding: 7px var(--main-layout-area-padding); - color: var(--darkgrey-l1); - font-weight: 400; - font-size: 1.45rem; - transition: none; -} - -.gh-nav-design-tab:hover { - color: var(--black); -} - -.gh-nav-design-tab.active { - color: var(--black); - font-weight: 400; - border-radius: var(--border-radius) var(--border-radius) 0 0; -} - -.gh-nav-bottom .gh-nav-design-tab { - justify-content: space-between; -} - -.gh-nav-bottom .gh-nav-design-tab span { - display: flex; - align-items: center; - color: var(--black); - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; -} - -.gh-nav-design-tab:not(.active):hover { - background: var(--mainmenu-color-hover-bg); -} - -.gh-nav-bottom .gh-nav-design-tab > a { - position: absolute; - inset: 0; -} - -.gh-nav-bottom .gh-nav-design-tab .active-theme { - color: var(--midgrey); - font-size: 1.3rem; - font-weight: 400; -} - -.gh-nav-design .gh-nav-list { - display: flex; - flex-direction: column; -} - -.gh-nav-design .gh-nav-list .active svg { - fill: none; -} - -.gh-nav-design .gh-nav-button-expand { - position: relative; - top: inherit; - left: inherit; - margin: 0 8px 0 auto; - padding-top: 3px; -} - -.gh-nav-design .gh-nav-button-expand svg { - margin-right: 0; -} - -.gh-nav-design .gh-nav-bottom { - position: sticky; - -webkit-position: sticky; - bottom: -24px; - z-index: 9997; - height: 120px; - padding: 0; - -webkit-backface-visibility: hidden; -} - -.gh-nav-design .gh-nav-bottom::before, -.gh-nav-design .gh-nav-bottom::after { - content: ""; - position: sticky; - -webkit-position: sticky; - display: block; - height: 24px; -} - -.gh-nav-design .gh-nav-bottom::before { - z-index: 9998; - bottom: 0; - background: var(--white); -} - -.gh-nav-design .gh-nav-bottom::after { - bottom: 72px; - box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 -8px 16px -3px rgba(0,0,0,.15); -} - -.gh-change-theme { - position: sticky; - -webkit-position: sticky; - bottom: 0; - z-index: 9999; - display: flex; - align-items: center; - height: 96px; - margin-bottom: -24px; - background: var(--white); -} - -.gh-theme-docs { - position: relative; - z-index: 10; - padding: 4px 8px; - line-height: 0; - color: var(--midgrey); -} - -.gh-theme-docs:hover { - color: #15171a; -} - -.gh-theme-docs svg { - width: 12px; - height: 12px; - fill: currentColor; -} - -.gh-nav-design-tabicon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - margin-right: -8px; - border-radius: 50%; -} - -.gh-nav-design-tab.active .gh-nav-design-tabicon { - background: var(--mainmenu-color-hover-bg); -} - -.gh-nav-design-tabicon svg { - width: 18px; - height: 18px; - fill: currentColor; -} - -.gh-nav-design-settings { - margin: 8px 0 24px; - padding: 24px var(--main-layout-area-padding) 16px; - background: var(--mainmenu-color-hover-bg); -} - -.gh-nav-design .gh-setting { - padding: 16px 0; -} - -.gh-nav-design .gh-setting-first { - padding-bottom: 16px; -} - -.gh-nav-design .gh-setting-boolean + .gh-setting-boolean { - padding-top: 8px; -} - -.gh-nav-design .gh-setting-title { - font-size: 1.3rem; - font-weight: 600; -} - -.gh-nav-design .gh-setting-action { - align-self: flex-start; -} - -.gh-nav-design .gh-select svg { - width: 12px; - height: 6px; - margin-right: 0; -} - -.gh-nav-design .input-color input { - position: relative; - height: 30px; - width: 102px; - padding: 3px 4px 3px 44px; - font-size: 1.3rem; -} - -.gh-nav-design .input-color::after { - top: 5px; - left: 34px; -} - -.gh-nav-design .color-box-container { - height: 26px; - width: 26px; - position: absolute; - overflow: hidden; - top: 2px; - left: 2px; - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} - -.gh-nav-design .color-box-container .color-picker { - position: absolute; - top: -10px; - left: -10px; - border: none; - outline: none; - padding: 0; - margin: 0; - width: 50px; - height: 50px; -} - -.gh-nav-design .gh-setting-action-largeimg-delete, -.gh-nav-design .gh-setting-action-smallimg-delete { - position: absolute; - top: 5px; - right: 5px; - background: rgba(0, 0, 0, 0.9); - border: 1px solid rgba(255, 255, 255, 0.25); - padding: 5px; - margin: 0; - border-radius: 3px; - opacity: 0; -} - -.gh-nav-design .gh-setting-action-largeimg-edit, -.gh-nav-design .gh-setting-action-smallimg-edit { - right: 40px; -} - -.gh-nav-design .gh-setting-action-largeimg-delete:hover, -.gh-nav-design .gh-setting-action-smallimg-delete:hover { - background: var(--red); - border-color: transparent; -} - -.gh-nav-design .gh-setting-action-smallimg-edit:hover, -.gh-nav-design .gh-setting-action-largeimg-edit:hover { - background: var(--middarkgrey); -} - -.gh-nav-design .gh-setting-action-largeimg-delete svg, -.gh-nav-design .gh-setting-action-smallimg-delete svg { - margin: 0; -} - -.gh-nav-design .for-switch label { - width: 34px !important; - height: 22px !important; - margin: 0; -} - -.gh-nav-design .for-checkbox .input-toggle-component { - background-color: var(--white); -} - -.gh-nav-design .kg-settings-panel-control-input { - margin-right: -4px; -} - -.gh-nav-design .kg-settings-headerstyle-btn-group .kg-headerstyle-btn-light { - border-color: color-mod(var(--lightgrey) l(-5%) s(-10%)); -} - -.gh-design { - display: flex; - flex-direction: column; - height: 100%; -} - -.gh-design.gh-canvas { - padding: 0 clamp(24px, 10.26vw + -116.1px, 48px); -} - -.gh-preview-page-selector, -.gh-preview-page-selector select { - height: 34px; - min-width: 160px; -} - -.gh-preview-page-selector svg { - margin-top: -.1em; -} - -.gh-design-preview-mode span { - line-height: 28px; -} - -.gh-design-preview-mode svg { - max-width: 16px; - height: 16px; - vertical-align: middle; - fill: var(--midgrey); -} - -.gh-design .view-container { - padding-bottom: 0; -} - -.gh-design .gh-pe-mobile-container { - margin: 4vmin 0 4rem; -} - -.gh-advanced svg { - width: auto; - height: 6px; - margin-right: .6em; - fill: var(--darkgrey); -} - -.gh-advanced svg path { - stroke: var(--darkgrey); -} - -.gh-themes-container { - margin-bottom: 40px; - background: var(--main-color-content-greybg); - border-radius: var(--border-radius); -} - -.gh-themes-container .apps-grid-cell { - background: none; -} - -.gh-themes-container .apps-grid-cell:hover { - background: var(--whitegrey-l1); -} - -.gh-themes-container .apps-card-app { - padding: 16px 24px; -} - -.gh-themes-container .apps-grid-cell:last-of-type .apps-card-app { - border-bottom: none; -} - -.gh-themes-container .apps-configured-action { - display: block; - margin-right: 16px; - padding: 2px 6px; - color: var(--green-d1); - border-radius: var(--border-radius); -} - -.gh-themes-container .gh-btn-icon { - background: none; -} - -.gh-themes-container .gh-btn-icon:hover { - background: var(--whitegrey-d1); -} - -.gh-themes-container .gh-btn-icon svg { - margin-right: 0; -} - -.gh-list-delete { - color: var(--red-d1) !important; -} - -@media (max-width: 500px) { - .gh-themes-container .apps-configured { - justify-content: flex-end; - } - .gh-themes-container .apps-card-meta { - flex-basis: auto; - } -} - -.gh-theme-directory-container { - padding: 8px 0 0; -} - -.theme-directory { - display: grid; - justify-content: space-between; - grid-template-columns: 1fr 1fr 1fr; - grid-column-gap: 40px; - grid-row-gap: 64px; - margin: 0 0 24px; -} - -@media (min-width: 1800px) { - .theme-directory { - grid-template-columns: 1fr 1fr 1fr 1fr; - } -} - -@media (max-width: 1120px) { - .theme-directory { - grid-template-columns: 1fr 1fr; - } -} - -@media (min-width: 800px) and (max-width: 890px) { - .theme-directory { - grid-template-columns: 1fr; - } -} - -@media (max-width: 800px) { - .theme-directory { - grid-column-gap: 32px; - grid-row-gap: 48px; - } -} - -@media (max-width: 430px) { - .theme-directory { - grid-template-columns: 1fr; - } -} - -.gh-theme-browser { - position: relative; - width: 100%; - height: 28px; - padding: 0 12px; - background: var(--whitegrey-l1); - border-radius: 3px 3px 0 0; -} - -.gh-theme-browser-button { - position: relative; - top: 11px; - display: block; - width: 6px; - height: 6px; - background: var(--lightgrey); - border-radius: 50%; -} - -.gh-theme-browser-button::before, -.gh-theme-browser-button::after { - content: ""; - position: absolute; - width: 6px; - height: 6px; - background: var(--lightgrey); - border-radius: 50%; -} - -.gh-theme-browser-button::before { - left: 12px; -} - -.gh-theme-browser-button::after { - left: 24px; -} - -.td-item-labs { - text-align: left !important; -} - -.td-item-screenshot-labs { - line-height: 0; - border-radius: 0 0 3px 3px; -} - -.td-item-screenshot-labs img { - border-radius: 0 0 3px 3px; -} - -.theme-directory .td-item-desc { - display: flex; - flex-direction: column; -} - -.td-item-name { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 4px; - font-size: 1.6rem; - text-transform: none; -} - -.td-item-labs .td-item-category { - margin-left: 0; - color: var(--midgrey); - font-size: 1.2rem; - font-weight: 500; - text-transform: uppercase; -} - -.gh-theme-directory-footer { - position: relative; - padding: 58px var(--main-layout-content-sidepadding); - color: var(--whitegrey-l2); - font-size: 1.75rem; - text-align: center; - background: #15171A; - background-repeat: no-repeat; - background-position: 100% 50%; - background-size: 35vw; -} - -.gh-theme-directory-footer .link { - color: var(--lime); - font-weight: 500; -} - -.gh-theme-preview { - max-width: none; - padding: 0 80px 48px; -} - -.gh-theme-preview .view-container { - padding-bottom: 0; -} - -.gh-theme-preview .gh-pe-mobile-container { - margin-top: 4vmin; -} - -/* Membership */ -.gh-setting-membership { - margin-bottom: 32px; -} - -@media (max-width: 1140px) { - .gh-setting-members-canvas::before { - display: none; - } -} - -.gh-setting-members-basicsform { - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.gh-setting-members-basicsform .intro { - margin: 0; - font-size: 1.6rem; - margin-bottom: 12px; -} - -.gh-setting-members-portalcta { - background: linear-gradient(to left, color-mod(var(--main-color-content-greybg) l(-3%)), var(--main-color-content-greybg)); -} - -.gh-setting-members-portalcta .gh-expandable-header button { - margin-left: 16px; -} - -@media (max-width: 500px), (min-width: 1140px) and (max-width: 1260px) { - .gh-setting-members-portalcta .gh-expandable-header { - flex-direction: column; - align-items: inherit; - } - - .gh-setting-members-portalcta .gh-expandable-header button { - margin-top: 1rem; - margin-left: 0; - } -} - -.gh-setting-members-portalcta .gh-expandable-description { - padding-top: 2px; - line-height: 1.4; -} - -.gh-setting-members-portalpreview { - justify-self: end; - font-size: 1.3rem; - font-weight: 500; - color: var(--midgrey); -} - -@media (max-width: 1140px) { - .gh-setting-members-portalpreview { - display: none; - } -} - -.gh-setting-dropdown { - margin-top: 1.2rem; - cursor: pointer; - background: var(--white); -} - -.gh-setting-dropdown[aria-disabled="true"] { - background: var(--whitegrey-l2); -} - -.gh-setting-dropdown[aria-disabled="true"] svg path { - fill: var(--lightgrey-d1); -} - -.gh-setting-dropdown[aria-disabled="true"] .gh-radio-label { - opacity: .65; -} - -.gh-setting-dropdown:focus-visible { - outline: none; -} - -.gh-setting-dropdown .ember-power-select-status-icon { - right: 24px; -} - -.gh-setting-dropdown .gh-setting-dropdown-content { - display: flex; - align-items: center; - margin: 1.2rem 2.4rem 1.2rem 0.8rem; -} - -.gh-setting-dropdown-list { - margin-top: -1px; - border-top: 1px solid var(--input-border-color) !important; -} - -.gh-setting-dropdown-list .ember-power-select-option { - padding: 6px 8px; -} - -.gh-setting-dropdown-list .gh-setting-dropdown-content { - display: flex; - align-items: center; - margin: 1.4rem 1rem; -} - -.gh-setting-dropdown-content svg { - width: 3rem; - height: 3rem; - margin-right: 1.2rem; -} - -.gh-setting-richdd-container { - margin: 36px 0 0; -} - -.gh-expandable-content .gh-setting-richdd-container { - margin: 0 0 30px; -} - -.gh-expandable-content .gh-setting-richdd-container .gh-setting-dropdown { - margin-top: 0; -} - -.gh-setting-large-dropdown .ember-power-select-multiple-trigger { - padding: 8px; -} - -.gh-setting-large-dropdown .segment-totals { - display: none; -} - -.gh-setting-rich-dropdown { - margin-bottom: 32px; -} - -.gh-setting-rich-dropdown .ember-power-select-status-icon { - right: 20px; -} - -.gh-setting-members-tierscontainer { - margin-top: 4vmin; -} - -.gh-settings-members-tiersheader { - display: flex; - align-items: flex-end; - justify-content: space-between; -} - -.gh-settings-members-tiersheader .gh-btn-stripe-status { - margin-bottom: 12px; -} - -.gh-settings-members-tiersheader .gh-btn-stripe-status span { - height: 28px; - line-height: 28px; - font-size: 1.25rem; -} - -.gh-stripe-connect-link { - color:#635bff; - font-weight:500; -} - -.gh-setting-members-portal-mock { - display: flex; - position: relative; - align-items: center; - justify-content: center; - background: #fff; - box-shadow: var(--box-shadow-preview-box); - width: 420px; - height: 562px; - margin-bottom: 32px; - border-radius: 5px; - pointer-events: none; - transition: height 0.17s ease-out; -} - -.gh-setting-members-portal-mock.mock-enabled { - pointer-events: unset; -} - -.gh-setting-members-portal-disabled { - display: flex; - flex-direction: column; - align-items: center; - margin: 32px; - text-align: center; -} - -.gh-setting-members-portal-disabled svg { - width: 44px; - height: 44px; -} - -.gh-setting-members-portal-disabled svg path { - stroke-width: 1.2px; -} - -.gh-setting-members-portal-disabled h4 { - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 500; - color: var(--darkgrey); -} - -.gh-setting-members-portal-disabled p { - max-width: 240px; - font-weight: 400; -} - -.gh-setting-members-portal-mock .site-frame { - border-radius: 5px; -} - -.gh-settings-members-pricetrialcont { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 32px; -} - -.gh-settings-members-pricetrialcont .trial-docs-link { - color: #30cf43; - white-space: nowrap; -} - -.gh-settings-members-pricelabelcont { - display: flex; - align-items: baseline; -} - -.gh-settings-members-pricelabelcont.free-trial-enabled { - justify-content: space-between; - margin-bottom: 3px; -} - -.gh-settings-members-pricelabelcont span { - margin: 0 4px; -} - -.gh-settings-members-pricelabelcont span, -.gh-settings-members-pricelabelcont div { - display: inline-block; - margin-bottom: 3px; -} - -.gh-settings-members-pricelabelcont .gh-select svg { - position: unset; - margin-top: -3px; -} - -.gh-settings-members-pricelabelcont .gh-select { - padding: 0; - height: 16px; - border: none; - margin-left: 0; - margin-right: 0; -} - -.gh-settings-members-pricelabelcont .gh-select select { - font-size: 1.4rem; - font-weight: 500; - border: none; - height: 16px; - width: 46px; - padding: 0; -} - -.gh-setting-members-prices { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 20px; -} - -.gh-setting-members-prices.free-trial-enabled { - display: grid; - grid-template-rows: 1fr 1fr; - grid-template-columns: none !important; - grid-gap: 6px; -} - -.gh-setting-members-currency { - position: relative; -} - -.gh-setting-members-currencylabel { - position: absolute; - display: flex !important; - align-items: center; - top: 0px; - left: 0px; - background: var(--main-color-content-greybg); - height: 20px; - font-weight: 500; - font-size: 1.3rem; - color: var(--middarkgrey); - text-transform: uppercase; - pointer-events: none; - min-width: 60px; -} - -.gh-setting-members-currencylabel span { - margin-right: 0; - pointer-events: none; - padding-right: 3px; -} - -.gh-input-group-tier-trial-disabled .gh-input-append { - border-color: #eceef0; - background-color: #fcfcfc; -} - -.gh-input-group-tier-trial-disabled .gh-input-append:before { - background-color: #fcfcfc; -} - -/* Stripe Connect modal */ -.fullscreen-modal-stripe-connect { - max-width: 860px; -} - -.fullscreen-modal-stripe-connect:not(.fullscreen-modal-stripe-connected) .gh-main-section { - margin: 0 0 -32px; -} - -.fullscreen-modal-stripe-connect:not(.fullscreen-modal-stripe-connected) .gh-btn-stripe-disconnect, -.fullscreen-modal-stripe-connect:not(.fullscreen-modal-stripe-connected) .gh-stripe-guide-container { - display: none; -} - -.fullscreen-modal-stripe-connect.fullscreen-modal-stripe-connected .modal-header { - display: none; -} - -.fullscreen-modal-stripe-connected .modal-content { - background: linear-gradient(to top, #F6F6F6, #ffffff); - border-radius: 12px; -} - -.fullscreen-modal-stripe-connected .gh-main-section { - margin-bottom: 0; -} - -.gh-members-stripe-info-header { - display: flex; - justify-content: space-between; - align-items: center; -} - -.gh-members-stripe-info-header h4 { - font-weight: 600; - margin: 0; - padding: 0; - color: #555ABF; -} - -.gh-members-stripe-info { - border-radius: 0.9rem; - background: color-mod(#555ABF a(12%)); - padding: 12px; - width: 380px; - color: #555ABF; -} - -.gh-members-stripe-badge { - width: 180px; -} - -.gh-members-stripe-link, -.gh-members-stripe-link:hover { - color: #555ABF; - text-decoration: underline; -} - -.gh-members-connectbutton-container { - margin-right: 4px; -} - -.gh-members-connectbutton-container .for-switch { - line-height: 1em; -} - -.gh-members-connectbutton-container .for-switch label { - width: 36px !important -} - -.gh-members-connectbutton-container .for-switch input:checked + .input-toggle-component { - background: #F1946A; -} - -.gh-members-connect-testmodeon { - color: #F1946A; -} - -.gh-members-stripe-connect-token { - background: var(--whitegrey-l2); - min-height: unset; - height: 80px; - font-family: var(--font-family-mono); - font-size: 1.3rem; - resize: none; -} - -.gh-members-connect-testmodelabel { - display: inline-block; - background: #f8e5b9; - color: #983705; - font-size: 1.3rem; - font-weight: 500; - line-height: 1em; - border-radius: 999px; - padding: 4px 8px; -} - -.gh-members-connect-savecontainer { - height: 0px; - overflow-y: hidden; - transition: all 0.2s ease-in-out; - opacity: 0; - margin-top: 16px; - margin-bottom: 0; -} - -.gh-members-connect-savecontainer.expanded { - margin-bottom: 20px; -} - -.gh-members-connect-savecontainer.expanded { - height: 36px; - opacity: 1.0; -} - -.gh-stripe-connected-container { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 7vw; -} - -.gh-stripe-connected-container h1 { - font-size: 2.6rem; - font-weight: 600; - text-align: center; - letter-spacing: -.5px; - margin: 20px 0 10px; -} - -.gh-stripe-connected-info { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - margin-bottom: 5vw; -} - -.gh-stripe-site-title { - display: flex; - justify-content: center; - align-items: center; -} - -.gh-stripe-connected-info p { - margin: 0 10px 0 0; - font-size: 1.5rem; - letter-spacing: -.1px; -} - -.gh-btn-stripe-disconnect { - position: absolute; - top: 0; - left: 0; - z-index: 9999; - margin: 0; -} - -.fullscreen-modal-stripe-connect .modal-content .close { - top: 32px; - right: 32px; -} - -.gh-stripe-error-hasactivesub { - margin: 24px 24px -8px; - color: var(--red); -} - -.gh-stripe-guide-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; -} - -.gh-stripe-guide-container > p { - text-transform: uppercase; - letter-spacing: .1em; - font-weight: 500; - margin-bottom: 20px; -} - -.gh-logos-stripe-connect { - display: flex; -} - -.gh-logo-squircle { - flex: 0 0 60px; - width: 60px; - height: 60px; - background-position: center center; - background-size: cover; - background-repeat: no-repeat; - border-radius: 35%; -} - -.gh-logo-squircle:first-of-type { - z-index: 101; -} - -.gh-logo-squircle:nth-of-type(2) { - z-index: 102; - box-shadow: -1.5px 0 0 1.5px #fff; - margin-left: -3px; -} - -.gh-stripe-guide { - display: flex; - width: 100%; - box-shadow: rgba(0,0,0,0.05) 0 14px 23px -18px; - color: var(--midgrey); - transition: all 0.2s ease-in-out; -} - -.gh-stripe-guide:hover { - box-shadow: rgba(0,0,0,0.1) 0 14px 23px -18px; -} - -.gh-stripe-guide-content { - display: flex; - flex-direction: column; - flex-grow: 1; - flex-basis: 100%; - padding: 20px; - overflow: hidden; - background-color: var(--white); - border-top: 1px; - border-bottom: 1px; - border-left: 1px; - border-right: 0; - border-style: solid; - border-color: var(--lightgrey-l2); - border-radius: 2px 0 0 2px; -} - -.gh-stripe-guide-content-body { - display: flex; - flex-direction: column; -} - -.gh-stripe-guide-content-body h3 { - font-size: 1.7rem; - font-weight: 600; -} - -.gh-stripe-guide-content-body p { - font-size: 1.4rem; -} - -.gh-stripe-guide-content-footer { - display: flex; - align-items: center; -} - -.gh-stripe-guide-content-footer svg { - width: 20px; - height: 20px; - flex: 0 0 20px; -} - -.gh-stripe-guide-content-footer h4, -.gh-stripe-guide-content-footer p { - margin: 0; -} - -.gh-stripe-guide-content-footer h4 { - font-size: 1.4rem; - font-weight: 500; - margin-left: 5px; - margin-right: 6px; - letter-spacing: -0.02em; -} - -.gh-stripe-guide-content-footer p { - font-weight: 500; - color: #15171a; -} - -.gh-stripe-guide-content-footer span { - margin-right: 6px; - font-size: 3.2rem; - color: var(--black); - line-height: .8em; -} - -.gh-stripe-guide-image { - position: relative; - flex-grow: 1; - min-width: 33%; - align-items: center; -} - -.gh-stripe-guide-image img { - width: 100%; - height: 100%; - object-fit: cover; - position: absolute; - top: 0; - left: 0; - border-radius: 0 2px 2px 0; -} - -@media (max-width: 1080px) { - .gh-settings-members-pricetrialcont { - display: grid; - grid-template-columns: none; - grid-gap: 0px; - margin-bottom: 24px; - } -} - -@media (max-width: 500px) { - .gh-members-stripe-info-header { - flex-direction: column; - align-items: stretch; - } - - .gh-members-stripe-info-header h4 { - order: 2; - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid var(--whitegrey); - } - - .gh-members-stripe-badge { - order: 1; - /* margin: -10px 0 0 -10px; */ - } - - .gh-members-stripe-info { - width: 100%; - } - - .gh-stripe-connected-container { - margin-top: 7rem; - } - - .gh-stripe-connected-container h1 { - font-size: 2.4rem; - } - - .gh-stripe-connected-info p { - font-size: 1.4rem; - } - - .gh-stripe-guide-image { - display: none; - } - - .gh-stripe-guide-content { - border-radius: 2px; - border: 1px solid var(--lightgrey-l2); - } -} - -@media (max-width: 430px) { - .gh-stripe-site-title { - flex-direction: column; - } - .gh-members-connect-testmodelabel { - margin-top: 15px; - } - .gh-logo-squircle { - flex-basis: 40px; - width: 40px; - height: 40px; - } -} - -.gh-setting-nossl { - border-top: 1px solid var(--whitegrey-d1); - display: flex; - flex-direction: column; - align-items: center; - margin: 16px -24px -12px; -} - -.gh-setting-nossl-container { - display: flex; - flex-direction: column; - align-items: center; - padding: 32px; - text-align: center; - max-width: 520px; -} - -.gh-setting-nossl-container svg { - width: 44px; - height: 44px; - margin-bottom: 12px; -} - -.gh-setting-nossl-container svg path, -.gh-setting-nossl-container svg rect, -.gh-setting-nossl-container svg circle { - stroke-width: 1px; -} - -.gh-setting-nossl-container h4 { - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 600; -} - -.gh-setting-nossl-container p { - margin: 8px 0 0; - color: var(--midgrey); -} - -/* Signup form modal */ -.gh-signup-form-embed { - margin: 6vw 0; -} - -.fullwidth-modal { - max-width: 100%; -} - -.epm-modal .modal-signup-form-embed { - display: grid; - grid-template-columns: 5fr 3fr; - max-width: 1120px; - max-height: 520px; - margin: 32px; - padding: 0; -} - -@media (max-width: 960px) { - .epm-modal .modal-signup-form-embed { - grid-template-columns: 1fr; - } -} - -.modal-signup-form-embed-preview { - position: relative; - margin: 1.6rem; - margin-right: 0; - overflow: hidden; - border: 1px solid var(--whitegrey); -} - -@media (max-width: 960px) { - .modal-signup-form-embed-preview { - display: none; - } -} - -.gh-signup-form-iframe { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - border: 0; - opacity: 1; - z-index: 0; - transition: none; -} - -.gh-signup-form-iframe.gh-iframe-hidden { - opacity: 0; - z-index: 1; - transition: 0.3s opacity; - pointer-events: none; -} - -.modal-signup-form-embed-main { - padding: 32px; - height: 100%; -} - -.modal-signup-form-embed-main .modal-body { - height: 320px; - margin: 0 -32px; - padding: 0 32px; - overflow-y: auto; -} - -.modal-signup-form-embed-main .form-group { - margin-bottom: 0; -} - -.gh-signup-form-container .gh-stack-item { - margin-bottom: 1.6rem; -} - -.gh-signup-form-container .gh-stack-item:last-child { - margin-bottom: 0; -} - -.gh-signup-form-embed-code-input { - min-height: 72px; - background: var(--whitegrey-l2); - font-family: var(--font-family-mono); - font-size: 1.35rem; -} - -.gh-signup-form-embed-code-input:focus { - background: var(--whitegrey-l2); - border-color: var(--lightgrey-l1) !important; - box-shadow: none; -} - -/* Tips & donations -/* ---------------------------------------------------------- */ -.gh-tips-and-donations-suggested-amount { - max-width: 200px; - margin-right: 24px; -} - -.gh-tips-and-donations-suggested-amount .gh-input { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -.gh-tips-and-donations-currency { - width: 120px; -} - -.gh-tips-and-donations-currency select { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - border-left: none !important; -} - -.gh-tips-and-donations-link-container { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - font-size: 1.4rem; - height: 38px; - background: var(--whitegrey-l2); - border-radius: 4px; - border: 1px solid var(--whitegrey); - color: var(--darkgrey); - font-weight: 500; - width: 100%; -} - -/* History log -/* ---------------------------------------------------------- */ -.gh-history-filter-li { - height: 32px; -} - -.gh-history-filter-li svg { - margin-right: 6px; -} - -.gh-activity-log-actions { - padding-top: 12px; - padding-bottom: 12px; -} - -.gh-activity-log-actions ul { - width: 240px; -} - -.gh-activity-log-actions hr { - margin: 12px -20px; -} - -.gh-activity-log-action-switch.for-switch.small label { - display: flex; - align-items: center; - justify-content: space-between; - width: unset !important; - height: unset !important; -} - -.gh-activity-log-action-switch.for-switch.small .input-toggle-component { - position: relative; - width: 30px!important; - height: 18px!important; -} - -.gh-activity-log-action-switch.for-switch.small .input-toggle-component:before { - width: 14px !important; - height: 14px !important; -} - -.gh-activity-log-action-switch.for-switch.small input:checked+.input-toggle-component:before { - transform: translateX(12px); -} - -.gh-history-container { - display: flex; - align-items: center; -} - -.gh-history-container strong { - font-weight: 600; -} - -.gh-history-action { - padding-right: 6px; -} - -.gh-history-object { - padding-left: 6px; -} - -.gh-history-object code { - background: none; - border: none; - color: var(--midgrey); -} - -.gh-history-dash { - padding-left: 0; - padding-right: 0; - color: var(--lightgrey); -} - -.gh-history-description { - font-size: 1.4rem; - color: var(--darkgrey); - white-space: nowrap; - margin-top: 2px; -} - -.gh-history-description a { - font-weight: 700; -} - -.gh-history-name { - font-size: 1.35rem; - font-weight: 400; - margin-bottom: 0 !important; - color: var(--midgrey); -} - -.gh-history-name a { - color: var(--midgrey) !important; - font-weight: 400; -} - -.gh-history-name a:hover { - color: var(--darkgrey) !important; -} - -.gh-history-datetime { - font-size: 1.2rem; - color: var(--midlightgrey); -} - -.gh-history-table .user-list-item-figure { - position: relative; - height: 34px; - width: 34px; - margin-left: 0; - margin-right: 16px; -} - -.gh-history-icon { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - right: -7px; - bottom: -7px; - background: var(--white); - width: 22px; - height: 22px; - color: var(--midgrey); - border-radius: 999px; - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.12); -} - -.gh-history-icon svg { - width: 12px; - height: 12px; -} - -@media (max-width: 620px) { - .gh-history-table .user-list-item-figure { - display: none; - } - - .gh-history-name { - font-size: 1.4rem !important; - min-width: 130px; - } -} - -/* About /ghost/settings/about/ -/* ---------------------------------------------------------- */ -.gh-about-logo svg { - position: relative; - width: 120px; - height: auto; -} - -.gh-about-logo { - border-bottom: 1px solid var(--lightgrey-l2); - padding-bottom: 10px; - margin-bottom: 16px; -} - -.gh-about-modal .gh-about-logo { - margin: 4px 0 20px; - border-bottom: none; - padding-bottom: 0; -} - -.gh-about-container { - display: grid; - grid-template-columns: 2fr 1fr; - grid-gap: 80px; -} - -.gh-whats-new-canvas .gh-about-container { - display: flex; - grid-template-columns: unset; - grid-gap: unset; - margin: 0 auto; - max-width: 920px; - margin-top: 60px; -} - -.gh-about-box { - position: sticky; - top: 96px; - right: 0; - display: flex; - flex-grow: 1; - flex-direction: column; - height: max-content; - border-radius: 3px; - min-width: 300px; -} - -.gh-about-box.grey { - border: none; - background: var(--main-color-content-greybg); -} - -.gh-env-details { - display: flex; - flex-grow: 1; - flex-direction: column; - padding: 24px 28px 28px; -} - -.gh-about-container h2 { - font-size: 1.65rem; - line-height: 1.4em; - font-weight: 600; - border-bottom: 1px solid var(--lightgrey-l2); - padding-bottom: 12px; - margin-bottom: 12px; -} - -.gh-env-list { - margin: 0; - padding: 0; - list-style: none; -} - -.gh-env-list li { - margin: 0 0 4px; - font-size: 1.4rem; - line-height: 1.5em; -} - -.gh-env-error { - margin: 1.2rem 0; - padding: 16px; - line-height: 1.4em; - border: none; - background: color-mod(var(--red) a(6%)); - border-radius: 3px; -} - -.gh-env-error a { - color: var(--red); -} - -.gh-env-help { - max-width: 200px; -} - -.gh-env-help .gh-btn { - margin: 4px 0; -} - -@media (max-width: 670px) { - .gh-env-details { - flex-direction: column; - } - .gh-env-help { - margin: 1em 0; - max-width: none; - } - .gh-env-help .gh-btn { - display: inline-block; - } -} - -.gh-about-content-actions { - display: none; -} - - - -/* Upgrade -/* ---------------------------------------------------------- */ - -.gh-upgrade-notification { - padding-top: 1em; -} - -.gh-upgrade-notification a { - text-decoration: underline; -} - -.gh-about-modal .gh-upgrade-notification { - background: color-mod(var(--green) a(8%)); - padding: 24px 28px 24px; - border-radius: 3px; - margin-bottom: 28px; -} - -/* Copyright Info -/* ---------------------------------------------------------- */ - -.gh-copyright-info { - color: var(--midgrey); - font-size: 1.3rem; - border-top: 1px solid var(--lightgrey-l2); - padding-top: 16px; - margin-top: 16px; - line-height: 1.45em; -} - -.gh-about-modal .gh-copyright-info { - margin: 4px 0 8px; - border-top: none; -} - - -/* Debug screen -/* ---------------------------------------------------------- */ -.gh-email-debug .gh-post-analytics-header .gh-canvas-header-content { - border-bottom: none; - padding-bottom: 0; -} - -.gh-email-debug-error { - display: flex; - margin-top: 20px; - padding: 12px 16px 12px 40px; -} - -.gh-email-debug-error svg { - height: 20px !important; - margin-top: -2px; -} - -.gh-email-debug-errortext { - flex-grow: 1; - margin-left: 4px; -} - -.gh-email-debug-error h4 { - font-size: 1.5rem; - font-weight: 600; - margin: 0; - padding: 0; -} - -.gh-email-debug-error button { - min-width: 80px; -} - -.gh-email-debug-settingstab-icon { - width: 20px; - height: 20px; - margin-bottom: 11px; - margin-top: 2px; -} - -.gh-email-debug-settingstab-icon svg path, -.gh-email-debug-settingstab-icon svg circle { - stroke-width: 2; -} - -.gh-email-debug .gh-list { - border-bottom: none; -} - -.gh-email-debug .gh-list thead, -.gh-email-debug .gh-list tbody { - width: 100%; -} - -.gh-email-debug .gh-list tr:first-of-type .gh-list-data { - border-top: none; -} - -.gh-email-debug .gh-list tr .gh-list-data:first-of-type { - padding-left: 0; -} - -.gh-email-debug-col-member { - padding-left: 0; - width: 25%; -} - -.gh-email-debug-member { - display: flex; - flex-wrap: nowrap; - align-items: center; -} - -.gh-email-debug-failure { - display: flex; -} - -.gh-email-debug-failure svg { - height: 16px; - width: 16px; - margin-right: 4px; -} - -.gh-email-debug-failure-details { - display: flex; - flex-direction: column; - gap: 2px; - width: 100%; -} - -.gh-email-debug-failure-codes { - display: flex; - gap: 20px; -} - -.gh-email-debug-failure-code { - color: var(--midgrey); -} - -.gh-email-debug-failure-code span { - color: var(--darkgrey); - font-weight: 500; -} - -.gh-email-debug .gh-list-data { - height: 98px; -} - -.gh-email-debug-permanent-failures .gh-list-data, -.gh-email-debug-temporary-failures .gh-list-data { - height: 80px; -} - -.gh-email-debug-batch-col-status span { - display: inline-block; - position: relative; - padding-left: 16px; - color: var(--midlightgrey); -} - -.gh-email-debug-batch-col-status span::before { - display: block; - position: absolute; - content: ""; - top: 6px; - left: 0; - width: 8px; - height: 8px; - border-radius: 999px; - background: var(--midlightgrey); -} - -.gh-email-debug-batch-col-status .failed { - color: color-mod(var(--red) l(-2%)); -} - -.gh-email-debug-batch-col-status .failed::before { - background: var(--red); -} - -.gh-email-debug-batch-col-status .submitting { - color: var(--blue); -} - -.gh-email-debug-batch-col-status .submitting::before { - background: var(--blue); -} - -.gh-email-debug-batch-col-status .submitted { - color: var(--green); -} - -.gh-email-debug-batch-col-status .submitted::before { - background: var(--green); -} - -.gh-email-debug-batch-col-created, -.gh-email-debug-batch-col-details { - color: var(--midgrey); -} - -.gh-email-debug-batch-col-created span, -.gh-email-debug-batch-col-segment span { - white-space: nowrap; -} - -.gh-email-debug-batch-col-details .detailtext div { - word-break: break-all; -} - -.gh-email-debug-batch-col-details .detailtext .noselect { - user-select: none; -} - -.gh-email-debug-batch-col-details .detailtext div code { - white-space: unset; - word-break: normal; - user-select: text; -} - -.gh-email-debug-batch-col-details .error { - color: color-mod(var(--red) l(-2%)); - font-weight: unset; -} - -.gh-email-debug-batch-col-segment span { - display: inline-block; - border-radius: 2px; - background: color-mod(var(--black) a(5%)); - padding: 1px 6px; - color: var(--middarkgrey); -} - -.gh-email-debug-batch-col-details span { - color: var(--darkgrey); - font-weight: 500; -} - -.gh-email-debug-batch-col-details .download-icon { - width: 20px; - height: 20px; - margin-left: 20px; -} - -.gh-email-debug-batch-col-details .download-icon path { - stroke: var(--midgrey); -} - -.gh-email-debug-batch-col-details .detailtext { - flex-grow: 1; -} - -.gh-email-debug-settings { - font-size: 1.3rem; - margin: 12px 0 20px; -} - -.gh-email-debug-settings .gh-type-number { - font-variant-numeric: tabular-nums; -} - -.gh-email-debug-settings hr { - margin: 8px 0; - border-top-color: var(--whitegrey); -} - -.gh-email-debug-settings tr td { - font-weight: 500; - padding: 6px 0; -} - -.gh-email-debug-settings tr td:first-of-type { - width: 30%; - white-space: nowrap; - color: var(--midgrey); -} - -.gh-email-debug-settings-icon svg { - width: 14px; - height: 14px; -} - -.gh-email-debug-settings-icon .check { - width: 18px; - height: 18px; -} - -.gh-email-debug-settings-icon .check path { - stroke: var(--green); - stroke-width: 2.5; -} - -.gh-email-debug-settings-icon .x path { - stroke: var(--midgrey); -} - -.gh-email-debug-schedule-analytics { - display: flex; - align-items: center; - width: max-content; - margin: .8rem 0 0; - color: var(--green-d1); -} - -.gh-email-debug-schedule-analytics svg { - width: 1rem; - height: 1rem; - margin-right: 6px; -} - -.gh-email-debug-schedule-analytics svg g { - stroke: var(--green-d1); - stroke-width: 3px; -} - -.gh-email-debug-empty-list { - margin: 120px 40px; - text-align: center; - font-size: 1.3rem; - color: var(--midgrey); -} - -.gh-email-debug-readmore-error { - display: inline-flex; - width: 100%; -} - -.gh-email-debug-readmore-error label { - order: 3; - cursor: pointer; -} - -.gh-email-debug-readmore-error .toggle-checkbox { - display: none; -} - -.gh-email-debug-readmore-error span { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 1; - overflow: hidden; - color: color-mod(var(--red) l(-2%)); - word-break: break-all; -} - -.gh-email-debug-readmore-error .toggle-checkbox:checked ~ span { - display: block; - -webkit-box-orient: unset; - -webkit-line-clamp: unset; - overflow: unset; - word-break: break-word; -} - -.gh-email-debug-readmore-error .toggle-checkbox:checked ~ label { - display: none !important; -} - -.gh-email-debug-readmore-error svg { - width: 12px; - height: 12px; - margin: 0; -} - -.gh-email-debug-readmore-error svg circle { - fill: var(--midgrey); -} - -.gh-email-debug-readmore-error label { - display: flex; - align-items: center; - padding: 0 4px; - border-radius: 2px; - height: 14px; - margin-top: 3px; - margin-left: 8px; - background: var(--whitegrey); -} - -/* Announcement bar */ - -.gh-announcement-editor { - min-height: 120px; - overflow: auto; - padding: 6px 12px 6px 12px; - border: 1px solid var(--whitegrey-d1); - background: var(--white); - border-radius: 4px; - word-break: break-word; -} - -.gh-announcement-editor.dark { - background: #15171A; -} - -.gh-announcement-editor .koenig-lexical *, -.gh-announcement-editor .koenig-lexical-editor-input-placeholder { - font-family: var(--font-family) !important; - font-size: 1.4rem !important; - line-height: 1.5em !important; -} - -.gh-announcement-editor .koenig-lexical p, -.gh-announcement-editor .koenig-lexical .kg-prose { - height: 108px; - color: var(--darkgrey); - font-size: 1.4rem !important; - line-height: 1.5em !important; -} - -.gh-announcement-editor .koenig-lexical a { - display: inline; - padding: 0; - height: auto; -} diff --git a/ghost/admin/app/styles/layouts/tags.css b/ghost/admin/app/styles/layouts/tags.css index c6aac8a854c..6f45974b378 100644 --- a/ghost/admin/app/styles/layouts/tags.css +++ b/ghost/admin/app/styles/layouts/tags.css @@ -177,6 +177,57 @@ label.gh-tag-setting-codeheader { width: 100%; } +.input-color { + display: flex; + position: relative; +} + +.input-color:after { + content: "#"; + position: absolute; + top: 9px; + left: 43px; + color: var(--midlightgrey); + font-family: "Consolas", monaco, monospace; + font-size: 13px; +} + +.input-color:focus { + border: none; +} + +.input-color input { + padding-left: 52px; + width: 112px; + height: 38px; + padding-right: 8px; + font-family: "Consolas", monaco, monospace; + font-size: 13px; +} + +.input-color .color-box { + position: absolute; + top: 1px; + left: 1px; + width: 36px; + height: 36px; + display: inline-block; + background-color: var(--lightgrey); + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + border-right: 1px solid var(--input-border); + box-shadow: inset 0 0 0 1px var(--white); +} + +.input-color input:focus + .color-box { + top: 2px; + left: 2px; + width: 35px; + height: 34px; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + .gh-tag-settings-colorcontainer .input-color input { position: relative; width: 112px; @@ -263,4 +314,180 @@ label.gh-tag-setting-codeheader { .gh-view-tag-link:hover svg { fill: var(--darkgrey); -} \ No newline at end of file +} + +.gh-setting-content-extended label { + display: block; + font-size: 1.3rem; + font-weight: 600; + color: var(--darkgrey); + margin-bottom: 4px; +} + +.gh-setting-content-extended textarea { + font-size: 1.5rem; + letter-spacing: 0; + line-height: 1.4em; + max-width: initial; +} + +.gh-setting-content-extended .gh-image-uploader { + margin: 0; + border: 1px solid var(--whitegrey-d2); +} + +.gh-setting-content-extended .gh-btn span { + height: 36px; + line-height: 36px; +} + +.gh-setting-content-extended { + width: 100%; +} + +/* Code injection +/* ---------------------------------------------------------- */ + +.settings-code { + max-width: 100%; +} + +.settings-code label { + font-size: 1.5rem; + letter-spacing: 0; + margin-bottom: 2px; +} + +.settings-code p { + margin: 0 0 8px; + font-size: 1.3rem; +} + +.settings-code code { + background-color: rgb(242, 244, 247); + border: 1px solid var(--lightgrey); + vertical-align: middle; + font-size: 1.2rem; +} + +.settings-code-editor { + padding: 0; + min-width: 250px; + min-height: 300px; + max-width: 1224px; + width: calc(100vw - 416px) !important; + height: auto; + line-height: 22px; + border: 1px solid var(--lightgrey); +} + +.settings-code-editor:hover { + cursor: text; +} + +.settings-code-editor textarea { + width: 100%; + max-width: none; + min-height: 300px; + line-height: 22px; + border: none; +} + +.settings-code-editor .CodeMirror { + padding: 0; + border: none; + border-radius: inherit; + background: var(--white); + color: var(--darkgrey); +} + +.settings-code-editor .CodeMirror-gutters { + background-color: var(--whitegrey-l2); + border-right: 1px solid var(--lightgrey); +} + +.settings-code-editor .CodeMirror-cursor { + border: 1px solid var(--midgrey); +} + +.settings-code-editor .cm-s-xq-light span.cm-meta { + color: #000; +} + +@media (max-width: 800px) { + .settings-code-editor { + width: calc(100vw - 8vw - 40px) !important; + } +} + +.gh-seo-container { + display: flex; + width: 100%; + margin-bottom: 2.4rem; + padding: 20px 30px 16px; + border: 1px solid var(--whitegrey-d1); + font-family: Arial, sans-serif; + background: #fff; + border-radius: 3px; +} + +.gh-seo-container svg { + width: 92px; + height: 30px; + margin-right: 32px; +} + +@media (max-width: 1360px) { + .gh-seo-settings { + flex-direction: column; + } + + .gh-seo-settings .form-group { + max-width: 100%; + } + + .gh-seo-settings-left, + .gh-seo-container { + max-width: 591px; + } +} + +@media (min-width: 1360px) { + .gh-seo-settings-left { + margin-right: 2.4rem; + } + + .gh-seo-container { + max-width: 1091px; + } +} + +.gh-og-container { + width: 476px; + margin-bottom: 2.4rem; + border: 1px solid var(--whitegrey-d1); + background: #fff; + border-radius: 3px; +} + +@media (max-width: 1080px) { + .gh-og-container { + width: 100%; + max-width: 476px; + } +} + +.gh-twitter-container { + width: 591px; + margin-bottom: 2.4rem; + border: 1px solid var(--whitegrey-d1); + background: #fff; + border-radius: 3px; +} + +@media (max-width: 1080px) { + .gh-twitter-container { + width: 100%; + max-width: 591px; + } +} diff --git a/ghost/admin/app/styles/layouts/tiers.css b/ghost/admin/app/styles/layouts/tiers.css index 0074d3850bc..4fa7f757c5c 100644 --- a/ghost/admin/app/styles/layouts/tiers.css +++ b/ghost/admin/app/styles/layouts/tiers.css @@ -1,30 +1,3 @@ -/* Tier list */ -.gh-tier-list { - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-gap: 32px; -} - -@media (max-width: 980px) { - .gh-tier-list { - grid-template-columns: repeat(1, 1fr); - } -} - -.gh-contentfilter-menu-trigger-tiers { - margin-right: 0 !important; - background: transparent !important; -} - -.gh-contentfilter-menu-trigger-tiers .ember-power-select-selected-item { - font-weight: 400 !important; - color: var(--darkgrey) !important; -} - -.gh-tier-cards { - margin: 0 0 24px; -} - .gh-tier-card { position:relative; display: flex; @@ -38,141 +11,6 @@ } } -.gh-tier-card-button-container { - position: absolute; - right: 24px; - top: 24px; - margin-right: 0; -} - -.gh-tier-card-actions-button, .gh-tier-card-edit-button { - margin-right: 0; - cursor: pointer; -} - -.gh-tier-card-actions-button.gh-btn span, .gh-tier-card-edit-button.gh-btn span { - height: 24px; -} - -.gh-tier-actions-menu { - top: calc(100% + 6px); - left: auto; - right: 0; -} - -.gh-tier-actions-menu.closed { - display: none; -} - -.gh-tier-card-block { - flex-basis: 30%; -} - -.gh-tier-card-block:not(:first-of-type) { - padding-left: 16px; -} - -.gh-tier-card-block h4 { - font-size: 1.3rem; - font-weight: 500; -} - -.gh-tier-card-block h4 .counter { - font-weight: 400; - color: var(--midgrey); -} - -.gh-tier-card-name { - font-size: 1.8rem; - font-weight: 600; - margin: 0; -} - -.gh-tier-card-empty-state { - display: flex; - justify-content: center; - align-items: center; -} - -.gh-tier-card-empty-state p { - margin-bottom: 0; - padding: 3.2rem; - color: var(--midgrey); -} - -.gh-tier-card-description { - font-size: 1.3rem; - line-height: 1.45em; - margin: 4px 20px 4px 0; - color: var(--midgrey); -} - -.gh-tier-card-block.title-block { - flex-basis: 40%; -} - -.gh-tier-card-block.benefits-block .gh-tier-card-description { - margin-top: 9px; -} - -.gh-tier-card-block ul.benefits { - list-style: none; - margin: 10px 0 0; - padding: 0; -} - -.gh-tier-card-block ul.benefits li { - display: flex; - align-items: flex-start; - font-size: 1.3rem; - line-height: 1.45em; - color: var(--middarkgrey); -} - -.gh-tier-card-block ul.benefits li svg { - flex-basis: 18px; - width: 14px; - height: 14px; - min-width: 18px; - margin-top: 3px; - margin-right: 4px; - color: var(--black); -} - -.gh-tier-card-block ul.benefits li span { - flex-grow: 1; -} - -.gh-tier-card-block.price-block { - display: flex; - flex-direction: column; - align-items: flex-end; - margin: 0 70px 0 20px; -} - -.gh-tier-price-container { - display: flex; - flex-direction: column; - border: 1px solid var(--whitegrey); -} - -.gh-expendable-free-membership .gh-tier-price-container { - margin: 0 60px 0 20px; -} - -.gh-expendable-free-membership .gh-tier-card-price { - border-right: none !important; -} - -.gh-expendable-free-membership .gh-tier-card-block{ - flex-basis: auto; -} - -.gh-tier-price-cards { - display: inline-flex; - justify-content: flex-end; -} - .gh-tier-card-price { display: flex; flex-direction: column; @@ -186,19 +24,6 @@ min-height: 66px; } -.gh-tier-free-trial-label { - text-align: center; - font-size: 1.25rem; - color: var(--midgrey); - padding: 4px; - border-top: 1px solid var(--whitegrey); -} - -.gh-tier-free-trial-days { - font-weight: 600; - color: var(--darkgrey); -} - .gh-tier-card-price:first-of-type { border-right: 1px solid var(--whitegrey); } @@ -249,581 +74,7 @@ margin-top: 2px; } -.gh-tier-cards-footer { - display: flex; - align-items: center; - margin-top: -7px; - color: var(--midgrey); - font-size: 1.35rem; -} - -.gh-btn-add-tier, -.gh-btn-add-tier:hover { - margin-right: 5px; -} - -.gh-btn-add-tier svg { - width: 1rem; - height: 1rem; - margin: 1px 4px 0 0; -} - -.gh-tier-list-icon { - display: flex; - align-items: flex-end; - justify-content: center; - color: var(--green); - margin-bottom: 8px; - height: 72px; -} - -.gh-tier-list-icon svg { - width: 60px; - height: 60px; -} - -.gh-tier-list-siteicon { - width: 54px; - height: 54px; - background-color: transparent; - background-size: 54px; - border-radius: 3px; - margin-bottom: 6px; -} - -.gh-tier-list-icon svg circle, -.gh-tier-list-icon svg path { - stroke-width: 1px !important; -} - -/* Tier details */ -.gh-tier-details { - display: grid; - grid-template-columns: 1fr; - grid-gap: 32px; - margin-bottom: 3vw; -} - -.gh-tier-details-form { - display: flex; - align-items: flex-start; - padding-top: 20px !important; -} - -.gh-tier-icon-container { - width: unset; - padding-bottom: 0; - margin-bottom: 0; -} - -.gh-tier-icon { - display: flex; - align-items: center; - justify-content: center; - background: var(--white); - width: 124px; - height: 124px; - margin-right: 24px; - border: 1px solid var(--whitegrey); - border-radius: 3px; -} - -.gh-tier-details-fields { - width: 100%; -} - -.gh-tier-details-fields .max-width { - max-width: 840px; -} - -.gh-tier-details-fields .form-group:last-of-type { - padding-bottom: 0; - margin-bottom: 0; -} - -.gh-tier-details section { - display: flex; - flex-direction: column; - justify-content: stretch; -} - -/* Tier stats */ -.gh-tier-stat-container { - display: flex; - flex-direction: column; -} - -.gh-tier-stat-details .data { - white-space: nowrap; - font-size: 3.1rem; - line-height: 1em; - font-weight: 700; - letter-spacing: 0; - margin: 0 0 2px; - padding: 0; -} - -.gh-tier-stat-details .info { - color: var(--midgrey); - margin: 0 0 10px; - padding: 0; -} - -.gh-tier-chart { - color: var(--whitegrey); - border: 1px solid var(--whitegrey); - border-top-color: transparent; - height: 90px; - display: flex; - align-items: center; - justify-content: center; - margin: 0 0 12px; -} - -/* Price list */ -.gh-price-list { - margin-bottom: 24px; -} - -.gh-price-list a span { - color: var(--midgrey); - font-size: 1.3rem; -} - -.gh-price-list-actionlist { - display: flex; - align-items: center; - justify-content: flex-end; - width: 100%; - line-height: 1; -} - -.gh-price-list .gh-list-row:hover .gh-price-list-actionlist { - opacity: 1; -} - -.gh-price-list-actionlist a, -.gh-price-list-actionlist button { - margin-left: 15px; - padding: 0; - line-height: 0; -} - -.gh-price-list-actionlist a span, -.gh-price-list-actionlist button span { - display: inline-block; - line-height: 1; - height: unset; - border-radius: 3px; - padding: 4px 6px; - color: var(--darkgrey); - font-weight: 500; - font-size: 1.2rem !important; - text-transform: uppercase; -} - -.gh-price-list-actionlist a:hover span, -.gh-price-list-actionlist button:hover span { - background: var(--whitegrey); -} - -.gh-price-list-actionlist a.archived:hover span, -.gh-price-list-actionlist button.archived:hover span { - background: color-mod(var(--red) a(10%)); - color: var(--red); -} - -.gh-price-list-title, -.gh-price-list-price { - width: 50%; -} - -.gh-price-list-name span.archived { - background: var(--lightgrey-l2); - color: var(--midgrey); - font-size: 1.2rem; -} - -.gh-price-list-archived .gh-price-list-name .name, -.gh-price-list-archived .gh-price-list-description, -.gh-price-list-archived .gh-price-list-price span, -.gh-price-list-archived .gh-price-list-subscriptions span { - opacity: 0.5; -} - -.gh-price-list-noprices { - text-align: center; - padding: 48px 0; - color: var(--midgrey); -} - -.gh-btn-archive-toggle { - width: 80px; -} - .tier-actions-menu.fade-out { animation-duration: 0.01s; pointer-events: none; } - -/* Add/edit tier modal */ -.fullscreen-modal-edit-tier { - max-width: 1080px; -} - -.gh-tier-modal-content { - margin: -32px -32px 0; - padding: 32px 32px 0; - max-height: calc(100vh - 16vw); - overflow-y: auto; -} - -.gh-form-edit-tier .gh-main-section { - margin-bottom: 32px; - grid-template-columns: 1fr 0.8fr 1.2fr; -} - -.gh-form-edit-tier .gh-main-section-block { - display: flex; - flex-direction: column; - margin-bottom: 0; -} - -.gh-form-edit-tier .gh-main-section-content { - padding-top: 16px; - margin-bottom: 0; -} - -.gh-tier-priceform-block { - margin-bottom: 32px; -} - -.gh-tier-priceform-block .form-group:last-of-type { - margin-bottom: 0; -} - -.gh-tier-priceform-pricecurrency { - display: grid; - grid-template-columns: 1fr 2fr; - grid-gap: 20px; -} - -.gh-form-edit-tier .gh-main-section-content.gh-tier-form-benefits { - padding-left: 8px; - margin-bottom: 0; -} - -.gh-tier-benefits .gh-input { - padding: 6px 28px 6px 30px; -} - -.gh-tier-benefits .gh-blognav-line { - position: relative; -} - -.gh-tier-benefits .gh-blognav-line svg { - position: absolute; - width: 12px; - height: 12px; - top: 13px; - left: 11px; -} - -.gh-tier-benefits .gh-blognav-line.placeholder { - color: var(--midlightgrey); -} - -.gh-tier-benefits .gh-blognav-line svg path { - stroke-width: 3px; -} - -.gh-tier-benefits .gh-blognav-item { - position: relative; - align-items: center; -} - -.gh-tier-benefits .gh-blognav-item.gh-blognav-item--error { - align-items: flex-start; -} - -.gh-blognav-item--error button.gh-blognav-add { - margin-top: 12px; -} - -.gh-tier-benefits .gh-blognav-label { - margin-right: 0; -} - -.gh-tier-benefits .gh-blognav-label .response { - position: relative; - font-size: 1.25rem; - margin: 2px 0 6px; -} - -.gh-tier-benefits .gh-blognav-delete { - position: absolute; - top: 4px; - right: 8px; - opacity: 0; - color: var(--midgrey); -} - -.gh-tier-benefits .gh-blognav-delete:hover { - color: var(--red); -} - -.gh-tier-benefits .gh-blognav-add { - margin-top: 2px; -} - -.gh-tier-benefits .gh-blognav-grab { - text-indent: 0px; - opacity: 0; -} - -.gh-tier-benefits .gh-blognav-item:hover .gh-blognav-delete, -.gh-tier-benefits .gh-blognav-item:hover .gh-blognav-grab { - opacity: 1; -} - -.gh-tier-benefits .gh-blognav-item:not(.gh-blognav-item--sortable):not(:last-of-type) { - margin-bottom: 16px; -} - -.gh-tier-benefit-hint { - color: var(--midgrey-d2); - font-size: 1.25rem !important; - font-weight: 400; - padding: 0 16px; - margin-top: -12px; -} - -.gh-tier-form-tierpreview-content { - position: sticky; - top: 45px; - height: max-content; -} - -.gh-tier-form-tierpreview .gh-main-section-content { - flex: 1; - max-width: 420px; - min-width: 320px; - position: relative; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: stretch; - background: white; - padding: 32px; - border-radius: 7px; - border: 1px solid #e1e1e1; - min-height: 200px; - letter-spacing: normal; -} - -.gh-portal-tier-card-header { - width: 100%; - min-height: 56px; -} - -.gh-tier-form-tierpreview .gh-main-section-content .gh-portal-tier-name { - font-size: 1.8rem; - font-weight: 600; - line-height: 1.3em; - letter-spacing: 0px; - margin-top: -4px; - margin-bottom: 0; - word-break: break-word; - width: 100%; -} - -.gh-tier-form-tierpreview .gh-main-section-content .gh-portal-tier-description { - font-size: 1.55rem; - font-weight: 600; - line-height: 1.4em; - width: 100%; - margin-top: 16px; - color: #3d3d3d; -} - -.gh-portal-tier-card-pricecontainer { - display: flex; - flex-direction: row; - align-items: flex-end; - justify-content: space-between; - flex-wrap: wrap; - row-gap: 10px; - column-gap: 4px; - width: 100%; - margin-top: 16px; -} - -.gh-portal-tier-price { - display: flex; - justify-content: center; - color: #1d1d1d; -} - -.gh-portal-tier-card-details { - flex: 1; - display: flex; - flex-direction: column; - width: 100%; -} - -.gh-portal-tier-card-detaildata { - flex: 1; -} - -.gh-portal-tier-price .amount { - font-size: 3.4rem; - font-weight: 700; - line-height: 1em; - letter-spacing: -1.3px; -} - -.gh-tier-form-tierpreivew-cadence { - display: flex; - align-items: baseline; - justify-content: space-between; -} - -.gh-tier-form-tierpreivew-cadence .gh-btn, -.gh-tier-form-tierpreivew-cadence .gh-btn span { - background: transparent !important; - padding: 0; - line-height: 1em; - height: auto; - font-size: 1.3rem; - color: var(--midlightgrey); - margin-left: 4px; - font-weight: 400; - overflow: unset; -} - -.gh-tier-form-tierpreivew-cadence .gh-btn:hover span { - color: var(--middarkgrey); -} - -.gh-tier-form-tierpreivew-cadence .gh-btn.selected span { - font-weight: 500; - color: var(--darkgrey); -} - -.gh-tier-form-tierpreview .monthly-price { - display: flex; - align-items: baseline; - font-size: 3.3rem; - font-weight: 500; - line-height: 1em; - color: #3d3d3d; -} - -.gh-tier-form-tierpreview .currency-sign { - align-self: flex-start; - font-size: 2.7rem; - font-weight: 700; - line-height: 1.115em; - text-transform: uppercase; -} - -.gh-tier-form-tierpreview .billing-period { - align-self: flex-end; - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.4em; - color: #686868; - margin-left: 5px; - font-weight: 400; -} - -.gh-portal-discount-label { - position: relative; - font-size: 1.25rem; - line-height: 1em; - font-weight: 600; - letter-spacing: 0.3px; - color: #1d1d1d; - padding: 6px 9px; - text-align: center; - white-space: nowrap; - border-radius: 999px; - margin-right: -4px; - margin-top: -4px; - max-height: 24.5px; -} - -.gh-portal-discount-label span { - position: absolute; - content: ""; - display: block; - top: 0; - right: 0; - bottom: 0; - left: 0; - border-radius: 999px; - opacity: 0.2; -} - -.gh-tier-form-tierpreview .gh-portal-tier-benefits { - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.4em; - width: 100%; - margin-top: 16px; -} - -.gh-tier-form-tierpreview .gh-portal-tier-benefit { - display: flex; - align-items: flex-start; - margin-bottom: 10px; - color: #3d3d3d; -} - -.gh-portal-tier-benefit svg { - width: 14px; - height: 14px; - min-width: 14px; - margin: 3px 10px 0 0; - overflow: visible; -} - -.gh-tier-form-tierpreview .gh-portal-tier-benefit polyline, -.gh-tier-form-tierpreview .gh-portal-tier-benefit g, -.gh-tier-form-tierpreview .gh-portal-tier-benefit path { - stroke-width: 3px; -} - -.gh-tier-form-tierpreview .gh-portal-benefit-title { - letter-spacing: normal; -} - -.gh-tier-form-tierpreview .placeholder { - opacity: 0.35; -} - -.gh-tier-form-tierpreview .gh-portal-discount-label-trial { - font-weight: 600; - font-size: 1.3rem; - line-height: 1; - margin-top: 4px; -} - -.gh-tier-setting-title { - font-size: 1.3rem; - font-weight: 600; - margin: 0; -} - -.gh-tier-settings .for-switch.small { - width: 36px !important; - height: 22px !important; -} - -.gh-tier-settings .gh-tier-settings-hide { - display: none !important; -} - -.gh-tier-settings .gh-tier-settings-show { - display: flex !important; -} \ No newline at end of file diff --git a/ghost/admin/app/styles/layouts/user.css b/ghost/admin/app/styles/layouts/user.css deleted file mode 100644 index b5457108902..00000000000 --- a/ghost/admin/app/styles/layouts/user.css +++ /dev/null @@ -1,212 +0,0 @@ -/* User profile /ghost/settings/users// -/* ---------------------------------------------------------- */ - - -/* User actions menu -/* ---------------------------------------------------------- */ - -.user-actions-cog { - margin-right: 10px; - color: var(--darkgrey); -} - -.user-actions-cog svg { - height: 16px; - width: 16px; - margin-right: 0; -} - -.user-actions-cog svg path { - stroke: var(--darkgrey); -} - -.user-actions-menu { - top: calc(100% + 6px); - right: 10px; - left: auto; -} - -.user-actions-menu.fade-out { - animation-duration: 0.01s; - pointer-events: none; -} - - -/* Layout -/* ---------------------------------------------------------- */ - -.settings-user { - padding: 0 0 3vw; -} - -.user-cover { - display: block; - overflow: hidden; - margin: 0; - width: 100%; - height: 300px; - margin-bottom: 30px; - background: #fafafa no-repeat center center; - background-size: cover; -} - -.user-cover-edit { - position: absolute; - top: 20px; - left: 20px; - z-index: 2; - min-height: 37px; - height: 37px; - border-width: 0; - background: rgba(0, 0, 0, 0.3); - border-radius: var(--border-radius); - color: rgba(255, 255, 255, 0.8); - text-shadow: none; - transition: color 0.3s ease, background 0.3s ease; -} - -.user-cover-edit:hover { - background: rgba(0, 0, 0, 0.5); - color: #fff; -} - -.user-details-bottom, -.user-details-form { - max-width: 540px; - margin: 2vw auto 0; -} - -.user-details-form { - border-top: 1px solid var(--lightgrey); - padding-top: 4vw; - margin-bottom: -2vw; -} - - -/* Edit user -/* ---------------------------------------------------------- */ - -.user-profile { - position: relative; - z-index: 1; -} - -@media (max-width: 550px) { - .user-profile fieldset { - padding: 0 15px; - } -} - -.user-profile textarea { - min-width: 100%; -} - - -/* Profile picture -/* ---------------------------------------------------------- */ - -.user-image { - position: absolute; - top: 236px; - left: 0; - right: 0; - z-index: 2; - margin: 0 auto; - padding: 0; - width: 120px; - height: 120px; - border-radius: 9999px; - border: 4px solid var(--white); - text-align: center; -} - -.user-image .img { - display: block; - width: 100%; - height: 100%; - background-position: center center; - background-size: cover; - border-radius: 9999px; -} - -.user-image:hover .edit-user-image { - opacity: 1; -} - -.edit-user-image { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - border-radius: 9999px; - color: #fff; - text-decoration: none; - text-transform: uppercase; - font-size: 12px; - line-height: 100px; - opacity: 0; - transition: opacity 0.3s ease; -} - -/* User roles modal -/* ---------------------------------------------------------- */ - -@media (max-height: 740px) { - .fullscreen-modal-change-role { - overflow-y: auto; - } - .fullscreen-modal-change-role .modal-content { - box-shadow: none !important; - } -} - -/* Delete user modal -/* ---------------------------------------------------------- */ - -.gh-transfer-tag strong { - color: var(--midgrey); - font-weight: 600; -} - -/* Notifications */ -.user-settings-heading { - margin-bottom: 3rem; - font-size: 1.55rem; - font-weight: 700; - line-height: 1.3em; -} - -.user-settings-subgroup { - border-top: 1px solid var(--lightgrey); - font-size: 1.55rem; - font-weight: 700; - line-height: 1.3em; - padding-top: 3vw; - margin-top: 4vw; - margin-bottom: -1vw; -} - -.user-settings-subgroup .form-group { - margin-bottom: 0; -} - -.user-setting-toggle { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 20px; -} - -.user-setting-toggle label { - margin-bottom: 0; -} - -.user-setting-toggle p { - margin-top: 0; -} - -.user-settings-subgroup .for-switch { - width: 38px !important; -} \ No newline at end of file diff --git a/ghost/admin/app/styles/layouts/users.css b/ghost/admin/app/styles/layouts/users.css deleted file mode 100644 index 7c9e44fb34e..00000000000 --- a/ghost/admin/app/styles/layouts/users.css +++ /dev/null @@ -1,291 +0,0 @@ -/* Users /ghost/settings/users/ -/* ---------------------------------------------------------- */ - -.gh-invited-users .apps-grid-cell:hover { - background: none; -} - -.gh-invited-users .gh-badge { - text-transform: none; -} - -@media (max-width: 500px) { - .gh-invited-users .apps-card-meta { - max-width: 165px; - } - - .gh-invited-users .apps-card-app-title { - width: 200px; - } - - .gh-invited-users .apps-card-app-desc { - max-height: none; - display: block; - } - - .gh-invited-users .apps-configured { - flex-direction: column; - align-items: flex-end; - } - - .gh-invited-users .apps-configured a { - margin-bottom: 7px - } -} - -@media (max-width: 600px) { - .gh-user-arrow-icon { - display: none; - } -} - - -.user-list-item-icon { - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - margin-right: 12px; - width: 36px; - height: 36px; - background: #E5EFF5; - border-radius: 100%; - color: transparent; - font-size: 0; -} - -.user-list-item-icon svg { - fill: var(--midgrey); - height: 14px; - width: auto; -} - -.user-list-item-figure { - position: relative; - display: block; - width: 36px; - height: 36px; - margin-right: 12px; - margin-left: 3px; - background-position: center center; - background-size: cover; - border-radius: 100%; -} - -.user-list-item-figure img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0; -} - -.user-list-item-no-interaction a.disabled { - cursor: default; -} - - -.user-list-item-no-interaction .gh-badge { - margin-right: 28px; -} - -.user-list-item-no-interaction.apps-grid-cell:hover { - background: transparent !important; -} - -.user-list-item-no-interaction .gh-user-arrow-icon { - display: none; -} - -.gh-team .apps-configured { - justify-content: flex-end; -} - -.gh-team .apps-configured .gh-badge { - margin-left: 15px; -} - -@media (max-width: 500px) { - .gh-active-users .apps-configured { - flex-wrap: nowrap; - } - - .gh-active-users .gh-badge:first-child { - margin-left: 0; - } -} - -/* Role Labels -/* ---------------------------------------------------------- */ - -.gh-badge.owner { - background: var(--middarkgrey); - text-transform: uppercase; - color: var(--white); -} - -.gh-badge.administrator { - background: color-mod(var(--pink) a(15%)); - text-transform: uppercase; - color: var(--pink-d2); -} - -.gh-badge.editor { - background: color-mod(var(--blue) a(20%)); - text-transform: uppercase; - color: var(--blue-d2); -} - -.gh-badge.contributor { - background: var(--lightgrey); - text-transform: uppercase; - color: var(--middarkgrey); -} - -.gh-badge.author { - background: color-mod(var(--green) a(20%)); - text-transform: uppercase; - color: var(--green-d2); -} - -.gh-badge.suspended { - background: var(--lightgrey); - text-transform: uppercase; - color: var(--middarkgrey); - margin-left: 1.2rem; -} - -.gh-badge.locked { - background: none; - color: var(--midgrey); -} - -.gh-badge.locked svg path { - fill: none; -} - -/* User invitation modal -/* ---------------------------------------------------------- */ - -@media (max-height: 900px) { - .fullscreen-modal-invite-user { - overflow-y: auto; - } - .fullscreen-modal-invite-user .modal-content { - box-shadow: none !important; - } -} - -.gh-modal-invite-user { - margin: -32px -32px 0; - padding: 32px 32px 0; - max-height: calc(100vh - 120px); - overflow-y: auto; -} - -.invite-new-user .modal-content { - width: 100%; - max-width: 600px; -} - -.gh-roles-container .form-group { - margin-bottom: 0; - padding: 0; -} - -.gh-roles-container .form-group label { - position: static; - display: block; - text-align: left; -} - -.gh-roles-container .form-group input { - width: 100%; -} - -.invite-new-user .gh-roles-container { - display: flex; - flex-direction: column; - margin: 2rem 0 0; -} - -.invite-new-user p { - margin: 4px 0 20px; - color: var(--midgrey); - font-size: 1.3rem; - line-height: 1.2em; - font-weight: 400; -} - -.gh-roles-container .gh-radio { - padding-bottom: 20px; - border-bottom: 1px solid var(--list-color-divider); -} - -.gh-roles-container .gh-radio:first-child { - padding-top: 20px; -} - -.gh-roles-container .gh-radio svg { - width: 16px; - height: 16px; - margin-left: 2px; - color: var(--midgrey); -} - -.gh-roles-container .gh-radio-content { - margin-right: 2.4rem; -} - -.gh-roles-container .popover { - width: 97%; - border: 1px solid var(--whitegrey-d1); - color: var(--darkgrey); - box-shadow: var(--shadow-3); -} - -.gh-roles-container .popover-arrow { - display: none; -} - -.gh-roles-container .popover table { - margin: .6em 0; -} - -.gh-roles-container .popover td.left { - padding-right: 16px; - font-weight: 600; - white-space: nowrap; -} - -.gh-roles-container .gh-btn-black, -.gh-roles-container .gh-btn-green { - margin: 0; - width: 100%; -} - -/* Reset all passwords modal -/* ---------------------------------------------------------- */ - -.gh-modal-reset-passwords .for-checkbox .input-toggle-component { - background: var(--white); -} - -.gh-modal-reset-passwords h4 { - margin-bottom: .4rem; - font-size: 1.4rem; - font-weight: 600; - line-height: 1.4em; -} - -.gh-modal-reset-passwords p { - margin: 0 0 2em; -} - -.gh-modal-reset-passwords .description { - color: var(--midgrey); - font-size: 1.4rem; - font-weight: 300; -} \ No newline at end of file diff --git a/ghost/admin/app/styles/layouts/whatsnew.css b/ghost/admin/app/styles/layouts/whatsnew.css index 44da196a4eb..c584a95ac59 100644 --- a/ghost/admin/app/styles/layouts/whatsnew.css +++ b/ghost/admin/app/styles/layouts/whatsnew.css @@ -280,6 +280,47 @@ 0 -4px 7px rgba(0, 0, 0, 0.06); } +.gh-about-container { + display: grid; + grid-template-columns: 2fr 1fr; + grid-gap: 80px; +} + +.gh-whats-new-canvas .gh-about-container { + display: flex; + grid-template-columns: unset; + grid-gap: unset; + margin: 0 auto; + max-width: 920px; + margin-top: 60px; +} + +.gh-about-container h2 { + font-size: 1.65rem; + line-height: 1.4em; + font-weight: 600; + border-bottom: 1px solid var(--lightgrey-l2); + padding-bottom: 12px; + margin-bottom: 12px; +} + +.gh-about-box { + position: sticky; + top: 96px; + right: 0; + display: flex; + flex-grow: 1; + flex-direction: column; + height: max-content; + border-radius: 3px; + min-width: 300px; +} + +.gh-about-box.grey { + border: none; + background: var(--main-color-content-greybg); +} + @media (max-width: 1380px) { .gh-wn-content { max-width: 36vw; @@ -303,18 +344,6 @@ grid-gap: 32px; } - .gh-env-details { - grid-row: 1/2; - } - - .gh-about-content-actions { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-gap: 20px; - grid-row: 2/3; - margin-top: -12px; - } - .gh-whats-new { grid-row: 3/4; } @@ -329,15 +358,6 @@ } } -@media (max-width: 540px) { - .gh-about-content-actions { - grid-template-columns: unset; - grid-template-rows: auto; - grid-gap: 12px; - margin-top: -20px; - } -} - /* Custom card styles /* ---------------------------------------------------------- */ diff --git a/ghost/admin/app/styles/patterns/global.css b/ghost/admin/app/styles/patterns/global.css index b296ab3f5a7..4c8769a93c1 100644 --- a/ghost/admin/app/styles/patterns/global.css +++ b/ghost/admin/app/styles/patterns/global.css @@ -591,10 +591,6 @@ button { line-height: inherit; } -i { - display: block; -} - img, input[type="image"] { max-width: 100%; diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs index 2a4fde9cebd..f5679a7fe3d 100644 --- a/ghost/admin/app/templates/lexical-editor.hbs +++ b/ghost/admin/app/templates/lexical-editor.hbs @@ -82,6 +82,7 @@ @setFeatureImage={{this.setFeatureImage}} @setFeatureImageAlt={{this.setFeatureImageAlt}} @setFeatureImageCaption={{this.setFeatureImageCaption}} + @handleFeatureImageCaptionBlur={{this.handleFeatureImageCaptionBlur}} @clearFeatureImage={{this.clearFeatureImage}} @cardOptions={{hash post=this.post diff --git a/ghost/admin/app/templates/settings.hbs b/ghost/admin/app/templates/settings.hbs deleted file mode 100644 index b9c512376f8..00000000000 --- a/ghost/admin/app/templates/settings.hbs +++ /dev/null @@ -1,124 +0,0 @@ -
- -

- Settings -

-
- {{#if this.upgradeStatus.message}} - - {{else}} - - {{/if}} -
-
- -
-
Website
-
- - {{svg-jar "settings"}} -
-

General

-

Basic publication details and site metadata

-
-
- - {{svg-jar "paint-palette"}} -
-

Design

-

Customize your site and manage themes

-
-
- - {{svg-jar "compass"}} -
-

Navigation

-

Set up primary and secondary menus

-
-
- - - {{svg-jar "staff"}} -
-

Staff

-

Manage authors, editor and collaborators

-
-
- - {{#if (feature 'announcementBar')}} - - {{svg-jar "confetti"}} -
-

Announcement bar

-

Highlight important updates or offers

-
-
- {{/if}} -
- -
Members
-
- - {{svg-jar "members"}} -
-

Membership

-

Access, subscription, and pricing options

-
-
- - {{svg-jar "email-stroke"}} -
-

Email newsletter

-

Customize emails and set email addresses

-
-
- - {{svg-jar "chart"}} -
-

Analytics

-

Decide what data you collect

-
-
-
- -
Advanced
-
- - {{svg-jar "module"}} -
-

Integrations

-

Make Ghost work with apps and tools

-
-
- - - {{svg-jar "brackets"}} -
-

Code injection

-

Add code to your publication

-
-
- - - {{svg-jar "labs"}} -
-

Labs

-

Testing ground for new features

-
-
- - - {{svg-jar "calendar-stroke"}} -
-

History

-

View system event log

-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/analytics.hbs b/ghost/admin/app/templates/settings/analytics.hbs deleted file mode 100644 index 60fd6f4ee05..00000000000 --- a/ghost/admin/app/templates/settings/analytics.hbs +++ /dev/null @@ -1,31 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Analytics -
-

- Analytics -

-
-
- -
-
- -
- -
-
-{{outlet}} \ No newline at end of file diff --git a/ghost/admin/app/templates/settings/announcement-bar/index.hbs b/ghost/admin/app/templates/settings/announcement-bar/index.hbs deleted file mode 100644 index 8a8a8d2fbce..00000000000 --- a/ghost/admin/app/templates/settings/announcement-bar/index.hbs +++ /dev/null @@ -1,43 +0,0 @@ -
- -

Announcement bar

-
- {{#if (gt this.themeManagement.availablePreviewTypes.length 1)}} -
- - {{svg-jar "arrow-down-small"}} -
- {{/if}} - -
- - -
- - -
-
- -
- - - -
-
\ No newline at end of file diff --git a/ghost/admin/app/templates/settings/code-injection-loading.hbs b/ghost/admin/app/templates/settings/code-injection-loading.hbs deleted file mode 100644 index cec4c073044..00000000000 --- a/ghost/admin/app/templates/settings/code-injection-loading.hbs +++ /dev/null @@ -1,22 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Code injection -
-

- Code injection -

-
-
- -
-
- -
- -
-
diff --git a/ghost/admin/app/templates/settings/code-injection.hbs b/ghost/admin/app/templates/settings/code-injection.hbs deleted file mode 100644 index 267d922e1aa..00000000000 --- a/ghost/admin/app/templates/settings/code-injection.hbs +++ /dev/null @@ -1,45 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Code injection -
-

- Code injection -

-
-
- -
-
- -
-
-
-

- {{svg-jar "idea"}} - Ghost allows you to inject code into the top and bottom of your theme files without editing them. This allows for quick modifications to insert useful things like tracking codes and meta tags. -

- -
- -

Code here will be injected into the \{{ghost_head}} tag on every page of the site

-
- -
-
- -
- -

Code here will be injected into the \{{ghost_foot}} tag on every page of the site

-
- -
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/design/change-theme.hbs b/ghost/admin/app/templates/settings/design/change-theme.hbs deleted file mode 100644 index fa400ea0fea..00000000000 --- a/ghost/admin/app/templates/settings/design/change-theme.hbs +++ /dev/null @@ -1,47 +0,0 @@ -
- -

Themes

-
- - -
-
- -
- {{#liquid-if this.showAdvanced}} -
- -
- {{/liquid-if}} - -
-
- {{#each this.themesList as |theme|}} - -
- -
-
- {{theme.name}} Theme -
-
-
{{theme.name}} - {{#if theme.isDefault}} - (Default) - {{/if}} - {{#if theme.isActive}} - Active - {{/if}} -
-
{{theme.category}}
-
-
- {{/each}} -
-
-
-
- - \ No newline at end of file diff --git a/ghost/admin/app/templates/settings/design/index.hbs b/ghost/admin/app/templates/settings/design/index.hbs deleted file mode 100644 index 46eac344f83..00000000000 --- a/ghost/admin/app/templates/settings/design/index.hbs +++ /dev/null @@ -1,43 +0,0 @@ -
- -

Site design

-
- {{#if (gt this.themeManagement.availablePreviewTypes.length 1)}} -
- - {{svg-jar "arrow-down-small"}} -
- {{/if}} - -
- - -
- - -
-
- -
- - - -
-
\ No newline at end of file diff --git a/ghost/admin/app/templates/settings/design/no-theme.hbs b/ghost/admin/app/templates/settings/design/no-theme.hbs deleted file mode 100644 index e6fafeacf04..00000000000 --- a/ghost/admin/app/templates/settings/design/no-theme.hbs +++ /dev/null @@ -1,20 +0,0 @@ -
- -

Site design

-
-
-
-
-
    -
  1. -
    -

    No theme is currently active

    - - Activate a theme - -
    -
  2. -
-
-
-{{outlet}} \ No newline at end of file diff --git a/ghost/admin/app/templates/settings/general-loading.hbs b/ghost/admin/app/templates/settings/general-loading.hbs deleted file mode 100644 index df1c78fe4c3..00000000000 --- a/ghost/admin/app/templates/settings/general-loading.hbs +++ /dev/null @@ -1,22 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} General -
-

- General -

-
-
- -
-
- -
- -
-
diff --git a/ghost/admin/app/templates/settings/general.hbs b/ghost/admin/app/templates/settings/general.hbs deleted file mode 100644 index 03a1cb6e9ed..00000000000 --- a/ghost/admin/app/templates/settings/general.hbs +++ /dev/null @@ -1,441 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} General -
-

- General -

-
-
- -
-
- -
-
-

Publication info

-
-
-
-
-

Title & description

-

The details used to identify your publication around the web

-
- -
-
- {{#liquid-if this.pubInfoOpen}} -
- - - -

The name of your site

-
- - - - -

A short description, used in your theme, meta data and search results

-
-
- {{/liquid-if}} -
-
- -
-
-
-

Site timezone

-

Set the time and date of your publication, used for all published posts

-
- -
-
- {{#liquid-if this.timezoneOpen}} -
- -
- {{/liquid-if}} -
-
- -
-
-
-

Publication Language

-

Set the language/locale which is used on your site

-
- -
-
- {{#liquid-if this.langOpen}} -
- - - -

Default: English (en); find out more about using Ghost in other languages

-
-
- {{/liquid-if}} -
-
-
-
- -
-

Site meta settings

-
-
-
-
-

Meta data

-

Extra content for search engines

-
- -
-
- {{#liquid-if this.metaDataOpen}} -
-
-
- - - - -

Recommended: 70 characters. You’ve used {{gh-count-down-characters this.settings.metaTitle 70}}

-
- - - - -

Recommended: 156 characters. You’ve used {{gh-count-down-characters this.settings.metaDescription 156}}

-
-
-
- -
-
-
- {{svg-jar "google"}} - -
- -
{{or this.settings.metaTitle this.settings.title}}
-
- {{truncate (or this.settings.metaDescription this.settings.description) 159}} -
-
-
-
-
-
- {{/liquid-if}} -
-
- -
-
-
-

Twitter card

-

Customize structured data of your site for Twitter

-
- -
-
- {{#liquid-if this.twitterCardOpen}} -
-
-
- - - - - - - - - - - - - - -
-
- -
-
- {{svg-jar "social-twitter" class="social-icon"}} -
- {{or this.settings.metaTitle this.settings.title}} - 12 hrs -
- - -
- - -
-
-
-
-
-
- {{/liquid-if}} -
-
- -
-
-
-

Facebook card

-

Customize structured data of your site

-
- -
-
- {{#liquid-if this.facebookCardOpen}} -
-
-
- - - - - - - - - - - - - - -
-
- -
-
- {{svg-jar "social-facebook" class="social-icon"}} -
-
{{or this.settings.metaTitle this.settings.title}}
-
12 hrs
-
-
-
- - -
-
- {{#if this.settings.ogImage}} -
- {{/if}} -
- {{!-- Ensures description is hidden if title exceeds one line --}} -
-
- {{this.config.blogDomain}} -
-
{{truncate (or this.settings.ogTitle this.settings.title)}}
-
{{truncate (or this.settings.ogDescription this.settings.description)}}
-
-
-
-
- {{svg-jar "facebook-like" class="z-999"}}{{svg-jar "facebook-heart" class="nl1"}}182 - 7 comments - 2 shares -
-
-
-
-
- {{/liquid-if}} -
-
- -
-
-
-

Social accounts

-

Link your social accounts for full structured data and rich card support

-
- -
-
- {{#liquid-if this.socialOpen}} -
- - - -

URL of your publication's Facebook Page

-
- - - -

URL of your publication's Twitter profile

-
-
- {{/liquid-if}} -
-
- -
-
- -
-

Advanced settings

-
-
-
-
-

Make this site private

-

- Enable protection with a simple shared password. All search engine optimization and social features will be disabled. -

-
-
- -
-
-
- {{#if this.settings.isPrivate}} -
- - A private RSS feed is available at - {{this.privateRSSUrl}} - - - - -

Set the password for this site

-
-
- {{/if}} -
-
-
-
-
-
-{{outlet}} \ No newline at end of file diff --git a/ghost/admin/app/templates/settings/history.hbs b/ghost/admin/app/templates/settings/history.hbs deleted file mode 100644 index 958c60a8d73..00000000000 --- a/ghost/admin/app/templates/settings/history.hbs +++ /dev/null @@ -1,64 +0,0 @@ -
- -
- {{#if this.userRecord}} -
- - Settings - - {{svg-jar "arrow-right-small"}} - - History - - {{svg-jar "arrow-right-small"}} User -
-

- {{or this.userRecord.name this.userRecord.email}} -

- {{else}} -
- - Settings - - {{svg-jar "arrow-right-small"}} History -
-

- History -

- {{/if}} -
-
- - - -
-
-
- {{#let (history-event-fetcher filter=(history-event-filter excludedEvents=this.fullExcludedEvents excludedResources=this.fullExcludedResources user=this.user) pageSize=200) as |eventsFetcher|}} - {{#if eventsFetcher.data}} -
- - - {{#if (not (or eventsFetcher.isLoading eventsFetcher.hasReachedEnd))}} - - {{/if}} -
- {{else}} - {{#unless eventsFetcher.isLoading}} - - {{/unless}} - {{/if}} - - {{#if eventsFetcher.isLoading}} -
- {{/if}} - {{/let}} -
-
- -{{outlet}} diff --git a/ghost/admin/app/templates/settings/integration.hbs b/ghost/admin/app/templates/settings/integration.hbs deleted file mode 100644 index 5d0158c8c67..00000000000 --- a/ghost/admin/app/templates/settings/integration.hbs +++ /dev/null @@ -1,287 +0,0 @@ -
-
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Edit integration -
-

- {{this.integration.name}} -

-
-
- -
-
- -
-

Configuration

-
-
-
-
- -
- - {{#unless this.integration.iconImage}} - {{svg-jar "integration" class="w11 h11"}} - {{/unless}} - - - - {{#if uploader.isUploading}} -
- {{uploader.progressBar}} -
- {{else}} - - {{/if}} -
- -
-
-
-
-
-
- - - -
- -
- - - -
- - - - - - - - - - - - - - - - -
Content API key -
- - {{this.integration.contentKey.secret}} - -
- - -
-
- {{#if (eq this.regeneratedApiKey.type this.integration.contentKey.type)}} -
Content API Key was successfully regenerated
- {{/if}} -
Admin API key -
- - {{this.integration.adminKey.secret}} - -
- - -
-
- {{#if (eq this.regeneratedApiKey.type this.integration.adminKey.type)}} -
Admin API key was successfully regenerated
- {{/if}} -
API URL -
- - {{this.apiUrl}} - -
- -
-
-
-
-
-
-
-
-
- -
-

Webhooks

-
- - - - - - - - - - - - {{#each this.filteredWebhooks as |webhook|}} - - - - - - - - {{else}} - - - - {{/each}} - - {{#if this.filteredWebhooks}} - - - - - - {{/if}} -
NameEventURLLast triggered
{{webhook.name}}{{event-name webhook.event}}{{webhook.targetUrl}}{{or webhook.lastTriggeredAtUTC "Not triggered"}} - {{!--
- - {{svg-jar "pen" class="w6 h6 fill-midgrey pa1 mr1"}} - - -
--}} - - - - {{svg-jar "dotdotdot"}} - - - - - - - -
-
-

- No webhooks configured -

- -
- {{svg-jar "plus"}} - Add webhook -
-
-
-
- -
- {{svg-jar "plus"}} - Add webhook -
-
-
-
-
- -
-
- -
-
-
- -{{outlet}} diff --git a/ghost/admin/app/templates/settings/integrations.hbs b/ghost/admin/app/templates/settings/integrations.hbs deleted file mode 100644 index 957c7c62afd..00000000000 --- a/ghost/admin/app/templates/settings/integrations.hbs +++ /dev/null @@ -1,295 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Integrations -
-

- Integrations -

-
-
- - - -
-

Built-in integrations

-
- {{#if this.zapierDisabled}} -
- -
-
-
-
-

Zapier

{{svg-jar "lock-filled"}}
-

Automation for your favorite apps

-
-
-
-
- -
-
-
-
-
- {{else}} -
- -
-
-
-
-

Zapier

-

Automation for your favorite apps

-
-
-
-
- Configure - {{svg-jar "arrow-right"}} -
-
-
-
-
- {{/if}} - -
- -
-
-
-
-

Slack

-

A messaging app for teams

-
-
-
-
- {{#if this.settings.slack.isActive}} - Active - {{else}} - Configure - {{/if}} - {{svg-jar "arrow-right"}} -
-
-
-
-
- -
- -
-
-
-
-

AMP

-

Google Accelerated Mobile Pages

-
-
-
-
- {{#if this.settings.amp}} - Active - {{else}} - Configure - {{/if}} - {{svg-jar "arrow-right"}} -
-
-
-
-
- -
- -
-
-
-
-

Unsplash

-

Beautiful, free photos

-
-
-
-
- {{#if this.settings.unsplash}} - Active - {{else}} - Configure - {{/if}} - {{svg-jar "arrow-right"}} -
-
-
-
-
-
- -
-
-
-
-

FirstPromoter

-

Launch your member referral program

-
-
-
-
- {{#if this.settings.firstpromoter}} - Active - {{else}} - Configure - {{/if}} - {{svg-jar "arrow-right"}} -
-
-
-
-
-
- -
-
-
-
-

Pintura

-

Advanced image editing

-
-
-
-
- {{#if this.settings.firstpromoter}} - Active - {{else}} - Configure - {{/if}} - {{svg-jar "arrow-right"}} -
-
-
-
-
-
-
- -
-

Custom integrations

-
- {{#each this.integrations as |integration|}} -
- -
-
-
- {{#unless integration.iconImage}} - {{svg-jar "integration" class="nudge-left--6 w9 stroke-darkgrey"}} - {{/unless}} -
-
-

- {{integration.name}} -

-

- {{integration.description}} -

-
-
-
-
- Configure - {{svg-jar "arrow-right"}} -
-
-
-
-
- {{else}} -
- {{#if this.fetchIntegrations.isRunning}} -
- {{else}} -
-

- Create your own custom Ghost integrations with dedicated API keys & webhooks -

- - {{svg-jar "plus"}} Add custom integration - -
- {{/if}} -
- {{/each}} -
- - {{#if this.integrations}} - {{!--
--}} -
- - {{svg-jar "plus"}} Add custom integration - -
- {{!--
--}} - {{/if}} -
- -
- -{{outlet}} diff --git a/ghost/admin/app/templates/settings/integrations/amp-loading.hbs b/ghost/admin/app/templates/settings/integrations/amp-loading.hbs deleted file mode 100644 index a1075403fda..00000000000 --- a/ghost/admin/app/templates/settings/integrations/amp-loading.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} AMP -
-
-
-
- -
- -
-
\ No newline at end of file diff --git a/ghost/admin/app/templates/settings/integrations/amp.hbs b/ghost/admin/app/templates/settings/integrations/amp.hbs deleted file mode 100644 index 9a1fc3385f7..00000000000 --- a/ghost/admin/app/templates/settings/integrations/amp.hbs +++ /dev/null @@ -1,85 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} AMP -
-
-
- -
-
- -
-
-
-
- -
-
-

AMP

-

Accelerated Mobile Pages

-
-
-
- -
-

AMP configuration

-
-
-
-
-
-
Enable AMP
-
Enable Google Accelerated Mobile Pages for your posts
-
-
-
- -
-
-
- {{#liquid-if this.settings.amp class=""}} -
-
-
Google Analytics Tracking ID
-
Tracks AMP traffic in Google Analytics, find your ID here
-
- - - - -
-
-
- {{/liquid-if}} -
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/integrations/firstpromoter.hbs b/ghost/admin/app/templates/settings/integrations/firstpromoter.hbs deleted file mode 100644 index 53bc9bf21e8..00000000000 --- a/ghost/admin/app/templates/settings/integrations/firstpromoter.hbs +++ /dev/null @@ -1,85 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} FirstPromoter -
-
-
- -
-
- -
-
-
-
- -
-
-

FirstPromoter

-

Launch your own member referral program

-
-
-
- -
-

FirstPromoter configuration

-
-
-
-
-
-
Enable FirstPromoter
-
Enable FirstPromoter for tracking referrals
-
-
-
- -
-
-
- {{#liquid-if this.settings.firstpromoter class=""}} -
-
-
FirstPromoter Account ID
-
Affiliate and referral tracking, find your ID here
-
- - - - -
-
-
- {{/liquid-if}} -
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/integrations/pintura.hbs b/ghost/admin/app/templates/settings/integrations/pintura.hbs deleted file mode 100644 index 5f3c362d929..00000000000 --- a/ghost/admin/app/templates/settings/integrations/pintura.hbs +++ /dev/null @@ -1,175 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Pintura -
-
-
- -
-
- -
-
-
-
- Pintura icon -
-
-

Pintura

-

Advanced image editing

-
-
-
- {{#unless this.config.pintura}} -
-
-
- Add advanced image editing to Ghost, with Pintura -

Pintura is a powerful JavaScript image editor that allows you to crop, rotate, annotate and modify images directly inside Ghost.

-

Try a demo, purchase a license, and download the required CSS/JS files from pqina.nl/pintura/ to activate this feature.

- -
- Pintura banner -
-
- {{/unless}} - - -
-

Pintura configuration

-
-
-
-
-
-
Enable Pintura
-
Enable Pintura for editing your images in Ghost
-
-
-
- -
-
-
- {{#unless this.config.pintura}} - {{#liquid-if this.settings.pintura class=""}} -
- -
-
-

Upload Pintura script

-

Upload the pintura-umd.js file from the Pintura package

-
-
- {{#if uploader.isUploading}} - {{uploader.progressBar}} - {{else}} - - {{/if}} - - {{#each uploader.errors as |error|}} -
{{or error.context error.message}}
- {{/each}} - -
- -
-
-
-
-
-
- -
-
-

Upload Pintura styles

-

Upload the pintura.css file from the Pintura package

-
-
- {{#if uploader.isUploading}} - {{uploader.progressBar}} - {{else}} - - {{/if}} - - {{#each uploader.errors as |error|}} -
{{or error.context error.message}}
- {{/each}} - -
- -
-
-
-
-
- {{/liquid-if}} - {{/unless}} -
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/integrations/slack-loading.hbs b/ghost/admin/app/templates/settings/integrations/slack-loading.hbs deleted file mode 100644 index f1fce17a88f..00000000000 --- a/ghost/admin/app/templates/settings/integrations/slack-loading.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Slack -
-
-
-
- -
- -
-
\ No newline at end of file diff --git a/ghost/admin/app/templates/settings/integrations/slack.hbs b/ghost/admin/app/templates/settings/integrations/slack.hbs deleted file mode 100644 index e12eb33fe86..00000000000 --- a/ghost/admin/app/templates/settings/integrations/slack.hbs +++ /dev/null @@ -1,96 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Slack -
-
-
- -
-
- -
-
-
-
- -
-
-

Slack

-

A messaging app for teams

-
-
-
- -
-

Slack configuration

-
-
-
- -
-
-
-
Webhook URL
-
Automatically send newly published posts to a channel in Slack or any Slack-compatible service like Discord or Mattermost.
-
- - - {{#if this.settings.errors}} - - {{else}} -

Set up a new incoming webhook here, and grab the URL.

- {{/if}} -
-
-
-
-
-
-
Username
-
The username to display messages from
-
- - - {{#if this.settings.errors}} - - {{/if}} - -
- -
-
-
-
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/integrations/unsplash-loading.hbs b/ghost/admin/app/templates/settings/integrations/unsplash-loading.hbs deleted file mode 100644 index ce1b6f38178..00000000000 --- a/ghost/admin/app/templates/settings/integrations/unsplash-loading.hbs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Unsplash -
-
-
-
- -
- -
-
\ No newline at end of file diff --git a/ghost/admin/app/templates/settings/integrations/unsplash.hbs b/ghost/admin/app/templates/settings/integrations/unsplash.hbs deleted file mode 100644 index d4c16e919a5..00000000000 --- a/ghost/admin/app/templates/settings/integrations/unsplash.hbs +++ /dev/null @@ -1,64 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Unsplash -
-
-
- -
-
- -
-
-
-
- Unsplash -
-
-

Unsplash

-

Beautiful, free photos

-
-
-
- -
-

Unsplash configuration

-
-
-
-
-
Enable Unsplash
-
Enable Unsplash image integration for your posts
-
-
-
-
- -
-
-
-
-
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/integrations/zapier.hbs b/ghost/admin/app/templates/settings/integrations/zapier.hbs deleted file mode 100644 index 55a7f3df193..00000000000 --- a/ghost/admin/app/templates/settings/integrations/zapier.hbs +++ /dev/null @@ -1,249 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} - - Integrations - - {{svg-jar "arrow-right-small"}} Zapier -
-
-
- -
- -
-
-
-
- -
-
-

Zapier

-

Automation for your favorite apps

- -
-
-
Admin API key
-
-
- - {{this.integration.adminKey.secret}} - -
- - -
-
- {{#if (eq this.regeneratedApiKey.type this.integration.adminKey.type)}} -
Admin API Key was successfully regenerated
- {{/if}} -
-
-
-
API URL
-
-
- - {{this.apiUrl}} - -
- -
-
-
-
-
-
-
-
-
- -
-

Zapier templates

-

Explore pre-built templates for common automation tasks

-
-
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Share new posts to Twitter

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Share scheduled posts with your team in Slack

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Connect Patreon to your Ghost membership site

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Protect email delivery with email verification

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Add members for successful sales in PayPal

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Unsubscribe members who cancel a subscription in PayPal

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Send new post drafts from Google Docs to Ghost

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Survey new members using Typeform

-
-
- -
-
-
-
-
-
-
- {{svg-jar "arrow-right" class="fill-midgrey w4 ml2"}} -
-
-
-

Sync email subscribers in Ghost + Mailchimp

-
-
- -
-
-
- - - -
-
-
-
-
diff --git a/ghost/admin/app/templates/settings/labs-loading.hbs b/ghost/admin/app/templates/settings/labs-loading.hbs deleted file mode 100644 index cb38236f14e..00000000000 --- a/ghost/admin/app/templates/settings/labs-loading.hbs +++ /dev/null @@ -1,19 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Labs -
-

- Labs -

-
-
- -
- -
-
diff --git a/ghost/admin/app/templates/settings/labs.hbs b/ghost/admin/app/templates/settings/labs.hbs deleted file mode 100644 index 46dbf086fc8..00000000000 --- a/ghost/admin/app/templates/settings/labs.hbs +++ /dev/null @@ -1,358 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Labs -
-

- Labs -

-
-
- -
-

{{svg-jar "idea"}}This is a testing ground for new or experimental features. They may change, break or inexplicably disappear at any time.

- -
-

Migration options

-
-
-
-
-

Import content

-

Import posts from a JSON or zip file

-
- - Open Importer - -
-
- -
-
-
-

Export your content

-

Download all of your posts and settings in a single, glorious JSON file

-
- -
-
- -
-
-
-

Delete all content

-

Permanently delete all posts and tags from the database, a hard reset

-
- -
-
-
-
- -
-

Beta features

-
- -
-
-
-

Substack migrator

-

A step-by-step tool to easily import all your content, members and paid subscriptions

-
- - Open - -
-
- -
-
-
-

Portal translation

-

- Translate your membership flows into your publication language (supported languages). Don’t see yours? Get involved -

-
-
- -
-
-
- -
- -
-
-

Redirects

-

Configure redirects for old or moved content, more info in the docs

-
-
- {{#if uploader.isUploading}} - {{uploader.progressBar}} - {{else}} - - - {{/if}} - - {{#each uploader.errors as |error|}} -
{{or error.context error.message}}
- {{/each}} - -
- -
-
-
-
-
- -
- -
-
-

Routes

-

Configure dynamic routing by modifying the routes.yaml file

-
-
- {{#if uploader.isUploading}} - {{uploader.progressBar}} - {{else}} - - - {{/if}} - - {{#each uploader.errors as |error|}} -
{{or error.context error.message}}
- {{/each}} - -
- -
-
-
-
-
-
-
- - - {{#if (enable-developer-experiments)}} -
-

Alpha Features

-
-
-
-
-

URL cache

-

- Enable URL Caching -

-
-
- -
-
-
- -
-
-
-

Lexical multiplayer

-

- Enables multiplayer editing in the lexical editor. -

-
-
- -
-
-
-
-
-
-

Webmentions

-

- Allows viewing received mentions on the dashboard. -

-
-
- -
-
-
-
-
-
-

Websockets

-

- Test out Websockets functionality at /ghost/#/websockets. -

-
-
- -
-
-
-
-
-
-

Stripe Automatic Tax

-

- Use Stripe Automatic Tax at Stripe Checkout. Needs to be enabled in Stripe -

-
-
- -
-
-
-
-
-
-

Email customization

-

- Adding more control over the newsletter template -

-
-
- -
-
-
- -
-
-
-

Collections

-

- Enables Collections 2.0 -

-
-
- -
-
-
- -
-
-
-

Collections Card

-

- Enables the Collections Card for pages - requires Collections and the beta Editor to be enabled -

-
-
- -
-
-
- -
-
-
-

Mail Events

-

- Enables processing of mail events -

-
-
- -
-
-
- -
-
-
-

Lexical indicators

-

- Show L/M indicator on posts list for easier debugging -

-
-
- -
-
-
- -
-
-
-

Import Member Tier

-

- Enables tier to be specified when importing members -

-
-
- -
-
-
- -
-
-
-

Tips & donations

-

- Enables publishers to collect one-time payments -

-
-
- -
-
-
-
-
- {{/if}} -
-
diff --git a/ghost/admin/app/templates/settings/labs/import.hbs b/ghost/admin/app/templates/settings/labs/import.hbs deleted file mode 100644 index 3a840859ebe..00000000000 --- a/ghost/admin/app/templates/settings/labs/import.hbs +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/ghost/admin/app/templates/settings/membership.hbs b/ghost/admin/app/templates/settings/membership.hbs deleted file mode 100644 index e46ff0cf799..00000000000 --- a/ghost/admin/app/templates/settings/membership.hbs +++ /dev/null @@ -1,246 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Membership -
-

- Membership -

-
-
- -
-
- -
- -
-
-

Fund your work with subscription revenue. Connect your Stripe account and offer premium content to your audience. Our creators are already making over $12 million per year, while Ghost takes 0% payment fees.

- -
-
-
-
-
-

Portal Settings

-

- Customize members modal signup flow -

-
- -
-
-
- -
- - - -
-
-
-
-
- {{#if (or (eq this.settings.membersSignupAccess 'none') this.switchFromNoneTask.isRunning)}} -
- {{svg-jar "portal-logo-stroke"}} -

Portal disabled

-

Change your Subscription Access setting to re-enable Portal

-
- {{else}} - - {{/if}} -
-
-
- -
-
-

Membership tiers

- {{#if this.session.user.isAdmin}} - {{#if (feature "tipsAndDonations")}} - {{#if this.membersUtils.isStripeEnabled}} - - {{else}} - - {{/if}} - {{else}} - - {{/if}} - {{/if}} -
-
-
-
-
-

Free

-

Free member sign up settings

-
- -
-
- {{#liquid-if this.freeOpen}} -
- - - - -

Redirect to this URL after signup for a free membership

-
-
- {{/liquid-if}} -
-
-
-
-
-

Premium

- {{#if (feature "tipsAndDonations")}} -

Set prices and paid member sign up settings. - {{#if this.membersUtils.isStripeEnabled}} - {{else}} - - {{/if}}

- {{else}} -

Set prices and paid member sign up settings

- {{/if}} -
- - {{#if (feature "tipsAndDonations")}} - {{#if this.membersUtils.isStripeEnabled}} - - {{else}} - - {{/if}} - {{else}} - {{#if this.membersUtils.isStripeEnabled}} - - {{else}} - - {{/if}} - {{/if}} -
- {{#if this.isConnectDisallowed}} -
-
- {{svg-jar "shield-lock"}} -

Your site is not secured

-

Paid memberships through Ghost can only be run on sites secured by SSL (HTTPS vs. HTTP). More information on adding a free SSL Certificate to your Ghost site can be found here.

-
-
- {{/if}} -
- {{#liquid-if this.paidOpen}} -
- {{#if this.fetchDefaultTier.isRunning}} - Loading... - {{else}} - - {{/if}} -
- {{/liquid-if}} -
-
-
-
-
- -
-

Growth

-
- - {{#if (feature "tipsAndDonations")}} - - {{/if}} -
-
- - {{#if this.showPortalSettings}} - - {{/if}} - - {{#if this.showStripeConnect}} - - {{/if}} - {{#if this.showTierModal}} - - {{/if}} -
diff --git a/ghost/admin/app/templates/settings/navigation.hbs b/ghost/admin/app/templates/settings/navigation.hbs deleted file mode 100644 index c3d0341565c..00000000000 --- a/ghost/admin/app/templates/settings/navigation.hbs +++ /dev/null @@ -1,81 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Navigation -
-

- Navigation -

-
-
- -
-
- -
-

Primary Navigation

-
-
-
- - {{#each this.settings.navigation as |navItem index|}} - - - - {{/each}} - - - -
-
- -

Secondary Navigation

-
-
-
- - {{#each this.settings.secondaryNavigation as |navItem index|}} - - - - {{/each}} - - - -
-
-
-
- -{{outlet}} - diff --git a/ghost/admin/app/templates/settings/newsletters.hbs b/ghost/admin/app/templates/settings/newsletters.hbs deleted file mode 100644 index 43dcc6836ed..00000000000 --- a/ghost/admin/app/templates/settings/newsletters.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
- -
-
- - Settings - - {{svg-jar "arrow-right-small"}} Email newsletter -
-

- Email newsletter -

-
-
- -
-
- -
-
- -
-
-
diff --git a/ghost/admin/app/templates/settings/staff/index.hbs b/ghost/admin/app/templates/settings/staff/index.hbs deleted file mode 100644 index fd080ed5814..00000000000 --- a/ghost/admin/app/templates/settings/staff/index.hbs +++ /dev/null @@ -1,156 +0,0 @@ -
- -
- {{#unless this.session.user.isEditor}} -
- - Settings - - {{svg-jar "arrow-right-small"}} Staff -
- {{/unless}} -

- Staff -

-
- - {{!-- Do not show Invite user button to authors --}} - {{#unless this.currentUser.isAuthorOrContributor}} -
- {{#if (gh-user-can-admin this.session.user)}} - - - - {{svg-jar "settings"}} - - - - -
  • - -
  • -
    -
    - {{/if}} - -
    - {{/unless}} -
    - - {{#if this.showInviteUserModal}} - - {{/if}} - - {{#if this.showResetAllPasswordsModal}} - - {{/if}} - -
    - {{!-- Show invited users to everyone except authors --}} - {{#unless this.currentUser.isAuthorOrContributor}} - {{#if this.invites}} -
    -

    Invited users

    -
    - - {{#each this.invites as |invite|}} - -
    -
    -
    - {{svg-jar "email"}}ic -
    -

    {{invite.email}}

    -

    - {{#if invite.pending}} - - Invitation not sent - please try again - - {{else}} - - Invitation sent: {{component.createdAt}}, - {{if component.isExpired "expired" "expires"}} {{component.expiresAt}} - - {{/if}} -

    -
    -
    -
    -
    - {{#if component.isSending}} - Sending Invite... - {{else}} - - Revoke - - - Resend - - - {{invite.role.name}} - {{/if}} -
    -
    -
    -
    -
    - {{/each}} - -
    -
    - {{/if}} - {{/unless}} - -
    -

    Active users

    -
    - {{!-- For authors/contributors, only show their own user --}} - {{#if this.currentUser.isAuthorOrContributor}} - - - - {{else}} - {{#vertical-collection this.activeUsers - key="id" - containerSelector=".gh-main" - estimateHeight=75 - as |user| - }} - - - - {{/vertical-collection}} - {{/if}} -
    -
    -
    - - {{!-- Don't show if we have no suspended users or logged in as an author --}} - {{#if (and this.suspendedUsers (not this.currentUser.isAuthorOrContributor))}} -
    - Suspended users -
    - {{#each this.suspendedUsers key="id" as |user|}} - - - - {{/each}} -
    -
    - {{/if}} -
    diff --git a/ghost/admin/app/templates/settings/staff/user-loading.hbs b/ghost/admin/app/templates/settings/staff/user-loading.hbs deleted file mode 100644 index 844fdb8c663..00000000000 --- a/ghost/admin/app/templates/settings/staff/user-loading.hbs +++ /dev/null @@ -1,37 +0,0 @@ -
    - - {{!-- Remove breadcrumbs for Authors and Contributors --}} - {{#if this.currentUser.isAuthorOrContributor}} -

    Your profile

    - {{else}} -
    -
    - {{#unless this.session.user.isEditor}} - - Settings - - {{svg-jar "arrow-right-small"}} - {{/unless}} - - Staff - - {{svg-jar "arrow-right-small"}} Profile -
    -

    - {{this.user.name}} - {{#if this.user.isSuspended}} - Suspended - {{/if}} -

    -
    - {{/if}} - -
    -
    Save
    -
    -
    - -
    - -
    -
    diff --git a/ghost/admin/app/templates/settings/staff/user.hbs b/ghost/admin/app/templates/settings/staff/user.hbs deleted file mode 100644 index c5953a19c90..00000000000 --- a/ghost/admin/app/templates/settings/staff/user.hbs +++ /dev/null @@ -1,501 +0,0 @@ -
    - - {{!-- Remove breadcrumbs for Authors and Contributors --}} - {{#if this.currentUser.isAuthorOrContributor}} -

    Your profile

    - {{else}} -
    -
    - {{#unless this.session.user.isEditor}} - - Settings - - {{svg-jar "arrow-right-small"}} - {{/unless}} - - Staff - - {{svg-jar "arrow-right-small"}} Profile -
    -

    - {{this.user.name}} - {{#if this.user.isSuspended}} - Suspended - {{/if}} -

    -
    - {{/if}} - -
    - {{#if (or this.userActionsAreVisible this.session.user.isAdmin)}} - - - - {{svg-jar "settings"}} - - - - - {{#if this.canMakeOwner}} -
  • - -
  • - {{/if}} - {{#if this.deleteUserActionIsVisible}} -
  • - -
  • - {{#if this.user.isActive}} -
  • - -
  • - {{/if}} - {{#if this.user.isSuspended}} -
  • - -
  • - {{/if}} - {{/if}} - -
  • - - View user activity - -
  • -
    -
    - {{/if}} - - -
    -
    - - {{#if this.user.isLocked}} -

    {{svg-jar "info"}}This user account is locked. To sign in, ask this user to perform a password reset on their account.

    - {{/if}} - - {{!--
    --}} -
    -
    - {{! user details form }} - - {{!-- If an administrator is viewing Owner's profile then hide inputs for change password --}} - {{#if this.canChangePassword}} - {{! change password form }} - {{/if}} - - {{#if this.isOwnProfile}} - - {{/if}} -
    -
    -
    diff --git a/ghost/admin/app/utils/currency.js b/ghost/admin/app/utils/currency.js index dec3badfb0a..225476d4285 100644 --- a/ghost/admin/app/utils/currency.js +++ b/ghost/admin/app/utils/currency.js @@ -168,7 +168,7 @@ export function getCurrencyOptions() { * based on Stripe's requirements. Values here are double the Stripe limits, to take conversions to the settlement currency into account. * @see https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts * @param {String} currency — Currency in the 3-letter ISO format (e.g. "USD", "EUR") -* @retuns {Number} — Minimum amount +* @returns {Number} — Minimum amount */ export function minimumAmountForCurrency(currency) { const isoCurrency = currency?.toUpperCase(); diff --git a/ghost/admin/app/validators/post.js b/ghost/admin/app/validators/post.js index 73aa67505b9..f6ab868a998 100644 --- a/ghost/admin/app/validators/post.js +++ b/ghost/admin/app/validators/post.js @@ -183,7 +183,7 @@ export default BaseValidator.create({ // don't validate the date if the time format is incorrect if (isEmpty(model.errors.errorsFor('publishedAtBlogTime'))) { - let status = model.statusScratch || model.status; + let status = model.status; let now = moment(); let publishedAtBlogTZ = model.publishedAtBlogTZ; let isInFuture = publishedAtBlogTZ.isSameOrAfter(now); diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js index dcbab764464..4455adba64d 100644 --- a/ghost/admin/ember-cli-build.js +++ b/ghost/admin/ember-cli-build.js @@ -121,7 +121,8 @@ module.exports = function (defaults) { 'woff2', 'mp4', 'ico' - ] + ], + exclude: ['**/chunk*.map'] }, minifyJS: { options: { @@ -205,6 +206,7 @@ module.exports = function (defaults) { autoImport: { publicAssetURL, webpack: { + devtool: 'source-map', resolve: { fallback: { util: require.resolve('util'), diff --git a/ghost/admin/lib/asset-delivery/index.js b/ghost/admin/lib/asset-delivery/index.js index 09f34229253..aca83ba7e43 100644 --- a/ghost/admin/lib/asset-delivery/index.js +++ b/ghost/admin/lib/asset-delivery/index.js @@ -63,11 +63,21 @@ module.exports = { // copy the index.html file fs.copySync(`${results.directory}/index.html`, `${assetsOut}/index.html`, {overwrite: true, dereference: true}); - // copy all the `/assets` files, except the `icons` folder + // get all the `/assets` files, except the `icons` folder const assets = walkSync(results.directory + '/assets', { ignore: ['icons'] }); + // loop over any sourcemaps and remove `assets/` key from each one + assets.filter((file) => file.endsWith('.map')).forEach((file) => { + const mapFilePath = `${results.directory}/assets/${file}`; + const mapFile = JSON.parse(fs.readFileSync(mapFilePath, 'utf8')); + // loop over the sources and remove `assets/` from each one + mapFile.sources = mapFile.sources.map((source) => source.replace('assets/', '')); + fs.writeFileSync(mapFilePath, JSON.stringify(mapFile)); + }); + + // copy the assets to assetsOut assets.forEach(function (relativePath) { if (relativePath.slice(-1) === '/') { return; } diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 3bc7b4c3c84..acf4dd1a5b5 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.70.2", + "version": "5.73.1", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", @@ -39,19 +39,19 @@ "@embroider/macros": "1.13.2", "@glimmer/component": "1.1.2", "@html-next/vertical-collection": "3.0.0", - "@sentry/ember": "7.70.0", - "@tryghost/color-utils": "0.1.24", + "@sentry/ember": "7.78.0", + "@tryghost/color-utils": "0.2.0", "@tryghost/ember-promise-modals": "2.0.1", - "@tryghost/helpers": "1.1.77", - "@tryghost/kg-clean-basic-html": "3.0.39", - "@tryghost/kg-converters": "0.0.21", - "@tryghost/koenig-lexical": "0.5.12", - "@tryghost/limit-service": "1.2.10", + "@tryghost/helpers": "1.1.88", + "@tryghost/kg-clean-basic-html": "3.0.41", + "@tryghost/kg-converters": "0.0.22", + "@tryghost/koenig-lexical": "0.5.17", + "@tryghost/limit-service": "1.2.12", "@tryghost/members-csv": "0.0.0", "@tryghost/nql": "0.11.0", "@tryghost/nql-lang": "0.5.0", - "@tryghost/string": "0.2.4", - "@tryghost/timezone-data": "0.3.8", + "@tryghost/string": "0.2.10", + "@tryghost/timezone-data": "0.4.1", "autoprefixer": "9.8.6", "babel-eslint": "10.1.0", "babel-plugin-transform-class-properties": "6.24.1", @@ -60,7 +60,7 @@ "broccoli-concat": "4.2.5", "broccoli-funnel": "3.0.8", "broccoli-merge-trees": "4.2.0", - "broccoli-terser-sourcemap": "4.1.0", + "broccoli-terser-sourcemap": "4.1.1", "chai": "4.3.8", "chai-dom": "1.11.0", "codemirror": "5.48.2", @@ -76,7 +76,7 @@ "ember-cli-chart": "3.7.2", "ember-cli-code-coverage": "1.0.3", "ember-cli-dependency-checker": "3.3.2", - "ember-cli-deprecation-workflow": "2.1.0", + "ember-cli-deprecation-workflow": "2.2.0", "ember-cli-htmlbars": "6.3.0", "ember-cli-inject-live-reload": "2.1.0", "ember-cli-mirage": "2.4.0", @@ -84,7 +84,7 @@ "ember-cli-postcss": "6.0.1", "ember-cli-shims": "1.2.0", "ember-cli-string-helpers": "6.1.0", - "ember-cli-terser": "4.0.2", + "ember-cli-terser": "4.0.1", "ember-cli-test-loader": "3.1.0", "ember-composable-helpers": "5.0.0", "ember-concurrency": "2.3.7", @@ -150,8 +150,8 @@ "ember-addon": { "paths": [ "lib/asset-delivery", - "lib/ember-power-calendar-utils", - "lib/ember-power-calendar-moment" + "lib/ember-power-calendar-moment", + "lib/ember-power-calendar-utils" ] }, "resolutions": { @@ -199,4 +199,4 @@ } } } -} +} \ No newline at end of file diff --git a/ghost/admin/public/assets/icons/arrow-top-right.svg b/ghost/admin/public/assets/icons/arrow-top-right.svg index b58a17c0b13..6d725636ff5 100644 --- a/ghost/admin/public/assets/icons/arrow-top-right.svg +++ b/ghost/admin/public/assets/icons/arrow-top-right.svg @@ -1,3 +1,4 @@ + arrow-top-right \ No newline at end of file diff --git a/ghost/admin/tests/acceptance/editor-test.js b/ghost/admin/tests/acceptance/editor-test.js index 9d596fd2181..7cdd12a5df5 100644 --- a/ghost/admin/tests/acceptance/editor-test.js +++ b/ghost/admin/tests/acceptance/editor-test.js @@ -235,7 +235,7 @@ describe('Acceptance: Editor', function () { }); it('shows author token input and allows changing of authors in PSM', async function () { - let adminRole = this.server.create('role', {name: 'Adminstrator'}); + let adminRole = this.server.create('role', {name: 'Administrator'}); let authorRole = this.server.create('role', {name: 'Author'}); let user1 = this.server.create('user', {name: 'Primary', roles: [adminRole]}); this.server.create('user', {name: 'Waldo', roles: [authorRole]}); diff --git a/ghost/admin/tests/acceptance/settings/amp-test.js b/ghost/admin/tests/acceptance/settings/amp-test.js deleted file mode 100644 index 46e5ec8ad24..00000000000 --- a/ghost/admin/tests/acceptance/settings/amp-test.js +++ /dev/null @@ -1,130 +0,0 @@ -// import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import { -// beforeEach, -// describe, -// it -// } from 'mocha'; -// import {click, currentURL, find, findAll, triggerEvent} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Integrations - AMP', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations/amp'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/amp'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/amp'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/amp'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it enables or disables AMP properly and saves it', async function () { -// await visit('/settings/integrations/amp'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp'); - -// // AMP is disabled by default -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.false; - -// await click('[data-test-amp-checkbox]'); - -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.true; - -// await click('[data-test-save-button]'); - -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(lastRequest.requestBody); - -// expect(params.settings.findBy('key', 'amp').value).to.equal(true); - -// // CMD-S shortcut works -// await click('[data-test-amp-checkbox]'); -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// // we've already saved in this test so there's no on-screen indication -// // that we've had another save, check the request was fired instead -// let [newRequest] = this.server.pretender.handledRequests.slice(-1); -// params = JSON.parse(newRequest.requestBody); - -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.false; -// expect(params.settings.findBy('key', 'amp').value).to.equal(false); -// }); - -// it('warns when leaving without saving', async function () { -// await visit('/settings/integrations/amp'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp'); - -// // AMP is disabled by default -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox default').to.be.false; - -// await click('[data-test-amp-checkbox]'); - -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox after click').to.be.true; - -// await visit('/settings/staff'); - -// expect(findAll('[data-test-modal="unsaved-settings"]').length, 'unsaved changes modal exists').to.equal(1); - -// // Leave without saving -// await click('[data-test-leave-button]'); - -// expect(currentURL(), 'currentURL after leave without saving').to.equal('/settings/staff'); - -// await visit('/settings/integrations/amp'); - -// expect(currentURL(), 'currentURL after return').to.equal('/settings/integrations/amp'); - -// // settings were not saved -// expect(find('[data-test-amp-checkbox]').checked, 'AMP checkbox').to.be.false; -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/analytics-test.js b/ghost/admin/tests/acceptance/settings/analytics-test.js deleted file mode 100644 index e19e7dbcd80..00000000000 --- a/ghost/admin/tests/acceptance/settings/analytics-test.js +++ /dev/null @@ -1,80 +0,0 @@ -// import {authenticateSession} from 'ember-simple-auth/test-support'; -// import {click, find} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Analytics', function () { -// const hooks = setupApplicationTest(); -// setupMirage(hooks); - -// beforeEach(async function () { -// this.server.loadFixtures('configs', 'newsletters'); - -// const role = this.server.create('role', {name: 'Owner'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('can manage open rate tracking', async function () { -// this.server.db.settings.update({key: 'email_track_opens'}, {value: 'true'}); - -// await visit('/settings/analytics'); - -// expect(find('[data-test-checkbox="email-track-opens"]')).to.be.checked; - -// await click('[data-test-label="email-track-opens"]'); -// expect(find('[data-test-checkbox="email-track-opens"]')).to.not.be.checked; - -// await click('[data-test-button="save-analytics-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'email_track_opens'}).value).to.equal(false); -// }); - -// it('can manage click tracking', async function () { -// this.server.db.settings.update({key: 'email_track_clicks'}, {value: 'true'}); - -// await visit('/settings/analytics'); - -// expect(find('[data-test-checkbox="email-track-clicks"]')).to.be.checked; - -// await click('[data-test-label="email-track-clicks"]'); -// expect(find('[data-test-checkbox="email-track-clicks"]')).to.not.be.checked; - -// await click('[data-test-button="save-analytics-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'email_track_clicks'}).value).to.equal(false); -// }); - -// it('can manage source tracking', async function () { -// this.server.db.settings.update({key: 'members_track_sources'}, {value: 'true'}); - -// await visit('/settings/analytics'); - -// expect(find('[data-test-checkbox="members-track-sources"]')).to.be.checked; - -// await click('[data-test-label="members-track-sources"]'); -// expect(find('[data-test-checkbox="members-track-sources"]')).to.not.be.checked; - -// await click('[data-test-button="save-analytics-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'members_track_sources'}).value).to.equal(false); -// }); - -// it('can manage outbound link tagging', async function () { -// this.server.db.settings.update({key: 'outbound_link_tagging'}, {value: 'true'}); - -// await visit('/settings/analytics'); - -// expect(find('[data-test-checkbox="outbound-link-tagging"]')).to.be.checked; - -// await click('[data-test-label="outbound-link-tagging"]'); -// expect(find('[data-test-checkbox="outbound-link-tagging"]')).to.not.be.checked; - -// await click('[data-test-button="save-analytics-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'outbound_link_tagging'}).value).to.equal(false); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/code-injection-test.js b/ghost/admin/tests/acceptance/settings/code-injection-test.js deleted file mode 100644 index 05d7d0db8a1..00000000000 --- a/ghost/admin/tests/acceptance/settings/code-injection-test.js +++ /dev/null @@ -1,113 +0,0 @@ -// import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import { -// beforeEach, -// describe, -// it -// } from 'mocha'; -// import {click, currentURL, fillIn, find, findAll, triggerEvent} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Code-Injection', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/code-injection'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/code-injection'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to staff page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/code-injection'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/code-injection'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it renders, loads and saves editors correctly', async function () { -// await visit('/settings/code-injection'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/code-injection'); - -// // has correct page title -// expect(document.title, 'page title').to.equal('Settings - Code injection - Test Blog'); - -// expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save'); - -// expect(findAll('#ghost-head .CodeMirror').length, 'ghost head codemirror element').to.equal(1); -// expect(find('#ghost-head .CodeMirror'), 'ghost head editor theme').to.have.class('cm-s-xq-light'); - -// expect(findAll('#ghost-foot .CodeMirror').length, 'ghost head codemirror element').to.equal(1); -// expect(find('#ghost-foot .CodeMirror'), 'ghost head editor theme').to.have.class('cm-s-xq-light'); - -// await fillIn('#settings-code #ghost-head textarea', 'Test'); - -// await click('[data-test-save-button]'); - -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(lastRequest.requestBody); - -// expect(params.settings.findBy('key', 'codeinjection_head').value).to.equal('Test'); -// // update should have been partial -// expect(params.settings.findBy('key', 'navigation')).to.be.undefined; -// expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save'); - -// await fillIn('#settings-code #ghost-head textarea', ''); - -// // CMD-S shortcut works -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// let [newRequest] = this.server.pretender.handledRequests.slice(-1); -// params = JSON.parse(newRequest.requestBody); - -// expect(params.settings.findBy('key', 'codeinjection_head').value).to.equal(''); -// expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save'); - -// // Saving when no changed have been made should work -// // (although no api request is expected) -// await click('[data-test-save-button]'); -// expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save'); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/design-test.js b/ghost/admin/tests/acceptance/settings/design-test.js deleted file mode 100644 index 1cb5ee351ae..00000000000 --- a/ghost/admin/tests/acceptance/settings/design-test.js +++ /dev/null @@ -1,159 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {fileUpload} from '../../helpers/file-upload'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Design', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// this.server.loadFixtures('themes'); - -// return await authenticateSession(); -// }); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('renders with no custom theme settings', async function () { -// await visit('/settings'); -// await click('[data-test-nav="design"]'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/design'); -// expect(document.title, 'page title').to.equal('Settings - Design - Test Blog'); - -// // side nav menu changes -// expect(find('[data-test-nav-menu="design"]'), 'design menu').to.exist; -// expect(find('[data-test-nav-menu="main"]'), 'main menu').to.not.exist; - -// // side nav defaults to general group open -// expect(find('[data-test-nav-toggle="general"]'), 'general toggle').to.exist; -// expect(find('[data-test-nav-group="general"]'), 'general form').to.exist; - -// // no other side nav groups exist -// expect(findAll('[data-test-nav-toggle]'), 'no of group toggles').to.have.lengthOf(1); -// expect(findAll('[data-test-nav-group]'), 'no of groups open').to.have.lengthOf(1); - -// // current theme is shown in nav menu -// expect(find('[data-test-text="current-theme"]')).to.contain.text('source - v1.0'); - -// // defaults to "home" desktop preview -// expect(find('[data-test-button="desktop-preview"]')).to.have.class('gh-btn-group-selected'); -// expect(find('[data-test-button="mobile-preview"]')).to.not.have.class('gh-btn-group-selected'); -// }); - -// it('has unsaved-changes confirmation', async function () { -// await visit('/settings/design'); -// await fillIn('[data-test-input="siteDescription"]', 'Changed'); -// await click('[data-test-link="back-to-settings"]'); - -// expect(find('[data-test-modal="unsaved-settings"]')).to.exist; - -// await click('[data-test-modal="unsaved-settings"] [data-test-button="close"]'); - -// expect(currentURL()).to.equal('/settings/design'); - -// await click('[data-test-link="back-to-settings"]'); -// await click('[data-test-modal="unsaved-settings"] [data-test-leave-button]'); - -// expect(currentURL()).to.equal('/settings'); - -// await click('[data-test-nav="design"]'); - -// expect(find('[data-test-input="siteDescription"]')).to.not.have.value('Changed'); -// }); - -// it('renders with custom theme settings'); - -// it('can install an official theme', async function () { -// await visit('/settings/design'); -// await click('[data-test-nav="change-theme"]'); -// expect(currentURL(), 'currentURL').to.equal('/settings/design/change-theme'); - -// await click('[data-test-theme-link="Journal"]'); -// expect(currentURL(), 'currentURL').to.equal('/settings/design/change-theme/Journal'); - -// await click('[data-test-button="install-theme"]'); -// expect(find('[data-test-modal="install-theme"]'), 'install-theme modal').to.exist; -// expect(find('[data-test-state="confirm"]'), 'confirm state').to.exist; -// expect(findAll('[data-test-state]').length, 'state count').to.equal(1); - -// await click('[data-test-button="confirm-install"]'); -// expect(find('[data-test-state="installed-no-notes"]'), 'success state').to.exist; -// expect(findAll('[data-test-state]').length, 'state count').to.equal(1); - -// // navigates back to design screen in background -// expect(currentURL(), 'currentURL').to.equal('/settings/design'); - -// await click('[data-test-button="cancel"]'); -// expect(find('[data-test-modal="install-theme"]')).to.not.exist; - -// // nav menu shows current theme -// expect(find('[data-test-text="current-theme"]')).to.contain.text('Journal - v0.1'); -// }); - -// it('can upload custom theme', async function () { -// this.server.post('/themes/upload/', function ({themes}) { -// const theme = themes.create({ -// name: 'custom', -// package: { -// name: 'Custom', -// version: '1.0' -// } -// }); - -// return {themes: [theme]}; -// }); - -// await visit('/settings/design/change-theme'); -// await click('[data-test-button="upload-theme"]'); - -// expect(find('[data-test-modal="upload-theme"]'), 'upload-theme modal').to.exist; - -// await fileUpload('[data-test-modal="upload-theme"] input[type="file"]', ['test'], {name: 'valid-theme.zip', type: 'application/zip'}); - -// expect(find('[data-test-state="installed-no-notes"]'), 'success state').to.exist; -// expect(currentURL(), 'url after upload').to.equal('/settings/design/change-theme'); - -// await click('[data-test-button="activate"]'); - -// expect(currentURL(), 'url after activate').to.equal('/settings/design'); -// expect(find('[data-test-modal="install-theme"]')).to.not.exist; -// expect(find('[data-test-text="current-theme"]')).to.contain.text('custom - v1.0'); -// }); - -// it('can change between installed themes'); -// it('can delete installed theme'); - -// describe('limits', function () { -// it('displays upgrade notice when custom themes are not allowed', async function () { -// this.server.loadFixtures('configs'); -// const config = this.server.db.configs.find(1); -// config.hostSettings = { -// limits: { -// customThemes: { -// allowlist: ['source', 'casper', 'dawn', 'lyra'], -// error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.' -// } -// } -// }; -// this.server.db.configs.update(1, config); - -// await visit('/settings/design/change-theme'); -// await click('[data-test-button="upload-theme"]'); - -// expect(find('[data-test-modal="limits/custom-theme"]'), 'limits/custom-theme modal').to.exist; -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/general-test.js b/ghost/admin/tests/acceptance/settings/general-test.js deleted file mode 100644 index fa900fdc68e..00000000000 --- a/ghost/admin/tests/acceptance/settings/general-test.js +++ /dev/null @@ -1,291 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {beforeEach, describe, it} from 'mocha'; -// import {blur, click, currentURL, fillIn, find, findAll, focus, triggerEvent} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {keyDown} from 'ember-keyboard/test-support/test-helpers'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - General', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it renders, handles image uploads', async function () { -// await visit('/settings/general'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/general'); - -// // has correct page title -// expect(document.title, 'page title').to.equal('Settings - General - Test Blog'); - -// // highlights nav menu -// expect(find('[data-test-nav="settings"]'), 'highlights nav menu item') -// .to.have.class('active'); - -// expect( -// find('[data-test-button="save"]').textContent.trim(), -// 'save button text' -// ).to.equal('Save'); - -// await click('[data-test-toggle-pub-info]'); -// await fillIn('[data-test-title-input]', 'New Blog Title'); -// await click('[data-test-button="save"]'); -// expect(document.title, 'page title').to.equal('Settings - General - New Blog Title'); - -// // CMD-S shortcut works -// // -------------------------------------------------------------- // -// await fillIn('[data-test-title-input]', 'CMD-S Test'); -// await keyDown('cmd+s'); -// // we've already saved in this test so there's no on-screen indication -// // that we've had another save, check the request was fired instead -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(lastRequest.requestBody); -// expect(params.settings.findBy('key', 'title').value).to.equal('CMD-S Test'); -// }); - -// it('renders timezone selector correctly', async function () { -// await visit('/settings/general'); -// await click('[data-test-toggle-timezone]'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/general'); - -// expect(findAll('#timezone option').length, 'available timezones').to.equal(66); -// expect(find('#timezone option:checked').textContent.trim()).to.equal('(GMT) UTC'); -// find('#timezone option[value="Africa/Cairo"]').selected = true; - -// await triggerEvent('#timezone', 'change'); -// await click('[data-test-button="save"]'); -// expect(find('#timezone option:checked').textContent.trim()).to.equal('(GMT +2:00) Cairo, Egypt'); -// }); - -// it('handles private blog settings correctly', async function () { -// await visit('/settings/general'); - -// // handles private blog settings correctly -// expect(find('[data-test-private-checkbox]').checked, 'isPrivate checkbox').to.be.false; - -// await click('[data-test-private-checkbox]'); - -// expect(find('[data-test-private-checkbox]').checked, 'isPrivate checkbox').to.be.true; -// expect(findAll('[data-test-password-input]').length, 'password input').to.equal(1); -// expect(find('[data-test-password-input]').value, 'password default value').to.not.equal(''); - -// await fillIn('[data-test-password-input]', ''); -// await blur('[data-test-password-input]'); - -// expect(find('[data-test-password-error]').textContent.trim(), 'empty password error') -// .to.equal('Password must be supplied'); - -// await fillIn('[data-test-password-input]', 'asdfg'); -// await blur('[data-test-password-input]'); - -// expect(find('[data-test-password-error]').textContent.trim(), 'present password error') -// .to.equal(''); -// }); - -// it('handles social blog settings correctly', async function () { -// let testSocialInput = async function (type, input, expectedValue, expectedError = '') { -// await fillIn(`[data-test-${type}-input]`, input); -// await blur(`[data-test-${type}-input]`); - -// expect( -// find(`[data-test-${type}-input]`).value, -// `${type} value for ${input}` -// ).to.equal(expectedValue); - -// expect( -// find(`[data-test-${type}-error]`).textContent.trim(), -// `${type} validation response for ${input}` -// ).to.equal(expectedError); - -// expect( -// find(`[data-test-${type}-input]`).closest('.form-group').classList.contains('error'), -// `${type} input should be in error state with '${input}'` -// ).to.equal(!!expectedError); -// }; - -// let testFacebookValidation = async (...args) => testSocialInput('facebook', ...args); -// let testTwitterValidation = async (...args) => testSocialInput('twitter', ...args); - -// await visit('/settings/general'); -// await click('[data-test-toggle-social]'); - -// // validates a facebook url correctly -// // loads fixtures and performs transform -// expect(find('[data-test-facebook-input]').value, 'initial facebook value') -// .to.equal('https://www.facebook.com/test'); - -// await focus('[data-test-facebook-input]'); -// await blur('[data-test-facebook-input]'); - -// // regression test: we still have a value after the input is -// // focused and then blurred without any changes -// expect(find('[data-test-facebook-input]').value, 'facebook value after blur with no change') -// .to.equal('https://www.facebook.com/test'); - -// await testFacebookValidation( -// 'facebook.com/username', -// 'https://www.facebook.com/username'); - -// await testFacebookValidation( -// 'testuser', -// 'https://www.facebook.com/testuser'); - -// await testFacebookValidation( -// 'ab99', -// 'https://www.facebook.com/ab99'); - -// await testFacebookValidation( -// 'page/ab99', -// 'https://www.facebook.com/page/ab99'); - -// await testFacebookValidation( -// 'page/*(&*(%%))', -// 'https://www.facebook.com/page/*(&*(%%))'); - -// await testFacebookValidation( -// 'facebook.com/pages/some-facebook-page/857469375913?ref=ts', -// 'https://www.facebook.com/pages/some-facebook-page/857469375913?ref=ts'); - -// await testFacebookValidation( -// 'https://www.facebook.com/groups/savethecrowninn', -// 'https://www.facebook.com/groups/savethecrowninn'); - -// await testFacebookValidation( -// 'http://github.com/username', -// 'http://github.com/username', -// 'The URL must be in a format like https://www.facebook.com/yourPage'); - -// await testFacebookValidation( -// 'http://github.com/pages/username', -// 'http://github.com/pages/username', -// 'The URL must be in a format like https://www.facebook.com/yourPage'); - -// // validates a twitter url correctly - -// // loads fixtures and performs transform -// expect(find('[data-test-twitter-input]').value, 'initial twitter value') -// .to.equal('https://twitter.com/test'); - -// await focus('[data-test-twitter-input]'); -// await blur('[data-test-twitter-input]'); - -// // regression test: we still have a value after the input is -// // focused and then blurred without any changes -// expect(find('[data-test-twitter-input]').value, 'twitter value after blur with no change') -// .to.equal('https://twitter.com/test'); - -// await testTwitterValidation( -// 'twitter.com/username', -// 'https://twitter.com/username'); - -// await testTwitterValidation( -// 'testuser', -// 'https://twitter.com/testuser'); - -// await testTwitterValidation( -// 'http://github.com/username', -// 'https://twitter.com/username'); - -// await testTwitterValidation( -// '*(&*(%%))', -// '*(&*(%%))', -// 'The URL must be in a format like https://twitter.com/yourUsername'); - -// await testTwitterValidation( -// 'thisusernamehasmorethan15characters', -// 'thisusernamehasmorethan15characters', -// 'Your Username is not a valid Twitter Username'); -// }); - -// it('warns when leaving without saving', async function () { -// await visit('/settings/general'); - -// expect( -// find('[data-test-private-checkbox]').checked, -// 'private blog checkbox' -// ).to.be.false; - -// await click('[data-test-toggle-pub-info]'); -// await fillIn('[data-test-title-input]', 'New Blog Title'); - -// await click('[data-test-private-checkbox]'); - -// expect( -// find('[data-test-private-checkbox]').checked, -// 'private blog checkbox' -// ).to.be.true; - -// await visit('/settings/staff'); - -// expect(findAll('[data-test-modal="unsaved-settings"]').length, 'modal exists').to.equal(1); - -// // Leave without saving -// await click('[data-test-leave-button]'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/staff'); - -// await visit('/settings/general'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/general'); - -// // settings were not saved -// expect( -// find('[data-test-private-checkbox]').checked, -// 'private blog checkbox' -// ).to.be.false; - -// expect( -// find('[data-test-title-input]').textContent.trim(), -// 'Blog title' -// ).to.equal(''); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/integrations-test.js b/ghost/admin/tests/acceptance/settings/integrations-test.js deleted file mode 100644 index 8943709f3d6..00000000000 --- a/ghost/admin/tests/acceptance/settings/integrations-test.js +++ /dev/null @@ -1,512 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {beforeEach, describe, it} from 'mocha'; -// import {blur, click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Integrations - Custom', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// describe('access permissions', function () { -// beforeEach(function () { -// this.server.create('integration', {name: 'Test'}); -// }); - -// it('redirects /integrations/ to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects /integrations/ to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects /integrations/ to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects /integrations/ to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/1'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects /integrations/:id/ to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations/1'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects /integrations/:id/ to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/1'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects /integrations/:id/ to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/1'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects /integrations/:id/ to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/1'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); -// }); - -// describe('navigation', function () { -// beforeEach(async function () { -// this.server.loadFixtures('settings'); - -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('renders defaults correctly', async function () { -// await visit('/settings/integrations'); - -// // slack is not configured in the fixtures -// expect( -// find('[data-test-app="slack"] [data-test-app-status]').textContent.trim(), -// 'slack app status' -// ).to.equal('Configure'); - -// // amp is disabled in the fixtures -// expect( -// find('[data-test-app="amp"] [data-test-app-status]').textContent.trim(), -// 'amp app status' -// ).to.equal('Configure'); -// }); - -// it('renders AMP active state', async function () { -// this.server.db.settings.update({key: 'amp', value: true}); -// await visit('/settings/integrations'); - -// // amp switches to active when enabled -// expect( -// find('[data-test-app="amp"] [data-test-app-status]').textContent.trim(), -// 'amp app status' -// ).to.equal('Active'); -// }); - -// it('it redirects to Slack when clicking on the grid', async function () { -// await visit('/settings/integrations'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations'); - -// await click('[data-test-link="slack"]'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/slack'); -// }); - -// it('it redirects to AMP when clicking on the grid', async function () { -// await visit('/settings/integrations'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations'); - -// await click('[data-test-link="amp"]'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/amp'); -// }); - -// it('it redirects to Unsplash when clicking on the grid', async function () { -// await visit('/settings/integrations'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations'); - -// await click('[data-test-link="unsplash"]'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash'); -// }); -// }); - -// describe('custom integrations', function () { -// beforeEach(async function () { -// this.server.loadFixtures('configs'); -// let config = this.server.schema.configs.first(); -// config.update({ -// enableDeveloperExperiments: true -// }); - -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('handles 404', async function () { -// await visit('/settings/integrations/1'); -// expect(currentRouteName()).to.equal('error404'); -// }); - -// it('can add new integration', async function () { -// // sanity check -// expect( -// this.server.db.integrations.length, -// 'number of integrations in db at start' -// ).to.equal(0); -// expect( -// this.server.db.apiKeys.length, -// 'number of apiKeys in db at start' -// ).to.equal(0); - -// // blank slate -// await visit('/settings/integrations'); - -// expect( -// find('[data-test-blank="custom-integrations"]'), -// 'initial blank slate' -// ).to.exist; - -// // new integration modal opens/closes -// await click('[data-test-button="new-integration"]'); - -// expect(currentURL(), 'url after clicking new').to.equal('/settings/integrations/new'); -// expect(find('[data-test-modal="new-integration"]'), 'modal after clicking new').to.exist; - -// await click('[data-test-button="cancel-new-integration"]'); - -// expect(find('[data-test-modal="new-integration"]'), 'modal after clicking cancel') -// .to.not.exist; - -// expect( -// find('[data-test-blank="custom-integrations"]'), -// 'blank slate after cancelled creation' -// ).to.exist; - -// // new integration validations -// await click('[data-test-button="new-integration"]'); -// await click('[data-test-button="create-integration"]'); - -// expect( -// find('[data-test-error="new-integration-name"]').textContent, -// 'name error after create with blank field' -// ).to.have.string('enter a name'); - -// await fillIn('[data-test-input="new-integration-name"]', 'Duplicate'); -// await click('[data-test-button="create-integration"]'); - -// expect( -// find('[data-test-error="new-integration-name"]').textContent, -// 'name error after create with duplicate name' -// ).to.have.string('already been used'); - -// // successful creation -// await fillIn('[data-test-input="new-integration-name"]', 'Test'); - -// expect( -// find('[data-test-error="new-integration-name"]').textContent.trim(), -// 'name error after typing in field' -// ).to.be.empty; - -// await click('[data-test-button="create-integration"]'); - -// expect( -// find('[data-test-modal="new-integration"]'), -// 'modal after successful create' -// ).to.not.exist; - -// expect( -// this.server.db.integrations.length, -// 'number of integrations in db after create' -// ).to.equal(1); -// // mirage sanity check -// expect( -// this.server.db.apiKeys.length, -// 'number of api keys in db after create' -// ).to.equal(2); - -// expect( -// currentURL(), -// 'url after integration creation' -// ).to.equal('/settings/integrations/1'); - -// // test navigation back to list then back to new integration -// await click('[data-test-link="integrations-back"]'); - -// expect( -// currentURL(), -// 'url after clicking "Back"' -// ).to.equal('/settings/integrations'); - -// expect( -// find('[data-test-blank="custom-integrations"]'), -// 'blank slate after creation' -// ).to.not.exist; - -// expect( -// findAll('[data-test-custom-integration]').length, -// 'number of custom integrations after creation' -// ).to.equal(1); - -// await click(`[data-test-integration="1"]`); - -// expect( -// currentURL(), -// 'url after clicking integration in list' -// ).to.equal('/settings/integrations/1'); -// }); - -// it('can manage an integration', async function () { -// this.server.create('integration'); - -// await visit('/settings/integrations/1'); - -// expect( -// currentURL(), -// 'initial URL' -// ).to.equal('/settings/integrations/1'); - -// expect( -// find('[data-test-screen-title]').textContent, -// 'screen title' -// ).to.have.string('Integration 1'); - -// // fields have expected values -// // TODO: add test for logo - -// expect( -// find('[data-test-input="name"]').value, -// 'initial name value' -// ).to.equal('Integration 1'); - -// expect( -// find('[data-test-input="description"]').value, -// 'initial description value' -// ).to.equal(''); - -// expect( -// find('[data-test-text="content-key"]'), -// 'content key text' -// ).to.have.trimmed.text('integration-1_content_key-12345'); - -// expect( -// find('[data-test-text="admin-key"]'), -// 'admin key text' -// ).to.have.trimmed.text('integration-1_admin_key-12345'); - -// expect( -// find('[data-test-text="api-url"]'), -// 'api url text' -// ).to.have.trimmed.text(window.location.origin); - -// // it can modify integration fields and has validation - -// expect( -// find('[data-test-error="name"]').textContent.trim(), -// 'initial name error' -// ).to.be.empty; - -// await fillIn('[data-test-input="name"]', ''); -// await await blur('[data-test-input="name"]'); - -// expect( -// find('[data-test-error="name"]').textContent, -// 'name validation for blank string' -// ).to.have.string('enter a name'); - -// await click('[data-test-button="save"]'); - -// expect( -// this.server.schema.integrations.first().name, -// 'db integration name after failed save' -// ).to.equal('Integration 1'); - -// await fillIn('[data-test-input="name"]', 'Test Integration'); -// await await blur('[data-test-input="name"]'); - -// expect( -// find('[data-test-error="name"]').textContent.trim(), -// 'name error after valid entry' -// ).to.be.empty; - -// await fillIn('[data-test-input="description"]', 'Description for Test Integration'); -// await await blur('[data-test-input="description"]'); -// await click('[data-test-button="save"]'); - -// // changes are reflected in the integrations list - -// await click('[data-test-link="integrations-back"]'); - -// expect( -// currentURL(), -// 'url after saving and clicking "back"' -// ).to.equal('/settings/integrations'); - -// expect( -// find('[data-test-integration="1"] [data-test-text="name"]').textContent.trim(), -// 'integration name after save' -// ).to.equal('Test Integration'); - -// expect( -// find('[data-test-integration="1"] [data-test-text="description"]').textContent.trim(), -// 'integration description after save' -// ).to.equal('Description for Test Integration'); - -// await click('[data-test-integration="1"]'); - -// // warns of unsaved changes when leaving - -// await fillIn('[data-test-input="name"]', 'Unsaved test'); -// await click('[data-test-link="integrations-back"]'); - -// expect( -// find('[data-test-modal="unsaved-settings"]'), -// 'modal shown when navigating with unsaved changes' -// ).to.exist; - -// await click('[data-test-stay-button]'); - -// expect( -// find('[data-test-modal="unsaved-settings"]'), -// 'modal is closed after clicking "stay"' -// ).to.not.exist; - -// expect( -// currentURL(), -// 'url after clicking "stay"' -// ).to.equal('/settings/integrations/1'); - -// await click('[data-test-link="integrations-back"]'); -// await click('[data-test-leave-button]'); - -// expect( -// find('[data-test-modal="unsaved-settings"]'), -// 'modal is closed after clicking "leave"' -// ).to.not.exist; - -// expect( -// currentURL(), -// 'url after clicking "leave"' -// ).to.equal('/settings/integrations'); - -// expect( -// find('[data-test-integration="1"] [data-test-text="name"]').textContent.trim(), -// 'integration name after leaving unsaved changes' -// ).to.equal('Test Integration'); -// }); - -// it('can manage an integration\'s webhooks', async function () { -// this.server.create('integration'); - -// await visit('/settings/integrations/1'); - -// expect(find('[data-test-webhooks-blank-slate]')).to.exist; - -// // open new webhook modal -// await click('[data-test-link="add-webhook"]'); -// expect(find('[data-test-modal="webhook-form"]')).to.exist; -// expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').textContent) -// .to.have.string('New webhook'); - -// // can cancel new webhook -// await click('[data-test-button="cancel-webhook"]'); -// expect(find('[data-test-modal="webhook-form"]')).to.not.exist; - -// // create new webhook -// await click('[data-test-link="add-webhook"]'); -// await fillIn('[data-test-input="webhook-name"]', 'First webhook'); -// await fillIn('[data-test-select="webhook-event"]', 'site.changed'); -// await fillIn('[data-test-input="webhook-targetUrl"]', 'https://example.com/first-webhook'); -// await click('[data-test-button="save-webhook"]'); - -// // modal closed and 1 webhook listed with correct details -// expect(find('[data-test-modal="webhook-form"]')).to.not.exist; -// expect(find('[data-test-webhook-row]')).to.exist; -// let row = find('[data-test-webhook-row="1"]'); -// expect(row.querySelector('[data-test-text="name"]').textContent) -// .to.have.string('First webhook'); -// expect(row.querySelector('[data-test-text="event"]').textContent) -// .to.have.string('Site changed (rebuild)'); -// expect(row.querySelector('[data-test-text="targetUrl"]').textContent) -// .to.have.string('https://example.com/first-webhook'); -// expect(row.querySelector('[data-test-text="last-triggered"]').textContent) -// .to.have.string('Not triggered'); - -// // click edit webhook link -// await click('[data-test-webhook-row="1"] [data-test-newsletter-menu-trigger]'); -// await click('[data-test-link="edit-webhook"]'); - -// // modal appears and has correct title -// expect(find('[data-test-modal="webhook-form"]')).to.exist; -// expect(find('[data-test-modal="webhook-form"] [data-test-text="title"]').textContent) -// .to.have.string('Edit webhook'); -// }); - -// // test to ensure the `value=description` passed to `gh-text-input` is `readonly` -// it('doesn\'t show unsaved changes modal after placing focus on description field', async function () { -// this.server.create('integration'); - -// await visit('/settings/integrations/1'); -// await click('[data-test-input="description"]'); -// await blur('[data-test-input="description"]'); -// await click('[data-test-link="integrations-back"]'); - -// expect( -// find('[data-test-modal="unsaved-settings"]'), -// 'unsaved changes modal is not shown' -// ).to.not.exist; - -// expect(currentURL()).to.equal('/settings/integrations'); -// }); - -// it('can delete integration', async function () { -// this.server.create('integration'); - -// await visit('/settings/integrations/1'); -// await click('[data-test-button="delete-integration"]'); - -// expect(find('[data-test-modal="delete-integration"]')).to.exist; - -// await click('[data-test-modal="delete-integration"] [data-test-button="confirm"]'); - -// expect(find('[data-test-modal="delete-integration"]')).to.not.exist; -// expect(currentURL()).to.equal('/settings/integrations'); -// expect(find('[data-test-custom-integration]')).to.not.exist; -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/labs-test.js b/ghost/admin/tests/acceptance/settings/labs-test.js deleted file mode 100644 index e4ebd314c48..00000000000 --- a/ghost/admin/tests/acceptance/settings/labs-test.js +++ /dev/null @@ -1,338 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {beforeEach, describe, it} from 'mocha'; -// import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {fileUpload} from '../../helpers/file-upload'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Labs', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/labs'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/labs'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/labs'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/labs'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it renders', async function () { -// await visit('/settings/labs'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/labs'); - -// // has correct page title -// expect(document.title, 'page title').to.equal('Settings - Labs - Test Blog'); - -// // highlights nav menu -// expect(find('[data-test-nav="settings"]'), 'highlights nav menu item') -// .to.have.class('active'); -// }); - -// it('can delete all content', async function () { -// await visit('/settings/labs'); -// await click('[data-test-button="delete-all"]'); - -// const modal = '[data-test-modal="confirm-delete-all"]'; -// expect(find(modal)).to.exist; - -// await click(`${modal} [data-test-button="confirm"]`); - -// // API request is correct -// const [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// expect(lastRequest.url).to.equal('/ghost/api/admin/db/'); -// expect(lastRequest.method).to.equal('DELETE'); - -// expect(find(modal)).to.not.exist; -// }); - -// it('can upload/download redirects', async function () { -// await visit('/settings/labs'); - -// // successful upload -// this.server.post('/redirects/upload/', {}, 200); - -// await fileUpload( -// '[data-test-file-input="redirects"] input', -// ['test'], -// {name: 'redirects.json', type: 'application/json'} -// ); - -// // TODO: tests for the temporary success/failure state have been -// // disabled because they were randomly failing - -// // this should be half-way through button reset timeout -// // await timeout(50); -// // -// // // shows success button -// // let buttons = findAll('[data-test-button="upload-redirects"]'); -// // expect(buttons.length, 'no of success buttons').to.equal(1); -// // expect( -// // buttons[0], -// // 'success button is green' -// // ).to.have.class('gh-btn-green); -// // expect( -// // button.textContent, -// // 'success button text' -// // ).to.have.string('Uploaded'); -// // -// // await wait(); - -// // returned to normal button -// let buttons = findAll('[data-test-button="upload-redirects"]'); -// expect(buttons.length, 'no of post-success buttons').to.equal(1); -// expect( -// buttons[0], -// 'post-success button doesn\'t have success class' -// ).to.not.have.class('gh-btn-green'); -// expect( -// buttons[0].textContent, -// 'post-success button text' -// ).to.have.string('Upload redirects'); - -// // failed upload -// this.server.post('/redirects/upload/', { -// errors: [{ -// type: 'BadRequestError', -// message: 'Test failure message' -// }] -// }, 400); - -// await fileUpload( -// '[data-test-file-input="redirects"] input', -// ['test'], -// {name: 'redirects-bad.json', type: 'application/json'} -// ); - -// // TODO: tests for the temporary success/failure state have been -// // disabled because they were randomly failing - -// // this should be half-way through button reset timeout -// // await timeout(50); -// // -// // shows failure button -// // buttons = findAll('[data-test-button="upload-redirects"]'); -// // expect(buttons.length, 'no of failure buttons').to.equal(1); -// // expect( -// // buttons[0], -// // 'failure button is red' -// // ).to.have.class('gh-btn-red); -// // expect( -// // buttons[0].textContent, -// // 'failure button text' -// // ).to.have.string('Upload Failed'); -// // -// // await wait(); - -// // shows error message -// expect( -// find('[data-test-error="redirects"]').textContent.trim(), -// 'upload error text' -// ).to.have.string('Test failure message'); - -// // returned to normal button -// buttons = findAll('[data-test-button="upload-redirects"]'); -// expect(buttons.length, 'no of post-failure buttons').to.equal(1); -// expect( -// buttons[0], -// 'post-failure button doesn\'t have failure class' -// ).to.not.have.class('gh-btn-red'); -// expect( -// buttons[0].textContent, -// 'post-failure button text' -// ).to.have.string('Upload redirects'); - -// // successful upload clears error -// this.server.post('/redirects/upload/', {}, 200); -// await fileUpload( -// '[data-test-file-input="redirects"] input', -// ['test'], -// {name: 'redirects-bad.json', type: 'application/json'} -// ); - -// expect(find('[data-test-error="redirects"]')).to.not.exist; - -// // can download redirects.json -// await click('[data-test-link="download-redirects"]'); - -// let iframe = document.querySelector('#iframeDownload'); -// expect(iframe.getAttribute('src')).to.have.string('/redirects/download/'); -// }); - -// it('can upload/download routes.yaml', async function () { -// await visit('/settings/labs'); - -// // successful upload -// this.server.post('/settings/routes/yaml/', {}, 200); - -// await fileUpload( -// '[data-test-file-input="routes"] input', -// ['test'], -// {name: 'routes.yaml', type: 'application/x-yaml'} -// ); - -// // TODO: tests for the temporary success/failure state have been -// // disabled because they were randomly failing - -// // this should be half-way through button reset timeout -// // await timeout(50); -// // -// // // shows success button -// // let button = find('[data-test-button="upload-routes"]'); -// // expect(button.length, 'no of success buttons').to.equal(1); -// // expect( -// // button.hasClass('gh-btn-green'), -// // 'success button is green' -// // ).to.be.true; -// // expect( -// // button.text().trim(), -// // 'success button text' -// // ).to.have.string('Uploaded'); -// // -// // await wait(); - -// // returned to normal button -// let buttons = findAll('[data-test-button="upload-routes"]'); -// expect(buttons.length, 'no of post-success buttons').to.equal(1); -// expect( -// buttons[0], -// 'routes post-success button doesn\'t have success class' -// ).to.not.have.class('gh-btn-green'); -// expect( -// buttons[0].textContent, -// 'routes post-success button text' -// ).to.have.string('Upload routes YAML'); - -// // failed upload -// this.server.post('/settings/routes/yaml/', { -// errors: [{ -// type: 'BadRequestError', -// message: 'Test failure message' -// }] -// }, 400); - -// await fileUpload( -// '[data-test-file-input="routes"] input', -// ['test'], -// {name: 'routes-bad.yaml', type: 'application/x-yaml'} -// ); - -// // TODO: tests for the temporary success/failure state have been -// // disabled because they were randomly failing - -// // this should be half-way through button reset timeout -// // await timeout(50); -// // -// // shows failure button -// // button = find('[data-test-button="upload-routes"]'); -// // expect(button.length, 'no of failure buttons').to.equal(1); -// // expect( -// // button.hasClass('gh-btn-red'), -// // 'failure button is red' -// // ).to.be.true; -// // expect( -// // button.text().trim(), -// // 'failure button text' -// // ).to.have.string('Upload Failed'); -// // -// // await wait(); - -// // shows error message -// expect( -// find('[data-test-error="routes"]').textContent, -// 'routes upload error text' -// ).to.have.string('Test failure message'); - -// // returned to normal button -// buttons = findAll('[data-test-button="upload-routes"]'); -// expect(buttons.length, 'no of post-failure buttons').to.equal(1); -// expect( -// buttons[0], -// 'routes post-failure button doesn\'t have failure class' -// ).to.not.have.class('gh-btn-red'); -// expect( -// buttons[0].textContent, -// 'routes post-failure button text' -// ).to.have.string('Upload routes YAML'); - -// // successful upload clears error -// this.server.post('/settings/routes/yaml/', {}, 200); -// await fileUpload( -// '[data-test-file-input="routes"] input', -// ['test'], -// {name: 'routes-good.yaml', type: 'application/x-yaml'} -// ); - -// expect(find('[data-test-error="routes"]')).to.not.exist; - -// // can download redirects.json -// await click('[data-test-link="download-routes"]'); - -// let iframe = document.querySelector('#iframeDownload'); -// expect(iframe.getAttribute('src')).to.have.string('/settings/routes/yaml/'); -// }); - -// describe('When logged in as Owner', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Owner'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it.skip('sets the mailgunBaseUrl to the default', async function () { -// await visit('/settings/members'); - -// await fillIn('[data-test-mailgun-api-key-input]', 'i_am_an_api_key'); -// await fillIn('[data-test-mailgun-domain-input]', 'https://domain.tld'); - -// await click('[data-test-button="save-members-settings"]'); - -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(lastRequest.requestBody); - -// expect(params.settings.findBy('key', 'mailgun_base_url').value).not.to.equal(null); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/membership-test.js b/ghost/admin/tests/acceptance/settings/membership-test.js deleted file mode 100644 index 1291e171bec..00000000000 --- a/ghost/admin/tests/acceptance/settings/membership-test.js +++ /dev/null @@ -1,292 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {blur, click, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Membership', function () { -// const hooks = setupApplicationTest(); -// setupMirage(hooks); - -// beforeEach(function () { -// }); - -// beforeEach(async function () { -// this.server.loadFixtures('configs'); -// this.server.loadFixtures('tiers'); - -// this.server.db.configs.update(1, {blogUrl: 'http://localhost:2368'}); - -// const role = this.server.create('role', {name: 'Owner'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// describe('permissions', function () { -// let visitAs; - -// before(function () { -// visitAs = async (roleName) => { -// const role = this.server.create('role', {name: roleName}); -// this.server.create('user', {roles: [role]}); -// await authenticateSession(); -// await visit('/settings/members'); -// }; -// }); - -// beforeEach(async function () { -// this.server.db.users.remove(); -// await invalidateSession(); -// }); - -// it('allows Owners', async function () { -// await visitAs('Owner'); -// expect(currentURL()).to.equal('/settings/members'); -// }); - -// it('allows Administrators', async function () { -// await visitAs('Administrator'); -// expect(currentURL()).to.equal('/settings/members'); -// }); - -// it('disallows Editors', async function () { -// await visitAs('Editor'); -// expect(currentURL()).to.not.equal('/settings/members'); -// }); - -// it('disallows Authors', async function () { -// await visitAs('Author'); -// expect(currentURL()).to.not.equal('/settings/members'); -// }); - -// it('disallows Contributors', async function () { -// await visitAs('Contributor'); -// expect(currentURL()).to.not.equal('/settings/members'); -// }); -// }); - -// it('can change subscription access', async function () { -// await visit('/settings/members'); - -// expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('all'); -// expect(find('[data-test-members-subscription-option="all"]'), 'initial selection is "all"').to.exist; -// expect(find('[data-test-iframe="portal-preview"]'), 'initial preview src matches "all"') -// .to.have.attribute('src').match(/membersSignupAccess=all/); - -// // open dropdown -// await click('[data-test-members-subscription-option="all"]'); - -// // all settings exist in dropdown -// expect(find('.ember-power-select-options [data-test-members-subscription-option="all"]'), 'all option').to.exist; -// expect(find('.ember-power-select-options [data-test-members-subscription-option="invite"]'), 'invite option').to.exist; -// expect(find('.ember-power-select-options [data-test-members-subscription-option="none"]'), 'none option').to.exist; - -// // switch to invite -// await click('.ember-power-select-options [data-test-members-subscription-option="invite"]'); - -// expect(find('.ember-power-select-options'), 'dropdown closes').to.not.exist; -// expect(find('[data-test-members-subscription-option="invite"]'), 'invite option shown after selected').to.exist; -// expect(find('[data-test-iframe="portal-preview"]')) -// .to.have.attribute('src').match(/membersSignupAccess=invite/); - -// await click('[data-test-button="save-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('invite'); - -// // switch to nobody -// await click('[data-test-members-subscription-option="invite"]'); -// await click('.ember-power-select-options [data-test-members-subscription-option="none"]'); - -// expect(find('.ember-power-select-options'), 'dropdown closes').to.not.exist; -// expect(find('[data-test-members-subscription-option="none"]'), 'none option shown after selected').to.exist; -// expect(find('[data-test-iframe="portal-preview"]')).to.not.exist; -// expect(find('[data-test-portal-preview-disabled]')).to.exist; - -// expect(find('[data-test-default-post-access] .ember-basic-dropdown-trigger')).to.have.attribute('aria-disabled', 'true'); - -// await click('[data-test-button="save-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('none'); - -// // automatically saves when switching back off nobody -// await click('[data-test-members-subscription-option="none"]'); -// await click('.ember-power-select-options [data-test-members-subscription-option="invite"]'); -// expect(this.server.db.settings.findBy({key: 'members_signup_access'}).value).to.equal('invite'); -// }); - -// it('can change default post access', async function () { -// await visit('/settings/members'); - -// // fixtures match what we expect -// expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('public'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('[]'); - -// expect(find('[data-test-default-post-access-option="public"]'), 'initial selection is "public"').to.exist; -// expect(find('[data-test-default-post-access-tiers]')).to.not.exist; - -// // open dropdown -// await click('[data-test-default-post-access-option="public"]'); - -// // all settings exist in dropdown -// expect(find('.ember-power-select-options [data-test-default-post-access-option="public"]'), 'public option').to.exist; -// expect(find('.ember-power-select-options [data-test-default-post-access-option="members"]'), 'members-only option').to.exist; -// expect(find('.ember-power-select-options [data-test-default-post-access-option="paid"]'), 'paid-only option').to.exist; -// expect(find('.ember-power-select-options [data-test-default-post-access-option="tiers"]'), 'specific tiers option').to.exist; - -// // switch to members only -// await click('.ember-power-select-options [data-test-default-post-access-option="members"]'); -// await click('[data-test-button="save-settings"]'); - -// expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('members'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('[]'); - -// expect(find('[data-test-default-post-access-option="members"]'), 'post-members selection is "members"').to.exist; -// expect(find('[data-test-default-post-access-tiers]')).to.not.exist; - -// // can switch to specific tiers -// await click('[data-test-default-post-access-option="members"]'); -// await click('.ember-power-select-options [data-test-default-post-access-option="tiers"]'); - -// // tiers input is shown -// expect(find('[data-test-default-post-access-tiers]')).to.exist; - -// // open tiers dropdown -// await click('[data-test-default-post-access-tiers] .ember-basic-dropdown-trigger'); - -// // paid tiers are available in tiers input -// expect(find('[data-test-default-post-access-tiers] [data-test-visibility-segment-option="Default Tier"]')).to.exist; - -// // select tier -// await click('[data-test-default-post-access-tiers] [data-test-visibility-segment-option="Default Tier"]'); - -// // save -// await click('[data-test-button="save-settings"]'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('tiers'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('["2"]'); - -// // switch back to non-tiers option -// await click('[data-test-default-post-access-option="tiers"]'); -// await click('.ember-power-select-options [data-test-default-post-access-option="paid"]'); - -// expect(find('[data-test-default-post-access-tiers]')).to.not.exist; - -// await click('[data-test-button="save-settings"]'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility'}).value).to.equal('paid'); -// expect(this.server.db.settings.findBy({key: 'default_content_visibility_tiers'}).value).to.equal('["2"]'); -// }); - -// it('can manage free tier', async function () { -// await visit('/settings/members'); -// await click('[data-test-button="toggle-free-settings"]'); -// expect(find('[data-test-free-settings-expanded]'), 'expanded free settings').to.exist; - -// // we aren't viewing the non-labs-flag input -// expect(find('[data-test-input="old-free-welcome-page"]')).to.not.exist; - -// // it can set free signup welcome page - -// // initial value -// expect(find('[data-test-input="free-welcome-page"]')).to.exist; -// expect(find('[data-test-input="free-welcome-page"]')).to.have.value(''); - -// // saving -// await fillIn('[data-test-input="free-welcome-page"]', 'not a url'); -// await blur('[data-test-input="free-welcome-page"]'); -// await click('[data-test-button="save-settings"]'); - -// expect(this.server.db.tiers.findBy({slug: 'free'}).welcomePageUrl) -// .to.equal('/not%20a%20url'); - -// // re-rendering will insert full URL in welcome page input -// await visit('/settings'); -// await visit('/settings/members'); - -// expect(find('[data-test-input="free-welcome-page"]')).to.exist; -// expect(find('[data-test-input="free-welcome-page"]')) -// .to.have.value('http://localhost:2368/not%20a%20url'); - -// // it can manage free tier description and benefits - -// // initial free tier details are as expected -// expect(find('[data-test-tier-card="free"]')).to.exist; -// expect(find('[data-test-tier-card="free"] [data-test-name]')).to.contain.text('Free'); -// expect(find('[data-test-tier-card="free"] [data-test-description]')).to.contain.text('No description'); -// expect(find('[data-test-tier-card="free"] [data-test-benefits]')).to.contain.text('No benefits'); -// expect(find('[data-test-tier-card="free"] [data-test-free-price]')).to.exist; - -// // open modal -// await click('[data-test-tier-card="free"] [data-test-button="edit-tier"]'); - -// // initial modal state is as expected -// const modal = '[data-test-modal="edit-tier"]'; -// expect(find(modal)).to.exist; -// expect(find(`${modal} [data-test-input="tier-name"]`)).to.not.exist; -// expect(find(`${modal} [data-test-input="tier-description"]`)).to.not.exist; -// expect(find(`${modal} [data-test-input="free-tier-description"]`)).to.exist; -// expect(find(`${modal} [data-test-input="free-tier-description"]`)).to.have.value(''); -// expect(find(`${modal} [data-test-formgroup="prices"]`)).to.not.exist; -// expect(find(`${modal} [data-test-benefit-item="new"]`)).to.exist; -// expect(findAll(`${modal} [data-test-benefit-item]`).length).to.equal(1); - -// expect(find(`${modal} [data-test-tierpreview-title]`)).to.contain.text('Free Membership Preview'); -// expect(find(`${modal} [data-test-tierpreview-description]`)).to.contain.text('Free preview of'); -// expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('Access to all public posts'); -// expect(find(`${modal} [data-test-tierpreview-price]`).textContent).to.match(/\$\s+0/); - -// // can change description -// await fillIn(`${modal} [data-test-input="free-tier-description"]`, 'Test description'); -// expect(find(`${modal} [data-test-tierpreview-description]`)).to.contain.text('Test description'); - -// // can manage benefits -// const newBenefit = `${modal} [data-test-benefit-item="new"]`; -// await fillIn(`${newBenefit} [data-test-input="benefit-label"]`, 'First benefit'); -// await click(`${newBenefit} [data-test-button="add-benefit"]`); - -// expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('First benefit'); - -// expect(find(`${modal} [data-test-benefit-item="0"]`)).to.exist; -// expect(find(`${modal} [data-test-benefit-item="new"]`)).to.exist; - -// await click(`${newBenefit} [data-test-button="add-benefit"]`); -// expect(find(`${newBenefit}`)).to.contain.text('Please enter a benefit'); - -// await fillIn(`${newBenefit} [data-test-input="benefit-label"]`, 'Second benefit'); -// await click(`${newBenefit} [data-test-button="add-benefit"]`); - -// expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('Second benefit'); -// expect(findAll(`${modal} [data-test-tierpreview-benefits] div`).length).to.equal(4); - -// await click(`${modal} [data-test-benefit-item="0"] [data-test-button="delete-benefit"]`); - -// expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.not.contain.text('First benefit'); -// expect(findAll(`${modal} [data-test-tierpreview-benefits] div`).length).to.equal(2); - -// // Add a new benefit that we will later rename to an empty name -// await fillIn(`${newBenefit} [data-test-input="benefit-label"]`, 'Third benefit'); -// await click(`${newBenefit} [data-test-button="add-benefit"]`); - -// expect(find(`${modal} [data-test-tierpreview-benefits]`)).to.contain.text('Third benefit'); -// expect(findAll(`${modal} [data-test-tierpreview-benefits] div`).length).to.equal(4); - -// // Clear the second benefit's name (it should get removed after saving) -// const secondBenefitItem = `${modal} [data-test-benefit-item="1"]`; -// await fillIn(`${secondBenefitItem} [data-test-input="benefit-label"]`, ''); - -// await click('[data-test-button="save-tier"]'); - -// expect(find(`${modal}`)).to.not.exist; -// expect(find('[data-test-tier-card="free"] [data-test-name]')).to.contain.text('Free'); -// expect(find('[data-test-tier-card="free"] [data-test-description]')).to.contain.text('Test description'); -// expect(find('[data-test-tier-card="free"] [data-test-benefits]')).to.contain.text('Benefits (1)'); -// expect(find('[data-test-tier-card="free"] [data-test-benefits] li:nth-of-type(1)')).to.contain.text('Second benefit'); -// expect(findAll(`[data-test-tier-card="free"] [data-test-benefits] li`).length).to.equal(1); - -// const freeTier = this.server.db.tiers.findBy({slug: 'free'}); -// expect(freeTier.description).to.equal('Test description'); -// expect(freeTier.welcomePageUrl).to.equal('/not%20a%20url'); -// expect(freeTier.benefits.length).to.equal(1); -// expect(freeTier.benefits[0]).to.equal('Second benefit'); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/navigation-test.js b/ghost/admin/tests/acceptance/settings/navigation-test.js deleted file mode 100644 index 69c0099e946..00000000000 --- a/ghost/admin/tests/acceptance/settings/navigation-test.js +++ /dev/null @@ -1,252 +0,0 @@ -// import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -// import {authenticateSession} from 'ember-simple-auth/test-support'; -// import {beforeEach, describe, it} from 'mocha'; -// import {blur, click, currentRouteName, currentURL, fillIn, find, findAll, triggerEvent, typeIn} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// // simulate jQuery's `:visible` pseudo-selector -// function withText(elements) { -// return Array.from(elements).filter(elem => elem.textContent.trim() !== ''); -// } - -// describe('Acceptance: Settings - Navigation', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// await authenticateSession(); -// }); - -// it('can visit /settings/navigation', async function () { -// await visit('/settings/navigation'); - -// expect(currentRouteName()).to.equal('settings.navigation'); -// expect(find('[data-test-save-button]').textContent.trim(), 'save button text').to.equal('Save'); - -// // fixtures contain two nav items, check for four rows as we -// // should have one extra that's blank for each navigation section -// expect( -// findAll('[data-test-navitem]').length, -// 'navigation items count' -// ).to.equal(4); -// }); - -// it('saves navigation settings', async function () { -// await visit('/settings/navigation'); -// await fillIn('#settings-navigation [data-test-navitem="0"] [data-test-input="label"]', 'Test'); -// await typeIn('#settings-navigation [data-test-navitem="0"] [data-test-input="url"]', '/test'); -// await click('[data-test-save-button]'); - -// let [navSetting] = this.server.db.settings.where({key: 'navigation'}); - -// expect(navSetting.value).to.equal('[{"label":"Test","url":"/test/"},{"label":"About","url":"/about"}]'); - -// // don't test against .error directly as it will pick up failed -// // tests "pre.error" elements -// expect(findAll('span.error').length, 'error messages count').to.equal(0); -// expect(findAll('.gh-alert').length, 'alerts count').to.equal(0); -// expect(withText(findAll('[data-test-error]')).length, 'validation errors count') -// .to.equal(0); -// }); - -// it('validates new item correctly on save', async function () { -// await visit('/settings/navigation'); -// await click('[data-test-save-button]'); - -// expect( -// findAll('#settings-navigation [data-test-navitem]').length, -// 'number of nav items after saving with blank new item' -// ).to.equal(3); - -// await fillIn('#settings-navigation [data-test-navitem="new"] [data-test-input="label"]', 'Test'); -// await fillIn('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]', ''); -// await typeIn('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]', 'http://invalid domain/'); - -// await click('[data-test-save-button]'); - -// expect( -// findAll('#settings-navigation [data-test-navitem]').length, -// 'number of nav items after saving with invalid new item' -// ).to.equal(3); - -// expect( -// withText(findAll('#settings-navigation [data-test-navitem="new"] [data-test-error]')).length, -// 'number of invalid fields in new item' -// ).to.equal(1); -// }); - -// it('clears unsaved settings when navigating away but warns with a confirmation dialog', async function () { -// await visit('/settings/navigation'); -// await fillIn('[data-test-navitem="0"] [data-test-input="label"]', 'Test'); -// await blur('[data-test-navitem="0"] [data-test-input="label"]'); - -// expect(find('[data-test-navitem="0"] [data-test-input="label"]').value).to.equal('Test'); - -// await visit('/settings/code-injection'); - -// expect(findAll('[data-test-modal="unsaved-settings"]').length, 'modal exists').to.equal(1); - -// // Leave without saving -// await click('[data-test-leave-button]'), 'leave without saving'; - -// expect(currentURL(), 'currentURL').to.equal('/settings/code-injection'); - -// await visit('/settings/navigation'); - -// expect(find('[data-test-navitem="0"] [data-test-input="label"]').value).to.equal('Home'); -// }); - -// it('can add and remove items', async function () { -// await visit('/settings/navigation'); -// await click('#settings-navigation .gh-blognav-add'); - -// expect( -// find('[data-test-navitem="new"] [data-test-error="label"]').textContent.trim(), -// 'blank label has validation error' -// ).to.not.be.empty; - -// await fillIn('[data-test-navitem="new"] [data-test-input="label"]', ''); -// await typeIn('[data-test-navitem="new"] [data-test-input="label"]', 'New'); - -// expect( -// find('[data-test-navitem="new"] [data-test-error="label"]').textContent.trim(), -// 'label validation is visible after typing' -// ).to.be.empty; - -// await fillIn('[data-test-navitem="new"] [data-test-input="url"]', ''); -// await typeIn('[data-test-navitem="new"] [data-test-input="url"]', '/new'); -// await blur('[data-test-navitem="new"] [data-test-input="url"]'); - -// expect( -// find('[data-test-navitem="new"] [data-test-error="url"]').textContent.trim(), -// 'url validation is visible after typing' -// ).to.be.empty; - -// expect( -// find('[data-test-navitem="new"] [data-test-input="url"]').value -// ).to.equal(`${window.location.origin}/new/`); - -// await click('.gh-blognav-add'); - -// expect( -// findAll('#settings-navigation [data-test-navitem]').length, -// 'number of nav items after successful add' -// ).to.equal(4); - -// expect( -// find('#settings-navigation [data-test-navitem="new"] [data-test-input="label"]').value, -// 'new item label value after successful add' -// ).to.be.empty; - -// expect( -// find('#settings-navigation [data-test-navitem="new"] [data-test-input="url"]').value, -// 'new item url value after successful add' -// ).to.equal(`${window.location.origin}/`); - -// expect( -// withText(findAll('[data-test-navitem] [data-test-error]')).length, -// 'number or validation errors shown after successful add' -// ).to.equal(0); - -// await click('#settings-navigation [data-test-navitem="0"] .gh-blognav-delete'); - -// expect( -// findAll('#settings-navigation [data-test-navitem]').length, -// 'number of nav items after successful remove' -// ).to.equal(3); - -// // CMD-S shortcut works -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// let [navSetting] = this.server.db.settings.where({key: 'navigation'}); - -// expect(navSetting.value).to.equal('[{"label":"About","url":"/about"},{"label":"New","url":"/new/"}]'); -// }); - -// it('can also add and remove items from seconday nav', async function () { -// await visit('/settings/navigation'); -// await click('#secondary-navigation .gh-blognav-add'); - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-error="label"]').textContent.trim(), -// 'blank label has validation error' -// ).to.not.be.empty; - -// await fillIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]', ''); -// await typeIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]', 'Foo'); - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-error="label"]').textContent.trim(), -// 'label validation is visible after typing' -// ).to.be.empty; - -// await fillIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]', ''); -// await typeIn('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]', '/bar'); -// await blur('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]'); - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-error="url"]').textContent.trim(), -// 'url validation is visible after typing' -// ).to.be.empty; - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]').value -// ).to.equal(`${window.location.origin}/bar/`); - -// await click('[data-test-save-button]'); - -// expect( -// findAll('#secondary-navigation [data-test-navitem]').length, -// 'number of nav items after successful add' -// ).to.equal(2); - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-input="label"]').value, -// 'new item label value after successful add' -// ).to.be.empty; - -// expect( -// find('#secondary-navigation [data-test-navitem="new"] [data-test-input="url"]').value, -// 'new item url value after successful add' -// ).to.equal(`${window.location.origin}/`); - -// expect( -// withText(findAll('#secondary-navigation [data-test-navitem] [data-test-error]')).length, -// 'number or validation errors shown after successful add' -// ).to.equal(0); - -// let [navSetting] = this.server.db.settings.where({key: 'secondary_navigation'}); - -// expect(navSetting.value).to.equal('[{"label":"Foo","url":"/bar/"}]'); - -// await click('#secondary-navigation [data-test-navitem="0"] .gh-blognav-delete'); - -// expect( -// findAll('#secondary-navigation [data-test-navitem]').length, -// 'number of nav items after successful remove' -// ).to.equal(1); - -// // CMD-S shortcut works -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// [navSetting] = this.server.db.settings.where({key: 'secondary_navigation'}); - -// expect(navSetting.value).to.equal('[]'); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/newsletters-test.js b/ghost/admin/tests/acceptance/settings/newsletters-test.js deleted file mode 100644 index ae2c2ed8051..00000000000 --- a/ghost/admin/tests/acceptance/settings/newsletters-test.js +++ /dev/null @@ -1,574 +0,0 @@ -// import {authenticateSession} from 'ember-simple-auth/test-support'; -// import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// async function checkValidationError(errors) { -// // Create the newsletter -// await click('[data-test-button="save-newsletter"]'); - -// // @todo: at the moment, the tabs don't open on error automatically -// // we need to remove these lines when this is fixed -// // and replace it with something like ± checkTabOpen('genexral') -// // await openTab('general.name'); - -// for (const selector of Object.keys(errors)) { -// expect(findAll(selector).length, 'field ' + selector + ' is not visible').to.equal(1); -// expect(findAll(selector + ' + .response').length, 'error message is displayed').to.equal(1); -// expect(find(selector + ' + .response').textContent).to.match(errors[selector]); -// } - -// // Check button is in error state -// expect(find('[data-test-button="save-newsletter"] > [data-test-task-button-state="failure"]')).to.exist; -// } - -// async function checkSave(options) { -// const name = options.edit ? 'edit' : 'create'; - -// // Create the newsletter -// await click('[data-test-button="save-newsletter"]'); - -// // No errors -// expect(findAll('.error > .response').length, 'error message is displayed').to.equal(0); - -// if (options.verifyEmail) { -// expect(find('[data-test-modal="confirm-newsletter-email"]'), 'Confirm email modal').to.exist; - -// // Check message -// if (typeof verifyEmail !== 'boolean') { -// const t = find('[data-test-modal="confirm-newsletter-email"] p').textContent.trim().replace(/\s+/g, ' '); -// expect(t).to.match(options.verifyEmail, t); -// } -// await click('[data-test-button="confirm-newsletter-email"]'); -// } - -// // Check if modal closes on save -// expect(find(`[data-test-modal="${name}-newsletter"]`), 'Newsletter modal should disappear after saving').to.not.exist; -// } - -// async function checkCancel(options) { -// const name = options.edit ? 'edit' : 'create'; - -// // Create the newsletter -// await click('[data-test-button="cancel-newsletter"]'); - -// if (options.shouldConfirm) { -// expect(find('[data-test-modal="unsaved-settings"]'), 'Confirm unsaved settings modal should be visible').to.exist; -// await click('[data-test-leave-button]'); -// } - -// // Check if modal closes on save -// expect(find(`[data-test-modal="${name}-newsletter"]`), 'Newsletter modal should disappear after canceling').to.not.exist; -// } - -// async function openTab(name, optional = true) { -// const generalToggleSelector = '[data-test-nav-toggle="' + name + '"]'; -// const generalToggle = find(generalToggleSelector); -// const doesExist = !!generalToggle; - -// if (!doesExist && !optional) { -// throw new Error('Expected tab ' + name + ' to exist'); -// } - -// if (doesExist && !generalToggle.classList.contains('active')) { -// await click(generalToggleSelector); - -// if (!generalToggle.classList.contains('active')) { -// throw new Error('Could not open ' + name + ' tab'); -// } -// } -// } - -// async function closeTab(name, optional = true) { -// const generalToggleSelector = '[data-test-nav-toggle="' + name + '"]'; -// const generalToggle = find(generalToggleSelector); -// const doesExist = !!generalToggle; - -// if (!doesExist && !optional) { -// throw new Error('Expected tab ' + name + ' to exist'); -// } - -// if (doesExist && generalToggle.classList.contains('active')) { -// await click(generalToggleSelector); - -// if (generalToggle.classList.contains('active')) { -// throw new Error('Could not close ' + name + ' tab'); -// } -// } -// } - -// async function fillName(name) { -// await openTab('general.name'); -// await fillIn('input#newsletter-title', name); -// } - -// describe('Acceptance: Settings - Newsletters', function () { -// const hooks = setupApplicationTest(); -// setupMirage(hooks); - -// beforeEach(async function () { -// this.server.loadFixtures('configs', 'newsletters'); - -// const role = this.server.create('role', {name: 'Owner'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('redirects old path', async function () { -// await visit('/settings/members-email'); -// expect(currentURL()).to.equal('/settings/newsletters'); -// }); - -// describe('Creating newsletters', function () { -// it('can create new newsletter', async function () { -// await visit('/settings/newsletters'); -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(1); -// await click('[data-test-button="add-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="create-newsletter"]'), 'Create newsletter modal').to.exist; - -// // Fill in the newsletter name -// await fillName('My new newsletter'); - -// // Fill in the newsletter description -// await fillIn('textarea#newsletter-description', 'My newsletter description'); - -// await checkSave({}); - -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown afterwards').to.equal(2); -// }); - -// it('validates create newsletter before saving', async function () { -// await visit('/settings/newsletters'); -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(1); - -// await click('[data-test-button="add-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="create-newsletter"]'), 'Create newsletter modal').to.exist; - -// // Invalid name error when you try to save -// await checkValidationError({'input#newsletter-title': /Please enter a name./}); - -// // Fill in the newsletter name -// await fillName('My new newsletter'); - -// // Everything should be valid -// await checkSave({}); - -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown afterwards').to.equal(2); -// }); - -// it('checks limits when creating a newsletter', async function () { -// const config = this.server.db.configs.find(1); -// config.hostSettings = { -// limits: { -// newsletters: { -// max: 1, -// error: 'Your plan supports up to {{max}} newsletters. Please upgrade to add more.' -// } -// } -// }; -// this.server.db.configs.update(1, config); - -// await visit('/settings/newsletters'); -// await click('[data-test-button="add-newsletter"]'); - -// // Check if modal doesn't open -// expect(find('[data-test-modal="create-newsletter"]'), 'Create newsletter modal').not.to.exist; - -// // Check limits modal is shown -// expect(find('[data-test-modal="limits/multiple-newsletters"]'), 'Limits modal').to.exist; - -// // Check can close modal -// await click('[data-test-button="cancel-upgrade"]'); - -// // Check modal is closed -// expect(find('[data-test-modal="limits/multiple-newsletters"]'), 'Limits modal').not.to.exist; -// }); -// }); - -// describe('Editing newsletters', function () { -// it('can edit via menu if multiple newsletters', async function () { -// // Create an extra newsletter -// this.server.create('newsletter', {status: 'active', name: 'test newsletter', slug: 'test-newsletter'}); -// await visit('/settings/newsletters'); - -// await click('[data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; -// }); - -// it('validates edit fields before saving', async function () { -// await visit('/settings/newsletters'); - -// // When we only have a single newsletter, the customize button is shown instead of the menu button -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; - -// // Clear newsletter name -// await fillName(''); - -// // Invalid name error when you try to save -// await checkValidationError({'input#newsletter-title': /Please enter a name./}); - -// // Fill in the newsletter name -// await fillName('My new newsletter'); - -// // Enter an invalid email -// await openTab('general.email'); -// await fillIn('input#newsletter-sender-email', 'invalid-email'); - -// // Check if it complains about the invalid email -// await checkValidationError({ -// 'input#newsletter-sender-email': /Invalid email./ -// }); - -// await fillIn('input#newsletter-sender-email', 'valid-email@email.com'); - -// // Everything should be valid -// await checkSave({ -// edit: true, -// verifyEmail: /default email address \(noreply/ -// }); -// }); - -// it('can open / close all tabs', async function () { -// await visit('/settings/newsletters'); - -// // When we only have a single newsletter, the customize button is shown instead of the menu button -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; - -// await openTab('general.name', false); -// await closeTab('general.name', false); - -// await openTab('general.email', false); -// await closeTab('general.email', false); - -// await openTab('general.member', false); -// await closeTab('general.member', false); - -// // todo: uncomment after `audienceFeedback` feature flag will be removed -// //await openTab('general.audienceFeedback', false); -// //await closeTab('general.audienceFeedback', false); - -// await openTab('design.header', false); -// await closeTab('design.header', false); - -// await openTab('design.body', false); -// await closeTab('design.body', false); - -// await openTab('design.footer', false); -// await closeTab('design.footer', false); -// }); - -// it('shows current sender email in verify modal', async function () { -// this.server.create('newsletter', {status: 'active', name: 'test newsletter', slug: 'test-newsletter', senderEmail: 'test@example.com'}); - -// await visit('/settings/newsletters'); - -// // Edit the last newsletter -// await click('[data-test-newsletter="test-newsletter"] [data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; - -// await openTab('general.email'); -// await fillIn('input#newsletter-sender-email', 'valid-email@email.com'); - -// // Everything should be valid -// await checkSave({ -// edit: true, -// verifyEmail: /previous email address \(test@example\.com\)/ -// }); -// }); - -// it('does not ask to confirm saved changes', async function () { -// await visit('/settings/newsletters'); - -// // When we only have a single newsletter, the customize button is shown instead of the menu button -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; - -// // Make no changes - -// // Everything should be valid -// await checkCancel({ -// edit: true, -// shouldConfirm: false -// }); -// }); - -// it('asks to confirm unsaved changes', async function () { -// async function doCheck(tabName, field) { -// await visit('/settings/newsletters'); - -// // When we only have a single newsletter, the customize button is shown instead of the menu button -// await click('[data-test-button="customize-newsletter"]'); - -// // Check if modal opens -// expect(find('[data-test-modal="edit-newsletter"]'), 'Edit newsletter modal').to.exist; - -// // Make a change -// await openTab(tabName, false); -// if (field.input) { -// await fillIn(field.input, field.value ?? 'my changed value'); -// } else if (field.toggle) { -// await click(field.toggle); -// } else if (field.dropdown) { -// // Open dropdown -// await click(`${field.dropdown} .ember-basic-dropdown-trigger`); - -// // Click first not-selected option -// await click(`${field.dropdown} li.ember-power-select-option[aria-current="false"]`); -// } - -// // Everything should be valid -// await checkCancel({ -// edit: true, -// shouldConfirm: true -// }); -// } - -// // General name -// await doCheck('general.name', { -// input: '#newsletter-title' -// }); - -// await doCheck('general.name', { -// input: '#newsletter-description' -// }); - -// // General email -// await doCheck('general.email', { -// input: '#newsletter-sender-name' -// }); - -// await doCheck('general.email', { -// input: '#newsletter-sender-email' -// }); - -// await doCheck('general.email', { -// input: '#newsletter-reply-to', -// value: 'support' -// }); - -// // Member settings -// await doCheck.call(this, 'general.member', { -// toggle: '[data-test-toggle="subscribeOnSignup"]' -// }); - -// // Newsletter analytics -// // todo: uncomment after `audienceFeedback` feature flag will be removed -// //await doCheck.call(this, 'general.audienceFeedback', { -// // toggle: '[data-test-toggle="feedbackEnabled"]' -// //}); - -// // Design header -// await doCheck.call(this, 'design.header', { -// toggle: '[data-test-toggle="showHeaderTitle"]' -// }); - -// await doCheck.call(this, 'design.header', { -// toggle: '[data-test-toggle="showHeaderName"]' -// }); - -// // Design body -// await doCheck.call(this, 'design.body', { -// dropdown: '[data-test-input="titleFontCategory"]' -// }); - -// await doCheck.call(this, 'design.body', { -// toggle: '#newsletter-title-alignment button:not(.gh-btn-group-selected)' -// }); - -// await doCheck.call(this, 'design.body', { -// dropdown: '[data-test-input="bodyFontCategory"]' -// }); - -// await doCheck.call(this, 'design.body', { -// toggle: '#show-feature-image' -// }); - -// // Design footer -// await doCheck('design.footer', { -// input: '[contenteditable="true"]' -// }); -// }); -// }); - -// describe('Archiving newsletters', function () { -// it('can archive newsletters', async function () { -// // Create an extra newsletter, because we cannot archive the last one -// this.server.create('newsletter', {status: 'active', name: 'test newsletter', slug: 'test-newsletter'}); -// await visit('/settings/newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(2); - -// // Toggle is hidden -// expect(find('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger')).not.to.exist; - -// await click('[data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="archive-newsletter"]'); - -// // Check if confimation modal opens -// expect(find('[data-test-modal="confirm-newsletter-archive"]'), 'Archive newsletter modal').to.exist; - -// // Confirm archive -// await click('[data-test-button="confirm-newsletter-archive"]'); - -// // Check total newsletters equals 1 -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(1); - -// // Toggle is shown now -// expect(find('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger')).to.exist; -// }); - -// it('can reactivate newsletters if only archived newsletter left', async function () { -// // Create an extra newsletter, to check counts -// this.server.create('newsletter', {status: 'active', name: 'test newsletter', slug: 'test-newsletter'}); - -// // Create an archived newsletter, beacuse the toggle is invisible otherwise -// this.server.create('newsletter', {status: 'archived', name: 'test newsletter 2', slug: 'test-newsletter2'}); -// await visit('/settings/newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(2); - -// // Go to archived newsletters -// await click('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger'); -// await click('.ember-power-select-option[aria-selected="false"]'); - -// // Check title okay -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Archived newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total archived newsletters shown').to.equal(1); - -// // Reactivate the newsletter -// await click('[data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="reactivate-newsletter"]'); - -// // Check if confimation modal opens -// expect(find('[data-test-modal="confirm-newsletter-reactivate"]'), 'Reactivate newsletter modal').to.exist; - -// // Confirm archive -// await click('[data-test-button="confirm-newsletter-reactivate"]'); - -// // Check automatically went back to all (because no newsletters archived) -// // Check title okay -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Active newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(3); -// }); - -// it('can reactivate newsletters', async function () { -// // Create an extra newsletter, to check counts -// this.server.create('newsletter', {status: 'active', name: 'test newsletter', slug: 'test-newsletter'}); - -// // Create an archived newsletter, beacuse the toggle is invisible otherwise -// this.server.create('newsletter', {status: 'archived', name: 'test newsletter 2', slug: 'test-newsletter2'}); -// this.server.create('newsletter', {status: 'archived', name: 'test newsletter 3', slug: 'test-newsletter3'}); -// await visit('/settings/newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(2); - -// // Go to archived newsletters -// await click('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger'); -// await click('.ember-power-select-option[aria-selected="false"]'); - -// // Check title okay -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Archived newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total archived newsletters shown').to.equal(2); - -// // Reactivate the newsletter -// await click('[data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="reactivate-newsletter"]'); - -// // Check if confimation modal opens -// expect(find('[data-test-modal="confirm-newsletter-reactivate"]'), 'Reactivate newsletter modal').to.exist; - -// // Confirm archive -// await click('[data-test-button="confirm-newsletter-reactivate"]'); - -// // Check still showing archived newsletters -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Archived newsletters'); - -// // Go to active newsletters -// await click('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger'); -// await click('.ember-power-select-option[aria-selected="false"]'); - -// // Check automatically went back to all (because no newsletters archived) -// // Check title okay -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Active newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(3); -// }); - -// it('checks limits when reactivating a newsletter', async function () { -// const config = this.server.db.configs.find(1); -// config.hostSettings = { -// limits: { -// newsletters: { -// max: 1, -// error: 'Your plan supports up to {{max}} newsletters. Please upgrade to add more.' -// } -// } -// }; -// this.server.db.configs.update(1, config); - -// // Create an archived newsletter, beacuse the toggle is invisible otherwise -// this.server.create('newsletter', {status: 'archived', name: 'test newsletter 2', slug: 'test-newsletter2'}); -// await visit('/settings/newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total newsletters shown').to.equal(1); - -// // Go to archived newsletters -// await click('[data-test-dropdown="newsletter-status-filter"] .ember-power-select-trigger'); -// await click('.ember-power-select-option[aria-selected="false"]'); - -// // Check title okay -// expect(find('.gh-newsletters .gh-expandable-title').textContent.trim(), 'Title').to.equal('Archived newsletters'); - -// // Check total newsletters shown -// expect(findAll('[data-test-newsletter]').length, 'Total archived newsletters shown').to.equal(1); - -// // Reactivate the newsletter -// await click('[data-test-newsletter-menu-trigger]'); -// await click('[data-test-button="reactivate-newsletter"]'); - -// // Check if confimation modal doesn't open -// expect(find('[data-test-modal="confirm-newsletter-reactivate"]'), 'Reactivate newsletter modal').not.to.exist; - -// // Check limits modal is shown -// expect(find('[data-test-modal="limits/multiple-newsletters"]'), 'Limits modal').to.exist; - -// // Check can close modal -// await click('[data-test-button="cancel-upgrade"]'); - -// // Check modal is closed -// expect(find('[data-test-modal="limits/multiple-newsletters"]'), 'Limits modal').not.to.exist; -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/slack-test.js b/ghost/admin/tests/acceptance/settings/slack-test.js deleted file mode 100644 index 830d1504520..00000000000 --- a/ghost/admin/tests/acceptance/settings/slack-test.js +++ /dev/null @@ -1,151 +0,0 @@ -// import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -// import {Response} from 'miragejs'; -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import {beforeEach, describe, it} from 'mocha'; -// import {blur, click, currentURL, fillIn, find, findAll, triggerEvent} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Integrations - Slack', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations/slack'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/slack'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/slack'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/slack'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it validates and saves slack settings properly', async function () { -// await visit('/settings/integrations/slack'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/slack'); - -// await fillIn('[data-test-slack-url-input]', 'notacorrecturl'); -// await click('[data-test-save-button]'); - -// expect(find('[data-test-error="slack-url"]').textContent.trim(), 'inline validation response') -// .to.equal('The URL must be in a format like https://hooks.slack.com/services/'); - -// // CMD-S shortcut works -// await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958430'); -// await fillIn('[data-test-slack-username-input]', 'SlackBot'); -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// let [newRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(newRequest.requestBody); - -// let urlResult = params.settings.findBy('key', 'slack_url').value; -// let usernameResult = params.settings.findBy('key', 'slack_username').value; - -// expect(urlResult).to.equal('https://hooks.slack.com/services/1275958430'); -// expect(usernameResult).to.equal('SlackBot'); -// expect(find('[data-test-error="slack-url"]'), 'inline validation response') -// .to.not.exist; - -// await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958430'); -// await click('[data-test-send-notification-button]'); - -// expect(findAll('.gh-notification').length, 'number of notifications').to.equal(1); -// expect(find('[data-test-error="slack-url"]'), 'inline validation response') -// .to.not.exist; - -// // modify model data or there will be no api call -// await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958431'); - -// this.server.put('/settings/', function () { -// return new Response(422, {}, { -// errors: [ -// { -// type: 'ValidationError', -// message: 'Test error' -// } -// ] -// }); -// }); - -// await click('.gh-notification .gh-notification-close'); -// await click('[data-test-send-notification-button]'); - -// // we shouldn't try to send the test request if the save fails -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// expect(lastRequest.url).to.not.match(/\/slack\/test/); -// expect(findAll('.gh-notification').length, 'check slack notification after api validation error').to.equal(0); -// }); - -// it('warns when leaving without saving', async function () { -// await visit('/settings/integrations/slack'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/slack'); - -// await fillIn('[data-test-slack-url-input]', 'https://hooks.slack.com/services/1275958430'); -// await blur('[data-test-slack-url-input]'); - -// await visit('/settings'); - -// expect(findAll('[data-test-modal="unsaved-settings"]').length, 'modal exists').to.equal(1); - -// // Leave without saving -// await click('[data-test-modal="unsaved-settings"] [data-test-leave-button]'); - -// expect(currentURL(), 'currentURL').to.equal('/settings'); - -// await visit('/settings/integrations/slack'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/slack'); - -// // settings were not saved -// expect( -// find('[data-test-slack-url-input]').textContent.trim(), -// 'Slack Webhook URL' -// ).to.equal(''); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/unsplash-test.js b/ghost/admin/tests/acceptance/settings/unsplash-test.js deleted file mode 100644 index 69032b48086..00000000000 --- a/ghost/admin/tests/acceptance/settings/unsplash-test.js +++ /dev/null @@ -1,132 +0,0 @@ -// import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import { -// beforeEach, -// describe, -// it -// } from 'mocha'; -// import {click, currentURL, find, findAll, triggerEvent} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Integrations - Unsplash', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations/unsplash'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/unsplash'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/unsplash'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/unsplash'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it can activate/deactivate', async function () { -// await visit('/settings/integrations/unsplash'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash'); - -// // it's enabled by default when settings is empty -// expect(find('[data-test-unsplash-checkbox]').checked, 'checked by default').to.be.true; - -// await click('[data-test-unsplash-checkbox]'); - -// expect(find('[data-test-unsplash-checkbox]').checked, 'unsplash checkbox').to.be.false; - -// // trigger a save -// await click('[data-test-save-button]'); - -// // server should now have an unsplash setting -// let [lastRequest] = this.server.pretender.handledRequests.slice(-1); -// let params = JSON.parse(lastRequest.requestBody); - -// expect(params.settings.findBy('key', 'unsplash').value).to.equal(false); - -// // save via CMD-S shortcut -// await click('[data-test-unsplash-checkbox]'); -// await triggerEvent('.gh-app', 'keydown', { -// keyCode: 83, // s -// metaKey: ctrlOrCmd === 'command', -// ctrlKey: ctrlOrCmd === 'ctrl' -// }); - -// // we've already saved in this test so there's no on-screen indication -// // that we've had another save, check the request was fired instead -// let [newRequest] = this.server.pretender.handledRequests.slice(-1); -// params = JSON.parse(newRequest.requestBody); - -// expect(find('[data-test-unsplash-checkbox]').checked, 'AMP checkbox').to.be.true; -// expect(params.settings.findBy('key', 'unsplash').value).to.equal(true); -// }); - -// it('warns when leaving without saving', async function () { -// await visit('/settings/integrations/unsplash'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash'); - -// // AMP is enabled by default -// expect(find('[data-test-unsplash-checkbox]').checked, 'AMP checkbox default').to.be.true; - -// await click('[data-test-unsplash-checkbox]'); - -// expect(find('[data-test-unsplash-checkbox]').checked, 'Unsplash checkbox').to.be.false; - -// await visit('/settings/labs'); - -// expect(findAll('[data-test-modal="unsaved-settings"]').length, 'unsaved changes modal exists').to.equal(1); - -// // Leave without saving -// await click('[data-test-modal="unsaved-settings"] [data-test-leave-button]'); - -// expect(currentURL(), 'currentURL after leave without saving').to.equal('/settings/labs'); - -// await visit('/settings/integrations/unsplash'); - -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/unsplash'); - -// // settings were not saved -// expect(find('[data-test-unsplash-checkbox]').checked, 'Unsplash checkbox').to.be.true; -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/settings/zapier-test.js b/ghost/admin/tests/acceptance/settings/zapier-test.js deleted file mode 100644 index 11b92a5b135..00000000000 --- a/ghost/admin/tests/acceptance/settings/zapier-test.js +++ /dev/null @@ -1,69 +0,0 @@ -// import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; -// import { -// beforeEach, -// describe, -// it -// } from 'mocha'; -// import {currentURL} from '@ember/test-helpers'; -// import {expect} from 'chai'; -// import {setupApplicationTest} from 'ember-mocha'; -// import {setupMirage} from 'ember-cli-mirage/test-support'; -// import {visit} from '../../helpers/visit'; - -// describe('Acceptance: Settings - Integrations - Zapier', function () { -// let hooks = setupApplicationTest(); -// setupMirage(hooks); - -// it('redirects to signin when not authenticated', async function () { -// await invalidateSession(); -// await visit('/settings/integrations/zapier'); - -// expect(currentURL(), 'currentURL').to.equal('/signin'); -// }); - -// it('redirects to home page when authenticated as contributor', async function () { -// let role = this.server.create('role', {name: 'Contributor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/zapier'); - -// expect(currentURL(), 'currentURL').to.equal('/posts'); -// }); - -// it('redirects to home page when authenticated as author', async function () { -// let role = this.server.create('role', {name: 'Author'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/zapier'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// it('redirects to home page when authenticated as editor', async function () { -// let role = this.server.create('role', {name: 'Editor'}); -// this.server.create('user', {roles: [role], slug: 'test-user'}); - -// await authenticateSession(); -// await visit('/settings/integrations/zapier'); - -// expect(currentURL(), 'currentURL').to.equal('/site'); -// }); - -// describe('when logged in', function () { -// beforeEach(async function () { -// let role = this.server.create('role', {name: 'Administrator'}); -// this.server.create('user', {roles: [role]}); - -// return await authenticateSession(); -// }); - -// it('it loads', async function () { -// await visit('/settings/integrations/zapier'); - -// // has correct url -// expect(currentURL(), 'currentURL').to.equal('/settings/integrations/zapier'); -// }); -// }); -// }); diff --git a/ghost/admin/tests/acceptance/signup-test.js b/ghost/admin/tests/acceptance/signup-test.js index 9d6ea83985f..b64c48a65ef 100644 --- a/ghost/admin/tests/acceptance/signup-test.js +++ b/ghost/admin/tests/acceptance/signup-test.js @@ -198,7 +198,7 @@ describe('Acceptance: Signup', function () { expect(find('.gh-alert-content').textContent).to.have.string('Invalid token'); }); - it('redirects with alert on non-existant or expired token', async function () { + it('redirects with alert on non-existent or expired token', async function () { this.server.get('/authentication/invitation', function () { return { invitation: [{valid: false}] diff --git a/ghost/admin/tests/acceptance/settings/tags-test.js b/ghost/admin/tests/acceptance/tags-test.js similarity index 99% rename from ghost/admin/tests/acceptance/settings/tags-test.js rename to ghost/admin/tests/acceptance/tags-test.js index bc4798ef810..bc8555c2dda 100644 --- a/ghost/admin/tests/acceptance/settings/tags-test.js +++ b/ghost/admin/tests/acceptance/tags-test.js @@ -5,7 +5,7 @@ import {click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember import {expect} from 'chai'; import {setupApplicationTest} from 'ember-mocha'; import {setupMirage} from 'ember-cli-mirage/test-support'; -import {visit} from '../../helpers/visit'; +import {visit} from '../helpers/visit'; describe('Acceptance: Tags', function () { let hooks = setupApplicationTest(); diff --git a/ghost/admin/tests/integration/components/gh-feature-flag-test.js b/ghost/admin/tests/integration/components/gh-feature-flag-test.js deleted file mode 100644 index 9d5f27047a7..00000000000 --- a/ghost/admin/tests/integration/components/gh-feature-flag-test.js +++ /dev/null @@ -1,44 +0,0 @@ -import Service from '@ember/service'; -import hbs from 'htmlbars-inline-precompile'; -import {click, find, render} from '@ember/test-helpers'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupRenderingTest} from 'ember-mocha'; - -const featureStub = Service.extend({ - testFlag: true -}); - -describe('Integration: Component: gh-feature-flag', function () { - setupRenderingTest(); - - beforeEach(function () { - this.owner.register('service:feature', featureStub); - }); - - it('renders properties correctly', async function () { - await render(hbs``); - expect(find('label').getAttribute('for')).to.equal(find('input[type="checkbox"]').id); - }); - - it('renders correctly when flag is set to true', async function () { - await render(hbs``); - expect(find('label input[type="checkbox"]').checked).to.be.true; - }); - - it('renders correctly when flag is set to false', async function () { - let feature = this.owner.lookup('service:feature'); - feature.set('testFlag', false); - - await render(hbs``); - expect(find('label input[type="checkbox"]').checked).to.be.false; - }); - - it('updates to reflect changes in flag property', async function () { - await render(hbs``); - expect(find('label input[type="checkbox"]').checked).to.be.true; - - await click('label'); - expect(find('label input[type="checkbox"]').checked).to.be.false; - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-file-uploader-test.js b/ghost/admin/tests/integration/components/gh-file-uploader-test.js deleted file mode 100644 index 2d50f0a6500..00000000000 --- a/ghost/admin/tests/integration/components/gh-file-uploader-test.js +++ /dev/null @@ -1,369 +0,0 @@ -import $ from 'jquery'; -import Pretender from 'pretender'; -import Service from '@ember/service'; -import ghostPaths from 'ghost-admin/utils/ghost-paths'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import {GENERIC_ERROR_MESSAGE} from 'ghost-admin/services/notifications'; -import {UnsupportedMediaTypeError} from 'ghost-admin/services/ajax'; -import {click, find, findAll, render, settled, triggerEvent} from '@ember/test-helpers'; -import {createFile, fileUpload} from '../../helpers/file-upload'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {run} from '@ember/runloop'; -import {setupRenderingTest} from 'ember-mocha'; - -const notificationsStub = Service.extend({ - showAPIError() { - // noop - to be stubbed - } -}); - -const stubSuccessfulUpload = function (server, delay = 0) { - server.post(`${ghostPaths().apiRoot}/images/`, function () { - return [200, {'Content-Type': 'application/json'}, '{"url":"/content/images/test.png"}']; - }, delay); -}; - -const stubFailedUpload = function (server, code, error, delay = 0) { - server.post(`${ghostPaths().apiRoot}/images/`, function () { - return [code, {'Content-Type': 'application/json'}, JSON.stringify({ - errors: [{ - type: error, - message: `Error: ${error}` - }] - })]; - }, delay); -}; - -describe('Integration: Component: gh-file-uploader', function () { - setupRenderingTest(); - - let server; - - beforeEach(function () { - server = new Pretender(); - this.set('uploadUrl', `${ghostPaths().apiRoot}/images/`); - - this.owner.register('service:notifications', notificationsStub); - }); - - afterEach(function () { - server.shutdown(); - }); - - it('renders', async function () { - await render(hbs``); - - expect(find('label').textContent.trim(), 'default label') - .to.equal('Select or drag-and-drop a file'); - }); - - it('allows file input "accept" attribute to be changed', async function () { - await render(hbs``); - expect( - find('input[type="file"]').getAttribute('accept'), - 'default "accept" attribute' - ).to.equal('text/csv'); - - await render(hbs``); - expect( - find('input[type="file"]').getAttribute('accept'), - 'specified "accept" attribute' - ).to.equal('application/zip'); - }); - - it('renders form with supplied label text', async function () { - this.set('labelText', 'My label'); - await render(hbs``); - - expect(find('label').textContent.trim(), 'label') - .to.equal('My label'); - }); - - it('generates request to supplied endpoint', async function () { - stubSuccessfulUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(server.handledRequests.length).to.equal(1); - expect(server.handledRequests[0].url).to.equal(`${ghostPaths().apiRoot}/images/`); - }); - - it('fires uploadSuccess action on successful upload', async function () { - let uploadSuccess = sinon.spy(); - this.set('uploadSuccess', uploadSuccess); - - stubSuccessfulUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(uploadSuccess.calledOnce).to.be.true; - expect(uploadSuccess.firstCall.args[0]).to.eql({url: '/content/images/test.png'}); - }); - - it('doesn\'t fire uploadSuccess action on failed upload', async function () { - let uploadSuccess = sinon.spy(); - this.set('uploadSuccess', uploadSuccess); - - stubFailedUpload(server, 500); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - await settled(); - expect(uploadSuccess.calledOnce).to.be.false; - }); - - it('fires fileSelected action on file selection', async function () { - let fileSelected = sinon.spy(); - this.set('fileSelected', fileSelected); - - stubSuccessfulUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(fileSelected.calledOnce).to.be.true; - expect(fileSelected.args[0]).to.not.be.empty; - }); - - it('fires uploadStarted action on upload start', async function () { - let uploadStarted = sinon.spy(); - this.set('uploadStarted', uploadStarted); - - stubSuccessfulUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(uploadStarted.calledOnce).to.be.true; - }); - - it('fires uploadFinished action on successful upload', async function () { - let uploadFinished = sinon.spy(); - this.set('uploadFinished', uploadFinished); - - stubSuccessfulUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(uploadFinished.calledOnce).to.be.true; - }); - - it('fires uploadFinished action on failed upload', async function () { - let uploadFinished = sinon.spy(); - this.set('uploadFinished', uploadFinished); - - stubFailedUpload(server); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(uploadFinished.calledOnce).to.be.true; - }); - - it('displays invalid file type error', async function () { - stubFailedUpload(server, 415, 'UnsupportedMediaTypeError'); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/); - expect(findAll('[data-test-upload-try-again-button]').length, 'reset button is displayed').to.equal(1); - expect(find('[data-test-upload-try-again-button]').textContent).to.equal('Try again'); - }); - - it('displays file too large for server error', async function () { - stubFailedUpload(server, 413, 'RequestEntityTooLargeError'); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/The file you uploaded was larger/); - }); - - it('handles file too large error directly from the web server', async function () { - server.post(`${ghostPaths().apiRoot}/images/`, function () { - return [413, {}, '']; - }); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/The file you uploaded was larger/); - }); - - it('displays other server-side error with message', async function () { - stubFailedUpload(server, 400, 'UnknownError'); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/Error: UnknownError/); - }); - - it('handles unknown failure', async function () { - server.post(`${ghostPaths().apiRoot}/images/`, function () { - return [500, {'Content-Type': 'application/json'}, '']; - }); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(new RegExp(GENERIC_ERROR_MESSAGE)); - }); - - it('triggers notifications.showAPIError for VersionMismatchError', async function () { - let showAPIError = sinon.spy(); - let notifications = this.owner.lookup('service:notifications'); - notifications.set('showAPIError', showAPIError); - - stubFailedUpload(server, 400, 'VersionMismatchError'); - - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(showAPIError.calledOnce).to.be.true; - }); - - it('doesn\'t trigger notifications.showAPIError for other errors', async function () { - let showAPIError = sinon.spy(); - let notifications = this.owner.lookup('service:notifications'); - notifications.set('showAPIError', showAPIError); - - stubFailedUpload(server, 400, 'UnknownError'); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(showAPIError.called).to.be.false; - }); - - it('can be reset after a failed upload', async function () { - stubFailedUpload(server, 400, 'UnknownError'); - await render(hbs``); - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - await click('[data-test-upload-try-again-button]'); - - expect(findAll('input[type="file"]').length).to.equal(1); - }); - - it('handles drag over/leave', async function () { - await render(hbs``); - - run(() => { - // eslint-disable-next-line new-cap - let dragover = $.Event('dragover', { - dataTransfer: { - files: [] - } - }); - $(find('.gh-image-uploader')).trigger(dragover); - }); - - await settled(); - - expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.true; - - await triggerEvent('.gh-image-uploader', 'dragleave'); - - expect(find('.gh-image-uploader').classList.contains('-drag-over'), 'has drag-over class').to.be.false; - }); - - it('triggers file upload on file drop', async function () { - let uploadSuccess = sinon.spy(); - // eslint-disable-next-line new-cap - let drop = $.Event('drop', { - dataTransfer: { - files: [createFile(['test'], {name: 'test.csv'})] - } - }); - - this.set('uploadSuccess', uploadSuccess); - - stubSuccessfulUpload(server); - await render(hbs``); - - run(() => { - $(find('.gh-image-uploader')).trigger(drop); - }); - - await settled(); - - expect(uploadSuccess.calledOnce).to.be.true; - expect(uploadSuccess.firstCall.args[0]).to.eql({url: '/content/images/test.png'}); - }); - - it('validates extension by default', async function () { - let uploadSuccess = sinon.spy(); - let uploadFailed = sinon.spy(); - - this.set('uploadSuccess', uploadSuccess); - this.set('uploadFailed', uploadFailed); - - stubSuccessfulUpload(server); - - await render(hbs``); - - await fileUpload('input[type="file"]', ['test'], {name: 'test.txt'}); - - expect(uploadSuccess.called).to.be.false; - expect(uploadFailed.calledOnce).to.be.true; - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/); - }); - - it('uploads if validate action supplied and returns true', async function () { - let validate = sinon.stub().returns(true); - let uploadSuccess = sinon.spy(); - - this.set('validate', validate); - this.set('uploadSuccess', uploadSuccess); - - stubSuccessfulUpload(server); - - await render(hbs``); - - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - await settled(); - - expect(validate.calledOnce).to.be.true; - expect(uploadSuccess.calledOnce).to.be.true; - }); - - it('skips upload and displays error if validate action supplied and doesn\'t return true', async function () { - let validate = sinon.stub().returns(new UnsupportedMediaTypeError()); - let uploadSuccess = sinon.spy(); - let uploadFailed = sinon.spy(); - - this.set('validate', validate); - this.set('uploadSuccess', uploadSuccess); - this.set('uploadFailed', uploadFailed); - - stubSuccessfulUpload(server); - - await render(hbs``); - - await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'}); - - expect(validate.calledOnce).to.be.true; - expect(uploadSuccess.called).to.be.false; - expect(uploadFailed.calledOnce).to.be.true; - expect(findAll('.failed').length, 'error message is displayed').to.equal(1); - expect(find('.failed').textContent).to.match(/The file type you uploaded is not supported/); - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-theme-table-test.js b/ghost/admin/tests/integration/components/gh-theme-table-test.js deleted file mode 100644 index cc274cf5cce..00000000000 --- a/ghost/admin/tests/integration/components/gh-theme-table-test.js +++ /dev/null @@ -1,158 +0,0 @@ -import hbs from 'htmlbars-inline-precompile'; -import {click, find, findAll, render} from '@ember/test-helpers'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupRenderingTest} from 'ember-mocha'; - -describe('Integration: Component: gh-theme-table', function () { - setupRenderingTest(); - - it('renders', async function () { - this.set('themes', [ - {name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true}, - {name: 'casper', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'source', package: {name: 'Source', version: '1.0.0'}}, - {name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}}, - {name: 'foo'} - ]); - - await render(hbs``); - - expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1); - expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(5); - - let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim()); - - expect(packageNames[0]).to.match(/Casper \(legacy\)/); - expect(packageNames[1]).to.match(/Daring\s+Active/); - expect(packageNames[2]).to.match(/foo/); - expect(packageNames[3]).to.match(/Lanyon/); - expect(packageNames[4]).to.match(/Source \(default\)/); - - expect( - find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'), - 'active theme is highlighted' - ).to.contain.text('Daring'); - - expect( - findAll('[data-test-button="activate"]').length, - 'non-active themes have an activate link' - ).to.equal(4); - - expect( - find('[data-test-theme-active="true"]').querySelector('[data-test-button="activate"]'), - 'active theme doesn\'t have an activate link' - ).to.not.exist; - }); - - it('has download button in actions dropdown for all themes', async function () { - const themes = [ - {name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true}, - {name: 'casper', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}}, - {name: 'foo'} - ]; - this.set('themes', themes); - - await render(hbs``); - - for (const theme of themes) { - await click(`[data-test-theme-id="${theme.name}"] [data-test-button="actions"]`); - expect( - find(`[data-test-actions-for="${theme.name}"] [data-test-button="download"]`) - ).to.exist; - } - }); - - it('has delete button for non-active, non-default, themes', async function () { - const themes = [ - {name: 'Daring', package: {name: 'Daring', version: '0.1.4'}}, - {name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}}, - {name: 'foo'} - ]; - this.set('themes', themes); - - await render(hbs``); - - for (const theme of themes) { - await click(`[data-test-theme-id="${theme.name}"] [data-test-button="actions"]`); - expect( - find(`[data-test-actions-for="${theme.name}"] [data-test-button="delete"]`) - ).to.exist; - } - }); - - it('does not show delete action for default themes', async function () { - const themes = [ - {name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true}, - {name: 'casper', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}}, - {name: 'foo'}, - {name: 'source', package: {name: 'Source', version: '1.0.0'}} - ]; - this.set('themes', themes); - - await render(hbs``); - - // Casper should not be deletable - await click(`[data-test-theme-id="casper"] [data-test-button="actions"]`); - expect(find('[data-test-actions-for="casper"]')).to.exist; - expect( - find(`[data-test-actions-for="casper"] [data-test-button="delete"]`) - ).to.not.exist; - - // Source should not be deletable - await click(`[data-test-theme-id="source"] [data-test-button="actions"]`); - expect(find('[data-test-actions-for="source"]')).to.exist; - expect( - find(`[data-test-actions-for="source"] [data-test-button="delete"]`) - ).to.not.exist; - }); - - it('does not show delete action for active theme', async function () { - const themes = [ - {name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true}, - {name: 'casper', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}}, - {name: 'foo'} - ]; - this.set('themes', themes); - - await render(hbs``); - - await click(`[data-test-theme-id="Daring"] [data-test-button="actions"]`); - expect(find('[data-test-actions-for="Daring"]')).to.exist; - expect( - find(`[data-test-actions-for="Daring"] [data-test-button="delete"]`) - ).to.not.exist; - }); - - it('displays folder names if there are duplicate package names', async function () { - this.set('themes', [ - {name: 'daring', package: {name: 'Daring', version: '0.1.4'}}, - {name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}}, - {name: 'source', package: {name: 'Source', version: '1.0.0'}}, - {name: 'casper', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'another', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'mine', package: {name: 'Casper', version: '1.3.1'}}, - {name: 'foo'} - ]); - - await render(hbs``); - - let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim()); - - expect( - packageNames, - 'themes are ordered by label, folder names shown for duplicates' - ).to.deep.equal([ - 'Casper (another)', - 'Casper (legacy)', - 'Casper (mine)', - 'Daring (daring)', - 'Daring (daring-0.1.5)', - 'foo', - 'Source (default)' - ]); - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-timezone-select-test.js b/ghost/admin/tests/integration/components/gh-timezone-select-test.js deleted file mode 100644 index 764aad73b80..00000000000 --- a/ghost/admin/tests/integration/components/gh-timezone-select-test.js +++ /dev/null @@ -1,68 +0,0 @@ -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import {blur, fillIn, find, findAll, render} from '@ember/test-helpers'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupRenderingTest} from 'ember-mocha'; - -describe('Integration: Component: gh-timezone-select', function () { - setupRenderingTest(); - - beforeEach(function () { - this.set('availableTimezones', [ - {name: 'Pacific/Pago_Pago', label: '(GMT -11:00) Midway Island, Samoa'}, - {name: 'Etc/UTC', label: '(GMT) UTC'}, - {name: 'Pacific/Kwajalein', label: '(GMT +12:00) International Date Line West'} - ]); - this.set('timezone', 'Etc/UTC'); - }); - - it('renders', async function () { - await render(hbs``); - - expect(this.element, 'top-level elements').to.exist; - expect(findAll('option'), 'number of options').to.have.length(3); - expect(find('select').value, 'selected option value').to.equal('Etc/UTC'); - }); - - it('handles an unknown timezone', async function () { - this.set('timezone', 'Europe/London'); - - await render(hbs``); - - // we have an additional blank option at the top - expect(findAll('option'), 'number of options').to.have.length(4); - // blank option is selected - expect(find('select').value, 'selected option value').to.equal(''); - // we indicate the manual override - expect(find('p').textContent).to.match(/Your timezone has been automatically set to Europe\/London/); - }); - - it('triggers update action on change', async function () { - let update = sinon.spy(); - this.set('update', update); - - await render(hbs``); - - await fillIn('select', 'Pacific/Pago_Pago'); - await blur('select'); - - expect(update.calledOnce, 'update was called once').to.be.true; - expect(update.firstCall.args[0].name, 'update was passed new timezone') - .to.equal('Pacific/Pago_Pago'); - }); - - // TODO: mock clock service, fake the time, test we have the correct - // local time and it changes alongside selection changes - it('renders local time'); -}); diff --git a/ghost/admin/tests/integration/components/gh-unsplash-test.js b/ghost/admin/tests/integration/components/gh-unsplash-test.js index f5b1631de79..916b2e1f03a 100644 --- a/ghost/admin/tests/integration/components/gh-unsplash-test.js +++ b/ghost/admin/tests/integration/components/gh-unsplash-test.js @@ -37,7 +37,7 @@ describe('Integration: Component: gh-unsplash', function () { describe('closing', function () { it('triggers close action'); - it('can be triggerd by escape key'); + it('can be triggered by escape key'); it('cannot be triggered by escape key when zoomed'); }); }); diff --git a/ghost/admin/tests/integration/components/settings/navigation/nav-item-test.js b/ghost/admin/tests/integration/components/settings/navigation/nav-item-test.js deleted file mode 100644 index 13acf318eca..00000000000 --- a/ghost/admin/tests/integration/components/settings/navigation/nav-item-test.js +++ /dev/null @@ -1,123 +0,0 @@ -import NavItem from 'ghost-admin/models/navigation-item'; -import hbs from 'htmlbars-inline-precompile'; -import {click, find, render, triggerEvent} from '@ember/test-helpers'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupRenderingTest} from 'ember-mocha'; - -describe('Integration: Component: settings/navigation/nav-item', function () { - setupRenderingTest(); - - beforeEach(function () { - this.set('baseUrl', 'http://localhost:2368'); - }); - - it('renders', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url'})); - - await render(hbs``); - let item = find('.gh-blognav-item'); - - expect(item.querySelector('.gh-blognav-grab'), 'grab').to.exist; - expect(item.querySelector('.gh-blognav-label'), 'label').to.exist; - expect(item.querySelector('.gh-blognav-url'), 'url').to.exist; - expect(item.querySelector('.gh-blognav-delete'), 'delete').to.exist; - - // doesn't show any errors - expect(find('.gh-blognav-item--error')).to.not.exist; - expect(find('.error')).to.not.exist; - expect(find('.response')).to.not.be.displayed; - }); - - it('doesn\'t show drag handle for new items', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true})); - - await render(hbs``); - let item = find('.gh-blognav-item'); - - expect(item.querySelector('.gh-blognav-grab')).to.not.exist; - }); - - it('shows add button for new items', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true})); - - await render(hbs``); - let item = find('.gh-blognav-item'); - - expect(item.querySelector('.gh-blognav-add')).to.exist; - expect(item.querySelector('.gh-blognav-delete')).to.not.exist; - }); - - it('triggers delete action', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url'})); - - let deleteActionCallCount = 0; - this.set('deleteItem', (navItem) => { - expect(navItem).to.equal(this.navItem); - deleteActionCallCount += 1; - }); - - await render(hbs``); - await click('.gh-blognav-delete'); - - expect(deleteActionCallCount).to.equal(1); - }); - - it('triggers add action', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url', isNew: true})); - - let addActionCallCount = 0; - this.set('add', () => { - addActionCallCount += 1; - }); - - await render(hbs``); - await click('.gh-blognav-add'); - - expect(addActionCallCount).to.equal(1); - }); - - it('triggers update url action', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url'})); - - let updateActionCallCount = 0; - this.set('update', (value) => { - updateActionCallCount += 1; - return value; - }); - - await render(hbs``); - await triggerEvent('.gh-blognav-url input', 'blur'); - - expect(updateActionCallCount).to.equal(1); - }); - - it('triggers update label action', async function () { - this.set('navItem', NavItem.create({label: 'Test', url: '/url'})); - - let updateActionCallCount = 0; - this.set('update', (value) => { - updateActionCallCount += 1; - return value; - }); - - await render(hbs``); - await triggerEvent('.gh-blognav-label input', 'blur'); - - expect(updateActionCallCount).to.equal(2); - }); - - it('displays inline errors', async function () { - this.set('navItem', NavItem.create({label: '', url: ''})); - this.navItem.validate(); - - await render(hbs``); - let item = find('.gh-blognav-item'); - - expect(item).to.have.class('gh-blognav-item--error'); - expect(item.querySelector('.gh-blognav-label')).to.have.class('error'); - expect(item.querySelector('.gh-blognav-label .response')).to.have.trimmed.text('You must specify a label'); - expect(item.querySelector('.gh-blognav-url')).to.have.class('error'); - expect(item.querySelector('.gh-blognav-url .response')).to.have.trimmed.text('You must specify a URL or relative path'); - }); -}); diff --git a/ghost/admin/tests/integration/components/settings/navigation/nav-item-url-input-test.js b/ghost/admin/tests/integration/components/settings/navigation/nav-item-url-input-test.js deleted file mode 100644 index 8e8453e7f7a..00000000000 --- a/ghost/admin/tests/integration/components/settings/navigation/nav-item-url-input-test.js +++ /dev/null @@ -1,408 +0,0 @@ -import hbs from 'htmlbars-inline-precompile'; -import {blur, click, fillIn, find, findAll, render, triggerKeyEvent} from '@ember/test-helpers'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupRenderingTest} from 'ember-mocha'; - -// we want baseUrl to match the running domain so relative URLs are -// handled as expected (browser auto-sets the domain when using a.href) -let currentUrl = `${window.location.protocol}//${window.location.host}/`; - -describe('Integration: Component: settings/navigation/nav-item-url-input', function () { - setupRenderingTest(); - - beforeEach(function () { - // set defaults - this.set('baseUrl', currentUrl); - this.set('url', ''); - this.set('isNew', false); - this.set('clearErrors', function () { - return null; - }); - }); - - it('renders correctly with blank url', async function () { - await render(hbs` - - `); - - expect(findAll('input')).to.have.length(1); - expect(find('input')).to.have.class('gh-input'); - expect(find('input')).to.have.value(currentUrl); - }); - - it('renders correctly with relative urls', async function () { - this.set('url', '/about'); - await render(hbs` - - `); - - expect(find('input')).to.have.value(`${currentUrl}about`); - - this.set('url', '/about#contact'); - expect(find('input')).to.have.value(`${currentUrl}about#contact`); - }); - - it('renders correctly with absolute urls', async function () { - this.set('url', 'https://example.com:2368/#test'); - await render(hbs` - - `); - - expect(find('input')).to.have.value('https://example.com:2368/#test'); - - this.set('url', 'mailto:test@example.com'); - expect(find('input')).to.have.value('mailto:test@example.com'); - - this.set('url', 'tel:01234-5678-90'); - expect(find('input')).to.have.value('tel:01234-5678-90'); - - this.set('url', '//protocol-less-url.com'); - expect(find('input')).to.have.value('//protocol-less-url.com'); - - this.set('url', '#anchor'); - expect(find('input')).to.have.value('#anchor'); - }); - - it('deletes base URL on backspace', async function () { - await render(hbs` - - `); - - expect(find('input')).to.have.value(currentUrl); - await triggerKeyEvent('input', 'keydown', 8); - expect(find('input')).to.have.value(''); - }); - - it('deletes base URL on delete', async function () { - await render(hbs` - - `); - - expect(find('input')).to.have.value(currentUrl); - await triggerKeyEvent('input', 'keydown', 46); - expect(find('input')).to.have.value(''); - }); - - it('adds base url to relative urls on blur', async function () { - this.set('updateUrl', val => val); - await render(hbs` - - `); - - await fillIn('input', '/about'); - await blur('input'); - - expect(find('input')).to.have.value(`${currentUrl}about/`); - }); - - it('adds "mailto:" to email addresses on blur', async function () { - this.set('updateUrl', val => val); - await render(hbs` - - `); - - await fillIn('input', 'test@example.com'); - await blur('input'); - - expect(find('input')).to.have.value('mailto:test@example.com'); - - // ensure we don't double-up on the mailto: - await blur('input'); - expect(find('input')).to.have.value('mailto:test@example.com'); - }); - - it('doesn\'t add base url to invalid urls on blur', async function () { - this.set('updateUrl', val => val); - await render(hbs` - - `); - - let changeValue = async (value) => { - await fillIn('input', value); - await blur('input'); - }; - - await changeValue('with spaces'); - expect(find('input')).to.have.value('with spaces'); - - await changeValue('/with spaces'); - expect(find('input')).to.have.value('/with spaces'); - }); - - it('doesn\'t mangle invalid urls on blur', async function () { - this.set('updateUrl', val => val); - await render(hbs` - - `); - - await fillIn('input', `${currentUrl} /test`); - await blur('input'); - - expect(find('input')).to.have.value(`${currentUrl} /test`); - }); - - // https://github.com/TryGhost/Ghost/issues/9373 - it('doesn\'t mangle urls when baseUrl has unicode characters', async function () { - this.set('updateUrl', val => val); - - this.set('baseUrl', 'http://exämple.com'); - - await render(hbs` - - `); - await fillIn('input', `${currentUrl}/test`); - await blur('input'); - - expect(find('input')).to.have.value(`${currentUrl}/test`); - }); - - it('triggers "update" action on blur', async function () { - let changeActionCallCount = 0; - this.set('updateUrl', (val) => { - changeActionCallCount += 1; - return val; - }); - - await render(hbs ` - - `); - await click('input'); - await blur('input'); - - expect(changeActionCallCount).to.equal(1); - }); - - it('triggers "update" action on enter', async function () { - let changeActionCallCount = 0; - this.set('updateUrl', (val) => { - changeActionCallCount += 1; - return val; - }); - - await render(hbs ` - - `); - await triggerKeyEvent('input', 'keypress', 13); - - expect(changeActionCallCount).to.equal(1); - }); - - it('triggers "update" action on CMD-S', async function () { - let changeActionCallCount = 0; - this.set('updateUrl', (val) => { - changeActionCallCount += 1; - return val; - }); - - await render(hbs ` - - `); - await triggerKeyEvent('input', 'keydown', 83, { - metaKey: true - }); - - expect(changeActionCallCount).to.equal(1); - }); - - it('sends absolute urls straight through to update action', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', url); - await blur('input'); - expect(lastSeenUrl).to.equal(url); - }; - - await testUrl('http://example.com'); - await testUrl('http://example.com/'); - await testUrl('https://example.com'); - await testUrl('//example.com'); - await testUrl('//localhost:1234'); - await testUrl('#anchor'); - await testUrl('mailto:test@example.com'); - await testUrl('tel:12345-567890'); - await testUrl('javascript:alert("testing");'); - }); - - it('strips base url from relative urls before sending to update action', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', `${currentUrl}${url}`); - await blur('input'); - expect(lastSeenUrl).to.equal(`/${url}`); - }; - - await testUrl('about/'); - await testUrl('about#contact'); - await testUrl('test/nested/'); - }); - - it('handles links to subdomains of blog domain', async function () { - let expectedUrl = ''; - - this.set('baseUrl', 'http://example.com/'); - - this.set('updateUrl', (url) => { - expect(url).to.equal(expectedUrl); - return url; - }); - - await render(hbs ` - - `); - - expectedUrl = 'http://test.example.com/'; - await fillIn('input', expectedUrl); - await blur('input'); - expect(find('input')).to.have.value(expectedUrl); - }); - - it('adds trailing slash to relative URL', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', `${currentUrl}${url}`); - await blur('input'); - expect(lastSeenUrl).to.equal(`/${url}/`); - }; - - await testUrl('about'); - await testUrl('test/nested'); - }); - - it('does not add trailing slash on relative URL with [.?#]', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', `${currentUrl}${url}`); - await blur('input'); - expect(lastSeenUrl).to.equal(`/${url}`); - }; - - await testUrl('about#contact'); - await testUrl('test/nested.svg'); - await testUrl('test?gho=sties'); - await testUrl('test/nested?sli=mer'); - }); - - it('does not add trailing slash on non-relative URLs', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', url); - await blur('input'); - expect(lastSeenUrl).to.equal(url); - }; - - await testUrl('http://woo.ff/test'); - await testUrl('http://me.ow:2342/nested/test'); - await testUrl('https://wro.om/car#race'); - await testUrl('https://kabo.om/explosion?really=now'); - }); - - describe('with sub-folder baseUrl', function () { - beforeEach(function () { - this.set('baseUrl', `${currentUrl}blog/`); - }); - - it('handles URLs relative to base url', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', `${currentUrl}blog${url}`); - await blur('input'); - expect(lastSeenUrl).to.equal(url); - }; - - await testUrl('/about/'); - await testUrl('/about#contact'); - await testUrl('/test/nested/'); - }); - - it('handles URLs relative to base host', async function () { - let lastSeenUrl = ''; - - this.set('updateUrl', (url) => { - lastSeenUrl = url; - return url; - }); - - await render(hbs ` - - `); - - let testUrl = async (url) => { - await fillIn('input', url); - await blur('input'); - expect(lastSeenUrl).to.equal(url); - }; - - await testUrl(`http://${window.location.host}`); - await testUrl(`https://${window.location.host}`); - await testUrl(`http://${window.location.host}/`); - await testUrl(`https://${window.location.host}/`); - await testUrl(`http://${window.location.host}/test`); - await testUrl(`https://${window.location.host}/test`); - await testUrl(`http://${window.location.host}/#test`); - await testUrl(`https://${window.location.host}/#test`); - await testUrl(`http://${window.location.host}/another/folder`); - await testUrl(`https://${window.location.host}/another/folder`); - }); - }); -}); diff --git a/ghost/admin/tests/unit/controllers/settings/design-test.js b/ghost/admin/tests/unit/controllers/settings/design-test.js deleted file mode 100644 index 543a0a2ac42..00000000000 --- a/ghost/admin/tests/unit/controllers/settings/design-test.js +++ /dev/null @@ -1,159 +0,0 @@ -import EmberObject from '@ember/object'; -import NavItem from 'ghost-admin/models/navigation-item'; -import {assert, expect} from 'chai'; -import {describe, it} from 'mocha'; -import {run} from '@ember/runloop'; -import {setupTest} from 'ember-mocha'; - -// const navSettingJSON = `[ -// {"label":"Home","url":"/"}, -// {"label":"JS Test","url":"javascript:alert('hello');"}, -// {"label":"About","url":"/about"}, -// {"label":"Sub Folder","url":"/blah/blah"}, -// {"label":"Telephone","url":"tel:01234-567890"}, -// {"label":"Mailto","url":"mailto:test@example.com"}, -// {"label":"External","url":"https://example.com/testing?query=test#anchor"}, -// {"label":"No Protocol","url":"//example.com"} -// ]`; - -describe.skip('Unit: Controller: settings/design', function () { - setupTest(); - - it('blogUrl: captures config and ensures trailing slash', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - ctrl.set('config.blogUrl', 'http://localhost:2368/blog'); - expect(ctrl.get('blogUrl')).to.equal('http://localhost:2368/blog/'); - }); - - it('init: creates a new navigation item', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - - run(() => { - expect(ctrl.get('newNavItem')).to.exist; - expect(ctrl.get('newNavItem.isNew')).to.be.true; - }); - }); - - it('blogUrl: captures config and ensures trailing slash', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - ctrl.set('config.blogUrl', 'http://localhost:2368/blog'); - expect(ctrl.get('blogUrl')).to.equal('http://localhost:2368/blog/'); - }); - - it('save: validates nav items', function (done) { - let ctrl = this.owner.lookup('controller:settings/design'); - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: [ - NavItem.create({label: 'First', url: '/'}), - NavItem.create({label: '', url: '/second'}), - NavItem.create({label: 'Third', url: ''}) - ]})); - // blank item won't get added because the last item is incomplete - expect(ctrl.settings.navigation.length).to.equal(3); - - ctrl.get('save').perform().then(function passedValidation() { - assert(false, 'navigationItems weren\'t validated on save'); - done(); - }).catch(function failedValidation() { - let navItems = ctrl.settings.navigation; - expect(navItems[0].get('errors').toArray()).to.be.empty; - expect(navItems[1].get('errors.firstObject.attribute')).to.equal('label'); - expect(navItems[2].get('errors.firstObject.attribute')).to.equal('url'); - done(); - }); - }); - }); - - it('save: ignores blank last item when saving', function (done) { - let ctrl = this.owner.lookup('controller:settings/design'); - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: [ - NavItem.create({label: 'First', url: '/'}), - NavItem.create({label: '', url: ''}) - ]})); - - expect(ctrl.settings.navigation.length).to.equal(2); - - ctrl.get('save').perform().then(function passedValidation() { - assert(false, 'navigationItems weren\'t validated on save'); - done(); - }).catch(function failedValidation() { - let navItems = ctrl.settings.navigation; - expect(navItems[0].get('errors').toArray()).to.be.empty; - done(); - }); - }); - }); - - it('action - addNavItem: adds item to navigationItems', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: [ - NavItem.create({label: 'First', url: '/first', last: true}) - ]})); - }); - - expect(ctrl.settings.navigation.length).to.equal(1); - - ctrl.set('newNavItem.label', 'New'); - ctrl.set('newNavItem.url', '/new'); - - run(() => { - ctrl.send('addNavItem', ctrl.get('newNavItem')); - }); - - expect(ctrl.settings.navigation.length).to.equal(2); - expect(ctrl.settings.navigation.lastObject.label).to.equal('New'); - expect(ctrl.settings.navigation.lastObject.url).to.equal('/new'); - expect(ctrl.settings.navigation.lastObject.isNew).to.be.false; - expect(ctrl.get('newNavItem.label')).to.be.empty; - expect(ctrl.get('newNavItem.url')).to.be.empty; - expect(ctrl.get('newNavItem.isNew')).to.be.true; - }); - - it('action - addNavItem: doesn\'t insert new item if last object is incomplete', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: [ - NavItem.create({label: '', url: '', last: true}) - ]})); - expect(ctrl.settings.navigation.length).to.equal(1); - ctrl.send('addNavItem', ctrl.settings.navigation.lastObject); - expect(ctrl.settings.navigation.length).to.equal(1); - }); - }); - - it('action - deleteNavItem: removes item from navigationItems', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - let navItems = [ - NavItem.create({label: 'First', url: '/first'}), - NavItem.create({label: 'Second', url: '/second', last: true}) - ]; - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: navItems})); - expect(ctrl.settings.navigation.mapBy('label')).to.deep.equal(['First', 'Second']); - ctrl.send('deleteNavItem', ctrl.settings.navigation.firstObject); - expect(ctrl.settings.navigation.mapBy('label')).to.deep.equal(['Second']); - }); - }); - - it('action - updateUrl: updates URL on navigationItem', function () { - let ctrl = this.owner.lookup('controller:settings/design'); - let navItems = [ - NavItem.create({label: 'First', url: '/first'}), - NavItem.create({label: 'Second', url: '/second', last: true}) - ]; - - run(() => { - ctrl.set('settings', EmberObject.create({navigation: navItems})); - expect(ctrl.settings.navigation.mapBy('url')).to.deep.equal(['/first', '/second']); - ctrl.send('updateUrl', '/new', ctrl.settings.navigation.firstObject); - expect(ctrl.settings.navigation.mapBy('url')).to.deep.equal(['/new', '/second']); - }); - }); -}); diff --git a/ghost/api-framework/package.json b/ghost/api-framework/package.json index 6c0e5ce0963..0310d0ca8ff 100644 --- a/ghost/api-framework/package.json +++ b/ghost/api-framework/package.json @@ -24,11 +24,11 @@ "sinon": "15.2.0" }, "dependencies": { - "@tryghost/debug": "0.1.24", + "@tryghost/debug": "0.1.26", "@tryghost/errors": "1.2.26", - "@tryghost/promise": "0.3.4", - "@tryghost/tpl": "0.1.24", - "@tryghost/validator": "0.2.4", + "@tryghost/promise": "0.3.6", + "@tryghost/tpl": "0.1.26", + "@tryghost/validator": "0.2.6", "jsonpath": "1.1.1", "lodash": "4.17.21" } diff --git a/ghost/audience-feedback/package.json b/ghost/audience-feedback/package.json index 1b4569ba923..499c563f25e 100644 --- a/ghost/audience-feedback/package.json +++ b/ghost/audience-feedback/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@tryghost/errors": "1.2.26", - "@tryghost/tpl": "0.1.24", + "@tryghost/tpl": "0.1.26", "bson-objectid": "2.0.4" } } diff --git a/ghost/collections/package.json b/ghost/collections/package.json index fd2d825e72f..04596d0940b 100644 --- a/ghost/collections/package.json +++ b/ghost/collections/package.json @@ -33,7 +33,7 @@ "@tryghost/nql": "0.11.0", "@tryghost/nql-filter-expansions": "0.0.0", "@tryghost/post-events": "0.0.0", - "@tryghost/tpl": "0.1.25", + "@tryghost/tpl": "0.1.26", "bson-objectid": "2.0.4", "lodash": "4.17.21" }, diff --git a/ghost/core/content/themes/source b/ghost/core/content/themes/source index ea08ce4e9dd..946e0631178 160000 --- a/ghost/core/content/themes/source +++ b/ghost/core/content/themes/source @@ -1 +1 @@ -Subproject commit ea08ce4e9dd6f67cdaae216bcc5b55a6490efc1d +Subproject commit 946e06311787f864cd347678fb9012f6d3abab22 diff --git a/ghost/core/core/frontend/apps/private-blogging/lib/views/private.hbs b/ghost/core/core/frontend/apps/private-blogging/lib/views/private.hbs index 87670059fbb..4d662e50aab 100644 --- a/ghost/core/core/frontend/apps/private-blogging/lib/views/private.hbs +++ b/ghost/core/core/frontend/apps/private-blogging/lib/views/private.hbs @@ -5,8 +5,35 @@ - {{@site.title}} - Private Site Access + {{@site.title}} + + + + + + + + + {{#if @site.icon}} + + {{/if}} + + {{#if @site.description}} + + + + {{/if}} + + {{#if @site.cover_image}} + + + {{/if}} + + + + + diff --git a/ghost/core/core/frontend/web/middleware/static-theme.js b/ghost/core/core/frontend/web/middleware/static-theme.js index e2a6d393faf..e3d5af00e7a 100644 --- a/ghost/core/core/frontend/web/middleware/static-theme.js +++ b/ghost/core/core/frontend/web/middleware/static-theme.js @@ -45,7 +45,7 @@ function isAllowedFile(file) { const normalizedFilePath = path.normalize(decodedFilePath); - const allowedFiles = ['manifest.json']; + const allowedFiles = ['manifest.json', 'assetlinks.json']; const allowedPath = '/assets/'; const alwaysDeny = ['.hbs']; diff --git a/ghost/core/core/server/api/endpoints/recommendations.js b/ghost/core/core/server/api/endpoints/recommendations.js index 368156f1200..afad5b4d658 100644 --- a/ghost/core/core/server/api/endpoints/recommendations.js +++ b/ghost/core/core/server/api/endpoints/recommendations.js @@ -48,6 +48,24 @@ module.exports = { } }, + /** + * Fetch metadata for a recommendation URL + */ + check: { + headers: { + cacheInvalidate: true + }, + options: [], + validation: {}, + permissions: { + // Everyone who has add permissions, can 'check' + method: 'add' + }, + async query(frame) { + return await recommendations.controller.check(frame); + } + }, + edit: { headers: { cacheInvalidate: true diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js index b14f7dcda12..06b4681e695 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js @@ -133,11 +133,23 @@ module.exports = { const html = frame.data.pages[0].html; if (frame.options.source === 'html' && !_.isEmpty(html)) { + if (process.env.CI) { + console.time('htmlToMobiledocConverter (page)'); // eslint-disable-line no-console + } frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); + if (process.env.CI) { + console.timeEnd('htmlToMobiledocConverter (page)'); // eslint-disable-line no-console + } // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion + if (process.env.CI) { + console.time('htmlToLexicalConverter (page)'); // eslint-disable-line no-console + } frame.data.pages[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html)); + if (process.env.CI) { + console.timeEnd('htmlToLexicalConverter (page)'); // eslint-disable-line no-console + } } } diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js index ff761f36e71..faf968afff6 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js @@ -167,11 +167,23 @@ module.exports = { const html = frame.data.posts[0].html; if (frame.options.source === 'html' && !_.isEmpty(html)) { + if (process.env.CI) { + console.time('htmlToMobiledocConverter (post)'); // eslint-disable-line no-console + } frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); + if (process.env.CI) { + console.timeEnd('htmlToMobiledocConverter (post)'); // eslint-disable-line no-console + } // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion + if (process.env.CI) { + console.time('htmlToLexicalConverter (post)'); // eslint-disable-line no-console + } frame.data.posts[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html)); + if (process.env.CI) { + console.timeEnd('htmlToLexicalConverter (post)'); // eslint-disable-line no-console + } } } diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/posts.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/posts.js index 14bedfe60f0..5db08da6223 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/posts.js @@ -77,6 +77,7 @@ module.exports = async (model, frame, options = {}) => { if (utils.isContentAPI(frame)) { date.forPost(jsonModel); gating.forPost(jsonModel, frame); + if (jsonModel.access) { if (commentsService?.api?.enabled !== 'off') { jsonModel.comments = true; @@ -87,6 +88,10 @@ module.exports = async (model, frame, options = {}) => { jsonModel.comments = false; } + // Strip any source formats + delete jsonModel.mobiledoc; + delete jsonModel.lexical; + // Add outbound link tags if (labs.isSet('outboundLinkTagging')) { // Only add it in the flag! Without the flag we only add it to emails. diff --git a/ghost/core/core/server/data/migrations/utils/schema.js b/ghost/core/core/server/data/migrations/utils/schema.js index 5dee3f7bdf2..d72215749af 100644 --- a/ghost/core/core/server/data/migrations/utils/schema.js +++ b/ghost/core/core/server/data/migrations/utils/schema.js @@ -96,6 +96,22 @@ function createSetNullableMigration(table, column, options = {}) { ); } +/** + * @param {string} table + * @param {string[]|string} columns One or multiple columns (in case the index should be for multiple columns) + * @returns {Migration} + */ +function createAddIndexMigration(table, columns) { + return createTransactionalMigration( + async function up(knex) { + await commands.addIndex(table, columns, knex); + }, + async function down(knex) { + await commands.dropIndex(table, columns, knex); + } + ); +} + /** * @param {string} table * @param {string} from @@ -163,7 +179,8 @@ module.exports = { createDropColumnMigration, createSetNullableMigration, createDropNullableMigration, - createRenameColumnMigration + createRenameColumnMigration, + createAddIndexMigration }; /** diff --git a/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-00-members-created-attribution-id-index.js b/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-00-members-created-attribution-id-index.js new file mode 100644 index 00000000000..9bcbb4cc0b3 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-00-members-created-attribution-id-index.js @@ -0,0 +1,3 @@ +const {createAddIndexMigration} = require('../../utils'); + +module.exports = createAddIndexMigration('members_created_events', ['attribution_id']); diff --git a/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-01-members-subscription-created-attribution-id-index.js b/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-01-members-subscription-created-attribution-id-index.js new file mode 100644 index 00000000000..346ea18886e --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.72/2023-10-31-11-06-01-members-subscription-created-attribution-id-index.js @@ -0,0 +1,3 @@ +const {createAddIndexMigration} = require('../../utils'); + +module.exports = createAddIndexMigration('members_subscription_created_events', ['attribution_id']); diff --git a/ghost/core/core/server/data/schema/commands.js b/ghost/core/core/server/data/schema/commands.js index c8b804dc93f..252225082df 100644 --- a/ghost/core/core/server/data/schema/commands.js +++ b/ghost/core/core/server/data/schema/commands.js @@ -175,6 +175,60 @@ async function renameColumn(tableName, from, to, transaction = db.knex) { }); } +/** + * Adds an non-unique index to a table over the given columns. + * + * @param {string} tableName - name of the table to add indexes to + * @param {string|string[]} columns - column(s) to add indexes for + * @param {import('knex').Knex} [transaction] - connection object containing knex reference + */ +async function addIndex(tableName, columns, transaction = db.knex) { + try { + logging.info(`Adding index for '${columns}' in table '${tableName}'`); + + return await transaction.schema.table(tableName, function (table) { + table.index(columns); + }); + } catch (err) { + if (err.code === 'SQLITE_ERROR') { + logging.warn(`Index for '${columns}' already exists for table '${tableName}'`); + return; + } + if (err.code === 'ER_DUP_KEYNAME') { + logging.warn(`Index for '${columns}' already exists for table '${tableName}'`); + return; + } + throw err; + } +} + +/** + * Drops a non-unique index from a table over the given columns. + * + * @param {string} tableName - name of the table to remove indexes from + * @param {string|string[]} columns - column(s) to remove indexes for + * @param {import('knex').Knex} [transaction] - connection object containing knex reference + */ +async function dropIndex(tableName, columns, transaction = db.knex) { + try { + logging.info(`Dropping index for '${columns}' in table '${tableName}'`); + + return await transaction.schema.table(tableName, function (table) { + table.dropIndex(columns); + }); + } catch (err) { + if (err.code === 'SQLITE_ERROR') { + logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`); + return; + } + if (err.code === 'ER_CANT_DROP_FIELD_OR_KEY') { + logging.warn(`Constraint for '${columns}' does not exist for table '${tableName}'`); + return; + } + throw err; + } +} + /** * Adds an unique index to a table over the given columns. * @@ -535,6 +589,8 @@ module.exports = { getIndexes, addUnique, dropUnique, + addIndex, + dropIndex, addPrimaryKey, addForeign, dropForeign, diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index 29a748ecacc..2fbe4699732 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -523,7 +523,7 @@ module.exports = { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, created_at: {type: 'dateTime', nullable: false}, member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true}, - attribution_id: {type: 'string', maxlength: 24, nullable: true}, + attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true}, attribution_type: { type: 'string', maxlength: 50, nullable: true, validations: { isIn: [['url', 'post', 'page', 'author', 'tag']] @@ -709,7 +709,7 @@ module.exports = { created_at: {type: 'dateTime', nullable: false}, member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true}, subscription_id: {type: 'string', maxlength: 24, nullable: false, references: 'members_stripe_customers_subscriptions.id', cascadeDelete: true}, - attribution_id: {type: 'string', maxlength: 24, nullable: true}, + attribution_id: {type: 'string', maxlength: 24, nullable: true, index: true}, attribution_type: { type: 'string', maxlength: 50, nullable: true, validations: { isIn: [['url', 'post', 'page', 'author', 'tag']] diff --git a/ghost/core/core/server/lib/lexical.js b/ghost/core/core/server/lib/lexical.js index 7fd5730efe1..8363e51aa4f 100644 --- a/ghost/core/core/server/lib/lexical.js +++ b/ghost/core/core/server/lib/lexical.js @@ -143,7 +143,17 @@ module.exports = { get htmlToLexicalConverter() { try { - return require('@tryghost/kg-html-to-lexical').htmlToLexical; + if (process.env.CI) { + console.time('require @tryghost/kg-html-to-lexical'); // eslint-disable-line no-console + } + + const htmlToLexical = require('@tryghost/kg-html-to-lexical').htmlToLexical; + + if (process.env.CI) { + console.timeEnd('require @tryghost/kg-html-to-lexical'); // eslint-disable-line no-console + } + + return htmlToLexical; } catch (err) { throw new errors.InternalServerError({ message: 'Unable to convert from source HTML to Lexical', diff --git a/ghost/core/core/server/lib/mobiledoc.js b/ghost/core/core/server/lib/mobiledoc.js index 790210966f9..20fc01c8e32 100644 --- a/ghost/core/core/server/lib/mobiledoc.js +++ b/ghost/core/core/server/lib/mobiledoc.js @@ -75,7 +75,17 @@ module.exports = { get htmlToMobiledocConverter() { try { - return require('@tryghost/html-to-mobiledoc').toMobiledoc; + if (process.env.CI) { + console.time('require @tryghost/html-to-mobiledoc'); // eslint-disable-line no-console + } + + const toMobiledoc = require('@tryghost/html-to-mobiledoc').toMobiledoc; + + if (process.env.CI) { + console.timeEnd('require @tryghost/html-to-mobiledoc'); // eslint-disable-line no-console + } + + return toMobiledoc; } catch (err) { return () => { throw new errors.InternalServerError({ diff --git a/ghost/core/core/server/services/custom-redirects/CustomRedirectsAPI.js b/ghost/core/core/server/services/custom-redirects/CustomRedirectsAPI.js index 5241f35d90b..54a54c6fdde 100644 --- a/ghost/core/core/server/services/custom-redirects/CustomRedirectsAPI.js +++ b/ghost/core/core/server/services/custom-redirects/CustomRedirectsAPI.js @@ -9,7 +9,7 @@ const errors = require('@tryghost/errors'); const messages = { jsonParse: 'Could not parse JSON: {context}.', yamlParse: 'Could not parse YAML: {context}.', - yamlPlainString: 'YAML input cannot be a plain string. Check the format of your YAML file.', + yamlInvalid: 'YAML input is invalid. Check the contents of your YAML file.', redirectsHelp: 'https://ghost.org/docs/themes/routing/#redirects', redirectsRegister: 'Could not register custom redirects.' }; @@ -78,13 +78,9 @@ const parseRedirectsFile = (content, ext) => { }); } - // yaml.load passes almost every yaml code. - // Because of that, it's hard to detect if there's an error in the file. - // But one of the obvious errors is the plain string output. - // Here we check if the user made this mistake. - if (typeof configYaml === 'string') { + if (typeof configYaml !== 'object' || configYaml === null) { throw new errors.BadRequestError({ - message: tpl(messages.yamlPlainString), + message: tpl(messages.yamlInvalid), help: tpl(messages.redirectsHelp) }); } diff --git a/ghost/core/core/server/services/members/MembersConfigProvider.js b/ghost/core/core/server/services/members/MembersConfigProvider.js index 560eb576f03..7d592e36526 100644 --- a/ghost/core/core/server/services/members/MembersConfigProvider.js +++ b/ghost/core/core/server/services/members/MembersConfigProvider.js @@ -35,10 +35,6 @@ class MembersConfigProvider { return this._settingsHelpers.getMembersSupportAddress(); } - getAuthEmailFromAddress() { - return this.getEmailSupportAddress(); - } - /** * @deprecated Use settingsHelpers.isStripeConnected instead */ diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index 69a8b989975..21d6318a536 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -47,7 +47,7 @@ function createApiInstance(config) { logging.warn(message.text); } let msg = Object.assign({ - from: config.getAuthEmailFromAddress(), + from: config.getEmailSupportAddress(), subject: 'Signin', forceTextContent: true }, message); diff --git a/ghost/core/core/server/services/members/middleware.js b/ghost/core/core/server/services/members/middleware.js index 23451c2ecdc..a3ad49a4fb9 100644 --- a/ghost/core/core/server/services/members/middleware.js +++ b/ghost/core/core/server/services/members/middleware.js @@ -265,8 +265,16 @@ const createSessionFromMagicLink = async function createSessionFromMagicLink(req const ensureEndsWith = (string, endsWith) => (string.endsWith(endsWith) ? string : string + endsWith); const removeLeadingSlash = string => string.replace(/^\//, ''); + // Add query parameters so the frontend can detect that the signup went fine + const redirectUrl = new URL(removeLeadingSlash(ensureEndsWith(customRedirect, '/')), ensureEndsWith(baseUrl, '/')); + if (urlUtils.isSiteUrl(redirectUrl)) { + // Add only for non-external URLs + redirectUrl.searchParams.set('success', 'true'); + redirectUrl.searchParams.set('action', 'signup'); + } + return res.redirect(redirectUrl.href); } } diff --git a/ghost/core/core/server/services/members/utils.js b/ghost/core/core/server/services/members/utils.js index 4d9140402ea..0b6113c96ae 100644 --- a/ghost/core/core/server/services/members/utils.js +++ b/ghost/core/core/server/services/members/utils.js @@ -1,6 +1,12 @@ function formatNewsletterResponse(newsletters) { - return newsletters.map(({id, name, description, sort_order: sortOrder}) => { - return {id, name, description, sort_order: sortOrder}; + return newsletters.map(({id, uuid, name, description, sort_order: sortOrder}) => { + return { + id, + uuid, + name, + description, + sort_order: sortOrder + }; }); } diff --git a/ghost/core/core/server/services/recommendations/RecommendationServiceWrapper.js b/ghost/core/core/server/services/recommendations/RecommendationServiceWrapper.js index 6c00e54dc8d..dd9bf714a65 100644 --- a/ghost/core/core/server/services/recommendations/RecommendationServiceWrapper.js +++ b/ghost/core/core/server/services/recommendations/RecommendationServiceWrapper.js @@ -50,6 +50,7 @@ class RecommendationServiceWrapper { const sentry = require('../../../shared/sentry'); const settings = require('../settings'); const RecommendationEnablerService = require('./RecommendationEnablerService'); + const { BookshelfRecommendationRepository, RecommendationService, @@ -58,7 +59,8 @@ class RecommendationServiceWrapper { BookshelfClickEventRepository, IncomingRecommendationController, IncomingRecommendationService, - IncomingRecommendationEmailRenderer + IncomingRecommendationEmailRenderer, + RecommendationMetadataService } = require('@tryghost/recommendations'); const mentions = require('../mentions'); @@ -87,13 +89,22 @@ class RecommendationServiceWrapper { sentry }); + const oembedService = require('../oembed'); + const externalRequest = require('../../../server/lib/request-external.js'); + + const recommendationMetadataService = new RecommendationMetadataService({ + oembedService, + externalRequest + }); + this.service = new RecommendationService({ repository: this.repository, recommendationEnablerService, wellknownService, mentionSendingService: mentions.sendingService, clickEventRepository: this.clickEventRepository, - subscribeEventRepository: this.subscribeEventRepository + subscribeEventRepository: this.subscribeEventRepository, + recommendationMetadataService }); const mail = require('../mail'); diff --git a/ghost/core/core/server/web/api/endpoints/admin/routes.js b/ghost/core/core/server/web/api/endpoints/admin/routes.js index 8dfd990d3e0..4fb71779544 100644 --- a/ghost/core/core/server/web/api/endpoints/admin/routes.js +++ b/ghost/core/core/server/web/api/endpoints/admin/routes.js @@ -351,6 +351,7 @@ module.exports = function apiRoutes() { router.get('/recommendations', mw.authAdminApi, http(api.recommendations.browse)); router.get('/recommendations/:id', mw.authAdminApi, http(api.recommendations.read)); router.post('/recommendations', mw.authAdminApi, http(api.recommendations.add)); + router.post('/recommendations/check', mw.authAdminApi, http(api.recommendations.check)); router.put('/recommendations/:id', mw.authAdminApi, http(api.recommendations.edit)); router.del('/recommendations/:id', mw.authAdminApi, http(api.recommendations.destroy)); diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index a1b0e2e1736..cfb05e85bef 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -20,7 +20,8 @@ const GA_FEATURES = [ 'outboundLinkTagging', 'announcementBar', 'signupForm', - 'recommendations' + 'recommendations', + 'editorEmojiPicker' ]; // NOTE: this allowlist is meant to be used to filter out any unexpected @@ -43,7 +44,7 @@ const ALPHA_FEATURES = [ 'importMemberTier', 'lexicalIndicators', 'listUnsubscribeHeader', - 'editorEmojiPicker' + 'adminXOffers' ]; module.exports.GA_KEYS = [...GA_FEATURES]; diff --git a/ghost/core/core/shared/sentry.js b/ghost/core/core/shared/sentry.js index c410247c75e..c2d52b10d72 100644 --- a/ghost/core/core/shared/sentry.js +++ b/ghost/core/core/shared/sentry.js @@ -22,13 +22,25 @@ if (sentryConfig && !sentryConfig.disabled) { event.exception.values[0].type = exception.context; } + // This is a mysql2 error — add some additional context + if (exception.sql) { + event.exception.values[0].type = `SQL Error ${exception.errno}: ${exception.sqlErrorCode}`; + event.exception.values[0].value = exception.sqlMessage; + event.contexts.mysql = { + errno: exception.errno, + code: exception.sqlErrorCode, + sql: exception.sql, + message: exception.sqlMessage, + state: exception.sqlState + }; + } + // This is a Ghost Error, copy all our extra data to tags event.tags.type = exception.errorType; event.tags.code = exception.code; event.tags.id = exception.id; - event.tags.statusCode = exception.statusCode; + event.tags.status_code = exception.statusCode; } - return event; } }); diff --git a/ghost/core/package.json b/ghost/core/package.json index f95c3919e63..b62801c95f5 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.70.2", + "version": "5.73.1", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", @@ -58,55 +58,55 @@ }, "dependencies": { "@extractus/oembed-extractor": "3.2.1", - "@sentry/node": "7.70.0", - "@tryghost/adapter-base-cache": "0.1.5", + "@sentry/node": "7.78.0", + "@tryghost/adapter-base-cache": "0.1.10", "@tryghost/adapter-cache-redis": "0.0.0", "@tryghost/adapter-manager": "0.0.0", - "@tryghost/admin-api-schema": "4.5.1", + "@tryghost/admin-api-schema": "4.5.3", "@tryghost/announcement-bar-settings": "0.0.0", "@tryghost/api-framework": "0.0.0", "@tryghost/api-version-compatibility-service": "0.0.0", "@tryghost/audience-feedback": "0.0.0", - "@tryghost/bookshelf-plugins": "0.6.10", + "@tryghost/bookshelf-plugins": "0.6.11", "@tryghost/bootstrap-socket": "0.0.0", "@tryghost/collections": "0.0.0", - "@tryghost/color-utils": "0.1.24", - "@tryghost/config-url-helpers": "1.0.6", + "@tryghost/color-utils": "0.2.0", + "@tryghost/config-url-helpers": "1.0.10", "@tryghost/constants": "0.0.0", "@tryghost/custom-theme-settings-service": "0.0.0", "@tryghost/data-generator": "0.0.0", - "@tryghost/database-info": "0.3.17", - "@tryghost/debug": "0.1.24", + "@tryghost/database-info": "0.3.20", + "@tryghost/debug": "0.1.26", "@tryghost/domain-events": "0.0.0", "@tryghost/donations": "0.0.0", "@tryghost/dynamic-routing-events": "0.0.0", "@tryghost/email-analytics-provider-mailgun": "0.0.0", "@tryghost/email-analytics-service": "0.0.0", "@tryghost/email-content-generator": "0.0.0", - "@tryghost/email-mock-receiver": "0.3.1", + "@tryghost/email-mock-receiver": "0.3.2", "@tryghost/email-service": "0.0.0", "@tryghost/email-suppression-list": "0.0.0", "@tryghost/errors": "1.2.26", "@tryghost/event-aware-cache-wrapper": "0.0.0", "@tryghost/express-dynamic-redirects": "0.0.0", "@tryghost/external-media-inliner": "0.0.0", - "@tryghost/helpers": "1.1.82", + "@tryghost/helpers": "1.1.88", "@tryghost/html-to-plaintext": "0.0.0", - "@tryghost/http-cache-utils": "0.1.9", + "@tryghost/http-cache-utils": "0.1.11", "@tryghost/i18n": "0.0.0", - "@tryghost/image-transform": "1.2.6", + "@tryghost/image-transform": "1.2.10", "@tryghost/importer-handler-content-files": "0.0.0", "@tryghost/importer-revue": "0.0.0", "@tryghost/job-manager": "0.0.0", - "@tryghost/kg-card-factory": "4.0.14", - "@tryghost/kg-converters": "0.0.21", + "@tryghost/kg-card-factory": "4.0.15", + "@tryghost/kg-converters": "0.0.22", "@tryghost/kg-default-atoms": "4.0.3", - "@tryghost/kg-default-cards": "9.1.8", - "@tryghost/kg-default-nodes": "0.2.6", - "@tryghost/kg-html-to-lexical": "0.1.7", - "@tryghost/kg-lexical-html-renderer": "0.3.42", - "@tryghost/kg-mobiledoc-html-renderer": "6.0.14", - "@tryghost/limit-service": "1.2.10", + "@tryghost/kg-default-cards": "9.1.9", + "@tryghost/kg-default-nodes": "0.2.8", + "@tryghost/kg-html-to-lexical": "0.1.9", + "@tryghost/kg-lexical-html-renderer": "0.3.45", + "@tryghost/kg-mobiledoc-html-renderer": "6.0.15", + "@tryghost/limit-service": "1.2.12", "@tryghost/link-redirects": "0.0.0", "@tryghost/link-replacer": "0.0.0", "@tryghost/link-tracking": "0.0.0", @@ -124,7 +124,7 @@ "@tryghost/members-ssr": "0.0.0", "@tryghost/members-stripe-service": "0.0.0", "@tryghost/mentions-email-report": "0.0.0", - "@tryghost/metrics": "1.0.24", + "@tryghost/metrics": "1.0.27", "@tryghost/milestones": "0.0.0", "@tryghost/minifier": "0.0.0", "@tryghost/model-to-domain-event-interceptor": "0.0.0", @@ -134,31 +134,31 @@ "@tryghost/mw-session-from-token": "0.0.0", "@tryghost/mw-version-match": "0.0.0", "@tryghost/mw-vhost": "0.0.0", - "@tryghost/nodemailer": "0.3.35", + "@tryghost/nodemailer": "0.3.37", "@tryghost/nql": "0.11.0", "@tryghost/oembed-service": "0.0.0", "@tryghost/package-json": "0.0.0", "@tryghost/post-revisions": "0.0.0", "@tryghost/posts-service": "0.0.0", - "@tryghost/pretty-cli": "1.2.36", - "@tryghost/promise": "0.3.4", + "@tryghost/pretty-cli": "1.2.38", + "@tryghost/promise": "0.3.6", "@tryghost/recommendations": "0.0.0", "@tryghost/request": "1.0.0", "@tryghost/security": "0.0.0", "@tryghost/session-service": "0.0.0", "@tryghost/settings-path-manager": "0.0.0", "@tryghost/slack-notifications": "0.0.0", - "@tryghost/social-urls": "0.1.36", + "@tryghost/social-urls": "0.1.41", "@tryghost/staff-service": "0.0.0", "@tryghost/stats-service": "0.0.0", - "@tryghost/string": "0.2.4", + "@tryghost/string": "0.2.10", "@tryghost/tiers": "0.0.0", - "@tryghost/tpl": "0.1.24", + "@tryghost/tpl": "0.1.26", "@tryghost/update-check-service": "0.0.0", - "@tryghost/url-utils": "4.4.0", - "@tryghost/validator": "0.2.4", + "@tryghost/url-utils": "4.4.6", + "@tryghost/validator": "0.2.6", "@tryghost/verification-trigger": "0.0.0", - "@tryghost/version": "0.1.22", + "@tryghost/version": "0.1.24", "@tryghost/webmentions": "0.0.0", "@tryghost/zip": "1.1.38", "amperize": "0.6.1", @@ -187,7 +187,7 @@ "ghost-storage-base": "1.0.0", "glob": "8.1.0", "got": "11.8.6", - "gscan": "4.39.4", + "gscan": "4.40.0", "human-number": "2.0.4", "image-size": "1.0.2", "intl": "1.2.5", @@ -203,13 +203,13 @@ "knex-migrator": "5.1.5", "lib0": "0.2.87", "lodash": "4.17.21", - "luxon": "3.4.3", + "luxon": "3.4.4", "moment": "2.24.0", "moment-timezone": "0.5.23", "multer": "1.4.4", - "mysql2": "3.6.2", + "mysql2": "3.6.3", "nconf": "0.12.1", - "newrelic": "11.4.0", + "newrelic": "11.5.0", "node-jose": "2.2.0", "path-match": "1.2.4", "probe-image-size": "7.2.3", @@ -225,15 +225,15 @@ "yjs": "13.6.8" }, "optionalDependencies": { - "@tryghost/html-to-mobiledoc": "2.0.36", + "@tryghost/html-to-mobiledoc": "2.0.42", "sqlite3": "5.1.6" }, "devDependencies": { "@actions/core": "1.10.1", "@playwright/test": "1.38.1", "@tryghost/express-test": "0.13.7", - "@tryghost/webhook-mock-receiver": "0.2.6", - "@types/common-tags": "1.8.2", + "@tryghost/webhook-mock-receiver": "0.2.8", + "@types/common-tags": "1.8.4", "c8": "8.0.1", "cli-progress": "3.12.0", "cssnano": "6.0.1", @@ -244,7 +244,7 @@ "mocha": "10.2.0", "mocha-slow-test-reporter": "0.1.2", "mock-knex": "TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192", - "monobundle": "TryGhost/monobundle#44fdf2c8e304e797a04858bfd7339b2a1fa47441", + "monobundle": "TryGhost/monobundle#811679e94b75f82a0fb1d00a11e6e0b9f6e5e44a", "nock": "13.3.3", "papaparse": "5.3.2", "postcss": "8.4.31", diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap index 116f918fa52..aa14686fcfd 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap @@ -191,8 +191,7 @@ Object { table.body p, table.body ul, table.body ol, -table.body td, -table.body span { +table.body td { font-size: 16px !important; } @@ -308,35 +307,30 @@ table.body .feedback-button-mobile-text { } table.body .latest-posts-header { - font-size: 14px !important; - } - - table.body .latest-post-img { - display: none !important; - } - - table.body .latest-post-img-mobile { - display: inline-block !important; - width: 100%; + font-size: 12px !important; } table.body .latest-post-title { display: inline-block !important; width: 100%; - padding-right: 0 !important; - padding-bottom: 8px !important; + padding-right: 8px !important; } table.body .latest-post h4, table.body .latest-post h4 span { - padding: 4px 0 !important; - font-size: 18px !important; + padding: 4px 0 6px !important; + font-size: 15px !important; } - table.body .latest-post p, -table.body .latest-post p span { + table.body .latest-post-excerpt, +table.body .latest-post-excerpt a, +table.body .latest-post-excerpt span { font-size: 13px !important; - line-height: 1.25em; + line-height: 1.2 !important; + } + + table.body .latest-post-excerpt span { + display: none !important; } table.body .subscription-box h3 { @@ -381,7 +375,8 @@ table.body .manage-subscription { line-height: 1.3em !important; } - table.body h2 { + table.body h2, +table.body h2 span { font-size: 26px !important; line-height: 1.22em !important; } @@ -427,11 +422,6 @@ table.body .manage-subscription { table.body hr { margin: 2em 0 !important; } - - table.body figcaption, -table.body figcaption a { - font-size: 13px !important; - } } @media all { .subscription-details p.hidden { @@ -735,7 +725,7 @@ exports[`Email Preview API Read can read post email preview with email card and Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "23925", + "content-length": "23765", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -840,8 +830,7 @@ Object { table.body p, table.body ul, table.body ol, -table.body td, -table.body span { +table.body td { font-size: 16px !important; } @@ -957,35 +946,30 @@ table.body .feedback-button-mobile-text { } table.body .latest-posts-header { - font-size: 14px !important; - } - - table.body .latest-post-img { - display: none !important; - } - - table.body .latest-post-img-mobile { - display: inline-block !important; - width: 100%; + font-size: 12px !important; } table.body .latest-post-title { display: inline-block !important; width: 100%; - padding-right: 0 !important; - padding-bottom: 8px !important; + padding-right: 8px !important; } table.body .latest-post h4, table.body .latest-post h4 span { - padding: 4px 0 !important; - font-size: 18px !important; + padding: 4px 0 6px !important; + font-size: 15px !important; } - table.body .latest-post p, -table.body .latest-post p span { + table.body .latest-post-excerpt, +table.body .latest-post-excerpt a, +table.body .latest-post-excerpt span { font-size: 13px !important; - line-height: 1.25em; + line-height: 1.2 !important; + } + + table.body .latest-post-excerpt span { + display: none !important; } table.body .subscription-box h3 { @@ -1030,7 +1014,8 @@ table.body .manage-subscription { line-height: 1.3em !important; } - table.body h2 { + table.body h2, +table.body h2 span { font-size: 26px !important; line-height: 1.22em !important; } @@ -1076,11 +1061,6 @@ table.body .manage-subscription { table.body hr { margin: 2em 0 !important; } - - table.body figcaption, -table.body figcaption a { - font-size: 13px !important; - } } @media all { .subscription-details p.hidden { @@ -1404,7 +1384,7 @@ exports[`Email Preview API Read can read post email preview with fields 4: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "28736", + "content-length": "28576", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1540,8 +1520,7 @@ Object { table.body p, table.body ul, table.body ol, -table.body td, -table.body span { +table.body td { font-size: 16px !important; } @@ -1657,35 +1636,30 @@ table.body .feedback-button-mobile-text { } table.body .latest-posts-header { - font-size: 14px !important; - } - - table.body .latest-post-img { - display: none !important; - } - - table.body .latest-post-img-mobile { - display: inline-block !important; - width: 100%; + font-size: 12px !important; } table.body .latest-post-title { display: inline-block !important; width: 100%; - padding-right: 0 !important; - padding-bottom: 8px !important; + padding-right: 8px !important; } table.body .latest-post h4, table.body .latest-post h4 span { - padding: 4px 0 !important; - font-size: 18px !important; + padding: 4px 0 6px !important; + font-size: 15px !important; } - table.body .latest-post p, -table.body .latest-post p span { + table.body .latest-post-excerpt, +table.body .latest-post-excerpt a, +table.body .latest-post-excerpt span { font-size: 13px !important; - line-height: 1.25em; + line-height: 1.2 !important; + } + + table.body .latest-post-excerpt span { + display: none !important; } table.body .subscription-box h3 { @@ -1730,7 +1704,8 @@ table.body .manage-subscription { line-height: 1.3em !important; } - table.body h2 { + table.body h2, +table.body h2 span { font-size: 26px !important; line-height: 1.22em !important; } @@ -1776,11 +1751,6 @@ table.body .manage-subscription { table.body hr { margin: 2em 0 !important; } - - table.body figcaption, -table.body figcaption a { - font-size: 13px !important; - } } @media all { .subscription-details p.hidden { @@ -2091,7 +2061,7 @@ exports[`Email Preview API Read has custom content transformations for email com Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "23679", + "content-length": "23519", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2556,8 +2526,7 @@ Object { table.body p, table.body ul, table.body ol, -table.body td, -table.body span { +table.body td { font-size: 16px !important; } @@ -2673,35 +2642,30 @@ table.body .feedback-button-mobile-text { } table.body .latest-posts-header { - font-size: 14px !important; - } - - table.body .latest-post-img { - display: none !important; - } - - table.body .latest-post-img-mobile { - display: inline-block !important; - width: 100%; + font-size: 12px !important; } table.body .latest-post-title { display: inline-block !important; width: 100%; - padding-right: 0 !important; - padding-bottom: 8px !important; + padding-right: 8px !important; } table.body .latest-post h4, table.body .latest-post h4 span { - padding: 4px 0 !important; - font-size: 18px !important; + padding: 4px 0 6px !important; + font-size: 15px !important; } - table.body .latest-post p, -table.body .latest-post p span { + table.body .latest-post-excerpt, +table.body .latest-post-excerpt a, +table.body .latest-post-excerpt span { font-size: 13px !important; - line-height: 1.25em; + line-height: 1.2 !important; + } + + table.body .latest-post-excerpt span { + display: none !important; } table.body .subscription-box h3 { @@ -2746,7 +2710,8 @@ table.body .manage-subscription { line-height: 1.3em !important; } - table.body h2 { + table.body h2, +table.body h2 span { font-size: 26px !important; line-height: 1.22em !important; } @@ -2792,11 +2757,6 @@ table.body .manage-subscription { table.body hr { margin: 2em 0 !important; } - - table.body figcaption, -table.body figcaption a { - font-size: 13px !important; - } } @media all { .subscription-details p.hidden { @@ -3117,7 +3077,7 @@ exports[`Email Preview API Read uses the newsletter provided through ?newsletter Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "24171", + "content-length": "24011", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3608,8 +3568,7 @@ Object { table.body p, table.body ul, table.body ol, -table.body td, -table.body span { +table.body td { font-size: 16px !important; } @@ -3725,35 +3684,30 @@ table.body .feedback-button-mobile-text { } table.body .latest-posts-header { - font-size: 14px !important; - } - - table.body .latest-post-img { - display: none !important; - } - - table.body .latest-post-img-mobile { - display: inline-block !important; - width: 100%; + font-size: 12px !important; } table.body .latest-post-title { display: inline-block !important; width: 100%; - padding-right: 0 !important; - padding-bottom: 8px !important; + padding-right: 8px !important; } table.body .latest-post h4, table.body .latest-post h4 span { - padding: 4px 0 !important; - font-size: 18px !important; + padding: 4px 0 6px !important; + font-size: 15px !important; } - table.body .latest-post p, -table.body .latest-post p span { + table.body .latest-post-excerpt, +table.body .latest-post-excerpt a, +table.body .latest-post-excerpt span { font-size: 13px !important; - line-height: 1.25em; + line-height: 1.2 !important; + } + + table.body .latest-post-excerpt span { + display: none !important; } table.body .subscription-box h3 { @@ -3798,7 +3752,8 @@ table.body .manage-subscription { line-height: 1.3em !important; } - table.body h2 { + table.body h2, +table.body h2 span { font-size: 26px !important; line-height: 1.22em !important; } @@ -3844,11 +3799,6 @@ table.body .manage-subscription { table.body hr { margin: 2em 0 !important; } - - table.body figcaption, -table.body figcaption a { - font-size: 13px !important; - } } @media all { .subscription-details p.hidden { @@ -4169,7 +4119,7 @@ exports[`Email Preview API Read uses the posts newsletter by default 4: [headers Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "24171", + "content-length": "24011", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index 5f8ebe3b5e4..7485981efe2 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -5947,6 +5947,171 @@ Object { } `; +exports[`Members API Updates the email_disabled field 1: [body] 1`] = ` +Object { + "members": Array [ + Object { + "attribution": Object { + "id": null, + "referrer_medium": "Ghost Admin", + "referrer_source": "Created manually", + "referrer_url": null, + "title": null, + "type": null, + "url": null, + }, + "avatar_image": null, + "comped": false, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "email": "ok@email.com", + "email_count": 0, + "email_open_rate": null, + "email_opened_count": 0, + "email_suppression": Object { + "info": null, + "suppressed": false, + }, + "geolocation": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "labels": Any, + "last_seen_at": null, + "name": "Existing Member", + "newsletters": Array [ + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Default Newsletter", + "status": "active", + }, + Object { + "description": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Weekly newsletter", + "status": "active", + }, + ], + "note": null, + "status": "free", + "subscribed": true, + "subscriptions": Any, + "tiers": Array [], + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], +} +`; + +exports[`Members API Updates the email_disabled field 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "862", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Members API Updates the email_disabled field when a member email is updated 1: [body] 1`] = ` +Object { + "members": Array [ + Object { + "attribution": null, + "avatar_image": null, + "comped": false, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "email": "suppressed@email.com", + "email_count": 0, + "email_open_rate": null, + "email_opened_count": 0, + "email_suppression": Object { + "info": Object { + "reason": "fail", + "timestamp": "2023-11-02T15:37:09.000Z", + }, + "suppressed": true, + }, + "geolocation": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "labels": Any, + "last_seen_at": null, + "name": "Test Member 123", + "newsletters": Array [], + "note": null, + "status": "free", + "subscribed": false, + "subscriptions": Any, + "tiers": Array [], + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], +} +`; + +exports[`Members API Updates the email_disabled field when a member email is updated 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "594", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + +exports[`Members API Updates the email_disabled field when a member email is updated 3: [body] 1`] = ` +Object { + "members": Array [ + Object { + "attribution": null, + "avatar_image": null, + "comped": false, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "email": "ok@email.com", + "email_count": 0, + "email_open_rate": null, + "email_opened_count": 0, + "email_suppression": Object { + "info": null, + "suppressed": false, + }, + "geolocation": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "labels": Any, + "last_seen_at": null, + "name": "Test Member 123", + "newsletters": Array [], + "note": null, + "status": "free", + "subscribed": false, + "subscriptions": Any, + "tiers": Array [], + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], +} +`; + +exports[`Members API Updates the email_disabled field when a member email is updated 4: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "535", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-powered-by": "Express", +} +`; + exports[`Members API Updating member data without newsletters does not change newsletters 1: [body] 1`] = ` Object { "members": Array [ diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap index 585ec0e9818..f4628112bf6 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap @@ -1789,7 +1789,7 @@ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "content-disposition": StringMatching /\\^Attachment; filename="post-analytics\\.\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}\\.csv"\\$/, - "content-length": "2721", + "content-length": "2716", "content-type": "text/csv; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1800,7 +1800,7 @@ Object { exports[`Posts API Export Can export 2 1`] = ` Object { - "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,free_signups,paid_conversions + "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,signups,paid_conversions 6194d3ce51e2700162531a77,Start here for a quick overview of everything you need to know,http://127.0.0.1:2369/welcome/,Ghost,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,Getting Started,Public,,,,,0,0 6194d3ce51e2700162531a76,Customizing your brand and design settings,http://127.0.0.1:2369/design/,Ghost,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,Getting Started,Public,,,,,0,0 6194d3ce51e2700162531a75,\\"Writing and managing content in Ghost, an advanced guide\\",http://127.0.0.1:2369/write/,Ghost,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,Getting Started,Public,,,,,0,0 @@ -1820,7 +1820,7 @@ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "content-disposition": StringMatching /\\^Attachment; filename="post-analytics\\.\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}\\.csv"\\$/, - "content-length": "587", + "content-length": "582", "content-type": "text/csv; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1831,7 +1831,7 @@ Object { exports[`Posts API Export Can export with filter 2 1`] = ` Object { - "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,free_signups,paid_conversions + "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,signups,paid_conversions 618ba1ffbe2896088840a6e7,\\"Not so short, bit complex\\",http://127.0.0.1:2369/not-so-short-bit-complex/,Joe Bloggs,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,true,,Public,,,,,0,0 618ba1ffbe2896088840a6e3,Short and Sweet,http://127.0.0.1:2369/short-and-sweet/,Joe Bloggs,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,true,chorizo,Public,,,,,0,0", } @@ -1842,7 +1842,7 @@ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "content-disposition": StringMatching /\\^Attachment; filename="post-analytics\\.\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}\\.csv"\\$/, - "content-length": "406", + "content-length": "401", "content-type": "text/csv; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1853,7 +1853,7 @@ Object { exports[`Posts API Export Can export with limit 2 1`] = ` Object { - "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,free_signups,paid_conversions + "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,signups,paid_conversions 6194d3ce51e2700162531a77,Start here for a quick overview of everything you need to know,http://127.0.0.1:2369/welcome/,Ghost,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,Getting Started,Public,,,,,0,0", } `; @@ -1863,7 +1863,7 @@ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "content-disposition": StringMatching /\\^Attachment; filename="post-analytics\\.\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}\\.csv"\\$/, - "content-length": "2721", + "content-length": "2716", "content-type": "text/csv; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1874,7 +1874,7 @@ Object { exports[`Posts API Export Can export with order 2 1`] = ` Object { - "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,free_signups,paid_conversions + "text": "id,title,url,author,status,created_at,updated_at,published_at,featured,tags,post_access,email_recipients,sends,opens,clicks,signups,paid_conversions 618ba1ffbe2896088840a6df,HTML Ipsum,http://127.0.0.1:2369/html-ipsum/,Joe Bloggs,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,\\"kitchen sink, bacon\\",Public,,,,,0,0 618ba1ffbe2896088840a6e1,Ghostly Kitchen Sink,http://127.0.0.1:2369/ghostly-kitchen-sink/,Joe Bloggs,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,false,\\"kitchen sink, bacon\\",Public,,,,,0,0 618ba1ffbe2896088840a6e3,Short and Sweet,http://127.0.0.1:2369/short-and-sweet/,Joe Bloggs,published only,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,2050-01-01T00:00:00.000Z,true,chorizo,Public,,,,,0,0 diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap index d6cf4091170..76a2963aaba 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/recommendations.test.js.snap @@ -2184,6 +2184,39 @@ Object { } `; +exports[`Recommendations Admin API check Can check a recommendation url 1: [body] 1`] = ` +Object { + "recommendations": Array [ + Object { + "created_at": null, + "description": null, + "excerpt": "Because dogs are cute", + "favicon": "https://dogpictures.com/favicon.ico", + "featured_image": "https://dogpictures.com/dog.jpg", + "id": null, + "one_click_subscribe": true, + "title": "Dog Pictures", + "updated_at": null, + "url": "https://dogpictures.com/", + }, + ], +} +`; + +exports[`Recommendations Admin API check Can check a recommendation url 2: [headers] 1`] = ` +Object { + "access-control-allow-origin": "http://127.0.0.1:2369", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "304", + "content-type": "application/json; charset=utf-8", + "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "vary": "Accept-Version, Origin, Accept-Encoding", + "x-cache-invalidate": "/*", + "x-powered-by": "Express", +} +`; + exports[`Recommendations Admin API delete Can delete recommendation 1: [body] 1`] = `Object {}`; exports[`Recommendations Admin API delete Can delete recommendation 2: [headers] 1`] = ` diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap index 2f7c7edf53a..ace2b6df16b 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap @@ -758,7 +758,7 @@ exports[`Settings API Edit Can edit a setting 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4280", + "content-length": "4307", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/members.test.js b/ghost/core/test/e2e-api/admin/members.test.js index 72c4caac3da..02b75e5f9ce 100644 --- a/ghost/core/test/e2e-api/admin/members.test.js +++ b/ghost/core/test/e2e-api/admin/members.test.js @@ -2097,6 +2097,51 @@ describe('Members API', function () { }); }); + describe('email_disabled', function () { + const testMemberId = '6543c13c13575e086a06b222'; + const suppressedEmail = 'suppressed@email.com'; + const okEmail = 'ok@email.com'; + + let testMember; + let suppression; + + beforeEach(async function () { + testMember = await models.Member.add({id: testMemberId, email: okEmail, name: 'Test Member 123', email_disabled: false}); + suppression = await models.Suppression.add({ + email: suppressedEmail, + reason: 'bounce' + }); + }); + + afterEach(async function () { + // Delete member & suppression + await models.Member.destroy({id: testMember.id}); + await models.Suppression.destroy({id: suppression.id}); + }); + + it('Updates the email_disabled field when a member email is updated', async function () { + // Now update the email address of the test member to suppressed email + await agent + .put(`/members/${testMember.id}/`) + .body({members: [{email: suppressedEmail}]}) + .expectStatus(200); + + // email_disabled should be true + await testMember.refresh(); + should(testMember.get('email_disabled')).be.true(); + + // Now update the email address of that member to a non-suppressed email + await agent + .put(`/members/${testMember.id}/`) + .body({members: [{email: okEmail}]}) + .expectStatus(200); + + // email_disabled should be false + await testMember.refresh(); + should(testMember.get('email_disabled')).be.false(); + }); + }); + // Delete a member it('Can destroy', async function () { diff --git a/ghost/core/test/e2e-api/admin/recommendations.test.js b/ghost/core/test/e2e-api/admin/recommendations.test.js index 8bf2bb29c19..699aa84c101 100644 --- a/ghost/core/test/e2e-api/admin/recommendations.test.js +++ b/ghost/core/test/e2e-api/admin/recommendations.test.js @@ -3,6 +3,7 @@ const {anyObjectId, anyErrorId, anyISODateTime, anyContentVersion, anyLocationFo const assert = require('assert/strict'); const recommendationsService = require('../../../core/server/services/recommendations'); const {Recommendation, ClickEvent, SubscribeEvent} = require('@tryghost/recommendations'); +const nock = require('nock'); async function addDummyRecommendation(i = 0) { const recommendation = Recommendation.create({ @@ -666,6 +667,44 @@ describe('Recommendations Admin API', function () { }); }); + describe('check', function () { + it('Can check a recommendation url', async function () { + nock('https://dogpictures.com') + .get('/members/api/site') + .reply(200, { + site: { + title: 'Dog Pictures', + description: 'Because dogs are cute', + cover_image: 'https://dogpictures.com/dog.jpg', + icon: 'https://dogpictures.com/favicon.ico', + allow_external_signup: true + } + }); + + const {body} = await agent.post('recommendations/check/') + .body({ + recommendations: [{ + url: 'https://dogpictures.com' + }] + }) + .expectStatus(200) + .matchHeaderSnapshot({ + 'content-version': anyContentVersion, + etag: anyEtag + }) + .matchBodySnapshot({}); + + // Check everything is set correctly + assert.equal(body.recommendations[0].title, 'Dog Pictures'); + assert.equal(body.recommendations[0].url, 'https://dogpictures.com/'); + assert.equal(body.recommendations[0].description, null); + assert.equal(body.recommendations[0].excerpt, 'Because dogs are cute'); + assert.equal(body.recommendations[0].featured_image, 'https://dogpictures.com/dog.jpg'); + assert.equal(body.recommendations[0].favicon, 'https://dogpictures.com/favicon.ico'); + assert.equal(body.recommendations[0].one_click_subscribe, true); + }); + }); + describe('delete', function () { it('Can delete recommendation', async function () { const id = await addDummyRecommendation(); diff --git a/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap index cdc3b5045ac..91bf10e8f98 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap @@ -290,6 +290,53 @@ Object { } `; +exports[`Pages Content API Cannot request pages with mobiledoc or lexical fields 1: [body] 1`] = ` +Object { + "meta": Object { + "pagination": Object { + "limit": 15, + "next": null, + "page": 1, + "pages": 1, + "prev": null, + "total": 5, + }, + }, + "pages": Array [ + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], +} +`; + exports[`Pages Content API Cannot request pages with mobiledoc or lexical formats 1: [body] 1`] = ` Object { "meta": Object { diff --git a/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap index 1de9cc8945d..8cd9dfd6997 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap @@ -4403,6 +4403,89 @@ Header Level 3 } `; +exports[`Posts Content API Cannot request mobiledoc or lexical fields 1: [body] 1`] = ` +Object { + "meta": Object { + "pagination": Object { + "limit": 15, + "next": null, + "page": 1, + "pages": 1, + "prev": null, + "total": 11, + }, + }, + "posts": Array [ + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + Object { + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000\\\\\\+\\\\d\\{2\\}:\\\\d\\{2\\}/, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + ], +} +`; + exports[`Posts Content API Cannot request mobiledoc or lexical formats 1: [body] 1`] = ` Object { "meta": Object { diff --git a/ghost/core/test/e2e-api/content/pages.test.js b/ghost/core/test/e2e-api/content/pages.test.js index b7770d3533b..c302d4b6b11 100644 --- a/ghost/core/test/e2e-api/content/pages.test.js +++ b/ghost/core/test/e2e-api/content/pages.test.js @@ -50,6 +50,15 @@ describe('Pages Content API', function () { }); }); + it('Cannot request pages with mobiledoc or lexical fields', async function () { + await agent + .get(`pages/?fields=mobiledoc,lexical,published_at,created_at,updated_at,uuid`) + .expectStatus(200) + .matchBodySnapshot({ + pages: new Array(5).fill(pageMatcher) + }); + }); + it('Can request page', async function () { const res = await agent.get(`pages/${fixtureManager.get('posts', 5).id}/`) .expectStatus(200) diff --git a/ghost/core/test/e2e-api/content/posts.test.js b/ghost/core/test/e2e-api/content/posts.test.js index 02b3ef87f73..2fba0680cf1 100644 --- a/ghost/core/test/e2e-api/content/posts.test.js +++ b/ghost/core/test/e2e-api/content/posts.test.js @@ -84,6 +84,15 @@ describe('Posts Content API', function () { }); }); + it('Cannot request mobiledoc or lexical fields', async function () { + await agent + .get(`posts/?fields=mobiledoc,lexical,published_at,created_at,updated_at,uuid`) + .expectStatus(200) + .matchBodySnapshot({ + posts: new Array(11).fill(postMatcher) + }); + }); + it('Can filter posts by tag', async function () { const res = await agent.get('posts/?filter=tag:kitchen-sink,featured:true&include=tags') .expectStatus(200) diff --git a/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap index 44872d36482..7cf8977a3dd 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/middleware.test.js.snap @@ -19,12 +19,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "paid": false, @@ -38,7 +40,7 @@ exports[`Comments API when authenticated can get member data 2: [headers] 1`] = Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "529", + "content-length": "621", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -144,12 +146,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "paid": false, @@ -163,7 +167,7 @@ exports[`Comments API when authenticated can update comment notifications 2: [he Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "541", + "content-length": "633", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -182,12 +186,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "status": "free", @@ -199,7 +205,7 @@ exports[`Comments API when authenticated can update comment notifications 4: [he Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "354", + "content-length": "446", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -226,12 +232,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "paid": false, @@ -245,7 +253,7 @@ exports[`Comments API when authenticated can update member expertise 2: [headers Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "542", + "content-length": "634", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -272,12 +280,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "paid": false, @@ -291,7 +301,7 @@ exports[`Comments API when authenticated can update name 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "540", + "content-length": "632", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -318,12 +328,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Default Newsletter", "sort_order": 0, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, Object { "description": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Weekly newsletter", "sort_order": 2, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "paid": false, @@ -337,7 +349,7 @@ exports[`Comments API when authenticated trims whitespace from expertise 2: [hea Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "531", + "content-length": "623", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -356,6 +368,7 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Daily newsletter", "sort_order": 1, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, ], "status": "free", @@ -367,7 +380,7 @@ exports[`Comments API when not authenticated but enabled can update comment noti Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "253", + "content-length": "299", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", diff --git a/ghost/core/test/e2e-api/members/middleware.test.js b/ghost/core/test/e2e-api/members/middleware.test.js index 8f97b479660..c5aa4579980 100644 --- a/ghost/core/test/e2e-api/members/middleware.test.js +++ b/ghost/core/test/e2e-api/members/middleware.test.js @@ -12,7 +12,8 @@ const memberMatcher = (newslettersCount) => { created_at: anyISODateTime, newsletters: new Array(newslettersCount).fill( { - id: anyObjectId + id: anyObjectId, + uuid: anyUuid } ) }; @@ -23,7 +24,8 @@ const buildMemberMatcher = (newslettersCount) => { uuid: anyUuid, newsletters: new Array(newslettersCount).fill( { - id: anyObjectId + id: anyObjectId, + uuid: anyUuid } ) }; diff --git a/ghost/core/test/e2e-api/members/signin.test.js b/ghost/core/test/e2e-api/members/signin.test.js index 7ef02f0e303..aaa238bbecb 100644 --- a/ghost/core/test/e2e-api/members/signin.test.js +++ b/ghost/core/test/e2e-api/members/signin.test.js @@ -3,6 +3,7 @@ const models = require('../../../core/server/models'); const assert = require('assert/strict'); require('should'); const sinon = require('sinon'); +const members = require('../../../core/server/services/members'); let membersAgent, membersService; @@ -64,7 +65,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); }); @@ -75,7 +76,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup-paid`) .expectStatus(302) - .expectHeader('Location', /\/welcome-paid\/$/) + .expectHeader('Location', /\/welcome-paid\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); }); @@ -86,10 +87,36 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=subscribe`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); }); + it('Will redirect to an external welcome page for subscribe', async function () { + // Alter the product welcome page to an external URL + const freeProduct = await members.api.productRepository.get({slug: 'free'}); + await members.api.productRepository.update({ + id: freeProduct.id, + welcome_page_url: 'https://externalsite.ghost/welcome/' + }); + + try { + const magicLink = await membersService.api.getMagicLink('member1@test.com', 'signup'); + const magicLinkUrl = new URL(magicLink); + const token = magicLinkUrl.searchParams.get('token'); + + await membersAgent.get(`/?token=${token}&action=subscribe`) + .expectStatus(302) + .expectHeader('Location', 'https://externalsite.ghost/welcome/') // no query params added + .expectHeader('Set-Cookie', /members-ssr.*/); + } finally { + // Change it back + await members.api.productRepository.update({ + id: freeProduct.id, + welcome_page_url: freeProduct.get('welcome_page_url') + }); + } + }); + it('Will create a new member on signup', async function () { const email = 'not-existent-member@test.com'; const magicLink = await membersService.api.getMagicLink(email, 'signup'); @@ -98,7 +125,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); const member = await getMemberByEmail(email); @@ -129,7 +156,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); }); @@ -173,7 +200,7 @@ describe('Members Signin', function () { // Use a first time await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); // Fetch token in the database @@ -189,7 +216,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); await model.refresh(); @@ -226,17 +253,17 @@ describe('Members Signin', function () { // Use a first time await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); // Fetch token in the database @@ -534,7 +561,7 @@ describe('Members Signin', function () { await membersAgent.get(`/?token=${token}&action=signup`) .expectStatus(302) - .expectHeader('Location', /\/welcome-free\/$/) + .expectHeader('Location', /\/welcome-free\/\?success=true&action=signup$/) .expectHeader('Set-Cookie', /members-ssr.*/); const member = await getMemberByEmail(email); diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index d1393872e44..b3c45482973 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -583,11 +583,11 @@ test.describe('Updating post access', () => { await page.getByTestId('timezone').getByRole('button', {name: 'Edit'}).click(); await page.getByTestId('timezone-select').click(); - await page.locator('[data-testid="select-option"]', {hasText: 'Kamchatka'}).click(); + await page.locator('[data-testid="select-option"]', {hasText: 'Tokyo'}).click(); await page.getByTestId('timezone').getByRole('button', {name: 'Save'}).click(); await expect(page.getByTestId('timezone-select')).toBeHidden(); - await expect(page.getByTestId('timezone')).toContainText('(GMT +12:00) Fiji, Kamchatka, Marshall Is.'); + await expect(page.getByTestId('timezone')).toContainText('(GMT +9:00) Osaka, Sapporo, Tokyo'); await page.getByTestId('exit-settings').click(); await page.locator('[data-test-nav="posts"]').click(); @@ -595,9 +595,9 @@ test.describe('Updating post access', () => { await openPostSettingsMenu(page); - await expect(page.locator('[data-test-date-time-picker-timezone]')).toHaveText('+12'); - await expect(page.locator('[data-test-date-time-picker-time-input]')).toHaveValue('00:00'); - await expect(page.locator('[data-test-date-time-picker-date-input]')).toHaveValue(/-16$/); + await expect(page.locator('[data-test-date-time-picker-timezone]')).toHaveText('JST'); + await expect(page.locator('[data-test-date-time-picker-time-input]')).toHaveValue('21:00'); + await expect(page.locator('[data-test-date-time-picker-date-input]')).toHaveValue(/-15$/); }); test('default recipient settings - usually nobody', async ({page}) => { diff --git a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js index aecdea200e2..42470903563 100644 --- a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js +++ b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js @@ -154,31 +154,6 @@ const deleteAllMembers = async (page) => { } }; -/** - * Archive all tiers, 1 by 1, using the UI - * @param {import('@playwright/test').Page} page - */ -const archiveAllTiers = async (page) => { - // Navigate to the member settings - await page.locator('[data-test-nav="settings"]').click(); - await page.locator('[data-test-nav="members-membership"]').click(); - - // Tiers request can take time, so waiting until there is no connections before interacting with them - await page.waitForLoadState('networkidle'); - - // Expand the premium tier list - await page.locator('[data-test-toggle-pub-info]').click(); - - // Archive if already exists - while (await page.locator('.gh-tier-card').first().isVisible()) { - const tierCard = page.locator('.gh-tier-card').first(); - await tierCard.locator('.gh-tier-card-actions-button').click(); - await tierCard.getByRole('button', {name: 'Archive'}).click(); - await page.locator('.modal-content').getByRole('button', {name: 'Archive'}).click(); - await page.locator('.modal-content').waitFor({state: 'detached', timeout: 1000}); - } -}; - /** * Allows impersonating a member by copying the impersonate link * opens site with member logged in via the link @@ -528,7 +503,6 @@ module.exports = { setupMailgun, deleteAllMembers, createTier, - archiveAllTiers, createOffer, createMember, createPostDraft, diff --git a/ghost/core/test/e2e-frontend/members.test.js b/ghost/core/test/e2e-frontend/members.test.js index 44f014c8ab7..5ffe92168f5 100644 --- a/ghost/core/test/e2e-frontend/members.test.js +++ b/ghost/core/test/e2e-frontend/members.test.js @@ -194,9 +194,10 @@ describe('Front-end members behavior', function () { getJsonResponse.newsletters.should.have.length(1); // NOTE: these should be snapshots not code - Object.keys(getJsonResponse.newsletters[0]).should.have.length(4); + Object.keys(getJsonResponse.newsletters[0]).should.have.length(5); getJsonResponse.newsletters[0].should.have.properties([ 'id', + 'uuid', 'name', 'description', 'sort_order' @@ -231,9 +232,10 @@ describe('Front-end members behavior', function () { restoreJsonResponse.should.not.have.property('id'); restoreJsonResponse.newsletters.should.have.length(1); // @NOTE: this seems like too much exposed information, needs a review - Object.keys(restoreJsonResponse.newsletters[0]).should.have.length(4); + Object.keys(restoreJsonResponse.newsletters[0]).should.have.length(5); restoreJsonResponse.newsletters[0].should.have.properties([ 'id', + 'uuid', 'name', 'description', 'sort_order' @@ -663,9 +665,10 @@ describe('Front-end members behavior', function () { memberData.newsletters.should.have.length(1); // @NOTE: this should be a snapshot test not code - Object.keys(memberData.newsletters[0]).should.have.length(4); + Object.keys(memberData.newsletters[0]).should.have.length(5); memberData.newsletters[0].should.have.properties([ 'id', + 'uuid', 'name', 'description', 'sort_order' diff --git a/ghost/core/test/e2e-server/services/__snapshots__/recommendation-emails.test.js.snap b/ghost/core/test/e2e-server/services/__snapshots__/recommendation-emails.test.js.snap index c1ac41b64ff..d603e456bcc 100644 --- a/ghost/core/test/e2e-server/services/__snapshots__/recommendation-emails.test.js.snap +++ b/ghost/core/test/e2e-server/services/__snapshots__/recommendation-emails.test.js.snap @@ -108,6 +108,13 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a padding-left: 20px; border-left: 3px solid #DDE1E5; } + .recommendation-card--outlook { + margin: 0; + padding: 0; + width: 100%; + border: 1px solid #F9F9FA; + background: #F9F9FA; + } @@ -132,35 +139,33 @@ exports[`Incoming Recommendation Emails Sends a different email if we receive a

    A site you’re recommending is now recommending you back:

    -
    - +
    +
    Other Ghost Site
    -
    -
    - +
    +
    Other Ghost Site
    -
    -
    - +
    +
    Other Ghost Site
    -
    -
    - +
    +
    {{#if recommendation.favicon}}{{/if}}
    {{recommendation.title}}
    -
    {{recommendation.excerpt}}
    + {{#if recommendation.excerpt}} +
    {{recommendation.excerpt}}
    + {{/if}}
    {{#if recommendation.featuredImage}}
    @@ -52,26 +54,30 @@