diff --git a/.circleci/config.yml b/.circleci/config.yml index fccb6a5169b37..a2585678146e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ references: defaults: &defaults working_directory: ~/wp-calypso docker: - - image: cimg/node:12.20.1 + - image: cimg/node:14.16.1 environment: CIRCLE_ARTIFACTS: /tmp/artifacts CIRCLE_TEST_REPORTS: /tmp/test_results @@ -38,52 +38,6 @@ references: "$CIRCLE_TEST_REPORTS/e2ereports" \ "$HOME/jest-cache" - # Jest cache caching - # - # Jest uses a cache to speed up builds. If we persist this cache across builds, - # we can improve the speed of subsequent builds. - # - # Circle caches never overwritten, so we must ensure that Jest caches from different jobs - # do not collide or we'll only cache 1 job. - # - # We also need to ensure that different nodes and different total nodes do not collide. - # When we split tests, different nodes will receive a different set of tests so each node's - # cache should be unique. - # - # Finally, we cache on the branch and revision, falling back to origin/HEAD. This should give us - # pretty good "nearest neighbor" primer for the Jest cache. - # - # More about the CircleCI cache: https://circleci.com/docs/2.0/caching - restore-jest-cache: &restore-jest-cache - name: Restore Jest cache - keys: - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v8-jest-{{ .Environment.CIRCLE_JOB }}-{{ .Environment.CIRCLE_NODE_INDEX }}/{{ .Environment.CIRCLE_NODE_TOTAL }}-{{ .Branch }}-{{ .Revision }} - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v8-jest-{{ .Environment.CIRCLE_JOB }}-{{ .Environment.CIRCLE_NODE_INDEX }}/{{ .Environment.CIRCLE_NODE_TOTAL }}-{{ .Branch }} - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v8-jest-{{ .Environment.CIRCLE_JOB }}-{{ .Environment.CIRCLE_NODE_INDEX }}/{{ .Environment.CIRCLE_NODE_TOTAL }}-trunk - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v8-jest-{{ .Environment.CIRCLE_JOB }}-{{ .Environment.CIRCLE_NODE_INDEX }}/{{ .Environment.CIRCLE_NODE_TOTAL }} - save-jest-cache: &save-jest-cache - name: Save Jest cache - key: v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v8-jest-{{ .Environment.CIRCLE_JOB }}-{{ .Environment.CIRCLE_NODE_INDEX }}/{{ .Environment.CIRCLE_NODE_TOTAL }}-{{ .Branch }}-{{ .Revision }} - paths: - - ~/jest-cache - - # - # Build cache - # - # This contains caches used by the build process (mainly webpack loaders and plugins) - restore-build-cache: &restore-build-cache - name: Restore build cache - keys: - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v0-build-{{ .Branch }}-{{ .Revision }} - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v0-build-{{ .Branch }} - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v0-build-trunk - - v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v0-build - save-build-cache: &save-build-cache - name: Save build cache - key: v{{ .Environment.GLOBAL_CACHE_PREFIX }}-v0-build-{{ .Branch }}-{{ .Revision }} - paths: - - .cache - # Git cache # # Calypso is a big repository with a lot of history. It can take a long time to do a full checkout. @@ -183,10 +137,7 @@ references: npm install -g yarn yarn run build-desktop:install-app-deps desktop-cache-paths: &desktop-cache-paths - - desktop/build - - desktop/client - desktop/config - - desktop/public - desktop/resource/certificates/win.p12 - desktop/resource/certificates/mac.p12 desktop-decrypt-assets: &desktop-decrypt-assets @@ -337,256 +288,6 @@ jobs: } }' - typecheck-strict: - <<: *defaults - parallelism: 1 - steps: - - prepare - - run: - name: TypeScript strict typecheck of individual subprojects - command: yarn run tsc --project client/landing/gutenboarding - - lint: - <<: *defaults - parallelism: 1 - steps: - - prepare - - run: - name: Lint Config Keys - when: always - command: yarn run lint:config-defaults - - run: - name: Lint yarn.lock - when: always - command: | - yarn - DIRTY_FILES=$(git status --porcelain 2>/dev/null) - if [[ ! -z "$DIRTY_FILES" ]]; then - echo "Repository contains uncommitted changes: " - echo "$DIRTY_FILES" - echo "You need to checkout the branch, run 'yarn' and commit those files." - exit 1 - fi - - run: - name: Lint Client and Server - when: always - command: | - # We may not have files to lint which returns non-0 exit - # Ensure this does not cause job failure (see `|| exit 0`) - FILES_TO_LINT=$( - git diff --name-only --diff-filter=d origin... \ - | grep -E '^(client/|server/|packages/)' \ - | grep -E '\.[jt]sx?$' - ) || exit 0 - - if [[ ! -z $FILES_TO_LINT ]]; then - ./node_modules/.bin/eslint \ - --format junit \ - --output-file "$CIRCLE_TEST_REPORTS/eslint/results.xml" \ - $FILES_TO_LINT - fi - - store_test_results: - path: /tmp/test_results - - build-notifications: - <<: *defaults - parallelism: 1 - steps: - - prepare - - run: - name: Build Notifications Panel - command: | - cd apps/notifications/ && NODE_ENV=production yarn run build --output-path=$CIRCLE_ARTIFACTS/notifications-panel - - store-artifacts-and-test-results - - build-o2-blocks: - <<: *defaults - parallelism: 1 - steps: - - prepare - - run: - name: Build Gutenberg Blocks for internal p2s - command: | - cd apps/o2-blocks/ && NODE_ENV=production yarn run build --output-path=$CIRCLE_ARTIFACTS/o2-blocks - - store-artifacts-and-test-results - - build-wpcom-block-editor: - <<: *defaults - parallelism: 1 - steps: - - prepare - - run: - name: Build the block editor in WordPress.com integration utils package - command: | - cd apps/wpcom-block-editor/ && yarn build --output-path=$CIRCLE_ARTIFACTS/wpcom-block-editor - - store-artifacts-and-test-results - - test-client: - <<: *defaults - parallelism: 6 - steps: - - prepare - - restore_cache: *restore-jest-cache - - run: - name: Run Client Tests - no_output_timeout: 2m - command: | - # Use Jest to list tests to run via config - ./node_modules/.bin/jest \ - --listTests \ - --config=test/client/jest.config.js \ - > ~/jest-tests - - # Run jest on the CircleCI split for parallelization across containers - # Avoid using `--split-by=timings` here so that per-node Jest caches - # receive a stable sub-set of tests for optimal cache usage. - JEST_JUNIT_OUTPUT_DIR="$CIRCLE_TEST_REPORTS/client" \ - JEST_JUNIT_OUTPUT_NAME="results.xml" \ - ./node_modules/.bin/jest \ - --cacheDirectory="$HOME/jest-cache" \ - --ci \ - --maxWorkers=2 \ - --reporters=default \ - --reporters=jest-junit \ - --runTestsByPath \ - --silent \ - --config=test/client/jest.config.js \ - $( circleci tests split < ~/jest-tests ) - - save_cache: *save-jest-cache - - store-artifacts-and-test-results - - test-integration: - <<: *defaults - parallelism: 1 - steps: - - prepare - - restore_cache: *restore-jest-cache - - run: - name: Run Integration Tests - command: | - JEST_JUNIT_OUTPUT_DIR="$CIRCLE_TEST_REPORTS/integration" \ - JEST_JUNIT_OUTPUT_NAME="results.xml" \ - ./node_modules/.bin/jest \ - --cacheDirectory="$HOME/jest-cache" \ - --ci \ - --maxWorkers=2 \ - --reporters=default \ - --reporters=jest-junit \ - --silent \ - --config=test/integration/jest.config.js - - save_cache: *save-jest-cache - - store-artifacts-and-test-results - - test-packages: - <<: *defaults - parallelism: 1 - steps: - - prepare - - restore_cache: *restore-jest-cache - - run: - name: Run Package Tests - no_output_timeout: 2m - command: | - JEST_JUNIT_OUTPUT_DIR="$CIRCLE_TEST_REPORTS/packages" \ - JEST_JUNIT_OUTPUT_NAME="results.xml" \ - ./node_modules/.bin/jest \ - --cacheDirectory="$HOME/jest-cache" \ - --ci \ - --maxWorkers=2 \ - --reporters=default \ - --reporters=jest-junit \ - --silent \ - --config=test/packages/jest.config.js - - save_cache: *save-jest-cache - - store-artifacts-and-test-results - - test-server: - <<: *defaults - parallelism: 1 - steps: - - prepare - - restore_cache: *restore-jest-cache - - run: - name: Run Server Tests - no_output_timeout: 2m - command: | - JEST_JUNIT_OUTPUT_DIR="$CIRCLE_TEST_REPORTS/server" \ - JEST_JUNIT_OUTPUT_NAME="results.xml" \ - ./node_modules/.bin/jest \ - --cacheDirectory="$HOME/jest-cache" \ - --ci \ - --maxWorkers=2 \ - --reporters=default \ - --reporters=jest-junit \ - --silent \ - --config=test/server/jest.config.js - - save_cache: *save-jest-cache - - store-artifacts-and-test-results - - # Prime calypso.live so it has a build ready - # - # We can send a request to calypso.live so that it gets a build ready. - # This saves time waiting later when waiting for the calypso.live - # - # Expected usage: - # - After setup - # - Only on branches (not the primary branch) - # - prime-calypso-live: - docker: - - image: buildpack-deps - working_directory: ~/wp-calypso - steps: - - run: - name: Prime calypso.live - command: | - if [[ -z $CIRCLE_PR_USERNAME ]]; then - curl --silent "https://hash-$CIRCLE_SHA1.calypso.live" - fi - - # Wait for calypso.live to be ready - # - # Expected usage: - # - After main tests have passed (test-client, test-server) - # - Before e2e tests run - # - wait-calypso-live: - docker: - - image: buildpack-deps - working_directory: ~/wp-calypso - steps: - - run: - name: Check external author - command: | - if [[ ! -z $CIRCLE_PR_USERNAME ]]; then - echo 'PRs from external authors cannot run on calypso.live' - exit 1 - fi - - restore_cache: *restore-git-cache - - checkout - # Don't bother updating the git cache here, it would be the second time in the workflow - - run: - name: Wait for calypso.live build - command: ./test/e2e/scripts/wait-for-running-branch.sh - - test-e2e: - <<: *defaults - working_directory: ~/wp-calypso/test/e2e - parallelism: 2 - steps: - - prepare - - run: *set-e2e-variables - - run: *install-specific-chrome-browser - - run: *save-chrome-version - - run: sudo apt-get install -y xvfb - - run: yarn run decryptconfig - - run: ./scripts/randomize.sh specs - - run: ./scripts/run-wrapper.sh - - run: *move-e2e-artifacts - - store-artifacts-and-test-results - - slack/status: - webhook: $SLACK_E2E - test-e2e-canary: <<: *defaults working_directory: ~/wp-calypso/test/e2e @@ -658,23 +359,19 @@ jobs: - slack/status: webhook: << parameters.slack-webhook >> - wp-desktop-source: - # Use Linux machine executor, NOT docker image for higher available RAM (7.5GB) - machine: - image: ubuntu-1604:202004-01 - resource_class: medium + wp-desktop-assets: + docker: + - image: circleci/node:12.16.2-browsers <<: *desktop_defaults environment: - NVM_DIR: '/opt/circleci/.nvm' VERSION: << pipeline.git.tag >> - CHROMEDRIVER_SKIP_DOWNLOAD: 'true' - DETECT_CHROMEDRIVER_VERSION: 'false' - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 'true' - NODE_OPTIONS: --max-old-space-size=6144 - YARN_CACHE_FOLDER: ~/.cache/yarn CONFIG_ENV: release + working_directory: ~/wp-calypso steps: - checkout + - attach_workspace: + at: ~/wp-calypso + - run: *update-node - when: condition: << pipeline.git.tag >> steps: @@ -682,66 +379,8 @@ jobs: name: Ensure package.json Version And Tag Match command: cd desktop/bin && node validate_tag.js $VERSION - run: *desktop-decrypt-assets - - run: - name: Compute cache checksums - command: | - # Get latest commit that modified packages directory - echo "$(git log -n 1 --format='%h' -- packages .nvmrc yarn.lock)" > packages-hash - - restore_cache: *desktop-restore-yarn-cache - - restore_cache: *desktop-restore-packages-cache - - restore_cache: *desktop-restore-calypso-build-cache - - run: - name: Install Linux deps - command: | - sudo apt update - sudo apt-get install -y git-restore-mtime bc - - run: - name: Restore file modified time - command: | - # Webpack cache-loader depends on file mtime, which is reset by "git clone". - # Use git-restore-mtime to restore original modification time of files based - # on date of most recent commit that modified them. - # Ref: http://manpages.ubuntu.com/manpages/bionic/man1/git-restore-mtime.1.html - # - # Note: this utility is highly optimized and is able to process all Calypso - # files in less than 10 seconds! - /usr/lib/git-core/git-restore-mtime --force --commit-time --skip-missing - - run: - name: Install Dependencies - command: | - # Install Node - # Note: this syntax is required to load NVM_DIR in the machine executor. - # Ref: https://discuss.circleci.com/t/circleci-forgetting-node-version-on-machine-executor/28813 - set +e - set +x - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install - nvm use - npm install -g yarn - - # defer to restored packages whenever possible - # On a Machine executor, using cached built packages is significant: - # https://github.com/Automattic/wp-calypso/pull/44696#discussion_r468010942 - if [ -f packages-cache-restored ]; then - export DISABLEPOSTINSTALL=1 - fi - yarn install --frozen-lockfile - echo "$(cat packages-hash)" > packages-cache-restored - - save_cache: *desktop-save-yarn-cache - - run: - name: Build Desktop and Calypso source - no_output_timeout: 15m - command: | - set +e - set +x - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm use - - yarn run build-desktop:source - - save_cache: *desktop-save-packages-cache - - save_cache: *desktop-save-calypso-build-cache - persist_to_workspace: - root: /home/circleci/wp-calypso + root: ~/wp-calypso paths: *desktop-cache-paths - run: *desktop-notify-github-failure - slack/status: *desktop-notify-slack-failure @@ -791,7 +430,7 @@ jobs: name: Persist Mac Executable command: | # If this isn't a full artifact build, ensure to persist the built application for inspection - if ls desktop/release/*.zip &>/dev/null + if ! ls desktop/release/*.zip &>/dev/null then ditto -ck --rsrc --sequesterRsrc desktop/release/mac desktop/release/mac.app.zip fi @@ -820,7 +459,7 @@ jobs: wp-desktop-linux: docker: - - image: circleci/node:12.20.1-browsers + - image: circleci/node:14.16.1-browsers <<: *desktop_defaults shell: /bin/bash --login steps: @@ -847,7 +486,7 @@ jobs: # Otherwise only build application executable required for end-to-end testing. ! git diff --name-only origin/trunk...HEAD | grep -E -q 'desktop/package.json|desktop/yarn.lock' && ELECTRON_BUILDER_ARGS='-c.linux.target=dir' - ELECTRON_BUILDER_ARGS=$ELECTRON_BUILDER_ARGS yarn run build-desktop:app + ELECTRON_BUILDER_ARGS=$ELECTRON_BUILDER_ARGS yarn run build-desktop - run: name: e2e Tests command: | @@ -934,6 +573,7 @@ jobs: If ( -Not $(git diff --name-only origin/trunk...HEAD | Select-String -Pattern desktop/package.json,desktop/yarn.lock) ) { $env:ARG2='-c.win.target=dir' } + make -f desktop/Makefile build-main make -f desktop/Makefile package ELECTRON_BUILDER_ARGS=$($env:ARG1,$env:ARG2 -join " ") - run: name: Archive Unpacked Directories @@ -968,7 +608,7 @@ jobs: # GitHub release and generate the release notes using the Git commits history wp-desktop-publish: docker: - - image: circleci/golang:1.12 + - image: circleci/golang:1.12-node working_directory: /home/circleci/wp-calypso environment: VERSION: << pipeline.git.tag >> @@ -979,9 +619,14 @@ jobs: - run: name: Install Dependencies command: go get github.com/tcnksm/ghr + - run: + name: Update wp-desktop repo README + command: | + node desktop/bin/github/update-desktop-repo-readme.js - run: name: Publish Github Release command: | + VERSION="${VERSION#desktop-}" echo "Publishing draft release for wp-desktop $VERSION..." NAME="WP-Desktop ${VERSION#?}" @@ -989,10 +634,10 @@ jobs: ./desktop/bin/make-changelog.sh > desktop/CHANGELOG.md ghr \ - --token "${GH_TOKEN}" \ + --token "${WP_DESKTOP_SECRET}" \ --username "${CIRCLE_PROJECT_USERNAME}" \ - --repository "${CIRCLE_PROJECT_REPONAME}" \ - --commitish "${CIRCLE_SHA1}" \ + --repository "wp-desktop" \ + --commitish "trunk" \ --name "${NAME}" \ --body "$(cat desktop/CHANGELOG.md)" \ --delete \ @@ -1006,151 +651,70 @@ workflows: calypso: jobs: - setup - - prime-calypso-live: - filters: - branches: - ignore: trunk - - build-notifications: - requires: - - setup - filters: - branches: - only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - build-o2-blocks: - requires: - - setup - filters: - branches: - only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - build-wpcom-block-editor: - requires: - - setup - filters: - branches: - only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - translate: requires: - setup - - lint: - requires: - - setup - filters: - branches: - only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - test-client: - requires: - - setup - filters: - branches: - only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - test-packages: - requires: - - setup + wp-desktop: + jobs: + - wp-desktop-assets: filters: branches: only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - test-server: + - trunk + - /release\/.*/ + - /desktop\/.*/ + - wp-desktop-mac: requires: - - setup + - wp-desktop-assets filters: branches: only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - typecheck-strict: + - trunk + - /release\/.*/ + - /desktop\/.*/ + - wp-desktop-linux: requires: - - setup + - wp-desktop-assets filters: branches: only: - # Only run for fork pull requets. - - /pull\/[0-9]+/ - - wait-calypso-live: - requires: - - setup - filters: - branches: - ignore: - # Do not spin up calypso.live for `trunk` - trunk - # Do not spin up calypso.live for fork pull requests. Calypso.live will not build them. - - /pull\/[0-9]+/ - # - test-e2e-canary: - # requires: - # - wait-calypso-live - # test-flags: '-C -S $CIRCLE_SHA1' - - test-e2e-canary: - name: test-e2e-canary-ie - requires: - - wait-calypso-live - test-flags: '-z -S $CIRCLE_SHA1' - test-target: 'IE11' - slack-webhook: '$SLACK_IE' - - test-e2e-canary: - name: test-e2e-canary-safari + - /release\/.*/ + - /desktop\/.*/ + - wp-desktop-windows: requires: - - wait-calypso-live - test-flags: '-y -S $CIRCLE_SHA1' - wp-desktop: - jobs: - - wp-desktop-source: + - wp-desktop-assets filters: branches: only: - trunk - /release\/.*/ - /desktop\/.*/ - - wp-desktop-mac: - requires: - - wp-desktop-source - # Even though a TeamCity Linux job is ran against trunk, - # we should continue to periodically validate the Circle - # job to ensure CircleCI continues to work on all - # platforms since the application is deployed via CircleCI. - # This also provides a benchmark and fallback for TeamCity. - - wp-desktop-linux: - requires: - - wp-desktop-source - - wp-desktop-windows: - requires: - - wp-desktop-source wp-desktop-release: when: << pipeline.git.tag >> jobs: - - wp-desktop-source: + - wp-desktop-assets: filters: tags: - only: /v.*/ + only: /desktop-v.*/ - wp-desktop-mac: requires: - - wp-desktop-source + - wp-desktop-assets filters: tags: - only: /v.*/ + only: /desktop-v.*/ - wp-desktop-linux: requires: - - wp-desktop-source + - wp-desktop-assets filters: tags: - only: /v.*/ + only: /desktop-v.*/ - wp-desktop-windows: requires: - - wp-desktop-source + - wp-desktop-assets filters: tags: - only: /v.*/ + only: /desktop-v.*/ - wp-desktop-publish: requires: - wp-desktop-mac @@ -1158,119 +722,7 @@ workflows: - wp-desktop-windows filters: tags: - only: /v.*/ - calypso-nightly: - jobs: - - setup - - test-client: - requires: - - setup - - test-integration: - requires: - - setup - - test-packages: - requires: - - setup - - test-server: - requires: - - setup - triggers: - - schedule: - cron: '0 4 * * *' - filters: - branches: - only: - - trunk - - e2e-full-suite-scheduled: - jobs: - - setup - - test-e2e: - requires: - - setup - triggers: - - schedule: - cron: '0 3,15 * * *' - filters: - branches: - only: trunk - - e2e-canary-scheduled: - jobs: - - setup - - test-e2e-canary: - name: test-e2e-canary-ie-sched - requires: - - setup - test-flags: '-w' - test-target: 'IE11' - slack-webhook: '$SLACK_IE' - - test-e2e-canary: - name: test-e2e-canary-woo-sched - requires: - - setup - test-flags: '-W' - test-target: 'WOO' - slack-webhook: '$SLACK_WOO' - triggers: - - schedule: - cron: '0 12 * * *' - filters: - branches: - only: trunk - - e2e-full-suite-edge-scheduled: - jobs: - - setup - - test-e2e-canary: - name: test-e2e-full-suite-edge - requires: - - setup - test-flags: '-g' - env-vars: 'SKIP_DOMAIN_TESTS=true GUTENBERG_EDGE=true' - triggers: - - schedule: - cron: '30 8 * * *' - filters: - branches: - only: trunk - - e2e-jetpack-be-scheduled: - jobs: - - setup - - test-e2e-canary: - name: test-e2e-jetpack - requires: - - setup - test-flags: '-j -s desktop' - jetpack-host: 'PRESSABLEBLEEDINGEDGE' - test-target: 'JETPACK' - slack-webhook: '$SLACK_JP' - triggers: - - schedule: - cron: '0 7 * * *' - filters: - branches: - only: trunk - - # Temporarily disabling these scheduled test runs - # e2e-jetpack-scheduled: - # jobs: - # - setup - # - test-e2e-canary: - # name: test-e2e-jetpack - # requires: - # - setup - # test-flags: "-j" - # jetpack-host: "PRESSABLE" - # test-target: "JETPACK" - # slack-webhook: "$SLACK_JP" - # triggers: - # - schedule: - # cron: "0 1,13 * * *" - # filters: - # branches: - # only: trunk + only: /desktop-v.*/ e2e-canary-i18n-monthly: jobs: diff --git a/.eslintrc.js b/.eslintrc.js index b39b9b9f97d2e..21a91f443e1ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,14 +1,19 @@ const { merge } = require( 'lodash' ); const reactVersion = require( './client/package.json' ).dependencies.react; +const path = require( 'path' ); module.exports = { root: true, + parserOptions: { + babelOptions: { + configFile: path.join( __dirname, './babel.config.js' ), + }, + }, extends: [ 'plugin:wpcalypso/react', 'plugin:jsx-a11y/recommended', 'plugin:jest/recommended', 'plugin:prettier/recommended', - 'prettier/react', 'plugin:md/prettier', 'plugin:@wordpress/eslint-plugin/i18n', ], @@ -47,20 +52,8 @@ module.exports = { files: [ 'packages/**/*' ], rules: { // These two rules are to ensure packages don't import from calypso by accident to avoid circular deps. - 'no-restricted-imports': [ - 'error', - { - patterns: [ 'calypso/*' ], - message: "Packages shouldn't import from calypso", - }, - ], - 'no-restricted-modules': [ - 'error', - { - patterns: [ 'calypso/*' ], - message: "Packages shouldn't import from calypso", - }, - ], + 'no-restricted-imports': [ 'error', { patterns: [ 'calypso/*' ] } ], + 'no-restricted-modules': [ 'error', { patterns: [ 'calypso/*' ] } ], }, }, { @@ -73,9 +66,20 @@ module.exports = { }, }, { - files: [ 'test/e2e/**/*' ], + plugins: [ 'mocha' ], + files: [ + 'test/e2e/**/*', + 'packages/magellan-mocha-plugin/test/**/*', + 'packages/magellan-mocha-plugin/test_support/**/*', + ], rules: { 'import/no-nodejs-modules': 'off', + 'mocha/no-exclusive-tests': 'error', + 'mocha/handle-done-callback': [ 'error', { ignoreSkipped: true } ], + 'mocha/no-global-tests': 'error', + 'mocha/no-async-describe': 'error', + 'mocha/no-top-level-hooks': 'error', + 'mocha/max-top-level-suites': [ 'error', { limit: 1 } ], 'no-console': 'off', // Disable all rules from "plugin:jest/recommended", as e2e tests use mocha ...Object.keys( require( 'eslint-plugin-jest' ).configs.recommended.rules ).reduce( @@ -102,7 +106,7 @@ module.exports = { .overrides[ 0 ].rules, }, // Prettier rules config - require( 'eslint-config-prettier/@typescript-eslint' ), + require( 'eslint-config-prettier' ), // Our own overrides { files: [ '**/*.ts', '**/*.tsx' ], @@ -151,6 +155,12 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', // REST API objects include underscores '@typescript-eslint/camelcase': 'off', + + // TypeScript compiler already takes care of these errors + 'import/no-extraneous-dependencies': 'off', + 'import/named': 'off', + 'import/namespace': 'off', + 'import/default': 'off', }, } ), @@ -212,7 +222,7 @@ module.exports = { // this is when Webpack last built the bundle BUILD_TIMESTAMP: true, }, - plugins: [ 'import' ], + plugins: [ 'import', 'you-dont-need-lodash-underscore' ], settings: { react: { version: reactVersion, @@ -272,8 +282,6 @@ module.exports = { 2, { paths: [ - // Error if any module depends on the data-observe mixin, which is deprecated. - 'lib/mixins/data-observe', // Prevent naked import of gridicons module. Use 'components/gridicon' instead. { name: 'gridicons', @@ -309,8 +317,6 @@ module.exports = { 2, { paths: [ - // Error if any module depends on the data-observe mixin, which is deprecated. - 'lib/mixins/data-observe', // Prevent naked import of gridicons module. Use 'components/gridicon' instead. { name: 'gridicons', @@ -376,11 +382,21 @@ module.exports = { // Force packages to declare their dependencies 'import/no-extraneous-dependencies': 'error', + 'import/named': 'error', + 'import/namespace': 'error', + 'import/default': 'error', + 'import/no-duplicates': 'error', 'wpcalypso/no-unsafe-wp-apis': [ 'error', { - '@wordpress/block-editor': [ '__experimentalBlock', '__experimentalInserterMenuExtension' ], + '@wordpress/block-editor': [ + '__experimentalBlock', + // InserterMenuExtension has been made unstable in this PR: https://github.com/WordPress/gutenberg/pull/31417 / Gutenberg v10.7.0-rc.1. + // We need to support both symbols until we're sure Gutenberg < v10.7.x is not used anymore in WPCOM. + '__unstableInserterMenuExtension', + '__experimentalInserterMenuExtension', + ], '@wordpress/date': [ '__experimentalGetSettings' ], '@wordpress/edit-post': [ '__experimentalMainDashboardButton' ], '@wordpress/components': [ '__experimentalNavigationBackButton' ], @@ -389,5 +405,53 @@ module.exports = { // Disabled, because in packages we are using globally defined `__i18n_text_domain__` constant at compile time '@wordpress/i18n-text-domain': 'off', + + // Disable Lodash methods that we've already migrated away from, see p4TIVU-9Bf-p2 for more details. + 'you-dont-need-lodash-underscore/all': 'error', + 'you-dont-need-lodash-underscore/any': 'error', + 'you-dont-need-lodash-underscore/assign': 'error', + 'you-dont-need-lodash-underscore/bind': 'error', + 'you-dont-need-lodash-underscore/cast-array': 'error', + 'you-dont-need-lodash-underscore/collect': 'error', + 'you-dont-need-lodash-underscore/contains': 'error', + 'you-dont-need-lodash-underscore/detect': 'error', + 'you-dont-need-lodash-underscore/drop': 'error', + 'you-dont-need-lodash-underscore/drop-right': 'error', + 'you-dont-need-lodash-underscore/each': 'error', + 'you-dont-need-lodash-underscore/ends-with': 'error', + 'you-dont-need-lodash-underscore/entries': 'error', + 'you-dont-need-lodash-underscore/every': 'error', + 'you-dont-need-lodash-underscore/extend-own': 'error', + 'you-dont-need-lodash-underscore/fill': 'error', + 'you-dont-need-lodash-underscore/first': 'error', + 'you-dont-need-lodash-underscore/foldl': 'error', + 'you-dont-need-lodash-underscore/foldr': 'error', + 'you-dont-need-lodash-underscore/index-of': 'error', + 'you-dont-need-lodash-underscore/inject': 'error', + 'you-dont-need-lodash-underscore/is-array': 'error', + 'you-dont-need-lodash-underscore/is-finite': 'error', + 'you-dont-need-lodash-underscore/is-function': 'error', + 'you-dont-need-lodash-underscore/is-integer': 'error', + 'you-dont-need-lodash-underscore/is-nan': 'error', + 'you-dont-need-lodash-underscore/is-nil': 'error', + 'you-dont-need-lodash-underscore/is-null': 'error', + 'you-dont-need-lodash-underscore/is-string': 'error', + 'you-dont-need-lodash-underscore/is-undefined': 'error', + 'you-dont-need-lodash-underscore/join': 'error', + 'you-dont-need-lodash-underscore/last-index-of': 'error', + 'you-dont-need-lodash-underscore/pad-end': 'error', + 'you-dont-need-lodash-underscore/pad-start': 'error', + 'you-dont-need-lodash-underscore/reduce-right': 'error', + 'you-dont-need-lodash-underscore/repeat': 'error', + 'you-dont-need-lodash-underscore/replace': 'error', + 'you-dont-need-lodash-underscore/reverse': 'error', + 'you-dont-need-lodash-underscore/select': 'error', + 'you-dont-need-lodash-underscore/slice': 'error', + 'you-dont-need-lodash-underscore/split': 'error', + 'you-dont-need-lodash-underscore/take-right': 'error', + 'you-dont-need-lodash-underscore/to-lower': 'error', + 'you-dont-need-lodash-underscore/to-pairs': 'error', + 'you-dont-need-lodash-underscore/to-upper': 'error', + 'you-dont-need-lodash-underscore/uniq': 'error', }, }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 87384e57ab515..ad863b7f95e38 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,26 +15,27 @@ # Customer Home /client/my-sites/customer-home @Automattic/manage -# G Suite -/client/components/data/query-gsuite-users @Automattic/cobalt -/client/components/domains/contact-details-form-fields/index.jsx @Automattic/cobalt -/client/components/gsuite @Automattic/cobalt -/client/components/marketing-survey/gsuite-cancel-purchase-dialog @Automattic/cobalt -/client/components/upgrades/gsuite @Automattic/cobalt -/client/lib/domains/email-forwarding @Automattic/cobalt -/client/lib/gsuite @Automattic/cobalt -/client/me/purchases/remove-purchase/index.jsx @Automattic/cobalt -/client/my-sites/checkout/checkout-thank-you/google-apps-details.jsx @Automattic/cobalt -/client/my-sites/checkout/checkout-thank-you/index.jsx @Automattic/cobalt -/client/my-sites/checkout/checkout/domain-details-form.jsx @Automattic/cobalt -/client/my-sites/checkout/checkout/index.jsx @Automattic/cobalt -/client/my-sites/checkout/controller.jsx @Automattic/cobalt -/client/my-sites/checkout/gsuite-nudge @Automattic/cobalt -/client/my-sites/domains/components/domain-warnings @Automattic/cobalt -/client/my-sites/domains/controller.jsx @Automattic/cobalt -/client/my-sites/email @Automattic/cobalt -/client/state/data-layer/wpcom/gsuite-users @Automattic/cobalt -/client/state/gsuite-users @Automattic/cobalt +# Email services +/client/components/data/query-gsuite-users @Automattic/letero +/client/components/domains/contact-details-form-fields/index.jsx @Automattic/letero +/client/components/gsuite @Automattic/letero +/client/components/marketing-survey/gsuite-cancel-purchase-dialog @Automattic/letero +/client/components/upgrades/gsuite @Automattic/letero +/client/lib/domains/email-forwarding @Automattic/letero +/client/lib/gsuite @Automattic/letero +/client/lib/titan @Automattic/letero +/client/me/purchases/remove-purchase/index.jsx @Automattic/letero +/client/my-sites/checkout/checkout-thank-you/google-apps-details.jsx @Automattic/letero +/client/my-sites/checkout/checkout-thank-you/index.jsx @Automattic/letero +/client/my-sites/checkout/checkout/domain-details-form.jsx @Automattic/letero +/client/my-sites/checkout/checkout/index.jsx @Automattic/letero +/client/my-sites/checkout/controller.jsx @Automattic/letero +/client/my-sites/checkout/gsuite-nudge @Automattic/letero +/client/my-sites/domains/components/domain-warnings @Automattic/letero +/client/my-sites/domains/controller.jsx @Automattic/letero +/client/my-sites/email @Automattic/letero +/client/state/data-layer/wpcom/gsuite-users @Automattic/letero +/client/state/gsuite-users @Automattic/letero # Payments /client/blocks/credit-card-form/ @Automattic/shilling @@ -67,6 +68,7 @@ /packages/composite-checkout/ @Automattic/shilling /packages/calypso-stripe/ @Automattic/shilling /packages/shopping-cart/ @Automattic/shilling +/packages/wpcom-checkout/ @Automattic/shilling # Reader /client/reader @Automattic/reader @@ -82,6 +84,7 @@ /apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse @Automattic/cylon /apps/editing-toolkit/editing-toolkit-plugin/posts-list-block @Automattic/ajax /apps/editing-toolkit/editing-toolkit-plugin/global-styles @Automattic/cylon @nosolosw +/apps/editing-toolkit/editing-toolkit-plugin/block-patterns @Automattic/dotcom-view-design # Editing Toolkit Workflow Files /.github/workflows/editing-toolkit-plugin.yml @noahtallen @@ -89,7 +92,40 @@ # Experimentation platform legacy library and new client /client/lib/abtest @Automattic/experimentation-platform -/client/components/data/query-experiments @Automattic/experimentation-platform -/client/components/experiment @Automattic/experimentation-platform -/client/state/data-layer/wpcom/experiments @Automattic/experimentation-platform -/client/state/experiments @Automattic/experimentation-platform +/client/lib/explat @Automattic/experimentation-platform +/packages/explat-* @Automattic/experimentation-platform + +# Framework +/packages/js-utils @Automattic/team-calypso-frameworks + +# Jetpack Search +/client/my-sites/jetpack-search @Automattic/jetpack-search + +# TeamCity configuration +/.teamcity @Automattic/team-calypso-platform + +# I18n +/bin/build-languages.js @Automattic/i18n +/build-tools/webpack/generate-chunks-map-plugin.js @Automattic/i18n +/build-tools/webpack/require-chunk-callback-plugin.js @Automattic/i18n +/client/boot/locale.js @Automattic/i18n +/client/components/calypso-i18n-provider @Automattic/i18n +/client/components/community-translator @Automattic/i18n +/client/components/language-picker @Automattic/i18n +/client/components/locale-suggestions @Automattic/i18n +/client/components/localized-moment @Automattic/i18n +/client/components/translatable @Automattic/i18n +/client/components/translator-invite @Automattic/i18n +/client/layout/community-translator @Automattic/i18n +/client/lib/i18n-utils @Automattic/i18n +/client/state/i18n @Automattic/i18n +/packages/babel-plugin-i18n-calypso @Automattic/i18n +/packages/i18n-calypso @Automattic/i18n +/packages/i18n-calypso-cli @Automattic/i18n +/packages/i18n-utils @Automattic/i18n +/packages/language-picker @Automattic/i18n +/packages/languages @Automattic/i18n + +# E2E specs +/test/e2e @WunderBart @scinos +/packages/calypso-e2e @worldomonation diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f3e04e610f1ff..6a259b3e1f3b8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Suggest an idea for this project title: '' -labels: Feature request +labels: "[Type] Feature Request" assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/flaky-e2e-spec-report.md b/.github/ISSUE_TEMPLATE/flaky-e2e-spec-report.md new file mode 100644 index 0000000000000..08b9816d2ddbf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/flaky-e2e-spec-report.md @@ -0,0 +1,23 @@ +--- +name: Flaky E2E spec report +about: Noticed an erratic E2E spec? Report it! +title: 'E2E: Flaky spec report' +labels: Flaky e2e +assignees: '' + +--- + + + +This spec seems to be flaky so let's try to fix it. + +#### Failing spec details + +From : + +``` +# Paste the full error info from the CI job here +``` diff --git a/.github/ISSUE_TEMPLATE/happiness-bug-report.md b/.github/ISSUE_TEMPLATE/happiness-bug-report.md new file mode 100644 index 0000000000000..d542d78a66eae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/happiness-bug-report.md @@ -0,0 +1,37 @@ +--- +name: Happiness Bug report +about: Create a report to help us improve +title: '' +labels: User Report, [Type] Bug +assignees: '' + +--- + + + +### Steps to reproduce the behavior + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +#### What I expected to happen + +#### What actually happened + +### Context + +#### Browser / OS version + +#### Is this specific to the applied theme? Which one? + +#### Does this happen on simple or atomic sites or both? + +#### Is there any console output or error text? + +#### Level of impact (Does it block purchases? Does it affect more than just one site?) + +#### Reproducibility (Consistent, Intermittent) Leave empty for consistent. + +#### Screenshot / Video: If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/simple-atomic-parity.md b/.github/ISSUE_TEMPLATE/simple-atomic-parity.md new file mode 100644 index 0000000000000..58390b261c5c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/simple-atomic-parity.md @@ -0,0 +1,26 @@ +--- +name: Simple/Atomic Feature Parity +about: Did you find something inconsistent or unexpected between the two platforms? +title: '' +labels: Simple/Atomic Parity +assignees: '' + +--- + +### What plan tier is the site on? + +### Where are you seeing the issue? + +1. Go to '...' +2. Click on '....' +3. See it! + +### What I expected on the WordPress.com site + +#### What I expected on the Atomic site + +#### What actually happened + +#### Is this specific to the applied theme? Which one? + +#### Screenshot / Video: If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000000000..ff3b75761f6c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,14 @@ +--- +name: Task +about: Create a task +title: '' +labels: "[Type] Task" +assignees: '' + +--- + +**Task Details** +<-- Details of the task to be added here. Try to be specific and reasonably sized. --> + +**Related to** +<-- If there are any other GitHub issue(s) that readers should be aware of, include them here. --> diff --git a/.github/workflows/calypso-live.yml b/.github/workflows/calypso-live.yml new file mode 100644 index 0000000000000..a50f793a5cd87 --- /dev/null +++ b/.github/workflows/calypso-live.yml @@ -0,0 +1,42 @@ +name: Calypso Live + +on: + pull_request: + types: ['opened'] + +jobs: + calypso-live: + name: 'Launch a Calypso.live instance for your branch' + runs-on: ubuntu-latest + # We only offer the Calypso.live link to PRs created from the Automattic organization. + if: github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + timeout-minutes: 10 + + steps: + - name: Build Calypso.live link. + run: | + echo '::set-output name=LINK::https://calypso.live/?branch=${{ github.event.pull_request.head.ref }}' + id: build_link + + - name: Post comment on PR + uses: actions/github-script@v3 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Test live: ${{ steps.build_link.outputs.LINK }}' + }) + + # Load the page so the build process starts before the PR author even clicks on the link. + - name: Fetch link + uses: fjogeleit/http-request-action@v1.8.0 + id: fetch_link + with: + url: '${{ steps.build_link.outputs.LINK }}' + method: 'GET' + timeout: '120' + preventFailureOnNoResponse: true + ignoreStatusCodes: '400,401' diff --git a/.github/workflows/editing-toolkit-plugin.yml b/.github/workflows/editing-toolkit-plugin.yml index b1cf3e4a9d3fc..72d3c6d946ace 100644 --- a/.github/workflows/editing-toolkit-plugin.yml +++ b/.github/workflows/editing-toolkit-plugin.yml @@ -8,13 +8,14 @@ name: Editing Toolkit Plugin jobs: build: - name: Build plugin + name: Run PHPunit tests. runs-on: ubuntu-latest steps: - name: Set up Node uses: actions/setup-node@v1 with: - node-version: '^12.20.1' + node-version: '^14.16.1' + - name: Checkout code uses: actions/checkout@HEAD @@ -31,7 +32,7 @@ jobs: path: | node_modules */*/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-1 - name: Install dependencies if: steps.cache.outputs.cache-hit != 'true' @@ -45,108 +46,6 @@ jobs: run: yarn build working-directory: apps/editing-toolkit - - name: Upload build artifact - uses: actions/upload-artifact@v1 - with: - name: editing-toolkit-build-archive - path: apps/editing-toolkit/editing-toolkit-plugin - - jest: - name: Run JS tests - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@HEAD - - # https://github.com/actions/cache/blob/HEAD/examples.md#node---lerna - - name: Restore node_modules cache - id: cache - uses: actions/cache@HEAD - with: - path: | - node_modules - */*/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install --frozen-lockfile # Not needed when restoring from cache. - - # It saves a bit of time to download the artifact rather than doing a build. - - name: Get build - uses: actions/download-artifact@HEAD - with: - name: editing-toolkit-build-archive - path: apps/editing-toolkit/editing-toolkit-plugin - - - name: Test JavaScript - run: yarn run test:js - working-directory: apps/editing-toolkit - - mc_upload: - name: Create wpcom sync diff - needs: build - runs-on: ubuntu-latest - env: - CALYPSO_APP: editing_toolkit_plugin - TRIGGER_CALYPSO_APP_BUILD_ENDPOINT: ${{ secrets.TRIGGER_CALYPSO_APP_BUILD_ENDPOINT }} - steps: - - name: Checkout code - uses: actions/checkout@HEAD - - # It saves a bit of time to download the artifact rather than doing a build. - - name: Get build - uses: actions/download-artifact@HEAD - with: - name: editing-toolkit-build-archive - path: apps/editing-toolkit/editing-toolkit-plugin - - - name: Send hook to Mission Control - run: .github/workflows/send-calypso-app-build-trigger.sh - - phpunit: - name: Run phpunit tests - needs: build - runs-on: ubuntu-latest - steps: - # Pin to Node v12 to work around issue: https://github.com/Automattic/wp-calypso/issues/47255 - # We should be able to remove this once a wp-env update is released that includes an updated nodegit - # More info at: https://github.com/WordPress/gutenberg/pull/26712 - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: '12.20.1' - - - name: Checkout code - uses: actions/checkout@HEAD - - # It saves a bit of time to download the artifact rather than doing a build. - - name: Get build - uses: actions/download-artifact@HEAD - with: - name: editing-toolkit-build-archive - path: apps/editing-toolkit/editing-toolkit-plugin - - - name: Composer install - uses: nick-zh/composer-php@HEAD - with: - action: 'install' - - # We still need to access some local node modules to run things. - - name: Restore node_modules cache - id: cache - uses: actions/cache@HEAD - with: - path: | - node_modules - */*/node_modules - key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} - - - name: Install dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: yarn install --frozen-lockfile - - name: Setup wp-env dependencies run: | echo '{ "plugins": [ "./editing-toolkit-plugin", "https://downloads.wordpress.org/plugin/gutenberg.latest-stable.zip" ], "themes": [] }' > .wp-env.override.json @@ -159,56 +58,3 @@ jobs: - name: Run phpunit command run: yarn test:php working-directory: apps/editing-toolkit - - phpcs: - name: Run phpcs - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@HEAD - - - name: Composer install - uses: nick-zh/composer-php@HEAD - with: - action: 'install' - - - name: Get changed files - id: changes - uses: lots0logs/gh-action-get-changed-files@2.1.4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Execute phpcs on changed files - run: ./vendor/bin/phpcs --standard=apps/phpcs.xml ${{ join( fromJson( steps.changes.outputs.modified ), ' ' ) }} ${{ join( fromJson( steps.changes.outputs.added ), ' ' ) }} - if: ${{ steps.changes.outputs.all != '' }} - - - name: No changes found - run: echo "No changes found to check!" - if: ${{ steps.changes.outputs.all == '' }} - - newspack-blocks: - name: Check newspack-blocks sync - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@HEAD - - # It saves a bit of time to download the artifact rather than doing a build. - - name: Get build - uses: actions/download-artifact@HEAD - with: - name: editing-toolkit-build-archive - path: apps/editing-toolkit/editing-toolkit-plugin - - - name: Check if newspack-blocks exists - run: test -f ./apps/editing-toolkit/editing-toolkit-plugin/newspack-blocks/synced-newspack-blocks/class-newspack-blocks.php - - - name: Composer install - uses: nick-zh/composer-php@HEAD - with: - action: 'install' - - # This will fail if the textdomain has not been changed to "full-site-editing", which indicates an issue with the sync or build scripts. - - name: Execute phpcs on newspack-blocks PHP files. - run: ./vendor/bin/phpcs --standard=apps/editing-toolkit/bin/newspack-block-sync-phpcs.xml diff --git a/.github/workflows/gardening.yml b/.github/workflows/gardening.yml new file mode 100644 index 0000000000000..85bf3a4e53add --- /dev/null +++ b/.github/workflows/gardening.yml @@ -0,0 +1,39 @@ +name: Repo gardening + +on: + # Listen to this event for PRs from forks (see Flag OSS and Notify tasks). + pull_request_target: + types: [opened, labeled] + # We need to listen to all these events to catch all scenarios + pull_request: + types: ['opened', 'synchronize', 'edited', 'closed', 'labeled'] + +jobs: + repo-gardening: + name: 'Assign issues, Clean up labels, and notify Design and Editorial when necessary' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 12 + + - name: Wait for prior instances of the workflow to finish + uses: softprops/turnstyle@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 'Run gardening action' + uses: automattic/action-repo-gardening@v1.3.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + slack_token: ${{ secrets.SLACK_TOKEN }} + slack_design_channel: ${{ secrets.SLACK_DESIGN_CHANNEL }} + slack_editorial_channel: ${{ secrets.SLACK_EDITORIAL_CHANNEL }} + slack_team_channel: ${{ secrets.SLACK_TEAM_CHANNEL }} + tasks: 'assignIssues,cleanLabels,notifyDesign,notifyEditorial,flagOss' diff --git a/.github/workflows/icfy-stats.yml b/.github/workflows/icfy-stats.yml index 74f17bc754a9e..c0770b16ef581 100644 --- a/.github/workflows/icfy-stats.yml +++ b/.github/workflows/icfy-stats.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v1 with: - node-version: '^12.20.1' + node-version: '^14.16.1' - name: Checkout code uses: actions/checkout@v2 - name: Fetch git history @@ -32,6 +32,7 @@ jobs: NODE_ENV: production BROWSERSLIST_ENV: defaults WORKERS: 2 + WEBPACK_OPTIONS: --progress=profile run: yarn run analyze-icfy - run: mkdir icfy-stats && mv client/{chart,stats}.json icfy-stats - uses: actions/upload-artifact@v1 diff --git a/.github/workflows/is-pull-request.yml b/.github/workflows/is-pull-request.yml new file mode 100644 index 0000000000000..c492a24092bd8 --- /dev/null +++ b/.github/workflows/is-pull-request.yml @@ -0,0 +1,25 @@ +name: Is Pull Request + +# This job succeeds for every pull request. Then, in settings, we protect trunk +# such that every commit to trunk must have this job succeed. This effectively +# makes it such that no one can push directly to `trunk`, since only PRs can +# include this check. + +# We need this because you can only forbid pushing to `trunk` based on branch +# protection rules. We only utilize the "Require status checks to pass before +# merging" branch protection rule. If we wanted every other normal "check" to NOT +# be required, then there would be no "required check" which prevents pushing +# to trunk. As a result, we have this little fake "check" which will always show +# up on PRs to prevent direct commits to trunk. + +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + is-pull-request: + name: 'Protect Trunk Branch' + runs-on: ubuntu-latest + steps: + - name: Tells you that it's a pull request + run: 'echo "This is a pull request 👍."' diff --git a/.github/workflows/mark-issue-stale.yml b/.github/workflows/mark-issue-stale.yml index 86651947431d3..d1c02baa78314 100644 --- a/.github/workflows/mark-issue-stale.yml +++ b/.github/workflows/mark-issue-stale.yml @@ -12,17 +12,23 @@ jobs: - uses: actions/stale@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} + # Message to be added to stale issues. stale-issue-message: 'This issue is stale because it has been 180 days with no activity. You can keep the issue open by adding a comment. If you do, please provide additional context and explain why you’d like it to remain open. You can also close the issue yourself — if you do, please add a brief explanation and apply one of relevant issue close labels.' # Days before issue is considered stale. days-before-issue-stale: 180 - # Exempt issue labels. - exempt-issue-labels: '[Pri] High,[Pri] BLOCKER,[Status] Keep Open' + # Exempted issue labels. + exempt-issue-labels: '[Pri] High,[Pri] BLOCKER,[Status] Keep Open,[Status] Blocked / Hold' + # Message to be added to stale PRs. + stale-pr-message: 'This PR has been marked as stale due to lack of activity within the last 30 days.' + # Exempted PR labels. + exempt-pr-labels: '[Pri] High,[Pri] BLOCKER,[Status] Keep Open,[Status] Blocked / Hold' # Disable auto-close of both issues and PRs. days-before-close: -1 # Get issues in ascending (oldest first) order. ascending: true # Label to apply when issue is marked stale. stale-issue-label: '[Status] Stale' + # Label to apply when PR is marked as stale. + stale-pr-label: '[Status] Stale' # Increase number of operations executed per run. - operations-per-run: 2000 - debug-only: true \ No newline at end of file + operations-per-run: 525 diff --git a/.github/workflows/send-calypso-app-build-trigger.sh b/.github/workflows/send-calypso-app-build-trigger.sh deleted file mode 100755 index 6446916cb7b12..0000000000000 --- a/.github/workflows/send-calypso-app-build-trigger.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash -set -Eeuo pipefail - -# cd here so that the parent directories are not included in the zip file. -cd apps/editing-toolkit/editing-toolkit-plugin - -echo -e "Creating archive file...\n" - -# Create a zip of the FSE plugin. Should include built files at this point. -build_archive=plugin-archive.zip -zip --quiet --recurse-paths $build_archive ./* - -echo -e "Creating JSON payload...\n" - -# Use node to process data into JSON file -node -e ' -const fs = require("fs"); -const trigger_payload = JSON.parse( fs.readFileSync( process.env.GITHUB_EVENT_PATH, "utf8" ) ); - -// Makes sure that the data we need exists. -const getEnv = ( varName ) => { - const envVal = process.env[ varName ]; - // Fail for any falsey value except 0 (including empty strings). - if ( ! envVal && envVal !== 0 ) { - throw new Error( `${ varName } env variable missing!` ); - } - return envVal; -} - -const output = JSON.stringify( { - action: getEnv( "GITHUB_ACTION" ), - actor: getEnv( "GITHUB_ACTOR" ), - run_id: getEnv( "GITHUB_RUN_ID" ), - run_num: getEnv( "GITHUB_RUN_NUMBER" ), - repo: getEnv( "GITHUB_REPOSITORY" ), - trigger_payload, -} ); -fs.writeFileSync( "workflow_data.json", output, "utf8" ); -' - -echo -e "Sending data to MC...\n" - -# Send metadata and build zip file to the endpoint. -response=`curl -s \ - --write-out "HTTPSTATUS:%{http_code}" \ - --form "meta= Unit): ScriptBuildStep { + val result = ScriptBuildStep(init) + result.scriptContent = """ + #!/bin/bash + # Update node + . "${'$'}NVM_DIR/nvm.sh" --no-use + nvm install + set -o errexit + set -o nounset + set -o pipefail + + # Existing script content set by caller: + ${result.scriptContent} + """.trimIndent() + result.dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux + result.dockerPull = true + result.dockerImage = result.dockerImage ?: "%docker_image%" + result.dockerRunParameters = result.dockerRunParameters ?: "-u %env.UID%" + step(result) + return result +} diff --git a/.teamcity/_self/projects/DesktopApp.kt b/.teamcity/_self/projects/DesktopApp.kt new file mode 100644 index 0000000000000..46a353e751508 --- /dev/null +++ b/.teamcity/_self/projects/DesktopApp.kt @@ -0,0 +1,168 @@ +package _self.projects + +import _self.bashNodeScript +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.ParameterDisplay +import jetbrains.buildServer.configs.kotlin.v2019_2.Project +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.* +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs + +object DesktopApp : Project({ + id("WpDesktop") + name = "Desktop app" + buildType(E2ETests) + + params { + text("docker_image_desktop", "registry.a8c.com/calypso/ci-desktop:latest", label = "Docker image", description = "Docker image to use for the run", allowEmpty = true) + password("CALYPSO_SECRETS_ENCRYPTION_KEY", "credentialsJSON:ff451a7d-df79-4635-b6e8-cbd6ec18ddd8", description = "password for encrypting/decrypting certificates and general secrets for the wp-desktop and simplenote-electron repo", display = ParameterDisplay.HIDDEN) + password("E2EGUTENBERGUSER", "credentialsJSON:27ca9d7b-c6b5-4e84-94d5-ea43879d8184", display = ParameterDisplay.HIDDEN) + password("E2EPASSWORD", "credentialsJSON:2c4425c4-07d2-414c-9f18-b64da307bdf2", display = ParameterDisplay.HIDDEN) + } +}) + +object E2ETests : BuildType({ + id("WpDesktop_DesktopE2ETests") + name = "Run e2e tests" + description = "Run wp-desktop e2e tests in Linux" + + artifactRules = """ + desktop/release => release + desktop/e2e/logs => logs + desktop/e2e/screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + # Restore mtime to maximize cache hits + /usr/lib/git-core/git-restore-mtime --force --commit-time --skip-missing + + # Decript certs + openssl aes-256-cbc -md md5 -d -in desktop/resource/calypso/secrets.json.enc -out config/secrets.json -k "%CALYPSO_SECRETS_ENCRYPTION_KEY%" + + # Install modules + ${_self.yarn_install_cmd} + yarn run build-desktop:install-app-deps + """ + dockerImage = "%docker_image_desktop%" + } + + bashNodeScript { + name = "Build Calypso source" + scriptContent = """ + export WEBPACK_OPTIONS='--progress=profile' + + # Build desktop + yarn run build-desktop:source + """ + dockerImage = "%docker_image_desktop%" + } + + bashNodeScript { + name = "Build app (linux)" + scriptContent = """ + export ELECTRON_BUILDER_ARGS='-c.linux.target=dir' + export USE_HARD_LINKS=false + + # Build app + yarn run build-desktop:app + """ + dockerImage = "%docker_image_desktop%" + } + + bashNodeScript { + name = "Run tests (linux)" + scriptContent = """ + export E2EGUTENBERGUSER="%E2EGUTENBERGUSER%" + export E2EPASSWORD="%E2EPASSWORD%" + export CI=true + + # Start framebuffer + Xvfb ${'$'}{DISPLAY} -screen 0 1280x1024x24 & + + # Run tests + yarn run test-desktop:e2e + """ + dockerImage = "%docker_image_desktop%" + // See https://stackoverflow.com/a/53975412 and https://blog.jessfraz.com/post/how-to-use-new-docker-seccomp-profiles/ + // TDLR: Chrome needs access to some kernel level operations to create a sandbox, this option unblocks them. + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json" + } + + bashNodeScript { + name = "Clean up artifacts" + executionMode = BuildStep.ExecutionMode.RUN_ON_SUCCESS + scriptContent = """ + # Delete artifacts if branch is not trunk + if [ "%teamcity.build.branch.is_default%" != "true" ]; then + rm -fr desktop/release/* + fi + """ + dockerImage = "%docker_image_desktop%" + } + } + + failureConditions { + executionTimeoutMin = 15 + } + + features { + feature { + type = "xml-report-plugin" + param("xmlReportParsing.reportType", "junit") + param("xmlReportParsing.reportDirs", "desktop/e2e/result.xml") + } + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + + notifications { + notifierSettings = slackNotifier { + connection = "PROJECT_EXT_11" + sendTo = "#wp-desktop-calypso-e2e" + messageFormat = verboseMessageFormat { + addBranch = true + maximumNumberOfChanges = 10 + } + } + buildFailedToStart = true + buildFailed = true + buildFinishedSuccessfully = true + firstSuccessAfterFailure = true + buildProbablyHanging = true + } + } + + triggers { + vcs { + branchFilter = """ + +:* + -:pull* + """.trimIndent() + } + } +}) diff --git a/.teamcity/_self/projects/WPComPlugins.kt b/.teamcity/_self/projects/WPComPlugins.kt new file mode 100644 index 0000000000000..13029bae8705d --- /dev/null +++ b/.teamcity/_self/projects/WPComPlugins.kt @@ -0,0 +1,171 @@ +package _self.projects + +import _self.PluginBaseBuild +import _self.bashNodeScript +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.Project + +object WPComPlugins : Project({ + id("WPComPlugins") + name = "WPCom Plugins" + description = "Builds for WordPress.com plugins developed in calypso and deployed to wp-admin." + + // Default params for WPcom Plugins. + params { + param("docker_image", "registry.a8c.com/calypso/ci-wpcom:latest") + param("release_tag", "%plugin_slug%-release-build") + param("with_slack_notify", "false") + param("build.prefix", "1") + param("normalize_files", "") + param("build_env", "production") + } + buildType(EditingToolkit) + buildType(WpcomBlockEditor) + buildType(Notifications) + buildType(O2Blocks) + + // For some reason, TeamCity needs this to reference the Template. + template(PluginBaseBuild()) + + cleanup { + keepRule { + id = "keepReleaseBuilds" + keepAtLeast = allBuilds() + applyToBuilds { + inBranches { + branchFilter = patterns("+:") + } + withStatus = successful() + withTags = anyOf("notifications-release-build", "etk-release-build", "wpcom-block-editor-release-build", "o2-blocks-release-build") + } + dataToKeep = everything() + applyPerEachBranch = true + preserveArtifactsDependencies = true + } + } +}) + + +private object EditingToolkit : BuildType({ + id("WPComPlugins_EditorToolKit") + name = "Editing ToolKit" + + templates(PluginBaseBuild()) + params { + param("with_slack_notify", "true") + param("plugin_slug", "editing-toolkit") + param("archive_dir", "./editing-toolkit-plugin/") + param("release_tag", "etk-release-build") + param("build.prefix", "3") + param("normalize_files", "sed -i -e \"/^\\s\\* Version:/c\\ * Version: %build.number%\" -e \"/^define( 'A8C_ETK_PLUGIN_VERSION'/c\\define( 'A8C_ETK_PLUGIN_VERSION', '%build.number%' );\" ./release-archive/full-site-editing-plugin.php && sed -i -e \"/^Stable tag:\\s/c\\Stable tag: %build.number%\" ./release-archive/readme.txt\n") + + } + + steps { + bashNodeScript { + name = "Update version" + scriptContent = """ + cd apps/editing-toolkit + # Update plugin version in the plugin file and readme.txt. + sed -i -e "/^\s\* Version:/c\ * Version: %build.number%" -e "/^define( 'A8C_ETK_PLUGIN_VERSION'/c\define( 'A8C_ETK_PLUGIN_VERSION', '%build.number%' );" ./editing-toolkit-plugin/full-site-editing-plugin.php + sed -i -e "/^Stable tag:\s/c\Stable tag: %build.number%" ./editing-toolkit-plugin/readme.txt + """ + } + bashNodeScript { + name = "Run JS tests" + scriptContent = """ + export JEST_JUNIT_OUTPUT_NAME="results.xml" + export JEST_JUNIT_OUTPUT_DIR="../../test_results/editing-toolkit" + + cd apps/editing-toolkit + yarn test:js --reporters=default --reporters=jest-junit --maxWorkers=${'$'}JEST_MAX_WORKERS + """ + } + // Note: We run the PHP lint after the build to verify that the newspack-blocks + // code is also formatted correctly. + bashNodeScript { + name = "Run PHP Lint" + scriptContent = """ + cd apps/editing-toolkit + if [ ! -d "./editing-toolkit-plugin/newspack-blocks/synced-newspack-blocks" ] ; then + echo "Newspack blocks were not built correctly." + exit 1 + fi + yarn lint:php + """ + } + } +}) + +private object WpcomBlockEditor : BuildType({ + templates(PluginBaseBuild()) + id("WPComPlugins_WpcomBlockEditor") + name = "Wpcom Block Editor" + + params { + param("plugin_slug", "wpcom-block-editor") + param("archive_dir", "./dist/") + param("build_env", "development") + } +}) + +private object Notifications : BuildType({ + templates(PluginBaseBuild()) + id("WPComPlugins_Notifications") + name = "Notifications" + + params { + param("plugin_slug", "notifications") + param("archive_dir", "./dist/") + // This param is executed in bash right before the build script compares + // the build with the previous release version. The purpose of this code + // is to remove sources of randomness so that the diff operation only + // compares legitimate changes. + param("normalize_files", """ + function get_hash { + # If the stylesheet in the HTML file is pointing at "build.min.css?foobar123", + # this will just return the "foobar123" portion of the file. This + # is a source of randomness which needs to be eliminated. + echo `sed -nE 's~.*.*~\1~p' ${'$'}1` + } + new_hash=`get_hash dist/index.html` + old_hash=`get_hash release-archive/index.html` + + # All scripts and styles use the same "hash" version, so replace any + # instances of the hash in the *old* files with the newest version. + sed -i "s~${'$'}old_hash~${'$'}new_hash~g" release-archive/index.html release-archive/rtl.html + + # Replace the old cache buster with the new one in the previous release html files. + new_cache_buster=`cat dist/cache-buster.txt` + old_cache_buster=`cat release-archive/cache-buster.txt` + sed -i "s~${'$'}old_cache_buster~${'$'}new_cache_buster~g" release-archive/index.html release-archive/rtl.html + """.trimIndent()) + } +}) + +private object O2Blocks : BuildType({ + templates(PluginBaseBuild()) + id("WPComPlugins_O2Blocks") + name = "O2 Blocks" + + params { + param("plugin_slug", "o2-blocks") + param("archive_dir", "./release-files/") + } + + steps { + bashNodeScript { + name = "Create release directory" + scriptContent = """ + cd apps/o2-blocks + + # Copy existing dist files to release directory + mkdir release-files + cp -r dist release-files/dist/ + + # Add index.php file + cp index.php release-files/ + """ + } + } +}) diff --git a/.teamcity/_self/projects/WPComTests.kt b/.teamcity/_self/projects/WPComTests.kt new file mode 100644 index 0000000000000..4838ae73d7d29 --- /dev/null +++ b/.teamcity/_self/projects/WPComTests.kt @@ -0,0 +1,400 @@ +package _self.projects + +import _self.bashNodeScript +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.Project +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPublisher +import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.BuildReportTab +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.notifications +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.perfmon +import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric +import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.schedule + +object WPComTests : Project({ + id("WPComTests") + name = "WPCom Tests" + description = "Builds which test WordPress.com functionality, such as the Gutenberg plugin." + + params { + param("docker_image", "%docker_image_e2e%") + param("build.prefix", "1") + } + + BuildReportTab { + title = "VR Report" + startPage= "vr-report.zip!/vr-report.zip!/test/visual/backstop_data/html_report/index.html" + } + + // Keep the previous ID in order to preserve the historical data + buildType(gutenbergBuildType("desktop", "aee94c18-ee11-4c80-b6aa-245b967a97db")); + buildType(gutenbergBuildType("mobile","2af2eaed-87d5-41f4-ab1d-4ed589d5ae82")); + buildType(jetpackBuildType("desktop")); + buildType(jetpackBuildType("mobile")); + buildType(VisualRegressionTests); +}) + +fun gutenbergBuildType(screenSize: String, buildUuid: String): BuildType { + return BuildType { + uuid = buildUuid + id("WPComTests_gutenberg_$screenSize") + name = "Gutenberg tests ($screenSize)" + description = "Runs Gutenberg E2E tests using $screenSize screen resolution" + + artifactRules = """ + reports => reports + logs.tgz => logs.tgz + screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + params { + text( + name = "URL", + value = "https://wordpress.com", + label = "Test URL", + description = "URL to test against", + allowEmpty = false + ) + checkbox( + name = "GUTENBERG_EDGE", + value = "false", + label = "Use gutenberg-edge", + description = "Use a blog with gutenberg-edge sticker", + checked = "true", + unchecked = "false" + ) + checkbox( + name = "COBLOCKS_EDGE", + value = "false", + label = "Use coblocks-edge", + description = "Use a blog with coblocks-edge sticker", + checked = "true", + unchecked = "false" + ) + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + """ + } + bashNodeScript { + name = "Run e2e tests ($screenSize)" + scriptContent = """ + shopt -s globstar + set -x + + cd test/e2e + mkdir temp + + export LIVEBRANCHES=false + export NODE_CONFIG_ENV=test + export TEST_VIDEO=true + export HIGHLIGHT_ELEMENT=true + export GUTENBERG_EDGE=%GUTENBERG_EDGE% + export COBLOCKS_EDGE=%COBLOCKS_EDGE% + export URL=%URL% + export BROWSERSIZE=$screenSize + export BROWSERLOCALE=en + export NODE_CONFIG="{\"calypsoBaseURL\":\"${'$'}{URL}\"}" + + # Instructs Magellan to not hide the output from individual `mocha` processes. This is required for + # mocha-teamcity-reporter to work. + export MAGELLANDEBUG=true + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + yarn magellan --config=magellan-wpcom.json --max_workers=%E2E_WORKERS% --local_browser=chrome --mocha_args="--reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter.json" + """.trimIndent() + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json --shm-size=8gb" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + mkdir -p screenshots + find test/e2e -type f -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots + + mkdir -p logs + find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz + """.trimIndent() + } + } + + features { + perfmon { + } + notifications { + notifierSettings = slackNotifier { + connection = "PROJECT_EXT_11" + sendTo = "#gutenberg-e2e" + messageFormat = verboseMessageFormat { + addBranch = true + addStatusText = true + maximumNumberOfChanges = 10 + } + } + branchFilter = "+:" + buildFailed = true + buildFinishedSuccessfully = true + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + } + + failureConditions { + executionTimeoutMin = 20 + // TeamCity will mute a test if it fails and then succeeds within the same build. Otherwise TeamCity UI will not + // display a difference between real errors and retries, making it hard to understand what is actually failing. + supportTestRetry = true + + // Don't fail if the runner exists with a non zero code. This allows a build to pass if the failed tests have + // been muted previously. + nonZeroExitCode = false + + // Fail if the number of passing tests is 50% or less than the last build. This will catch the case where the test runner + // crashes and no tests are run. + failOnMetricChange { + metric = BuildFailureOnMetric.MetricType.PASSED_TEST_COUNT + threshold = 50 + units = BuildFailureOnMetric.MetricUnit.PERCENTS + comparison = BuildFailureOnMetric.MetricComparison.LESS + compareTo = build { + buildRule = lastSuccessful() + } + } + } + + triggers { + schedule { + schedulingPolicy = daily { + hour = 4 + } + branchFilter = """ + +:trunk + """.trimIndent() + triggerBuild = always() + withPendingChangesOnly = false + } + } + } +} + +fun jetpackBuildType(screenSize: String): BuildType { + return BuildType { + id("WPComTests_jetpack_$screenSize") + name = "Jetpack tests ($screenSize)" + description = "Runs Calypso Jetpack E2E tests using $screenSize screen resolution" + + params { + select( + name = "JETPACKHOST", + value = "PRESSABLEBLEEDINGEDGE", + label = "Jetpack Host", + options = listOf("PRESSABLE","PRESSABLEBLEEDINGEDGE") + ) + } + + artifactRules = """ + reports => reports + logs.tgz => logs.tgz + screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + """ + } + bashNodeScript { + name = "Run e2e tests ($screenSize)" + scriptContent = """ + shopt -s globstar + set -x + + cd test/e2e + mkdir temp + + export LIVEBRANCHES=false + export NODE_CONFIG_ENV=test + export TEST_VIDEO=true + export HIGHLIGHT_ELEMENT=true + export BROWSERSIZE=$screenSize + export BROWSERLOCALE=en + export JETPACKHOST=%JETPACKHOST% + export TARGET=JETPACK + + # Instructs Magellan to not hide the output from individual `mocha` processes. This is required for + # mocha-teamcity-reporter to work. + export MAGELLANDEBUG=true + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + yarn magellan --config=magellan-jetpack.json --max_workers=%E2E_WORKERS% --local_browser=chrome --mocha_args="--reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter.json" + """.trimIndent() + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json --shm-size=8gb" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + mkdir -p screenshots + find test/e2e -type f -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots + + mkdir -p logs + find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz + """.trimIndent() + } + } + + features { + perfmon { + } + notifications { + notifierSettings = slackNotifier { + connection = "PROJECT_EXT_11" + sendTo = "#e2e-jetpack-notif" + messageFormat = verboseMessageFormat { + addBranch = true + addStatusText = true + maximumNumberOfChanges = 10 + } + } + branchFilter = "+:" + buildFailed = true + buildFinishedSuccessfully = true + } + } + + failureConditions { + executionTimeoutMin = 20 + // TeamCity will mute a test if it fails and then succeeds within the same build. Otherwise TeamCity UI will not + // display a difference between real errors and retries, making it hard to understand what is actually failing. + supportTestRetry = true + + // Don't fail if the runner exists with a non zero code. This allows a build to pass if the failed tests have + // been muted previously. + nonZeroExitCode = false + + // Fail if the number of passing tests is 50% or less than the last build. This will catch the case where the test runner + // crashes and no tests are run. + failOnMetricChange { + metric = BuildFailureOnMetric.MetricType.PASSED_TEST_COUNT + threshold = 50 + units = BuildFailureOnMetric.MetricUnit.PERCENTS + comparison = BuildFailureOnMetric.MetricComparison.LESS + compareTo = build { + buildRule = lastSuccessful() + } + } + } + + triggers { + schedule { + schedulingPolicy = daily { + hour = 2 + } + branchFilter = """ + +:trunk + """.trimIndent() + triggerBuild = always() + withPendingChangesOnly = false + } + } + } +} + +private object VisualRegressionTests : BuildType({ + name = "Visual Regression Tests" + description = "Runs visual regression tests" + + artifactRules = """ + vr-report.zip => vr-report.zip + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + """ + } + bashNodeScript { + name = "Run Visual Regression Tests" + scriptContent = """ + set -x + export NODE_ENV="test" + export CAPTURE_LIMIT=16 + export COMPARE_LIMIT=150 + + apt-get install -y docker-compose + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./test/visual/config/encrypted.enc -out ./test/visual/config/development.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + yarn test-visual + """.trimIndent() + dockerRunParameters = "-v /var/run/docker.sock:/var/run/docker.sock" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + zip -r vr-report.zip test/visual/backstop_data/html_report test/visual/backstop_data/bitmaps_test test/visual/backstop_data/bitmaps_reference + + """.trimIndent() + } + } + + failureConditions { + executionTimeoutMin = 30 + } +}) + diff --git a/.teamcity/_self/projects/WebApp.kt b/.teamcity/_self/projects/WebApp.kt new file mode 100644 index 0000000000000..412d90e46fd9f --- /dev/null +++ b/.teamcity/_self/projects/WebApp.kt @@ -0,0 +1,839 @@ +package _self.projects + +import _self.bashNodeScript +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep +import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType +import jetbrains.buildServer.configs.kotlin.v2019_2.Project +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.* +import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.dockerCommand +import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.* +import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs + +object WebApp : Project({ + id("WebApp") + name = "Web app" + + buildType(RunCalypsoE2eDesktopTests) + buildType(RunCalypsoE2eMobileTests) + buildType(RunAllUnitTests) + buildType(CheckCodeStyleBranch) + buildType(BuildDockerImage) + buildType(RunCalypsoPlaywrightE2eTests) +}) + +object RunCalypsoE2eDesktopTests : BuildType({ + uuid = "52f38738-92b2-43cb-b7fb-19fce03cb67c" + name = "E2E tests (desktop)" + description = "Runs Calypso E2E tests using desktop screen resolution" + + artifactRules = """ + reports => reports + logs.tgz => logs.tgz + screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + + # Build package + yarn workspace @automattic/mocha-debug-reporter build + """ + dockerImage = "%docker_image_e2e%" + } + bashNodeScript { + name = "Run e2e tests (desktop)" + scriptContent = """ + shopt -s globstar + set -x + + cd test/e2e + mkdir temp + + export LIVEBRANCHES=true + export NODE_CONFIG_ENV=test + export TEST_VIDEO=true + export HIGHLIGHT_ELEMENT=true + + # Instructs Magellan to not hide the output from individual `mocha` processes. This is required for + # mocha-teamcity-reporter to work. + export MAGELLANDEBUG=true + + IMAGE_URL="https://calypso.live?image=registry.a8c.com/calypso/app:build-${BuildDockerImage.depParamRefs.buildNumber}"; + MAX_LOOP=10 + COUNTER=0 + + # Transform an URL like https://calypso.live?image=... into https://.calypso.live + while [[ ${'$'}COUNTER -le ${'$'}MAX_LOOP ]]; do + COUNTER=${'$'}((COUNTER+1)) + REDIRECT=${'$'}(curl --output /dev/null --silent --show-error --write-out "%{http_code} %{redirect_url}" "${'$'}{IMAGE_URL}") + read HTTP_STATUS URL <<< "${'$'}{REDIRECT}" + + # 202 means the image is being downloaded, retry in a few seconds + if [[ "${'$'}{HTTP_STATUS}" -eq "202" ]]; then + sleep 5 + continue + fi + + break + done + + if [[ -z "${'$'}URL" ]]; then + echo "Can't redirect to ${'$'}{IMAGE_URL}" >&2 + echo "Curl response: ${'$'}{REDIRECT}" >&2 + exit 1 + fi + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + export BROWSERSIZE="desktop" + export BROWSERLOCALE="en" + export NODE_CONFIG="{\"calypsoBaseURL\":\"${'$'}{URL%/}\"}" + + yarn magellan --config=magellan-calypso.json --max_workers=%E2E_WORKERS% --local_browser=chrome --mocha_args="--reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter.json" + """.trimIndent() + dockerImage = "%docker_image_e2e%" + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json --shm-size=8gb" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + mkdir -p screenshots + find test/e2e -type f -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots + + mkdir -p logs + find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz + """.trimIndent() + dockerImage = "%docker_image_e2e%" + } + } + + features { + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + } + + triggers { + vcs { + branchFilter = """ + +:* + -:pull* + """.trimIndent() + } + } + + failureConditions { + executionTimeoutMin = 20 + // TeamCity will mute a test if it fails and then succeeds within the same build. Otherwise TeamCity UI will not + // display a difference between real errors and retries, making it hard to understand what is actually failing. + supportTestRetry = true + + // Don't fail if the runner exists with a non zero code. This allows a build to pass if the failed tests have + // been muted previously. + nonZeroExitCode = false + + // Fail if the number of passing tests is 50% or less than the last build. This will catch the case where the test runner + // crashes and no tests are run. + failOnMetricChange { + metric = BuildFailureOnMetric.MetricType.PASSED_TEST_COUNT + threshold = 50 + units = BuildFailureOnMetric.MetricUnit.PERCENTS + comparison = BuildFailureOnMetric.MetricComparison.LESS + compareTo = build { + buildRule = lastSuccessful() + } + } + } + + dependencies { + snapshot(BuildDockerImage) { + } + } +}) + +object RunCalypsoE2eMobileTests : BuildType({ + name = "E2E tests (mobile)" + description = "Runs Calypso E2E tests using mobile screen resolution" + + artifactRules = """ + reports => reports + logs.tgz => logs.tgz + screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + + # Build package + yarn workspace @automattic/mocha-debug-reporter build + """ + dockerImage = "%docker_image_e2e%" + } + bashNodeScript { + name = "Run e2e tests (mobile)" + scriptContent = """ + shopt -s globstar + set -x + + cd test/e2e + mkdir temp + + export LIVEBRANCHES=true + export NODE_CONFIG_ENV=test + export TEST_VIDEO=true + export HIGHLIGHT_ELEMENT=true + + # Instructs Magellan to not hide the output from individual `mocha` processes. This is required for + # mocha-teamcity-reporter to work. + export MAGELLANDEBUG=true + + IMAGE_URL="https://calypso.live?image=registry.a8c.com/calypso/app:build-${BuildDockerImage.depParamRefs.buildNumber}"; + MAX_LOOP=10 + COUNTER=0 + + # Transform an URL like https://calypso.live?image=... into https://.calypso.live + while [[ ${'$'}COUNTER -le ${'$'}MAX_LOOP ]]; do + COUNTER=${'$'}((COUNTER+1)) + REDIRECT=${'$'}(curl --output /dev/null --silent --show-error --write-out "%{http_code} %{redirect_url}" "${'$'}{IMAGE_URL}") + read HTTP_STATUS URL <<< "${'$'}{REDIRECT}" + + # 202 means the image is being downloaded, retry in a few seconds + if [[ "${'$'}{HTTP_STATUS}" -eq "202" ]]; then + sleep 5 + continue + fi + + break + done + + if [[ -z "${'$'}URL" ]]; then + echo "Can't redirect to ${'$'}{IMAGE_URL}" >&2 + echo "Curl response: ${'$'}{REDIRECT}" >&2 + exit 1 + fi + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + export BROWSERSIZE="mobile" + export BROWSERLOCALE="en" + export NODE_CONFIG="{\"calypsoBaseURL\":\"${'$'}{URL%/}\"}" + + yarn magellan --config=magellan-calypso.json --max_workers=%E2E_WORKERS% --local_browser=chrome --mocha_args="--reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter.json" + """.trimIndent() + dockerImage = "%docker_image_e2e%" + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json --shm-size=8gb" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + mkdir -p screenshots + find test/e2e -type f -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots + + mkdir -p logs + find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz + """.trimIndent() + dockerImage = "%docker_image_e2e%" + } + } + + features { + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + } + + triggers { + vcs { + branchFilter = """ + +:* + -:pull* + """.trimIndent() + } + } + + failureConditions { + executionTimeoutMin = 20 + // TeamCity will mute a test if it fails and then succeeds within the same build. Otherwise TeamCity UI will not + // display a difference between real errors and retries, making it hard to understand what is actually failing. + supportTestRetry = true + + // Don't fail if the runner exists with a non zero code. This allows a build to pass if the failed tests have + // been muted previously. + nonZeroExitCode = false + + // Fail if the number of passing tests is 50% or less than the last build. This will catch the case where the test runner + // crashes and no tests are run. + failOnMetricChange { + metric = BuildFailureOnMetric.MetricType.PASSED_TEST_COUNT + threshold = 50 + units = BuildFailureOnMetric.MetricUnit.PERCENTS + comparison = BuildFailureOnMetric.MetricComparison.LESS + compareTo = build { + buildRule = lastSuccessful() + } + } + } + + dependencies { + snapshot(BuildDockerImage) { + } + } +}) + +object BuildDockerImage : BuildType({ + uuid = "89fff49e-c79b-4e68-a012-a7ba405359b6" + name = "Docker image" + description = "Build docker image containing Calypso" + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + dockerCommand { + name = "Build docker image" + commandType = build { + source = file { + path = "Dockerfile" + } + namesAndTags = """ + registry.a8c.com/calypso/app:build-%build.number% + registry.a8c.com/calypso/app:commit-${Settings.WpCalypso.paramRefs.buildVcsNumber} + """.trimIndent() + commandArgs = """ + --pull + --label com.a8c.image-builder=teamcity + --label com.a8c.target=calypso-live + --label com.a8c.build-id=%teamcity.build.id% + --build-arg workers=16 + --build-arg node_memory=32768 + --build-arg use_cache=true + """.trimIndent().replace("\n"," ") + } + param("dockerImage.platform", "linux") + } + dockerCommand { + commandType = push { + namesAndTags = """ + registry.a8c.com/calypso/app:build-%build.number% + registry.a8c.com/calypso/app:commit-${Settings.WpCalypso.paramRefs.buildVcsNumber} + """.trimIndent() + } + } + } + + failureConditions { + executionTimeoutMin = 20 + } + + features { + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + } +}) + +object RunAllUnitTests : BuildType({ + uuid = "beb75760-2786-472b-8909-ec33457bdece" + name = "Unit tests" + description = "Run unit tests (client + server + packages)" + + artifactRules = """ + test_results => test_results + artifacts => artifacts + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + """ + } + bashNodeScript { + name = "Prevent uncommited changes" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export NODE_ENV="test" + + # Prevent uncommited changes + DIRTY_FILES=${'$'}(git status --porcelain 2>/dev/null) + if [ ! -z "${'$'}DIRTY_FILES" ]; then + echo "Repository contains uncommitted changes: " + echo "${'$'}DIRTY_FILES" + echo "You need to checkout the branch, run 'yarn' and commit those files." + exit 1 + fi + """ + } + bashNodeScript { + name = "Prevent duplicated packages" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + # Duplicated packages + DUPLICATED_PACKAGES=${'$'}(npx yarn-deduplicate --list) + if [[ -n "${'$'}DUPLICATED_PACKAGES" ]]; then + echo "Repository contains duplicated packages: " + echo "" + echo "${'$'}DUPLICATED_PACKAGES" + echo "" + echo "To fix them, you need to checkout the branch, run 'npx yarn-deduplicate && yarn'," + echo "verify that the new packages work and commit the changes in 'yarn.lock'." + exit 1 + else + echo "No duplicated packages found." + fi + """ + } + bashNodeScript { + name = "Run type checks" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export NODE_ENV="test" + + # Run type checks + yarn tsc --build packages/tsconfig.json + yarn tsc --build apps/editing-toolkit/tsconfig.json + yarn tsc --project client/landing/gutenboarding + """ + } + bashNodeScript { + name = "Run unit tests for client" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export JEST_JUNIT_OUTPUT_NAME="results.xml" + unset NODE_ENV + unset CALYPSO_ENV + + # Run client tests + JEST_JUNIT_OUTPUT_DIR="./test_results/client" yarn test-client --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent + """ + } + bashNodeScript { + name = "Run unit tests for server" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export JEST_JUNIT_OUTPUT_NAME="results.xml" + unset NODE_ENV + unset CALYPSO_ENV + + # Run server tests + JEST_JUNIT_OUTPUT_DIR="./test_results/server" yarn test-server --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent + """ + } + bashNodeScript { + name = "Run unit tests for packages" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export JEST_JUNIT_OUTPUT_NAME="results.xml" + unset NODE_ENV + unset CALYPSO_ENV + + # Run packages tests + JEST_JUNIT_OUTPUT_DIR="./test_results/packages" yarn test-packages --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent + """ + } + bashNodeScript { + name = "Run unit tests for build tools" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export JEST_JUNIT_OUTPUT_NAME="results.xml" + unset NODE_ENV + unset CALYPSO_ENV + + # Run build-tools tests + JEST_JUNIT_OUTPUT_DIR="./test_results/build-tools" yarn test-build-tools --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent + """ + } + bashNodeScript { + name = "Build components storybook" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export NODE_ENV="production" + + yarn components:storybook:start --ci --smoke-test + """ + } + bashNodeScript { + name = "Build search storybook" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + export NODE_ENV="production" + + yarn search:storybook:start --ci --smoke-test + """ + } + } + + triggers { + vcs { + branchFilter = """ + +:* + -:pull* + """.trimIndent() + } + } + + failureConditions { + executionTimeoutMin = 10 + } + + features { + feature { + type = "xml-report-plugin" + param("xmlReportParsing.reportType", "junit") + param("xmlReportParsing.reportDirs", "test_results/**/*.xml") + } + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + + notifications { + notifierSettings = slackNotifier { + connection = "PROJECT_EXT_11" + sendTo = "#team-calypso-bot" + messageFormat = simpleMessageFormat() + } + branchFilter = """ + +:trunk + """.trimIndent() + buildFailedToStart = true + buildFailed = true + buildFinishedSuccessfully = true + firstSuccessAfterFailure = true + buildProbablyHanging = true + } + } +}) + +object CheckCodeStyleBranch : BuildType({ + uuid = "dfee7987-6bbc-4250-bb10-ef9dd7322bd2" + name = "Code style" + description = "Check code style" + + artifactRules = """ + checkstyle_results => checkstyle_results + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + + # Install modules + ${_self.yarn_install_cmd} + """ + } + bashNodeScript { + name = "Run linters" + scriptContent = """ + export NODE_ENV="test" + + # Find files to lint + if [ "%calypso.run_full_eslint%" = "true" ]; then + FILES_TO_LINT="." + else + FILES_TO_LINT=${'$'}(git diff --name-only --diff-filter=d refs/remotes/origin/trunk...HEAD | grep -E '(\.[jt]sx?|\.md)${'$'}' || exit 0) + fi + echo "Files to lint:" + echo ${'$'}FILES_TO_LINT + echo "" + + # Lint files + if [ ! -z "${'$'}FILES_TO_LINT" ]; then + yarn run eslint --format checkstyle --output-file "./checkstyle_results/eslint/results.xml" ${'$'}FILES_TO_LINT + fi + """ + } + } + + triggers { + vcs { + branchFilter = """ + +:* + -:trunk + -:pull* + """.trimIndent() + } + } + + failureConditions { + executionTimeoutMin = 20 + } + + features { + feature { + type = "xml-report-plugin" + param("xmlReportParsing.reportType", "checkstyle") + param("xmlReportParsing.reportDirs", "checkstyle_results/**/*.xml") + param("xmlReportParsing.verboseOutput", "true") + } + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + } +}) + +object RunCalypsoPlaywrightE2eTests : BuildType({ + name = "Playwright E2E tests" + description = "Runs Calypso e2e tests using Playwright" + params { + param("use_cached_node_modules", "false") + } + + artifactRules = """ + reports => reports + logs.tgz => logs.tgz + screenshots => screenshots + """.trimIndent() + + vcs { + root(Settings.WpCalypso) + cleanCheckout = true + } + + steps { + bashNodeScript { + name = "Prepare environment" + scriptContent = """ + export NODE_ENV="test" + export PLAYWRIGHT_BROWSERS_PATH=0 + + # Install modules + ${_self.yarn_install_cmd} + + # Build packages + yarn workspace @automattic/calypso-e2e build + yarn workspace @automattic/mocha-debug-reporter build + """ + dockerImage = "%docker_image_e2e%" + } + bashNodeScript { + name = "Run e2e tests" + scriptContent = """ + shopt -s globstar + set -x + + cd test/e2e + mkdir temp + + export LIVEBRANCHES=true + export NODE_CONFIG_ENV=test + export PLAYWRIGHT_BROWSERS_PATH=0 + + # Instructs Magellan to not hide the output from individual `mocha` processes. This is required for + # mocha-teamcity-reporter to work. + export MAGELLANDEBUG=true + + IMAGE_URL="https://calypso.live?image=registry.a8c.com/calypso/app:build-${BuildDockerImage.depParamRefs.buildNumber}"; + MAX_LOOP=10 + COUNTER=0 + + # Transform an URL like https://calypso.live?image=... into https://.calypso.live + while [[ ${'$'}COUNTER -le ${'$'}MAX_LOOP ]]; do + COUNTER=${'$'}((COUNTER+1)) + REDIRECT=${'$'}(curl --output /dev/null --silent --show-error --write-out "%{http_code} %{redirect_url}" "${'$'}{IMAGE_URL}") + read HTTP_STATUS URL <<< "${'$'}{REDIRECT}" + + # 202 means the image is being downloaded, retry in a few seconds + if [[ "${'$'}{HTTP_STATUS}" -eq "202" ]]; then + sleep 5 + continue + fi + + break + done + + if [[ -z "${'$'}URL" ]]; then + echo "Can't redirect to ${'$'}{IMAGE_URL}" >&2 + echo "Curl response: ${'$'}{REDIRECT}" >&2 + exit 1 + fi + + # Decrypt config + openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" + + # Run the test + export VIEWPORT_SIZE="mobile" + export LOCALE="en" + export NODE_CONFIG="{\"calypsoBaseURL\":\"${'$'}{URL%/}\"}" + + xvfb-run yarn magellan --config=magellan-playwright.json --max_workers=%E2E_WORKERS% --local_browser=chrome --mocha_args="--reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter.json" + """.trimIndent() + dockerImage = "%docker_image_e2e%" + dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json --shm-size=8gb" + } + bashNodeScript { + name = "Collect results" + executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE + scriptContent = """ + set -x + + mkdir -p screenshots + find test/e2e/temp -type f -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots + + mkdir -p logs + find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz + """.trimIndent() + dockerImage = "%docker_image_e2e%" + } + } + + features { + perfmon { + } + pullRequests { + vcsRootExtId = "${Settings.WpCalypso.id}" + provider = github { + authType = token { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY + } + } + commitStatusPublisher { + vcsRootExtId = "${Settings.WpCalypso.id}" + publisher = github { + githubUrl = "https://api.github.com" + authType = personalToken { + token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" + } + } + } + } + + failureConditions { + executionTimeoutMin = 20 + // TeamCity will mute a test if it fails and then succeeds within the same build. Otherwise TeamCity UI will not + // display a difference between real errors and retries, making it hard to understand what is actually failing. + supportTestRetry = true + } + + dependencies { + snapshot(BuildDockerImage) { + } + } +}) diff --git a/.teamcity/_self/yarnInstall.kt b/.teamcity/_self/yarnInstall.kt new file mode 100644 index 0000000000000..4ea2cc4b48d15 --- /dev/null +++ b/.teamcity/_self/yarnInstall.kt @@ -0,0 +1,17 @@ +package _self + +/** + * Use variable in scripts to take advantage of node_module caching. + */ +val yarn_install_cmd = """ + # Load existing node_modules to reduce install time. + if [ -d /calypso/node_modules ] && [ "%use_cached_node_modules%" == "true" ] ; then + echo "Loading existing found node_modules..." + mv /calypso/node_modules ./node_modules + else + echo "No existing node_modules were found." + fi + + # Install modules. + yarn install +""".trimIndent() diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts index 066ef2cce93ae..ed95a4b1c9489 100644 --- a/.teamcity/settings.kts +++ b/.teamcity/settings.kts @@ -1,18 +1,18 @@ +import _self.bashNodeScript +import _self.yarn_install_cmd import jetbrains.buildServer.configs.kotlin.v2019_2.* -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.PullRequests -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPublisher import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.dockerSupport -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.notifications import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.perfmon -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests -import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.ScriptBuildStep -import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.script import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.dockerRegistry import jetbrains.buildServer.configs.kotlin.v2019_2.buildSteps.dockerCommand import jetbrains.buildServer.configs.kotlin.v2019_2.projectFeatures.githubConnection import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.schedule -import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot +import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.failOnMetricChange +import jetbrains.buildServer.configs.kotlin.v2019_2.failureConditions.BuildFailureOnMetric +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.PullRequests +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPublisher +import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests /* The settings script is an entry point for defining a TeamCity @@ -41,17 +41,17 @@ version = "2020.2" project { vcsRoot(WpCalypso) - subProject(WpDesktop) - subProject(WPComPlugins) - buildType(RunAllUnitTests) + subProject(_self.projects.DesktopApp) + subProject(_self.projects.WPComPlugins) + subProject(_self.projects.WPComTests) + subProject(_self.projects.WebApp) buildType(BuildBaseImages) buildType(CheckCodeStyle) - buildType(CheckCodeStyleBranch) - buildType(BuildDockerImage) - buildType(RunCanaryE2eTests) + buildType(SmartBuildLauncher) params { param("env.NODE_OPTIONS", "--max-old-space-size=32000") + param("use_cached_node_modules", "true") text("E2E_WORKERS", "16", label = "Magellan parallel workers", description = "Number of parallel workers in Magellan (e2e tests)", allowEmpty = true) text("env.JEST_MAX_WORKERS", "16", label = "Jest max workers", description = "How many tests run in parallel", allowEmpty = true) password("matticbot_oauth_token", "credentialsJSON:34cb38a5-9124-41c4-8497-74ed6289d751", display = ParameterDisplay.HIDDEN) @@ -63,6 +63,8 @@ project { text("env.DOCKER_BUILDKIT", "1", label = "Enable Docker BuildKit", description = "Enables BuildKit (faster image generation). Values 0 or 1", allowEmpty = true) password("CONFIG_E2E_ENCRYPTION_KEY", "credentialsJSON:16d15e36-f0f2-4182-8477-8d8072d0b5ec", display = ParameterDisplay.HIDDEN) text("env.SKIP_TSC", "true", label = "Skip TS type generation", description = "Skips running `tsc` on yarn install", allowEmpty = true) + password("mc_post_root", "credentialsJSON:2f764583-d399-4d5f-8ee1-06f68ef2e2a6", display = ParameterDisplay.HIDDEN ) + password("mc_auth_secret", "credentialsJSON:5b1903f9-4b03-43ff-bba8-4a7509d07088", display = ParameterDisplay.HIDDEN) } features { @@ -196,433 +198,6 @@ object BuildBaseImages : BuildType({ } }) -object BuildDockerImage : BuildType({ - name = "Build docker image" - description = "Build docker image for Calypso" - - vcs { - root(WpCalypso) - cleanCheckout = true - } - - steps { - dockerCommand { - name = "Build docker image" - commandType = build { - source = file { - path = "Dockerfile" - } - namesAndTags = """ - registry.a8c.com/calypso/app:build-%build.number% - registry.a8c.com/calypso/app:commit-${WpCalypso.paramRefs.buildVcsNumber} - """.trimIndent() - commandArgs = """ - --pull - --label com.a8c.image-builder=teamcity - --label com.a8c.target=calypso-live - --label com.a8c.build-id=%teamcity.build.id% - --build-arg workers=16 - --build-arg node_memory=32768 - --build-arg use_cache=true - """.trimIndent().replace("\n"," ") - } - param("dockerImage.platform", "linux") - } - dockerCommand { - commandType = push { - namesAndTags = """ - registry.a8c.com/calypso/app:build-%build.number% - registry.a8c.com/calypso/app:commit-${WpCalypso.paramRefs.buildVcsNumber} - """.trimIndent() - } - } - } - - failureConditions { - executionTimeoutMin = 20 - } - - features { - perfmon { - } - pullRequests { - vcsRootExtId = "${WpCalypso.id}" - provider = github { - authType = token { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY - } - } - } -}) - -object RunAllUnitTests : BuildType({ - name = "Run unit tests" - description = "Run unit tests" - - artifactRules = """ - test_results => test_results - artifacts => artifacts - """.trimIndent() - - vcs { - root(WpCalypso) - cleanCheckout = true - } - - steps { - script { - name = "Prepare environment" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Install modules - yarn install - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Prevent uncommited changes" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Prevent uncommited changes - DIRTY_FILES=${'$'}(git status --porcelain 2>/dev/null) - if [ ! -z "${'$'}DIRTY_FILES" ]; then - echo "Repository contains uncommitted changes: " - echo "${'$'}DIRTY_FILES" - echo "You need to checkout the branch, run 'yarn' and commit those files." - exit 1 - fi - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Prevent duplicated packages" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - # Duplicated packages - DUPLICATED_PACKAGES=${'$'}(npx yarn-deduplicate --list) - if [[ -n "${'$'}DUPLICATED_PACKAGES" ]]; then - echo "Repository contains duplicated packages: " - echo "" - echo "${'$'}DUPLICATED_PACKAGES" - echo "" - echo "To fix them, you need to checkout the branch, run 'npx yarn-deduplicate && yarn'," - echo "verify that the new packages work and commit the changes in 'yarn.lock'." - exit 1 - else - echo "No duplicated packages found." - fi - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run type checks" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Run type checks - yarn tsc --build packages/tsconfig.json - yarn tsc --project client/landing/gutenboarding - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run unit tests for client" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export JEST_JUNIT_OUTPUT_NAME="results.xml" - unset NODE_ENV - unset CALYPSO_ENV - - # Run client tests - JEST_JUNIT_OUTPUT_DIR="./test_results/client" yarn test-client --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run unit tests for server" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export JEST_JUNIT_OUTPUT_NAME="results.xml" - unset NODE_ENV - unset CALYPSO_ENV - - # Run server tests - JEST_JUNIT_OUTPUT_DIR="./test_results/server" yarn test-server --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run unit tests for packages" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export JEST_JUNIT_OUTPUT_NAME="results.xml" - unset NODE_ENV - unset CALYPSO_ENV - - # Run packages tests - JEST_JUNIT_OUTPUT_DIR="./test_results/packages" yarn test-packages --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run unit tests for build tools" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export JEST_JUNIT_OUTPUT_NAME="results.xml" - unset NODE_ENV - unset CALYPSO_ENV - - # Run build-tools tests - JEST_JUNIT_OUTPUT_DIR="./test_results/build-tools" yarn test-build-tools --maxWorkers=${'$'}JEST_MAX_WORKERS --ci --reporters=default --reporters=jest-junit --silent - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Build artifacts" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="production" - - # Build o2-blocks - (cd apps/o2-blocks/ && yarn build --output-path="../../artifacts/o2-blocks") - - # Build wpcom-block-editor - (cd apps/wpcom-block-editor/ && NODE_ENV=development yarn build --output-path="../../artifacts/wpcom-block-editor") - - # Build notifications - (cd apps/notifications/ && yarn build --output-path="../../artifacts/notifications") - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Build components storybook" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="production" - - yarn components:storybook:start --ci --smoke-test - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Build search storybook" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="production" - - yarn search:storybook:start --ci --smoke-test - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - } - - triggers { - vcs { - branchFilter = """ - +:* - -:pull* - """.trimIndent() - } - } - - failureConditions { - executionTimeoutMin = 10 - } - - features { - feature { - type = "xml-report-plugin" - param("xmlReportParsing.reportType", "junit") - param("xmlReportParsing.reportDirs", "test_results/**/*.xml") - } - perfmon { - } - pullRequests { - vcsRootExtId = "${WpCalypso.id}" - provider = github { - authType = token { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY - } - } - commitStatusPublisher { - vcsRootExtId = "${WpCalypso.id}" - publisher = github { - githubUrl = "https://api.github.com" - authType = personalToken { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - } - } - - notifications { - notifierSettings = slackNotifier { - connection = "PROJECT_EXT_11" - sendTo = "#team-calypso-bot" - messageFormat = simpleMessageFormat() - } - branchFilter = """ - +:trunk - """.trimIndent() - buildFailedToStart = true - buildFailed = true - buildFinishedSuccessfully = true - firstSuccessAfterFailure = true - buildProbablyHanging = true - } - } -}) - object CheckCodeStyle : BuildType({ name = "Check code style" description = "Check code style" @@ -637,52 +212,24 @@ object CheckCodeStyle : BuildType({ } steps { - script { + bashNodeScript { name = "Prepare environment" scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - export NODE_ENV="test" # Install modules - yarn install - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" + ${_self.yarn_install_cmd} + """ } - script { + bashNodeScript { name = "Run linters" executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - export NODE_ENV="test" # Lint files yarn run eslint --format checkstyle --output-file "./checkstyle_results/eslint/results.xml" . - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" + """ } } @@ -701,6 +248,15 @@ object CheckCodeStyle : BuildType({ failureConditions { executionTimeoutMin = 20 + nonZeroExitCode = false + failOnMetricChange { + metric = BuildFailureOnMetric.MetricType.INSPECTION_ERROR_COUNT + units = BuildFailureOnMetric.MetricUnit.DEFAULT_UNIT + comparison = BuildFailureOnMetric.MetricComparison.MORE + compareTo = build { + buildRule = lastSuccessful() + } + } } features { @@ -715,106 +271,18 @@ object CheckCodeStyle : BuildType({ } }) -object CheckCodeStyleBranch : BuildType({ - name = "Check code style for branches" - description = "Check code style for branches" - - artifactRules = """ - checkstyle_results => checkstyle_results - """.trimIndent() +object SmartBuildLauncher : BuildType({ + name = "Smart Build Launcher" + description = "Launches TeamCity builds based on which files were modified in VCS." vcs { - root(WpCalypso) + root(Settings.WpCalypso) cleanCheckout = true } - steps { - script { - name = "Prepare environment" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Install modules - yarn install - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run linters" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Find files to lint - if [ "%calypso.run_full_eslint%" = "true" ]; then - FILES_TO_LINT="." - else - FILES_TO_LINT=${'$'}(git diff --name-only --diff-filter=d refs/remotes/origin/trunk...HEAD | grep -E '(\.[jt]sx?|\.md)${'$'}' || exit 0) - fi - echo "Files to lint:" - echo ${'$'}FILES_TO_LINT - echo "" - - # Lint files - if [ ! -z "${'$'}FILES_TO_LINT" ]; then - yarn run eslint --format checkstyle --output-file "./checkstyle_results/eslint/results.xml" ${'$'}FILES_TO_LINT - fi - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image%" - dockerRunParameters = "-u %env.UID%" - } - } - - triggers { - vcs { - branchFilter = """ - +:* - -:trunk - -:pull* - """.trimIndent() - } - } - - failureConditions { - executionTimeoutMin = 20 - } - features { - feature { - type = "xml-report-plugin" - param("xmlReportParsing.reportType", "checkstyle") - param("xmlReportParsing.reportDirs", "checkstyle_results/**/*.xml") - param("xmlReportParsing.verboseOutput", "true") - } - perfmon { - } pullRequests { - vcsRootExtId = "${WpCalypso.id}" + vcsRootExtId = "${Settings.WpCalypso.id}" provider = github { authType = token { token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" @@ -822,8 +290,9 @@ object CheckCodeStyleBranch : BuildType({ filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY } } + commitStatusPublisher { - vcsRootExtId = "${WpCalypso.id}" + vcsRootExtId = "${Settings.WpCalypso.id}" publisher = github { githubUrl = "https://api.github.com" authType = personalToken { @@ -832,180 +301,24 @@ object CheckCodeStyleBranch : BuildType({ } } } -}) - -object RunCanaryE2eTests : BuildType({ - name = "Canary e2e tests" - description = "Run canary e2e tests" - - artifactRules = """ - reports => reports - logs.tgz => logs.tgz - screenshots => screenshots - """.trimIndent() - - vcs { - root(WpCalypso) - cleanCheckout = true - } steps { - script { - name = "Prepare environment" + bashNodeScript { + name = "Install and build dependencies" scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="test" - - # Install modules - yarn install - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_e2e%" - dockerRunParameters = "-u %env.UID%" + $yarn_install_cmd + yarn workspace @automattic/dependency-finder build + """ } - script { - name = "Run e2e tests: Canary (mobile, desktop)" + bashNodeScript { + name = "Launch relevant builds" scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - set -x - - cd test/e2e - mkdir temp - - export LIVEBRANCHES=true - export NODE_CONFIG_ENV=test - export MAGELLANDEBUG=true - export TEST_VIDEO=true - - IMAGE_URL="https://calypso.live?image=registry.a8c.com/calypso/app:build-${BuildDockerImage.depParamRefs.buildNumber}"; - MAX_LOOP=10 - COUNTER=0 - - # Transform an URL like https://calypso.live?image=... into https://.calypso.live - while [[ ${'$'}COUNTER -le ${'$'}MAX_LOOP ]]; do - COUNTER=${'$'}((COUNTER+1)) - REDIRECT=${'$'}(curl --output /dev/null --silent --show-error --write-out "%{http_code} %{redirect_url}" "${'$'}{IMAGE_URL}") - read HTTP_STATUS URL <<< "${'$'}{REDIRECT}" - - # 202 means the image is being downloaded, retry in a few seconds - if [[ "${'$'}{HTTP_STATUS}" -eq "202" ]]; then - sleep 5 - continue - fi - - break - done - - if [[ -z "${'$'}URL" ]]; then - echo "Can't redirect to ${'$'}{IMAGE_URL}" >&2 - echo "Curl response: ${'$'}{REDIRECT}" >&2 - exit 1 - fi - - # Decrypt config - openssl aes-256-cbc -md sha1 -d -in ./config/encrypted.enc -out ./config/local-test.json -k "%CONFIG_E2E_ENCRYPTION_KEY%" - - # Run the test - ./run.sh -R -a %E2E_WORKERS% -C -s "mobile,desktop" -u "${'$'}{URL%/}" - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerImage = "%docker_image_e2e%" - dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json" - } - script { - name = "Collect results" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - set -o errexit - set -o nounset - set -o pipefail - set -x - - # Collect results - mkdir -p reports - find test/e2e/temp -path '*/reports/*' -print0 | xargs -r -0 mv -t reports - - mkdir -p screenshots - find test/e2e/temp -path '*/screenshots/*' -print0 | xargs -r -0 mv -t screenshots - - mkdir -p logs - find test/e2e -name '*.log' -print0 | xargs -r -0 tar cvfz logs.tgz - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerImage = "%docker_image_e2e%" - dockerRunParameters = "-u %env.UID%" - } - } - - features { - feature { - type = "xml-report-plugin" - param("xmlReportParsing.reportType", "junit") - param("xmlReportParsing.reportDirs", "reports/*.xml") - } - perfmon { - } - pullRequests { - vcsRootExtId = "${WpCalypso.id}" - provider = github { - authType = token { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY - } - } - commitStatusPublisher { - vcsRootExtId = "${WpCalypso.id}" - publisher = github { - githubUrl = "https://api.github.com" - authType = personalToken { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - } - } - } - - triggers { - vcs { - branchFilter = """ - +:* - -:trunk - -:pull* - """.trimIndent() - } - } - - failureConditions { - executionTimeoutMin = 20 - } - - dependencies { - snapshot(BuildDockerImage) { + node ./packages/dependency-finder/dist/esm/index.js + """ } } }) - object WpCalypso : GitVcsRoot({ name = "wp-calypso" url = "git@github.com:Automattic/wp-calypso.git" @@ -1018,385 +331,3 @@ object WpCalypso : GitVcsRoot({ } }) -object WpDesktop : Project({ - name = "Desktop" - buildType(WpDesktop_DesktopE2ETests) - - params { - text("docker_image_dekstop", "registry.a8c.com/calypso/ci-desktop:latest", label = "Docker image", description = "Docker image to use for the run", allowEmpty = true) - password("CALYPSO_SECRETS_ENCRYPTION_KEY", "credentialsJSON:ff451a7d-df79-4635-b6e8-cbd6ec18ddd8", description = "password for encrypting/decrypting certificates and general secrets for the wp-desktop and simplenote-electron repo", display = ParameterDisplay.HIDDEN) - password("E2EGUTENBERGUSER", "credentialsJSON:27ca9d7b-c6b5-4e84-94d5-ea43879d8184", display = ParameterDisplay.HIDDEN) - password("E2EPASSWORD", "credentialsJSON:2c4425c4-07d2-414c-9f18-b64da307bdf2", display = ParameterDisplay.HIDDEN) - } -}) - -object WpDesktop_DesktopE2ETests : BuildType({ - name = "Desktop e2e tests" - description = "Run wp-desktop e2e tests in Linux" - - artifactRules = """ - desktop/release => release - desktop/e2e/logs => logs - desktop/e2e/screenshots => screenshots - """.trimIndent() - - vcs { - root(WpCalypso) - cleanCheckout = true - } - - steps { - script { - name = "Prepare environment" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - # Restore mtime to maximize cache hits - /usr/lib/git-core/git-restore-mtime --force --commit-time --skip-missing - - # Decript certs - openssl aes-256-cbc -md md5 -d -in desktop/resource/calypso/secrets.json.enc -out config/secrets.json -k "%CALYPSO_SECRETS_ENCRYPTION_KEY%" - - # Install modules - yarn install - yarn run build-desktop:install-app-deps - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_dekstop%" - dockerRunParameters = "-u %env.UID%" - } - - script { - name = "Build Calypso source" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - # Build desktop - yarn run build-desktop:source - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_dekstop%" - dockerRunParameters = "-u %env.UID%" - } - - script { - name = "Build app (linux)" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export ELECTRON_BUILDER_ARGS='-c.linux.target=dir' - export USE_HARD_LINKS=false - - # Build app - yarn run build-desktop:app - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_dekstop%" - dockerRunParameters = "-u %env.UID%" - } - - script { - name = "Run tests (linux)" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export E2EGUTENBERGUSER="%E2EGUTENBERGUSER%" - export E2EPASSWORD="%E2EPASSWORD%" - export CI=true - - # Start framebuffer - Xvfb ${'$'}{DISPLAY} -screen 0 1280x1024x24 & - - # Run tests - yarn run test-desktop:e2e - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_dekstop%" - // See https://stackoverflow.com/a/53975412 and https://blog.jessfraz.com/post/how-to-use-new-docker-seccomp-profiles/ - // TDLR: Chrome needs access to some kernel level operations to create a sandbox, this option unblocks them. - dockerRunParameters = "-u %env.UID% --security-opt seccomp=.teamcity/docker-seccomp.json" - } - - script { - name = "Clean up artifacts" - executionMode = BuildStep.ExecutionMode.RUN_ON_SUCCESS - scriptContent = """ - #!/bin/bash - set -o errexit - set -o nounset - set -o pipefail - - # Delete artifacts if branch is not trunk - if [ "%teamcity.build.branch.is_default%" != "true" ]; then - rm -fr desktop/release/* - fi - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_dekstop%" - dockerRunParameters = "-u %env.UID% " - } - } - - failureConditions { - executionTimeoutMin = 15 - } - - features { - feature { - type = "xml-report-plugin" - param("xmlReportParsing.reportType", "junit") - param("xmlReportParsing.reportDirs", "desktop/e2e/result.xml") - } - perfmon { - } - pullRequests { - vcsRootExtId = "${WpCalypso.id}" - provider = github { - authType = token { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY - } - } - commitStatusPublisher { - vcsRootExtId = "${WpCalypso.id}" - publisher = github { - githubUrl = "https://api.github.com" - authType = personalToken { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - } - } - - notifications { - notifierSettings = slackNotifier { - connection = "PROJECT_EXT_11" - sendTo = "#wp-desktop-calypso-e2e" - messageFormat = verboseMessageFormat { - addBranch = true - maximumNumberOfChanges = 10 - } - } - buildFailedToStart = true - buildFailed = true - buildFinishedSuccessfully = true - firstSuccessAfterFailure = true - buildProbablyHanging = true - } - } - - triggers { - vcs { - branchFilter = """ - +:* - -:pull* - """.trimIndent() - } - } -}) - -object WPComPlugins : Project({ - name = "WPCom Plugins" - buildType(WPComPlugins_EditorToolKit) - params { - text("docker_image_wpcom", "registry.a8c.com/calypso/ci-wpcom:latest", label = "Docker image", description = "Docker image to use for the run", allowEmpty = true) - } -}) - -object WPComPlugins_EditorToolKit : BuildType({ - name = "Editor ToolKit" - - artifactRules = "editing-toolkit.zip" - - dependencies { - artifacts(AbsoluteId("calypso_WPComPlugins_EditorToolKit")) { - buildRule = tag("etk-release-build", "+:trunk") - artifactRules = """ - +:editing-toolkit.zip!** => etk-release-build - """.trimIndent() - } - } - - buildNumberPattern = "%build.prefix%.%build.counter%" - params { - param("build.prefix", "3") - } - - vcs { - root(WpCalypso) - cleanCheckout = true - } - - triggers { - vcs { - branchFilter = """ - +:* - -:pull* - """.trimIndent() - } - } - - features { - pullRequests { - vcsRootExtId = "${WpCalypso.id}" - provider = github { - authType = token { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - filterAuthorRole = PullRequests.GitHubRoleFilter.EVERYBODY - } - } - - commitStatusPublisher { - vcsRootExtId = "${WpCalypso.id}" - publisher = github { - githubUrl = "https://api.github.com" - authType = personalToken { - token = "credentialsJSON:57e22787-e451-48ed-9fea-b9bf30775b36" - } - } - } - } - - steps { - script { - name = "Prepare environment" - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - # Update composer - composer install - - # Install modules - yarn install - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_wpcom%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Run tests" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export JEST_JUNIT_OUTPUT_NAME="results.xml" - export JEST_JUNIT_OUTPUT_DIR="../../test_results/editing-toolkit" - - cd apps/editing-toolkit - yarn test:js --reporters=default --reporters=jest-junit --maxWorkers=${'$'}JEST_MAX_WORKERS - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_wpcom%" - dockerRunParameters = "-u %env.UID%" - } - script { - name = "Build artifacts" - executionMode = BuildStep.ExecutionMode.RUN_ON_FAILURE - scriptContent = """ - #!/bin/bash - - # Update node - . "${'$'}NVM_DIR/nvm.sh" --no-use - nvm install - - set -o errexit - set -o nounset - set -o pipefail - - export NODE_ENV="production" - - cd apps/editing-toolkit - yarn build - - echo - prev_release_build_num=${'$'}(grep build_number ../../etk-release-build/build_meta.txt | sed -e "s/build_number=//") - rm ../../etk-release-build/build_meta.txt # Adds a source of randomness which we don't want for comparison. - echo "Previous tagged trunk build: ${'$'}prev_release_build_num" - - # Update plugin version in the plugin file and readme.txt. - # Note: we also update the previous release build to the same version to restore idempotence - sed -i -e "/^\s\* Version:/c\ * Version: %build.number%" -e "/^define( 'A8C_ETK_PLUGIN_VERSION'/c\define( 'A8C_ETK_PLUGIN_VERSION', '%build.number%' );" ./editing-toolkit-plugin/full-site-editing-plugin.php ../../etk-release-build/full-site-editing-plugin.php - sed -i -e "/^Stable tag:\s/c\Stable tag: %build.number%" ./editing-toolkit-plugin/readme.txt ../../etk-release-build/readme.txt - - if ! diff -rq ./editing-toolkit-plugin/ ../../etk-release-build/ ; then - echo "The build is different from the last release build. Therefore, this can be tagged as a release build." - curl -X POST -H "Content-Type: text/plain" --data "etk-release-build" -u "%system.teamcity.auth.userId%:%system.teamcity.auth.password%" %teamcity.serverUrl%/httpAuth/app/rest/builds/id:%teamcity.build.id%/tags/ - else - echo "The build is not different from the last release build. Therefore, this build has no effect." - fi - - cd editing-toolkit-plugin/ - # Metadata file with info for the download script. - tee build_meta.txt <<-EOM - commit_hash=%build.vcs.number% - commit_url=https://github.com/Automattic/wp-calypso/commit/%build.vcs.number% - build_number=%build.number% - EOM - - echo - zip -r ../../../editing-toolkit.zip . - - """.trimIndent() - dockerImagePlatform = ScriptBuildStep.ImagePlatform.Linux - dockerPull = true - dockerImage = "%docker_image_wpcom%" - dockerRunParameters = "-u %env.UID%" - } - } -}) diff --git a/CREDITS.md b/CREDITS.md index b823177dc48c4..ebe581bb7adc2 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -56,8 +56,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE ## -### client/components/tinymce/plugins/wplink - ### client/lib/media/constants.js ### client/post-editor/media-modal/markup.js diff --git a/Dockerfile b/Dockerfile index 98928acc66f61..def25a07e5a83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ ARG use_cache=false -ARG node_version=12.20.1 +ARG node_version=14.16.1 ################### -FROM node:${node_version} as builder-cache-false +FROM node:${node_version}-buster as builder-cache-false ################### @@ -20,7 +20,7 @@ ARG commit_sha="(unknown)" ARG workers=4 ARG node_memory=8192 ENV CONTAINER 'docker' -ENV PROGRESS true +ENV WEBPACK_OPTIONS '--progress=profile' ENV COMMIT_SHA $commit_sha ENV CALYPSO_ENV production ENV NODE_ENV production @@ -28,6 +28,7 @@ ENV WORKERS $workers ENV BUILD_TRANSLATION_CHUNKS true ENV CHROMEDRIVER_SKIP_DOWNLOAD true ENV PUPPETEER_SKIP_DOWNLOAD true +ENV PLAYWRIGHT_SKIP_DOWNLOAD true ENV NODE_OPTIONS --max-old-space-size=$node_memory WORKDIR /calypso diff --git a/Dockerfile.base b/Dockerfile.base index 8b6c9ce2639a4..ad69600c69997 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -1,7 +1,7 @@ #### cache image #### This image is not pushed to any repository and it shouldn't be used as base image for any other docker build. #### Its main goal is to create a `/calypso/.cache` that can be copied over other images that can benefit from a warm cache. -FROM node:12.20.1 as cache +FROM node:14.16.1-buster as cache ARG node_memory=8192 WORKDIR /calypso @@ -34,9 +34,10 @@ ENTRYPOINT [ "/bin/bash" ] #### base image #### This image can be used as a base image for other builds, or to uni test and build calypso. -FROM node:12.20.1 as base +FROM node:14.16.1-buster as base ARG node_memory=8192 +ARG user=calypso ARG UID=1003 WORKDIR /calypso @@ -47,7 +48,16 @@ ENV NODE_OPTIONS=--max-old-space-size=$node_memory ENV HOME=/calypso ENV CHROMEDRIVER_SKIP_DOWNLOAD=true ENV PUPPETEER_SKIP_DOWNLOAD=true -RUN chown $UID /calypso +ENV PLAYWRIGHT_SKIP_DOWNLOAD=true + +# Add user calypso with uid 1003, give it sudo permissions +RUN apt-get update \ + && apt-get install -y sudo zip jq \ + && adduser --uid $UID --disabled-password $user \ + && echo "$user ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$user \ + && chmod 0440 /etc/sudoers.d/$user \ + && chown $UID /calypso \ + && rm -rf /var/lib/apt/lists/* # Set bash as the default shell RUN rm /bin/sh && ln -s /bin/bash /bin/sh @@ -57,6 +67,9 @@ COPY --from=cache --chown=$UID /calypso/.nvm /calypso/.nvm COPY --from=cache --chown=$UID /calypso/.cache /calypso/.cache COPY --from=cache --chown=$UID /calypso/.bashrc /calypso/.bashrc +# Copy node_modules to reduce install time of future builds. +COPY --from=cache --chown=$UID /calypso/node_modules /calypso/node_modules + ENTRYPOINT [ "/bin/bash" ] #### ci-desktop image @@ -67,6 +80,7 @@ ENV ELECTRON_BUILDER_ARGS='-c.linux.target=dir' ENV USE_HARD_LINKS=false ENV CHROMEDRIVER_SKIP_DOWNLOAD=false ENV PUPPETEER_SKIP_DOWNLOAD=false +ENV PLAYWRIGHT_SKIP_DOWNLOAD=false # This chrome image is the latest version supported by wp-desktop's chromedriver, declared in desktop/package.json. ENV CHROME_VERSION="80.0.3987.163-1" ENV DISPLAY=:99 @@ -81,6 +95,7 @@ RUN apt update \ libatk-bridge2.0-0 \ libatspi2.0-0 \ libgtk-3-0 \ + libgbm-dev \ libnspr4 \ libnss3 \ libnss3 \ @@ -107,14 +122,12 @@ ENTRYPOINT [ "/bin/bash" ] #### This image is used to run e2e tests. The only difference with ci-desktop is the Chrome version. FROM ci-desktop as ci-e2e -# This chrome image is the latest version that accepts SameSite=None. # test/e2e/test/mocha.env.js will install a compatible chromedriver. -ENV CHROME_VERSION="84.0.4147.135-1" ENV DETECT_CHROMEDRIVER_VERSION=true -RUN wget --no-verbose https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \ - && apt-get install -y ./google-chrome-stable_${CHROME_VERSION}_amd64.deb \ - && rm ./google-chrome-stable_${CHROME_VERSION}_amd64.deb +RUN wget --no-verbose https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ + && apt-get install -y ./google-chrome-stable_current_amd64.deb \ + && rm ./google-chrome-stable_current_amd64.deb ENTRYPOINT [ "/bin/bash" ] @@ -127,11 +140,11 @@ COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer COPY --from=cache --chown=$UID /calypso/composer.* /calypso/ RUN apt update &&\ - apt-get install -y apt-transport-https zip &&\ + apt-get install -y apt-transport-https &&\ wget https://packages.sury.org/php/apt.gpg -O /etc/apt/trusted.gpg.d/php-sury.gpg &&\ - echo "deb https://packages.sury.org/php/ stretch main" > /etc/apt/sources.list.d/php-sury.list &&\ + echo "deb https://packages.sury.org/php/ buster main" > /etc/apt/sources.list.d/php-sury.list &&\ apt update &&\ - apt-get install -y php7.4-cli php7.4-xml php7.4-mbstring &&\ + apt-get install -y php7.4-cli php7.4-xml php7.4-mbstring docker-compose &&\ composer install ENTRYPOINT [ "/bin/bash" ] diff --git a/SECURITY.md b/SECURITY.md index 8a24ac22e59f0..f48aacdacdf6e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -Although we strive to create the most secure products possible, we are not perfect. If you happen to find a security vulnerability in one of our services, we would appreciate letting us know and allowing us to respond before disclosing the issue publicly. We take security seriously, and we will try to review and reply to every legitimate security report personally within 24 hours. Other reports submitted will not be replied to. +Although we strive to create the most secure products possible, we are not perfect. If you happen to find a security vulnerability in one of our services, we would appreciate letting us know and allowing us to respond before disclosing the issue publicly. We take security seriously, and we will try to review and reply to every legitimate security report personally within 24 hours. Other reports submitted will not be replied to. ## Supported Versions @@ -16,27 +16,31 @@ Only the latest version of WordPress desktop is supported. To receive security u [Calypso](https://developer.wordpress.com/calypso/) is an open-source wp-admin replacement. Our HackerOne program covers the software. + + **For responsible disclosure of security issues and to be eligible for our bug bounty program, please submit your report via the [HackerOne](https://hackerone.com/automattic) portal.** Our most critical targets are: -* wordpress.com -* cloud.jetpack.com +- wordpress.com +- cloud.jetpack.com For more targets, see the `In Scope` section on [HackerOne](https://hackerone.com/automattic). + + _Please note that the **WordPress software is a separate entity** from Automattic. Please report vulnerabilities for WordPress through [the WordPress Foundation's HackerOne page](https://hackerone.com/wordpress)._ ## Guidelines We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines: -* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). -* Pen-testing Production: - * Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). - * If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. - * **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. - * To be eligible for a bounty, all of these guidelines must be followed. -* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. +- Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). +- Pen-testing Production: + - Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). + - If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. + - **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. + - To be eligible for a bounty, all of these guidelines must be followed. +- Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties. diff --git a/apps/README.md b/apps/README.md index 7741b38a70a4e..9d55809e1fb0e 100644 --- a/apps/README.md +++ b/apps/README.md @@ -1,6 +1,6 @@ # Apps -This directory exists to hold a variety of projects that can produce independent, binary-like outputs deployed elsewhere. Typically not published to NPM or build on `yarn start` +This directory exists to hold a variety of projects that can produce independent, binary-like outputs deployed elsewhere. Typically not published to NPM or built on `yarn start`. For packages that we might publish as NPM packages, see [`/packages`](../packages). diff --git a/apps/editing-toolkit/.eslintrc.js b/apps/editing-toolkit/.eslintrc.js index fea697e7e24d5..ac6f2ee2e14ed 100644 --- a/apps/editing-toolkit/.eslintrc.js +++ b/apps/editing-toolkit/.eslintrc.js @@ -14,12 +14,4 @@ module.exports = { 'wpcalypso/jsx-classname-namespace': 0, }, ignorePatterns: [ '**/dist/*' ], - overrides: [ - { - files: [ './**/?(*.)spec.[jt]s?(x)', './editing-toolkit-plugin/e2e-test-helpers/**' ], - globals: { - page: 'readonly', - }, - }, - ], }; diff --git a/apps/editing-toolkit/README.md b/apps/editing-toolkit/README.md index c68138cd04968..293b59d674769 100644 --- a/apps/editing-toolkit/README.md +++ b/apps/editing-toolkit/README.md @@ -2,6 +2,8 @@ This plugin includes many sub-features which add blocks and new functionality to the Gutenberg editor. The plugin provides a single codebase which can be installed on any platform which requires these features, such as the WordPress.com multisite or other standalone WordPress instances. +This code is developed in the calypso monorepo at . + ## Rename Info This plugin has been renamed from Full Site Editing Plugin to WordPress.com Editing Toolkit Plugin. diff --git a/apps/editing-toolkit/bin/js-unit-config.js b/apps/editing-toolkit/bin/js-unit-config.js deleted file mode 100644 index 45ac4598f7cc2..0000000000000 --- a/apps/editing-toolkit/bin/js-unit-config.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Test configuration for the FSE plugin. - * - * Will match files such that: - * 1. Must be in the apps/editing-toolkit/ directory - * 2. Must have .test.EXT at the end of the filename - * 3. EXT (above) must be one of js, ts, jsx, or tsx. - * - * Note: In order to use a different jest config for e2e tests, this config file - * must be kept in the bin/ folder to prevent it from being detected as the - * config file for e2e tests. - */ - -// @wordpress/scripts manually adds additional Jest config ontop of -// @wordpress/jest-preset-default so we pull in this file to extend it -const defaults = require( '@wordpress/scripts/config/jest-unit.config.js' ); -const path = require( 'path' ); - -// Basically, CWD, so 'apps/editing-toolkit'. -// Without this, it tries to use 'apps/editing-toolkit/bin' -const pluginRoot = path.resolve( './' ); - -const config = { - ...defaults, - rootDir: path.normalize( '../../../' ), // To detect wp-calypso root node_modules - testMatch: [ `${ pluginRoot }/**/?(*.)test.[jt]s?(x)` ], - transform: { '^.+\\.[jt]sx?$': path.join( __dirname, 'babel-transform' ) }, - setupFilesAfterEnv: [ - ...( defaults.setupFilesAfterEnv || [] ), // extend if present - '/apps/editing-toolkit/bin/js-unit-setup', - ], -}; - -module.exports = config; diff --git a/apps/editing-toolkit/bin/js-unit-setup.js b/apps/editing-toolkit/bin/js-unit-setup.js index 9bbb71c881f74..04fdb5cd125c9 100644 --- a/apps/editing-toolkit/bin/js-unit-setup.js +++ b/apps/editing-toolkit/bin/js-unit-setup.js @@ -22,3 +22,5 @@ beforeAll( () => { afterAll( () => { global.console.error.mockRestore(); } ); + +jest.mock( 'a8c-fse-common-data-stores', () => {}, { virtual: true } ); diff --git a/apps/editing-toolkit/bin/newspack-block-sync-phpcs.xml b/apps/editing-toolkit/bin/newspack-block-sync-phpcs.xml deleted file mode 100644 index 8906c22d72c04..0000000000000 --- a/apps/editing-toolkit/bin/newspack-block-sync-phpcs.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - Detect issues with newspack blocks sync - - - - - - - - - - ../editing-toolkit-plugin/newspack-blocks/synced-newspack-blocks - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips.js b/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips.js index 758520f286a11..84cd967a2dd9a 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips.js @@ -7,7 +7,10 @@ import { debounce } from 'lodash'; * WordPress dependencies */ import { useState } from '@wordpress/element'; -import { __experimentalInserterMenuExtension as InserterMenuExtension } from '@wordpress/block-editor'; +import { + __unstableInserterMenuExtension, + __experimentalInserterMenuExtension, +} from '@wordpress/block-editor'; import { registerPlugin } from '@wordpress/plugins'; /** @@ -16,6 +19,13 @@ import { registerPlugin } from '@wordpress/plugins'; import ContextualTip from './contextual-tips/contextual-tip'; import './contextual-tips/style.scss'; +// InserterMenuExtension has been made unstable in this PR: https://github.com/WordPress/gutenberg/pull/31417 / Gutenberg v10.7.0-rc.1. +// We need to support both symbols until we're sure Gutenberg < v10.7.x is not used anymore in WPCOM. +const InserterMenuExtension = + typeof __unstableInserterMenuExtension !== 'undefined' + ? __unstableInserterMenuExtension + : __experimentalInserterMenuExtension; + const ContextualTips = function () { const [ debouncedFilterValue, setFilterValue ] = useState( '' ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips/contextual-tip.js b/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips/contextual-tip.js index f1730cd2ee082..67309710ebd1e 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips/contextual-tip.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-inserter-modifications/contextual-tips/contextual-tip.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, filter, deburr, lowerCase, includes, uniq } from 'lodash'; +import { get, filter, deburr, lowerCase, includes } from 'lodash'; /** * WordPress dependencies @@ -30,7 +30,8 @@ function ContextualTip( { searchTerm, random = false, canUserCreate } ) { tipsList, ( { keywords, permission } ) => canUserCreate( permission ) && - filter( uniq( keywords ), ( keyword ) => includes( normalizedSearchTerm, keyword ) ).length + filter( [ ...new Set( keywords ) ], ( keyword ) => includes( normalizedSearchTerm, keyword ) ) + .length ); if ( ! foundTips.length ) { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-from-api.php b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-from-api.php index 6be0b078d408e..d59128447c295 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-from-api.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-from-api.php @@ -7,6 +7,8 @@ namespace A8C\FSE; +require_once __DIR__ . '/class-block-patterns-utils.php'; + /** * Class Block_Patterns_From_API */ @@ -22,125 +24,148 @@ class Block_Patterns_From_API { private static $instance = null; /** - * Cache key for patterns array. + * Valid source strings for retrieving patterns. * - * @var string + * @var array */ - private $patterns_cache_key; + private $valid_patterns_sources = array( 'block_patterns', 'fse_block_patterns' ); /** - * Block_Patterns constructor. + * Patterns source sites. + * + * @var array */ - private function __construct() { - $this->patterns_cache_key = sha1( - implode( - '_', - array( - 'block_patterns', - A8C_ETK_PLUGIN_VERSION, - $this->get_block_patterns_locale(), - ) - ) - ); + private $patterns_sources; - $this->register_patterns(); - } + /** + * A collection of utility methods. + * + * @var Block_Patterns_Utils + */ + private $utils; /** - * Creates instance. + * A dictionary to map existing WPCOM pattern categories to core patterns. + * These should match the categories in $patterns_sources, + * which are registered in $this->register_patterns() * - * @return \A8C\FSE\Block_Patterns + * @var array */ - public static function get_instance() { - if ( null === self::$instance ) { - self::$instance = new self(); - } + private $core_to_wpcom_categories_dictionary; - return self::$instance; + /** + * Block_Patterns constructor. + * + * @param array $patterns_sources A array of strings, each of which matches a valid source for retrieving patterns. + * @param Block_Patterns_Utils $utils A class dependency containing utils methods. + */ + public function __construct( $patterns_sources, Block_Patterns_Utils $utils = null ) { + $patterns_sources = empty( $patterns_sources ) ? array( 'block_patterns' ) : $patterns_sources; + $this->patterns_sources = empty( array_diff( $patterns_sources, $this->valid_patterns_sources ) ) ? $patterns_sources : array( 'block_patterns' ); + $this->utils = empty( $utils ) ? new \A8C\FSE\Block_Patterns_Utils() : $utils; + // Add categories to this array using the core pattern name as the key for core patterns we wish to "recategorize". + $this->core_to_wpcom_categories_dictionary = array( + 'core/quote' => array( + 'quotes' => __( 'Quotes', 'full-site-editing' ), + 'text' => __( 'Text', 'full-site-editing' ), + ), + ); } /** * Register FSE block patterns and categories. + * + * @return array Results of pattern registration. */ - private function register_patterns() { - if ( class_exists( 'WP_Block_Patterns_Registry' ) ) { - // Remove core patterns. - foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $pattern ) { - if ( 'core/' === substr( $pattern['name'], 0, 5 ) ) { - unregister_block_pattern( $pattern['name'] ); - } - } - } + public function register_patterns() { + $this->reregister_core_patterns(); - $pattern_categories = array(); - $block_patterns = $this->get_patterns(); + // Used to track which patterns we successfully register. + $results = array(); - foreach ( (array) $block_patterns as $pattern ) { - foreach ( (array) $pattern['categories'] as $slug => $category ) { - $pattern_categories[ $slug ] = array( 'label' => $category['title'] ); - } - } + // For every pattern source site, fetch the patterns. + foreach ( $this->patterns_sources as $patterns_source ) { + $patterns_cache_key = $this->utils->get_patterns_cache_key( $patterns_source ); - // Unregister existing categories so that we can insert them in the desired order (alphabetically). - $existing_categories = array(); - foreach ( \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered() as $existing_category ) { - $existing_categories[ $existing_category['name'] ] = $existing_category; - unregister_block_pattern_category( $existing_category['name'] ); - } + $pattern_categories = array(); + $block_patterns = $this->get_patterns( $patterns_cache_key, $patterns_source ); - $pattern_categories = array_merge( $pattern_categories, $existing_categories ); + foreach ( (array) $block_patterns as $pattern ) { + foreach ( (array) $pattern['categories'] as $slug => $category ) { + $pattern_categories[ $slug ] = array( 'label' => $category['title'] ); + } + } - // Order categories alphabetically by their label. - uasort( - $pattern_categories, - function ( $a, $b ) { - return strnatcasecmp( $a['label'], $b['label'] ); + // Unregister existing categories so that we can insert them in the desired order (alphabetically). + $existing_categories = array(); + foreach ( \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered() as $existing_category ) { + $existing_categories[ $existing_category['name'] ] = $existing_category; + unregister_block_pattern_category( $existing_category['name'] ); } - ); - // Move the Featured category to be the first category. - if ( isset( $pattern_categories['featured'] ) ) { - $featured_category = $pattern_categories['featured']; - $pattern_categories = array( 'featured' => $featured_category ) + $pattern_categories; - } + $pattern_categories = array_merge( $pattern_categories, $existing_categories ); - // Register categories (and re-register existing categories). - foreach ( (array) $pattern_categories as $slug => $category_properties ) { - register_block_pattern_category( $slug, $category_properties ); - } + // Order categories alphabetically by their label. + uasort( + $pattern_categories, + function ( $a, $b ) { + return strnatcasecmp( $a['label'], $b['label'] ); + } + ); - foreach ( (array) $block_patterns as $pattern ) { - if ( $this->can_register_pattern( $pattern ) ) { - $is_premium = isset( $pattern['pattern_meta']['is_premium'] ) ? boolval( $pattern['pattern_meta']['is_premium'] ) : false; + // Move the Featured category to be the first category. + if ( isset( $pattern_categories['featured'] ) ) { + $featured_category = $pattern_categories['featured']; + $pattern_categories = array( 'featured' => $featured_category ) + $pattern_categories; + } - // Set custom viewport width for the pattern preview with a - // default width of 1280 and ensure a safe minimum width of 320. - $viewport_width = isset( $pattern['pattern_meta']['viewport_width'] ) ? intval( $pattern['pattern_meta']['viewport_width'] ) : 1280; - $viewport_width = $viewport_width < 320 ? 320 : $viewport_width; + // Register categories (and re-register existing categories). + foreach ( (array) $pattern_categories as $slug => $category_properties ) { + register_block_pattern_category( $slug, $category_properties ); + } - register_block_pattern( - self::PATTERN_NAMESPACE . $pattern['name'], - array( - 'title' => $pattern['title'], - 'description' => $pattern['description'], - 'content' => $pattern['html'], - 'viewportWidth' => $viewport_width, - 'categories' => array_keys( - $pattern['categories'] - ), - 'isPremium' => $is_premium, - ) - ); + foreach ( (array) $block_patterns as $pattern ) { + if ( $this->can_register_pattern( $pattern ) ) { + $is_premium = isset( $pattern['pattern_meta']['is_premium'] ) ? boolval( $pattern['pattern_meta']['is_premium'] ) : false; + + // Set custom viewport width for the pattern preview with a + // default width of 1280 and ensure a safe minimum width of 320. + $viewport_width = isset( $pattern['pattern_meta']['viewport_width'] ) ? intval( $pattern['pattern_meta']['viewport_width'] ) : 1280; + $viewport_width = $viewport_width < 320 ? 320 : $viewport_width; + $pattern_name = self::PATTERN_NAMESPACE . $pattern['name']; + $block_types = $this->utils->maybe_get_pattern_block_types_from_pattern_meta( $pattern ); + + $results[ $pattern_name ] = register_block_pattern( + $pattern_name, + array( + 'title' => $pattern['title'], + 'description' => $pattern['description'], + 'content' => $pattern['html'], + 'viewportWidth' => $viewport_width, + 'categories' => array_keys( + $pattern['categories'] + ), + 'isPremium' => $is_premium, + 'blockTypes' => $block_types, + ) + ); + } } } + + $this->update_core_patterns_with_wpcom_categories(); + + return $results; } /** * Returns a list of patterns. * - * @return array + * @param string $patterns_cache_key Key to store responses to and fetch responses from cache. + * @param string $patterns_source Slug for valid patterns source site, e.g., `block_patterns`. + * @return array The list of translated patterns. */ - private function get_patterns() { + private function get_patterns( $patterns_cache_key, $patterns_source ) { $override_source_site = apply_filters( 'a8c_override_patterns_source_site', false ); if ( $override_source_site ) { // Skip caching and request all patterns from a specified source site. @@ -154,65 +179,36 @@ private function get_patterns() { 'tags' => 'pattern', 'pattern_meta' => 'is_web', ), - 'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->get_block_patterns_locale() + 'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->utils->get_block_patterns_locale() ) ); - $args = array( 'timeout' => 20 ); - - if ( function_exists( 'wpcom_json_api_get' ) ) { - $response = wpcom_json_api_get( $request_url, $args ); - } else { - $response = wp_remote_get( $request_url, $args ); - } - - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { - return array(); - } - return json_decode( wp_remote_retrieve_body( $response ), true ); + return $this->utils->remote_get( $request_url ); } - $block_patterns = wp_cache_get( $this->patterns_cache_key, 'ptk_patterns' ); + $block_patterns = $this->utils->cache_get( $patterns_cache_key, 'ptk_patterns' ); // Load fresh data if we don't have any patterns. if ( false === $block_patterns || ( defined( 'WP_DISABLE_PATTERN_CACHE' ) && WP_DISABLE_PATTERN_CACHE ) ) { $request_url = esc_url_raw( add_query_arg( array( - 'tags' => 'pattern', - 'pattern_meta' => 'is_web', + 'tags' => 'pattern', + 'pattern_meta' => 'is_web', + 'patterns_source' => $patterns_source, ), - 'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->get_block_patterns_locale() + 'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->utils->get_block_patterns_locale() ) ); - $args = array( 'timeout' => 20 ); - - if ( function_exists( 'wpcom_json_api_get' ) ) { - $response = wpcom_json_api_get( $request_url, $args ); - } else { - $response = wp_remote_get( $request_url, $args ); - } + $block_patterns = $this->utils->remote_get( $request_url ); - if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { - return array(); - } - $block_patterns = json_decode( wp_remote_retrieve_body( $response ), true ); - wp_cache_add( $this->patterns_cache_key, $block_patterns, 'ptk_patterns', DAY_IN_SECONDS ); + $this->utils->cache_add( $patterns_cache_key, $block_patterns, 'ptk_patterns', DAY_IN_SECONDS ); } return $block_patterns; } - /** - * Get the locale to be used for fetching block patterns - */ - private function get_block_patterns_locale() { - // Make sure to get blog locale, not user locale. - $language = function_exists( 'get_blog_lang_code' ) ? get_blog_lang_code() : get_locale(); - return \A8C\FSE\Common\get_iso_639_locale( $language ); - } - /** * Check that the pattern is allowed to be registered. * @@ -246,5 +242,62 @@ private function can_register_pattern( $pattern ) { return true; } + + /** + * Unregister all core patterns, then reregister core patterns in core WordPress only, + * that is those in wp-includes/block-patterns.php + * Gutenberg adds new and overrides existing core patterns. We don't want these for now. + */ + private function reregister_core_patterns() { + if ( class_exists( 'WP_Block_Patterns_Registry' ) ) { + foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $pattern ) { + // Gutenberg registers patterns with varying prefixes, but categorizes them using `core/*` in a blockTypes array. + // This will ensure we remove `query/*` blocks for example. + // TODO: We need to revisit our usage or $pattern['blockTypes']: they are currently an experimental feature and not guaranteed to reference `core/*` blocks. + $pattern_block_type_or_name = + isset( $pattern['blockTypes'] ) && ! empty( $pattern['blockTypes'][0] ) + ? $pattern['blockTypes'][0] + : $pattern['name']; + if ( 'core/' === substr( $pattern_block_type_or_name, 0, 5 ) ) { + unregister_block_pattern( $pattern['name'] ); + } + } + if ( function_exists( '_register_core_block_patterns_and_categories' ) ) { + $did_switch_locale = switch_to_locale( $this->utils->get_block_patterns_locale() ); + _register_core_block_patterns_and_categories(); + // The site locale might be the same as the current locale so switching could have failed in such instances. + if ( false !== $did_switch_locale ) { + restore_previous_locale(); + } + } + } + } + + /** + * Update categories for core patterns if a records exists in $this->core_to_wpcom_categories_dictionary + * and reregister them. + */ + private function update_core_patterns_with_wpcom_categories() { + if ( class_exists( 'WP_Block_Patterns_Registry' ) ) { + foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $pattern ) { + $wpcom_categories = + $pattern['name'] && isset( $this->core_to_wpcom_categories_dictionary[ $pattern['name'] ] ) + ? $this->core_to_wpcom_categories_dictionary[ $pattern['name'] ] + : null; + if ( $wpcom_categories ) { + unregister_block_pattern( $pattern['name'] ); + $pattern_properties = array_merge( + $pattern, + array( 'categories' => array_keys( $wpcom_categories ) ) + ); + unset( $pattern_properties['name'] ); + register_block_pattern( + $pattern['name'], + $pattern_properties + ); + } + } + } + } } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-utils.php b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-utils.php new file mode 100644 index 0000000000000..55934c7dc49a1 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/class-block-patterns-utils.php @@ -0,0 +1,118 @@ + 20 ); + + if ( function_exists( 'wpcom_json_api_get' ) ) { + $response = wpcom_json_api_get( $request_url, $args ); + } else { + $response = wp_remote_get( $request_url, $args ); + } + + if ( 200 !== wp_remote_retrieve_response_code( $response ) ) { + return array(); + } + return json_decode( wp_remote_retrieve_body( $response ), true ); + } + + /** + * A wrapper for wp_cache_add. + * + * @param int|string $key The cache key to use for retrieval later. + * @param mixed $data The data to add to the cache. + * @param string $group The group to add the cache to. Enables the same key to be used across groups. Default empty. + * @param int $expire When the cache data should expire, in seconds. + * Default 0 (no expiration). + * @return bool True on success, false if cache key and group already exist. + */ + public function cache_add( $key, $data, $group, $expire ) { + return wp_cache_add( $key, $data, $group, $expire ); + } + + /** + * A wrapper for wp_cache_get. + * + * @param int|string $key The key under which the cache contents are stored. + * @param string $group Where the cache contents are grouped. Default empty. + * @return mixed|false The cache contents on success, false on failure to retrieve contents. + */ + public function cache_get( $key, $group ) { + return wp_cache_get( $key, $group ); + } + + /** + * Returns the sha1 hash of a concatenated string to use as a cache key. + * + * @param string $patterns_slug A slug for a patterns source site, e.g., `block_patterns`. + * @return string locale slug + */ + public function get_patterns_cache_key( $patterns_slug ) { + return sha1( + implode( + '_', + array( + $patterns_slug, + A8C_ETK_PLUGIN_VERSION, + $this->get_block_patterns_locale(), + ) + ) + ); + } + + /** + * Get the locale to be used for fetching block patterns + * + * @return string locale slug + */ + public function get_block_patterns_locale() { + // Make sure to get blog locale, not user locale. + $language = function_exists( 'get_blog_lang_code' ) ? get_blog_lang_code() : get_locale(); + return \A8C\FSE\Common\get_iso_639_locale( $language ); + } + + /** + * Check for block type values in the pattern_meta tag. + * When tags have a prefix of `block_type_`, we expect the remaining suffix to be a blockType value. + * We'll add these values to the `(array) blockType` options property when registering the pattern + * via `register_block_pattern`. + * + * @param array $pattern A pattern with a 'pattern_meta' array. + * + * @return array An array of block types defined in pattern meta. + */ + public function maybe_get_pattern_block_types_from_pattern_meta( $pattern ) { + $block_types = array(); + + if ( ! isset( $pattern['pattern_meta'] ) || empty( $pattern['pattern_meta'] ) ) { + return $block_types; + } + + foreach ( $pattern['pattern_meta'] as $pattern_meta => $value ) { + // Match against tags starting with `block_type_`. + $split_slug = preg_split( '/^block_type_/', $pattern_meta ); + + if ( isset( $split_slug[1] ) ) { + $block_types[] = $split_slug[1]; + } + } + + return $block_types; + } +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/src/premium-block-patterns.tsx b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/src/premium-block-patterns.tsx index f2db00bda71bd..95ffd783803d9 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/src/premium-block-patterns.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/src/premium-block-patterns.tsx @@ -1,10 +1,11 @@ /** * External dependencies */ -import * as React from 'react'; +import React, { ReactNode } from 'react'; import { useDispatch, useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { registerPlugin as originalRegisterPlugin, PluginSettings } from '@wordpress/plugins'; +import type { EditorSettings } from '@wordpress/block-editor'; /** * Internal dependencies @@ -16,13 +17,17 @@ interface PatternTitleProps { description?: string; } +interface ExperimentalEditorSettings extends EditorSettings { + __experimentalBlockPatterns: ExperimentalBlockPattern[]; +} + interface ExperimentalBlockPattern { categories: string[]; content: string; description: string; isPremium: boolean; name: string; - title: string; + title: ReactNode; viewportWidth: number; } @@ -40,12 +45,16 @@ export const PatternTitleContainer: React.FunctionComponent< PatternTitleProps > export const PremiumBlockPatterns: React.FunctionComponent = () => { const { __experimentalBlockPatterns } = useSelect( ( select ) => select( 'core/block-editor' ).getSettings() + ) as ExperimentalEditorSettings; + const { + updateSettings, + }: { updateSettings: ( settings: Partial< ExperimentalEditorSettings > ) => void } = useDispatch( + 'core/block-editor' ); - const { updateSettings } = useDispatch( 'core/block-editor' ); if ( __experimentalBlockPatterns ) { let shouldUpdateBlockPatterns = false; - const updatedPatterns: Array = []; + const updatedPatterns: ExperimentalBlockPattern[] = []; __experimentalBlockPatterns.forEach( ( originalPattern: ExperimentalBlockPattern ) => { const pattern = { ...originalPattern }; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-from-api-test.php b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-from-api-test.php new file mode 100644 index 0000000000000..a62e51f0fba77 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-from-api-test.php @@ -0,0 +1,156 @@ +pattern_mock_object = array( + 'ID' => '1', + 'site_id' => '2', + 'title' => 'test title', + 'name' => 'test pattern name', + 'description' => 'test description', + 'html' => '

test

', + 'source_url' => 'http;//test', + 'modified_date' => 'dd:mm:YY', + 'categories' => array( + array( + 'title' => 'test-category', + ), + ), + ); + } + + /** + * Returns a mock of Block_Patterns_Utils. + * + * @param array $pattern_mock_response What we want Block_Patterns_Utils->remote_get() to return. + * @param bool|array $cache_get What we want Block_Patterns_Utils->cache_get() to return. + * @param bool $cache_add What we want Block_Patterns_Utils->cache_add() to return. + * @param string $get_patterns_cache_key What we want Block_Patterns_Utils->get_patterns_cache_key() to return. + * @param string $get_block_patterns_locale What we want Block_Patterns_Utils->get_block_patterns_locale() to return. + * @return obj PHP Unit mock object. + */ + public function createBlockPatternsUtilsMock( $pattern_mock_response, $cache_get = false, $cache_add = true, $get_patterns_cache_key = 'key-largo', $get_block_patterns_locale = 'fr' ) { + $mock = $this->createMock( Block_Patterns_Utils::class ); + + $mock->method( 'remote_get' ) + ->willReturn( $pattern_mock_response ); + + $mock->method( 'cache_get' ) + ->willReturn( $cache_get ); + + $mock->method( 'cache_add' ) + ->willReturn( $cache_add ); + + $mock->method( 'get_patterns_cache_key' ) + ->willReturn( $get_patterns_cache_key ); + + $mock->method( 'get_block_patterns_locale' ) + ->willReturn( $get_block_patterns_locale ); + + return $mock; + } + + /** + * Tests that we're making a request where there are no cached patterns. + */ + public function test_patterns_request_succeeds_with_empty_cache() { + $utils_mock = $this->createBlockPatternsUtilsMock( array( $this->pattern_mock_object ) ); + $block_patterns_from_api = new Block_Patterns_From_API( array(), $utils_mock ); + + $utils_mock->expects( $this->once() ) + ->method( 'cache_get' ) + ->willReturn( false ); + + $utils_mock->expects( $this->once() ) + ->method( 'remote_get' ) + ->with( 'https://public-api.wordpress.com/rest/v1/ptk/patterns/fr?tags=pattern&pattern_meta=is_web&patterns_source=block_patterns' ); + + $utils_mock->expects( $this->once() ) + ->method( 'cache_add' ) + ->with( $this->stringContains( 'key-largo' ), array( $this->pattern_mock_object ), 'ptk_patterns', DAY_IN_SECONDS ); + + $this->assertEquals( array( 'a8c/' . $this->pattern_mock_object['name'] => true ), $block_patterns_from_api->register_patterns() ); + } + + /** + * Tests that we're NOT making a request where there ARE cached patterns. + */ + public function test_patterns_request_succeeds_with_set_cache() { + $utils_mock = $this->createBlockPatternsUtilsMock( array( $this->pattern_mock_object ), array( $this->pattern_mock_object ) ); + $block_patterns_from_api = new Block_Patterns_From_API( array(), $utils_mock ); + + $utils_mock->expects( $this->once() ) + ->method( 'cache_get' ) + ->with( $this->stringContains( 'key-largo' ), 'ptk_patterns' ); + + $utils_mock->expects( $this->never() ) + ->method( 'remote_get' ); + + $utils_mock->expects( $this->never() ) + ->method( 'cache_add' ); + + $this->assertEquals( array( 'a8c/' . $this->pattern_mock_object['name'] => true ), $block_patterns_from_api->register_patterns() ); + } + + /** + * Tests that we're making a request where we're overriding the source site. + */ + public function test_patterns_request_succeeds_with_override_source_site() { + $example_site = function () { + return 'dotcom'; + }; + + add_filter( 'a8c_override_patterns_source_site', $example_site ); + $utils_mock = $this->createBlockPatternsUtilsMock( array( $this->pattern_mock_object ) ); + $block_patterns_from_api = new Block_Patterns_From_API( array(), $utils_mock ); + + $utils_mock->expects( $this->never() ) + ->method( 'cache_get' ); + + $utils_mock->expects( $this->never() ) + ->method( 'cache_add' ); + + $utils_mock->expects( $this->once() ) + ->method( 'remote_get' ) + ->with( 'https://public-api.wordpress.com/rest/v1/ptk/patterns/fr?site=dotcom&tags=pattern&pattern_meta=is_web' ); + + $this->assertEquals( array( 'a8c/' . $this->pattern_mock_object['name'] => true ), $block_patterns_from_api->register_patterns() ); + + remove_filter( 'a8c_override_patterns_source_site', $example_site ); + } +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-utils-test.php b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-utils-test.php new file mode 100644 index 0000000000000..792b53d96854d --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/block-patterns/test/class-block-patterns-utils-test.php @@ -0,0 +1,90 @@ +utils = new Block_Patterns_Utils(); + } + + /** + * Tests that we receive an empty block_types array where there are no block types in pattern_meta + */ + public function test_should_return_empty_array_from_block_types_check() { + $test_pattern = $this->get_test_pattern(); + $block_types = $this->utils->maybe_get_pattern_block_types_from_pattern_meta( $test_pattern ); + + $this->assertEmpty( $block_types ); + } + + /** + * Tests that we can parse block types from pattern_meta. + */ + public function test_should_return_block_types_from_patterns_meta() { + $test_pattern = $this->get_test_pattern( + array( + 'pattern_meta' => array( + 'block_type_core/template-part/footer' => true, + ), + ) + ); + $block_types = $this->utils->maybe_get_pattern_block_types_from_pattern_meta( $test_pattern ); + + $this->assertEquals( array( 'core/template-part/footer' ), $block_types ); + } + + /** + * Util function from grabbing a test pattern. + * + * @param array $new_pattern_values Values to merge into the default array. + * @return array A test pattern. + */ + private function get_test_pattern( $new_pattern_values = array() ) { + $default_pattern = array( + 'ID' => '1', + 'site_id' => '2', + 'title' => 'test title', + 'name' => 'test pattern name', + 'description' => 'test description', + 'html' => '

test

', + 'source_url' => 'http;//test', + 'modified_date' => 'dd:mm:YY', + 'categories' => array( + array( + 'title' => 'test-category', + ), + ), + 'pattern_meta' => array( + 'is_web' => true, + ), + ); + + return array_merge( $default_pattern, $new_pattern_values ); + } +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/coming-soon/fallback-coming-soon-page.php b/apps/editing-toolkit/editing-toolkit-plugin/coming-soon/fallback-coming-soon-page.php index b21eecd98ce27..cdb0c44efbe52 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/coming-soon/fallback-coming-soon-page.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/coming-soon/fallback-coming-soon-page.php @@ -150,6 +150,9 @@ function get_onboarding_url() { margin-right: 16px; margin-bottom: 0; } + .wpcom-coming-soon-wplogo a { + border: none; + } .wpcom-coming-soon-marketing-copy-text { line-height: 1.4; margin: 0; @@ -278,7 +281,7 @@ function get_onboarding_url() {

-

+

diff --git a/apps/editing-toolkit/editing-toolkit-plugin/common/data-stores/domain-suggestions.ts b/apps/editing-toolkit/editing-toolkit-plugin/common/data-stores/domain-suggestions.ts index 58ff549fb5f77..7ea72f2a2388f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/common/data-stores/domain-suggestions.ts +++ b/apps/editing-toolkit/editing-toolkit-plugin/common/data-stores/domain-suggestions.ts @@ -3,4 +3,4 @@ */ import { DomainSuggestions } from '@automattic/data-stores'; -DomainSuggestions.register( { vendor: 'variation2_front' } ); +DomainSuggestions.register(); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/common/hide-plugin-buttons-mobile.scss b/apps/editing-toolkit/editing-toolkit-plugin/common/hide-plugin-buttons-mobile.scss index fe82c7dd1195d..1dd9db456c224 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/common/hide-plugin-buttons-mobile.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/common/hide-plugin-buttons-mobile.scss @@ -1,6 +1,6 @@ -@import '~@wordpress/base-styles/mixins'; -@import '~@wordpress/base-styles/variables'; -@import '~@wordpress/base-styles/breakpoints'; +@import '@wordpress/base-styles/mixins'; +@import '@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/breakpoints'; .interface-pinned-items > button:not( :first-child ) { @media ( max-width: $break-medium ) { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/common/index.scss b/apps/editing-toolkit/editing-toolkit-plugin/common/index.scss index 712a34236f43d..db6e1d6a25302 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/common/index.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/common/index.scss @@ -1,12 +1,9 @@ body.hide-homepage-title { - .editor-post-title { - display: none; - } - // When the title is hidden in the block editor while editing the homepage, - // leave enough space above the first block for the toolbar to show correctly. - .block-editor-block-list__layout.is-root-container > .wp-block:first-child { - margin-top: 64px; + // Allow homepage title to be edited even when hidden + // Lighter color to signify not visible from front page + .editor-post-title { + opacity: .4; } } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-description/edit.js b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-description/edit.js index 205554d543b39..3d4f0cb6f9b00 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-description/edit.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-description/edit.js @@ -3,7 +3,6 @@ * External dependencies */ import classNames from 'classnames'; -import { noop } from 'lodash'; /** * WordPress dependencies @@ -30,6 +29,8 @@ import { PanelBody } from '@wordpress/components'; */ import { withSiteOptions } from '../../lib'; +const noop = () => {}; + function SiteDescriptionEdit( { attributes, backgroundColor, diff --git a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-title/edit.js b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-title/edit.js index 2e72ff17a87f3..a3ad81fb80fae 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-title/edit.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/site-title/edit.js @@ -3,7 +3,6 @@ * External dependencies */ import classNames from 'classnames'; -import { noop } from 'lodash'; /** * WordPress dependencies @@ -29,6 +28,8 @@ import { PanelBody } from '@wordpress/components'; */ import { withSiteOptions } from '../../lib'; +const noop = () => {}; + function SiteTitleEdit( { attributes, className, diff --git a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/template/edit.js b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/template/edit.js index b6b44360876be..90d3c30353439 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/template/edit.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/blocks/template/edit.js @@ -4,7 +4,7 @@ * External dependencies */ import classNames from 'classnames'; -import { get, noop } from 'lodash'; +import { get } from 'lodash'; /** * WordPress dependencies @@ -23,6 +23,8 @@ import { addQueryArgs } from '@wordpress/url'; */ import './style.scss'; +const noop = () => {}; + const TemplateEdit = compose( withState( { templateClientId: null } ), withSelect( ( select, { attributes, templateClientId } ) => { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/editor/image-block-keywords/index.js b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/editor/image-block-keywords/index.js index 03cbf6ce4de41..6b3d491cbb6a1 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/editor/image-block-keywords/index.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/editor/image-block-keywords/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { assign } from 'lodash'; import { addFilter } from '@wordpress/hooks'; const additionalKeywords = [ 'logo', 'brand', 'emblem', 'hallmark' ]; @@ -14,9 +13,10 @@ addFilter( return settings; } - settings = assign( {}, settings, { + settings = { + ...settings, keywords: settings.keywords.concat( additionalKeywords ), - } ); + }; return settings; } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/tests/fse-back-button.spec.js b/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/tests/fse-back-button.spec.js deleted file mode 100644 index a097dbecd47a0..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/dotcom-fse/tests/fse-back-button.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -const { createNewPost } = require( '@wordpress/e2e-test-utils' ); - -import { activateTheme } from '../../e2e-test-helpers'; - -describe( 'Full Site Editing Back Button', () => { - beforeAll( async () => { - // Otherwise, tests often do not pass quickly enough. - jest.setTimeout( 10000 ); - await activateTheme( 'maywood' ); - // Creates a new page and uses the blank page layout. - await createNewPost( { postType: 'page', title: 'New e2e Page!' } ); - await page.click( '.page-template-modal__buttons .components-button.is-primary.is-large' ); - } ); - - it( 'Should have an overriden button', async () => { - // Target the toolbar__override class we added to see if our custom button exists. - const button = await page.$( - '.components-toolbar.edit-post-fullscreen-mode-close__toolbar.edit-post-fullscreen-mode-close__toolbar__override' - ); - // Button is null if it does not exist. - expect( button ).toBeTruthy(); - } ); -} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/activate-theme.js b/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/activate-theme.js deleted file mode 100644 index 093cec148e26b..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/activate-theme.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * External dependencies - */ -const { visitAdminPage } = require( '@wordpress/e2e-test-utils' ); - -export async function activateTheme( themeSlug ) { - await visitAdminPage( 'themes.php' ); - const wrapperSelector = `div[data-slug="${ themeSlug }"]`; - const theme = await page.$( wrapperSelector ); - if ( ! theme ) { - throw new Error( `The theme ${ themeSlug } is not installed on the test site!` ); - } - const activateLink = await page.$( `${ wrapperSelector } a.activate` ); - // It is already active. - if ( ! activateLink ) { - return; - } - await page.click( `${ wrapperSelector } a.activate` ); -} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/index.js b/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/index.js deleted file mode 100644 index 2cf0f6c0a98dd..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/e2e-test-helpers/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './activate-theme'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/index.php b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/index.php index 24cdfdcfbb397..2fa5e63d0f8cb 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/index.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/index.php @@ -76,25 +76,14 @@ function enqueue_launch_button_script_and_style( $site_launch_options ) { * @param array $site_launch_options Site launch options. */ function enqueue_launch_flow_script_and_style( $site_launch_options ) { + $anchor_podcast = $site_launch_options['anchor_podcast']; - $launch_flow = $site_launch_options['launch_flow']; - - // Determine script name by launch flow. - // We are avoiding string concatenation for security reasons. - switch ( $launch_flow ) { - case 'gutenboarding-launch': - $script_name = 'gutenboarding-launch'; - break; - case 'focused-launch': - $script_name = 'focused-launch'; - break; - case 'launch-site': - // @TODO: this is just temporary for testing via feature flag. Remove it once focused-launch is live - $script_name = 'focused-launch'; - break; - default: - // For redirect or invalid flows, skip & exit early. - return; + if ( ! empty( $anchor_podcast ) ) { + // AnchorFM flow runs on focused-launch. + $script_name = 'focused-launch'; + } else { + // For redirect or non-AnchorFM sites, skip & exit early. + return; } $asset_file = include plugin_dir_path( __FILE__ ) . 'dist/' . $script_name . '.asset.php'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-focused-launch.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-focused-launch.tsx index 10d0ad450296d..f6a7e62a52992 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-focused-launch.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-focused-launch.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { registerPlugin as originalRegisterPlugin, PluginSettings } from '@wordpress/plugins'; import { useSelect, useDispatch } from '@wordpress/data'; import { hasQueryArg } from '@wordpress/url'; @@ -28,7 +28,7 @@ registerPlugin( 'a8c-editor-editor-focused-launch', { [ currentSiteId ] ); - const { isFocusedLaunchOpen } = useSelect( + const { isFocusedLaunchOpen, isAnchorFm } = useSelect( ( select ) => select( LAUNCH_STORE ).getState(), [] ); @@ -60,7 +60,7 @@ registerPlugin( 'a8c-editor-editor-focused-launch', { siteId={ currentSiteId } getCurrentLaunchFlowUrl={ getCurrentLaunchFlowUrl } isInIframe={ inIframe() } - isLaunchImmediately={ shouldLaunch } + isLaunchImmediately={ shouldLaunch || isAnchorFm } /> ) : null; }, diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-launch-sidebar.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-launch-sidebar.tsx index cc379a683af9f..9f9879de25297 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-launch-sidebar.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/attach-launch-sidebar.tsx @@ -1,12 +1,12 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useDispatch, useSelect } from '@wordpress/data'; import { registerPlugin as originalRegisterPlugin, PluginSettings } from '@wordpress/plugins'; import { doAction, hasAction } from '@wordpress/hooks'; import { LaunchContext } from '@automattic/launch'; -import { LocaleProvider } from '@automattic/i18n-utils'; +import { LocaleProvider, i18nDefaultLocaleSlug } from '@automattic/i18n-utils'; /** * Internal dependencies @@ -23,9 +23,7 @@ const registerPlugin = ( name: string, settings: Omit< PluginSettings, 'icon' > registerPlugin( 'a8c-editor-site-launch', { render: function LaunchSidebar() { - const { isSidebarOpen, isAnchorFm } = useSelect( ( select ) => - select( LAUNCH_STORE ).getState() - ); + const { isSidebarOpen } = useSelect( ( select ) => select( LAUNCH_STORE ).getState() ); const { closeSidebar, setSidebarFullscreen, unsetSidebarFullscreen } = useDispatch( LAUNCH_STORE ); @@ -47,7 +45,7 @@ registerPlugin( 'a8c-editor-site-launch', { } return ( - + - + ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-button/index.ts b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-button/index.ts index 3d83e7f50cf3a..88863018a9f9b 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-button/index.ts +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-button/index.ts @@ -12,7 +12,7 @@ import '@wordpress/editor'; * Internal dependencies */ import { inIframe } from '../../../block-inserter-modifications/contextual-tips/utils'; -import { GUTENBOARDING_LAUNCH_FLOW, FOCUSED_LAUNCH_FLOW, SITE_LAUNCH_FLOW } from '../constants'; +import { GUTENBOARDING_LAUNCH_FLOW } from '../constants'; import './styles.scss'; let handled = false; @@ -40,7 +40,14 @@ domReady( () => { return; } - const { launchUrl, launchFlow, isGutenboarding, anchorFmPodcastId } = siteLaunchOptions; + const { launchFlow, isGutenboarding, anchorFmPodcastId } = siteLaunchOptions; + // Enable anchor-flavoured features (the launch button works immediately). + const isAnchorFm = !! anchorFmPodcastId; + + // Display the Launch button only for the AnchorFM flow + if ( launchFlow !== GUTENBOARDING_LAUNCH_FLOW || ! isAnchorFm ) { + return; + } // Wrap 'Launch' button link to control launch flow. const launchButton = document.createElement( 'button' ); @@ -55,8 +62,6 @@ domReady( () => { is_in_iframe: inIframe(), } ); - // Enable anchor-flavoured gutenboarding features (the launch button works immediately). - const isAnchorFm = !! anchorFmPodcastId; if ( isAnchorFm ) { dispatch( 'automattic/launch' ).enableAnchorFm(); } @@ -66,22 +71,15 @@ domReady( () => { switch ( launchFlow ) { case GUTENBOARDING_LAUNCH_FLOW: + // @TODO: remove this temporary solution once backend returns correct launch flow value + if ( isAnchorFm ) { + dispatch( 'automattic/launch' ).openFocusedLaunch(); + break; + } // Save post in the background while step-by-step flow opens dispatch( 'automattic/launch' ).openSidebar(); delayedSavePost(); break; - case FOCUSED_LAUNCH_FLOW: - // Save post in the background while focused launch flow opens - dispatch( 'automattic/launch' ).openFocusedLaunch(); - delayedSavePost(); - break; - case SITE_LAUNCH_FLOW: - // Save post first before redirecting to launch url - ( async () => { - await savePost(); - window.top.location.href = launchUrl; - } )(); - break; } } ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/index.tsx index 47c6c6d775f81..2b1f78f74583d 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/index.tsx @@ -1,9 +1,9 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useI18n } from '@automattic/react-i18n'; +import { useI18n } from '@wordpress/react-i18n'; /** * Internal dependencies diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/item.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/item.tsx index bac380a6f52e1..f0a8c92c106f1 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/item.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/item.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { Button, SVG, Circle } from '@wordpress/components'; import { Icon, check } from '@wordpress/icons'; import classnames from 'classnames'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/styles.scss index 8a9faee269a36..7b014b0e732d8 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-menu/styles.scss @@ -1,4 +1,4 @@ -@import '~@automattic/onboarding/styles/mixins'; +@import '@automattic/onboarding/styles/mixins'; .nux-launch-menu { h4 { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/index.tsx index 269346d120f7c..e4bed3d85532a 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import classnames from 'classnames'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/styles.scss index 3a08797de9f49..f779a7d8fd1d0 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-modal/styles.scss @@ -1,10 +1,10 @@ -@import '~@wordpress/base-styles/mixins'; -@import '~@wordpress/base-styles/variables'; -@import '~@wordpress/base-styles/breakpoints'; -@import '~@wordpress/base-styles/z-index'; -@import '~@automattic/typography/styles/variables'; -@import '~@automattic/onboarding/styles/variables'; -@import '~@automattic/onboarding/styles/mixins'; +@import '@wordpress/base-styles/mixins'; +@import '@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/breakpoints'; +@import '@wordpress/base-styles/z-index'; +@import '@automattic/typography/styles/variables'; +@import '@automattic/onboarding/styles/variables'; +@import '@automattic/onboarding/styles/mixins'; body.has-nux-launch-modal { overflow: hidden; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-progress/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-progress/index.tsx index f1b06aaee9676..671b9cc30fff6 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-progress/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-progress/index.tsx @@ -1,10 +1,10 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useSelect } from '@wordpress/data'; import { sprintf } from '@wordpress/i18n'; -import { useI18n } from '@automattic/react-i18n'; +import { useI18n } from '@wordpress/react-i18n'; /** * Internal dependencies diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/index.tsx index b0129f963b5ee..7d00f9c5bf321 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { Title, SubTitle, ActionButtons, NextButton } from '@automattic/onboarding'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/styles.scss index 44aeccdc4047d..87a3d04ae4776 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-sidebar/styles.scss @@ -1,5 +1,5 @@ -@import '~@wordpress/base-styles/breakpoints'; -@import '~@automattic/onboarding/styles/mixins'; +@import '@wordpress/base-styles/breakpoints'; +@import '@automattic/onboarding/styles/mixins'; .nux-launch-sidebar { @include onboarding-block-margin; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/index.tsx index 6eafc96de7742..798fd43ffdab4 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; /** * Internal dependencies diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/styles.scss index 73ec53001cf18..a1d8a7d31c6ac 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-step/styles.scss @@ -1,7 +1,7 @@ -@import '~@wordpress/base-styles/variables'; -@import '~@wordpress/base-styles/breakpoints'; -@import '~@automattic/typography/styles/fonts'; -@import '~@automattic/onboarding/styles/mixins'; +@import '@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/breakpoints'; +@import '@automattic/typography/styles/fonts'; +@import '@automattic/onboarding/styles/mixins'; .nux-launch-step__header { @include onboarding-heading-padding; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/domain-step/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/domain-step/index.tsx index 2dce0df2678c3..7446645127654 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/domain-step/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/domain-step/index.tsx @@ -1,9 +1,10 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { useI18n } from '@wordpress/react-i18n'; +import { useLocale } from '@automattic/i18n-utils'; import DomainPicker, { mockDomainSuggestion } from '@automattic/domain-picker'; import { Title, SubTitle, ActionButtons, BackButton, NextButton } from '@automattic/onboarding'; import { recordTracksEvent } from '@automattic/calypso-analytics'; @@ -18,6 +19,9 @@ import { useDomainSelection, useSiteDomains, useDomainSearch } from '@automattic import { FLOW_ID } from '../../constants'; const DomainStep: React.FunctionComponent< LaunchStepProps > = ( { onPrevStep, onNextStep } ) => { + const { __, hasTranslation } = useI18n(); + const locale = useLocale(); + const { onDomainSelect, onExistingSubdomainSelect, currentDomain } = useDomainSelection(); const { siteSubdomain } = useSiteDomains(); const { domainSearch, setDomainSearch } = useDomainSearch(); @@ -41,14 +45,25 @@ const DomainStep: React.FunctionComponent< LaunchStepProps > = ( { onPrevStep, o } ); }; + const fallbackSubtitleText = __( + 'Free for the first year with any paid plan.', + 'full-site-editing' + ); + const newSubtitleText = __( + 'Free for the first year with any annual plan.', + 'full-site-editing' + ); + const subtitleText = + locale === 'en' || hasTranslation?.( 'Free for the first year with any annual plan.' ) + ? newSubtitleText + : fallbackSubtitleText; + return (
{ __( 'Choose a domain', 'full-site-editing' ) } - - { __( 'Free for the first year with any paid plan.', 'full-site-editing' ) } - + { subtitleText }
@@ -66,7 +81,7 @@ const DomainStep: React.FunctionComponent< LaunchStepProps > = ( { onPrevStep, o onExistingSubdomainSelect={ onExistingSubdomainSelect } analyticsUiAlgo="editor_domain_modal" segregateFreeAndPaid - locale={ document.documentElement.lang } + locale={ locale } />
diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/index.tsx index 26df370562866..35f343610870d 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/index.tsx @@ -1,16 +1,17 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import classnames from 'classnames'; import { ThemeProvider } from 'emotion-theming'; import { createInterpolateElement } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { sprintf } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; import { Button, Tip } from '@wordpress/components'; import { Icon, check } from '@wordpress/icons'; import { useSiteDomains, useDomainSuggestion, useDomainSearch, useTitle } from '@automattic/launch'; -import { useLocalizeUrl } from '@automattic/i18n-utils'; +import { useLocale, useLocalizeUrl } from '@automattic/i18n-utils'; +import { useI18n } from '@wordpress/react-i18n'; import { Title, SubTitle, ActionButtons, BackButton } from '@automattic/onboarding'; import { CheckoutStepBody, @@ -22,7 +23,6 @@ import { SubmitButtonWrapper, FormStatus, } from '@automattic/composite-checkout'; -import { useLocale } from '@automattic/i18n-utils'; /** * Internal dependencies @@ -48,6 +48,7 @@ const FinalStep: React.FunctionComponent< LaunchStepProps > = ( { onNextStep, on } ); + const { __, hasTranslation } = useI18n(); const locale = useLocale(); const [ plan, planProduct ] = useSelect( ( select ) => [ @@ -127,7 +128,17 @@ const FinalStep: React.FunctionComponent< LaunchStepProps > = ( { onNextStep, on
); - const planSummaryCostLabelAnnually = __( 'billed annually', 'full-site-editing' ); + const fallbackPlanSummaryCostLabelAnnually = __( 'billed annually', 'full-site-editing' ); + // translators: %s is the cost per year (e.g "billed as 96$ annually") + const newPlanSummaryCostLabelAnnually = __( + 'per month, billed as %s annually', + 'full-site-editing' + ); + const planSummaryCostLabelAnnually = + locale === 'en' || hasTranslation?.( 'per month, billed as %s annually' ) + ? sprintf( newPlanSummaryCostLabelAnnually, planProduct?.annualPrice ) + : fallbackPlanSummaryCostLabelAnnually; + const planSummaryCostLabelMonthly = __( 'per month, billed monthly', 'full-site-editing' ); const planSummary = ( diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/styles.scss index 2a4243ec5a576..38e140f992db4 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/final-step/styles.scss @@ -1,5 +1,5 @@ -@import '~@automattic/typography/styles/variables'; -@import '~@automattic/onboarding/styles/mixins'; +@import '@automattic/typography/styles/variables'; +@import '@automattic/onboarding/styles/mixins'; // TODO: This is former dark-gray-500 from @wordpress/base-styles. // Replace with a color from the standard palette. diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/index.tsx index 0b6c8a68234d8..672c46fd6adde 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { __ } from '@wordpress/i18n'; import { TextControl, Tip } from '@wordpress/components'; import { Title, SubTitle, ActionButtons, BackButton, NextButton } from '@automattic/onboarding'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/styles.scss index 6136b26e2853c..56b29d623cb03 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/name-step/styles.scss @@ -1,4 +1,4 @@ -@import '~@automattic/onboarding/styles/variables'; +@import '@automattic/onboarding/styles/variables'; .nux-launch-step__input { position: relative; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/index.tsx index 4d23d0232a238..ca9f4be1858bf 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useSelect, useDispatch } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import PlansGrid from '@automattic/plans-grid'; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/styles.scss b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/styles.scss index 3045d41de8afd..308d285fb673d 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/styles.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch-steps/plan-step/styles.scss @@ -1,5 +1,5 @@ -@import '~@automattic/onboarding/styles/mixins'; -@import '~@automattic/plans-grid/src/variables'; +@import '@automattic/onboarding/styles/mixins'; +@import '@automattic/plans-grid/src/variables'; .nux-launch-modal.step-plan { // Remove extraneous whitespace after plans details. diff --git a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch/index.tsx index e2a2c9c4642b4..ada335b1cbc3d 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/editor-site-launch/src/launch/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import * as React from 'react'; +import React from 'react'; import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies diff --git a/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.js b/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.js new file mode 100644 index 0000000000000..3e02379065d20 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Errors that happened before this script had a chance to load + * are captured in a global array. See `./index.php`. + */ +const headErrors = window._jsErr || []; +const headErrorHandler = window._headJsErrorHandler; + +const reportError = ( { error } ) => { + // Sanitized error event objects do not include a nested error attribute. In + // that case, we return early to prevent a needless TypeError when defining + // `data`, below. Also, sanitized errors don't include any useful information, + // so the sensible thing to do is to completely ignore them. + if ( ! error ) { + return; + } + + const data = { + message: error.message, + trace: error.stack, + url: document.location.href, + feature: 'wp-admin', + }; + + return ( + apiFetch( { + global: true, + path: '/rest/v1.1/js-error', + method: 'POST', + data: { error: JSON.stringify( data ) }, + } ) + // eslint-disable-next-line no-console + .catch( () => console.error( 'Error: Unable to record the error in Logstash.' ) ) + ); +}; + +window.addEventListener( 'error', reportError ); + +// Remove the head handler as it's not needed anymore after we set the main one above +window.removeEventListener( 'error', headErrorHandler ); +delete window._headJsErrorHandler; + +// We still need to report the head errors, if any. +Promise.allSettled( headErrors.map( reportError ) ).then( () => delete window._jsErr ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.php b/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.php new file mode 100644 index 0000000000000..fff32664117e4 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/error-reporting/index.php @@ -0,0 +1,84 @@ + + /', $tag ) ) { + return str_replace( ' src', " crossorigin='anonymous' src", $tag ); + }; + return $tag; +} + +/** + * Enqueue assets + */ +function enqueue_script() { + $asset_file = include plugin_dir_path( __FILE__ ) . 'dist/error-reporting.asset.php'; + $script_dependencies = isset( $asset_file['dependencies'] ) ? $asset_file['dependencies'] : array(); + $script_version = isset( $asset_file['version'] ) ? $asset_file['version'] : filemtime( plugin_dir_path( __FILE__ ) . 'dist/error-reporting.js' ); + + wp_enqueue_script( + 'a8c-fse-error-reporting-script', + plugins_url( 'dist/error-reporting.js', __FILE__ ), + $script_dependencies, + $script_version, + true + ); +} + +/** + * Effectivelly activates the error reporting module by setting the necessary hooks. + */ +function activate_error_reporting() { + add_action( 'admin_print_scripts', __NAMESPACE__ . '\head_error_handler' ); + add_filter( 'script_loader_tag', __NAMESPACE__ . '\add_crossorigin_to_script_els', 99, 2 ); + // We load as last as possible for perf reasons. The head handler will + // capture errors until the main handler is loaded. + add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_script', 99 ); +} + +/** + * Returns whether or not the site loading ETK is in the WoA env. + * + * @return bool + */ +function is_atomic() { + return defined( 'IS_ATOMIC' ) && IS_ATOMIC; +} + +// We don't want to activate this module in AT just yet. See https://wp.me/p4TIVU-9DI#comment-10922. +// @todo Remove once we have a version that works for WPCOM simple sites and WoA. +if ( ! is_atomic() ) { + activate_error_reporting(); +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/edit.js b/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/edit.js index 24a3e25556d19..2682ab1bb8777 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/edit.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/edit.js @@ -50,12 +50,7 @@ const edit = ( { attributes, setAttributes, className } ) => { ( - ) } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/editor.scss b/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/editor.scss index 07ae705787d13..031d174e83f3f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/editor.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/event-countdown-block/blocks/src/editor.scss @@ -9,12 +9,13 @@ .event-countdown__event-title { min-width: 240px; + width: 100%; } } // Fix so datetime picker doesn't have horizontal scrollbar. // This is a global fix, unscoped, and not very nice. -// @todo: It won't be necessary once https://github.com/WordPress/gutenberg/pull/18235/files gets merged. +// @todo: It won't be necessary once https://github.com/WordPress/gutenberg/pull/18235/files gets merged. .components-datetime { padding: 8px; -} \ No newline at end of file +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/full-site-editing-plugin.php b/apps/editing-toolkit/editing-toolkit-plugin/full-site-editing-plugin.php index 719cfd7a9de29..9241e4c1a5b0f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/full-site-editing-plugin.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/full-site-editing-plugin.php @@ -33,13 +33,23 @@ * * Can be used in cache keys to invalidate caches on plugin update. * + * Note: this constant is updated via TeamCity continuous integration. That + * change is not copied back to VCS, so we use "dev" here to indicate that the + * version in wp-calypso is for development. + * + * On WordPress.com, the version here should show up in the "info" section of + * the "more options" menu in Gutenberg. + * * @var string */ -define( 'A8C_ETK_PLUGIN_VERSION', '2.21' ); +define( 'A8C_ETK_PLUGIN_VERSION', 'dev' ); // Always include these helper files for dotcom FSE. require_once __DIR__ . '/dotcom-fse/helpers.php'; +// Enqueues the shared JS data stores and defines shared helper functions. +require_once __DIR__ . '/common/index.php'; + /** * Load dotcom-FSE. */ @@ -117,7 +127,7 @@ function load_global_styles() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_global_styles' ); /** - * Load Event Countdown Block + * Load Event Countdown Block. */ function load_countdown_block() { require_once __DIR__ . '/event-countdown-block/index.php'; @@ -125,21 +135,13 @@ function load_countdown_block() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_countdown_block' ); /** - * Load Timeline Block + * Load Timeline Block. */ function load_timeline_block() { require_once __DIR__ . '/jetpack-timeline/index.php'; } add_action( 'plugins_loaded', __NAMESPACE__ . '\load_timeline_block' ); -/** - * Load common module. - */ -function load_common_module() { - require_once __DIR__ . '/common/index.php'; -} -add_action( 'plugins_loaded', __NAMESPACE__ . '\load_common_module' ); - /** * Load Editor Site Launch. */ @@ -230,7 +232,7 @@ function load_blog_posts_block() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_blog_posts_block' ); /** - * Load WPCOM Block Editor NUX + * Load WPCOM Block Editor NUX. */ function load_wpcom_block_editor_nux() { require_once __DIR__ . '/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php'; @@ -247,17 +249,27 @@ function load_block_patterns_from_api( $current_screen ) { return; } - if ( ! $current_screen->is_block_editor ) { + $is_site_editor = ( function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $current_screen->id ) ); + + if ( ! $current_screen->is_block_editor && ! $is_site_editor ) { return; } + $patterns_sources = array( 'block_patterns' ); + + // While we're still testing the FSE patterns, limit activation via a filter. + if ( $is_site_editor && apply_filters( 'a8c_enable_fse_block_patterns_api', false ) ) { + $patterns_sources[] = 'fse_block_patterns'; + } + require_once __DIR__ . '/block-patterns/class-block-patterns-from-api.php'; - Block_Patterns_From_API::get_instance(); + $block_patterns_from_api = new Block_Patterns_From_API( $patterns_sources ); + $block_patterns_from_api->register_patterns(); } add_action( 'current_screen', __NAMESPACE__ . '\load_block_patterns_from_api' ); /** - * Load WPCOM Block Patterns Modifications + * Load WPCOM Block Patterns Modifications. * * This is responsible for modifying how block patterns behave in the editor, * including adding support for premium block patterns. The patterns themselves @@ -274,7 +286,7 @@ function load_wpcom_block_patterns_modifications() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_wpcom_block_patterns_modifications' ); /** - * Load Block Inserter Modifications module + * Load Block Inserter Modifications module. */ function load_block_inserter_modifications() { require_once __DIR__ . '/block-inserter-modifications/index.php'; @@ -282,7 +294,7 @@ function load_block_inserter_modifications() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_block_inserter_modifications' ); /** - * Load Mailerlite module + * Load Mailerlite module. */ function load_mailerlite() { require_once __DIR__ . '/mailerlite/subscriber-popup.php'; @@ -290,7 +302,7 @@ function load_mailerlite() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_mailerlite' ); /** - * Load WPCOM block editor nav sidebar + * Load WPCOM block editor nav sidebar. */ function load_wpcom_block_editor_sidebar() { if ( @@ -303,7 +315,7 @@ function load_wpcom_block_editor_sidebar() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_wpcom_block_editor_sidebar' ); /** - * Coming soon + * Coming soon. */ function load_coming_soon() { if ( @@ -316,9 +328,17 @@ function load_coming_soon() { add_action( 'plugins_loaded', __NAMESPACE__ . '\load_coming_soon' ); /** - * What's New section of the Tools menu + * What's New section of the Tools menu. */ function load_whats_new() { require_once __DIR__ . '/whats-new/class-whats-new.php'; } add_action( 'plugins_loaded', __NAMESPACE__ . '\load_whats_new' ); + +/** + * Error reporting for wp-admin / Gutenberg + */ +function load_error_reporting() { + require_once __DIR__ . '/error-reporting/index.php'; +} +add_action( 'plugins_loaded', __NAMESPACE__ . '\load_error_reporting' ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/jetpack-timeline/blocks/src/style.scss b/apps/editing-toolkit/editing-toolkit-plugin/jetpack-timeline/blocks/src/style.scss index 0c398732f9a93..e581e4c813c1f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/jetpack-timeline/blocks/src/style.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/jetpack-timeline/blocks/src/style.scss @@ -12,7 +12,6 @@ $timeline-border-width: 4px; // This padding needs extra specificity. &.wp-block-jetpack-timeline { - margin: 0; padding: 0; } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/phpunit.xml.dist b/apps/editing-toolkit/editing-toolkit-plugin/phpunit.xml.dist index de3fac4af4c91..b5716046925d9 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/phpunit.xml.dist +++ b/apps/editing-toolkit/editing-toolkit-plugin/phpunit.xml.dist @@ -19,5 +19,8 @@ ./coming-soon/test/ + + ./block-patterns/test/ + diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-starter-page-templates.php b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-starter-page-templates.php index 83550f0b13a63..71a39db4179ee 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-starter-page-templates.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-starter-page-templates.php @@ -36,7 +36,7 @@ private function __construct() { 'starter_page_templates', A8C_ETK_PLUGIN_VERSION, get_option( 'site_vertical', 'default' ), - get_locale(), + $this->get_verticals_locale(), ) ); @@ -170,14 +170,12 @@ public function enqueue_assets() { $config = apply_filters( 'fse_starter_page_templates_config', array( - 'templates' => array_merge( $default_templates, $page_templates ), + 'templates' => array_merge( $default_templates, $page_templates ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended - 'screenAction' => isset( $_GET['new-homepage'] ) ? 'add' : $screen->action, - 'theme' => normalize_theme_slug( get_stylesheet() ), + 'screenAction' => isset( $_GET['new-homepage'] ) ? 'add' : $screen->action, + 'theme' => normalize_theme_slug( get_stylesheet() ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended - 'isFrontPage' => isset( $_GET['post'] ) && get_option( 'page_on_front' ) === $_GET['post'], - 'hideFrontPageTitle' => get_theme_mod( 'hide_front_page_title' ), - 'locale' => $this->get_verticals_locale(), + 'locale' => $this->get_verticals_locale(), ) ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php index 8f8aa6ba08346..a728a81e4539a 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/class-wp-rest-sideload-image-controller.php @@ -46,10 +46,11 @@ public function register_routes() { '/' . $this->rest_base . '/batch', array( array( - 'methods' => \WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_items' ), - 'show_in_index' => false, - 'args' => array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_items' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'show_in_index' => false, + 'args' => array( 'resources' => array( 'description' => 'URL to the image to be side-loaded.', 'type' => 'array', diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js deleted file mode 100644 index ae58bc72b747b..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * External dependencies - */ -import { registerPlugin } from '@wordpress/plugins'; -import { dispatch } from '@wordpress/data'; -import { initializeTracksWithIdentity } from '@automattic/page-template-modal'; - -/** - * Internal dependencies - */ -import { PageTemplatesPlugin } from './page-template-plugin'; -import './store'; -import './index.scss'; - -// Load config passed from backend. -const { - templates = [], - vertical, - segment, - tracksUserData, - screenAction, - theme, - isFrontPage, - locale, - hideFrontPageTitle, -} = window.starterPageTemplatesConfig; - -if ( tracksUserData ) { - initializeTracksWithIdentity( tracksUserData ); -} - -const templatesPluginSharedProps = { - segment, - templates, - theme, - vertical, - isFrontPage, - locale, - hidePageTitle: Boolean( isFrontPage && hideFrontPageTitle ), -}; - -// Open plugin only if we are creating new page. -if ( screenAction === 'add' ) { - dispatch( 'automattic/starter-page-layouts' ).setOpenState( 'OPEN_FROM_ADD_PAGE' ); -} - -// Always register ability to open from document sidebar. -registerPlugin( 'page-templates', { - render: () => { - return ( - <> - - - ); - }, -} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.scss b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.scss index dc4a004d92d52..55e455e8ddd10 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.scss @@ -1,11 +1,11 @@ -@import '~@automattic/page-template-modal/src/styles/page-template-modal'; +@import '@automattic/page-pattern-modal/src/styles/page-pattern-modal'; .sidebar-modal-opener { display: flex; flex-direction: column; align-items: center; justify-content: center; - .template-selector-item__label { + .pattern-selector-item__label { max-width: 300px; } } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.tsx new file mode 100644 index 0000000000000..0ebce928e3b59 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.tsx @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; +import { dispatch } from '@wordpress/data'; +import { initializeTracksWithIdentity, PatternDefinition } from '@automattic/page-pattern-modal'; +import React from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { PagePatternsPlugin } from './page-patterns-plugin'; +import './store'; +import './index.scss'; + +declare global { + interface Window { + starterPageTemplatesConfig?: { + templates?: PatternDefinition[]; + locale?: string; + theme?: string; + screenAction?: string; + tracksUserData?: Parameters< typeof initializeTracksWithIdentity >[ 0 ]; + }; + } +} + +// Load config passed from backend. +const { templates: patterns = [], tracksUserData, screenAction, theme, locale } = + window.starterPageTemplatesConfig ?? {}; + +if ( tracksUserData ) { + initializeTracksWithIdentity( tracksUserData ); +} + +// Open plugin only if we are creating new page. +if ( screenAction === 'add' ) { + dispatch( 'automattic/starter-page-layouts' ).setOpenState( 'OPEN_FROM_ADD_PAGE' ); +} + +// Always register ability to open from document sidebar. +registerPlugin( 'page-patterns', { + render: () => { + return ; + }, + + // `registerPlugin()` types assume `icon` is mandatory however it isn't + // actually required. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: undefined as any, +} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-patterns-plugin.tsx b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-patterns-plugin.tsx new file mode 100644 index 0000000000000..a1bce70d7366c --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-patterns-plugin.tsx @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import '@wordpress/nux'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { addFilter, removeFilter } from '@wordpress/hooks'; +import { PagePatternModal, PatternDefinition } from '@automattic/page-pattern-modal'; +import React, { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +const INSERTING_HOOK_NAME = 'isInsertingPagePattern'; +const INSERTING_HOOK_NAMESPACE = 'automattic/full-site-editing/inserting-pattern'; + +interface PagePatternsPluginProps { + patterns: PatternDefinition[]; + locale?: string; + theme?: string; +} + +export function PagePatternsPlugin( props: PagePatternsPluginProps ): JSX.Element { + const { setOpenState } = useDispatch( 'automattic/starter-page-layouts' ); + const { setUsedPageOrPatternsModal } = useDispatch( 'automattic/wpcom-welcome-guide' ); + const { replaceInnerBlocks } = useDispatch( 'core/block-editor' ); + const { editPost } = useDispatch( 'core/editor' ); + const { toggleFeature } = useDispatch( 'core/edit-post' ); + const { disableTips } = useDispatch( 'core/nux' ); + + const selectProps = useSelect( ( select ) => { + const { isOpen, isPatternPicker } = select( 'automattic/starter-page-layouts' ); + return { + isOpen: isOpen(), + isWelcomeGuideActive: select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ) as boolean, // Gutenberg 7.2.0 or higher + areTipsEnabled: select( 'core/nux' ) + ? ( select( 'core/nux' ).areTipsEnabled() as boolean ) + : false, // Gutenberg 7.1.0 or lower + ...( isPatternPicker() && { + title: __( 'Choose a Pattern', 'full-site-editing' ), + description: __( + 'Pick a pre-defined layout or continue with a blank page', + 'full-site-editing' + ), + } ), + }; + } ); + + const { getMeta, postContentBlock } = useSelect( ( select ) => { + const getMeta = () => select( 'core/editor' ).getEditedPostAttribute( 'meta' ); + const currentBlocks = select( 'core/editor' ).getBlocks(); + return { + getMeta, + postContentBlock: currentBlocks.find( ( block ) => block.name === 'a8c/post-content' ), + }; + } ); + + const savePatternChoice = useCallback( + ( name: string ) => { + // Save selected pattern slug in meta. + const currentMeta = getMeta(); + editPost( { + meta: { + ...currentMeta, + _starter_page_template: name, + }, + } ); + }, + [ editPost, getMeta ] + ); + + const insertPattern = useCallback( + ( title, blocks ) => { + // Add filter to let the tracking library know we are inserting a template. + addFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE, () => true ); + + // Set post title. + if ( title ) { + editPost( { title } ); + } + + // Replace blocks. + replaceInnerBlocks( postContentBlock ? postContentBlock.clientId : '', blocks, false ); + + // Remove filter. + removeFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE ); + }, + [ editPost, postContentBlock, replaceInnerBlocks ] + ); + + const { isWelcomeGuideActive, areTipsEnabled } = selectProps; + + const hideWelcomeGuide = useCallback( () => { + if ( isWelcomeGuideActive ) { + // Gutenberg 7.2.0 or higher. + toggleFeature( 'welcomeGuide' ); + } else if ( areTipsEnabled ) { + // Gutenberg 7.1.0 or lower. + disableTips(); + } + }, [ areTipsEnabled, disableTips, isWelcomeGuideActive, toggleFeature ] ); + + const handleClose = useCallback( () => { + setUsedPageOrPatternsModal(); + setOpenState( 'CLOSED' ); + }, [ setOpenState, setUsedPageOrPatternsModal ] ); + + return ( + + ); +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-plugin.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-plugin.js deleted file mode 100644 index 8e7d86114ec00..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-plugin.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * External dependencies - */ -import { stubTrue } from 'lodash'; -import '@wordpress/nux'; -import { compose } from '@wordpress/compose'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { addFilter, removeFilter } from '@wordpress/hooks'; -import { PageTemplateModal } from '@automattic/page-template-modal'; - -const INSERTING_HOOK_NAME = 'isInsertingPageTemplate'; -const INSERTING_HOOK_NAMESPACE = 'automattic/full-site-editing/inserting-template'; - -export const PageTemplatesPlugin = compose( - withSelect( ( select ) => { - const getMeta = () => select( 'core/editor' ).getEditedPostAttribute( 'meta' ); - const { _starter_page_template } = getMeta(); - const { isOpen } = select( 'automattic/starter-page-layouts' ); - const currentBlocks = select( 'core/editor' ).getBlocks(); - return { - isOpen: isOpen(), - getMeta, - _starter_page_template, - currentBlocks, - currentPostTitle: select( 'core/editor' ).getCurrentPost().title, - postContentBlock: currentBlocks.find( ( block ) => block.name === 'a8c/post-content' ), - isWelcomeGuideActive: select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ), // Gutenberg 7.2.0 or higher - areTipsEnabled: select( 'core/nux' ) ? select( 'core/nux' ).areTipsEnabled() : false, // Gutenberg 7.1.0 or lower - }; - } ), - withDispatch( ( dispatch, ownProps ) => { - const editorDispatcher = dispatch( 'core/editor' ); - const { setOpenState } = dispatch( 'automattic/starter-page-layouts' ); - return { - setOpenState, - saveTemplateChoice: ( name ) => { - // Save selected template slug in meta. - const currentMeta = ownProps.getMeta(); - editorDispatcher.editPost( { - meta: { - ...currentMeta, - _starter_page_template: name, - }, - } ); - }, - insertTemplate: ( title, blocks ) => { - // Add filter to let the tracking library know we are inserting a template. - addFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE, stubTrue ); - - // Set post title. - if ( title ) { - editorDispatcher.editPost( { title } ); - } - - // Replace blocks. - const postContentBlock = ownProps.postContentBlock; - dispatch( 'core/block-editor' ).replaceInnerBlocks( - postContentBlock ? postContentBlock.clientId : '', - blocks, - false - ); - - // Remove filter. - removeFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE ); - }, - hideWelcomeGuide: () => { - if ( ownProps.isWelcomeGuideActive ) { - // Gutenberg 7.2.0 or higher. - dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); - } else if ( ownProps.areTipsEnabled ) { - // Gutenberg 7.1.0 or lower. - dispatch( 'core/nux' ).disableTips(); - } - }, - }; - } ) -)( PageTemplateModal ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.js deleted file mode 100644 index d9770d7581c51..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * External dependencies - */ -import { registerStore } from '@wordpress/data'; - -const reducer = ( state = 'CLOSED', { type, ...action } ) => - 'SET_IS_OPEN' === type ? action.openState : state; - -const actions = { - setOpenState: ( openState ) => ( { - type: 'SET_IS_OPEN', - openState: openState || 'CLOSED', - } ), -}; - -const selectors = { - isOpen: ( state ) => 'CLOSED' !== state, -}; - -registerStore( 'automattic/starter-page-layouts', { - reducer, - actions, - selectors, -} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.ts b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.ts new file mode 100644 index 0000000000000..01004ef6b3356 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/store.ts @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import type { Reducer } from 'redux'; + +type OpenState = 'CLOSED' | 'OPEN_FROM_ADD_PAGE' | 'OPEN_FOR_BLANK_CANVAS'; +type Action = ReturnType< typeof actions.setOpenState >; + +const reducer: Reducer< OpenState, Action > = ( state = 'CLOSED', { type, ...action } ) => + 'SET_IS_OPEN' === type ? action.openState : state; + +const actions = { + setOpenState: ( openState: OpenState | false ) => ( { + type: 'SET_IS_OPEN' as const, + openState: openState || 'CLOSED', + } ), +}; + +const selectors = { + isOpen: ( state: OpenState ): boolean => 'CLOSED' !== state, + isPatternPicker: ( state: OpenState ): boolean => 'OPEN_FOR_BLANK_CANVAS' === state, +}; + +const STORE_KEY = 'automattic/starter-page-layouts'; + +registerStore( STORE_KEY, { + // In reality the store can dispatch any action, however `reducer` has a + // strongly typed action type to make the typings inside the function + // easier to work with. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reducer: reducer as any, + actions, + selectors, +} ); + +declare module '@wordpress/data' { + function dispatch( key: 'automattic/starter-page-layouts' ): typeof actions; + function select( + key: 'automattic/starter-page-layouts' + ): { + isOpen: () => ReturnType< typeof selectors.isOpen >; + isPatternPicker: () => ReturnType< typeof selectors.isPatternPicker >; + }; +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/style.scss b/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/style.scss deleted file mode 100644 index 9f8f75ae7332d..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/style.scss +++ /dev/null @@ -1,193 +0,0 @@ -// @TODO: remove the ignore rule and replace font sizes accordingly -/* stylelint-disable scales/font-size */ - -@import '~@automattic/typography/styles/fonts'; -@import '~@automattic/onboarding/styles/mixins'; - -$wpcom-modal-breakpoint: 660px; - -$wpcom-modal-padding-v: 30px; -$wpcom-modal-padding-h: 25px; -$wpcom-modal-content-min-height: 350px; -$wpcom-modal-footer-padding-v: 20px; -$wpcom-modal-footer-height: 30px + ( $wpcom-modal-footer-padding-v * 2 ); - -// Core modal style overrides -.whats-new { - &.components-modal__frame { - overflow: visible; - height: 65vh; - top: calc( 17.5vh - #{$wpcom-modal-footer-height / 2} ); - - @media ( max-width: $wpcom-modal-breakpoint ) { - width: 90vw; - min-width: 90vw; - left: 5vw; - right: 5vw; - } - - @media ( min-width: $wpcom-modal-breakpoint ) { - width: 720px; - height: $wpcom-modal-content-min-height; - top: calc( 50% - #{$wpcom-modal-footer-height / 2} ); - } - } - - .components-modal__header { - display: none; - } - - .components-guide__container { - margin-top: 0; - } - - .components-guide__footer { - position: absolute; - width: 100%; - height: $wpcom-modal-footer-height; - bottom: $wpcom-modal-footer-height * -1; - left: 0; - padding: $wpcom-modal-footer-padding-v 0; - margin: 0; - display: flex; - justify-content: center; - background: white; - border-top: 1px solid #dcdcde; - - @media ( min-width: $wpcom-modal-breakpoint ) { - border-top: none; - } - } - - .components-guide__page { - position: absolute; - width: 100%; - height: 100%; - justify-content: start; - } - - .components-guide__page-control { - position: relative; - height: 0; - top: 100%; - overflow: visible; - margin: 0 auto; - z-index: 2; - - &::before { - display: inline-block; - content: ''; - height: $wpcom-modal-footer-height; - vertical-align: middle; - } - - li { - vertical-align: middle; - margin-bottom: 0; - } - - // Temporarily disable dots on mobile as alignment is wonky. - display: none; - @media ( min-width: $wpcom-modal-breakpoint ) { - display: block; - } - } -} - -.whats-new__page { - display: flex; - flex-direction: column-reverse; - justify-content: flex-end; - background: white; - width: 100%; - height: 100%; - - @media ( min-width: $wpcom-modal-breakpoint ) { - flex-direction: row; - justify-content: flex-start; - position: absolute; - min-height: $wpcom-modal-content-min-height; - bottom: 0; - } -} - -.whats-new__text, -.whats-new__visual { - @media ( min-width: $wpcom-modal-breakpoint ) { - flex: 1 auto; - min-width: 290px; - } -} - -.whats-new__text { - padding: 0 25px 25px; - height: 60%; - - @media ( min-width: $wpcom-modal-breakpoint ) { - height: auto; - padding: $wpcom-modal-padding-v $wpcom-modal-padding-h; - } -} -.whats-new__visual { - height: 40%; - padding: 25px; - background: #1581d8; - text-align: center; - - @media ( min-width: $wpcom-modal-breakpoint ) { - height: auto; - } -} - -.whats-new__heading { - @include onboarding-font-recoleta; - /* Gray / Gray 90 */ - color: #1d2327; - - font-size: 24px; - line-height: 1.19; - - @media ( min-width: $wpcom-modal-breakpoint ) { - font-size: 24px; - } - - // TODO: remove this hack once the welcome editor deals better with - // overflowing text - body.locale-de & { - font-size: 24px; - - @media ( min-width: $wpcom-modal-breakpoint ) { - font-size: 28px; - } - } -} - -.whats-new__description p { - font-size: 15px; - line-height: 22px; - - /* Gray / Gray 60 */ - color: #50575e; - - @media ( min-width: $wpcom-modal-breakpoint ) { - font-size: 17px; - line-height: 26px; - } -} - -.whats-new__image { - max-width: 100%; - height: auto; - flex: 1; - align-self: center; - - &.align-bottom { - align-self: flex-end; - } - - max-height: 100%; - - @media ( min-width: $wpcom-modal-breakpoint ) { - max-height: none; - } -} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/whats-new.js b/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/whats-new.js index 2159ed5c0d0c1..4c84cbd477f36 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/whats-new.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/whats-new/src/whats-new.js @@ -4,162 +4,36 @@ import './public-path'; /** * External dependencies */ -import { registerPlugin } from '@wordpress/plugins'; -import { Fill, Guide, GuidePage, MenuItem } from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { recordTracksEvent } from '@automattic/calypso-analytics'; -import { useEffect, createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import './style.scss'; -const blockPatternsImage = 'https://s0.wp.com/i/whats-new/block-patterns.png'; -const dragDropImage = 'https://s0.wp.com/i/whats-new/drag-drop.png'; -const singlePageSiteImage = 'https://s0.wp.com/i/whats-new/single-page-website.png'; +import { Fill, MenuItem } from '@wordpress/components'; +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { registerPlugin } from '@wordpress/plugins'; +import { useEffect } from '@wordpress/element'; +import { useState } from 'react'; +import WhatsNewGuide from '@automattic/whats-new'; function WhatsNewMenuItem() { - const { toggleWhatsNew } = useDispatch( 'automattic/whats-new' ); - const isActive = useSelect( ( select ) => select( 'automattic/whats-new' ).isWhatsNewActive() ); - const whatsNewPages = getWhatsNewPages(); + const [ showGuide, setShowGuide ] = useState( false ); + const openWhatsNew = () => setShowGuide( true ); + const closeWhatsNew = () => setShowGuide( false ); // Record Tracks event if user opens What's New useEffect( () => { - if ( isActive ) { + if ( showGuide ) { recordTracksEvent( 'calypso_block_editor_whats_new_open' ); } - }, [ isActive ] ); + }, [ showGuide ] ); return ( <> - { __( "What's new", 'full-site-editing' ) } + { __( "What's new", 'full-site-editing' ) } - { isActive && ( - - { whatsNewPages.map( ( page, index ) => ( - - ) ) } - - ) } + { showGuide && } ); } -function getWhatsNewPages() { - return [ - { - imgSrc: blockPatternsImage, - heading: __( 'New block patterns', 'full-site-editing' ), - description: createInterpolateElement( - /* translators: the embed is a link */ - __( - '

Choose from hundreds of pre-made patterns for buttons, headers, galleries, and more available via the + button at the top of all pages.

Learn more

', - 'full-site-editing' - ), - { - Link: ( - - ), - p:

, - } - ), - }, - { - imgSrc: dragDropImage, - heading: __( 'Drag and drop blocks and patterns in the editor', 'full-site-editing' ), - description: createInterpolateElement( - /* translators: the embed is a link */ - __( - '

You can now drag and drop Blocks, and even Block Patterns, into your content directly from the Block Inserter.

Learn more

', - 'full-site-editing' - ), - { - Link: ( -
- ), - p:

, - } - ), - }, - { - imgSrc: singlePageSiteImage, - heading: __( 'Quickly build single-page websites', 'full-site-editing' ), - description: createInterpolateElement( - /* translators: the embed is a link */ - __( - '

Introducing our freshly-launched Blank Canvas theme, which is optimized for single-page websites.

Learn more

', - 'full-site-editing' - ), - { - Link: ( -
- ), - p:

, - } - ), - }, - ]; -} - -function WhatsNewPage( { - pageNumber, - isLastPage, - alignBottom = false, - heading, - description, - imgSrc, -} ) { - useEffect( () => { - recordTracksEvent( 'calypso_block_editor_whats_new_slide_view', { - slide_number: pageNumber, - is_last_slide: isLastPage, - } ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); - return ( - -

-

{ heading }

-
{ description }
-
-
- -
- - ); -} - export default WhatsNewMenuItem; registerPlugin( 'whats-new', { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/attach-sidebar.tsx b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/attach-sidebar.tsx index c8119914e3180..bd3d30b33b83a 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/attach-sidebar.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/attach-sidebar.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import React from 'react'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect, createPortal, useState } from '@wordpress/element'; import { __experimentalMainDashboardButton as MainDashboardButton } from '@wordpress/edit-post'; @@ -13,7 +14,7 @@ import WpcomBlockEditorNavSidebar from './components/nav-sidebar'; import ToggleSidebarButton from './components/toggle-sidebar-button'; const registerPlugin = ( name: string, settings: Omit< PluginSettings, 'icon' > ) => - originalRegisterPlugin( name, settings as any ); + originalRegisterPlugin( name, settings as PluginSettings ); if ( typeof MainDashboardButton !== 'undefined' ) { registerPlugin( 'a8c-full-site-editing-nav-sidebar', { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/create-page/index.tsx b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/create-page/index.tsx index 1a12933e18049..0d93fcef142f4 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/create-page/index.tsx +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/create-page/index.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import React from 'react'; import { get } from 'lodash'; import { Button as OriginalButton } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; @@ -17,7 +18,7 @@ import './style.scss'; const Button = ( { children, ...rest -}: OriginalButton.Props & { icon?: any; iconSize?: number } ) => ( +}: OriginalButton.Props & { icon?: unknown; iconSize?: number } ) => ( { children } ); @@ -25,7 +26,7 @@ interface Props { postType: { slug: string }; } -export default function CreatePage( { postType }: Props ) { +export default function CreatePage( { postType }: Props ): JSX.Element { const defaultLabel = get( postType, [ 'labels', 'add_new_item' ], @@ -35,14 +36,14 @@ export default function CreatePage( { postType }: Props ) { 'a8c.WpcomBlockEditorNavSidebar.createPostLabel', defaultLabel, postType.slug - ); + ) as string; const defaultUrl = addQueryArgs( 'post-new.php', { post_type: postType.slug } ); const url = applyFilters( 'a8c.WpcomBlockEditorNavSidebar.createPostUrl', defaultUrl, postType.slug - ); + ) as string; const trackEvent = () => { recordTracksEvent( `calypso_editor_sidebar_item_add`, { post_type: postType.slug } ); @@ -50,7 +51,9 @@ export default function CreatePage( { postType }: Props ) { return (
-

{ decodeEntities( siteTitle ) }

+

{ siteTitle ? decodeEntities( siteTitle ) : '' }

); } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/toggle-sidebar-button/style.scss b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/toggle-sidebar-button/style.scss index 6cf4c147c725a..e7bf34d33784f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/toggle-sidebar-button/style.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nav-sidebar/src/components/toggle-sidebar-button/style.scss @@ -1,7 +1,12 @@ -@import '~@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/mixins'; +@import '../nav-sidebar/style.scss'; .wpcom-block-editor-nav-sidebar-toggle-sidebar-button__button { - height: $header-height !important; + // Match this animation with the nav-sidebar, so that the button is not hidden + // before the sidebar menu finishes sliding out. + transition: visibility $sidebar-transition-period linear; + @include reduce-motion( 'transition' ); &.is-hidden { visibility: hidden; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php index 06e94dd884f54..9bef8de3c683f 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wp-rest-wpcom-block-editor-nux-status-controller.php @@ -11,6 +11,11 @@ * Class WP_REST_WPCOM_Block_Editor_NUX_Status_Controller. */ class WP_REST_WPCOM_Block_Editor_NUX_Status_Controller extends \WP_REST_Controller { + /** + * Use 30 minutes in case the user isn't taken to the editor immediately. See pbxlJb-Ly-p2#comment-1028. + */ + const NEW_SITE_AGE_SECONDS = 30 * 60; + /** * WP_REST_WPCOM_Block_Editor_NUX_Status_Controller constructor. */ @@ -51,39 +56,57 @@ public function permission_callback() { } /** - * Check if NUX is enabled. + * Should we show the wpcom welcome guide (i.e. welcome tour or nux modal) * * @param mixed $nux_status Can be "enabled", "dismissed", or undefined. * @return boolean */ - public function is_nux_enabled( $nux_status ) { - if ( defined( 'SHOW_WELCOME_TOUR' ) && SHOW_WELCOME_TOUR ) { - return true; - } + public function show_wpcom_welcome_guide( $nux_status ) { return 'enabled' === $nux_status; } /** * Return the WPCOM NUX status * + * This is only called for sites where the user hasn't already dismissed the tour. + * Once the tour has been dismissed, the closed state is saved in local storage (for the current site) + * see src/block-editor-nux.js + * * @return WP_REST_Response */ public function get_nux_status() { - // Check if we want to show the Welcome Tour variant pbxNRc-Cb-p2 - // Performing the check here means that we'll only assign a user to an experiment when this API is first called. - $welcome_tour_show_variant = ( defined( 'SHOW_WELCOME_TOUR' ) && SHOW_WELCOME_TOUR ) || apply_filters( 'a8c_enable_wpcom_welcome_tour', false ); - if ( has_filter( 'wpcom_block_editor_nux_get_status' ) ) { + $should_open_patterns_panel = (bool) get_option( 'was_created_with_blank_canvas_design' ); + + if ( wp_is_mobile() ) { + // Designs for welcome tour on mobile are in progress, until then do not show on mobile. + $variant = 'modal'; + } elseif ( $should_open_patterns_panel ) { + $variant = 'blank-canvas-tour'; + } else { + $variant = 'tour'; + } + + if ( function_exists( 'get_blog_details' ) ) { + $blog_age = time() - strtotime( get_blog_details()->registered ); + } + + if ( isset( $blog_age ) && $blog_age < self::NEW_SITE_AGE_SECONDS ) { + $nux_status = 'enabled'; + } elseif ( has_filter( 'wpcom_block_editor_nux_get_status' ) ) { $nux_status = apply_filters( 'wpcom_block_editor_nux_get_status', false ); } elseif ( ! metadata_exists( 'user', get_current_user_id(), 'wpcom_block_editor_nux_status' ) ) { $nux_status = 'enabled'; } else { $nux_status = get_user_meta( get_current_user_id(), 'wpcom_block_editor_nux_status', true ); } + + $show_welcome_guide = $this->show_wpcom_welcome_guide( $nux_status ); + return rest_ensure_response( array( - 'is_nux_enabled' => $this->is_nux_enabled( $nux_status ), - 'welcome_tour_show_variant' => $welcome_tour_show_variant, + 'show_welcome_guide' => $show_welcome_guide, + 'variant' => $variant, ) ); } @@ -96,11 +119,11 @@ public function get_nux_status() { */ public function update_nux_status( $request ) { $params = $request->get_json_params(); - $nux_status = $params['isNuxEnabled'] ? 'enabled' : 'dismissed'; + $nux_status = $params['show_welcome_guide'] ? 'enabled' : 'dismissed'; if ( has_action( 'wpcom_block_editor_nux_update_status' ) ) { do_action( 'wpcom_block_editor_nux_update_status', $nux_status ); } update_user_meta( get_current_user_id(), 'wpcom_block_editor_nux_status', $nux_status ); - return rest_ensure_response( array( 'is_nux_enabled' => $this->is_nux_enabled( $nux_status ) ) ); + return rest_ensure_response( array( 'show_welcome_guide' => $this->show_wpcom_welcome_guide( $nux_status ) ) ); } } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php index 90fefa2f39926..45d442c89fed2 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php @@ -59,6 +59,11 @@ public function enqueue_script_and_style() { 'wpcomBlockEditorNuxAssetsUrl', plugins_url( 'dist/', __FILE__ ) ); + wp_localize_script( + 'wpcom-block-editor-nux-script', + 'wpcomBlockEditorNuxLocale', + \A8C\FSE\Common\get_iso_639_locale( determine_locale() ) + ); wp_set_script_translations( 'wpcom-block-editor-nux-script', 'full-site-editing' ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/index.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/index.js index 9e80875d29cfa..15802458b3ef9 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/index.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/index.js @@ -1,6 +1,8 @@ /** * Internal dependencies */ -import './src/store'; +import { register } from './src/store'; import './src/disable-core-nux'; import './src/block-editor-nux'; + +register(); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/block-editor-nux.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/block-editor-nux.js index b4e746d422700..1665edd499c84 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/block-editor-nux.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/block-editor-nux.js @@ -9,53 +9,64 @@ import { Guide, GuidePage } from '@wordpress/components'; import { registerPlugin } from '@wordpress/plugins'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; +import { LocaleProvider, i18nDefaultLocaleSlug } from '@automattic/i18n-utils'; /** * Internal dependencies */ -import WpcomNux from './welcome-modal/wpcom-nux'; import LaunchWpcomWelcomeTour from './welcome-tour/tour-launch'; +import WpcomNux from './welcome-modal/wpcom-nux'; +import { DEFAULT_VARIANT, BLANK_CANVAS_VARIANT } from './store'; registerPlugin( 'wpcom-block-editor-nux', { render: function WpcomBlockEditorNux() { - const { site, isWpcomNuxEnabled, showWpcomNuxVariant, isSPTOpen } = useSelect( ( select ) => ( { - site: select( 'automattic/site' ).getSite( window._currentSiteId ), - isWpcomNuxEnabled: select( 'automattic/nux' ).isWpcomNuxEnabled(), - showWpcomNuxVariant: select( 'automattic/nux' ).shouldShowWpcomNuxVariant(), - isSPTOpen: - select( 'automattic/starter-page-layouts' ) && // Handle the case where SPT is not initalized. - select( 'automattic/starter-page-layouts' ).isOpen(), - } ) ); + const { show, isLoaded, variant, isManuallyOpened, isNewPageLayoutModalOpen } = useSelect( + ( select ) => { + const welcomeGuideStoreSelect = select( 'automattic/wpcom-welcome-guide' ); + const starterPageLayoutsStoreSelect = select( 'automattic/starter-page-layouts' ); + return { + show: welcomeGuideStoreSelect.isWelcomeGuideShown(), + isLoaded: welcomeGuideStoreSelect.isWelcomeGuideStatusLoaded(), + variant: welcomeGuideStoreSelect.getWelcomeGuideVariant(), + isManuallyOpened: welcomeGuideStoreSelect.isWelcomeGuideManuallyOpened(), + isNewPageLayoutModalOpen: starterPageLayoutsStoreSelect?.isOpen(), // Handle the case where SPT is not initalized. + }; + }, + [] + ); - const { setWpcomNuxStatus, setShowWpcomNuxVariant } = useDispatch( 'automattic/nux' ); + const setOpenState = useDispatch( 'automattic/starter-page-layouts' )?.setOpenState; - // On mount check if the WPCOM NUX status exists in state, otherwise fetch it from the API. + const { fetchWelcomeGuideStatus } = useDispatch( 'automattic/wpcom-welcome-guide' ); + + // On mount check if the WPCOM welcome guide status exists in state (from local storage), otherwise fetch it from the API. useEffect( () => { - if ( typeof isWpcomNuxEnabled !== 'undefined' ) { - return; + if ( ! isLoaded ) { + fetchWelcomeGuideStatus(); } + }, [ fetchWelcomeGuideStatus, isLoaded ] ); - const fetchWpcomNuxStatus = async () => { - const response = await apiFetch( { path: '/wpcom/v2/block-editor/nux' } ); - setWpcomNuxStatus( { isNuxEnabled: response.is_nux_enabled, bypassApi: true } ); - setShowWpcomNuxVariant( { showVariant: response.welcome_tour_show_variant } ); - }; - - fetchWpcomNuxStatus(); - }, [ isWpcomNuxEnabled, setWpcomNuxStatus, setShowWpcomNuxVariant ] ); - - if ( ! isWpcomNuxEnabled || isSPTOpen ) { + if ( ! show || isNewPageLayoutModalOpen ) { return null; } - const isPodcastingSite = !! site?.options?.anchor_podcast; + // Open patterns panel before Welcome Tour if necessary (e.g. when using Blank Canvas theme) + // Do this only when Welcome Tour is not manually opened. + // NOTE: at the moment, 'starter-page-templates' assets are not loaded on /site-editor/ page so 'setOpenState' may be undefined + if ( variant === BLANK_CANVAS_VARIANT && ! isManuallyOpened && setOpenState ) { + setOpenState( 'OPEN_FOR_BLANK_CANVAS' ); + return null; + } - if ( showWpcomNuxVariant && ! isPodcastingSite ) { - return ; + if ( variant === DEFAULT_VARIANT ) { + return ( + + ; + + ); } - if ( Guide && GuidePage ) { + if ( variant === 'modal' && Guide && GuidePage ) { return ; } diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/disable-core-nux.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/disable-core-nux.js index ea8bf1520e7ad..78b126877f0ba 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/disable-core-nux.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/disable-core-nux.js @@ -13,16 +13,17 @@ const unsubscribe = subscribe( () => { unsubscribe(); } ); -// Listen for these features being triggered to call dotcom nux instead. +// Listen for these features being triggered to call dotcom welcome guide instead. // Note migration of areTipsEnabled: https://github.com/WordPress/gutenberg/blob/5c3a32dabe4393c45f7fe6ac5e4d78aebd5ee274/packages/data/src/plugins/persistence/index.js#L269 subscribe( () => { if ( select( 'core/nux' ).areTipsEnabled() ) { dispatch( 'core/nux' ).disableTips(); - dispatch( 'automattic/nux' ).setWpcomNuxStatus( { isNuxEnabled: true } ); + dispatch( 'automattic/wpcom-welcome-guide' ).setShowWelcomeGuide( true ); } if ( select( 'core/edit-post' )?.isFeatureActive( 'welcomeGuide' ) ) { dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); - dispatch( 'automattic/nux' ).setWpcomNuxStatus( { isNuxEnabled: true } ); - dispatch( 'automattic/nux' ).setTourOpenStatus( { isTourManuallyOpened: true } ); + dispatch( 'automattic/wpcom-welcome-guide' ).setShowWelcomeGuide( true, { + openedManually: true, + } ); } } ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/store.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/store.js index 5b4bd55303bb1..0c3e2013aaa33 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/store.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/store.js @@ -3,20 +3,36 @@ */ import 'a8c-fse-common-data-stores'; import apiFetch from '@wordpress/api-fetch'; +import { apiFetch as apiFetchControls, controls } from '@wordpress/data-controls'; import { combineReducers, registerStore } from '@wordpress/data'; -const isNuxEnabledReducer = ( state = undefined, action ) => { +export const DEFAULT_VARIANT = 'tour'; +export const BLANK_CANVAS_VARIANT = 'blank-canvas-tour'; + +const showWelcomeGuideReducer = ( state = undefined, action ) => { switch ( action.type ) { - case 'WPCOM_BLOCK_EDITOR_NUX_SET_STATUS': - return action.isNuxEnabled; + case 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS': + return action.response.show_welcome_guide; + case 'WPCOM_WELCOME_GUIDE_SHOW_SET': + return action.show; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return undefined; default: return state; } }; -const isTourManuallyOpenedReducer = ( state = false, action ) => { + +const welcomeGuideManuallyOpenedReducer = ( state = false, action ) => { switch ( action.type ) { - case 'WPCOM_BLOCK_EDITOR_SET_TOUR_OPEN': - return action.isTourManuallyOpened; + case 'WPCOM_WELCOME_GUIDE_SHOW_SET': + if ( typeof action.openedManually !== 'undefined' ) { + return action.openedManually; + } + return state; + + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return false; + default: return state; } @@ -25,72 +41,84 @@ const isTourManuallyOpenedReducer = ( state = false, action ) => { // TODO: next PR convert file to Typescript to ensure control of tourRating values: null, 'thumbs-up' 'thumbs-down' const tourRatingReducer = ( state = undefined, action ) => { switch ( action.type ) { - case 'WPCOM_BLOCK_EDITOR_SET_TOUR_RATING': + case 'WPCOM_WELCOME_GUIDE_TOUR_RATING_SET': return action.tourRating; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return undefined; default: return state; } }; -const showWpcomNuxVariantReducer = ( state = false, action ) => { +const welcomeGuideVariantReducer = ( state = DEFAULT_VARIANT, action ) => { switch ( action.type ) { - case 'WPCOM_BLOCK_EDITOR_SET_NUX_VARIANT': - return action.showVariant; + case 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS': + return action.response.variant; + case 'WPCOM_HAS_USED_PATTERNS_MODAL': + return state === BLANK_CANVAS_VARIANT ? DEFAULT_VARIANT : state; + case 'WPCOM_WELCOME_GUIDE_RESET_STORE': + return DEFAULT_VARIANT; default: return state; } }; const reducer = combineReducers( { - isNuxEnabled: isNuxEnabledReducer, - isTourManuallyOpened: isTourManuallyOpenedReducer, + welcomeGuideManuallyOpened: welcomeGuideManuallyOpenedReducer, + showWelcomeGuide: showWelcomeGuideReducer, tourRating: tourRatingReducer, - showWpcomNuxVariant: showWpcomNuxVariantReducer, + welcomeGuideVariant: welcomeGuideVariantReducer, } ); const actions = { - // TODO: Clarify variable naming of nux vs tour for consistency and to better reflect terminology in core - // isFeatureActive instead of isNuxEnabled would match core nad make this logic easier to understand. - setWpcomNuxStatus: ( { isNuxEnabled, bypassApi } ) => { - if ( ! bypassApi ) { - apiFetch( { - path: '/wpcom/v2/block-editor/nux', - method: 'POST', - data: { isNuxEnabled }, - } ); - } + *fetchWelcomeGuideStatus() { + const response = yield apiFetchControls( { path: '/wpcom/v2/block-editor/nux' } ); + return { - type: 'WPCOM_BLOCK_EDITOR_NUX_SET_STATUS', - isNuxEnabled, + type: 'WPCOM_WELCOME_GUIDE_FETCH_STATUS_SUCCESS', + response, }; }, - setTourRating: ( tourRating ) => { - return { type: 'WPCOM_BLOCK_EDITOR_SET_TOUR_RATING', tourRating }; - }, - setShowWpcomNuxVariant: ( { showVariant } ) => { + setShowWelcomeGuide: ( show, { openedManually } = {} ) => { + apiFetch( { + path: '/wpcom/v2/block-editor/nux', + method: 'POST', + data: { show_welcome_guide: show }, + } ); + return { - type: 'WPCOM_BLOCK_EDITOR_SET_NUX_VARIANT', - showVariant, + type: 'WPCOM_WELCOME_GUIDE_SHOW_SET', + show, + openedManually, }; }, - setTourOpenStatus: ( { isTourManuallyOpened } ) => { - return { - type: 'WPCOM_BLOCK_EDITOR_SET_TOUR_OPEN', - isTourManuallyOpened, - }; + setTourRating: ( tourRating ) => { + return { type: 'WPCOM_WELCOME_GUIDE_TOUR_RATING_SET', tourRating }; + }, + setUsedPageOrPatternsModal: () => { + return { type: 'WPCOM_HAS_USED_PATTERNS_MODAL' }; }, + // The `resetStore` action is only used for testing to reset the + // store inbetween tests. + resetStore: () => ( { + type: 'WPCOM_WELCOME_GUIDE_RESET_STORE', + } ), }; const selectors = { - isTourManuallyOpened: ( state ) => state.isTourManuallyOpened, - isWpcomNuxEnabled: ( state ) => state.isNuxEnabled, - tourRating: ( state ) => state.tourRating, - shouldShowWpcomNuxVariant: ( state ) => state.showWpcomNuxVariant, + isWelcomeGuideManuallyOpened: ( state ) => state.welcomeGuideManuallyOpened, + isWelcomeGuideShown: ( state ) => !! state.showWelcomeGuide, + isWelcomeGuideStatusLoaded: ( state ) => typeof state.showWelcomeGuide !== 'undefined', + getTourRating: ( state ) => state.tourRating, + getWelcomeGuideVariant: ( state ) => state.welcomeGuideVariant, }; -registerStore( 'automattic/nux', { - reducer, - actions, - selectors, - persist: true, -} ); +export function register() { + return registerStore( 'automattic/wpcom-welcome-guide', { + reducer, + actions, + selectors, + controls, + persist: true, + } ); +} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/test/store.test.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/test/store.test.js new file mode 100644 index 0000000000000..0aff31ddd5279 --- /dev/null +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/test/store.test.js @@ -0,0 +1,133 @@ +/** + * External dependencies + */ +import waitForExpect from 'wait-for-expect'; + +/** + * Internal dependencies + */ +import { dispatch, select } from '@wordpress/data'; +import { register, DEFAULT_VARIANT } from '../store'; + +const STORE_KEY = 'automattic/wpcom-welcome-guide'; + +beforeAll( () => { + register(); +} ); + +let originalFetch; +beforeEach( () => { + dispatch( STORE_KEY ).resetStore(); + originalFetch = window.fetch; + window.fetch = jest.fn(); +} ); + +afterEach( () => { + window.fetch = originalFetch; +} ); + +test( 'resetting the store', async () => { + window.fetch.mockResolvedValue( { + status: 200, + json: () => Promise.resolve( { show_welcome_guide: true, variant: 'modal' } ), + } ); + + dispatch( STORE_KEY ).fetchWelcomeGuideStatus(); + await waitForExpect( () => + expect( select( STORE_KEY ).isWelcomeGuideStatusLoaded() ).toBe( true ) + ); + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + dispatch( STORE_KEY ).setTourRating( 'thumbs-up' ); + + dispatch( STORE_KEY ).resetStore(); + + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( false ); + expect( select( STORE_KEY ).isWelcomeGuideStatusLoaded() ).toBe( false ); + expect( select( STORE_KEY ).getTourRating() ).toBeUndefined(); + expect( select( STORE_KEY ).getWelcomeGuideVariant() ).toBe( DEFAULT_VARIANT ); +} ); + +test( "by default the store isn't loaded", () => { + const isLoaded = select( STORE_KEY ).isWelcomeGuideStatusLoaded(); + expect( isLoaded ).toBe( false ); +} ); + +test( 'after fetching the guide status the store is loaded', async () => { + window.fetch.mockResolvedValue( { + status: 200, + json: () => Promise.resolve( { show_welcome_guide: true, variant: DEFAULT_VARIANT } ), + } ); + + dispatch( STORE_KEY ).fetchWelcomeGuideStatus(); + + await waitForExpect( () => { + const isLoaded = select( STORE_KEY ).isWelcomeGuideStatusLoaded(); + expect( isLoaded ).toBe( true ); + } ); + + expect( window.fetch ).toHaveBeenCalledWith( + '/wpcom/v2/block-editor/nux?_locale=user', + expect.anything() + ); + + // Check the store is loaded with the state that came from the server + const isWelcomeGuideShown = select( STORE_KEY ).isWelcomeGuideShown(); + expect( isWelcomeGuideShown ).toBe( true ); + const welcomeGuideVariant = select( STORE_KEY ).getWelcomeGuideVariant(); + expect( welcomeGuideVariant ).toBe( DEFAULT_VARIANT ); +} ); + +test( 'toggle welcome guide visibility', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( false ); + expect( select( STORE_KEY ).isWelcomeGuideShown() ).toBe( false ); +} ); + +test( 'guide manually opened flag is false by default', () => { + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); +} ); + +test( '"manually opened" flag can be set when opening welcome guide', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: false } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); +} ); + +test( 'leaving `openedManually` unspecified leaves the flag unchanged', () => { + // setShowWelcomeGuide kicks off a save. This mock fixes unresolved promise + // rejection errors that appear in CLI output + window.fetch.mockResolvedValue( { status: 200, json: () => Promise.resolve( {} ) } ); + + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( false ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( true, { openedManually: true } ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); + + dispatch( STORE_KEY ).setShowWelcomeGuide( false ); + expect( select( STORE_KEY ).isWelcomeGuideManuallyOpened() ).toBe( true ); +} ); + +test( 'tour rating is "undefined" by default', () => { + expect( select( STORE_KEY ).getTourRating() ).toBeUndefined(); +} ); + +test( 'tour rating can be set to "thumbs-up" or "thumbs-down"', () => { + dispatch( STORE_KEY ).setTourRating( 'thumbs-up' ); + expect( select( STORE_KEY ).getTourRating() ).toBe( 'thumbs-up' ); + + dispatch( STORE_KEY ).setTourRating( 'thumbs-down' ); + expect( select( STORE_KEY ).getTourRating() ).toBe( 'thumbs-down' ); +} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/images/block.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/images/block.svg deleted file mode 100644 index ca4a8893255de..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/images/block.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/style.scss b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/style.scss index e075964f15403..ee67f41c02a97 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/style.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/style.scss @@ -1,8 +1,8 @@ // @TODO: remove the ignore rule and replace font sizes accordingly /* stylelint-disable scales/font-size */ -@import '~@automattic/typography/styles/fonts'; -@import '~@automattic/onboarding/styles/mixins'; +@import '@automattic/typography/styles/fonts'; +@import '@automattic/onboarding/styles/mixins'; $wpcom-modal-breakpoint: 660px; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js index 1bd1bfe7aa69a..6a568d80941f9 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-modal/wpcom-nux.js @@ -18,33 +18,33 @@ import previewImage from './images/preview.svg'; import privateImage from './images/private.svg'; function WpcomNux() { - const { isWpcomNuxEnabled, isSPTOpen, isTourManuallyOpened } = useSelect( ( select ) => ( { - isWpcomNuxEnabled: select( 'automattic/nux' ).isWpcomNuxEnabled(), - isSPTOpen: + const { show, isNewPageLayoutModalOpen, isManuallyOpened } = useSelect( ( select ) => ( { + show: select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideShown(), + isNewPageLayoutModalOpen: select( 'automattic/starter-page-layouts' ) && // Handle the case where SPT is not initalized. select( 'automattic/starter-page-layouts' ).isOpen(), - isTourManuallyOpened: select( 'automattic/nux' ).isTourManuallyOpened(), + isManuallyOpened: select( 'automattic/wpcom-welcome-guide' ).isWelcomeGuideManuallyOpened(), } ) ); const { closeGeneralSidebar } = useDispatch( 'core/edit-post' ); - const { setWpcomNuxStatus, setTourOpenStatus } = useDispatch( 'automattic/nux' ); + const { setShowWelcomeGuide } = useDispatch( 'automattic/wpcom-welcome-guide' ); // Hide editor sidebar first time users sees the editor useEffect( () => { - isWpcomNuxEnabled && closeGeneralSidebar(); - }, [ closeGeneralSidebar, isWpcomNuxEnabled ] ); + show && closeGeneralSidebar(); + }, [ closeGeneralSidebar, show ] ); - // Track opening of the NUX Guide + // Track opening of the welcome guide useEffect( () => { - if ( isWpcomNuxEnabled && ! isSPTOpen ) { + if ( show && ! isNewPageLayoutModalOpen ) { recordTracksEvent( 'calypso_editor_wpcom_nux_open', { is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, - is_manually_opened: isTourManuallyOpened, + is_manually_opened: isManuallyOpened, } ); } - }, [ isWpcomNuxEnabled, isSPTOpen, isTourManuallyOpened ] ); + }, [ isManuallyOpened, isNewPageLayoutModalOpen, show ] ); - if ( ! isWpcomNuxEnabled || isSPTOpen ) { + if ( ! show || isNewPageLayoutModalOpen ) { return null; } @@ -52,8 +52,7 @@ function WpcomNux() { recordTracksEvent( 'calypso_editor_wpcom_nux_dismiss', { is_gutenboarding: window.calypsoifyGutenberg?.isGutenboarding, } ); - setWpcomNuxStatus( { isNuxEnabled: false } ); - setTourOpenStatus( { isTourManuallyOpened: false } ); + setShowWelcomeGuide( false, { openedManually: false } ); }; const nuxPages = getWpcomNuxPages(); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/disable-core-nux.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/disable-core-nux.js deleted file mode 100644 index 2a04f2ee8c2b0..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/disable-core-nux.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * External dependencies - */ -import { select, dispatch, subscribe } from '@wordpress/data'; -import '@wordpress/nux'; //ensure nux store loads - -// Disable nux and welcome guide features from core. -const unsubscribe = subscribe( () => { - dispatch( 'core/nux' ).disableTips(); - if ( select( 'core/edit-post' )?.isFeatureActive( 'welcomeGuide' ) ) { - dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); - } - unsubscribe(); -} ); - -// Listen for these features being triggered to call dotcom nux instead. -// Note migration of areTipsEnabled: https://github.com/WordPress/gutenberg/blob/5c3a32dabe4393c45f7fe6ac5e4d78aebd5ee274/packages/data/src/plugins/persistence/index.js#L269 -subscribe( () => { - if ( select( 'core/nux' ).areTipsEnabled() ) { - dispatch( 'core/nux' ).disableTips(); - dispatch( 'automattic/nux' ).setWpcomNuxStatus( { isNuxEnabled: true } ); - } - if ( select( 'core/edit-post' )?.isFeatureActive( 'welcomeGuide' ) ) { - dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); - dispatch( 'automattic/nux' ).setWpcomNuxStatus( { - isNuxEnabled: true, - } ); - dispatch( 'automattic/nux' ).setTourOpenStatus( { isTourManuallyOpened: true } ); - } -} ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block-picker.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block-picker.svg deleted file mode 100644 index a7fe75f9d393e..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block-picker.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block.svg deleted file mode 100644 index ca4a8893255de..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/block.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/editor.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/editor.svg deleted file mode 100644 index b3f080ffd46fd..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/editor.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/finish.png b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/finish.png deleted file mode 100644 index 09f5f58792526..0000000000000 Binary files a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/finish.png and /dev/null differ diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/preview.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/preview.svg deleted file mode 100644 index 120e144993c70..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/preview.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/private.svg b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/private.svg deleted file mode 100644 index 6603602512258..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/images/private.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss index a0f817271afa2..574f5c5c2757d 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/style-tour.scss @@ -1,7 +1,7 @@ -@import '~@wordpress/base-styles/colors'; -@import '~@wordpress/base-styles/mixins'; -@import '~@wordpress/base-styles/variables'; -@import '~@wordpress/base-styles/z-index'; +@import '@wordpress/base-styles/colors'; +@import '@wordpress/base-styles/mixins'; +@import '@wordpress/base-styles/variables'; +@import '@wordpress/base-styles/z-index'; $welcome-tour-button-background-color: #32373c; // former $dark-gray-700. TODO: replace with standard color @@ -13,6 +13,8 @@ $welcome-tour-button-background-color: #32373c; // former $dark-gray-700. TODO: left: 16px; position: fixed; z-index: 9999; + // Avoid the text cursor when the text is not selectable + cursor: default; } .welcome-tour-card__heading { @@ -69,7 +71,7 @@ $welcome-tour-button-background-color: #32373c; // former $dark-gray-700. TODO: &.components-card { border: none; - border-radius: 4px; + border-radius: 4px; /* stylelint-disable-line */ } .components-card__body { @@ -91,7 +93,7 @@ $welcome-tour-button-background-color: #32373c; // former $dark-gray-700. TODO: .welcome-tour__end-icon.components-button.has-icon { background-color: #f6f7f7; - border-radius: 50%; + border-radius: 50%; /* stylelint-disable-line */ color: $gray-600; margin-left: 8px; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/tour-card.js b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/tour-card.js index ad35448523f1a..8a93607b40dfa 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/tour-card.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/wpcom-block-editor-nux/src/welcome-tour/tour-card.js @@ -69,7 +69,7 @@ function WelcomeTourCard( { isTertiary onClick={ () => setCurrentCardIndex( 0 ) } > - Restart tour + { __( 'Restart tour', 'full-site-editing' ) } ) : null }

@@ -91,6 +91,10 @@ function WelcomeTourCard( { } function CardNavigation( { cardIndex, lastCardIndex, onDismiss, setCurrentCardIndex } ) { + // These are defined on their own lines because of a minification issue. + // __('translations') do not always work correctly when used inside of ternary statements. + const startTourLabel = __( 'Start Tour', 'full-site-editing' ); + const nextLabel = __( 'Next', 'full-site-editing' ); return ( <> setCurrentCardIndex( cardIndex + 1 ) } > - { cardIndex === 0 - ? __( 'Start Tour', 'full-site-editing' ) - : __( 'Next', 'full-site-editing' ) } + { cardIndex === 0 ? startTourLabel : nextLabel } @@ -136,7 +138,7 @@ function CardOverlayControls( { onMinimize, onDismiss, slideNumber } ) {
); - }, -} ); + } +} + +/* eslint-enable wpcalypso/jsx-classname-namespace */ export default localize( NoteBody ); diff --git a/apps/notifications/src/panel/templates/button-edit.jsx b/apps/notifications/src/panel/templates/button-edit.jsx index 5dddf4b9f79f1..fc37c1fc05c0e 100644 --- a/apps/notifications/src/panel/templates/button-edit.jsx +++ b/apps/notifications/src/panel/templates/button-edit.jsx @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { localize } from 'i18n-calypso'; -import { get } from 'lodash'; /** * Internal dependencies @@ -16,7 +15,7 @@ import { getEditCommentLink } from '../helpers/notes'; import { editComment } from '../state/ui/actions'; const EditButton = ( { editComment, note, translate } ) => { - const { site: siteId, post: postId, comment: commentId } = get( note, 'meta.ids', {} ); + const { site: siteId, post: postId, comment: commentId } = note?.meta?.ids ?? {}; return ( - 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch ); - -const CommentReplyInput = createReactClass( { - displayName: 'CommentReplyInput', - mixins: [ repliesCache.LocalStorageMixin, Suggestions ], +function stopEvent( event ) { + event.stopPropagation(); + event.preventDefault(); +} - statics: { - stopEvent: function ( event ) { - event.stopPropagation(); - event.preventDefault(); - }, - }, - - getInitialState: function () { - const getSavedReply = function () { - const savedReply = this.localStorage.getItem( this.savedReplyKey ); +class CommentReplyInput extends React.Component { + replyInput = React.createRef(); - return savedReply ? savedReply[ 0 ] : ''; - }; + constructor( props ) { + super( props ); this.savedReplyKey = 'reply_' + this.props.note.id; - return { - value: getSavedReply.apply( this ), + const savedReply = repliesCache.getItem( this.savedReplyKey ); + const savedReplyValue = savedReply ? savedReply[ 0 ] : ''; + + this.state = { + value: savedReplyValue, hasClicked: false, isSubmitting: false, rowCount: 1, retryCount: 0, }; - }, + } - componentDidMount: function () { + componentDidMount() { window.addEventListener( 'keydown', this.handleKeyDown, false ); window.addEventListener( 'keydown', this.handleCtrlEnter, false ); - }, + } - componentWillUnmount: function () { + componentWillUnmount() { window.removeEventListener( 'keydown', this.handleKeyDown, false ); window.removeEventListener( 'keydown', this.handleCtrlEnter, false ); this.props.enableKeyboardShortcuts(); - }, + } - handleKeyDown: function ( event ) { + handleKeyDown = ( event ) => { if ( ! this.props.keyboardShortcutsAreEnabled ) { return; } @@ -74,37 +68,37 @@ const CommentReplyInput = createReactClass( { return; } - if ( 82 == event.keyCode ) { + if ( 82 === event.keyCode ) { /* 'r' key */ - this.replyInput.focus(); - CommentReplyInput.stopEvent( event ); + this.replyInput.current.focus(); + stopEvent( event ); } - }, + }; - handleCtrlEnter: function ( event ) { + handleCtrlEnter = ( event ) => { if ( this.state.isSubmitting ) { return; } - if ( ( event.ctrlKey || event.metaKey ) && ( 10 == event.keyCode || 13 == event.keyCode ) ) { - CommentReplyInput.stopEvent( event ); + if ( ( event.ctrlKey || event.metaKey ) && ( 10 === event.keyCode || 13 === event.keyCode ) ) { + stopEvent( event ); this.handleSubmit(); } - }, + }; - handleSendEnter: function ( event ) { + handleSendEnter = ( event ) => { if ( this.state.isSubmitting ) { return; } - if ( 13 == event.keyCode ) { - CommentReplyInput.stopEvent( event ); + if ( 13 === event.keyCode ) { + stopEvent( event ); this.handleSubmit(); } - }, + }; - handleChange: function ( event ) { - const textarea = this.replyInput; + handleChange = ( event ) => { + const textarea = this.replyInput.current; this.props.disableKeyboardShortcuts(); @@ -118,11 +112,11 @@ const CommentReplyInput = createReactClass( { // persist the comment reply on local storage if ( this.savedReplyKey ) { - this.localStorage.setItem( this.savedReplyKey, [ event.target.value, Date.now() ] ); + repliesCache.setItem( this.savedReplyKey, [ event.target.value, Date.now() ] ); } - }, + }; - handleClick: function ( event ) { + handleClick = () => { this.props.disableKeyboardShortcuts(); if ( ! this.state.hasClicked ) { @@ -130,30 +124,29 @@ const CommentReplyInput = createReactClass( { hasClicked: true, } ); } - }, + }; - handleFocus: function () { + handleFocus = () => { this.props.disableKeyboardShortcuts(); - }, + }; - handleBlur: function () { + handleBlur = () => { this.props.enableKeyboardShortcuts(); // Reset the field if there's no valid user input // The regex strips whitespace - if ( '' == this.state.value.replace( /^\s+|\s+$/g, '' ) ) { + if ( '' === this.state.value.replace( /^\s+|\s+$/g, '' ) ) { this.setState( { value: '', hasClicked: false, rowCount: 1, } ); } - }, + }; - handleSubmit( event ) { + handleSubmit = ( event ) => { let wpObject; let submitComment; - const component = this; let statusMessage; const successMessage = this.props.translate( 'Reply posted!' ); const linkMessage = this.props.translate( 'View your comment.' ); @@ -162,7 +155,7 @@ const CommentReplyInput = createReactClass( { event.preventDefault(); } - if ( '' == this.state.value ) return; + if ( '' === this.state.value ) return; this.props.global.toggleNavigation( false ); @@ -170,7 +163,7 @@ const CommentReplyInput = createReactClass( { isSubmitting: true, } ); - if ( this.state.retryCount == 0 ) { + if ( this.state.retryCount === 0 ) { bumpStat( 'notes-click-action', 'replyto-comment' ); recordTracksEvent( 'calypso_notification_note_reply', { note_type: this.props.note.type, @@ -204,29 +197,27 @@ const CommentReplyInput = createReactClass( { isSubmitting: false, retryCount: 0, } ); - this.replyInput.focus(); + this.replyInput.current.focus(); this.props.global.updateStatusBar( errorMessageDuplicateComment, [ 'fail' ], 6000 ); this.props.unselectNote(); - } else if ( component.state.retryCount < 3 ) { - component.setState( { - retryCount: component.state.retryCount + 1, + } else if ( this.state.retryCount < 3 ) { + this.setState( { + retryCount: this.state.retryCount + 1, } ); - window.setTimeout( function () { - component.handleSubmit(); - }, 2000 * component.state.retryCount ); + window.setTimeout( () => this.handleSubmit(), 2000 * this.state.retryCount ); } else { - component.setState( { + this.setState( { isSubmitting: false, retryCount: 0, } ); /* Flag submission failure */ - component.props.global.updateStatusBar( errorMessage, [ 'fail' ], 6000 ); + this.props.global.updateStatusBar( errorMessage, [ 'fail' ], 6000 ); - component.props.enableKeyboardShortcuts(); - component.props.global.toggleNavigation( true ); + this.props.enableKeyboardShortcuts(); + this.props.global.toggleNavigation( true ); debug( 'Failed to submit comment reply: %s', error.message ); } @@ -234,15 +225,15 @@ const CommentReplyInput = createReactClass( { return; } - if ( component.props.note.meta.ids.comment ) { + if ( this.props.note.meta.ids.comment ) { // pre-emptively approve the comment if it wasn't already - component.props.approveNote( component.props.note.id, true ); - component.props.global.client.getNote( component.props.note.id ); + this.props.approveNote( this.props.note.id, true ); + this.props.global.client.getNote( this.props.note.id ); } // remove focus from textarea so we can resume using keyboard // shortcuts without typing in the field - component.replyInput.blur(); + this.replyInput.current.blur(); if ( data.URL && validURL.test( data.URL ) ) { statusMessage = formatString( @@ -255,12 +246,12 @@ const CommentReplyInput = createReactClass( { statusMessage = successMessage; } - component.props.global.updateStatusBar( statusMessage, [ 'success' ], 12000 ); + this.props.global.updateStatusBar( statusMessage, [ 'success' ], 12000 ); - component.props.enableKeyboardShortcuts(); - component.props.global.toggleNavigation( true ); + this.props.enableKeyboardShortcuts(); + this.props.global.toggleNavigation( true ); - component.setState( { + this.setState( { value: '', isSubmitting: false, hasClicked: false, @@ -269,18 +260,38 @@ const CommentReplyInput = createReactClass( { } ); // remove the comment reply from local storage - component.localStorage.removeItem( component.savedReplyKey ); + repliesCache.removeItem( this.savedReplyKey ); // route back to list after successful comment post - component.props.selectNote( component.props.note.id ); + this.props.selectNote( this.props.note.id ); } ); - }, + }; - storeReplyInput( ref ) { - this.replyInput = ref; - }, + insertSuggestion = ( suggestion, suggestionsQuery ) => { + if ( ! suggestion ) { + return; + } + + const element = this.replyInput.current; + const caretPosition = element.selectionStart; + const startString = this.state.value.slice( + 0, + Math.max( caretPosition - suggestionsQuery.length, 0 ) + ); + const endString = this.state.value.slice( caretPosition ); + + this.setState( { + value: startString + suggestion.user_login + ' ' + endString, + } ); + + element.focus(); + + // move the caret after the inserted suggestion + const insertPosition = startString.length + suggestion.user_login.length + 1; + setTimeout( () => element.setSelectionRange( insertPosition, insertPosition ), 0 ); + }; - render: function () { + render() { const value = this.state.value; let submitLink = ''; const sendText = this.props.translate( 'Send', { context: 'verb: imperative' } ); @@ -314,7 +325,7 @@ const CommentReplyInput = createReactClass( {